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  }