github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/generators/plugin_test.go (about) 1 package generators 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/http/httptest" 9 "strings" 10 "testing" 11 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 corev1 "k8s.io/api/core/v1" 15 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "sigs.k8s.io/controller-runtime/pkg/client" 18 "sigs.k8s.io/controller-runtime/pkg/client/fake" 19 20 "github.com/argoproj/argo-cd/v3/applicationset/services/plugin" 21 argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 22 ) 23 24 func TestPluginGenerateParams(t *testing.T) { 25 testCases := []struct { 26 name string 27 configmap *corev1.ConfigMap 28 secret *corev1.Secret 29 inputParameters map[string]apiextensionsv1.JSON 30 values map[string]string 31 gotemplate bool 32 expected []map[string]any 33 content []byte 34 expectedError error 35 }{ 36 { 37 name: "simple case", 38 configmap: &corev1.ConfigMap{ 39 ObjectMeta: metav1.ObjectMeta{ 40 Name: "first-plugin-cm", 41 Namespace: "default", 42 }, 43 Data: map[string]string{ 44 "baseUrl": "http://127.0.0.1", 45 "token": "$plugin.token", 46 }, 47 }, 48 secret: &corev1.Secret{ 49 ObjectMeta: metav1.ObjectMeta{ 50 Name: "argocd-secret", 51 Namespace: "default", 52 }, 53 Data: map[string][]byte{ 54 "plugin.token": []byte("my-secret"), 55 }, 56 }, 57 inputParameters: map[string]apiextensionsv1.JSON{ 58 "pkey1": {Raw: []byte(`"val1"`)}, 59 "pkey2": {Raw: []byte(`"val2"`)}, 60 }, 61 gotemplate: false, 62 content: []byte(`{"output": { 63 "parameters": [{ 64 "key1": "val1", 65 "key2": { 66 "key2_1": "val2_1", 67 "key2_2": { 68 "key2_2_1": "val2_2_1" 69 } 70 }, 71 "key3": 123 72 }] 73 }}`), 74 expected: []map[string]any{ 75 { 76 "key1": "val1", 77 "key2.key2_1": "val2_1", 78 "key2.key2_2.key2_2_1": "val2_2_1", 79 "key3": "123", 80 "generator": map[string]any{ 81 "input": argoprojiov1alpha1.PluginInput{ 82 Parameters: argoprojiov1alpha1.PluginParameters{ 83 "pkey1": {Raw: []byte(`"val1"`)}, 84 "pkey2": {Raw: []byte(`"val2"`)}, 85 }, 86 }, 87 }, 88 }, 89 }, 90 expectedError: nil, 91 }, 92 { 93 name: "simple case with values", 94 configmap: &corev1.ConfigMap{ 95 ObjectMeta: metav1.ObjectMeta{ 96 Name: "first-plugin-cm", 97 Namespace: "default", 98 }, 99 Data: map[string]string{ 100 "baseUrl": "http://127.0.0.1", 101 "token": "$plugin.token", 102 }, 103 }, 104 secret: &corev1.Secret{ 105 ObjectMeta: metav1.ObjectMeta{ 106 Name: "argocd-secret", 107 Namespace: "default", 108 }, 109 Data: map[string][]byte{ 110 "plugin.token": []byte("my-secret"), 111 }, 112 }, 113 inputParameters: map[string]apiextensionsv1.JSON{ 114 "pkey1": {Raw: []byte(`"val1"`)}, 115 "pkey2": {Raw: []byte(`"val2"`)}, 116 }, 117 values: map[string]string{ 118 "valuekey1": "valuevalue1", 119 "valuekey2": "templated-{{key1}}", 120 }, 121 gotemplate: false, 122 content: []byte(`{"output": { 123 "parameters": [{ 124 "key1": "val1", 125 "key2": { 126 "key2_1": "val2_1", 127 "key2_2": { 128 "key2_2_1": "val2_2_1" 129 } 130 }, 131 "key3": 123 132 }] 133 }}`), 134 expected: []map[string]any{ 135 { 136 "key1": "val1", 137 "key2.key2_1": "val2_1", 138 "key2.key2_2.key2_2_1": "val2_2_1", 139 "key3": "123", 140 "values.valuekey1": "valuevalue1", 141 "values.valuekey2": "templated-val1", 142 "generator": map[string]any{ 143 "input": argoprojiov1alpha1.PluginInput{ 144 Parameters: argoprojiov1alpha1.PluginParameters{ 145 "pkey1": {Raw: []byte(`"val1"`)}, 146 "pkey2": {Raw: []byte(`"val2"`)}, 147 }, 148 }, 149 }, 150 }, 151 }, 152 expectedError: nil, 153 }, 154 { 155 name: "simple case with gotemplate", 156 configmap: &corev1.ConfigMap{ 157 ObjectMeta: metav1.ObjectMeta{ 158 Name: "first-plugin-cm", 159 Namespace: "default", 160 }, 161 Data: map[string]string{ 162 "baseUrl": "http://127.0.0.1", 163 "token": "$plugin.token", 164 }, 165 }, 166 secret: &corev1.Secret{ 167 ObjectMeta: metav1.ObjectMeta{ 168 Name: "argocd-secret", 169 Namespace: "default", 170 }, 171 Data: map[string][]byte{ 172 "plugin.token": []byte("my-secret"), 173 }, 174 }, 175 inputParameters: map[string]apiextensionsv1.JSON{ 176 "pkey1": {Raw: []byte(`"val1"`)}, 177 "pkey2": {Raw: []byte(`"val2"`)}, 178 }, 179 gotemplate: true, 180 content: []byte(`{"output": { 181 "parameters": [{ 182 "key1": "val1", 183 "key2": { 184 "key2_1": "val2_1", 185 "key2_2": { 186 "key2_2_1": "val2_2_1" 187 } 188 }, 189 "key3": 123 190 }] 191 }}`), 192 expected: []map[string]any{ 193 { 194 "key1": "val1", 195 "key2": map[string]any{ 196 "key2_1": "val2_1", 197 "key2_2": map[string]any{ 198 "key2_2_1": "val2_2_1", 199 }, 200 }, 201 "key3": float64(123), 202 "generator": map[string]any{ 203 "input": argoprojiov1alpha1.PluginInput{ 204 Parameters: argoprojiov1alpha1.PluginParameters{ 205 "pkey1": {Raw: []byte(`"val1"`)}, 206 "pkey2": {Raw: []byte(`"val2"`)}, 207 }, 208 }, 209 }, 210 }, 211 }, 212 expectedError: nil, 213 }, 214 { 215 name: "simple case with appended params", 216 configmap: &corev1.ConfigMap{ 217 ObjectMeta: metav1.ObjectMeta{ 218 Name: "first-plugin-cm", 219 Namespace: "default", 220 }, 221 Data: map[string]string{ 222 "baseUrl": "http://127.0.0.1", 223 "token": "$plugin.token", 224 }, 225 }, 226 secret: &corev1.Secret{ 227 ObjectMeta: metav1.ObjectMeta{ 228 Name: "argocd-secret", 229 Namespace: "default", 230 }, 231 Data: map[string][]byte{ 232 "plugin.token": []byte("my-secret"), 233 }, 234 }, 235 inputParameters: map[string]apiextensionsv1.JSON{ 236 "pkey1": {Raw: []byte(`"val1"`)}, 237 "pkey2": {Raw: []byte(`"val2"`)}, 238 }, 239 gotemplate: false, 240 content: []byte(`{"output": {"parameters": [{ 241 "key1": "val1", 242 "key2": { 243 "key2_1": "val2_1", 244 "key2_2": { 245 "key2_2_1": "val2_2_1" 246 } 247 }, 248 "key3": 123, 249 "pkey2": "valplugin" 250 }]}}`), 251 expected: []map[string]any{ 252 { 253 "key1": "val1", 254 "key2.key2_1": "val2_1", 255 "key2.key2_2.key2_2_1": "val2_2_1", 256 "key3": "123", 257 "pkey2": "valplugin", 258 "generator": map[string]any{ 259 "input": argoprojiov1alpha1.PluginInput{ 260 Parameters: argoprojiov1alpha1.PluginParameters{ 261 "pkey1": {Raw: []byte(`"val1"`)}, 262 "pkey2": {Raw: []byte(`"val2"`)}, 263 }, 264 }, 265 }, 266 }, 267 }, 268 expectedError: nil, 269 }, 270 { 271 name: "no params", 272 configmap: &corev1.ConfigMap{ 273 ObjectMeta: metav1.ObjectMeta{ 274 Name: "first-plugin-cm", 275 Namespace: "default", 276 }, 277 Data: map[string]string{ 278 "baseUrl": "http://127.0.0.1", 279 "token": "$plugin.token", 280 }, 281 }, 282 secret: &corev1.Secret{ 283 ObjectMeta: metav1.ObjectMeta{ 284 Name: "argocd-secret", 285 Namespace: "default", 286 }, 287 Data: map[string][]byte{ 288 "plugin.token": []byte("my-secret"), 289 }, 290 }, 291 inputParameters: argoprojiov1alpha1.PluginParameters{}, 292 gotemplate: false, 293 content: []byte(`{"output": { 294 "parameters": [{ 295 "key1": "val1", 296 "key2": { 297 "key2_1": "val2_1", 298 "key2_2": { 299 "key2_2_1": "val2_2_1" 300 } 301 }, 302 "key3": 123 303 }] 304 }}`), 305 expected: []map[string]any{ 306 { 307 "key1": "val1", 308 "key2.key2_1": "val2_1", 309 "key2.key2_2.key2_2_1": "val2_2_1", 310 "key3": "123", 311 "generator": map[string]any{ 312 "input": map[string]map[string]any{ 313 "parameters": {}, 314 }, 315 }, 316 }, 317 }, 318 expectedError: nil, 319 }, 320 { 321 name: "empty return", 322 configmap: &corev1.ConfigMap{ 323 ObjectMeta: metav1.ObjectMeta{ 324 Name: "first-plugin-cm", 325 Namespace: "default", 326 }, 327 Data: map[string]string{ 328 "baseUrl": "http://127.0.0.1", 329 "token": "$plugin.token", 330 }, 331 }, 332 secret: &corev1.Secret{ 333 ObjectMeta: metav1.ObjectMeta{ 334 Name: "argocd-secret", 335 Namespace: "default", 336 }, 337 Data: map[string][]byte{ 338 "plugin.token": []byte("my-secret"), 339 }, 340 }, 341 inputParameters: map[string]apiextensionsv1.JSON{}, 342 gotemplate: false, 343 content: []byte(`{"input": {"parameters": []}}`), 344 expected: []map[string]any{}, 345 expectedError: nil, 346 }, 347 { 348 name: "wrong return", 349 configmap: &corev1.ConfigMap{ 350 ObjectMeta: metav1.ObjectMeta{ 351 Name: "first-plugin-cm", 352 Namespace: "default", 353 }, 354 Data: map[string]string{ 355 "baseUrl": "http://127.0.0.1", 356 "token": "$plugin.token", 357 }, 358 }, 359 secret: &corev1.Secret{ 360 ObjectMeta: metav1.ObjectMeta{ 361 Name: "argocd-secret", 362 Namespace: "default", 363 }, 364 Data: map[string][]byte{ 365 "plugin.token": []byte("my-secret"), 366 }, 367 }, 368 inputParameters: map[string]apiextensionsv1.JSON{}, 369 gotemplate: false, 370 content: []byte(`wrong body ...`), 371 expected: []map[string]any{}, 372 expectedError: errors.New("error listing params: error get api 'set': invalid character 'w' looking for beginning of value: wrong body ..."), 373 }, 374 { 375 name: "external secret", 376 configmap: &corev1.ConfigMap{ 377 ObjectMeta: metav1.ObjectMeta{ 378 Name: "first-plugin-cm", 379 Namespace: "default", 380 }, 381 Data: map[string]string{ 382 "baseUrl": "http://127.0.0.1", 383 "token": "$plugin-secret:plugin.token", 384 }, 385 }, 386 secret: &corev1.Secret{ 387 ObjectMeta: metav1.ObjectMeta{ 388 Name: "plugin-secret", 389 Namespace: "default", 390 }, 391 Data: map[string][]byte{ 392 "plugin.token": []byte("my-secret"), 393 }, 394 }, 395 inputParameters: map[string]apiextensionsv1.JSON{ 396 "pkey1": {Raw: []byte(`"val1"`)}, 397 "pkey2": {Raw: []byte(`"val2"`)}, 398 }, 399 gotemplate: false, 400 content: []byte(`{"output": {"parameters": [{ 401 "key1": "val1", 402 "key2": { 403 "key2_1": "val2_1", 404 "key2_2": { 405 "key2_2_1": "val2_2_1" 406 } 407 }, 408 "key3": 123, 409 "pkey2": "valplugin" 410 }]}}`), 411 expected: []map[string]any{ 412 { 413 "key1": "val1", 414 "key2.key2_1": "val2_1", 415 "key2.key2_2.key2_2_1": "val2_2_1", 416 "key3": "123", 417 "pkey2": "valplugin", 418 "generator": map[string]any{ 419 "input": argoprojiov1alpha1.PluginInput{ 420 Parameters: argoprojiov1alpha1.PluginParameters{ 421 "pkey1": {Raw: []byte(`"val1"`)}, 422 "pkey2": {Raw: []byte(`"val2"`)}, 423 }, 424 }, 425 }, 426 }, 427 }, 428 expectedError: nil, 429 }, 430 { 431 name: "no secret", 432 configmap: &corev1.ConfigMap{ 433 ObjectMeta: metav1.ObjectMeta{ 434 Name: "first-plugin-cm", 435 Namespace: "default", 436 }, 437 Data: map[string]string{ 438 "baseUrl": "http://127.0.0.1", 439 "token": "$plugin.token", 440 }, 441 }, 442 secret: &corev1.Secret{}, 443 inputParameters: map[string]apiextensionsv1.JSON{ 444 "pkey1": {Raw: []byte(`"val1"`)}, 445 "pkey2": {Raw: []byte(`"val2"`)}, 446 }, 447 gotemplate: false, 448 content: []byte(`{"output": { 449 "parameters": [{ 450 "key1": "val1", 451 "key2": { 452 "key2_1": "val2_1", 453 "key2_2": { 454 "key2_2_1": "val2_2_1" 455 } 456 }, 457 "key3": 123 458 }] 459 }}`), 460 expected: []map[string]any{ 461 { 462 "key1": "val1", 463 "key2.key2_1": "val2_1", 464 "key2.key2_2.key2_2_1": "val2_2_1", 465 "key3": "123", 466 "generator": map[string]any{ 467 "input": argoprojiov1alpha1.PluginInput{ 468 Parameters: argoprojiov1alpha1.PluginParameters{ 469 "pkey1": {Raw: []byte(`"val1"`)}, 470 "pkey2": {Raw: []byte(`"val2"`)}, 471 }, 472 }, 473 }, 474 }, 475 }, 476 expectedError: errors.New("error getting plugin from generator: error fetching Secret token: error fetching secret default/argocd-secret: secrets \"argocd-secret\" not found"), 477 }, 478 { 479 name: "no configmap", 480 configmap: &corev1.ConfigMap{}, 481 secret: &corev1.Secret{ 482 ObjectMeta: metav1.ObjectMeta{ 483 Name: "argocd-secret", 484 Namespace: "default", 485 }, 486 Data: map[string][]byte{ 487 "plugin.token": []byte("my-secret"), 488 }, 489 }, 490 inputParameters: map[string]apiextensionsv1.JSON{ 491 "pkey1": {Raw: []byte(`"val1"`)}, 492 "pkey2": {Raw: []byte(`"val2"`)}, 493 }, 494 gotemplate: false, 495 content: []byte(`{"output": { 496 "parameters": [{ 497 "key1": "val1", 498 "key2": { 499 "key2_1": "val2_1", 500 "key2_2": { 501 "key2_2_1": "val2_2_1" 502 } 503 }, 504 "key3": 123 505 }] 506 }}`), 507 expected: []map[string]any{ 508 { 509 "key1": "val1", 510 "key2.key2_1": "val2_1", 511 "key2.key2_2.key2_2_1": "val2_2_1", 512 "key3": "123", 513 "generator": map[string]any{ 514 "input": argoprojiov1alpha1.PluginInput{ 515 Parameters: argoprojiov1alpha1.PluginParameters{ 516 "pkey1": {Raw: []byte(`"val1"`)}, 517 "pkey2": {Raw: []byte(`"val2"`)}, 518 }, 519 }, 520 }, 521 }, 522 }, 523 expectedError: errors.New("error getting plugin from generator: error fetching ConfigMap: configmaps \"\" not found"), 524 }, 525 { 526 name: "no baseUrl", 527 configmap: &corev1.ConfigMap{ 528 ObjectMeta: metav1.ObjectMeta{ 529 Name: "first-plugin-cm", 530 Namespace: "default", 531 }, 532 Data: map[string]string{ 533 "token": "$plugin.token", 534 }, 535 }, 536 secret: &corev1.Secret{ 537 ObjectMeta: metav1.ObjectMeta{ 538 Name: "argocd-secret", 539 Namespace: "default", 540 }, 541 Data: map[string][]byte{ 542 "plugin.token": []byte("my-secret"), 543 }, 544 }, 545 inputParameters: map[string]apiextensionsv1.JSON{ 546 "pkey1": {Raw: []byte(`"val1"`)}, 547 "pkey2": {Raw: []byte(`"val2"`)}, 548 }, 549 gotemplate: false, 550 content: []byte(`{"output": { 551 "parameters": [{ 552 "key1": "val1", 553 "key2": { 554 "key2_1": "val2_1", 555 "key2_2": { 556 "key2_2_1": "val2_2_1" 557 } 558 }, 559 "key3": 123 560 }] 561 }}`), 562 expected: []map[string]any{ 563 { 564 "key1": "val1", 565 "key2.key2_1": "val2_1", 566 "key2.key2_2.key2_2_1": "val2_2_1", 567 "key3": "123", 568 "generator": map[string]any{ 569 "input": argoprojiov1alpha1.PluginInput{ 570 Parameters: argoprojiov1alpha1.PluginParameters{ 571 "pkey1": {Raw: []byte(`"val1"`)}, 572 "pkey2": {Raw: []byte(`"val2"`)}, 573 }, 574 }, 575 }, 576 }, 577 }, 578 expectedError: errors.New("error getting plugin from generator: error fetching ConfigMap: baseUrl not found in ConfigMap"), 579 }, 580 { 581 name: "no token", 582 configmap: &corev1.ConfigMap{ 583 ObjectMeta: metav1.ObjectMeta{ 584 Name: "first-plugin-cm", 585 Namespace: "default", 586 }, 587 Data: map[string]string{ 588 "baseUrl": "http://127.0.0.1", 589 }, 590 }, 591 secret: &corev1.Secret{}, 592 inputParameters: map[string]apiextensionsv1.JSON{ 593 "pkey1": {Raw: []byte(`"val1"`)}, 594 "pkey2": {Raw: []byte(`"val2"`)}, 595 }, 596 gotemplate: false, 597 content: []byte(`{"output": { 598 "parameters": [{ 599 "key1": "val1", 600 "key2": { 601 "key2_1": "val2_1", 602 "key2_2": { 603 "key2_2_1": "val2_2_1" 604 } 605 }, 606 "key3": 123 607 }] 608 }}`), 609 expected: []map[string]any{ 610 { 611 "key1": "val1", 612 "key2.key2_1": "val2_1", 613 "key2.key2_2.key2_2_1": "val2_2_1", 614 "key3": "123", 615 "generator": map[string]any{ 616 "input": argoprojiov1alpha1.PluginInput{ 617 Parameters: argoprojiov1alpha1.PluginParameters{ 618 "pkey1": {Raw: []byte(`"val1"`)}, 619 "pkey2": {Raw: []byte(`"val2"`)}, 620 }, 621 }, 622 }, 623 }, 624 }, 625 expectedError: errors.New("error getting plugin from generator: error fetching ConfigMap: token not found in ConfigMap"), 626 }, 627 } 628 629 for _, testCase := range testCases { 630 t.Run(testCase.name, func(t *testing.T) { 631 generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{ 632 Plugin: &argoprojiov1alpha1.PluginGenerator{ 633 ConfigMapRef: argoprojiov1alpha1.PluginConfigMapRef{Name: testCase.configmap.Name}, 634 Input: argoprojiov1alpha1.PluginInput{ 635 Parameters: testCase.inputParameters, 636 }, 637 Values: testCase.values, 638 }, 639 } 640 641 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 642 authHeader := r.Header.Get("Authorization") 643 _, tokenKey := plugin.ParseSecretKey(testCase.configmap.Data["token"]) 644 expectedToken := testCase.secret.Data[strings.ReplaceAll(tokenKey, "$", "")] 645 if authHeader != "Bearer "+string(expectedToken) { 646 w.WriteHeader(http.StatusUnauthorized) 647 return 648 } 649 650 w.Header().Set("Content-Type", "application/json") 651 _, err := w.Write(testCase.content) 652 if err != nil { 653 require.NoError(t, fmt.Errorf("Error Write %w", err)) 654 } 655 }) 656 657 fakeServer := httptest.NewServer(handler) 658 659 defer fakeServer.Close() 660 661 if _, ok := testCase.configmap.Data["baseUrl"]; ok { 662 testCase.configmap.Data["baseUrl"] = fakeServer.URL 663 } 664 665 fakeClientWithCache := fake.NewClientBuilder().WithObjects([]client.Object{testCase.configmap, testCase.secret}...).Build() 666 667 pluginGenerator := NewPluginGenerator(fakeClientWithCache, "default") 668 669 applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ 670 ObjectMeta: metav1.ObjectMeta{ 671 Name: "set", 672 }, 673 Spec: argoprojiov1alpha1.ApplicationSetSpec{ 674 GoTemplate: testCase.gotemplate, 675 }, 676 } 677 678 got, err := pluginGenerator.GenerateParams(&generatorConfig, &applicationSetInfo, nil) 679 if err != nil { 680 fmt.Println(err) 681 } 682 683 if testCase.expectedError != nil { 684 require.EqualError(t, err, testCase.expectedError.Error()) 685 } else { 686 require.NoError(t, err) 687 expectedJSON, err := json.Marshal(testCase.expected) 688 require.NoError(t, err) 689 gotJSON, err := json.Marshal(got) 690 require.NoError(t, err) 691 assert.JSONEq(t, string(expectedJSON), string(gotJSON)) 692 } 693 }) 694 } 695 }