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