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

     1  package lua
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/argoproj/gitops-engine/pkg/health"
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  	lua "github.com/yuin/gopher-lua"
    12  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    13  	"sigs.k8s.io/yaml"
    14  
    15  	applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
    16  	appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    17  	"github.com/argoproj/argo-cd/v3/util/grpc"
    18  )
    19  
    20  const objJSON = `
    21  apiVersion: argoproj.io/v1alpha1
    22  kind: Rollout
    23  metadata:
    24    labels:
    25      app.kubernetes.io/instance: helm-guestbook
    26    name: helm-guestbook
    27    namespace: default
    28    resourceVersion: "123"
    29  `
    30  
    31  const objWithNoScriptJSON = `
    32  apiVersion: not-an-endpoint.io/v1alpha1
    33  kind: Test
    34  metadata:
    35    labels:
    36      app.kubernetes.io/instance: helm-guestbook
    37    name: helm-guestbook
    38    namespace: default
    39    resourceVersion: "123"
    40  `
    41  
    42  const ec2AWSCrossplaneObjJSON = `
    43  apiVersion: ec2.aws.crossplane.io/v1alpha1
    44  kind: Instance
    45  metadata:
    46    name: sample-crosspalne-ec2-instance
    47  spec:
    48    forProvider:
    49      region: us-west-2
    50      instanceType: t2.micro
    51      keyName: my-crossplane-key-pair
    52    providerConfigRef:
    53      name: awsconfig
    54  `
    55  
    56  const newHealthStatusFunction = `a = {}
    57  a.status = "Healthy"
    58  a.message ="NeedsToBeChanged"
    59  if obj.metadata.name == "helm-guestbook" then
    60  	a.message = "testMessage"
    61  end
    62  return a`
    63  
    64  const newWildcardHealthStatusFunction = `a = {}
    65  a.status = "Healthy"
    66  a.message ="NeedsToBeChanged"
    67  if obj.metadata.name == "sample-crosspalne-ec2-instance" then
    68  	a.message = "testWildcardMessage"
    69  end
    70  return a`
    71  
    72  func StrToUnstructured(jsonStr string) *unstructured.Unstructured {
    73  	obj := make(map[string]any)
    74  	err := yaml.Unmarshal([]byte(jsonStr), &obj)
    75  	if err != nil {
    76  		panic(err)
    77  	}
    78  	return &unstructured.Unstructured{Object: obj}
    79  }
    80  
    81  func TestExecuteNewHealthStatusFunction(t *testing.T) {
    82  	testObj := StrToUnstructured(objJSON)
    83  	vm := VM{}
    84  	status, err := vm.ExecuteHealthLua(testObj, newHealthStatusFunction)
    85  	require.NoError(t, err)
    86  	expectedHealthStatus := &health.HealthStatus{
    87  		Status:  "Healthy",
    88  		Message: "testMessage",
    89  	}
    90  	assert.Equal(t, expectedHealthStatus, status)
    91  }
    92  
    93  func TestExecuteWildcardHealthStatusFunction(t *testing.T) {
    94  	testObj := StrToUnstructured(ec2AWSCrossplaneObjJSON)
    95  	vm := VM{}
    96  	status, err := vm.ExecuteHealthLua(testObj, newWildcardHealthStatusFunction)
    97  	require.NoError(t, err)
    98  	expectedHealthStatus := &health.HealthStatus{
    99  		Status:  "Healthy",
   100  		Message: "testWildcardMessage",
   101  	}
   102  	assert.Equal(t, expectedHealthStatus, status)
   103  }
   104  
   105  const osLuaScript = `os.getenv("HOME")`
   106  
   107  func TestFailExternalLibCall(t *testing.T) {
   108  	testObj := StrToUnstructured(objJSON)
   109  	vm := VM{}
   110  	_, err := vm.ExecuteHealthLua(testObj, osLuaScript)
   111  	require.Error(t, err)
   112  	assert.IsType(t, &lua.ApiError{}, err)
   113  }
   114  
   115  const returnInt = `return 1`
   116  
   117  func TestFailLuaReturnNonTable(t *testing.T) {
   118  	testObj := StrToUnstructured(objJSON)
   119  	vm := VM{}
   120  	_, err := vm.ExecuteHealthLua(testObj, returnInt)
   121  	assert.Equal(t, fmt.Errorf(incorrectReturnType, "table", "number"), err)
   122  }
   123  
   124  const invalidHealthStatusStatus = `local healthStatus = {}
   125  healthStatus.status = "test"
   126  return healthStatus
   127  `
   128  
   129  func TestInvalidHealthStatusStatus(t *testing.T) {
   130  	testObj := StrToUnstructured(objJSON)
   131  	vm := VM{}
   132  	status, err := vm.ExecuteHealthLua(testObj, invalidHealthStatusStatus)
   133  	require.NoError(t, err)
   134  	expectedStatus := &health.HealthStatus{
   135  		Status:  health.HealthStatusUnknown,
   136  		Message: invalidHealthStatus,
   137  	}
   138  	assert.Equal(t, expectedStatus, status)
   139  }
   140  
   141  const validReturnNothingHealthStatusStatus = `local healthStatus = {}
   142  return
   143  `
   144  
   145  func TestNoReturnHealthStatusStatus(t *testing.T) {
   146  	testObj := StrToUnstructured(objJSON)
   147  	vm := VM{}
   148  	status, err := vm.ExecuteHealthLua(testObj, validReturnNothingHealthStatusStatus)
   149  	require.NoError(t, err)
   150  	expectedStatus := &health.HealthStatus{}
   151  	assert.Equal(t, expectedStatus, status)
   152  }
   153  
   154  const validNilHealthStatusStatus = `local healthStatus = {}
   155  return nil
   156  `
   157  
   158  func TestNilHealthStatusStatus(t *testing.T) {
   159  	testObj := StrToUnstructured(objJSON)
   160  	vm := VM{}
   161  	status, err := vm.ExecuteHealthLua(testObj, validNilHealthStatusStatus)
   162  	require.NoError(t, err)
   163  	expectedStatus := &health.HealthStatus{}
   164  	assert.Equal(t, expectedStatus, status)
   165  }
   166  
   167  const validEmptyArrayHealthStatusStatus = `local healthStatus = {}
   168  return healthStatus
   169  `
   170  
   171  func TestEmptyHealthStatusStatus(t *testing.T) {
   172  	testObj := StrToUnstructured(objJSON)
   173  	vm := VM{}
   174  	status, err := vm.ExecuteHealthLua(testObj, validEmptyArrayHealthStatusStatus)
   175  	require.NoError(t, err)
   176  	expectedStatus := &health.HealthStatus{}
   177  	assert.Equal(t, expectedStatus, status)
   178  }
   179  
   180  const infiniteLoop = `while true do ; end`
   181  
   182  func TestHandleInfiniteLoop(t *testing.T) {
   183  	testObj := StrToUnstructured(objJSON)
   184  	vm := VM{}
   185  	_, err := vm.ExecuteHealthLua(testObj, infiniteLoop)
   186  	assert.IsType(t, &lua.ApiError{}, err)
   187  }
   188  
   189  func TestGetHealthScriptWithOverride(t *testing.T) {
   190  	testObj := StrToUnstructured(objJSON)
   191  	vm := VM{
   192  		ResourceOverrides: map[string]appv1.ResourceOverride{
   193  			"argoproj.io/Rollout": {
   194  				HealthLua:   newHealthStatusFunction,
   195  				UseOpenLibs: false,
   196  			},
   197  		},
   198  	}
   199  	script, useOpenLibs, err := vm.GetHealthScript(testObj)
   200  	require.NoError(t, err)
   201  	assert.False(t, useOpenLibs)
   202  	assert.Equal(t, newHealthStatusFunction, script)
   203  }
   204  
   205  func TestGetHealthScriptWithKindWildcardOverride(t *testing.T) {
   206  	testObj := StrToUnstructured(objJSON)
   207  	vm := VM{
   208  		ResourceOverrides: map[string]appv1.ResourceOverride{
   209  			"argoproj.io/*": {
   210  				HealthLua:   newHealthStatusFunction,
   211  				UseOpenLibs: false,
   212  			},
   213  		},
   214  	}
   215  
   216  	script, useOpenLibs, err := vm.GetHealthScript(testObj)
   217  	require.NoError(t, err)
   218  	assert.False(t, useOpenLibs)
   219  	assert.Equal(t, newHealthStatusFunction, script)
   220  }
   221  
   222  func TestGetHealthScriptWithGroupWildcardOverride(t *testing.T) {
   223  	testObj := StrToUnstructured(objJSON)
   224  	vm := VM{
   225  		ResourceOverrides: map[string]appv1.ResourceOverride{
   226  			"*.io/Rollout": {
   227  				HealthLua:   newHealthStatusFunction,
   228  				UseOpenLibs: false,
   229  			},
   230  		},
   231  	}
   232  
   233  	script, useOpenLibs, err := vm.GetHealthScript(testObj)
   234  	require.NoError(t, err)
   235  	assert.False(t, useOpenLibs)
   236  	assert.Equal(t, newHealthStatusFunction, script)
   237  }
   238  
   239  func TestGetHealthScriptWithGroupAndKindWildcardOverride(t *testing.T) {
   240  	testObj := StrToUnstructured(ec2AWSCrossplaneObjJSON)
   241  	vm := VM{
   242  		ResourceOverrides: map[string]appv1.ResourceOverride{
   243  			"*.aws.crossplane.io/*": {
   244  				HealthLua:   newHealthStatusFunction,
   245  				UseOpenLibs: false,
   246  			},
   247  		},
   248  	}
   249  
   250  	script, useOpenLibs, err := vm.GetHealthScript(testObj)
   251  	require.NoError(t, err)
   252  	assert.False(t, useOpenLibs)
   253  	assert.Equal(t, newHealthStatusFunction, script)
   254  }
   255  
   256  func TestGetHealthScriptPredefined(t *testing.T) {
   257  	testObj := StrToUnstructured(objJSON)
   258  	vm := VM{}
   259  	script, useOpenLibs, err := vm.GetHealthScript(testObj)
   260  	require.NoError(t, err)
   261  	assert.True(t, useOpenLibs)
   262  	assert.NotEmpty(t, script)
   263  }
   264  
   265  func TestGetHealthScriptNoPredefined(t *testing.T) {
   266  	testObj := StrToUnstructured(objWithNoScriptJSON)
   267  	vm := VM{}
   268  	script, useOpenLibs, err := vm.GetHealthScript(testObj)
   269  	require.NoError(t, err)
   270  	assert.False(t, useOpenLibs)
   271  	assert.Empty(t, script)
   272  }
   273  
   274  func TestGetResourceActionPredefined(t *testing.T) {
   275  	testObj := StrToUnstructured(objJSON)
   276  	vm := VM{}
   277  
   278  	action, err := vm.GetResourceAction(testObj, "resume")
   279  	require.NoError(t, err)
   280  	assert.NotEmpty(t, action)
   281  }
   282  
   283  func TestGetResourceActionNoPredefined(t *testing.T) {
   284  	testObj := StrToUnstructured(objWithNoScriptJSON)
   285  	vm := VM{}
   286  	action, err := vm.GetResourceAction(testObj, "test")
   287  	require.ErrorIs(t, err, errScriptDoesNotExist)
   288  	assert.Empty(t, action.ActionLua)
   289  }
   290  
   291  func TestGetResourceActionWithOverride(t *testing.T) {
   292  	testObj := StrToUnstructured(objJSON)
   293  	test := appv1.ResourceActionDefinition{
   294  		Name:      "test",
   295  		ActionLua: "return obj",
   296  	}
   297  
   298  	vm := VM{
   299  		ResourceOverrides: map[string]appv1.ResourceOverride{
   300  			"argoproj.io/Rollout": {
   301  				Actions: string(grpc.MustMarshal(appv1.ResourceActions{
   302  					Definitions: []appv1.ResourceActionDefinition{
   303  						test,
   304  					},
   305  				})),
   306  			},
   307  		},
   308  	}
   309  	action, err := vm.GetResourceAction(testObj, "test")
   310  	require.NoError(t, err)
   311  	assert.Equal(t, test, action)
   312  }
   313  
   314  func TestGetResourceActionDiscoveryPredefined(t *testing.T) {
   315  	testObj := StrToUnstructured(objJSON)
   316  	vm := VM{}
   317  
   318  	discoveryLua, err := vm.GetResourceActionDiscovery(testObj)
   319  	require.NoError(t, err)
   320  	assert.NotEmpty(t, discoveryLua)
   321  }
   322  
   323  func TestGetResourceActionDiscoveryNoPredefined(t *testing.T) {
   324  	testObj := StrToUnstructured(objWithNoScriptJSON)
   325  	vm := VM{}
   326  	discoveryLua, err := vm.GetResourceActionDiscovery(testObj)
   327  	require.NoError(t, err)
   328  	assert.Empty(t, discoveryLua)
   329  }
   330  
   331  func TestGetResourceActionDiscoveryWithOverride(t *testing.T) {
   332  	testObj := StrToUnstructured(objJSON)
   333  	vm := VM{
   334  		ResourceOverrides: map[string]appv1.ResourceOverride{
   335  			"argoproj.io/Rollout": {
   336  				Actions: string(grpc.MustMarshal(appv1.ResourceActions{
   337  					ActionDiscoveryLua: validDiscoveryLua,
   338  				})),
   339  			},
   340  		},
   341  	}
   342  	discoveryLua, err := vm.GetResourceActionDiscovery(testObj)
   343  	require.NoError(t, err)
   344  	assert.Equal(t, validDiscoveryLua, discoveryLua[0])
   345  }
   346  
   347  func TestGetResourceActionsWithBuiltInActionsFlag(t *testing.T) {
   348  	testObj := StrToUnstructured(objJSON)
   349  	vm := VM{
   350  		ResourceOverrides: map[string]appv1.ResourceOverride{
   351  			"argoproj.io/Rollout": {
   352  				Actions: string(grpc.MustMarshal(appv1.ResourceActions{
   353  					ActionDiscoveryLua:  validDiscoveryLua,
   354  					MergeBuiltinActions: true,
   355  				})),
   356  			},
   357  		},
   358  	}
   359  
   360  	discoveryLua, err := vm.GetResourceActionDiscovery(testObj)
   361  	require.NoError(t, err)
   362  	assert.Equal(t, validDiscoveryLua, discoveryLua[0])
   363  }
   364  
   365  const validDiscoveryLua = `
   366  scaleParams = { {name = "replicas", type = "number"} }
   367  scale = {name = 'scale', params = scaleParams}
   368  
   369  resume = {name = 'resume'}
   370  
   371  a = {scale = scale, resume = resume}
   372  
   373  return a
   374  `
   375  
   376  const additionalValidDiscoveryLua = `
   377  scaleParams = { {name = "override", type = "number"} }
   378  scale = {name = 'scale', params = scaleParams}
   379  prebuilt = {prebuilt = 'prebuilt', type = 'number'}
   380  
   381  a = {scale = scale, prebuilt = prebuilt}
   382  
   383  return a
   384  `
   385  
   386  func TestExecuteResourceActionDiscovery(t *testing.T) {
   387  	testObj := StrToUnstructured(objJSON)
   388  	vm := VM{}
   389  	actions, err := vm.ExecuteResourceActionDiscovery(testObj, []string{validDiscoveryLua})
   390  	require.NoError(t, err)
   391  	expectedActions := []appv1.ResourceAction{
   392  		{
   393  			Name: "resume",
   394  		}, {
   395  			Name: "scale",
   396  			Params: []appv1.ResourceActionParam{{
   397  				Name: "replicas",
   398  			}},
   399  		},
   400  	}
   401  	for _, expectedAction := range expectedActions {
   402  		assert.Contains(t, actions, expectedAction)
   403  	}
   404  }
   405  
   406  func TestExecuteResourceActionDiscoveryWithDuplicationActions(t *testing.T) {
   407  	testObj := StrToUnstructured(objJSON)
   408  	vm := VM{}
   409  	actions, err := vm.ExecuteResourceActionDiscovery(testObj, []string{validDiscoveryLua, additionalValidDiscoveryLua})
   410  	require.NoError(t, err)
   411  	expectedActions := []appv1.ResourceAction{
   412  		{
   413  			Name: "resume",
   414  		},
   415  		{
   416  			Name: "scale",
   417  			Params: []appv1.ResourceActionParam{{
   418  				Name: "replicas",
   419  			}},
   420  		},
   421  		{
   422  			Name: "prebuilt",
   423  		},
   424  	}
   425  	for _, expectedAction := range expectedActions {
   426  		assert.Contains(t, actions, expectedAction)
   427  	}
   428  }
   429  
   430  const discoveryLuaWithInvalidResourceAction = `
   431  resume = {name = 'resume', invalidField: "test""}
   432  a = {resume = resume}
   433  return a`
   434  
   435  func TestExecuteResourceActionDiscoveryInvalidResourceAction(t *testing.T) {
   436  	testObj := StrToUnstructured(objJSON)
   437  	vm := VM{}
   438  	actions, err := vm.ExecuteResourceActionDiscovery(testObj, []string{discoveryLuaWithInvalidResourceAction})
   439  	require.Error(t, err)
   440  	assert.Nil(t, actions)
   441  }
   442  
   443  const invalidDiscoveryLua = `
   444  a = 1
   445  return a
   446  `
   447  
   448  func TestExecuteResourceActionDiscoveryInvalidReturn(t *testing.T) {
   449  	testObj := StrToUnstructured(objJSON)
   450  	vm := VM{}
   451  	actions, err := vm.ExecuteResourceActionDiscovery(testObj, []string{invalidDiscoveryLua})
   452  	assert.Nil(t, actions)
   453  	require.Error(t, err)
   454  }
   455  
   456  const validActionLua = `
   457  obj.metadata.labels["test"] = "test"
   458  return obj
   459  `
   460  
   461  const expectedLuaUpdatedResult = `
   462  apiVersion: argoproj.io/v1alpha1
   463  kind: Rollout
   464  metadata:
   465    labels:
   466      app.kubernetes.io/instance: helm-guestbook
   467      test: test
   468    name: helm-guestbook
   469    namespace: default
   470    resourceVersion: "123"
   471  `
   472  
   473  // Test an action that returns a single k8s resource json
   474  func TestExecuteOldStyleResourceAction(t *testing.T) {
   475  	testObj := StrToUnstructured(objJSON)
   476  	expectedLuaUpdatedObj := StrToUnstructured(expectedLuaUpdatedResult)
   477  	vm := VM{}
   478  	newObjects, err := vm.ExecuteResourceAction(testObj, validActionLua, nil)
   479  	require.NoError(t, err)
   480  	assert.Len(t, newObjects, 1)
   481  	assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch"))
   482  	assert.Equal(t, expectedLuaUpdatedObj, newObjects[0].UnstructuredObj)
   483  }
   484  
   485  const cronJobObjYaml = `
   486  apiVersion: batch/v1
   487  kind: CronJob
   488  metadata:
   489    name: hello
   490    namespace: test-ns
   491  `
   492  
   493  const expectedCreatedJobObjList = `
   494  - operation: create
   495    resource:
   496      apiVersion: batch/v1
   497      kind: Job
   498      metadata:
   499        name: hello-1
   500        namespace: test-ns
   501  `
   502  
   503  const expectedCreatedMultipleJobsObjList = `
   504  - operation: create
   505    resource:
   506      apiVersion: batch/v1
   507      kind: Job
   508      metadata:
   509        name: hello-1
   510        namespace: test-ns
   511  - operation: create
   512    resource:
   513      apiVersion: batch/v1
   514      kind: Job
   515      metadata:
   516        name: hello-2
   517        namespace: test-ns
   518  `
   519  
   520  const expectedActionMixedOperationObjList = `
   521  - operation: create
   522    resource:
   523      apiVersion: batch/v1
   524      kind: Job
   525      metadata:
   526        name: hello-1
   527        namespace: test-ns
   528  - operation: patch
   529    resource:
   530      apiVersion: batch/v1
   531      kind: CronJob
   532      metadata:
   533        name: hello
   534        namespace: test-ns
   535        labels:
   536          test: test
   537  `
   538  
   539  const createJobActionLua = `
   540  job = {}
   541  job.apiVersion = "batch/v1"
   542  job.kind = "Job"
   543  
   544  job.metadata = {}
   545  job.metadata.name = "hello-1"
   546  job.metadata.namespace = "test-ns"
   547  
   548  impactedResource = {}
   549  impactedResource.operation = "create"
   550  impactedResource.resource = job
   551  result = {}
   552  result[1] = impactedResource
   553  
   554  return result
   555  `
   556  
   557  const createMultipleJobsActionLua = `
   558  job1 = {}
   559  job1.apiVersion = "batch/v1"
   560  job1.kind = "Job"
   561  
   562  job1.metadata = {}
   563  job1.metadata.name = "hello-1"
   564  job1.metadata.namespace = "test-ns"
   565  
   566  impactedResource1 = {}
   567  impactedResource1.operation = "create"
   568  impactedResource1.resource = job1
   569  result = {}
   570  result[1] = impactedResource1
   571  
   572  job2 = {}
   573  job2.apiVersion = "batch/v1"
   574  job2.kind = "Job"
   575  
   576  job2.metadata = {}
   577  job2.metadata.name = "hello-2"
   578  job2.metadata.namespace = "test-ns"
   579  
   580  impactedResource2 = {}
   581  impactedResource2.operation = "create"
   582  impactedResource2.resource = job2
   583  
   584  result[2] = impactedResource2
   585  
   586  return result
   587  `
   588  
   589  const mixedOperationActionLuaOk = `
   590  job1 = {}
   591  job1.apiVersion = "batch/v1"
   592  job1.kind = "Job"
   593  
   594  job1.metadata = {}
   595  job1.metadata.name = "hello-1"
   596  job1.metadata.namespace = obj.metadata.namespace
   597  
   598  impactedResource1 = {}
   599  impactedResource1.operation = "create"
   600  impactedResource1.resource = job1
   601  result = {}
   602  result[1] = impactedResource1
   603  
   604  obj.metadata.labels = {}
   605  obj.metadata.labels["test"] = "test"
   606  
   607  impactedResource2 = {}
   608  impactedResource2.operation = "patch"
   609  impactedResource2.resource = obj
   610  
   611  result[2] = impactedResource2
   612  
   613  return result
   614  `
   615  
   616  const createMixedOperationActionLuaFailing = `
   617  job1 = {}
   618  job1.apiVersion = "batch/v1"
   619  job1.kind = "Job"
   620  
   621  job1.metadata = {}
   622  job1.metadata.name = "hello-1"
   623  job1.metadata.namespace = obj.metadata.namespace
   624  
   625  impactedResource1 = {}
   626  impactedResource1.operation = "create"
   627  impactedResource1.resource = job1
   628  result = {}
   629  result[1] = impactedResource1
   630  
   631  obj.metadata.labels = {}
   632  obj.metadata.labels["test"] = "test"
   633  
   634  impactedResource2 = {}
   635  impactedResource2.operation = "thisShouldFail"
   636  impactedResource2.resource = obj
   637  
   638  result[2] = impactedResource2
   639  
   640  return result
   641  `
   642  
   643  func TestExecuteNewStyleCreateActionSingleResource(t *testing.T) {
   644  	testObj := StrToUnstructured(cronJobObjYaml)
   645  	jsonBytes, err := yaml.YAMLToJSON([]byte(expectedCreatedJobObjList))
   646  	require.NoError(t, err)
   647  	t.Log(bytes.NewBuffer(jsonBytes).String())
   648  	expectedObjects, err := UnmarshalToImpactedResources(bytes.NewBuffer(jsonBytes).String())
   649  	require.NoError(t, err)
   650  	vm := VM{}
   651  	newObjects, err := vm.ExecuteResourceAction(testObj, createJobActionLua, nil)
   652  	require.NoError(t, err)
   653  	assert.Equal(t, expectedObjects, newObjects)
   654  }
   655  
   656  func TestExecuteNewStyleCreateActionMultipleResources(t *testing.T) {
   657  	testObj := StrToUnstructured(cronJobObjYaml)
   658  	jsonBytes, err := yaml.YAMLToJSON([]byte(expectedCreatedMultipleJobsObjList))
   659  	require.NoError(t, err)
   660  	// t.Log(bytes.NewBuffer(jsonBytes).String())
   661  	expectedObjects, err := UnmarshalToImpactedResources(bytes.NewBuffer(jsonBytes).String())
   662  	require.NoError(t, err)
   663  	vm := VM{}
   664  	newObjects, err := vm.ExecuteResourceAction(testObj, createMultipleJobsActionLua, nil)
   665  	require.NoError(t, err)
   666  	assert.Equal(t, expectedObjects, newObjects)
   667  }
   668  
   669  func TestExecuteNewStyleActionMixedOperationsOk(t *testing.T) {
   670  	testObj := StrToUnstructured(cronJobObjYaml)
   671  	jsonBytes, err := yaml.YAMLToJSON([]byte(expectedActionMixedOperationObjList))
   672  	require.NoError(t, err)
   673  	// t.Log(bytes.NewBuffer(jsonBytes).String())
   674  	expectedObjects, err := UnmarshalToImpactedResources(bytes.NewBuffer(jsonBytes).String())
   675  	require.NoError(t, err)
   676  	vm := VM{}
   677  	newObjects, err := vm.ExecuteResourceAction(testObj, mixedOperationActionLuaOk, nil)
   678  	require.NoError(t, err)
   679  	assert.Equal(t, expectedObjects, newObjects)
   680  }
   681  
   682  func TestExecuteNewStyleActionMixedOperationsFailure(t *testing.T) {
   683  	testObj := StrToUnstructured(cronJobObjYaml)
   684  	vm := VM{}
   685  	_, err := vm.ExecuteResourceAction(testObj, createMixedOperationActionLuaFailing, nil)
   686  	assert.ErrorContains(t, err, "unsupported operation")
   687  }
   688  
   689  func TestExecuteResourceActionNonTableReturn(t *testing.T) {
   690  	testObj := StrToUnstructured(objJSON)
   691  	vm := VM{}
   692  	_, err := vm.ExecuteResourceAction(testObj, returnInt, nil)
   693  	assert.Errorf(t, err, incorrectReturnType, "table", "number")
   694  }
   695  
   696  const invalidTableReturn = `newObj = {}
   697  newObj["test"] = "test"
   698  return newObj
   699  `
   700  
   701  func TestExecuteResourceActionInvalidUnstructured(t *testing.T) {
   702  	testObj := StrToUnstructured(objJSON)
   703  	vm := VM{}
   704  	_, err := vm.ExecuteResourceAction(testObj, invalidTableReturn, nil)
   705  	require.Error(t, err)
   706  }
   707  
   708  func TestCleanPatch(t *testing.T) {
   709  	t.Run("Empty Struct preserved", func(t *testing.T) {
   710  		const obj = `
   711  apiVersion: argoproj.io/v1alpha1
   712  kind: Test
   713  metadata:
   714    labels:
   715      app.kubernetes.io/instance: helm-guestbook
   716      test: test
   717    name: helm-guestbook
   718    namespace: default
   719    resourceVersion: "123"
   720  spec:
   721    resources: {}
   722    updated:
   723      something: true
   724    containers:
   725     - name: name1
   726       test: {}
   727       anotherList:
   728       - name: name2
   729         test2: {}
   730  `
   731  		const expected = `
   732  apiVersion: argoproj.io/v1alpha1
   733  kind: Test
   734  metadata:
   735    labels:
   736      app.kubernetes.io/instance: helm-guestbook
   737      test: test
   738    name: helm-guestbook
   739    namespace: default
   740    resourceVersion: "123"
   741  spec:
   742    resources: {}
   743    updated: {}
   744    containers:
   745     - name: name1
   746       test: {}
   747       anotherList:
   748       - name: name2
   749         test2: {}
   750  `
   751  		const luaAction = `
   752  obj.spec.updated = {}
   753  return obj
   754  `
   755  		testObj := StrToUnstructured(obj)
   756  		expectedObj := StrToUnstructured(expected)
   757  		vm := VM{}
   758  		newObjects, err := vm.ExecuteResourceAction(testObj, luaAction, nil)
   759  		require.NoError(t, err)
   760  		assert.Len(t, newObjects, 1)
   761  		assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch"))
   762  		assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj)
   763  	})
   764  
   765  	t.Run("New item added to array", func(t *testing.T) {
   766  		const obj = `
   767  apiVersion: argoproj.io/v1alpha1
   768  kind: Test
   769  metadata:
   770    labels:
   771      app.kubernetes.io/instance: helm-guestbook
   772      test: test
   773    name: helm-guestbook
   774    namespace: default
   775    resourceVersion: "123"
   776  spec:
   777    containers:
   778     - name: name1
   779       test: {}
   780       anotherList:
   781       - name: name2
   782         test2: {}
   783  `
   784  		const expected = `
   785  apiVersion: argoproj.io/v1alpha1
   786  kind: Test
   787  metadata:
   788    labels:
   789      app.kubernetes.io/instance: helm-guestbook
   790      test: test
   791    name: helm-guestbook
   792    namespace: default
   793    resourceVersion: "123"
   794  spec:
   795    containers:
   796     - name: name1
   797       test: {}
   798       anotherList:
   799       - name: name2
   800         test2: {}
   801     - name: added
   802       #test: {}       ### would be decoded as an empty array and is not supported. The type is unknown
   803       testArray: []   ### works since it is decoded in the correct type
   804       another:
   805         supported: true
   806  `
   807  		// `test: {}` in new container would be decoded as an empty array and is not supported. The type is unknown
   808  		// `testArray: []` works since it is decoded in the correct type
   809  		const luaAction = `
   810  table.insert(obj.spec.containers, {name = "added", testArray = {}, another = {supported = true}})
   811  return obj
   812  `
   813  		testObj := StrToUnstructured(obj)
   814  		expectedObj := StrToUnstructured(expected)
   815  		vm := VM{}
   816  		newObjects, err := vm.ExecuteResourceAction(testObj, luaAction, nil)
   817  		require.NoError(t, err)
   818  		assert.Len(t, newObjects, 1)
   819  		assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch"))
   820  		assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj)
   821  	})
   822  
   823  	t.Run("Last item removed from array", func(t *testing.T) {
   824  		const obj = `
   825  apiVersion: argoproj.io/v1alpha1
   826  kind: Test
   827  metadata:
   828    labels:
   829      app.kubernetes.io/instance: helm-guestbook
   830      test: test
   831    name: helm-guestbook
   832    namespace: default
   833    resourceVersion: "123"
   834  spec:
   835    containers:
   836     - name: name1
   837       test: {}
   838       anotherList:
   839       - name: name2
   840         test2: {}
   841     - name: name3
   842       test: {}
   843       anotherList:
   844       - name: name4
   845         test2: {}
   846  `
   847  		const expected = `
   848  apiVersion: argoproj.io/v1alpha1
   849  kind: Test
   850  metadata:
   851    labels:
   852      app.kubernetes.io/instance: helm-guestbook
   853      test: test
   854    name: helm-guestbook
   855    namespace: default
   856    resourceVersion: "123"
   857  spec:
   858    containers:
   859     - name: name1
   860       test: {}
   861       anotherList:
   862       - name: name2
   863         test2: {}
   864  `
   865  		const luaAction = `
   866  table.remove(obj.spec.containers)
   867  return obj
   868  `
   869  		testObj := StrToUnstructured(obj)
   870  		expectedObj := StrToUnstructured(expected)
   871  		vm := VM{}
   872  		newObjects, err := vm.ExecuteResourceAction(testObj, luaAction, nil)
   873  		require.NoError(t, err)
   874  		assert.Len(t, newObjects, 1)
   875  		assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch"))
   876  		assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj)
   877  	})
   878  }
   879  
   880  func TestGetResourceHealth(t *testing.T) {
   881  	const testSA = `
   882  apiVersion: v1
   883  kind: ServiceAccount
   884  metadata:
   885    name: test
   886    namespace: test`
   887  
   888  	const script = `
   889  hs = {}
   890  str = "Using lua standard library"
   891  if string.find(str, "standard") then
   892    hs.message = "Standard lib was used"
   893  else
   894    hs.message = "Standard lib was not used"
   895  end
   896  hs.status = "Healthy"
   897  return hs`
   898  
   899  	const healthWildcardOverrideScript = `
   900   hs = {}
   901   hs.status = "Healthy"
   902   return hs`
   903  
   904  	const healthWildcardOverrideScriptUnhealthy = `
   905   hs = {}
   906   hs.status = "UnHealthy"
   907   return hs`
   908  
   909  	getHealthOverride := func(openLibs bool) ResourceHealthOverrides {
   910  		return ResourceHealthOverrides{
   911  			"ServiceAccount": appv1.ResourceOverride{
   912  				HealthLua:   script,
   913  				UseOpenLibs: openLibs,
   914  			},
   915  		}
   916  	}
   917  
   918  	getWildcardHealthOverride := ResourceHealthOverrides{
   919  		"*.aws.crossplane.io/*": appv1.ResourceOverride{
   920  			HealthLua: healthWildcardOverrideScript,
   921  		},
   922  	}
   923  
   924  	getMultipleWildcardHealthOverrides := ResourceHealthOverrides{
   925  		"*.aws.crossplane.io/*": appv1.ResourceOverride{
   926  			HealthLua: "",
   927  		},
   928  		"*.aws*": appv1.ResourceOverride{
   929  			HealthLua: healthWildcardOverrideScriptUnhealthy,
   930  		},
   931  	}
   932  
   933  	getBaseWildcardHealthOverrides := ResourceHealthOverrides{
   934  		"*/*": appv1.ResourceOverride{
   935  			HealthLua: "",
   936  		},
   937  	}
   938  
   939  	t.Run("Enable Lua standard lib", func(t *testing.T) {
   940  		testObj := StrToUnstructured(testSA)
   941  		overrides := getHealthOverride(true)
   942  		status, err := overrides.GetResourceHealth(testObj)
   943  		require.NoError(t, err)
   944  		expectedStatus := &health.HealthStatus{
   945  			Status:  health.HealthStatusHealthy,
   946  			Message: "Standard lib was used",
   947  		}
   948  		assert.Equal(t, expectedStatus, status)
   949  	})
   950  
   951  	t.Run("Disable Lua standard lib", func(t *testing.T) {
   952  		testObj := StrToUnstructured(testSA)
   953  		overrides := getHealthOverride(false)
   954  		status, err := overrides.GetResourceHealth(testObj)
   955  		assert.IsType(t, &lua.ApiError{}, err)
   956  		expectedErr := "<string>:4: attempt to index a non-table object(nil) with key 'find'"
   957  		require.EqualError(t, err, expectedErr)
   958  		assert.Nil(t, status)
   959  	})
   960  
   961  	t.Run("Get resource health for wildcard override", func(t *testing.T) {
   962  		testObj := StrToUnstructured(ec2AWSCrossplaneObjJSON)
   963  		overrides := getWildcardHealthOverride
   964  		status, err := overrides.GetResourceHealth(testObj)
   965  		require.NoError(t, err)
   966  		expectedStatus := &health.HealthStatus{
   967  			Status: health.HealthStatusHealthy,
   968  		}
   969  		assert.Equal(t, expectedStatus, status)
   970  	})
   971  
   972  	t.Run("Get resource health for wildcard override with non-empty health.lua", func(t *testing.T) {
   973  		testObj := StrToUnstructured(ec2AWSCrossplaneObjJSON)
   974  		overrides := getMultipleWildcardHealthOverrides
   975  		status, err := overrides.GetResourceHealth(testObj)
   976  		require.NoError(t, err)
   977  		expectedStatus := &health.HealthStatus{Status: "Unknown", Message: "Lua returned an invalid health status"}
   978  		assert.Equal(t, expectedStatus, status)
   979  	})
   980  
   981  	t.Run("Get resource health for */* override with empty health.lua", func(t *testing.T) {
   982  		testObj := StrToUnstructured(objWithNoScriptJSON)
   983  		overrides := getBaseWildcardHealthOverrides
   984  		status, err := overrides.GetResourceHealth(testObj)
   985  		require.NoError(t, err)
   986  		assert.Nil(t, status)
   987  	})
   988  
   989  	t.Run("Resource health for wildcard override not found", func(t *testing.T) {
   990  		testObj := StrToUnstructured(testSA)
   991  		overrides := getWildcardHealthOverride
   992  		status, err := overrides.GetResourceHealth(testObj)
   993  		require.NoError(t, err)
   994  		assert.Nil(t, status)
   995  	})
   996  }
   997  
   998  func TestExecuteResourceActionWithParams(t *testing.T) {
   999  	deploymentObj := createMockResource("Deployment", "test-deployment", 1)
  1000  	statefulSetObj := createMockResource("StatefulSet", "test-statefulset", 1)
  1001  
  1002  	actionLua := `
  1003  		obj.spec.replicas = tonumber(actionParams["replicas"])
  1004  		return obj
  1005  		`
  1006  
  1007  	params := []*applicationpkg.ResourceActionParameters{
  1008  		{
  1009  			Name:  func() *string { s := "replicas"; return &s }(),
  1010  			Value: func() *string { s := "3"; return &s }(),
  1011  		},
  1012  	}
  1013  
  1014  	vm := VM{}
  1015  
  1016  	// Test with Deployment
  1017  	t.Run("Test with Deployment", func(t *testing.T) {
  1018  		impactedResources, err := vm.ExecuteResourceAction(deploymentObj, actionLua, params)
  1019  		require.NoError(t, err)
  1020  
  1021  		for _, impactedResource := range impactedResources {
  1022  			modifiedObj := impactedResource.UnstructuredObj
  1023  
  1024  			// Check the replicas in the modified object
  1025  			actualReplicas, found, err := unstructured.NestedInt64(modifiedObj.Object, "spec", "replicas")
  1026  			require.NoError(t, err)
  1027  			assert.True(t, found, "spec.replicas should be found in the modified object")
  1028  			assert.Equal(t, int64(3), actualReplicas, "replicas should be updated to 3")
  1029  		}
  1030  	})
  1031  
  1032  	// Test with StatefulSet
  1033  	t.Run("Test with StatefulSet", func(t *testing.T) {
  1034  		impactedResources, err := vm.ExecuteResourceAction(statefulSetObj, actionLua, params)
  1035  		require.NoError(t, err)
  1036  
  1037  		for _, impactedResource := range impactedResources {
  1038  			modifiedObj := impactedResource.UnstructuredObj
  1039  
  1040  			// Check the replicas in the modified object
  1041  			actualReplicas, found, err := unstructured.NestedInt64(modifiedObj.Object, "spec", "replicas")
  1042  			require.NoError(t, err)
  1043  			assert.True(t, found, "spec.replicas should be found in the modified object")
  1044  			assert.Equal(t, int64(3), actualReplicas, "replicas should be updated to 3")
  1045  		}
  1046  	})
  1047  }
  1048  
  1049  func createMockResource(kind string, name string, replicas int) *unstructured.Unstructured {
  1050  	return StrToUnstructured(fmt.Sprintf(`
  1051      apiVersion: apps/v1
  1052      kind: %s
  1053      metadata:
  1054        name: %s
  1055        namespace: default
  1056      spec:
  1057        replicas: %d
  1058        template:
  1059          metadata:
  1060            labels:
  1061              app: test
  1062          spec:
  1063            containers:
  1064            - name: test-container
  1065              image: nginx
  1066      `, kind, name, replicas))
  1067  }
  1068  
  1069  func Test_getHealthScriptPaths(t *testing.T) {
  1070  	paths, err := getGlobHealthScriptPaths()
  1071  	require.NoError(t, err)
  1072  
  1073  	// This test will fail any time a glob pattern is added to the health script paths. We don't expect that to happen
  1074  	// often.
  1075  	assert.Equal(t, []string{"_.crossplane.io/_", "_.upbound.io/_"}, paths)
  1076  }