diff --git a/build/drone-helm b/build/drone-helm new file mode 100755 index 0000000..793b9a8 Binary files /dev/null and b/build/drone-helm differ diff --git a/docs/parameter_reference.md b/docs/parameter_reference.md index 82cf58a..ad28ae6 100644 --- a/docs/parameter_reference.md +++ b/docs/parameter_reference.md @@ -37,6 +37,7 @@ Installations are triggered when the `mode` setting is "upgrade." They can also | kube_certificate | string | | kubernetes_certificate | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. | | chart_version | string | | | Specific chart version to install. | | dry_run | boolean | | | Pass `--dry-run` to `helm upgrade`. | +| dependencies_action | string | | | Calls `helm dependency build` OR `helm dependency update` before running the main command. Possible values: `build`, `update`. | | wait_for_upgrade | boolean | | wait | Wait until kubernetes resources are in a ready state before marking the installation successful. | | timeout | duration | | | Timeout for any *individual* Kubernetes operation. The installation's full runtime may exceed this duration. | | force_upgrade | boolean | | force | Pass `--force` to `helm upgrade`. | diff --git a/internal/env/config.go b/internal/env/config.go index ab338b5..dd5c352 100644 --- a/internal/env/config.go +++ b/internal/env/config.go @@ -23,7 +23,8 @@ type Config struct { // Configuration for drone-helm itself Command string `envconfig:"mode"` // Helm command to run DroneEvent string `envconfig:"drone_build_event"` // Drone event that invoked this plugin. - UpdateDependencies bool `split_words:"true"` // Call `helm dependency update` before the main command + UpdateDependencies bool `split_words:"true"` // [Deprecated] Call `helm dependency update` before the main command (deprecated, use dependencies_action: update instead) + DependenciesAction string `split_words:"true"` // Call `helm dependency build` or `helm dependency update` before the main command AddRepos []string `split_words:"true"` // Call `helm repo add` before the main command RepoCertificate string `envconfig:"repo_certificate"` // The Helm chart repository's self-signed certificate (must be base64-encoded) RepoCACertificate string `envconfig:"repo_ca_certificate"` // The Helm chart repository CA's self-signed certificate (must be base64-encoded) diff --git a/internal/helm/plan.go b/internal/helm/plan.go index c09bdc8..4f62162 100644 --- a/internal/helm/plan.go +++ b/internal/helm/plan.go @@ -1,6 +1,7 @@ package helm import ( + "errors" "fmt" "github.com/pelotech/drone-helm3/internal/env" "github.com/pelotech/drone-helm3/internal/run" @@ -30,6 +31,10 @@ func NewPlan(cfg env.Config) (*Plan, error) { cfg: cfg, } + if cfg.UpdateDependencies && cfg.DependenciesAction != "" { + return nil, errors.New("update_dependencies is deprecated and cannot be provided together with dependencies_action") + } + p.steps = (*determineSteps(cfg))(cfg) for i, step := range p.steps { @@ -91,9 +96,15 @@ var upgrade = func(cfg env.Config) []Step { for _, repo := range cfg.AddRepos { steps = append(steps, run.NewAddRepo(cfg, repo)) } + + if cfg.DependenciesAction != "" { + steps = append(steps, run.NewDepAction(cfg)) + } + if cfg.UpdateDependencies { steps = append(steps, run.NewDepUpdate(cfg)) } + steps = append(steps, run.NewUpgrade(cfg)) return steps diff --git a/internal/run/depaction.go b/internal/run/depaction.go new file mode 100644 index 0000000..9a8abf8 --- /dev/null +++ b/internal/run/depaction.go @@ -0,0 +1,59 @@ +package run + +import ( + "errors" + "fmt" + "github.com/pelotech/drone-helm3/internal/env" +) + +const ( + actionBuild = "build" + actionUpdate = "update" +) + +// DepAction is an execution step that calls `helm dependency update` or `helm dependency build` when executed. +type DepAction struct { + *config + chart string + cmd cmd + action string +} + +// NewDepAction creates a DepAction using fields from the given Config. No validation is performed at this time. +func NewDepAction(cfg env.Config) *DepAction { + return &DepAction{ + config: newConfig(cfg), + chart: cfg.Chart, + action: cfg.DependenciesAction, + } +} + +// Execute executes the `helm upgrade` command. +func (d *DepAction) Execute() error { + return d.cmd.Run() +} + +// Prepare gets the DepAction ready to execute. +func (d *DepAction) Prepare() error { + if d.chart == "" { + return fmt.Errorf("chart is required") + } + + args := d.globalFlags() + + if d.action != actionBuild && d.action != actionUpdate { + return errors.New("unknown dependency_action: " + d.action) + } + + args = append(args, "dependency", d.action, d.chart) + + d.cmd = command(helmBin, args...) + d.cmd.Stdout(d.stdout) + d.cmd.Stderr(d.stderr) + + if d.debug { + fmt.Fprintf(d.stderr, "Generated command: '%s'\n", d.cmd.String()) + } + + return nil +} diff --git a/internal/run/depaction_test.go b/internal/run/depaction_test.go new file mode 100644 index 0000000..f80c754 --- /dev/null +++ b/internal/run/depaction_test.go @@ -0,0 +1,131 @@ +package run + +import ( + "errors" + "github.com/golang/mock/gomock" + "github.com/pelotech/drone-helm3/internal/env" + "github.com/stretchr/testify/suite" + "strings" + "testing" +) + +type DepActionTestSuite struct { + suite.Suite + ctrl *gomock.Controller + mockCmd *Mockcmd + originalCommand func(string, ...string) cmd +} + +func (suite *DepActionTestSuite) 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 *DepActionTestSuite) AfterTest(_, _ string) { + command = suite.originalCommand +} + +func TestDepActionTestSuite(t *testing.T) { + suite.Run(t, new(DepActionTestSuite)) +} + +func (suite *DepActionTestSuite) TestNewDepAction() { + cfg := env.Config{ + Chart: "scatterplot", + } + d := NewDepAction(cfg) + suite.Equal("scatterplot", d.chart) +} + +func (suite *DepActionTestSuite) TestPrepareAndExecuteBuild() { + defer suite.ctrl.Finish() + + stdout := strings.Builder{} + stderr := strings.Builder{} + cfg := env.Config{ + Chart: "your_top_songs_2019", + Stdout: &stdout, + Stderr: &stderr, + DependenciesAction: "build", + } + + command = func(path string, args ...string) cmd { + suite.Equal(helmBin, path) + suite.Equal([]string{"dependency", "build", "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 := NewDepAction(cfg) + + suite.Require().NoError(d.Prepare()) + suite.NoError(d.Execute()) +} + +func (suite *DepActionTestSuite) TestPrepareAndExecuteUpdate() { + defer suite.ctrl.Finish() + + stdout := strings.Builder{} + stderr := strings.Builder{} + cfg := env.Config{ + Chart: "your_top_songs_2019", + Stdout: &stdout, + Stderr: &stderr, + DependenciesAction: "update", + } + + 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 := NewDepAction(cfg) + + suite.Require().NoError(d.Prepare()) + suite.NoError(d.Execute()) +} + +func (suite *DepActionTestSuite) TestPrepareAndExecuteUnknown() { + defer suite.ctrl.Finish() + + stdout := strings.Builder{} + stderr := strings.Builder{} + cfg := env.Config{ + Chart: "your_top_songs_2019", + Stdout: &stdout, + Stderr: &stderr, + DependenciesAction: "downgrade", + } + + d := NewDepAction(cfg) + suite.Require().Equal(errors.New("unknown dependency_action: downgrade"), d.Prepare()) +} + +func (suite *DepActionTestSuite) TestPrepareChartRequired() { + d := NewDepAction(env.Config{}) + + suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() + suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() + + err := d.Prepare() + suite.EqualError(err, "chart is required") +}