github.com/argoproj/argo-cd/v2@v2.10.9/applicationset/generators/cluster_test.go (about) 1 package generators 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 corev1 "k8s.io/api/core/v1" 9 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 "k8s.io/apimachinery/pkg/runtime" 11 "sigs.k8s.io/controller-runtime/pkg/client" 12 "sigs.k8s.io/controller-runtime/pkg/client/fake" 13 14 kubefake "k8s.io/client-go/kubernetes/fake" 15 16 "github.com/argoproj/argo-cd/v2/applicationset/utils" 17 argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 18 19 "github.com/stretchr/testify/assert" 20 ) 21 22 type possiblyErroringFakeCtrlRuntimeClient struct { 23 client.Client 24 shouldError bool 25 } 26 27 func (p *possiblyErroringFakeCtrlRuntimeClient) List(ctx context.Context, secretList client.ObjectList, opts ...client.ListOption) error { 28 if p.shouldError { 29 return fmt.Errorf("could not list Secrets") 30 } 31 return p.Client.List(ctx, secretList, opts...) 32 } 33 34 func TestGenerateParams(t *testing.T) { 35 clusters := []client.Object{ 36 &corev1.Secret{ 37 TypeMeta: metav1.TypeMeta{ 38 Kind: "Secret", 39 APIVersion: "v1", 40 }, 41 ObjectMeta: metav1.ObjectMeta{ 42 Name: "staging-01", 43 Namespace: "namespace", 44 Labels: map[string]string{ 45 "argocd.argoproj.io/secret-type": "cluster", 46 "environment": "staging", 47 "org": "foo", 48 }, 49 Annotations: map[string]string{ 50 "foo.argoproj.io": "staging", 51 }, 52 }, 53 Data: map[string][]byte{ 54 "config": []byte("{}"), 55 "name": []byte("staging-01"), 56 "server": []byte("https://staging-01.example.com"), 57 }, 58 Type: corev1.SecretType("Opaque"), 59 }, 60 &corev1.Secret{ 61 TypeMeta: metav1.TypeMeta{ 62 Kind: "Secret", 63 APIVersion: "v1", 64 }, 65 ObjectMeta: metav1.ObjectMeta{ 66 Name: "production-01", 67 Namespace: "namespace", 68 Labels: map[string]string{ 69 "argocd.argoproj.io/secret-type": "cluster", 70 "environment": "production", 71 "org": "bar", 72 }, 73 Annotations: map[string]string{ 74 "foo.argoproj.io": "production", 75 }, 76 }, 77 Data: map[string][]byte{ 78 "config": []byte("{}"), 79 "name": []byte("production_01/west"), 80 "server": []byte("https://production-01.example.com"), 81 }, 82 Type: corev1.SecretType("Opaque"), 83 }, 84 } 85 testCases := []struct { 86 name string 87 selector metav1.LabelSelector 88 values map[string]string 89 expected []map[string]interface{} 90 // clientError is true if a k8s client error should be simulated 91 clientError bool 92 expectedError error 93 }{ 94 { 95 name: "no label selector", 96 selector: metav1.LabelSelector{}, 97 values: map[string]string{ 98 "lol1": "lol", 99 "lol2": "{{values.lol1}}{{values.lol1}}", 100 "lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", 101 "foo": "bar", 102 "bar": "{{ metadata.annotations.foo.argoproj.io }}", 103 "bat": "{{ metadata.labels.environment }}", 104 "aaa": "{{ server }}", 105 "no-op": "{{ this-does-not-exist }}", 106 }, expected: []map[string]interface{}{ 107 {"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "production", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "production", "values.aaa": "https://production-01.example.com", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar", 108 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production"}, 109 110 {"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "staging", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "staging", "values.aaa": "https://staging-01.example.com", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo", 111 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging"}, 112 113 {"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "{{ metadata.annotations.foo.argoproj.io }}", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "{{ metadata.labels.environment }}", "values.aaa": "https://kubernetes.default.svc", "nameNormalized": "in-cluster", "name": "in-cluster", "server": "https://kubernetes.default.svc"}, 114 }, 115 clientError: false, 116 expectedError: nil, 117 }, 118 { 119 name: "secret type label selector", 120 selector: metav1.LabelSelector{ 121 MatchLabels: map[string]string{ 122 "argocd.argoproj.io/secret-type": "cluster", 123 }, 124 }, 125 values: nil, 126 expected: []map[string]interface{}{ 127 {"name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar", 128 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production"}, 129 130 {"name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo", 131 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging"}, 132 }, 133 clientError: false, 134 expectedError: nil, 135 }, 136 { 137 name: "production-only", 138 selector: metav1.LabelSelector{ 139 MatchLabels: map[string]string{ 140 "environment": "production", 141 }, 142 }, 143 values: map[string]string{ 144 "foo": "bar", 145 }, 146 expected: []map[string]interface{}{ 147 {"values.foo": "bar", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar", 148 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production"}, 149 }, 150 clientError: false, 151 expectedError: nil, 152 }, 153 { 154 name: "production or staging", 155 selector: metav1.LabelSelector{ 156 MatchExpressions: []metav1.LabelSelectorRequirement{ 157 { 158 Key: "environment", 159 Operator: "In", 160 Values: []string{ 161 "production", 162 "staging", 163 }, 164 }, 165 }, 166 }, 167 values: map[string]string{ 168 "foo": "bar", 169 }, 170 expected: []map[string]interface{}{ 171 {"values.foo": "bar", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo", 172 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging"}, 173 {"values.foo": "bar", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar", 174 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production"}, 175 }, 176 clientError: false, 177 expectedError: nil, 178 }, 179 { 180 name: "production or staging with match labels", 181 selector: metav1.LabelSelector{ 182 MatchExpressions: []metav1.LabelSelectorRequirement{ 183 { 184 Key: "environment", 185 Operator: "In", 186 Values: []string{ 187 "production", 188 "staging", 189 }, 190 }, 191 }, 192 MatchLabels: map[string]string{ 193 "org": "foo", 194 }, 195 }, 196 values: map[string]string{ 197 "name": "baz", 198 }, 199 expected: []map[string]interface{}{ 200 {"values.name": "baz", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo", 201 "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging"}, 202 }, 203 clientError: false, 204 expectedError: nil, 205 }, 206 { 207 name: "simulate client error", 208 selector: metav1.LabelSelector{}, 209 values: nil, 210 expected: nil, 211 clientError: true, 212 expectedError: fmt.Errorf("could not list Secrets"), 213 }, 214 } 215 216 // convert []client.Object to []runtime.Object, for use by kubefake package 217 runtimeClusters := []runtime.Object{} 218 for _, clientCluster := range clusters { 219 runtimeClusters = append(runtimeClusters, clientCluster) 220 } 221 222 for _, testCase := range testCases { 223 224 t.Run(testCase.name, func(t *testing.T) { 225 226 appClientset := kubefake.NewSimpleClientset(runtimeClusters...) 227 228 fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build() 229 cl := &possiblyErroringFakeCtrlRuntimeClient{ 230 fakeClient, 231 testCase.clientError, 232 } 233 234 var clusterGenerator = NewClusterGenerator(cl, context.Background(), appClientset, "namespace") 235 236 applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ 237 ObjectMeta: metav1.ObjectMeta{ 238 Name: "set", 239 }, 240 Spec: argoprojiov1alpha1.ApplicationSetSpec{}, 241 } 242 243 got, err := clusterGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 244 Clusters: &argoprojiov1alpha1.ClusterGenerator{ 245 Selector: testCase.selector, 246 Values: testCase.values, 247 }, 248 }, &applicationSetInfo) 249 250 if testCase.expectedError != nil { 251 assert.EqualError(t, err, testCase.expectedError.Error()) 252 } else { 253 assert.NoError(t, err) 254 assert.ElementsMatch(t, testCase.expected, got) 255 } 256 257 }) 258 } 259 } 260 261 func TestGenerateParamsGoTemplate(t *testing.T) { 262 clusters := []client.Object{ 263 &corev1.Secret{ 264 TypeMeta: metav1.TypeMeta{ 265 Kind: "Secret", 266 APIVersion: "v1", 267 }, 268 ObjectMeta: metav1.ObjectMeta{ 269 Name: "staging-01", 270 Namespace: "namespace", 271 Labels: map[string]string{ 272 "argocd.argoproj.io/secret-type": "cluster", 273 "environment": "staging", 274 "org": "foo", 275 }, 276 Annotations: map[string]string{ 277 "foo.argoproj.io": "staging", 278 }, 279 }, 280 Data: map[string][]byte{ 281 "config": []byte("{}"), 282 "name": []byte("staging-01"), 283 "server": []byte("https://staging-01.example.com"), 284 }, 285 Type: corev1.SecretType("Opaque"), 286 }, 287 &corev1.Secret{ 288 TypeMeta: metav1.TypeMeta{ 289 Kind: "Secret", 290 APIVersion: "v1", 291 }, 292 ObjectMeta: metav1.ObjectMeta{ 293 Name: "production-01", 294 Namespace: "namespace", 295 Labels: map[string]string{ 296 "argocd.argoproj.io/secret-type": "cluster", 297 "environment": "production", 298 "org": "bar", 299 }, 300 Annotations: map[string]string{ 301 "foo.argoproj.io": "production", 302 }, 303 }, 304 Data: map[string][]byte{ 305 "config": []byte("{}"), 306 "name": []byte("production_01/west"), 307 "server": []byte("https://production-01.example.com"), 308 }, 309 Type: corev1.SecretType("Opaque"), 310 }, 311 } 312 testCases := []struct { 313 name string 314 selector metav1.LabelSelector 315 values map[string]string 316 expected []map[string]interface{} 317 // clientError is true if a k8s client error should be simulated 318 clientError bool 319 expectedError error 320 }{ 321 { 322 name: "no label selector", 323 selector: metav1.LabelSelector{}, 324 values: map[string]string{ 325 "lol1": "lol", 326 "lol2": "{{ .values.lol1 }}{{ .values.lol1 }}", 327 "lol3": "{{ .values.lol2 }}{{ .values.lol2 }}{{ .values.lol2 }}", 328 "foo": "bar", 329 "bar": "{{ if not (empty .metadata) }}{{index .metadata.annotations \"foo.argoproj.io\" }}{{ end }}", 330 "bat": "{{ if not (empty .metadata) }}{{.metadata.labels.environment}}{{ end }}", 331 "aaa": "{{ .server }}", 332 "no-op": "{{ .thisDoesNotExist }}", 333 }, expected: []map[string]interface{}{ 334 { 335 "name": "production_01/west", 336 "nameNormalized": "production-01-west", 337 "server": "https://production-01.example.com", 338 "metadata": map[string]interface{}{ 339 "labels": map[string]string{ 340 "argocd.argoproj.io/secret-type": "cluster", 341 "environment": "production", 342 "org": "bar", 343 }, 344 "annotations": map[string]string{ 345 "foo.argoproj.io": "production", 346 }, 347 }, 348 "values": map[string]string{ 349 "lol1": "lol", 350 "lol2": "<no value><no value>", 351 "lol3": "<no value><no value><no value>", 352 "foo": "bar", 353 "bar": "production", 354 "bat": "production", 355 "aaa": "https://production-01.example.com", 356 "no-op": "<no value>", 357 }, 358 }, 359 { 360 "name": "staging-01", 361 "nameNormalized": "staging-01", 362 "server": "https://staging-01.example.com", 363 "metadata": map[string]interface{}{ 364 "labels": map[string]string{ 365 "argocd.argoproj.io/secret-type": "cluster", 366 "environment": "staging", 367 "org": "foo", 368 }, 369 "annotations": map[string]string{ 370 "foo.argoproj.io": "staging", 371 }, 372 }, 373 "values": map[string]string{ 374 "lol1": "lol", 375 "lol2": "<no value><no value>", 376 "lol3": "<no value><no value><no value>", 377 "foo": "bar", 378 "bar": "staging", 379 "bat": "staging", 380 "aaa": "https://staging-01.example.com", 381 "no-op": "<no value>", 382 }, 383 }, 384 { 385 "nameNormalized": "in-cluster", 386 "name": "in-cluster", 387 "server": "https://kubernetes.default.svc", 388 "values": map[string]string{ 389 "lol1": "lol", 390 "lol2": "<no value><no value>", 391 "lol3": "<no value><no value><no value>", 392 "foo": "bar", 393 "bar": "", 394 "bat": "", 395 "aaa": "https://kubernetes.default.svc", 396 "no-op": "<no value>", 397 }, 398 }, 399 }, 400 clientError: false, 401 expectedError: nil, 402 }, 403 { 404 name: "secret type label selector", 405 selector: metav1.LabelSelector{ 406 MatchLabels: map[string]string{ 407 "argocd.argoproj.io/secret-type": "cluster", 408 }, 409 }, 410 values: nil, 411 expected: []map[string]interface{}{ 412 { 413 "name": "production_01/west", 414 "nameNormalized": "production-01-west", 415 "server": "https://production-01.example.com", 416 "metadata": map[string]interface{}{ 417 "labels": map[string]string{ 418 "argocd.argoproj.io/secret-type": "cluster", 419 "environment": "production", 420 "org": "bar", 421 }, 422 "annotations": map[string]string{ 423 "foo.argoproj.io": "production", 424 }, 425 }, 426 }, 427 { 428 "name": "staging-01", 429 "nameNormalized": "staging-01", 430 "server": "https://staging-01.example.com", 431 "metadata": map[string]interface{}{ 432 "labels": map[string]string{ 433 "argocd.argoproj.io/secret-type": "cluster", 434 "environment": "staging", 435 "org": "foo", 436 }, 437 "annotations": map[string]string{ 438 "foo.argoproj.io": "staging", 439 }, 440 }, 441 }, 442 }, 443 clientError: false, 444 expectedError: nil, 445 }, 446 { 447 name: "production-only", 448 selector: metav1.LabelSelector{ 449 MatchLabels: map[string]string{ 450 "environment": "production", 451 }, 452 }, 453 values: map[string]string{ 454 "foo": "bar", 455 }, 456 expected: []map[string]interface{}{ 457 { 458 "name": "production_01/west", 459 "nameNormalized": "production-01-west", 460 "server": "https://production-01.example.com", 461 "metadata": map[string]interface{}{ 462 "labels": map[string]string{ 463 "argocd.argoproj.io/secret-type": "cluster", 464 "environment": "production", 465 "org": "bar", 466 }, 467 "annotations": map[string]string{ 468 "foo.argoproj.io": "production", 469 }, 470 }, 471 "values": map[string]string{ 472 "foo": "bar", 473 }, 474 }, 475 }, 476 clientError: false, 477 expectedError: nil, 478 }, 479 { 480 name: "production or staging", 481 selector: metav1.LabelSelector{ 482 MatchExpressions: []metav1.LabelSelectorRequirement{ 483 { 484 Key: "environment", 485 Operator: "In", 486 Values: []string{ 487 "production", 488 "staging", 489 }, 490 }, 491 }, 492 }, 493 values: map[string]string{ 494 "foo": "bar", 495 }, 496 expected: []map[string]interface{}{ 497 { 498 "name": "production_01/west", 499 "nameNormalized": "production-01-west", 500 "server": "https://production-01.example.com", 501 "metadata": map[string]interface{}{ 502 "labels": map[string]string{ 503 "argocd.argoproj.io/secret-type": "cluster", 504 "environment": "production", 505 "org": "bar", 506 }, 507 "annotations": map[string]string{ 508 "foo.argoproj.io": "production", 509 }, 510 }, 511 "values": map[string]string{ 512 "foo": "bar", 513 }, 514 }, 515 { 516 "name": "staging-01", 517 "nameNormalized": "staging-01", 518 "server": "https://staging-01.example.com", 519 "metadata": map[string]interface{}{ 520 "labels": map[string]string{ 521 "argocd.argoproj.io/secret-type": "cluster", 522 "environment": "staging", 523 "org": "foo", 524 }, 525 "annotations": map[string]string{ 526 "foo.argoproj.io": "staging", 527 }, 528 }, 529 "values": map[string]string{ 530 "foo": "bar", 531 }, 532 }, 533 }, 534 clientError: false, 535 expectedError: nil, 536 }, 537 { 538 name: "production or staging with match labels", 539 selector: metav1.LabelSelector{ 540 MatchExpressions: []metav1.LabelSelectorRequirement{ 541 { 542 Key: "environment", 543 Operator: "In", 544 Values: []string{ 545 "production", 546 "staging", 547 }, 548 }, 549 }, 550 MatchLabels: map[string]string{ 551 "org": "foo", 552 }, 553 }, 554 values: map[string]string{ 555 "name": "baz", 556 }, 557 expected: []map[string]interface{}{ 558 { 559 "name": "staging-01", 560 "nameNormalized": "staging-01", 561 "server": "https://staging-01.example.com", 562 "metadata": map[string]interface{}{ 563 "labels": map[string]string{ 564 "argocd.argoproj.io/secret-type": "cluster", 565 "environment": "staging", 566 "org": "foo", 567 }, 568 "annotations": map[string]string{ 569 "foo.argoproj.io": "staging", 570 }, 571 }, 572 "values": map[string]string{ 573 "name": "baz", 574 }, 575 }, 576 }, 577 clientError: false, 578 expectedError: nil, 579 }, 580 { 581 name: "simulate client error", 582 selector: metav1.LabelSelector{}, 583 values: nil, 584 expected: nil, 585 clientError: true, 586 expectedError: fmt.Errorf("could not list Secrets"), 587 }, 588 } 589 590 // convert []client.Object to []runtime.Object, for use by kubefake package 591 runtimeClusters := []runtime.Object{} 592 for _, clientCluster := range clusters { 593 runtimeClusters = append(runtimeClusters, clientCluster) 594 } 595 596 for _, testCase := range testCases { 597 598 t.Run(testCase.name, func(t *testing.T) { 599 600 appClientset := kubefake.NewSimpleClientset(runtimeClusters...) 601 602 fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build() 603 cl := &possiblyErroringFakeCtrlRuntimeClient{ 604 fakeClient, 605 testCase.clientError, 606 } 607 608 var clusterGenerator = NewClusterGenerator(cl, context.Background(), appClientset, "namespace") 609 610 applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ 611 ObjectMeta: metav1.ObjectMeta{ 612 Name: "set", 613 }, 614 Spec: argoprojiov1alpha1.ApplicationSetSpec{ 615 GoTemplate: true, 616 }, 617 } 618 619 got, err := clusterGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{ 620 Clusters: &argoprojiov1alpha1.ClusterGenerator{ 621 Selector: testCase.selector, 622 Values: testCase.values, 623 }, 624 }, &applicationSetInfo) 625 626 if testCase.expectedError != nil { 627 assert.EqualError(t, err, testCase.expectedError.Error()) 628 } else { 629 assert.NoError(t, err) 630 assert.ElementsMatch(t, testCase.expected, got) 631 } 632 633 }) 634 } 635 } 636 637 func TestSanitizeClusterName(t *testing.T) { 638 t.Run("valid DNS-1123 subdomain name", func(t *testing.T) { 639 assert.Equal(t, "cluster-name", utils.SanitizeName("cluster-name")) 640 }) 641 t.Run("invalid DNS-1123 subdomain name", func(t *testing.T) { 642 invalidName := "-.--CLUSTER/name -./.-" 643 assert.Equal(t, "cluster-name", utils.SanitizeName(invalidName)) 644 }) 645 }