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