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