From 4cbb4922fb93ffbcc9b678d788d726d365203c47 Mon Sep 17 00:00:00 2001 From: Erin Call Date: Mon, 9 Dec 2019 16:25:47 -0800 Subject: [PATCH] Implement the debug flag and help command I'm vacillating about the choice to have separate Config structs in the `helm` and `run` packages. I can't tell whether it's "good separation of concerns" or "cumbersome and over-engineered." It seems appropriate at the moment, though. --- README.md | 2 +- internal/helm/plan.go | 61 +++++++++++++++++++++--- internal/helm/plan_test.go | 26 +++++++---- internal/run/config.go | 22 +++++++++ internal/run/help.go | 27 +++++++---- internal/run/help_test.go | 55 ++++++++++++++++++---- internal/run/upgrade.go | 40 ++++++++++------ internal/run/upgrade_test.go | 90 ++++++++++++++++++++++++++++++------ 8 files changed, 260 insertions(+), 63 deletions(-) create mode 100644 internal/run/config.go diff --git a/README.md b/README.md index 0b5661a..192d697 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ TODO: * [x] Make a `Dockerfile` that's sufficient for launching the built image * [x] Make `cmd/drone-helm/main.go` actually invoke `helm` * [x] Make `golint` part of the build process (and make it pass) -* [ ] Implement debug output +* [x] Implement debug output * [ ] Flesh out `helm upgrade` until it's capable of working * [ ] Implement config settings for `upgrade` * [ ] Implement `helm lint` diff --git a/internal/helm/plan.go b/internal/helm/plan.go index 52b280d..285c7b6 100644 --- a/internal/helm/plan.go +++ b/internal/helm/plan.go @@ -2,12 +2,15 @@ package helm import ( "errors" + "fmt" "github.com/pelotech/drone-helm3/internal/run" + "os" ) // A Step is one step in the plan. type Step interface { - Run() error + Prepare(run.Config) error + Execute() error } // A Plan is a series of steps to perform. @@ -17,10 +20,26 @@ type Plan struct { // NewPlan makes a plan for running a helm operation. func NewPlan(cfg Config) (*Plan, error) { + runCfg := run.Config{ + Debug: cfg.Debug, + KubeConfig: cfg.KubeConfig, + Values: cfg.Values, + StringValues: cfg.StringValues, + ValuesFiles: cfg.ValuesFiles, + Namespace: cfg.Namespace, + Token: cfg.Token, + SkipTLSVerify: cfg.SkipTLSVerify, + Certificate: cfg.Certificate, + APIServer: cfg.APIServer, + ServiceAccount: cfg.ServiceAccount, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + p := Plan{} switch cfg.Command { case "upgrade": - steps, err := upgrade(cfg) + steps, err := upgrade(cfg, runCfg) if err != nil { return nil, err } @@ -30,11 +49,15 @@ func NewPlan(cfg Config) (*Plan, error) { case "lint": return nil, errors.New("not implemented") case "help": - return nil, errors.New("not implemented") + steps, err := help(cfg, runCfg) + if err != nil { + return nil, err + } + p.steps = steps default: switch cfg.DroneEvent { case "push", "tag", "deployment", "pull_request", "promote", "rollback": - steps, err := upgrade(cfg) + steps, err := upgrade(cfg, runCfg) if err != nil { return nil, err } @@ -50,7 +73,7 @@ func NewPlan(cfg Config) (*Plan, error) { // Execute runs each step in the plan, aborting and reporting on error func (p *Plan) Execute() error { for _, step := range p.steps { - if err := step.Run(); err != nil { + if err := step.Execute(); err != nil { return err } } @@ -58,9 +81,33 @@ func (p *Plan) Execute() error { return nil } -func upgrade(cfg Config) ([]Step, error) { +func upgrade(cfg Config, runCfg run.Config) ([]Step, error) { steps := make([]Step, 0) - steps = append(steps, run.NewUpgrade(cfg.Release, cfg.Chart)) + upgrade := &run.Upgrade{ + Chart: cfg.Chart, + Release: cfg.Release, + ChartVersion: cfg.ChartVersion, + Wait: cfg.Wait, + ReuseValues: cfg.ReuseValues, + Timeout: cfg.Timeout, + Force: cfg.Force, + } + if err := upgrade.Prepare(runCfg); err != nil { + err = fmt.Errorf("while preparing upgrade step: %w", err) + return steps, err + } + steps = append(steps, upgrade) return steps, nil } + +func help(cfg Config, runCfg run.Config) ([]Step, error) { + help := &run.Help{} + + if err := help.Prepare(runCfg); err != nil { + err = fmt.Errorf("while preparing help step: %w", err) + return []Step{}, err + } + + return []Step{help}, nil +} diff --git a/internal/helm/plan_test.go b/internal/helm/plan_test.go index 017a8fe..22bbd3b 100644 --- a/internal/helm/plan_test.go +++ b/internal/helm/plan_test.go @@ -25,15 +25,13 @@ func (suite *PlanTestSuite) TestNewPlanUpgradeCommand() { plan, err := NewPlan(cfg) suite.Require().Nil(err) - suite.Equal(1, len(plan.steps)) + suite.Require().Equal(1, len(plan.steps)) - switch step := plan.steps[0].(type) { - case *run.Upgrade: - suite.Equal("billboard_top_100", step.Chart) - suite.Equal("post_malone_circles", step.Release) - default: - suite.Failf("Wrong type for step 1", "Expected Upgrade, got %T", step) - } + suite.Require().IsType(&run.Upgrade{}, plan.steps[0]) + step, _ := plan.steps[0].(*run.Upgrade) + + suite.Equal("billboard_top_100", step.Chart) + suite.Equal("post_malone_circles", step.Release) } func (suite *PlanTestSuite) TestNewPlanUpgradeFromDroneEvent() { @@ -51,3 +49,15 @@ func (suite *PlanTestSuite) TestNewPlanUpgradeFromDroneEvent() { suite.IsType(&run.Upgrade{}, plan.steps[0], fmt.Sprintf("for event type '%s'", event)) } } + +func (suite *PlanTestSuite) TestNewPlanHelpCommand() { + cfg := Config{ + Command: "help", + } + + plan, err := NewPlan(cfg) + suite.Require().Nil(err) + suite.Equal(1, len(plan.steps)) + + suite.Require().IsType(&run.Help{}, plan.steps[0]) +} diff --git a/internal/run/config.go b/internal/run/config.go new file mode 100644 index 0000000..59b9429 --- /dev/null +++ b/internal/run/config.go @@ -0,0 +1,22 @@ +package run + +import ( + "io" +) + +// Config contains configuration applicable to all helm commands +type Config struct { + Debug bool + KubeConfig string + Values string + StringValues string + ValuesFiles []string + Namespace string + Token string + SkipTLSVerify bool + Certificate string + APIServer string + ServiceAccount string + Stdout io.Writer + Stderr io.Writer +} diff --git a/internal/run/help.go b/internal/run/help.go index 842dcc0..03805f6 100644 --- a/internal/run/help.go +++ b/internal/run/help.go @@ -1,7 +1,7 @@ package run import ( - "os" + "fmt" ) // Help is a step in a helm Plan that calls `helm help`. @@ -9,18 +9,25 @@ type Help struct { cmd cmd } -// Run launches the command. -func (h *Help) Run() error { +// Execute executes the `helm help` command. +func (h *Help) Execute() error { return h.cmd.Run() } -// NewHelp returns a new Help. -func NewHelp() *Help { - h := Help{} +// Prepare gets the Help ready to execute. +func (h *Help) Prepare(cfg Config) error { + args := []string{"help"} + if cfg.Debug { + args = append([]string{"--debug"}, args...) + } - h.cmd = command(helmBin, "help") - h.cmd.Stdout(os.Stdout) - h.cmd.Stderr(os.Stderr) + h.cmd = command(helmBin, args...) + h.cmd.Stdout(cfg.Stdout) + h.cmd.Stderr(cfg.Stderr) - return &h + if cfg.Debug { + fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", h.cmd.String()) + } + + return nil } diff --git a/internal/run/help_test.go b/internal/run/help_test.go index 815404f..7339c69 100644 --- a/internal/run/help_test.go +++ b/internal/run/help_test.go @@ -1,33 +1,72 @@ package run import ( + "fmt" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "strings" "testing" ) -func TestHelp(t *testing.T) { - ctrl := gomock.NewController(t) +type HelpTestSuite struct { + suite.Suite +} + +func TestHelpTestSuite(t *testing.T) { + suite.Run(t, new(HelpTestSuite)) +} + +func (suite *HelpTestSuite) TestPrepare() { + ctrl := gomock.NewController(suite.T()) defer ctrl.Finish() mCmd := NewMockcmd(ctrl) originalCommand := command command = func(path string, args ...string) cmd { - assert.Equal(t, helmBin, path) - assert.Equal(t, []string{"help"}, args) + assert.Equal(suite.T(), helmBin, path) + assert.Equal(suite.T(), []string{"help"}, args) return mCmd } defer func() { command = originalCommand }() + stdout := strings.Builder{} + stderr := strings.Builder{} + mCmd.EXPECT(). - Stdout(gomock.Any()) + Stdout(&stdout) mCmd.EXPECT(). - Stderr(gomock.Any()) + Stderr(&stderr) mCmd.EXPECT(). Run(). Times(1) - h := NewHelp() - h.Run() + cfg := Config{ + Stdout: &stdout, + Stderr: &stderr, + } + + h := Help{} + err := h.Prepare(cfg) + suite.Require().Nil(err) + h.Execute() +} + +func (suite *HelpTestSuite) TestPrepareDebugFlag() { + help := Help{} + + stdout := strings.Builder{} + stderr := strings.Builder{} + cfg := Config{ + Debug: true, + Stdout: &stdout, + Stderr: &stderr, + } + + help.Prepare(cfg) + + want := fmt.Sprintf("Generated command: '%s --debug help'\n", helmBin) + suite.Equal(want, stderr.String()) + suite.Equal("", stdout.String()) } diff --git a/internal/run/upgrade.go b/internal/run/upgrade.go index 636c4da..1a76b46 100644 --- a/internal/run/upgrade.go +++ b/internal/run/upgrade.go @@ -1,31 +1,43 @@ package run import ( - "os" + "fmt" ) -// Upgrade is a step in a helm Plan that calls `helm upgrade`. +// Upgrade is an execution step that calls `helm upgrade` when executed. type Upgrade struct { Chart string Release string - cmd cmd + + ChartVersion string + Wait bool + ReuseValues bool + Timeout string + Force bool + + cmd cmd } -// Run launches the command. -func (u *Upgrade) Run() error { +// Execute executes the `helm upgrade` command. +func (u *Upgrade) Execute() error { return u.cmd.Run() } -// NewUpgrade creates a new Upgrade. -func NewUpgrade(release, chart string) *Upgrade { - u := Upgrade{ - Chart: chart, - Release: release, - cmd: command(helmBin, "upgrade", "--install", release, chart), +// Prepare gets the Upgrade ready to execute. +func (u *Upgrade) Prepare(cfg Config) error { + args := []string{"upgrade", "--install", u.Release, u.Chart} + + if cfg.Debug { + args = append([]string{"--debug"}, args...) } - u.cmd.Stdout(os.Stdout) - u.cmd.Stderr(os.Stderr) + u.cmd = command(helmBin, args...) + u.cmd.Stdout(cfg.Stdout) + u.cmd.Stderr(cfg.Stderr) - return &u + if cfg.Debug { + fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", u.cmd.String()) + } + + return nil } diff --git a/internal/run/upgrade_test.go b/internal/run/upgrade_test.go index 41384f5..48c670d 100644 --- a/internal/run/upgrade_test.go +++ b/internal/run/upgrade_test.go @@ -1,34 +1,94 @@ package run import ( + "fmt" "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "strings" "testing" ) -func TestNewUpgrade(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() +type UpgradeTestSuite struct { + suite.Suite + ctrl *gomock.Controller + mockCmd *Mockcmd + originalCommand func(string, ...string) cmd +} - mCmd := NewMockcmd(ctrl) - originalCommand := command +func (suite *UpgradeTestSuite) 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 *UpgradeTestSuite) AfterTest(_, _ string) { + command = suite.originalCommand +} + +func TestUpgradeTestSuite(t *testing.T) { + suite.Run(t, new(UpgradeTestSuite)) +} + +func (suite *UpgradeTestSuite) TestPrepare() { + defer suite.ctrl.Finish() + + u := Upgrade{ + Chart: "at40", + Release: "jonas_brothers_only_human", + } command = func(path string, args ...string) cmd { - assert.Equal(t, helmBin, path) - assert.Equal(t, []string{"upgrade", "--install", "jonas_brothers_only_human", "at40"}, args) + suite.Equal(helmBin, path) + suite.Equal([]string{"upgrade", "--install", "jonas_brothers_only_human", "at40"}, args) - return mCmd + return suite.mockCmd } - defer func() { command = originalCommand }() - mCmd.EXPECT(). + suite.mockCmd.EXPECT(). Stdout(gomock.Any()) - mCmd.EXPECT(). + suite.mockCmd.EXPECT(). Stderr(gomock.Any()) - mCmd.EXPECT(). + suite.mockCmd.EXPECT(). Run(). Times(1) - u := NewUpgrade("jonas_brothers_only_human", "at40") - u.Run() + err := u.Prepare(Config{}) + suite.Require().Nil(err) + u.Execute() +} + +func (suite *UpgradeTestSuite) TestPrepareDebugFlag() { + u := Upgrade{ + Chart: "at40", + Release: "lewis_capaldi_someone_you_loved", + } + + 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(&stdout) + suite.mockCmd.EXPECT(). + Stderr(&stderr) + + u.Prepare(cfg) + + want := fmt.Sprintf("Generated command: '%s --debug upgrade --install lewis_capaldi_someone_you_loved at40'\n", helmBin) + suite.Equal(want, stderr.String()) + suite.Equal("", stdout.String()) }