github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/render/helm/template_test.go (about) 1 package helm 2 3 import ( 4 "os" 5 "path" 6 "path/filepath" 7 "testing" 8 9 "github.com/emosbaugh/yaml" 10 "github.com/golang/mock/gomock" 11 "github.com/replicatedhq/libyaml" 12 "github.com/replicatedhq/ship/pkg/api" 13 "github.com/replicatedhq/ship/pkg/constants" 14 "github.com/replicatedhq/ship/pkg/lifecycle/render/root" 15 "github.com/replicatedhq/ship/pkg/process" 16 state2 "github.com/replicatedhq/ship/pkg/state" 17 "github.com/replicatedhq/ship/pkg/templates" 18 "github.com/replicatedhq/ship/pkg/test-mocks/helm" 19 "github.com/replicatedhq/ship/pkg/test-mocks/state" 20 "github.com/replicatedhq/ship/pkg/testing/logger" 21 "github.com/spf13/afero" 22 "github.com/spf13/viper" 23 "github.com/stretchr/testify/require" 24 "k8s.io/helm/pkg/chartutil" 25 ) 26 27 func TestLocalTemplater(t *testing.T) { 28 tests := []struct { 29 name string 30 describe string 31 expectError string 32 helmOpts []string 33 helmValues map[string]interface{} 34 expectedHelmValues []string 35 templateContext map[string]interface{} 36 channelName string 37 expectedChannelName string 38 ontemplate func(req *require.Assertions, mockFs afero.Afero) func(chartRoot string, args []string) error 39 state *state2.State 40 requirements *chartutil.Requirements 41 repoAdd []string 42 namespace string 43 }{ 44 { 45 name: "helm test proper args", 46 describe: "test that helm is invoked with the proper args. The subprocess will fail if its not called with the args set in EXPECT_HELM_ARGV", 47 expectError: "", 48 }, 49 { 50 name: "helm with set value", 51 describe: "ensure any helm.helm_opts are forwarded down to the call to `helm template`", 52 expectError: "", 53 helmOpts: []string{"--set", "service.clusterIP=10.3.9.2"}, 54 }, 55 { 56 name: "helm with subcharts", 57 describe: "ensure any helm.helm_opts are forwarded down to the call to `helm template`", 58 expectError: "", 59 helmOpts: []string{"--set", "service.clusterIP=10.3.9.2"}, 60 ontemplate: func(req *require.Assertions, mockFs afero.Afero) func(chartRoot string, args []string) error { 61 return func(chartRoot string, args []string) error { 62 mockFolderPathToCreate := path.Join(constants.ShipPathInternalTmp, "chartrendered", "frobnitz", "templates") 63 req.NoError(mockFs.MkdirAll(mockFolderPathToCreate, 0755)) 64 mockChartsPathToCreate := path.Join(constants.ShipPathInternalTmp, "chartrendered", "frobnitz", "charts") 65 req.NoError(mockFs.MkdirAll(mockChartsPathToCreate, 0755)) 66 return nil 67 } 68 }, 69 }, 70 { 71 name: "helm values from asset value", 72 describe: "ensure any helm.helm_opts are forwarded down to the call to `helm template`", 73 expectError: "", 74 helmValues: map[string]interface{}{ 75 "service.clusterIP": "10.3.9.2", 76 }, 77 expectedHelmValues: []string{ 78 "--set", "service.clusterIP=10.3.9.2", 79 }, 80 }, 81 { 82 name: "helm replaces spacial characters in ", 83 expectError: "", 84 helmValues: map[string]interface{}{ 85 "service.clusterIP": "10.3.9.2", 86 }, 87 expectedHelmValues: []string{ 88 "--set", "service.clusterIP=10.3.9.2", 89 }, 90 channelName: "1-2-3---------frobnitz", 91 expectedChannelName: "1-2-3---------frobnitz", 92 }, 93 { 94 name: "helm templates values from context", 95 expectError: "", 96 helmValues: map[string]interface{}{ 97 "service.clusterIP": "{{repl ConfigOption \"cluster_ip\"}}", 98 }, 99 templateContext: map[string]interface{}{ 100 "cluster_ip": "10.3.9.2", 101 }, 102 expectedHelmValues: []string{ 103 "--set", "service.clusterIP=10.3.9.2", 104 }, 105 channelName: "1-2-3---------frobnitz", 106 expectedChannelName: "1-2-3---------frobnitz", 107 }, 108 { 109 name: "helm values from asset value with incubator requirement", 110 describe: "calls helm repo add", 111 expectError: "", 112 helmValues: map[string]interface{}{ 113 "service.clusterIP": "10.3.9.2", 114 }, 115 expectedHelmValues: []string{ 116 "--set", "service.clusterIP=10.3.9.2", 117 }, 118 requirements: &chartutil.Requirements{ 119 Dependencies: []*chartutil.Dependency{ 120 { 121 Repository: "https://kubernetes-charts-incubator.storage.googleapis.com/", 122 }, 123 }, 124 }, 125 repoAdd: []string{"kubernetes-charts-incubator", "https://kubernetes-charts-incubator.storage.googleapis.com/"}, 126 }, 127 { 128 name: "helm template with namespace in state", 129 describe: "template uses namespace from state", 130 expectError: "", 131 state: &state2.State{ 132 V1: &state2.V1{ 133 Namespace: "test-namespace", 134 }, 135 }, 136 namespace: "test-namespace", 137 }, 138 } 139 for _, test := range tests { 140 t.Run(test.name, func(t *testing.T) { 141 req := require.New(t) 142 143 mc := gomock.NewController(t) 144 testLogger := &logger.TestLogger{T: t} 145 mockState := state.NewMockManager(mc) 146 mockCommands := helm.NewMockCommands(mc) 147 memMapFs := afero.MemMapFs{} 148 mockFs := afero.Afero{Fs: &memMapFs} 149 tpl := &LocalTemplater{ 150 Commands: mockCommands, 151 Logger: testLogger, 152 FS: mockFs, 153 BuilderBuilder: templates.NewBuilderBuilder(testLogger, viper.New(), &state.MockManager{}), 154 Viper: viper.New(), 155 StateManager: mockState, 156 process: process.Process{Logger: testLogger}, 157 } 158 159 channelName := "frobnitz" 160 expectedChannelName := "frobnitz" 161 if test.channelName != "" { 162 channelName = test.channelName 163 } 164 if test.expectedChannelName != "" { 165 expectedChannelName = test.expectedChannelName 166 } 167 168 if test.templateContext == nil { 169 test.templateContext = map[string]interface{}{} 170 } 171 172 if test.state == nil { 173 mockState.EXPECT().CachedState().Return(state2.State{ 174 V1: &state2.V1{ 175 HelmValues: "we fake", 176 ReleaseName: channelName, 177 }, 178 }, nil) 179 } else { 180 testState := *test.state 181 testState.V1.ReleaseName = channelName 182 mockState.EXPECT().CachedState().Return(testState, nil) 183 } 184 185 chartRoot := "/tmp/chartroot" 186 optionAndValuesArgs := append( 187 test.helmOpts, 188 test.expectedHelmValues..., 189 ) 190 191 if test.requirements != nil { 192 requirementsB, err := yaml.Marshal(test.requirements) 193 req.NoError(err) 194 err = mockFs.WriteFile(path.Join(chartRoot, "requirements.yaml"), requirementsB, 0755) 195 req.NoError(err) 196 } 197 198 templateArgs := append( 199 []string{ 200 "--output-dir", ".ship/tmp/chartrendered", 201 "--name", expectedChannelName, 202 }, 203 optionAndValuesArgs..., 204 ) 205 206 if len(test.namespace) > 0 { 207 templateArgs = addArgIfNotPresent(templateArgs, "--namespace", test.namespace) 208 } else { 209 templateArgs = addArgIfNotPresent(templateArgs, "--namespace", "default") 210 } 211 212 mockCommands.EXPECT().Init().Return(nil) 213 if test.requirements != nil { 214 absTempHelmHome, err := filepath.Abs(constants.InternalTempHelmHome) 215 req.NoError(err) 216 mockCommands.EXPECT().RepoAdd(test.repoAdd[0], test.repoAdd[1], absTempHelmHome) 217 218 requirementsB, err := mockFs.ReadFile(filepath.Join(chartRoot, "requirements.yaml")) 219 req.NoError(err) 220 chartRequirements := chartutil.Requirements{} 221 err = yaml.Unmarshal(requirementsB, &chartRequirements) 222 req.NoError(err) 223 224 mockCommands.EXPECT().MaybeDependencyUpdate(chartRoot, chartRequirements).Return(nil) 225 } else { 226 mockCommands.EXPECT().MaybeDependencyUpdate(chartRoot, chartutil.Requirements{}).Return(nil) 227 } 228 229 if test.ontemplate != nil { 230 mockCommands.EXPECT().Template(chartRoot, templateArgs).DoAndReturn(test.ontemplate(req, mockFs)) 231 } else { 232 233 mockCommands.EXPECT().Template(chartRoot, templateArgs).DoAndReturn(func(rootDir string, args []string) error { 234 mockFolderPathToCreate := path.Join(constants.ShipPathInternalTmp, "chartrendered", expectedChannelName, "templates") 235 req.NoError(mockFs.MkdirAll(mockFolderPathToCreate, 0755)) 236 return nil 237 }) 238 } 239 240 err := tpl.Template( 241 "/tmp/chartroot", 242 root.Fs{ 243 Afero: mockFs, 244 RootPath: "", 245 }, 246 api.HelmAsset{ 247 AssetShared: api.AssetShared{ 248 Dest: "k8s/", 249 }, 250 HelmOpts: test.helmOpts, 251 Values: test.helmValues, 252 }, 253 api.ReleaseMetadata{ 254 Semver: "1.0.0", 255 ChannelName: channelName, 256 ShipAppMetadata: api.ShipAppMetadata{ 257 Name: expectedChannelName, 258 }, 259 }, 260 []libyaml.ConfigGroup{}, 261 test.templateContext, 262 ) 263 264 t.Logf("checking error %v", err) 265 if test.expectError == "" { 266 req.NoError(err) 267 } else { 268 req.Error(err, "expected error "+test.expectError) 269 req.Equal(test.expectError, err.Error()) 270 } 271 272 }) 273 } 274 } 275 276 func TestTryRemoveKustomizeBasePath(t *testing.T) { 277 tests := []struct { 278 name string 279 describe string 280 baseDir string 281 expectError bool 282 }{ 283 { 284 name: "base exists", 285 describe: "ensure base is removed and removeAll doesn't error", 286 baseDir: constants.KustomizeBasePath, 287 expectError: false, 288 }, 289 { 290 name: "base does not exist", 291 describe: "missing base, ensure removeAll doesn't error", 292 baseDir: "", 293 expectError: false, 294 }, 295 } 296 297 for _, test := range tests { 298 t.Run(test.name, func(t *testing.T) { 299 req := require.New(t) 300 301 testLogger := &logger.TestLogger{T: t} 302 303 fakeFS := afero.Afero{Fs: afero.NewMemMapFs()} 304 305 // create the base directory 306 err := fakeFS.MkdirAll(path.Join(test.baseDir, "myCoolManifest.yaml"), 0777) 307 req.NoError(err) 308 309 // verify path actually exists 310 successfulMkdirAll, err := fakeFS.DirExists(path.Join(test.baseDir, "myCoolManifest.yaml")) 311 req.True(successfulMkdirAll) 312 req.NoError(err) 313 314 ft := &LocalTemplater{ 315 FS: fakeFS, 316 Logger: testLogger, 317 } 318 319 removeErr := ft.FS.RemoveAll(constants.KustomizeBasePath) 320 321 if test.expectError { 322 req.Error(removeErr) 323 } else { 324 if dirExists, existErr := ft.FS.DirExists(constants.KustomizeBasePath); dirExists { 325 req.NoError(existErr) 326 // if dir exists, we expect tryRemoveKustomizeBasePath to have err'd 327 req.Error(removeErr) 328 } else { 329 // if dir does not exist, we expect tryRemoveKustomizeBasePath to have succeeded without err'ing 330 req.NoError(removeErr) 331 } 332 } 333 }) 334 } 335 } 336 337 func Test_addArgIfNotPresent(t *testing.T) { 338 type args struct { 339 existingArgs []string 340 newArg string 341 newDefault string 342 } 343 tests := []struct { 344 name string 345 args args 346 want []string 347 }{ 348 { 349 name: "empty", 350 args: args{ 351 existingArgs: []string{}, 352 newArg: "--test", 353 newDefault: "newDefault", 354 }, 355 want: []string{"--test", "newDefault"}, 356 }, 357 { 358 name: "not present, not empty", 359 args: args{ 360 existingArgs: []string{"--notTest", "notDefault"}, 361 newArg: "--test", 362 newDefault: "newDefault", 363 }, 364 want: []string{"--notTest", "notDefault", "--test", "newDefault"}, 365 }, 366 { 367 name: "present", 368 args: args{ 369 existingArgs: []string{"--test", "notDefault"}, 370 newArg: "--test", 371 newDefault: "newDefault", 372 }, 373 want: []string{"--test", "notDefault"}, 374 }, 375 { 376 name: "present with others", 377 args: args{ 378 existingArgs: []string{"--notTest", "notDefault", "--test", "alsoNotDefault"}, 379 newArg: "--test", 380 newDefault: "newDefault", 381 }, 382 want: []string{"--notTest", "notDefault", "--test", "alsoNotDefault"}, 383 }, 384 { 385 name: "present as substring", 386 args: args{ 387 existingArgs: []string{"--notTest", "notDefault", "abc--test", "alsoNotDefault"}, 388 newArg: "--test", 389 newDefault: "newDefault", 390 }, 391 want: []string{"--notTest", "notDefault", "abc--test", "alsoNotDefault", "--test", "newDefault"}, 392 }, 393 } 394 for _, tt := range tests { 395 t.Run(tt.name, func(t *testing.T) { 396 req := require.New(t) 397 398 got := addArgIfNotPresent(tt.args.existingArgs, tt.args.newArg, tt.args.newDefault) 399 400 req.Equal(tt.want, got) 401 }) 402 } 403 } 404 405 func Test_validateGeneratedFiles(t *testing.T) { 406 407 type file struct { 408 contents string 409 path string 410 } 411 tests := []struct { 412 name string 413 inputFiles []file 414 dir string 415 outputFiles []file 416 }{ 417 { 418 name: "no_files", 419 dir: "", 420 inputFiles: []file{}, 421 outputFiles: []file{}, 422 }, 423 { 424 name: "irrelevant_files", 425 dir: "test", 426 inputFiles: []file{ 427 { 428 path: "outside", 429 contents: `irrelevant`, 430 }, 431 { 432 path: "test/inside", 433 contents: `irrelevant 434 `, 435 }, 436 }, 437 outputFiles: []file{ 438 { 439 path: "outside", 440 contents: `irrelevant`, 441 }, 442 { 443 path: "test/inside", 444 contents: `irrelevant 445 `, 446 }, 447 }, 448 }, 449 { 450 name: "relevant_args_files", 451 dir: "test", 452 inputFiles: []file{ 453 { 454 path: "test/something.yaml", 455 contents: ` args: {}`, 456 }, 457 { 458 path: "test/missingArgs.yaml", 459 contents: ` args:`, 460 }, 461 { 462 path: "test/notMissingMultilineArgs.yaml", 463 contents: ` 464 args: 465 something 466 args: 467 - something`, 468 }, 469 { 470 path: "test/missingMultilineArgs.yaml", 471 contents: ` 472 args: 473 something:`, 474 }, 475 }, 476 outputFiles: []file{ 477 { 478 path: "test/something.yaml", 479 contents: ` args: {}`, 480 }, 481 { 482 path: "test/missingArgs.yaml", 483 contents: ` args: []`, 484 }, 485 { 486 path: "test/notMissingMultilineArgs.yaml", 487 contents: ` 488 args: 489 something 490 args: 491 - something`, 492 }, 493 { 494 path: "test/missingMultilineArgs.yaml", 495 contents: ` 496 args: [] 497 something:`, 498 }, 499 }, 500 }, 501 { 502 name: "relevant_env_files", 503 dir: "test", 504 inputFiles: []file{ 505 { 506 path: "test/something.yaml", 507 contents: ` env: []`, 508 }, 509 { 510 path: "test/missingEnv.yaml", 511 contents: ` env:`, 512 }, 513 { 514 path: "test/notMissingMultilineEnv.yaml", 515 contents: ` 516 env: 517 something 518 env: 519 - something`, 520 }, 521 { 522 path: "test/missingMultilineEnv.yaml", 523 contents: ` 524 env: 525 something:`, 526 }, 527 }, 528 outputFiles: []file{ 529 { 530 path: "test/something.yaml", 531 contents: ` env: []`, 532 }, 533 { 534 path: "test/missingEnv.yaml", 535 contents: ` env: []`, 536 }, 537 { 538 path: "test/notMissingMultilineEnv.yaml", 539 contents: ` 540 env: 541 something 542 env: 543 - something`, 544 }, 545 { 546 path: "test/missingMultilineEnv.yaml", 547 contents: ` 548 env: [] 549 something:`, 550 }, 551 }, 552 }, 553 { 554 name: "relevant_value_files", 555 dir: "test", 556 inputFiles: []file{ 557 { 558 path: "test/something.yaml", 559 contents: ` value: {}`, 560 }, 561 { 562 path: "test/missingValue.yaml", 563 contents: ` value:`, 564 }, 565 }, 566 outputFiles: []file{ 567 { 568 path: "test/something.yaml", 569 contents: ` value: {}`, 570 }, 571 { 572 path: "test/missingValue.yaml", 573 contents: ` value: ""`, 574 }, 575 }, 576 }, 577 { 578 name: "blank lines", 579 dir: "test", 580 inputFiles: []file{ 581 { 582 path: "test/blank_line_env.yaml", 583 contents: ` 584 env: 585 586 item 587 `, 588 }, 589 { 590 path: "test/blank_line_args.yaml", 591 contents: ` 592 args: 593 594 item 595 `, 596 }, 597 }, 598 outputFiles: []file{ 599 { 600 path: "test/blank_line_env.yaml", 601 contents: ` 602 env: 603 604 item 605 `, 606 }, 607 { 608 path: "test/blank_line_args.yaml", 609 contents: ` 610 args: 611 612 item 613 `, 614 }, 615 }, 616 }, 617 { 618 name: "comment lines", 619 dir: "test", 620 inputFiles: []file{ 621 { 622 path: "test/comment_line_env.yaml", 623 contents: ` 624 env: 625 #item 626 627 env: 628 #item 629 item2 630 `, 631 }, 632 { 633 path: "test/comment_line_args.yaml", 634 contents: ` 635 args: 636 #item 637 638 args: 639 #item 640 item2 641 `, 642 }, 643 }, 644 outputFiles: []file{ 645 { 646 path: "test/comment_line_env.yaml", 647 contents: ` 648 env: [] 649 #item 650 651 env: 652 #item 653 item2 654 `, 655 }, 656 { 657 path: "test/comment_line_args.yaml", 658 contents: ` 659 args: [] 660 #item 661 662 args: 663 #item 664 item2 665 `, 666 }, 667 }, 668 }, 669 { 670 name: "null values", 671 dir: "test", 672 inputFiles: []file{ 673 { 674 path: "test/null_values.yaml", 675 contents: ` 676 value: null 677 #item 678 679 value: 680 null 681 682 value: 683 value: null 684 `, 685 }, 686 }, 687 outputFiles: []file{ 688 { 689 path: "test/null_values.yaml", 690 contents: ` 691 value: "" 692 #item 693 694 value: 695 null 696 697 value: 698 value: "" 699 `, 700 }, 701 }, 702 }, 703 { 704 name: "templated values", 705 dir: "test", 706 inputFiles: []file{ 707 { 708 path: "test/null_values.yaml", 709 contents: ` 710 value: 711 712 {{ template }} 713 714 value: 715 {{ template }} 716 717 value: 718 value: {{ template }} 719 `, 720 }, 721 }, 722 outputFiles: []file{ 723 { 724 path: "test/null_values.yaml", 725 contents: ` 726 value: 727 728 {{ template }} 729 730 value: 731 {{ template }} 732 733 value: 734 value: {{ template }} 735 `, 736 }, 737 }, 738 }, 739 { 740 name: "everything", 741 dir: "test", 742 inputFiles: []file{ 743 { 744 path: "test/everything.yaml", 745 contents: ` 746 args: 747 env: 748 volumes: 749 value: 750 value: null 751 initContainers: 752 `, 753 }, 754 }, 755 outputFiles: []file{ 756 { 757 path: "test/everything.yaml", 758 contents: ` 759 args: [] 760 env: [] 761 volumes: [] 762 value: "" 763 value: "" 764 initContainers: [] 765 `, 766 }, 767 }, 768 }, 769 } 770 for _, tt := range tests { 771 t.Run(tt.name, func(t *testing.T) { 772 req := require.New(t) 773 774 testLogger := &logger.TestLogger{T: t} 775 776 fakeFS := afero.Afero{Fs: afero.NewMemMapFs()} 777 lt := &LocalTemplater{ 778 FS: fakeFS, 779 Logger: testLogger, 780 } 781 782 // add inputFiles to fakeFS 783 for _, file := range tt.inputFiles { 784 req.NoError(fakeFS.WriteFile(file.path, []byte(file.contents), os.FileMode(777))) 785 } 786 787 req.NoError(lt.validateGeneratedFiles(fakeFS, tt.dir)) 788 789 // check outputFiles from fakeFS 790 for _, file := range tt.outputFiles { 791 contents, err := fakeFS.ReadFile(file.path) 792 req.NoError(err) 793 req.Equal(file.contents, string(contents), "expected %s contents to be equal", file.path) 794 } 795 }) 796 } 797 } 798 799 func TestLocalTemplater_writeStateHelmValuesTo(t *testing.T) { 800 tests := []struct { 801 name string 802 dest string 803 defaultValuesPath string 804 defaultValuesContent string 805 }{ 806 { 807 name: "simple", 808 dest: "some/values.yaml", 809 defaultValuesPath: "random/values.yaml", 810 defaultValuesContent: ` 811 something: maybe 812 `, 813 }, 814 } 815 for _, tt := range tests { 816 req := require.New(t) 817 t.Run(tt.name, func(t *testing.T) { 818 mc := gomock.NewController(t) 819 mockState := state.NewMockManager(mc) 820 mockFs := afero.Afero{Fs: afero.NewMemMapFs()} 821 err := mockFs.WriteFile(tt.defaultValuesPath, []byte(tt.defaultValuesContent), 0755) 822 req.NoError(err) 823 824 mockState.EXPECT().CachedState().Return(state2.State{V1: &state2.V1{}}, nil) 825 f := &LocalTemplater{ 826 Logger: &logger.TestLogger{T: t}, 827 FS: mockFs, 828 StateManager: mockState, 829 } 830 err = f.writeStateHelmValuesTo(tt.dest, tt.defaultValuesPath) 831 req.NoError(err) 832 833 readFileB, err := mockFs.ReadFile(tt.dest) 834 req.NoError(err) 835 req.Equal(tt.defaultValuesContent, string(readFileB)) 836 }) 837 } 838 }