github.com/argoproj/argo-cd/v2@v2.10.9/applicationset/generators/generator_spec_processor_test.go (about) 1 package generators 2 3 import ( 4 "context" 5 "testing" 6 7 log "github.com/sirupsen/logrus" 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 11 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 13 "github.com/argoproj/argo-cd/v2/applicationset/services/mocks" 14 15 argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 16 17 "github.com/stretchr/testify/mock" 18 corev1 "k8s.io/api/core/v1" 19 "k8s.io/apimachinery/pkg/runtime" 20 kubefake "k8s.io/client-go/kubernetes/fake" 21 crtclient "sigs.k8s.io/controller-runtime/pkg/client" 22 "sigs.k8s.io/controller-runtime/pkg/client/fake" 23 ) 24 25 func TestMatchValues(t *testing.T) { 26 testCases := []struct { 27 name string 28 elements []apiextensionsv1.JSON 29 selector *metav1.LabelSelector 30 expected []map[string]interface{} 31 }{ 32 { 33 name: "no filter", 34 elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}}, 35 selector: &metav1.LabelSelector{}, 36 expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}}, 37 }, 38 { 39 name: "nil", 40 elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}}, 41 selector: nil, 42 expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}}, 43 }, 44 { 45 name: "values.foo should be foo but is ignore element", 46 elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}}, 47 selector: &metav1.LabelSelector{ 48 MatchLabels: map[string]string{ 49 "values.foo": "foo", 50 }, 51 }, 52 expected: []map[string]interface{}{}, 53 }, 54 { 55 name: "values.foo should be bar", 56 elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}}, 57 selector: &metav1.LabelSelector{ 58 MatchLabels: map[string]string{ 59 "values.foo": "bar", 60 }, 61 }, 62 expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values.foo": "bar"}}, 63 }, 64 } 65 66 for _, testCase := range testCases { 67 t.Run(testCase.name, func(t *testing.T) { 68 var listGenerator = NewListGenerator() 69 var data = map[string]Generator{ 70 "List": listGenerator, 71 } 72 73 applicationSetInfo := argov1alpha1.ApplicationSet{ 74 ObjectMeta: metav1.ObjectMeta{ 75 Name: "set", 76 }, 77 Spec: argov1alpha1.ApplicationSetSpec{ 78 GoTemplate: false, 79 }, 80 } 81 82 results, err := Transform(argov1alpha1.ApplicationSetGenerator{ 83 Selector: testCase.selector, 84 List: &argov1alpha1.ListGenerator{ 85 Elements: testCase.elements, 86 Template: emptyTemplate(), 87 }}, 88 data, 89 emptyTemplate(), 90 &applicationSetInfo, nil) 91 92 assert.NoError(t, err) 93 assert.ElementsMatch(t, testCase.expected, results[0].Params) 94 }) 95 } 96 } 97 98 func TestMatchValuesGoTemplate(t *testing.T) { 99 testCases := []struct { 100 name string 101 elements []apiextensionsv1.JSON 102 selector *metav1.LabelSelector 103 expected []map[string]interface{} 104 }{ 105 { 106 name: "no filter", 107 elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}}, 108 selector: &metav1.LabelSelector{}, 109 expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}}, 110 }, 111 { 112 name: "nil", 113 elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}}, 114 selector: nil, 115 expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}}, 116 }, 117 { 118 name: "values.foo should be foo but is ignore element", 119 elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}}, 120 selector: &metav1.LabelSelector{ 121 MatchLabels: map[string]string{ 122 "values.foo": "foo", 123 }, 124 }, 125 expected: []map[string]interface{}{}, 126 }, 127 { 128 name: "values.foo should be bar", 129 elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}}, 130 selector: &metav1.LabelSelector{ 131 MatchLabels: map[string]string{ 132 "values.foo": "bar", 133 }, 134 }, 135 expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values": map[string]interface{}{"foo": "bar"}}}, 136 }, 137 { 138 name: "values.0 should be bar", 139 elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":["bar"]}`)}}, 140 selector: &metav1.LabelSelector{ 141 MatchLabels: map[string]string{ 142 "values.0": "bar", 143 }, 144 }, 145 expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values": []interface{}{"bar"}}}, 146 }, 147 } 148 149 for _, testCase := range testCases { 150 t.Run(testCase.name, func(t *testing.T) { 151 var listGenerator = NewListGenerator() 152 var data = map[string]Generator{ 153 "List": listGenerator, 154 } 155 156 applicationSetInfo := argov1alpha1.ApplicationSet{ 157 ObjectMeta: metav1.ObjectMeta{ 158 Name: "set", 159 }, 160 Spec: argov1alpha1.ApplicationSetSpec{ 161 GoTemplate: true, 162 }, 163 } 164 165 results, err := Transform(argov1alpha1.ApplicationSetGenerator{ 166 Selector: testCase.selector, 167 List: &argov1alpha1.ListGenerator{ 168 Elements: testCase.elements, 169 Template: emptyTemplate(), 170 }}, 171 data, 172 emptyTemplate(), 173 &applicationSetInfo, nil) 174 175 assert.NoError(t, err) 176 assert.ElementsMatch(t, testCase.expected, results[0].Params) 177 }) 178 } 179 } 180 181 func TestTransForm(t *testing.T) { 182 testCases := []struct { 183 name string 184 selector *metav1.LabelSelector 185 expected []map[string]interface{} 186 }{ 187 { 188 name: "server filter", 189 selector: &metav1.LabelSelector{ 190 MatchLabels: map[string]string{"server": "https://production-01.example.com"}, 191 }, 192 expected: []map[string]interface{}{{ 193 "metadata.annotations.foo.argoproj.io": "production", 194 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", 195 "metadata.labels.environment": "production", 196 "metadata.labels.org": "bar", 197 "name": "production_01/west", 198 "nameNormalized": "production-01-west", 199 "server": "https://production-01.example.com", 200 }}, 201 }, 202 { 203 name: "server filter with long url", 204 selector: &metav1.LabelSelector{ 205 MatchLabels: map[string]string{"server": "https://some-really-long-url-that-will-exceed-63-characters.com"}, 206 }, 207 expected: []map[string]interface{}{{ 208 "metadata.annotations.foo.argoproj.io": "production", 209 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", 210 "metadata.labels.environment": "production", 211 "metadata.labels.org": "bar", 212 "name": "some-really-long-server-url", 213 "nameNormalized": "some-really-long-server-url", 214 "server": "https://some-really-long-url-that-will-exceed-63-characters.com", 215 }}, 216 }, 217 } 218 219 for _, testCase := range testCases { 220 t.Run(testCase.name, func(t *testing.T) { 221 testGenerators := map[string]Generator{ 222 "Clusters": getMockClusterGenerator(), 223 } 224 225 applicationSetInfo := argov1alpha1.ApplicationSet{ 226 ObjectMeta: metav1.ObjectMeta{ 227 Name: "set", 228 }, 229 Spec: argov1alpha1.ApplicationSetSpec{}, 230 } 231 232 results, err := Transform( 233 argov1alpha1.ApplicationSetGenerator{ 234 Selector: testCase.selector, 235 Clusters: &argov1alpha1.ClusterGenerator{ 236 Selector: metav1.LabelSelector{}, 237 Template: argov1alpha1.ApplicationSetTemplate{}, 238 Values: nil, 239 }}, 240 testGenerators, 241 emptyTemplate(), 242 &applicationSetInfo, nil) 243 244 assert.NoError(t, err) 245 assert.ElementsMatch(t, testCase.expected, results[0].Params) 246 }) 247 } 248 } 249 250 func emptyTemplate() argov1alpha1.ApplicationSetTemplate { 251 return argov1alpha1.ApplicationSetTemplate{ 252 Spec: argov1alpha1.ApplicationSpec{ 253 Project: "project", 254 }, 255 } 256 } 257 258 func getMockClusterGenerator() Generator { 259 clusters := []crtclient.Object{ 260 &corev1.Secret{ 261 TypeMeta: metav1.TypeMeta{ 262 Kind: "Secret", 263 APIVersion: "v1", 264 }, 265 ObjectMeta: metav1.ObjectMeta{ 266 Name: "staging-01", 267 Namespace: "namespace", 268 Labels: map[string]string{ 269 "argocd.argoproj.io/secret-type": "cluster", 270 "environment": "staging", 271 "org": "foo", 272 }, 273 Annotations: map[string]string{ 274 "foo.argoproj.io": "staging", 275 }, 276 }, 277 Data: map[string][]byte{ 278 "config": []byte("{}"), 279 "name": []byte("staging-01"), 280 "server": []byte("https://staging-01.example.com"), 281 }, 282 Type: corev1.SecretType("Opaque"), 283 }, 284 &corev1.Secret{ 285 TypeMeta: metav1.TypeMeta{ 286 Kind: "Secret", 287 APIVersion: "v1", 288 }, 289 ObjectMeta: metav1.ObjectMeta{ 290 Name: "production-01", 291 Namespace: "namespace", 292 Labels: map[string]string{ 293 "argocd.argoproj.io/secret-type": "cluster", 294 "environment": "production", 295 "org": "bar", 296 }, 297 Annotations: map[string]string{ 298 "foo.argoproj.io": "production", 299 }, 300 }, 301 Data: map[string][]byte{ 302 "config": []byte("{}"), 303 "name": []byte("production_01/west"), 304 "server": []byte("https://production-01.example.com"), 305 }, 306 Type: corev1.SecretType("Opaque"), 307 }, 308 &corev1.Secret{ 309 TypeMeta: metav1.TypeMeta{ 310 Kind: "Secret", 311 APIVersion: "v1", 312 }, 313 ObjectMeta: metav1.ObjectMeta{ 314 Name: "some-really-long-server-url", 315 Namespace: "namespace", 316 Labels: map[string]string{ 317 "argocd.argoproj.io/secret-type": "cluster", 318 "environment": "production", 319 "org": "bar", 320 }, 321 Annotations: map[string]string{ 322 "foo.argoproj.io": "production", 323 }, 324 }, 325 Data: map[string][]byte{ 326 "config": []byte("{}"), 327 "name": []byte("some-really-long-server-url"), 328 "server": []byte("https://some-really-long-url-that-will-exceed-63-characters.com"), 329 }, 330 Type: corev1.SecretType("Opaque"), 331 }, 332 } 333 runtimeClusters := []runtime.Object{} 334 for _, clientCluster := range clusters { 335 runtimeClusters = append(runtimeClusters, clientCluster) 336 } 337 appClientset := kubefake.NewSimpleClientset(runtimeClusters...) 338 339 fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build() 340 return NewClusterGenerator(fakeClient, context.Background(), appClientset, "namespace") 341 } 342 343 func getMockGitGenerator() Generator { 344 argoCDServiceMock := mocks.Repos{} 345 argoCDServiceMock.On("GetDirectories", mock.Anything, mock.Anything, mock.Anything).Return([]string{"app1", "app2", "app_3", "p1/app4"}, nil) 346 var gitGenerator = NewGitGenerator(&argoCDServiceMock) 347 return gitGenerator 348 } 349 350 func TestGetRelevantGenerators(t *testing.T) { 351 352 testGenerators := map[string]Generator{ 353 "Clusters": getMockClusterGenerator(), 354 "Git": getMockGitGenerator(), 355 } 356 357 testGenerators["Matrix"] = NewMatrixGenerator(testGenerators) 358 testGenerators["Merge"] = NewMergeGenerator(testGenerators) 359 testGenerators["List"] = NewListGenerator() 360 361 requestedGenerator := &argov1alpha1.ApplicationSetGenerator{ 362 List: &argov1alpha1.ListGenerator{ 363 Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}}, 364 }} 365 366 relevantGenerators := GetRelevantGenerators(requestedGenerator, testGenerators) 367 assert.Len(t, relevantGenerators, 1) 368 assert.IsType(t, &ListGenerator{}, relevantGenerators[0]) 369 370 requestedGenerator = &argov1alpha1.ApplicationSetGenerator{ 371 Clusters: &argov1alpha1.ClusterGenerator{ 372 Selector: metav1.LabelSelector{}, 373 Template: argov1alpha1.ApplicationSetTemplate{}, 374 Values: nil, 375 }, 376 } 377 378 relevantGenerators = GetRelevantGenerators(requestedGenerator, testGenerators) 379 assert.Len(t, relevantGenerators, 1) 380 assert.IsType(t, &ClusterGenerator{}, relevantGenerators[0]) 381 382 requestedGenerator = &argov1alpha1.ApplicationSetGenerator{ 383 Git: &argov1alpha1.GitGenerator{ 384 RepoURL: "", 385 Directories: nil, 386 Files: nil, 387 Revision: "", 388 RequeueAfterSeconds: nil, 389 Template: argov1alpha1.ApplicationSetTemplate{}, 390 }, 391 } 392 393 relevantGenerators = GetRelevantGenerators(requestedGenerator, testGenerators) 394 assert.Len(t, relevantGenerators, 1) 395 assert.IsType(t, &GitGenerator{}, relevantGenerators[0]) 396 } 397 398 func TestInterpolateGenerator(t *testing.T) { 399 requestedGenerator := &argov1alpha1.ApplicationSetGenerator{ 400 Clusters: &argov1alpha1.ClusterGenerator{ 401 Selector: metav1.LabelSelector{ 402 MatchLabels: map[string]string{ 403 "argocd.argoproj.io/secret-type": "cluster", 404 "path-basename": "{{path.basename}}", 405 "path-zero": "{{path[0]}}", 406 "path-full": "{{path}}", 407 }}, 408 }, 409 } 410 gitGeneratorParams := map[string]interface{}{ 411 "path": "p1/p2/app3", 412 "path.basename": "app3", 413 "path[0]": "p1", 414 "path[1]": "p2", 415 "path.basenameNormalized": "app3", 416 } 417 interpolatedGenerator, err := InterpolateGenerator(requestedGenerator, gitGeneratorParams, false, nil) 418 if err != nil { 419 log.WithError(err).WithField("requestedGenerator", requestedGenerator).Error("error interpolating Generator") 420 return 421 } 422 assert.Equal(t, "app3", interpolatedGenerator.Clusters.Selector.MatchLabels["path-basename"]) 423 assert.Equal(t, "p1", interpolatedGenerator.Clusters.Selector.MatchLabels["path-zero"]) 424 assert.Equal(t, "p1/p2/app3", interpolatedGenerator.Clusters.Selector.MatchLabels["path-full"]) 425 426 fileNamePath := argov1alpha1.GitFileGeneratorItem{ 427 Path: "{{name}}", 428 } 429 fileServerPath := argov1alpha1.GitFileGeneratorItem{ 430 Path: "{{server}}", 431 } 432 433 requestedGenerator = &argov1alpha1.ApplicationSetGenerator{ 434 Git: &argov1alpha1.GitGenerator{ 435 Files: append([]argov1alpha1.GitFileGeneratorItem{}, fileNamePath, fileServerPath), 436 Template: argov1alpha1.ApplicationSetTemplate{}, 437 }, 438 } 439 clusterGeneratorParams := map[string]interface{}{ 440 "name": "production_01/west", "server": "https://production-01.example.com", 441 } 442 interpolatedGenerator, err = InterpolateGenerator(requestedGenerator, clusterGeneratorParams, false, nil) 443 if err != nil { 444 log.WithError(err).WithField("requestedGenerator", requestedGenerator).Error("error interpolating Generator") 445 return 446 } 447 assert.Equal(t, "production_01/west", interpolatedGenerator.Git.Files[0].Path) 448 assert.Equal(t, "https://production-01.example.com", interpolatedGenerator.Git.Files[1].Path) 449 } 450 451 func TestInterpolateGenerator_go(t *testing.T) { 452 requestedGenerator := &argov1alpha1.ApplicationSetGenerator{ 453 Clusters: &argov1alpha1.ClusterGenerator{ 454 Selector: metav1.LabelSelector{ 455 MatchLabels: map[string]string{ 456 "argocd.argoproj.io/secret-type": "cluster", 457 "path-basename": "{{base .path.path}}", 458 "path-zero": "{{index .path.segments 0}}", 459 "path-full": "{{.path.path}}", 460 "kubernetes.io/environment": `{{default "foo" .my_label}}`, 461 }}, 462 }, 463 } 464 gitGeneratorParams := map[string]interface{}{ 465 "path": map[string]interface{}{ 466 "path": "p1/p2/app3", 467 "segments": []string{"p1", "p2", "app3"}, 468 }, 469 } 470 interpolatedGenerator, err := InterpolateGenerator(requestedGenerator, gitGeneratorParams, true, nil) 471 require.NoError(t, err) 472 if err != nil { 473 log.WithError(err).WithField("requestedGenerator", requestedGenerator).Error("error interpolating Generator") 474 return 475 } 476 assert.Equal(t, "app3", interpolatedGenerator.Clusters.Selector.MatchLabels["path-basename"]) 477 assert.Equal(t, "p1", interpolatedGenerator.Clusters.Selector.MatchLabels["path-zero"]) 478 assert.Equal(t, "p1/p2/app3", interpolatedGenerator.Clusters.Selector.MatchLabels["path-full"]) 479 480 fileNamePath := argov1alpha1.GitFileGeneratorItem{ 481 Path: "{{.name}}", 482 } 483 fileServerPath := argov1alpha1.GitFileGeneratorItem{ 484 Path: "{{.server}}", 485 } 486 487 requestedGenerator = &argov1alpha1.ApplicationSetGenerator{ 488 Git: &argov1alpha1.GitGenerator{ 489 Files: append([]argov1alpha1.GitFileGeneratorItem{}, fileNamePath, fileServerPath), 490 Template: argov1alpha1.ApplicationSetTemplate{}, 491 }, 492 } 493 clusterGeneratorParams := map[string]interface{}{ 494 "name": "production_01/west", "server": "https://production-01.example.com", 495 } 496 interpolatedGenerator, err = InterpolateGenerator(requestedGenerator, clusterGeneratorParams, true, nil) 497 if err != nil { 498 log.WithError(err).WithField("requestedGenerator", requestedGenerator).Error("error interpolating Generator") 499 return 500 } 501 assert.Equal(t, "production_01/west", interpolatedGenerator.Git.Files[0].Path) 502 assert.Equal(t, "https://production-01.example.com", interpolatedGenerator.Git.Files[1].Path) 503 } 504 505 func TestInterpolateGeneratorError(t *testing.T) { 506 type args struct { 507 requestedGenerator *argov1alpha1.ApplicationSetGenerator 508 params map[string]interface{} 509 useGoTemplate bool 510 goTemplateOptions []string 511 } 512 tests := []struct { 513 name string 514 args args 515 want argov1alpha1.ApplicationSetGenerator 516 expectedErrStr string 517 }{ 518 {name: "Empty Gen", args: args{ 519 requestedGenerator: nil, 520 params: nil, 521 useGoTemplate: false, 522 goTemplateOptions: nil, 523 }, want: argov1alpha1.ApplicationSetGenerator{}, expectedErrStr: "generator is empty"}, 524 {name: "No Params", args: args{ 525 requestedGenerator: &argov1alpha1.ApplicationSetGenerator{}, 526 params: map[string]interface{}{}, 527 useGoTemplate: false, 528 goTemplateOptions: nil, 529 }, want: argov1alpha1.ApplicationSetGenerator{}, expectedErrStr: ""}, 530 {name: "Error templating", args: args{ 531 requestedGenerator: &argov1alpha1.ApplicationSetGenerator{Git: &argov1alpha1.GitGenerator{ 532 RepoURL: "foo", 533 Files: []argov1alpha1.GitFileGeneratorItem{{Path: "bar/"}}, 534 Revision: "main", 535 Values: map[string]string{ 536 "git_test": "{{ toPrettyJson . }}", 537 "selection": "{{ default .override .test }}", 538 "resolved": "{{ index .rmap (default .override .test) }}", 539 }, 540 }}, 541 params: map[string]interface{}{ 542 "name": "in-cluster", 543 "override": "foo", 544 }, 545 useGoTemplate: true, 546 goTemplateOptions: []string{}, 547 }, want: argov1alpha1.ApplicationSetGenerator{}, expectedErrStr: "failed to replace parameters in generator: failed to execute go template {{ index .rmap (default .override .test) }}: template: :1:3: executing \"\" at <index .rmap (default .override .test)>: error calling index: index of untyped nil"}, 548 } 549 for _, tt := range tests { 550 t.Run(tt.name, func(t *testing.T) { 551 got, err := InterpolateGenerator(tt.args.requestedGenerator, tt.args.params, tt.args.useGoTemplate, tt.args.goTemplateOptions) 552 if tt.expectedErrStr != "" { 553 assert.EqualError(t, err, tt.expectedErrStr) 554 } else { 555 require.NoError(t, err) 556 } 557 assert.Equalf(t, tt.want, got, "InterpolateGenerator(%v, %v, %v, %v)", tt.args.requestedGenerator, tt.args.params, tt.args.useGoTemplate, tt.args.goTemplateOptions) 558 }) 559 } 560 }