github.com/argoproj/argo-cd/v3@v3.2.1/util/lua/lua.go (about)

     1  package lua
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"reflect"
    13  	"slices"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/argoproj/gitops-engine/pkg/health"
    19  	glob "github.com/bmatcuk/doublestar/v4"
    20  	lua "github.com/yuin/gopher-lua"
    21  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    22  	"k8s.io/apimachinery/pkg/runtime/schema"
    23  	luajson "layeh.com/gopher-json"
    24  
    25  	applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
    26  	appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    27  	"github.com/argoproj/argo-cd/v3/resource_customizations"
    28  	argoglob "github.com/argoproj/argo-cd/v3/util/glob"
    29  )
    30  
    31  const (
    32  	incorrectReturnType       = "expect %s output from Lua script, not %s"
    33  	invalidHealthStatus       = "Lua returned an invalid health status"
    34  	healthScriptFile          = "health.lua"
    35  	actionScriptFile          = "action.lua"
    36  	actionDiscoveryScriptFile = "discovery.lua"
    37  )
    38  
    39  // errScriptDoesNotExist is an error type for when a built-in script does not exist.
    40  var errScriptDoesNotExist = errors.New("built-in script does not exist")
    41  
    42  type ResourceHealthOverrides map[string]appv1.ResourceOverride
    43  
    44  func (overrides ResourceHealthOverrides) GetResourceHealth(obj *unstructured.Unstructured) (*health.HealthStatus, error) {
    45  	luaVM := VM{
    46  		ResourceOverrides: overrides,
    47  	}
    48  	script, useOpenLibs, err := luaVM.GetHealthScript(obj)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	if script == "" {
    53  		return nil, nil
    54  	}
    55  	// enable/disable the usage of lua standard library
    56  	luaVM.UseOpenLibs = useOpenLibs
    57  	result, err := luaVM.ExecuteHealthLua(obj, script)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	return result, nil
    62  }
    63  
    64  // VM Defines a struct that implements the luaVM
    65  type VM struct {
    66  	ResourceOverrides map[string]appv1.ResourceOverride
    67  	// UseOpenLibs flag to enable open libraries. Libraries are disabled by default while running, but enabled during testing to allow the use of print statements
    68  	UseOpenLibs bool
    69  }
    70  
    71  func (vm VM) runLua(obj *unstructured.Unstructured, script string) (*lua.LState, error) {
    72  	return vm.runLuaWithResourceActionParameters(obj, script, nil)
    73  }
    74  
    75  func (vm VM) runLuaWithResourceActionParameters(obj *unstructured.Unstructured, script string, resourceActionParameters []*applicationpkg.ResourceActionParameters) (*lua.LState, error) {
    76  	l := lua.NewState(lua.Options{
    77  		SkipOpenLibs: !vm.UseOpenLibs,
    78  	})
    79  	defer l.Close()
    80  	// Opens table library to allow access to functions to manipulate tables
    81  	for _, pair := range []struct {
    82  		n string
    83  		f lua.LGFunction
    84  	}{
    85  		{lua.LoadLibName, lua.OpenPackage},
    86  		{lua.BaseLibName, lua.OpenBase},
    87  		{lua.TabLibName, lua.OpenTable},
    88  		// load our 'safe' version of the OS library
    89  		{lua.OsLibName, OpenSafeOs},
    90  	} {
    91  		if err := l.CallByParam(lua.P{
    92  			Fn:      l.NewFunction(pair.f),
    93  			NRet:    0,
    94  			Protect: true,
    95  		}, lua.LString(pair.n)); err != nil {
    96  			panic(err)
    97  		}
    98  	}
    99  	// preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work
   100  	l.PreloadModule(lua.OsLibName, SafeOsLoader)
   101  
   102  	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
   103  	defer cancel()
   104  	l.SetContext(ctx)
   105  
   106  	// Inject action parameters as a hash table global variable
   107  	actionParams := l.CreateTable(0, len(resourceActionParameters))
   108  	for _, resourceActionParameter := range resourceActionParameters {
   109  		value := decodeValue(l, resourceActionParameter.GetValue())
   110  		actionParams.RawSetH(lua.LString(resourceActionParameter.GetName()), value)
   111  	}
   112  	l.SetGlobal("actionParams", actionParams) // Set the actionParams table as a global variable
   113  
   114  	objectValue := decodeValue(l, obj.Object)
   115  	l.SetGlobal("obj", objectValue)
   116  	err := l.DoString(script)
   117  
   118  	// Remove the default lua stack trace from execution errors since these
   119  	// errors will make it back to the user
   120  	var apiErr *lua.ApiError
   121  	if errors.As(err, &apiErr) {
   122  		if apiErr.Type == lua.ApiErrorRun {
   123  			apiErr.StackTrace = ""
   124  			err = apiErr
   125  		}
   126  	}
   127  
   128  	return l, err
   129  }
   130  
   131  // ExecuteHealthLua runs the lua script to generate the health status of a resource
   132  func (vm VM) ExecuteHealthLua(obj *unstructured.Unstructured, script string) (*health.HealthStatus, error) {
   133  	l, err := vm.runLua(obj, script)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	returnValue := l.Get(-1)
   138  	if returnValue.Type() == lua.LTTable {
   139  		jsonBytes, err := luajson.Encode(returnValue)
   140  		if err != nil {
   141  			return nil, err
   142  		}
   143  		healthStatus := &health.HealthStatus{}
   144  		err = json.Unmarshal(jsonBytes, healthStatus)
   145  		if err != nil {
   146  			// Validate if the error is caused by an empty object
   147  			typeError := &json.UnmarshalTypeError{Value: "array", Type: reflect.TypeOf(healthStatus)}
   148  			if errors.As(err, &typeError) {
   149  				return &health.HealthStatus{}, nil
   150  			}
   151  			return nil, err
   152  		}
   153  		if !isValidHealthStatusCode(healthStatus.Status) {
   154  			return &health.HealthStatus{
   155  				Status:  health.HealthStatusUnknown,
   156  				Message: invalidHealthStatus,
   157  			}, nil
   158  		}
   159  
   160  		return healthStatus, nil
   161  	} else if returnValue.Type() == lua.LTNil {
   162  		return &health.HealthStatus{}, nil
   163  	}
   164  	return nil, fmt.Errorf(incorrectReturnType, "table", returnValue.Type().String())
   165  }
   166  
   167  // GetHealthScript attempts to read lua script from config and then filesystem for that resource. If none exists, return
   168  // an empty string.
   169  func (vm VM) GetHealthScript(obj *unstructured.Unstructured) (script string, useOpenLibs bool, err error) {
   170  	// first, search the gvk as is in the ResourceOverrides
   171  	key := GetConfigMapKey(obj.GroupVersionKind())
   172  
   173  	if script, ok := vm.ResourceOverrides[key]; ok && script.HealthLua != "" {
   174  		return script.HealthLua, script.UseOpenLibs, nil
   175  	}
   176  
   177  	// if not found as is, perhaps it matches a wildcard entry in the configmap
   178  	getWildcardHealthOverride, useOpenLibs := getWildcardHealthOverrideLua(vm.ResourceOverrides, obj.GroupVersionKind())
   179  
   180  	if getWildcardHealthOverride != "" {
   181  		return getWildcardHealthOverride, useOpenLibs, nil
   182  	}
   183  
   184  	// if not found in the ResourceOverrides at all, search it as is in the built-in scripts
   185  	// (as built-in scripts are files in folders, named after the GVK, currently there is no wildcard support for them)
   186  	builtInScript, err := vm.getPredefinedLuaScripts(key, healthScriptFile)
   187  	if err != nil {
   188  		if errors.Is(err, errScriptDoesNotExist) {
   189  			// Try to find a wildcard built-in health script
   190  			builtInScript, err = getWildcardBuiltInHealthOverrideLua(key)
   191  			if err != nil {
   192  				return "", false, fmt.Errorf("error while fetching built-in health script: %w", err)
   193  			}
   194  			if builtInScript != "" {
   195  				return builtInScript, true, nil
   196  			}
   197  
   198  			// It's okay if no built-in health script exists. Just return an empty string and let the caller handle it.
   199  			return "", false, nil
   200  		}
   201  		return "", false, err
   202  	}
   203  	// standard libraries will be enabled for all built-in scripts
   204  	return builtInScript, true, err
   205  }
   206  
   207  func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string, resourceActionParameters []*applicationpkg.ResourceActionParameters) ([]ImpactedResource, error) {
   208  	l, err := vm.runLuaWithResourceActionParameters(obj, script, resourceActionParameters)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	returnValue := l.Get(-1)
   213  	if returnValue.Type() == lua.LTTable {
   214  		jsonBytes, err := luajson.Encode(returnValue)
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  
   219  		var impactedResources []ImpactedResource
   220  
   221  		jsonString := bytes.NewBuffer(jsonBytes).String()
   222  		// nolint:staticcheck // Lua is fine to be capitalized.
   223  		if len(jsonString) < 2 {
   224  			return nil, errors.New("Lua output was not a valid json object or array")
   225  		}
   226  		// The output from Lua is either an object (old-style action output) or an array (new-style action output).
   227  		// Check whether the string starts with an opening square bracket and ends with a closing square bracket,
   228  		// avoiding programming by exception.
   229  		if jsonString[0] == '[' && jsonString[len(jsonString)-1] == ']' {
   230  			// The string represents a new-style action array output
   231  			impactedResources, err = UnmarshalToImpactedResources(string(jsonBytes))
   232  			if err != nil {
   233  				return nil, err
   234  			}
   235  		} else {
   236  			// The string represents an old-style action object output
   237  			newObj, err := appv1.UnmarshalToUnstructured(string(jsonBytes))
   238  			if err != nil {
   239  				return nil, err
   240  			}
   241  			// Wrap the old-style action output with a single-member array.
   242  			// The default definition of the old-style action is a "patch" one.
   243  			impactedResources = append(impactedResources, ImpactedResource{newObj, PatchOperation})
   244  		}
   245  
   246  		for _, impactedResource := range impactedResources {
   247  			// Cleaning the resource is only relevant to "patch"
   248  			if impactedResource.K8SOperation == PatchOperation {
   249  				impactedResource.UnstructuredObj.Object = cleanReturnedObj(impactedResource.UnstructuredObj.Object, obj.Object)
   250  			}
   251  		}
   252  		return impactedResources, nil
   253  	}
   254  	return nil, fmt.Errorf(incorrectReturnType, "table", returnValue.Type().String())
   255  }
   256  
   257  // UnmarshalToImpactedResources unmarshals an ImpactedResource array representation in JSON to ImpactedResource array
   258  func UnmarshalToImpactedResources(resources string) ([]ImpactedResource, error) {
   259  	if resources == "" || resources == "null" {
   260  		return nil, nil
   261  	}
   262  
   263  	var impactedResources []ImpactedResource
   264  	err := json.Unmarshal([]byte(resources), &impactedResources)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	return impactedResources, nil
   269  }
   270  
   271  // cleanReturnedObj Lua cannot distinguish an empty table as an array or map, and the library we are using choose to
   272  // decoded an empty table into an empty array. This function prevents the lua scripts from unintentionally changing an
   273  // empty struct into empty arrays
   274  func cleanReturnedObj(newObj, obj map[string]any) map[string]any {
   275  	mapToReturn := newObj
   276  	for key := range obj {
   277  		if newValueInterface, ok := newObj[key]; ok {
   278  			oldValueInterface, ok := obj[key]
   279  			if !ok {
   280  				continue
   281  			}
   282  			switch newValue := newValueInterface.(type) {
   283  			case map[string]any:
   284  				if oldValue, ok := oldValueInterface.(map[string]any); ok {
   285  					convertedMap := cleanReturnedObj(newValue, oldValue)
   286  					mapToReturn[key] = convertedMap
   287  				}
   288  
   289  			case []any:
   290  				switch oldValue := oldValueInterface.(type) {
   291  				case map[string]any:
   292  					if len(newValue) == 0 {
   293  						// Lua incorrectly decoded the empty object as an empty array, so set it to an empty object
   294  						mapToReturn[key] = map[string]any{}
   295  					}
   296  				case []any:
   297  					newArray := cleanReturnedArray(newValue, oldValue)
   298  					mapToReturn[key] = newArray
   299  				}
   300  			}
   301  		}
   302  	}
   303  	return mapToReturn
   304  }
   305  
   306  // cleanReturnedArray allows Argo CD to recurse into nested arrays when checking for unintentional empty struct to
   307  // empty array conversions.
   308  func cleanReturnedArray(newObj, obj []any) []any {
   309  	arrayToReturn := newObj
   310  	for i := range newObj {
   311  		if i >= len(obj) {
   312  			// If the new object is longer than the old one, we added an item to the array
   313  			break
   314  		}
   315  		switch newValue := newObj[i].(type) {
   316  		case map[string]any:
   317  			if oldValue, ok := obj[i].(map[string]any); ok {
   318  				convertedMap := cleanReturnedObj(newValue, oldValue)
   319  				arrayToReturn[i] = convertedMap
   320  			}
   321  		case []any:
   322  			if oldValue, ok := obj[i].([]any); ok {
   323  				convertedMap := cleanReturnedArray(newValue, oldValue)
   324  				arrayToReturn[i] = convertedMap
   325  			}
   326  		}
   327  	}
   328  	return arrayToReturn
   329  }
   330  
   331  func (vm VM) ExecuteResourceActionDiscovery(obj *unstructured.Unstructured, scripts []string) ([]appv1.ResourceAction, error) {
   332  	if len(scripts) == 0 {
   333  		return nil, errors.New("no action discovery script provided")
   334  	}
   335  	availableActionsMap := make(map[string]appv1.ResourceAction)
   336  
   337  	for _, script := range scripts {
   338  		l, err := vm.runLua(obj, script)
   339  		if err != nil {
   340  			return nil, err
   341  		}
   342  		returnValue := l.Get(-1)
   343  		if returnValue.Type() != lua.LTTable {
   344  			return nil, fmt.Errorf(incorrectReturnType, "table", returnValue.Type().String())
   345  		}
   346  		jsonBytes, err := luajson.Encode(returnValue)
   347  		if err != nil {
   348  			return nil, fmt.Errorf("error in converting to lua table: %w", err)
   349  		}
   350  		if noAvailableActions(jsonBytes) {
   351  			continue
   352  		}
   353  		actionsMap := make(map[string]any)
   354  		err = json.Unmarshal(jsonBytes, &actionsMap)
   355  		if err != nil {
   356  			return nil, fmt.Errorf("error unmarshaling action table: %w", err)
   357  		}
   358  		for key, value := range actionsMap {
   359  			resourceAction := appv1.ResourceAction{Name: key, Disabled: isActionDisabled(value)}
   360  			if _, exist := availableActionsMap[key]; exist {
   361  				continue
   362  			}
   363  			if emptyResourceActionFromLua(value) {
   364  				availableActionsMap[key] = resourceAction
   365  				continue
   366  			}
   367  			resourceActionBytes, err := json.Marshal(value)
   368  			if err != nil {
   369  				return nil, fmt.Errorf("error marshaling resource action: %w", err)
   370  			}
   371  
   372  			err = json.Unmarshal(resourceActionBytes, &resourceAction)
   373  			if err != nil {
   374  				return nil, fmt.Errorf("error unmarshaling resource action: %w", err)
   375  			}
   376  			availableActionsMap[key] = resourceAction
   377  		}
   378  	}
   379  
   380  	availableActions := make([]appv1.ResourceAction, 0, len(availableActionsMap))
   381  	for _, action := range availableActionsMap {
   382  		availableActions = append(availableActions, action)
   383  	}
   384  
   385  	return availableActions, nil
   386  }
   387  
   388  // Actions are enabled by default
   389  func isActionDisabled(actionsMap any) bool {
   390  	actions, ok := actionsMap.(map[string]any)
   391  	if !ok {
   392  		return false
   393  	}
   394  	for key, val := range actions {
   395  		if vv, ok := val.(bool); ok {
   396  			if key == "disabled" {
   397  				return vv
   398  			}
   399  		}
   400  	}
   401  	return false
   402  }
   403  
   404  func emptyResourceActionFromLua(i any) bool {
   405  	_, ok := i.([]any)
   406  	return ok
   407  }
   408  
   409  func noAvailableActions(jsonBytes []byte) bool {
   410  	// When the Lua script returns an empty table, it is decoded as a empty array.
   411  	return string(jsonBytes) == "[]"
   412  }
   413  
   414  func (vm VM) GetResourceActionDiscovery(obj *unstructured.Unstructured) ([]string, error) {
   415  	key := GetConfigMapKey(obj.GroupVersionKind())
   416  	var discoveryScripts []string
   417  
   418  	// Check if there are resource overrides for the given key
   419  	override, ok := vm.ResourceOverrides[key]
   420  	if ok && override.Actions != "" {
   421  		actions, err := override.GetActions()
   422  		if err != nil {
   423  			return nil, err
   424  		}
   425  		// Append the action discovery Lua script if built-in actions are to be included
   426  		if !actions.MergeBuiltinActions {
   427  			return []string{actions.ActionDiscoveryLua}, nil
   428  		}
   429  		discoveryScripts = append(discoveryScripts, actions.ActionDiscoveryLua)
   430  	}
   431  
   432  	// Fetch predefined Lua scripts
   433  	discoveryKey := key + "/actions/"
   434  	discoveryScript, err := vm.getPredefinedLuaScripts(discoveryKey, actionDiscoveryScriptFile)
   435  	if err != nil {
   436  		if errors.Is(err, errScriptDoesNotExist) {
   437  			// No worries, just return what we have.
   438  			return discoveryScripts, nil
   439  		}
   440  		return nil, fmt.Errorf("error while fetching predefined lua scripts: %w", err)
   441  	}
   442  
   443  	discoveryScripts = append(discoveryScripts, discoveryScript)
   444  
   445  	return discoveryScripts, nil
   446  }
   447  
   448  // GetResourceAction attempts to read lua script from config and then filesystem for that resource
   449  func (vm VM) GetResourceAction(obj *unstructured.Unstructured, actionName string) (appv1.ResourceActionDefinition, error) {
   450  	key := GetConfigMapKey(obj.GroupVersionKind())
   451  	override, ok := vm.ResourceOverrides[key]
   452  	if ok && override.Actions != "" {
   453  		actions, err := override.GetActions()
   454  		if err != nil {
   455  			return appv1.ResourceActionDefinition{}, err
   456  		}
   457  		for _, action := range actions.Definitions {
   458  			if action.Name == actionName {
   459  				return action, nil
   460  			}
   461  		}
   462  	}
   463  
   464  	actionKey := fmt.Sprintf("%s/actions/%s", key, actionName)
   465  	actionScript, err := vm.getPredefinedLuaScripts(actionKey, actionScriptFile)
   466  	if err != nil {
   467  		return appv1.ResourceActionDefinition{}, err
   468  	}
   469  
   470  	return appv1.ResourceActionDefinition{
   471  		Name:      actionName,
   472  		ActionLua: actionScript,
   473  	}, nil
   474  }
   475  
   476  func GetConfigMapKey(gvk schema.GroupVersionKind) string {
   477  	if gvk.Group == "" {
   478  		return gvk.Kind
   479  	}
   480  	return fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
   481  }
   482  
   483  // getWildcardHealthOverrideLua returns the first encountered resource override which matches the wildcard and has a
   484  // non-empty health script. Having multiple wildcards with non-empty health checks that can match the GVK is
   485  // non-deterministic.
   486  func getWildcardHealthOverrideLua(overrides map[string]appv1.ResourceOverride, gvk schema.GroupVersionKind) (string, bool) {
   487  	gvkKeyToMatch := GetConfigMapKey(gvk)
   488  
   489  	for key, override := range overrides {
   490  		if argoglob.Match(key, gvkKeyToMatch) && override.HealthLua != "" {
   491  			return override.HealthLua, override.UseOpenLibs
   492  		}
   493  	}
   494  	return "", false
   495  }
   496  
   497  func (vm VM) getPredefinedLuaScripts(objKey string, scriptFile string) (string, error) {
   498  	data, err := resource_customizations.Embedded.ReadFile(filepath.Join(objKey, scriptFile))
   499  	if err != nil {
   500  		if os.IsNotExist(err) {
   501  			return "", errScriptDoesNotExist
   502  		}
   503  		return "", err
   504  	}
   505  	return string(data), nil
   506  }
   507  
   508  // globHealthScriptPathsOnce is a sync.Once instance to ensure that the globHealthScriptPaths are only initialized once.
   509  // The globs come from an embedded filesystem, so it won't change at runtime.
   510  var globHealthScriptPathsOnce sync.Once
   511  
   512  // globHealthScriptPaths is a cache for the glob patterns of directories containing health.lua files. Don't use this
   513  // directly, use getGlobHealthScriptPaths() instead.
   514  var globHealthScriptPaths []string
   515  
   516  // getGlobHealthScriptPaths returns the paths of the directories containing health.lua files where the path contains a
   517  // glob pattern. It uses a sync.Once to ensure that the paths are only initialized once.
   518  func getGlobHealthScriptPaths() ([]string, error) {
   519  	var err error
   520  	globHealthScriptPathsOnce.Do(func() {
   521  		// Walk through the embedded filesystem and get the directory names of all directories containing a health.lua.
   522  		var patterns []string
   523  		err = fs.WalkDir(resource_customizations.Embedded, ".", func(path string, d fs.DirEntry, err error) error {
   524  			if err != nil {
   525  				return fmt.Errorf("error walking path %q: %w", path, err)
   526  			}
   527  
   528  			// Skip non-directories at the top level
   529  			if d.IsDir() && filepath.Dir(path) == "." {
   530  				return nil
   531  			}
   532  
   533  			// Check if the directory contains a health.lua file
   534  			if filepath.Base(path) != healthScriptFile {
   535  				return nil
   536  			}
   537  
   538  			groupKindPath := filepath.Dir(path)
   539  			// Check if the path contains a wildcard. If it doesn't, skip it.
   540  			if !strings.Contains(groupKindPath, "_") {
   541  				return nil
   542  			}
   543  
   544  			pattern := strings.ReplaceAll(groupKindPath, "_", "*")
   545  			// Check that the pattern is valid.
   546  			if !glob.ValidatePattern(pattern) {
   547  				return fmt.Errorf("invalid glob pattern %q: %w", pattern, err)
   548  			}
   549  
   550  			patterns = append(patterns, groupKindPath)
   551  			return nil
   552  		})
   553  		if err != nil {
   554  			return
   555  		}
   556  
   557  		// Sort the patterns to ensure deterministic choice of wildcard directory for a given GK.
   558  		slices.Sort(patterns)
   559  
   560  		globHealthScriptPaths = patterns
   561  	})
   562  	if err != nil {
   563  		return nil, fmt.Errorf("error getting health script glob directories: %w", err)
   564  	}
   565  	return globHealthScriptPaths, nil
   566  }
   567  
   568  func getWildcardBuiltInHealthOverrideLua(objKey string) (string, error) {
   569  	// Check if the GVK matches any of the wildcard directories
   570  	globs, err := getGlobHealthScriptPaths()
   571  	if err != nil {
   572  		return "", fmt.Errorf("error getting health script globs: %w", err)
   573  	}
   574  	for _, g := range globs {
   575  		pattern := strings.ReplaceAll(g, "_", "*")
   576  		if !glob.PathMatchUnvalidated(pattern, objKey) {
   577  			continue
   578  		}
   579  
   580  		var script []byte
   581  		script, err = resource_customizations.Embedded.ReadFile(filepath.Join(g, healthScriptFile))
   582  		if err != nil {
   583  			return "", fmt.Errorf("error reading %q file in embedded filesystem: %w", filepath.Join(objKey, healthScriptFile), err)
   584  		}
   585  		return string(script), nil
   586  	}
   587  	return "", nil
   588  }
   589  
   590  func isValidHealthStatusCode(statusCode health.HealthStatusCode) bool {
   591  	switch statusCode {
   592  	case health.HealthStatusUnknown, health.HealthStatusProgressing, health.HealthStatusSuspended, health.HealthStatusHealthy, health.HealthStatusDegraded, health.HealthStatusMissing:
   593  		return true
   594  	}
   595  	return false
   596  }
   597  
   598  // Took logic from the link below and added the int, int32, and int64 types since the value would have type int64
   599  // while actually running in the controller and it was not reproducible through testing.
   600  // https://github.com/layeh/gopher-json/blob/97fed8db84274c421dbfffbb28ec859901556b97/json.go#L154
   601  func decodeValue(l *lua.LState, value any) lua.LValue {
   602  	switch converted := value.(type) {
   603  	case bool:
   604  		return lua.LBool(converted)
   605  	case float64:
   606  		return lua.LNumber(converted)
   607  	case string:
   608  		return lua.LString(converted)
   609  	case json.Number:
   610  		return lua.LString(converted)
   611  	case int:
   612  		return lua.LNumber(converted)
   613  	case int32:
   614  		return lua.LNumber(converted)
   615  	case int64:
   616  		return lua.LNumber(converted)
   617  	case []any:
   618  		arr := l.CreateTable(len(converted), 0)
   619  		for _, item := range converted {
   620  			arr.Append(decodeValue(l, item))
   621  		}
   622  		return arr
   623  	case map[string]any:
   624  		tbl := l.CreateTable(0, len(converted))
   625  		for key, item := range converted {
   626  			tbl.RawSetH(lua.LString(key), decodeValue(l, item))
   627  		}
   628  		return tbl
   629  	case nil:
   630  		return lua.LNil
   631  	}
   632  
   633  	return lua.LNil
   634  }