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  }