github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/generators/merge_test.go (about)

     1  package generators
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/require"
    10  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    11  
    12  	argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    13  )
    14  
    15  func getNestedListGenerator(json string) *argoprojiov1alpha1.ApplicationSetNestedGenerator {
    16  	return &argoprojiov1alpha1.ApplicationSetNestedGenerator{
    17  		List: &argoprojiov1alpha1.ListGenerator{
    18  			Elements: []apiextensionsv1.JSON{{Raw: []byte(json)}},
    19  		},
    20  	}
    21  }
    22  
    23  func getTerminalListGeneratorMultiple(jsons []string) argoprojiov1alpha1.ApplicationSetTerminalGenerator {
    24  	elements := make([]apiextensionsv1.JSON, len(jsons))
    25  
    26  	for i, json := range jsons {
    27  		elements[i] = apiextensionsv1.JSON{Raw: []byte(json)}
    28  	}
    29  
    30  	generator := argoprojiov1alpha1.ApplicationSetTerminalGenerator{
    31  		List: &argoprojiov1alpha1.ListGenerator{
    32  			Elements: elements,
    33  		},
    34  	}
    35  
    36  	return generator
    37  }
    38  
    39  func listOfMapsToSet(maps []map[string]any) (map[string]bool, error) {
    40  	set := make(map[string]bool, len(maps))
    41  	for _, paramMap := range maps {
    42  		paramMapAsJSON, err := json.Marshal(paramMap)
    43  		if err != nil {
    44  			return nil, err
    45  		}
    46  
    47  		set[string(paramMapAsJSON)] = false
    48  	}
    49  	return set, nil
    50  }
    51  
    52  func TestMergeGenerate(t *testing.T) {
    53  	t.Parallel()
    54  
    55  	testCases := []struct {
    56  		name           string
    57  		baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
    58  		mergeKeys      []string
    59  		expectedErr    error
    60  		expected       []map[string]any
    61  	}{
    62  		{
    63  			name:           "no generators",
    64  			baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{},
    65  			mergeKeys:      []string{"b"},
    66  			expectedErr:    ErrLessThanTwoGeneratorsInMerge,
    67  		},
    68  		{
    69  			name: "one generator",
    70  			baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
    71  				*getNestedListGenerator(`{"a": "1_1","b": "same","c": "1_3"}`),
    72  			},
    73  			mergeKeys:   []string{"b"},
    74  			expectedErr: ErrLessThanTwoGeneratorsInMerge,
    75  		},
    76  		{
    77  			name: "happy flow - generate paramSets",
    78  			baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
    79  				*getNestedListGenerator(`{"a": "1_1","b": "same","c": "1_3"}`),
    80  				*getNestedListGenerator(`{"a": "2_1","b": "same"}`),
    81  				*getNestedListGenerator(`{"a": "3_1","b": "different","c": "3_3"}`), // gets ignored because its merge key value isn't in the base params set
    82  			},
    83  			mergeKeys: []string{"b"},
    84  			expected: []map[string]any{
    85  				{"a": "2_1", "b": "same", "c": "1_3"},
    86  			},
    87  		},
    88  		{
    89  			name: "merge keys absent - do not merge",
    90  			baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
    91  				*getNestedListGenerator(`{"a": "a"}`),
    92  				*getNestedListGenerator(`{"a": "a"}`),
    93  			},
    94  			mergeKeys: []string{"b"},
    95  			expected: []map[string]any{
    96  				{"a": "a"},
    97  			},
    98  		},
    99  		{
   100  			name: "merge key present in first set, absent in second - do not merge",
   101  			baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
   102  				*getNestedListGenerator(`{"a": "a"}`),
   103  				*getNestedListGenerator(`{"b": "b"}`),
   104  			},
   105  			mergeKeys: []string{"b"},
   106  			expected: []map[string]any{
   107  				{"a": "a"},
   108  			},
   109  		},
   110  		{
   111  			name: "merge nested matrix with some lists",
   112  			baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
   113  				{
   114  					Matrix: toAPIExtensionsJSON(t, &argoprojiov1alpha1.NestedMatrixGenerator{
   115  						Generators: []argoprojiov1alpha1.ApplicationSetTerminalGenerator{
   116  							getTerminalListGeneratorMultiple([]string{`{"a": "1"}`, `{"a": "2"}`}),
   117  							getTerminalListGeneratorMultiple([]string{`{"b": "1"}`, `{"b": "2"}`}),
   118  						},
   119  					}),
   120  				},
   121  				*getNestedListGenerator(`{"a": "1", "b": "1", "c": "added"}`),
   122  			},
   123  			mergeKeys: []string{"a", "b"},
   124  			expected: []map[string]any{
   125  				{"a": "1", "b": "1", "c": "added"},
   126  				{"a": "1", "b": "2"},
   127  				{"a": "2", "b": "1"},
   128  				{"a": "2", "b": "2"},
   129  			},
   130  		},
   131  		{
   132  			name: "merge nested merge with some lists",
   133  			baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
   134  				{
   135  					Merge: toAPIExtensionsJSON(t, &argoprojiov1alpha1.NestedMergeGenerator{
   136  						MergeKeys: []string{"a"},
   137  						Generators: []argoprojiov1alpha1.ApplicationSetTerminalGenerator{
   138  							getTerminalListGeneratorMultiple([]string{`{"a": "1", "b": "1"}`, `{"a": "2", "b": "2"}`}),
   139  							getTerminalListGeneratorMultiple([]string{`{"a": "1", "b": "3", "c": "added"}`, `{"a": "3", "b": "2"}`}), // First gets merged, second gets ignored
   140  						},
   141  					}),
   142  				},
   143  				*getNestedListGenerator(`{"a": "1", "b": "3", "d": "added"}`),
   144  			},
   145  			mergeKeys: []string{"a", "b"},
   146  			expected: []map[string]any{
   147  				{"a": "1", "b": "3", "c": "added", "d": "added"},
   148  				{"a": "2", "b": "2"},
   149  			},
   150  		},
   151  	}
   152  
   153  	for _, testCase := range testCases {
   154  		testCaseCopy := testCase // since tests may run in parallel
   155  
   156  		t.Run(testCaseCopy.name, func(t *testing.T) {
   157  			t.Parallel()
   158  
   159  			appSet := &argoprojiov1alpha1.ApplicationSet{}
   160  
   161  			mergeGenerator := NewMergeGenerator(
   162  				map[string]Generator{
   163  					"List": &ListGenerator{},
   164  					"Matrix": &MatrixGenerator{
   165  						supportedGenerators: map[string]Generator{
   166  							"List": &ListGenerator{},
   167  						},
   168  					},
   169  					"Merge": &MergeGenerator{
   170  						supportedGenerators: map[string]Generator{
   171  							"List": &ListGenerator{},
   172  						},
   173  					},
   174  				},
   175  			)
   176  
   177  			got, err := mergeGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
   178  				Merge: &argoprojiov1alpha1.MergeGenerator{
   179  					Generators: testCaseCopy.baseGenerators,
   180  					MergeKeys:  testCaseCopy.mergeKeys,
   181  					Template:   argoprojiov1alpha1.ApplicationSetTemplate{},
   182  				},
   183  			}, appSet, nil)
   184  
   185  			if testCaseCopy.expectedErr != nil {
   186  				require.EqualError(t, err, testCaseCopy.expectedErr.Error())
   187  			} else {
   188  				expectedSet, err := listOfMapsToSet(testCaseCopy.expected)
   189  				require.NoError(t, err)
   190  
   191  				actualSet, err := listOfMapsToSet(got)
   192  				require.NoError(t, err)
   193  
   194  				require.NoError(t, err)
   195  				assert.Equal(t, expectedSet, actualSet)
   196  			}
   197  		})
   198  	}
   199  }
   200  
   201  func toAPIExtensionsJSON(t *testing.T, g any) *apiextensionsv1.JSON {
   202  	t.Helper()
   203  	resVal, err := json.Marshal(g)
   204  	if err != nil {
   205  		t.Error("unable to unmarshal json", g)
   206  		return nil
   207  	}
   208  
   209  	res := &apiextensionsv1.JSON{Raw: resVal}
   210  
   211  	return res
   212  }
   213  
   214  func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
   215  	t.Parallel()
   216  
   217  	testCases := []struct {
   218  		name        string
   219  		mergeKeys   []string
   220  		paramSets   []map[string]any
   221  		expectedErr error
   222  		expected    map[string]map[string]any
   223  	}{
   224  		{
   225  			name:        "no merge keys",
   226  			mergeKeys:   []string{},
   227  			expectedErr: ErrNoMergeKeys,
   228  		},
   229  		{
   230  			name:      "no paramSets",
   231  			mergeKeys: []string{"key"},
   232  			expected:  make(map[string]map[string]any),
   233  		},
   234  		{
   235  			name:      "simple key, unique paramSets",
   236  			mergeKeys: []string{"key"},
   237  			paramSets: []map[string]any{{"key": "a"}, {"key": "b"}},
   238  			expected: map[string]map[string]any{
   239  				`{"key":"a"}`: {"key": "a"},
   240  				`{"key":"b"}`: {"key": "b"},
   241  			},
   242  		},
   243  		{
   244  			name:      "simple key object, unique paramSets",
   245  			mergeKeys: []string{"key"},
   246  			paramSets: []map[string]any{{"key": map[string]any{"hello": "world"}}, {"key": "b"}},
   247  			expected: map[string]map[string]any{
   248  				`{"key":{"hello":"world"}}`: {"key": map[string]any{"hello": "world"}},
   249  				`{"key":"b"}`:               {"key": "b"},
   250  			},
   251  		},
   252  		{
   253  			name:        "simple key, non-unique paramSets",
   254  			mergeKeys:   []string{"key"},
   255  			paramSets:   []map[string]any{{"key": "a"}, {"key": "b"}, {"key": "b"}},
   256  			expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key":"b"}`),
   257  		},
   258  		{
   259  			name:      "simple key, duplicated key name, unique paramSets",
   260  			mergeKeys: []string{"key", "key"},
   261  			paramSets: []map[string]any{{"key": "a"}, {"key": "b"}},
   262  			expected: map[string]map[string]any{
   263  				`{"key":"a"}`: {"key": "a"},
   264  				`{"key":"b"}`: {"key": "b"},
   265  			},
   266  		},
   267  		{
   268  			name:        "simple key, duplicated key name, non-unique paramSets",
   269  			mergeKeys:   []string{"key", "key"},
   270  			paramSets:   []map[string]any{{"key": "a"}, {"key": "b"}, {"key": "b"}},
   271  			expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key":"b"}`),
   272  		},
   273  		{
   274  			name:      "compound key, unique paramSets",
   275  			mergeKeys: []string{"key1", "key2"},
   276  			paramSets: []map[string]any{
   277  				{"key1": "a", "key2": "a"},
   278  				{"key1": "a", "key2": "b"},
   279  				{"key1": "b", "key2": "a"},
   280  			},
   281  			expected: map[string]map[string]any{
   282  				`{"key1":"a","key2":"a"}`: {"key1": "a", "key2": "a"},
   283  				`{"key1":"a","key2":"b"}`: {"key1": "a", "key2": "b"},
   284  				`{"key1":"b","key2":"a"}`: {"key1": "b", "key2": "a"},
   285  			},
   286  		},
   287  		{
   288  			name:      "compound key object, unique paramSets",
   289  			mergeKeys: []string{"key1", "key2"},
   290  			paramSets: []map[string]any{
   291  				{"key1": "a", "key2": map[string]any{"hello": "world"}},
   292  				{"key1": "a", "key2": "b"},
   293  				{"key1": "b", "key2": "a"},
   294  			},
   295  			expected: map[string]map[string]any{
   296  				`{"key1":"a","key2":{"hello":"world"}}`: {"key1": "a", "key2": map[string]any{"hello": "world"}},
   297  				`{"key1":"a","key2":"b"}`:               {"key1": "a", "key2": "b"},
   298  				`{"key1":"b","key2":"a"}`:               {"key1": "b", "key2": "a"},
   299  			},
   300  		},
   301  		{
   302  			name:      "compound key, duplicate key names, unique paramSets",
   303  			mergeKeys: []string{"key1", "key1", "key2"},
   304  			paramSets: []map[string]any{
   305  				{"key1": "a", "key2": "a"},
   306  				{"key1": "a", "key2": "b"},
   307  				{"key1": "b", "key2": "a"},
   308  			},
   309  			expected: map[string]map[string]any{
   310  				`{"key1":"a","key2":"a"}`: {"key1": "a", "key2": "a"},
   311  				`{"key1":"a","key2":"b"}`: {"key1": "a", "key2": "b"},
   312  				`{"key1":"b","key2":"a"}`: {"key1": "b", "key2": "a"},
   313  			},
   314  		},
   315  		{
   316  			name:      "compound key, non-unique paramSets",
   317  			mergeKeys: []string{"key1", "key2"},
   318  			paramSets: []map[string]any{
   319  				{"key1": "a", "key2": "a"},
   320  				{"key1": "a", "key2": "a"},
   321  				{"key1": "b", "key2": "a"},
   322  			},
   323  			expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key1":"a","key2":"a"}`),
   324  		},
   325  		{
   326  			name:      "compound key, duplicate key names, non-unique paramSets",
   327  			mergeKeys: []string{"key1", "key1", "key2"},
   328  			paramSets: []map[string]any{
   329  				{"key1": "a", "key2": "a"},
   330  				{"key1": "a", "key2": "a"},
   331  				{"key1": "b", "key2": "a"},
   332  			},
   333  			expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key1":"a","key2":"a"}`),
   334  		},
   335  	}
   336  
   337  	for _, testCase := range testCases {
   338  		testCaseCopy := testCase // since tests may run in parallel
   339  
   340  		t.Run(testCaseCopy.name, func(t *testing.T) {
   341  			t.Parallel()
   342  
   343  			got, err := getParamSetsByMergeKey(testCaseCopy.mergeKeys, testCaseCopy.paramSets)
   344  
   345  			if testCaseCopy.expectedErr != nil {
   346  				require.EqualError(t, err, testCaseCopy.expectedErr.Error())
   347  			} else {
   348  				require.NoError(t, err)
   349  				assert.Equal(t, testCaseCopy.expected, got)
   350  			}
   351  		})
   352  	}
   353  }