github.com/argoproj/argo-cd/v2@v2.10.9/applicationset/generators/matrix_test.go (about) 1 package generators 2 3 import ( 4 "context" 5 "testing" 6 "time" 7 8 "github.com/stretchr/testify/require" 9 corev1 "k8s.io/api/core/v1" 10 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 "k8s.io/apimachinery/pkg/runtime" 12 kubefake "k8s.io/client-go/kubernetes/fake" 13 "sigs.k8s.io/controller-runtime/pkg/client" 14 "sigs.k8s.io/controller-runtime/pkg/client/fake" 15 16 "github.com/argoproj/argo-cd/v2/applicationset/services/mocks" 17 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/mock" 20 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 21 22 argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 23 ) 24 25 func TestMatrixGenerate(t *testing.T) { 26 27 gitGenerator := &argoprojiov1alpha1.GitGenerator{ 28 RepoURL: "RepoURL", 29 Revision: "Revision", 30 Directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}}, 31 } 32 33 listGenerator := &argoprojiov1alpha1.ListGenerator{ 34 Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url", "templated": "test-{{path.basenameNormalized}}"}`)}}, 35 } 36 37 testCases := []struct { 38 name string 39 baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator 40 expectedErr error 41 expected []map[string]interface{} 42 }{ 43 { 44 name: "happy flow - generate params", 45 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 46 { 47 Git: gitGenerator, 48 }, 49 { 50 List: listGenerator, 51 }, 52 }, 53 expected: []map[string]interface{}{ 54 {"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1", "cluster": "Cluster", "url": "Url", "templated": "test-app1"}, 55 {"path": "app2", "path.basename": "app2", "path.basenameNormalized": "app2", "cluster": "Cluster", "url": "Url", "templated": "test-app2"}, 56 }, 57 }, 58 { 59 name: "happy flow - generate params from two lists", 60 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 61 { 62 List: &argoprojiov1alpha1.ListGenerator{ 63 Elements: []apiextensionsv1.JSON{ 64 {Raw: []byte(`{"a": "1"}`)}, 65 {Raw: []byte(`{"a": "2"}`)}, 66 }, 67 }, 68 }, 69 { 70 List: &argoprojiov1alpha1.ListGenerator{ 71 Elements: []apiextensionsv1.JSON{ 72 {Raw: []byte(`{"b": "1"}`)}, 73 {Raw: []byte(`{"b": "2"}`)}, 74 }, 75 }, 76 }, 77 }, 78 expected: []map[string]interface{}{ 79 {"a": "1", "b": "1"}, 80 {"a": "1", "b": "2"}, 81 {"a": "2", "b": "1"}, 82 {"a": "2", "b": "2"}, 83 }, 84 }, 85 { 86 name: "returns error if there is less than two base generators", 87 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 88 { 89 Git: gitGenerator, 90 }, 91 }, 92 expectedErr: ErrLessThanTwoGenerators, 93 }, 94 { 95 name: "returns error if there is more than two base generators", 96 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 97 { 98 List: listGenerator, 99 }, 100 { 101 List: listGenerator, 102 }, 103 { 104 List: listGenerator, 105 }, 106 }, 107 expectedErr: ErrMoreThanTwoGenerators, 108 }, 109 { 110 name: "returns error if there is more than one inner generator in the first base generator", 111 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 112 { 113 Git: gitGenerator, 114 List: listGenerator, 115 }, 116 { 117 Git: gitGenerator, 118 }, 119 }, 120 expectedErr: ErrMoreThenOneInnerGenerators, 121 }, 122 { 123 name: "returns error if there is more than one inner generator in the second base generator", 124 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 125 { 126 List: listGenerator, 127 }, 128 { 129 Git: gitGenerator, 130 List: listGenerator, 131 }, 132 }, 133 expectedErr: ErrMoreThenOneInnerGenerators, 134 }, 135 } 136 137 for _, testCase := range testCases { 138 testCaseCopy := testCase // Since tests may run in parallel 139 140 t.Run(testCaseCopy.name, func(t *testing.T) { 141 genMock := &generatorMock{} 142 appSet := &argoprojiov1alpha1.ApplicationSet{ 143 ObjectMeta: metav1.ObjectMeta{ 144 Name: "set", 145 }, 146 Spec: argoprojiov1alpha1.ApplicationSetSpec{}, 147 } 148 149 for _, g := range testCaseCopy.baseGenerators { 150 151 gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{ 152 Git: g.Git, 153 List: g.List, 154 } 155 genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]interface{}{ 156 { 157 "path": "app1", 158 "path.basename": "app1", 159 "path.basenameNormalized": "app1", 160 }, 161 { 162 "path": "app2", 163 "path.basename": "app2", 164 "path.basenameNormalized": "app2", 165 }, 166 }, nil) 167 168 genMock.On("GetTemplate", &gitGeneratorSpec). 169 Return(&argoprojiov1alpha1.ApplicationSetTemplate{}) 170 } 171 172 var matrixGenerator = NewMatrixGenerator( 173 map[string]Generator{ 174 "Git": genMock, 175 "List": &ListGenerator{}, 176 }, 177 ) 178 179 got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 180 Matrix: &argoprojiov1alpha1.MatrixGenerator{ 181 Generators: testCaseCopy.baseGenerators, 182 Template: argoprojiov1alpha1.ApplicationSetTemplate{}, 183 }, 184 }, appSet) 185 186 if testCaseCopy.expectedErr != nil { 187 assert.ErrorIs(t, err, testCaseCopy.expectedErr) 188 } else { 189 assert.NoError(t, err) 190 assert.Equal(t, testCaseCopy.expected, got) 191 } 192 193 }) 194 195 } 196 } 197 198 func TestMatrixGenerateGoTemplate(t *testing.T) { 199 200 gitGenerator := &argoprojiov1alpha1.GitGenerator{ 201 RepoURL: "RepoURL", 202 Revision: "Revision", 203 Directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}}, 204 } 205 206 listGenerator := &argoprojiov1alpha1.ListGenerator{ 207 Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url"}`)}}, 208 } 209 210 testCases := []struct { 211 name string 212 baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator 213 expectedErr error 214 expected []map[string]interface{} 215 }{ 216 { 217 name: "happy flow - generate params", 218 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 219 { 220 Git: gitGenerator, 221 }, 222 { 223 List: listGenerator, 224 }, 225 }, 226 expected: []map[string]interface{}{ 227 { 228 "path": map[string]string{ 229 "path": "app1", 230 "basename": "app1", 231 "basenameNormalized": "app1", 232 }, 233 "cluster": "Cluster", 234 "url": "Url", 235 }, 236 { 237 "path": map[string]string{ 238 "path": "app2", 239 "basename": "app2", 240 "basenameNormalized": "app2", 241 }, 242 "cluster": "Cluster", 243 "url": "Url", 244 }, 245 }, 246 }, 247 { 248 name: "happy flow - generate params from two lists", 249 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 250 { 251 List: &argoprojiov1alpha1.ListGenerator{ 252 Elements: []apiextensionsv1.JSON{ 253 {Raw: []byte(`{"a": "1"}`)}, 254 {Raw: []byte(`{"a": "2"}`)}, 255 }, 256 }, 257 }, 258 { 259 List: &argoprojiov1alpha1.ListGenerator{ 260 Elements: []apiextensionsv1.JSON{ 261 {Raw: []byte(`{"b": "1"}`)}, 262 {Raw: []byte(`{"b": "2"}`)}, 263 }, 264 }, 265 }, 266 }, 267 expected: []map[string]interface{}{ 268 {"a": "1", "b": "1"}, 269 {"a": "1", "b": "2"}, 270 {"a": "2", "b": "1"}, 271 {"a": "2", "b": "2"}, 272 }, 273 }, 274 { 275 name: "parameter override: first list elements take precedence", 276 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 277 { 278 List: &argoprojiov1alpha1.ListGenerator{ 279 Elements: []apiextensionsv1.JSON{ 280 {Raw: []byte(`{"booleanFalse": false, "booleanTrue": true, "stringFalse": "false", "stringTrue": "true"}`)}, 281 }, 282 }, 283 }, 284 { 285 List: &argoprojiov1alpha1.ListGenerator{ 286 Elements: []apiextensionsv1.JSON{ 287 {Raw: []byte(`{"booleanFalse": true, "booleanTrue": false, "stringFalse": "true", "stringTrue": "false"}`)}, 288 }, 289 }, 290 }, 291 }, 292 expected: []map[string]interface{}{ 293 {"booleanFalse": false, "booleanTrue": true, "stringFalse": "false", "stringTrue": "true"}, 294 }, 295 }, 296 { 297 name: "returns error if there is less than two base generators", 298 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 299 { 300 Git: gitGenerator, 301 }, 302 }, 303 expectedErr: ErrLessThanTwoGenerators, 304 }, 305 { 306 name: "returns error if there is more than two base generators", 307 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 308 { 309 List: listGenerator, 310 }, 311 { 312 List: listGenerator, 313 }, 314 { 315 List: listGenerator, 316 }, 317 }, 318 expectedErr: ErrMoreThanTwoGenerators, 319 }, 320 { 321 name: "returns error if there is more than one inner generator in the first base generator", 322 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 323 { 324 Git: gitGenerator, 325 List: listGenerator, 326 }, 327 { 328 Git: gitGenerator, 329 }, 330 }, 331 expectedErr: ErrMoreThenOneInnerGenerators, 332 }, 333 { 334 name: "returns error if there is more than one inner generator in the second base generator", 335 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 336 { 337 List: listGenerator, 338 }, 339 { 340 Git: gitGenerator, 341 List: listGenerator, 342 }, 343 }, 344 expectedErr: ErrMoreThenOneInnerGenerators, 345 }, 346 } 347 348 for _, testCase := range testCases { 349 testCaseCopy := testCase // Since tests may run in parallel 350 351 t.Run(testCaseCopy.name, func(t *testing.T) { 352 genMock := &generatorMock{} 353 appSet := &argoprojiov1alpha1.ApplicationSet{ 354 ObjectMeta: metav1.ObjectMeta{ 355 Name: "set", 356 }, 357 Spec: argoprojiov1alpha1.ApplicationSetSpec{ 358 GoTemplate: true, 359 }, 360 } 361 362 for _, g := range testCaseCopy.baseGenerators { 363 364 gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{ 365 Git: g.Git, 366 List: g.List, 367 } 368 genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]interface{}{ 369 { 370 "path": map[string]string{ 371 "path": "app1", 372 "basename": "app1", 373 "basenameNormalized": "app1", 374 }, 375 }, 376 { 377 "path": map[string]string{ 378 "path": "app2", 379 "basename": "app2", 380 "basenameNormalized": "app2", 381 }, 382 }, 383 }, nil) 384 385 genMock.On("GetTemplate", &gitGeneratorSpec). 386 Return(&argoprojiov1alpha1.ApplicationSetTemplate{}) 387 } 388 389 var matrixGenerator = NewMatrixGenerator( 390 map[string]Generator{ 391 "Git": genMock, 392 "List": &ListGenerator{}, 393 }, 394 ) 395 396 got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 397 Matrix: &argoprojiov1alpha1.MatrixGenerator{ 398 Generators: testCaseCopy.baseGenerators, 399 Template: argoprojiov1alpha1.ApplicationSetTemplate{}, 400 }, 401 }, appSet) 402 403 if testCaseCopy.expectedErr != nil { 404 assert.ErrorIs(t, err, testCaseCopy.expectedErr) 405 } else { 406 assert.NoError(t, err) 407 assert.Equal(t, testCaseCopy.expected, got) 408 } 409 410 }) 411 412 } 413 } 414 415 func TestMatrixGetRequeueAfter(t *testing.T) { 416 417 gitGenerator := &argoprojiov1alpha1.GitGenerator{ 418 RepoURL: "RepoURL", 419 Revision: "Revision", 420 Directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}}, 421 } 422 423 listGenerator := &argoprojiov1alpha1.ListGenerator{ 424 Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url"}`)}}, 425 } 426 427 pullRequestGenerator := &argoprojiov1alpha1.PullRequestGenerator{} 428 429 scmGenerator := &argoprojiov1alpha1.SCMProviderGenerator{} 430 431 duckTypeGenerator := &argoprojiov1alpha1.DuckTypeGenerator{} 432 433 testCases := []struct { 434 name string 435 baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator 436 gitGetRequeueAfter time.Duration 437 expected time.Duration 438 }{ 439 { 440 name: "return NoRequeueAfter if all the inner baseGenerators returns it", 441 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 442 { 443 Git: gitGenerator, 444 }, 445 { 446 List: listGenerator, 447 }, 448 }, 449 gitGetRequeueAfter: NoRequeueAfter, 450 expected: NoRequeueAfter, 451 }, 452 { 453 name: "returns the minimal time", 454 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 455 { 456 Git: gitGenerator, 457 }, 458 { 459 List: listGenerator, 460 }, 461 }, 462 gitGetRequeueAfter: time.Duration(1), 463 expected: time.Duration(1), 464 }, 465 { 466 name: "returns the minimal time for pull request", 467 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 468 { 469 Git: gitGenerator, 470 }, 471 { 472 PullRequest: pullRequestGenerator, 473 }, 474 }, 475 gitGetRequeueAfter: time.Duration(15 * time.Second), 476 expected: time.Duration(15 * time.Second), 477 }, 478 { 479 name: "returns the default time if no requeueAfterSeconds is provided", 480 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 481 { 482 Git: gitGenerator, 483 }, 484 { 485 PullRequest: pullRequestGenerator, 486 }, 487 }, 488 expected: time.Duration(30 * time.Minute), 489 }, 490 { 491 name: "returns the default time for duck type generator", 492 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 493 { 494 Git: gitGenerator, 495 }, 496 { 497 ClusterDecisionResource: duckTypeGenerator, 498 }, 499 }, 500 expected: time.Duration(3 * time.Minute), 501 }, 502 { 503 name: "returns the default time for scm generator", 504 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 505 { 506 Git: gitGenerator, 507 }, 508 { 509 SCMProvider: scmGenerator, 510 }, 511 }, 512 expected: time.Duration(30 * time.Minute), 513 }, 514 } 515 516 for _, testCase := range testCases { 517 testCaseCopy := testCase // Since tests may run in parallel 518 519 t.Run(testCaseCopy.name, func(t *testing.T) { 520 mock := &generatorMock{} 521 522 for _, g := range testCaseCopy.baseGenerators { 523 gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{ 524 Git: g.Git, 525 List: g.List, 526 PullRequest: g.PullRequest, 527 SCMProvider: g.SCMProvider, 528 ClusterDecisionResource: g.ClusterDecisionResource, 529 } 530 mock.On("GetRequeueAfter", &gitGeneratorSpec).Return(testCaseCopy.gitGetRequeueAfter, nil) 531 } 532 533 var matrixGenerator = NewMatrixGenerator( 534 map[string]Generator{ 535 "Git": mock, 536 "List": &ListGenerator{}, 537 "PullRequest": &PullRequestGenerator{}, 538 "SCMProvider": &SCMProviderGenerator{}, 539 "ClusterDecisionResource": &DuckTypeGenerator{}, 540 }, 541 ) 542 543 got := matrixGenerator.GetRequeueAfter(&argoprojiov1alpha1.ApplicationSetGenerator{ 544 Matrix: &argoprojiov1alpha1.MatrixGenerator{ 545 Generators: testCaseCopy.baseGenerators, 546 Template: argoprojiov1alpha1.ApplicationSetTemplate{}, 547 }, 548 }) 549 550 assert.Equal(t, testCaseCopy.expected, got) 551 552 }) 553 554 } 555 } 556 557 func TestInterpolatedMatrixGenerate(t *testing.T) { 558 interpolatedGitGenerator := &argoprojiov1alpha1.GitGenerator{ 559 RepoURL: "RepoURL", 560 Revision: "Revision", 561 Files: []argoprojiov1alpha1.GitFileGeneratorItem{ 562 {Path: "examples/git-generator-files-discovery/cluster-config/dev/config.json"}, 563 {Path: "examples/git-generator-files-discovery/cluster-config/prod/config.json"}, 564 }, 565 } 566 567 interpolatedClusterGenerator := &argoprojiov1alpha1.ClusterGenerator{ 568 Selector: metav1.LabelSelector{ 569 MatchLabels: map[string]string{"environment": "{{path.basename}}"}, 570 MatchExpressions: nil, 571 }, 572 } 573 testCases := []struct { 574 name string 575 baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator 576 expectedErr error 577 expected []map[string]interface{} 578 clientError bool 579 }{ 580 { 581 name: "happy flow - generate interpolated params", 582 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 583 { 584 Git: interpolatedGitGenerator, 585 }, 586 { 587 Clusters: interpolatedClusterGenerator, 588 }, 589 }, 590 expected: []map[string]interface{}{ 591 {"path": "examples/git-generator-files-discovery/cluster-config/dev/config.json", "path.basename": "dev", "path.basenameNormalized": "dev", "name": "dev-01", "nameNormalized": "dev-01", "server": "https://dev-01.example.com", "metadata.labels.environment": "dev", "metadata.labels.argocd.argoproj.io/secret-type": "cluster"}, 592 {"path": "examples/git-generator-files-discovery/cluster-config/prod/config.json", "path.basename": "prod", "path.basenameNormalized": "prod", "name": "prod-01", "nameNormalized": "prod-01", "server": "https://prod-01.example.com", "metadata.labels.environment": "prod", "metadata.labels.argocd.argoproj.io/secret-type": "cluster"}, 593 }, 594 clientError: false, 595 }, 596 } 597 clusters := []client.Object{ 598 &corev1.Secret{ 599 TypeMeta: metav1.TypeMeta{ 600 Kind: "Secret", 601 APIVersion: "v1", 602 }, 603 ObjectMeta: metav1.ObjectMeta{ 604 Name: "dev-01", 605 Namespace: "namespace", 606 Labels: map[string]string{ 607 "argocd.argoproj.io/secret-type": "cluster", 608 "environment": "dev", 609 }, 610 }, 611 Data: map[string][]byte{ 612 "config": []byte("{}"), 613 "name": []byte("dev-01"), 614 "server": []byte("https://dev-01.example.com"), 615 }, 616 Type: corev1.SecretType("Opaque"), 617 }, 618 &corev1.Secret{ 619 TypeMeta: metav1.TypeMeta{ 620 Kind: "Secret", 621 APIVersion: "v1", 622 }, 623 ObjectMeta: metav1.ObjectMeta{ 624 Name: "prod-01", 625 Namespace: "namespace", 626 Labels: map[string]string{ 627 "argocd.argoproj.io/secret-type": "cluster", 628 "environment": "prod", 629 }, 630 }, 631 Data: map[string][]byte{ 632 "config": []byte("{}"), 633 "name": []byte("prod-01"), 634 "server": []byte("https://prod-01.example.com"), 635 }, 636 Type: corev1.SecretType("Opaque"), 637 }, 638 } 639 // convert []client.Object to []runtime.Object, for use by kubefake package 640 runtimeClusters := []runtime.Object{} 641 for _, clientCluster := range clusters { 642 runtimeClusters = append(runtimeClusters, clientCluster) 643 } 644 645 for _, testCase := range testCases { 646 testCaseCopy := testCase // Since tests may run in parallel 647 648 t.Run(testCaseCopy.name, func(t *testing.T) { 649 genMock := &generatorMock{} 650 appSet := &argoprojiov1alpha1.ApplicationSet{} 651 652 appClientset := kubefake.NewSimpleClientset(runtimeClusters...) 653 fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build() 654 cl := &possiblyErroringFakeCtrlRuntimeClient{ 655 fakeClient, 656 testCase.clientError, 657 } 658 var clusterGenerator = NewClusterGenerator(cl, context.Background(), appClientset, "namespace") 659 660 for _, g := range testCaseCopy.baseGenerators { 661 662 gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{ 663 Git: g.Git, 664 Clusters: g.Clusters, 665 } 666 genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]interface{}{ 667 { 668 "path": "examples/git-generator-files-discovery/cluster-config/dev/config.json", 669 "path.basename": "dev", 670 "path.basenameNormalized": "dev", 671 }, 672 { 673 "path": "examples/git-generator-files-discovery/cluster-config/prod/config.json", 674 "path.basename": "prod", 675 "path.basenameNormalized": "prod", 676 }, 677 }, nil) 678 genMock.On("GetTemplate", &gitGeneratorSpec). 679 Return(&argoprojiov1alpha1.ApplicationSetTemplate{}) 680 } 681 var matrixGenerator = NewMatrixGenerator( 682 map[string]Generator{ 683 "Git": genMock, 684 "Clusters": clusterGenerator, 685 }, 686 ) 687 688 got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 689 Matrix: &argoprojiov1alpha1.MatrixGenerator{ 690 Generators: testCaseCopy.baseGenerators, 691 Template: argoprojiov1alpha1.ApplicationSetTemplate{}, 692 }, 693 }, appSet) 694 695 if testCaseCopy.expectedErr != nil { 696 assert.ErrorIs(t, err, testCaseCopy.expectedErr) 697 } else { 698 assert.NoError(t, err) 699 assert.Equal(t, testCaseCopy.expected, got) 700 } 701 702 }) 703 } 704 } 705 706 func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) { 707 interpolatedGitGenerator := &argoprojiov1alpha1.GitGenerator{ 708 RepoURL: "RepoURL", 709 Revision: "Revision", 710 Files: []argoprojiov1alpha1.GitFileGeneratorItem{ 711 {Path: "examples/git-generator-files-discovery/cluster-config/dev/config.json"}, 712 {Path: "examples/git-generator-files-discovery/cluster-config/prod/config.json"}, 713 }, 714 } 715 716 interpolatedClusterGenerator := &argoprojiov1alpha1.ClusterGenerator{ 717 Selector: metav1.LabelSelector{ 718 MatchLabels: map[string]string{"environment": "{{.path.basename}}"}, 719 MatchExpressions: nil, 720 }, 721 } 722 testCases := []struct { 723 name string 724 baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator 725 expectedErr error 726 expected []map[string]interface{} 727 clientError bool 728 }{ 729 { 730 name: "happy flow - generate interpolated params", 731 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 732 { 733 Git: interpolatedGitGenerator, 734 }, 735 { 736 Clusters: interpolatedClusterGenerator, 737 }, 738 }, 739 expected: []map[string]interface{}{ 740 { 741 "path": map[string]string{ 742 "path": "examples/git-generator-files-discovery/cluster-config/dev/config.json", 743 "basename": "dev", 744 "basenameNormalized": "dev", 745 }, 746 "name": "dev-01", 747 "nameNormalized": "dev-01", 748 "server": "https://dev-01.example.com", 749 "metadata": map[string]interface{}{ 750 "labels": map[string]string{ 751 "environment": "dev", 752 "argocd.argoproj.io/secret-type": "cluster", 753 }, 754 }, 755 }, 756 { 757 "path": map[string]string{ 758 "path": "examples/git-generator-files-discovery/cluster-config/prod/config.json", 759 "basename": "prod", 760 "basenameNormalized": "prod", 761 }, 762 "name": "prod-01", 763 "nameNormalized": "prod-01", 764 "server": "https://prod-01.example.com", 765 "metadata": map[string]interface{}{ 766 "labels": map[string]string{ 767 "environment": "prod", 768 "argocd.argoproj.io/secret-type": "cluster", 769 }, 770 }, 771 }, 772 }, 773 clientError: false, 774 }, 775 } 776 clusters := []client.Object{ 777 &corev1.Secret{ 778 TypeMeta: metav1.TypeMeta{ 779 Kind: "Secret", 780 APIVersion: "v1", 781 }, 782 ObjectMeta: metav1.ObjectMeta{ 783 Name: "dev-01", 784 Namespace: "namespace", 785 Labels: map[string]string{ 786 "argocd.argoproj.io/secret-type": "cluster", 787 "environment": "dev", 788 }, 789 }, 790 Data: map[string][]byte{ 791 "config": []byte("{}"), 792 "name": []byte("dev-01"), 793 "server": []byte("https://dev-01.example.com"), 794 }, 795 Type: corev1.SecretType("Opaque"), 796 }, 797 &corev1.Secret{ 798 TypeMeta: metav1.TypeMeta{ 799 Kind: "Secret", 800 APIVersion: "v1", 801 }, 802 ObjectMeta: metav1.ObjectMeta{ 803 Name: "prod-01", 804 Namespace: "namespace", 805 Labels: map[string]string{ 806 "argocd.argoproj.io/secret-type": "cluster", 807 "environment": "prod", 808 }, 809 }, 810 Data: map[string][]byte{ 811 "config": []byte("{}"), 812 "name": []byte("prod-01"), 813 "server": []byte("https://prod-01.example.com"), 814 }, 815 Type: corev1.SecretType("Opaque"), 816 }, 817 } 818 // convert []client.Object to []runtime.Object, for use by kubefake package 819 runtimeClusters := []runtime.Object{} 820 for _, clientCluster := range clusters { 821 runtimeClusters = append(runtimeClusters, clientCluster) 822 } 823 824 for _, testCase := range testCases { 825 testCaseCopy := testCase // Since tests may run in parallel 826 827 t.Run(testCaseCopy.name, func(t *testing.T) { 828 genMock := &generatorMock{} 829 appSet := &argoprojiov1alpha1.ApplicationSet{ 830 Spec: argoprojiov1alpha1.ApplicationSetSpec{ 831 GoTemplate: true, 832 }, 833 } 834 835 appClientset := kubefake.NewSimpleClientset(runtimeClusters...) 836 fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build() 837 cl := &possiblyErroringFakeCtrlRuntimeClient{ 838 fakeClient, 839 testCase.clientError, 840 } 841 var clusterGenerator = NewClusterGenerator(cl, context.Background(), appClientset, "namespace") 842 843 for _, g := range testCaseCopy.baseGenerators { 844 845 gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{ 846 Git: g.Git, 847 Clusters: g.Clusters, 848 } 849 genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]interface{}{ 850 851 { 852 "path": map[string]string{ 853 "path": "examples/git-generator-files-discovery/cluster-config/dev/config.json", 854 "basename": "dev", 855 "basenameNormalized": "dev", 856 }, 857 }, 858 { 859 "path": map[string]string{ 860 "path": "examples/git-generator-files-discovery/cluster-config/prod/config.json", 861 "basename": "prod", 862 "basenameNormalized": "prod", 863 }, 864 }, 865 }, nil) 866 genMock.On("GetTemplate", &gitGeneratorSpec). 867 Return(&argoprojiov1alpha1.ApplicationSetTemplate{}) 868 } 869 var matrixGenerator = NewMatrixGenerator( 870 map[string]Generator{ 871 "Git": genMock, 872 "Clusters": clusterGenerator, 873 }, 874 ) 875 876 got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 877 Matrix: &argoprojiov1alpha1.MatrixGenerator{ 878 Generators: testCaseCopy.baseGenerators, 879 Template: argoprojiov1alpha1.ApplicationSetTemplate{}, 880 }, 881 }, appSet) 882 883 if testCaseCopy.expectedErr != nil { 884 assert.ErrorIs(t, err, testCaseCopy.expectedErr) 885 } else { 886 assert.NoError(t, err) 887 assert.Equal(t, testCaseCopy.expected, got) 888 } 889 890 }) 891 892 } 893 } 894 895 func TestMatrixGenerateListElementsYaml(t *testing.T) { 896 897 gitGenerator := &argoprojiov1alpha1.GitGenerator{ 898 RepoURL: "RepoURL", 899 Revision: "Revision", 900 Files: []argoprojiov1alpha1.GitFileGeneratorItem{ 901 {Path: "config.yaml"}, 902 }, 903 } 904 905 listGenerator := &argoprojiov1alpha1.ListGenerator{ 906 Elements: []apiextensionsv1.JSON{}, 907 ElementsYaml: "{{ .foo.bar | toJson }}", 908 } 909 910 testCases := []struct { 911 name string 912 baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator 913 expectedErr error 914 expected []map[string]interface{} 915 }{ 916 { 917 name: "happy flow - generate params", 918 baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 919 { 920 Git: gitGenerator, 921 }, 922 { 923 List: listGenerator, 924 }, 925 }, 926 expected: []map[string]interface{}{ 927 { 928 "chart": "a", 929 "version": "1", 930 "foo": map[string]interface{}{ 931 "bar": []interface{}{ 932 map[string]interface{}{ 933 "chart": "a", 934 "version": "1", 935 }, 936 map[string]interface{}{ 937 "chart": "b", 938 "version": "2", 939 }, 940 }, 941 }, 942 "path": map[string]interface{}{ 943 "basename": "dir", 944 "basenameNormalized": "dir", 945 "filename": "file_name.yaml", 946 "filenameNormalized": "file-name.yaml", 947 "path": "path/dir", 948 "segments": []string{ 949 "path", 950 "dir", 951 }, 952 }, 953 }, 954 { 955 "chart": "b", 956 "version": "2", 957 "foo": map[string]interface{}{ 958 "bar": []interface{}{ 959 map[string]interface{}{ 960 "chart": "a", 961 "version": "1", 962 }, 963 map[string]interface{}{ 964 "chart": "b", 965 "version": "2", 966 }, 967 }, 968 }, 969 "path": map[string]interface{}{ 970 "basename": "dir", 971 "basenameNormalized": "dir", 972 "filename": "file_name.yaml", 973 "filenameNormalized": "file-name.yaml", 974 "path": "path/dir", 975 "segments": []string{ 976 "path", 977 "dir", 978 }, 979 }, 980 }, 981 }, 982 }, 983 } 984 985 for _, testCase := range testCases { 986 testCaseCopy := testCase // Since tests may run in parallel 987 988 t.Run(testCaseCopy.name, func(t *testing.T) { 989 genMock := &generatorMock{} 990 appSet := &argoprojiov1alpha1.ApplicationSet{ 991 ObjectMeta: metav1.ObjectMeta{ 992 Name: "set", 993 }, 994 Spec: argoprojiov1alpha1.ApplicationSetSpec{ 995 GoTemplate: true, 996 }, 997 } 998 999 for _, g := range testCaseCopy.baseGenerators { 1000 1001 gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{ 1002 Git: g.Git, 1003 List: g.List, 1004 } 1005 genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]any{{ 1006 "foo": map[string]interface{}{ 1007 "bar": []interface{}{ 1008 map[string]interface{}{ 1009 "chart": "a", 1010 "version": "1", 1011 }, 1012 map[string]interface{}{ 1013 "chart": "b", 1014 "version": "2", 1015 }, 1016 }, 1017 }, 1018 "path": map[string]interface{}{ 1019 "basename": "dir", 1020 "basenameNormalized": "dir", 1021 "filename": "file_name.yaml", 1022 "filenameNormalized": "file-name.yaml", 1023 "path": "path/dir", 1024 "segments": []string{ 1025 "path", 1026 "dir", 1027 }, 1028 }, 1029 }}, nil) 1030 genMock.On("GetTemplate", &gitGeneratorSpec). 1031 Return(&argoprojiov1alpha1.ApplicationSetTemplate{}) 1032 1033 } 1034 1035 var matrixGenerator = NewMatrixGenerator( 1036 map[string]Generator{ 1037 "Git": genMock, 1038 "List": &ListGenerator{}, 1039 }, 1040 ) 1041 1042 got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 1043 Matrix: &argoprojiov1alpha1.MatrixGenerator{ 1044 Generators: testCaseCopy.baseGenerators, 1045 Template: argoprojiov1alpha1.ApplicationSetTemplate{}, 1046 }, 1047 }, appSet) 1048 1049 if testCaseCopy.expectedErr != nil { 1050 assert.ErrorIs(t, err, testCaseCopy.expectedErr) 1051 } else { 1052 assert.NoError(t, err) 1053 assert.Equal(t, testCaseCopy.expected, got) 1054 } 1055 1056 }) 1057 1058 } 1059 } 1060 1061 type generatorMock struct { 1062 mock.Mock 1063 } 1064 1065 func (g *generatorMock) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate { 1066 args := g.Called(appSetGenerator) 1067 1068 return args.Get(0).(*argoprojiov1alpha1.ApplicationSetTemplate) 1069 } 1070 1071 func (g *generatorMock) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet) ([]map[string]interface{}, error) { 1072 args := g.Called(appSetGenerator, appSet) 1073 1074 return args.Get(0).([]map[string]interface{}), args.Error(1) 1075 } 1076 1077 func (g *generatorMock) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration { 1078 args := g.Called(appSetGenerator) 1079 1080 return args.Get(0).(time.Duration) 1081 1082 } 1083 1084 func TestGitGenerator_GenerateParams_list_x_git_matrix_generator(t *testing.T) { 1085 // Given a matrix generator over a list generator and a git files generator, the nested git files generator should 1086 // be treated as a files generator, and it should produce parameters. 1087 1088 // This tests for a specific bug where a nested git files generator was being treated as a directory generator. This 1089 // happened because, when the matrix generator was being processed, the nested git files generator was being 1090 // interpolated by the deeplyReplace function. That function cannot differentiate between a nil slice and an empty 1091 // slice. So it was replacing the `Directories` field with an empty slice, which the ApplicationSet controller 1092 // interpreted as meaning this was a directory generator, not a files generator. 1093 1094 // Now instead of checking for nil, we check whether the field is a non-empty slice. This test prevents a regression 1095 // of that bug. 1096 1097 listGeneratorMock := &generatorMock{} 1098 listGeneratorMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), mock.AnythingOfType("*v1alpha1.ApplicationSet")).Return([]map[string]interface{}{ 1099 {"some": "value"}, 1100 }, nil) 1101 listGeneratorMock.On("GetTemplate", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator")).Return(&argoprojiov1alpha1.ApplicationSetTemplate{}) 1102 1103 gitGeneratorSpec := &argoprojiov1alpha1.GitGenerator{ 1104 RepoURL: "https://git.example.com", 1105 Files: []argoprojiov1alpha1.GitFileGeneratorItem{ 1106 {Path: "some/path.json"}, 1107 }, 1108 } 1109 1110 repoServiceMock := &mocks.Repos{} 1111 repoServiceMock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string][]byte{ 1112 "some/path.json": []byte("test: content"), 1113 }, nil) 1114 gitGenerator := NewGitGenerator(repoServiceMock) 1115 1116 matrixGenerator := NewMatrixGenerator(map[string]Generator{ 1117 "List": listGeneratorMock, 1118 "Git": gitGenerator, 1119 }) 1120 1121 matrixGeneratorSpec := &argoprojiov1alpha1.MatrixGenerator{ 1122 Generators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{ 1123 { 1124 List: &argoprojiov1alpha1.ListGenerator{ 1125 Elements: []apiextensionsv1.JSON{ 1126 { 1127 Raw: []byte(`{"some": "value"}`), 1128 }, 1129 }, 1130 }, 1131 }, 1132 { 1133 Git: gitGeneratorSpec, 1134 }, 1135 }, 1136 } 1137 params, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 1138 Matrix: matrixGeneratorSpec, 1139 }, &argoprojiov1alpha1.ApplicationSet{}) 1140 require.NoError(t, err) 1141 assert.Equal(t, []map[string]interface{}{{ 1142 "path": "some", 1143 "path.basename": "some", 1144 "path.basenameNormalized": "some", 1145 "path.filename": "path.json", 1146 "path.filenameNormalized": "path.json", 1147 "path[0]": "some", 1148 "some": "value", 1149 "test": "content", 1150 }}, params) 1151 }