diff --git a/docs/parameter_reference.md b/docs/parameter_reference.md index c11aac5..b246761 100644 --- a/docs/parameter_reference.md +++ b/docs/parameter_reference.md @@ -4,7 +4,7 @@ | Param name | Type | Purpose | |---------------------|-----------------|---------| | helm_command | string | Indicates the operation to perform. Recommended, but not required. Valid options are `upgrade`, `uninstall`, `lint`, and `help`. | -| update_dependencies | boolean | Calls `helm dependency update` before running the main command. **Not currently implemented**; see [#25](https://github.com/pelotech/drone-helm3/issues/25).| +| update_dependencies | boolean | Calls `helm dependency update` before running the main command.| | helm_repos | list\ | Calls `helm repo add $repo` before running the main command. Each string should be formatted as `repo_name=https://repo.url/`. **Not currently implemented**; see [#26](https://github.com/pelotech/drone-helm3/issues/26). | | namespace | string | Kubernetes namespace to use for this operation. | | prefix | string | Expect environment variables to be prefixed with the given string. For more details, see "Using the prefix setting" below. **Not currently implemented**; see [#19](https://github.com/pelotech/drone-helm3/issues/19). | @@ -58,6 +58,7 @@ Uninstallations are triggered when the `helm_command` setting is "uninstall" or | dry_run | boolean | | Pass `--dry-run` to `helm uninstall`. | | timeout | duration | | Timeout for any *individual* Kubernetes operation. The uninstallation's full runtime may exceed this duration. | | skip_tls_verify | boolean | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. | +| chart | string | | Required when the global `update_dependencies` parameter is true. No effect otherwise. | ### Where to put settings diff --git a/internal/helm/plan.go b/internal/helm/plan.go index e6c8721..ad2e5ae 100644 --- a/internal/helm/plan.go +++ b/internal/helm/plan.go @@ -96,7 +96,9 @@ func (p *Plan) Execute() error { var upgrade = func(cfg Config) []Step { steps := initKube(cfg) - + if cfg.UpdateDependencies { + steps = append(steps, depUpdate(cfg)...) + } steps = append(steps, &run.Upgrade{ Chart: cfg.Chart, Release: cfg.Release, @@ -113,6 +115,9 @@ var upgrade = func(cfg Config) []Step { var uninstall = func(cfg Config) []Step { steps := initKube(cfg) + if cfg.UpdateDependencies { + steps = append(steps, depUpdate(cfg)...) + } steps = append(steps, &run.Uninstall{ Release: cfg.Release, DryRun: cfg.DryRun, @@ -122,11 +127,15 @@ var uninstall = func(cfg Config) []Step { } var lint = func(cfg Config) []Step { - lint := &run.Lint{ - Chart: cfg.Chart, + steps := make([]Step, 0) + if cfg.UpdateDependencies { + steps = append(steps, depUpdate(cfg)...) } + steps = append(steps, &run.Lint{ + Chart: cfg.Chart, + }) - return []Step{lint} + return steps } var help = func(cfg Config) []Step { @@ -147,3 +156,11 @@ func initKube(cfg Config) []Step { }, } } + +func depUpdate(cfg Config) []Step { + return []Step{ + &run.DepUpdate{ + Chart: cfg.Chart, + }, + } +} diff --git a/internal/helm/plan_test.go b/internal/helm/plan_test.go index 4808aef..2cdde5c 100644 --- a/internal/helm/plan_test.go +++ b/internal/helm/plan_test.go @@ -167,7 +167,17 @@ func (suite *PlanTestSuite) TestUpgrade() { suite.Equal(expected, upgrade) } -func (suite *PlanTestSuite) TestDel() { +func (suite *PlanTestSuite) TestUpgradeWithUpdateDependencies() { + cfg := Config{ + UpdateDependencies: true, + } + steps := upgrade(cfg) + suite.Require().Equal(3, len(steps), "upgrade should have a third step when DepUpdate is true") + suite.IsType(&run.InitKube{}, steps[0]) + suite.IsType(&run.DepUpdate{}, steps[1]) +} + +func (suite *PlanTestSuite) TestUninstall() { cfg := Config{ KubeToken: "b2YgbXkgYWZmZWN0aW9u", SkipTLSVerify: true, @@ -205,6 +215,16 @@ func (suite *PlanTestSuite) TestDel() { suite.Equal(expected, actual) } +func (suite *PlanTestSuite) TestUninstallWithUpdateDependencies() { + cfg := Config{ + UpdateDependencies: true, + } + steps := uninstall(cfg) + suite.Require().Equal(3, len(steps), "uninstall should have a third step when DepUpdate is true") + suite.IsType(&run.InitKube{}, steps[0]) + suite.IsType(&run.DepUpdate{}, steps[1]) +} + func (suite *PlanTestSuite) TestInitKube() { cfg := Config{ KubeToken: "cXVlZXIgY2hhcmFjdGVyCg==", @@ -231,6 +251,23 @@ func (suite *PlanTestSuite) TestInitKube() { suite.Equal(expected, init) } +func (suite *PlanTestSuite) TestDepUpdate() { + cfg := Config{ + UpdateDependencies: true, + Chart: "scatterplot", + } + + steps := depUpdate(cfg) + suite.Require().Equal(1, len(steps), "depUpdate should return one step") + suite.Require().IsType(&run.DepUpdate{}, steps[0]) + update, _ := steps[0].(*run.DepUpdate) + + expected := &run.DepUpdate{ + Chart: "scatterplot", + } + suite.Equal(expected, update) +} + func (suite *PlanTestSuite) TestLint() { cfg := Config{ Chart: "./flow", @@ -245,6 +282,15 @@ func (suite *PlanTestSuite) TestLint() { suite.Equal(want, steps[0]) } +func (suite *PlanTestSuite) TestLintWithUpdateDependencies() { + cfg := Config{ + UpdateDependencies: true, + } + steps := lint(cfg) + suite.Require().Equal(2, len(steps), "lint should have a second step when DepUpdate is true") + suite.IsType(&run.DepUpdate{}, steps[0]) +} + func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() { cfg := Config{ Command: "upgrade", diff --git a/internal/run/depupdate.go b/internal/run/depupdate.go new file mode 100644 index 0000000..a9b6c91 --- /dev/null +++ b/internal/run/depupdate.go @@ -0,0 +1,44 @@ +package run + +import ( + "fmt" +) + +// DepUpdate is an execution step that calls `helm dependency update` when executed. +type DepUpdate struct { + Chart string + cmd cmd +} + +// Execute executes the `helm upgrade` command. +func (d *DepUpdate) Execute(_ Config) error { + return d.cmd.Run() +} + +// Prepare gets the DepUpdate ready to execute. +func (d *DepUpdate) Prepare(cfg Config) error { + if d.Chart == "" { + return fmt.Errorf("chart is required") + } + + args := make([]string, 0) + + if cfg.Namespace != "" { + args = append(args, "--namespace", cfg.Namespace) + } + if cfg.Debug { + args = append(args, "--debug") + } + + args = append(args, "dependency", "update", d.Chart) + + d.cmd = command(helmBin, args...) + d.cmd.Stdout(cfg.Stdout) + d.cmd.Stderr(cfg.Stderr) + + if cfg.Debug { + fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", d.cmd.String()) + } + + return nil +} diff --git a/internal/run/depupdate_test.go b/internal/run/depupdate_test.go new file mode 100644 index 0000000..315b351 --- /dev/null +++ b/internal/run/depupdate_test.go @@ -0,0 +1,128 @@ +package run + +import ( + "fmt" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/suite" + "strings" + "testing" +) + +type DepUpdateTestSuite struct { + suite.Suite + ctrl *gomock.Controller + mockCmd *Mockcmd + originalCommand func(string, ...string) cmd +} + +func (suite *DepUpdateTestSuite) BeforeTest(_, _ string) { + suite.ctrl = gomock.NewController(suite.T()) + suite.mockCmd = NewMockcmd(suite.ctrl) + + suite.originalCommand = command + command = func(path string, args ...string) cmd { return suite.mockCmd } +} + +func (suite *DepUpdateTestSuite) AfterTest(_, _ string) { + command = suite.originalCommand +} + +func TestDepUpdateTestSuite(t *testing.T) { + suite.Run(t, new(DepUpdateTestSuite)) +} + +func (suite *DepUpdateTestSuite) TestPrepareAndExecute() { + defer suite.ctrl.Finish() + + stdout := strings.Builder{} + stderr := strings.Builder{} + cfg := Config{ + Stdout: &stdout, + Stderr: &stderr, + } + + command = func(path string, args ...string) cmd { + suite.Equal(helmBin, path) + suite.Equal([]string{"dependency", "update", "your_top_songs_2019"}, args) + + return suite.mockCmd + } + suite.mockCmd.EXPECT(). + Stdout(&stdout) + suite.mockCmd.EXPECT(). + Stderr(&stderr) + suite.mockCmd.EXPECT(). + Run(). + Times(1) + + d := DepUpdate{ + Chart: "your_top_songs_2019", + } + + suite.Require().NoError(d.Prepare(cfg)) + suite.NoError(d.Execute(cfg)) +} + +func (suite *DepUpdateTestSuite) TestPrepareNamespaceFlag() { + defer suite.ctrl.Finish() + + cfg := Config{ + Namespace: "spotify", + } + + command = func(path string, args ...string) cmd { + suite.Equal([]string{"--namespace", "spotify", "dependency", "update", "your_top_songs_2019"}, args) + + return suite.mockCmd + } + suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() + suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() + + d := DepUpdate{ + Chart: "your_top_songs_2019", + } + + suite.Require().NoError(d.Prepare(cfg)) +} + +func (suite *DepUpdateTestSuite) TestPrepareDebugFlag() { + defer suite.ctrl.Finish() + + stdout := strings.Builder{} + stderr := strings.Builder{} + cfg := Config{ + Debug: true, + Stdout: &stdout, + Stderr: &stderr, + } + + command = func(path string, args ...string) cmd { + suite.mockCmd.EXPECT(). + String(). + Return(fmt.Sprintf("%s %s", path, strings.Join(args, " "))) + + return suite.mockCmd + } + suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() + suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() + + d := DepUpdate{ + Chart: "your_top_songs_2019", + } + + suite.Require().NoError(d.Prepare(cfg)) + + want := fmt.Sprintf("Generated command: '%s --debug dependency update your_top_songs_2019'\n", helmBin) + suite.Equal(want, stderr.String()) + suite.Equal("", stdout.String()) +} + +func (suite *DepUpdateTestSuite) TestPrepareChartRequired() { + d := DepUpdate{} + + suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() + suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() + + err := d.Prepare(Config{}) + suite.EqualError(err, "chart is required") +}