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 }