Files
argo-cd/util/lua/lua_test.go
2025-12-15 16:38:24 +00:00

1085 lines
28 KiB
Go

package lua
import (
"bytes"
"fmt"
"testing"
"github.com/argoproj/gitops-engine/pkg/health"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
lua "github.com/yuin/gopher-lua"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/grpc"
)
const objJSON = `
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
name: helm-guestbook
namespace: default
resourceVersion: "123"
`
const objWithNoScriptJSON = `
apiVersion: not-an-endpoint.io/v1alpha1
kind: Test
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
name: helm-guestbook
namespace: default
resourceVersion: "123"
`
const ec2AWSCrossplaneObjJSON = `
apiVersion: ec2.aws.crossplane.io/v1alpha1
kind: Instance
metadata:
name: sample-crosspalne-ec2-instance
spec:
forProvider:
region: us-west-2
instanceType: t2.micro
keyName: my-crossplane-key-pair
providerConfigRef:
name: awsconfig
`
const newHealthStatusFunction = `a = {}
a.status = "Healthy"
a.message ="NeedsToBeChanged"
if obj.metadata.name == "helm-guestbook" then
a.message = "testMessage"
end
return a`
const newWildcardHealthStatusFunction = `a = {}
a.status = "Healthy"
a.message ="NeedsToBeChanged"
if obj.metadata.name == "sample-crosspalne-ec2-instance" then
a.message = "testWildcardMessage"
end
return a`
func StrToUnstructured(jsonStr string) *unstructured.Unstructured {
obj := make(map[string]any)
err := yaml.Unmarshal([]byte(jsonStr), &obj)
if err != nil {
panic(err)
}
return &unstructured.Unstructured{Object: obj}
}
func TestExecuteNewHealthStatusFunction(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
status, err := vm.ExecuteHealthLua(testObj, newHealthStatusFunction)
require.NoError(t, err)
expectedHealthStatus := &health.HealthStatus{
Status: "Healthy",
Message: "testMessage",
}
assert.Equal(t, expectedHealthStatus, status)
}
func TestExecuteWildcardHealthStatusFunction(t *testing.T) {
testObj := StrToUnstructured(ec2AWSCrossplaneObjJSON)
vm := VM{}
status, err := vm.ExecuteHealthLua(testObj, newWildcardHealthStatusFunction)
require.NoError(t, err)
expectedHealthStatus := &health.HealthStatus{
Status: "Healthy",
Message: "testWildcardMessage",
}
assert.Equal(t, expectedHealthStatus, status)
}
const osLuaScript = `os.getenv("HOME")`
func TestFailExternalLibCall(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
_, err := vm.ExecuteHealthLua(testObj, osLuaScript)
require.Error(t, err)
var target *lua.ApiError
assert.ErrorAs(t, err, &target)
}
const returnInt = `return 1`
func TestFailLuaReturnNonTable(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
_, err := vm.ExecuteHealthLua(testObj, returnInt)
assert.Equal(t, fmt.Errorf(incorrectReturnType, "table", "number"), err)
}
const invalidHealthStatusStatus = `local healthStatus = {}
healthStatus.status = "test"
return healthStatus
`
func TestInvalidHealthStatusStatus(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
status, err := vm.ExecuteHealthLua(testObj, invalidHealthStatusStatus)
require.NoError(t, err)
expectedStatus := &health.HealthStatus{
Status: health.HealthStatusUnknown,
Message: invalidHealthStatus,
}
assert.Equal(t, expectedStatus, status)
}
const validReturnNothingHealthStatusStatus = `local healthStatus = {}
return
`
func TestNoReturnHealthStatusStatus(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
status, err := vm.ExecuteHealthLua(testObj, validReturnNothingHealthStatusStatus)
require.NoError(t, err)
expectedStatus := &health.HealthStatus{}
assert.Equal(t, expectedStatus, status)
}
const validNilHealthStatusStatus = `local healthStatus = {}
return nil
`
func TestNilHealthStatusStatus(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
status, err := vm.ExecuteHealthLua(testObj, validNilHealthStatusStatus)
require.NoError(t, err)
expectedStatus := &health.HealthStatus{}
assert.Equal(t, expectedStatus, status)
}
const validEmptyArrayHealthStatusStatus = `local healthStatus = {}
return healthStatus
`
func TestEmptyHealthStatusStatus(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
status, err := vm.ExecuteHealthLua(testObj, validEmptyArrayHealthStatusStatus)
require.NoError(t, err)
expectedStatus := &health.HealthStatus{}
assert.Equal(t, expectedStatus, status)
}
const infiniteLoop = `while true do ; end`
func TestHandleInfiniteLoop(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
_, err := vm.ExecuteHealthLua(testObj, infiniteLoop)
var target *lua.ApiError
require.ErrorAs(t, err, &target)
}
func TestGetHealthScriptWithOverride(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{
ResourceOverrides: map[string]appv1.ResourceOverride{
"argoproj.io/Rollout": {
HealthLua: newHealthStatusFunction,
UseOpenLibs: false,
},
},
}
script, useOpenLibs, err := vm.GetHealthScript(testObj)
require.NoError(t, err)
assert.False(t, useOpenLibs)
assert.Equal(t, newHealthStatusFunction, script)
}
func TestGetHealthScriptWithKindWildcardOverride(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{
ResourceOverrides: map[string]appv1.ResourceOverride{
"argoproj.io/*": {
HealthLua: newHealthStatusFunction,
UseOpenLibs: false,
},
},
}
script, useOpenLibs, err := vm.GetHealthScript(testObj)
require.NoError(t, err)
assert.False(t, useOpenLibs)
assert.Equal(t, newHealthStatusFunction, script)
}
func TestGetHealthScriptWithGroupWildcardOverride(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{
ResourceOverrides: map[string]appv1.ResourceOverride{
"*.io/Rollout": {
HealthLua: newHealthStatusFunction,
UseOpenLibs: false,
},
},
}
script, useOpenLibs, err := vm.GetHealthScript(testObj)
require.NoError(t, err)
assert.False(t, useOpenLibs)
assert.Equal(t, newHealthStatusFunction, script)
}
func TestGetHealthScriptWithGroupAndKindWildcardOverride(t *testing.T) {
testObj := StrToUnstructured(ec2AWSCrossplaneObjJSON)
vm := VM{
ResourceOverrides: map[string]appv1.ResourceOverride{
"*.aws.crossplane.io/*": {
HealthLua: newHealthStatusFunction,
UseOpenLibs: false,
},
},
}
script, useOpenLibs, err := vm.GetHealthScript(testObj)
require.NoError(t, err)
assert.False(t, useOpenLibs)
assert.Equal(t, newHealthStatusFunction, script)
}
func TestGetHealthScriptPredefined(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
script, useOpenLibs, err := vm.GetHealthScript(testObj)
require.NoError(t, err)
assert.True(t, useOpenLibs)
assert.NotEmpty(t, script)
}
func TestGetHealthScriptNoPredefined(t *testing.T) {
testObj := StrToUnstructured(objWithNoScriptJSON)
vm := VM{}
script, useOpenLibs, err := vm.GetHealthScript(testObj)
require.NoError(t, err)
assert.False(t, useOpenLibs)
assert.Empty(t, script)
}
func TestGetResourceActionPredefined(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
action, err := vm.GetResourceAction(testObj, "resume")
require.NoError(t, err)
assert.NotEmpty(t, action)
}
func TestGetResourceActionNoPredefined(t *testing.T) {
testObj := StrToUnstructured(objWithNoScriptJSON)
vm := VM{}
action, err := vm.GetResourceAction(testObj, "test")
require.ErrorIs(t, err, errScriptDoesNotExist)
assert.Empty(t, action.ActionLua)
}
func TestGetResourceActionWithOverride(t *testing.T) {
testObj := StrToUnstructured(objJSON)
test := appv1.ResourceActionDefinition{
Name: "test",
ActionLua: "return obj",
}
vm := VM{
ResourceOverrides: map[string]appv1.ResourceOverride{
"argoproj.io/Rollout": {
Actions: string(grpc.MustMarshal(appv1.ResourceActions{
Definitions: []appv1.ResourceActionDefinition{
test,
},
})),
},
},
}
action, err := vm.GetResourceAction(testObj, "test")
require.NoError(t, err)
assert.Equal(t, test, action)
}
func TestGetResourceActionDiscoveryPredefined(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
discoveryLua, err := vm.GetResourceActionDiscovery(testObj)
require.NoError(t, err)
assert.NotEmpty(t, discoveryLua)
}
func TestGetResourceActionDiscoveryNoPredefined(t *testing.T) {
testObj := StrToUnstructured(objWithNoScriptJSON)
vm := VM{}
discoveryLua, err := vm.GetResourceActionDiscovery(testObj)
require.NoError(t, err)
assert.Empty(t, discoveryLua)
}
func TestGetResourceActionDiscoveryWithOverride(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{
ResourceOverrides: map[string]appv1.ResourceOverride{
"argoproj.io/Rollout": {
Actions: string(grpc.MustMarshal(appv1.ResourceActions{
ActionDiscoveryLua: validDiscoveryLua,
})),
},
},
}
discoveryLua, err := vm.GetResourceActionDiscovery(testObj)
require.NoError(t, err)
assert.Equal(t, validDiscoveryLua, discoveryLua[0])
}
func TestGetResourceActionsWithBuiltInActionsFlag(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{
ResourceOverrides: map[string]appv1.ResourceOverride{
"argoproj.io/Rollout": {
Actions: string(grpc.MustMarshal(appv1.ResourceActions{
ActionDiscoveryLua: validDiscoveryLua,
MergeBuiltinActions: true,
})),
},
},
}
discoveryLua, err := vm.GetResourceActionDiscovery(testObj)
require.NoError(t, err)
assert.Equal(t, validDiscoveryLua, discoveryLua[0])
}
const validDiscoveryLua = `
scaleParams = { {name = "replicas", type = "number"} }
scale = {name = 'scale', params = scaleParams}
resume = {name = 'resume'}
a = {scale = scale, resume = resume}
return a
`
const additionalValidDiscoveryLua = `
scaleParams = { {name = "override", type = "number"} }
scale = {name = 'scale', params = scaleParams}
prebuilt = {prebuilt = 'prebuilt', type = 'number'}
a = {scale = scale, prebuilt = prebuilt}
return a
`
func TestExecuteResourceActionDiscovery(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
actions, err := vm.ExecuteResourceActionDiscovery(testObj, []string{validDiscoveryLua})
require.NoError(t, err)
expectedActions := []appv1.ResourceAction{
{
Name: "resume",
}, {
Name: "scale",
Params: []appv1.ResourceActionParam{{
Name: "replicas",
}},
},
}
for _, expectedAction := range expectedActions {
assert.Contains(t, actions, expectedAction)
}
}
func TestExecuteResourceActionDiscoveryWithDuplicationActions(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
actions, err := vm.ExecuteResourceActionDiscovery(testObj, []string{validDiscoveryLua, additionalValidDiscoveryLua})
require.NoError(t, err)
expectedActions := []appv1.ResourceAction{
{
Name: "resume",
},
{
Name: "scale",
Params: []appv1.ResourceActionParam{{
Name: "replicas",
}},
},
{
Name: "prebuilt",
},
}
for _, expectedAction := range expectedActions {
assert.Contains(t, actions, expectedAction)
}
}
const discoveryLuaWithInvalidResourceAction = `
resume = {name = 'resume', invalidField: "test""}
a = {resume = resume}
return a`
func TestExecuteResourceActionDiscoveryInvalidResourceAction(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
actions, err := vm.ExecuteResourceActionDiscovery(testObj, []string{discoveryLuaWithInvalidResourceAction})
require.Error(t, err)
assert.Nil(t, actions)
}
const invalidDiscoveryLua = `
a = 1
return a
`
func TestExecuteResourceActionDiscoveryInvalidReturn(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
actions, err := vm.ExecuteResourceActionDiscovery(testObj, []string{invalidDiscoveryLua})
assert.Nil(t, actions)
require.Error(t, err)
}
const validActionLua = `
obj.metadata.labels["test"] = "test"
return obj
`
const expectedLuaUpdatedResult = `
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
test: test
name: helm-guestbook
namespace: default
resourceVersion: "123"
`
// Test an action that returns a single k8s resource json
func TestExecuteOldStyleResourceAction(t *testing.T) {
testObj := StrToUnstructured(objJSON)
expectedLuaUpdatedObj := StrToUnstructured(expectedLuaUpdatedResult)
vm := VM{}
newObjects, err := vm.ExecuteResourceAction(testObj, validActionLua, nil)
require.NoError(t, err)
assert.Len(t, newObjects, 1)
assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch"))
assert.Equal(t, expectedLuaUpdatedObj, newObjects[0].UnstructuredObj)
}
const cronJobObjYaml = `
apiVersion: batch/v1
kind: CronJob
metadata:
name: hello
namespace: test-ns
`
const expectedCreatedJobObjList = `
- operation: create
resource:
apiVersion: batch/v1
kind: Job
metadata:
name: hello-1
namespace: test-ns
`
const expectedCreatedMultipleJobsObjList = `
- operation: create
resource:
apiVersion: batch/v1
kind: Job
metadata:
name: hello-1
namespace: test-ns
- operation: create
resource:
apiVersion: batch/v1
kind: Job
metadata:
name: hello-2
namespace: test-ns
`
const expectedActionMixedOperationObjList = `
- operation: create
resource:
apiVersion: batch/v1
kind: Job
metadata:
name: hello-1
namespace: test-ns
- operation: patch
resource:
apiVersion: batch/v1
kind: CronJob
metadata:
name: hello
namespace: test-ns
labels:
test: test
`
const createJobActionLua = `
job = {}
job.apiVersion = "batch/v1"
job.kind = "Job"
job.metadata = {}
job.metadata.name = "hello-1"
job.metadata.namespace = "test-ns"
impactedResource = {}
impactedResource.operation = "create"
impactedResource.resource = job
result = {}
result[1] = impactedResource
return result
`
const createMultipleJobsActionLua = `
job1 = {}
job1.apiVersion = "batch/v1"
job1.kind = "Job"
job1.metadata = {}
job1.metadata.name = "hello-1"
job1.metadata.namespace = "test-ns"
impactedResource1 = {}
impactedResource1.operation = "create"
impactedResource1.resource = job1
result = {}
result[1] = impactedResource1
job2 = {}
job2.apiVersion = "batch/v1"
job2.kind = "Job"
job2.metadata = {}
job2.metadata.name = "hello-2"
job2.metadata.namespace = "test-ns"
impactedResource2 = {}
impactedResource2.operation = "create"
impactedResource2.resource = job2
result[2] = impactedResource2
return result
`
const mixedOperationActionLuaOk = `
job1 = {}
job1.apiVersion = "batch/v1"
job1.kind = "Job"
job1.metadata = {}
job1.metadata.name = "hello-1"
job1.metadata.namespace = obj.metadata.namespace
impactedResource1 = {}
impactedResource1.operation = "create"
impactedResource1.resource = job1
result = {}
result[1] = impactedResource1
obj.metadata.labels = {}
obj.metadata.labels["test"] = "test"
impactedResource2 = {}
impactedResource2.operation = "patch"
impactedResource2.resource = obj
result[2] = impactedResource2
return result
`
const createMixedOperationActionLuaFailing = `
job1 = {}
job1.apiVersion = "batch/v1"
job1.kind = "Job"
job1.metadata = {}
job1.metadata.name = "hello-1"
job1.metadata.namespace = obj.metadata.namespace
impactedResource1 = {}
impactedResource1.operation = "create"
impactedResource1.resource = job1
result = {}
result[1] = impactedResource1
obj.metadata.labels = {}
obj.metadata.labels["test"] = "test"
impactedResource2 = {}
impactedResource2.operation = "thisShouldFail"
impactedResource2.resource = obj
result[2] = impactedResource2
return result
`
func TestExecuteNewStyleCreateActionSingleResource(t *testing.T) {
testObj := StrToUnstructured(cronJobObjYaml)
jsonBytes, err := yaml.YAMLToJSON([]byte(expectedCreatedJobObjList))
require.NoError(t, err)
t.Log(bytes.NewBuffer(jsonBytes).String())
expectedObjects, err := UnmarshalToImpactedResources(bytes.NewBuffer(jsonBytes).String())
require.NoError(t, err)
vm := VM{}
newObjects, err := vm.ExecuteResourceAction(testObj, createJobActionLua, nil)
require.NoError(t, err)
assert.Equal(t, expectedObjects, newObjects)
}
func TestExecuteNewStyleCreateActionMultipleResources(t *testing.T) {
testObj := StrToUnstructured(cronJobObjYaml)
jsonBytes, err := yaml.YAMLToJSON([]byte(expectedCreatedMultipleJobsObjList))
require.NoError(t, err)
// t.Log(bytes.NewBuffer(jsonBytes).String())
expectedObjects, err := UnmarshalToImpactedResources(bytes.NewBuffer(jsonBytes).String())
require.NoError(t, err)
vm := VM{}
newObjects, err := vm.ExecuteResourceAction(testObj, createMultipleJobsActionLua, nil)
require.NoError(t, err)
assert.Equal(t, expectedObjects, newObjects)
}
func TestExecuteNewStyleActionMixedOperationsOk(t *testing.T) {
testObj := StrToUnstructured(cronJobObjYaml)
jsonBytes, err := yaml.YAMLToJSON([]byte(expectedActionMixedOperationObjList))
require.NoError(t, err)
// t.Log(bytes.NewBuffer(jsonBytes).String())
expectedObjects, err := UnmarshalToImpactedResources(bytes.NewBuffer(jsonBytes).String())
require.NoError(t, err)
vm := VM{}
newObjects, err := vm.ExecuteResourceAction(testObj, mixedOperationActionLuaOk, nil)
require.NoError(t, err)
assert.Equal(t, expectedObjects, newObjects)
}
func TestExecuteNewStyleActionMixedOperationsFailure(t *testing.T) {
testObj := StrToUnstructured(cronJobObjYaml)
vm := VM{}
_, err := vm.ExecuteResourceAction(testObj, createMixedOperationActionLuaFailing, nil)
assert.ErrorContains(t, err, "unsupported operation")
}
func TestExecuteResourceActionNonTableReturn(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
_, err := vm.ExecuteResourceAction(testObj, returnInt, nil)
assert.Errorf(t, err, incorrectReturnType, "table", "number")
}
const invalidTableReturn = `newObj = {}
newObj["test"] = "test"
return newObj
`
func TestExecuteResourceActionInvalidUnstructured(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
_, err := vm.ExecuteResourceAction(testObj, invalidTableReturn, nil)
require.Error(t, err)
}
func TestCleanPatch(t *testing.T) {
t.Run("Empty Struct preserved", func(t *testing.T) {
const obj = `
apiVersion: argoproj.io/v1alpha1
kind: Test
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
test: test
name: helm-guestbook
namespace: default
resourceVersion: "123"
spec:
resources: {}
updated:
something: true
containers:
- name: name1
test: {}
anotherList:
- name: name2
test2: {}
`
const expected = `
apiVersion: argoproj.io/v1alpha1
kind: Test
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
test: test
name: helm-guestbook
namespace: default
resourceVersion: "123"
spec:
resources: {}
updated: {}
containers:
- name: name1
test: {}
anotherList:
- name: name2
test2: {}
`
const luaAction = `
obj.spec.updated = {}
return obj
`
testObj := StrToUnstructured(obj)
expectedObj := StrToUnstructured(expected)
vm := VM{}
newObjects, err := vm.ExecuteResourceAction(testObj, luaAction, nil)
require.NoError(t, err)
assert.Len(t, newObjects, 1)
assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch"))
assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj)
})
t.Run("New item added to array", func(t *testing.T) {
const obj = `
apiVersion: argoproj.io/v1alpha1
kind: Test
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
test: test
name: helm-guestbook
namespace: default
resourceVersion: "123"
spec:
containers:
- name: name1
test: {}
anotherList:
- name: name2
test2: {}
`
const expected = `
apiVersion: argoproj.io/v1alpha1
kind: Test
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
test: test
name: helm-guestbook
namespace: default
resourceVersion: "123"
spec:
containers:
- name: name1
test: {}
anotherList:
- name: name2
test2: {}
- name: added
#test: {} ### would be decoded as an empty array and is not supported. The type is unknown
testArray: [] ### works since it is decoded in the correct type
another:
supported: true
`
// `test: {}` in new container would be decoded as an empty array and is not supported. The type is unknown
// `testArray: []` works since it is decoded in the correct type
const luaAction = `
table.insert(obj.spec.containers, {name = "added", testArray = {}, another = {supported = true}})
return obj
`
testObj := StrToUnstructured(obj)
expectedObj := StrToUnstructured(expected)
vm := VM{}
newObjects, err := vm.ExecuteResourceAction(testObj, luaAction, nil)
require.NoError(t, err)
assert.Len(t, newObjects, 1)
assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch"))
assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj)
})
t.Run("Last item removed from array", func(t *testing.T) {
const obj = `
apiVersion: argoproj.io/v1alpha1
kind: Test
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
test: test
name: helm-guestbook
namespace: default
resourceVersion: "123"
spec:
containers:
- name: name1
test: {}
anotherList:
- name: name2
test2: {}
- name: name3
test: {}
anotherList:
- name: name4
test2: {}
`
const expected = `
apiVersion: argoproj.io/v1alpha1
kind: Test
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
test: test
name: helm-guestbook
namespace: default
resourceVersion: "123"
spec:
containers:
- name: name1
test: {}
anotherList:
- name: name2
test2: {}
`
const luaAction = `
table.remove(obj.spec.containers)
return obj
`
testObj := StrToUnstructured(obj)
expectedObj := StrToUnstructured(expected)
vm := VM{}
newObjects, err := vm.ExecuteResourceAction(testObj, luaAction, nil)
require.NoError(t, err)
assert.Len(t, newObjects, 1)
assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch"))
assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj)
})
}
func TestGetResourceHealth(t *testing.T) {
const testSA = `
apiVersion: v1
kind: ServiceAccount
metadata:
name: test
namespace: test`
const script = `
hs = {}
str = "Using lua standard library"
if string.find(str, "standard") then
hs.message = "Standard lib was used"
else
hs.message = "Standard lib was not used"
end
hs.status = "Healthy"
return hs`
const healthWildcardOverrideScript = `
hs = {}
hs.status = "Healthy"
return hs`
const healthWildcardOverrideScriptUnhealthy = `
hs = {}
hs.status = "UnHealthy"
return hs`
getHealthOverride := func(openLibs bool) ResourceHealthOverrides {
return ResourceHealthOverrides{
"ServiceAccount": appv1.ResourceOverride{
HealthLua: script,
UseOpenLibs: openLibs,
},
}
}
getWildcardHealthOverride := ResourceHealthOverrides{
"*.aws.crossplane.io/*": appv1.ResourceOverride{
HealthLua: healthWildcardOverrideScript,
},
}
getMultipleWildcardHealthOverrides := ResourceHealthOverrides{
"*.aws.crossplane.io/*": appv1.ResourceOverride{
HealthLua: "",
},
"*.aws*": appv1.ResourceOverride{
HealthLua: healthWildcardOverrideScriptUnhealthy,
},
}
getBaseWildcardHealthOverrides := ResourceHealthOverrides{
"*/*": appv1.ResourceOverride{
HealthLua: "",
},
}
t.Run("Enable Lua standard lib", func(t *testing.T) {
testObj := StrToUnstructured(testSA)
overrides := getHealthOverride(true)
status, err := overrides.GetResourceHealth(testObj)
require.NoError(t, err)
expectedStatus := &health.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "Standard lib was used",
}
assert.Equal(t, expectedStatus, status)
})
t.Run("Disable Lua standard lib", func(t *testing.T) {
testObj := StrToUnstructured(testSA)
overrides := getHealthOverride(false)
status, err := overrides.GetResourceHealth(testObj)
var target *lua.ApiError
require.ErrorAs(t, err, &target)
expectedErr := "<string>:4: attempt to index a non-table object(nil) with key 'find'"
require.EqualError(t, err, expectedErr)
assert.Nil(t, status)
})
t.Run("Get resource health for wildcard override", func(t *testing.T) {
testObj := StrToUnstructured(ec2AWSCrossplaneObjJSON)
overrides := getWildcardHealthOverride
status, err := overrides.GetResourceHealth(testObj)
require.NoError(t, err)
expectedStatus := &health.HealthStatus{
Status: health.HealthStatusHealthy,
}
assert.Equal(t, expectedStatus, status)
})
t.Run("Get resource health for wildcard override with non-empty health.lua", func(t *testing.T) {
testObj := StrToUnstructured(ec2AWSCrossplaneObjJSON)
overrides := getMultipleWildcardHealthOverrides
status, err := overrides.GetResourceHealth(testObj)
require.NoError(t, err)
expectedStatus := &health.HealthStatus{Status: "Unknown", Message: "Lua returned an invalid health status"}
assert.Equal(t, expectedStatus, status)
})
t.Run("Get resource health for */* override with empty health.lua", func(t *testing.T) {
testObj := StrToUnstructured(objWithNoScriptJSON)
overrides := getBaseWildcardHealthOverrides
status, err := overrides.GetResourceHealth(testObj)
require.NoError(t, err)
assert.Nil(t, status)
})
t.Run("Resource health for wildcard override not found", func(t *testing.T) {
testObj := StrToUnstructured(testSA)
overrides := getWildcardHealthOverride
status, err := overrides.GetResourceHealth(testObj)
require.NoError(t, err)
assert.Nil(t, status)
})
}
func TestExecuteResourceActionWithParams(t *testing.T) {
deploymentObj := createMockResource("Deployment", "test-deployment", 1)
statefulSetObj := createMockResource("StatefulSet", "test-statefulset", 1)
actionLua := `
obj.spec.replicas = tonumber(actionParams["replicas"])
return obj
`
params := []*applicationpkg.ResourceActionParameters{
{
Name: func() *string { s := "replicas"; return &s }(),
Value: func() *string { s := "3"; return &s }(),
},
}
vm := VM{}
// Test with Deployment
t.Run("Test with Deployment", func(t *testing.T) {
impactedResources, err := vm.ExecuteResourceAction(deploymentObj, actionLua, params)
require.NoError(t, err)
for _, impactedResource := range impactedResources {
modifiedObj := impactedResource.UnstructuredObj
// Check the replicas in the modified object
actualReplicas, found, err := unstructured.NestedInt64(modifiedObj.Object, "spec", "replicas")
require.NoError(t, err)
assert.True(t, found, "spec.replicas should be found in the modified object")
assert.Equal(t, int64(3), actualReplicas, "replicas should be updated to 3")
}
})
// Test with StatefulSet
t.Run("Test with StatefulSet", func(t *testing.T) {
impactedResources, err := vm.ExecuteResourceAction(statefulSetObj, actionLua, params)
require.NoError(t, err)
for _, impactedResource := range impactedResources {
modifiedObj := impactedResource.UnstructuredObj
// Check the replicas in the modified object
actualReplicas, found, err := unstructured.NestedInt64(modifiedObj.Object, "spec", "replicas")
require.NoError(t, err)
assert.True(t, found, "spec.replicas should be found in the modified object")
assert.Equal(t, int64(3), actualReplicas, "replicas should be updated to 3")
}
})
}
func createMockResource(kind string, name string, replicas int) *unstructured.Unstructured {
return StrToUnstructured(fmt.Sprintf(`
apiVersion: apps/v1
kind: %s
metadata:
name: %s
namespace: default
spec:
replicas: %d
template:
metadata:
labels:
app: test
spec:
containers:
- name: test-container
image: nginx
`, kind, name, replicas))
}
func Test_getHealthScriptPaths(t *testing.T) {
paths, err := getGlobHealthScriptPaths()
require.NoError(t, err)
// This test will fail any time a glob pattern is added to the health script paths. We don't expect that to happen
// often.
assert.Equal(t, []string{
"_.cnrm.cloud.google.com/_",
"_.crossplane.io/_",
"_.upbound.io/_",
"grafana-org-operator.kubitus-project.gitlab.io/_",
}, paths)
}