diff --git a/internal/helm/plan.go b/internal/helm/plan.go index 61bc98e..1d4ced9 100644 --- a/internal/helm/plan.go +++ b/internal/helm/plan.go @@ -59,8 +59,8 @@ func determineSteps(cfg Config) *func(Config) []Step { switch cfg.Command { case "upgrade": return &upgrade - case "delete": - panic("not implemented") + case "uninstall", "delete": + return &uninstall case "lint": return &lint case "help": @@ -69,6 +69,8 @@ func determineSteps(cfg Config) *func(Config) []Step { switch cfg.DroneEvent { case "push", "tag", "deployment", "pull_request", "promote", "rollback": return &upgrade + case "delete": + return &uninstall default: panic("not implemented") } @@ -91,16 +93,7 @@ func (p *Plan) Execute() error { } var upgrade = func(cfg Config) []Step { - steps := make([]Step, 0) - - steps = append(steps, &run.InitKube{ - SkipTLSVerify: cfg.SkipTLSVerify, - Certificate: cfg.Certificate, - APIServer: cfg.APIServer, - ServiceAccount: cfg.ServiceAccount, - Token: cfg.KubeToken, - TemplateFile: kubeConfigTemplate, - }) + steps := initKube(cfg) steps = append(steps, &run.Upgrade{ Chart: cfg.Chart, @@ -116,6 +109,16 @@ var upgrade = func(cfg Config) []Step { return steps } +var uninstall = func(cfg Config) []Step { + steps := initKube(cfg) + steps = append(steps, &run.Uninstall{ + Release: cfg.Release, + DryRun: cfg.DryRun, + }) + + return steps +} + var lint = func(cfg Config) []Step { lint := &run.Lint{ Chart: cfg.Chart, @@ -128,3 +131,16 @@ var help = func(cfg Config) []Step { help := &run.Help{} return []Step{help} } + +func initKube(cfg Config) []Step { + return []Step{ + &run.InitKube{ + SkipTLSVerify: cfg.SkipTLSVerify, + Certificate: cfg.Certificate, + APIServer: cfg.APIServer, + ServiceAccount: cfg.ServiceAccount, + Token: cfg.KubeToken, + TemplateFile: kubeConfigTemplate, + }, + } +} diff --git a/internal/helm/plan_test.go b/internal/helm/plan_test.go index 34708e6..e5a1a96 100644 --- a/internal/helm/plan_test.go +++ b/internal/helm/plan_test.go @@ -87,43 +87,24 @@ func (suite *PlanTestSuite) TestNewPlanAbortsOnError() { func (suite *PlanTestSuite) TestUpgrade() { cfg := Config{ - KubeToken: "cXVlZXIgY2hhcmFjdGVyCg==", - SkipTLSVerify: true, - Certificate: "b2Ygd29rZW5lc3MK", - APIServer: "123.456.78.9", - ServiceAccount: "helmet", - ChartVersion: "seventeen", - DryRun: true, - Wait: true, - ReuseValues: true, - Timeout: "go sit in the corner", - Chart: "billboard_top_100", - Release: "post_malone_circles", - Force: true, + ChartVersion: "seventeen", + DryRun: true, + Wait: true, + ReuseValues: true, + Timeout: "go sit in the corner", + Chart: "billboard_top_100", + Release: "post_malone_circles", + Force: true, } steps := upgrade(cfg) - - suite.Equal(2, len(steps)) - + suite.Require().Equal(2, len(steps), "upgrade should return 2 steps") suite.Require().IsType(&run.InitKube{}, steps[0]) - init, _ := steps[0].(*run.InitKube) - - var expected Step = &run.InitKube{ - SkipTLSVerify: cfg.SkipTLSVerify, - Certificate: cfg.Certificate, - APIServer: cfg.APIServer, - ServiceAccount: cfg.ServiceAccount, - Token: cfg.KubeToken, - TemplateFile: kubeConfigTemplate, - } - - suite.Equal(expected, init) suite.Require().IsType(&run.Upgrade{}, steps[1]) upgrade, _ := steps[1].(*run.Upgrade) - expected = &run.Upgrade{ + expected := &run.Upgrade{ Chart: cfg.Chart, Release: cfg.Release, ChartVersion: cfg.ChartVersion, @@ -137,6 +118,68 @@ func (suite *PlanTestSuite) TestUpgrade() { suite.Equal(expected, upgrade) } +func (suite *PlanTestSuite) TestDel() { + cfg := Config{ + KubeToken: "b2YgbXkgYWZmZWN0aW9u", + SkipTLSVerify: true, + Certificate: "cHJvY2xhaW1zIHdvbmRlcmZ1bCBmcmllbmRzaGlw", + APIServer: "98.765.43.21", + ServiceAccount: "greathelm", + DryRun: true, + Timeout: "think about what you did", + Release: "jetta_id_love_to_change_the_world", + } + + steps := uninstall(cfg) + suite.Require().Equal(2, len(steps), "uninstall should return 2 steps") + + suite.Require().IsType(&run.InitKube{}, steps[0]) + init, _ := steps[0].(*run.InitKube) + var expected Step = &run.InitKube{ + SkipTLSVerify: true, + Certificate: "cHJvY2xhaW1zIHdvbmRlcmZ1bCBmcmllbmRzaGlw", + APIServer: "98.765.43.21", + ServiceAccount: "greathelm", + Token: "b2YgbXkgYWZmZWN0aW9u", + TemplateFile: kubeConfigTemplate, + } + + suite.Equal(expected, init) + + suite.Require().IsType(&run.Uninstall{}, steps[1]) + actual, _ := steps[1].(*run.Uninstall) + expected = &run.Uninstall{ + Release: "jetta_id_love_to_change_the_world", + DryRun: true, + } + suite.Equal(expected, actual) +} + +func (suite *PlanTestSuite) TestInitKube() { + cfg := Config{ + KubeToken: "cXVlZXIgY2hhcmFjdGVyCg==", + SkipTLSVerify: true, + Certificate: "b2Ygd29rZW5lc3MK", + APIServer: "123.456.78.9", + ServiceAccount: "helmet", + } + + steps := initKube(cfg) + suite.Require().Equal(1, len(steps), "initKube should return one step") + suite.Require().IsType(&run.InitKube{}, steps[0]) + init, _ := steps[0].(*run.InitKube) + + expected := &run.InitKube{ + SkipTLSVerify: true, + Certificate: "b2Ygd29rZW5lc3MK", + APIServer: "123.456.78.9", + ServiceAccount: "helmet", + Token: "cXVlZXIgY2hhcmFjdGVyCg==", + TemplateFile: kubeConfigTemplate, + } + suite.Equal(expected, init) +} + func (suite *PlanTestSuite) TestLint() { cfg := Config{ Chart: "./flow", @@ -170,6 +213,31 @@ func (suite *PlanTestSuite) TestDeterminePlanUpgradeFromDroneEvent() { } } +func (suite *PlanTestSuite) TestDeterminePlanUninstallCommand() { + cfg := Config{ + Command: "uninstall", + } + stepsMaker := determineSteps(cfg) + suite.Same(&uninstall, stepsMaker) +} + +// helm_command = delete is provided as an alias for backwards-compatibility with drone-helm +func (suite *PlanTestSuite) TestDeterminePlanDeleteCommand() { + cfg := Config{ + Command: "delete", + } + stepsMaker := determineSteps(cfg) + suite.Same(&uninstall, stepsMaker) +} + +func (suite *PlanTestSuite) TestDeterminePlanDeleteFromDroneEvent() { + cfg := Config{ + DroneEvent: "delete", + } + stepsMaker := determineSteps(cfg) + suite.Same(&uninstall, stepsMaker) +} + func (suite *PlanTestSuite) TestDeterminePlanLintCommand() { cfg := Config{ Command: "lint", diff --git a/internal/run/uninstall.go b/internal/run/uninstall.go new file mode 100644 index 0000000..1b9d0b2 --- /dev/null +++ b/internal/run/uninstall.go @@ -0,0 +1,51 @@ +package run + +import ( + "fmt" +) + +// Uninstall is an execution step that calls `helm uninstall` when executed. +type Uninstall struct { + Release string + DryRun bool + cmd cmd +} + +// Execute executes the `helm uninstall` command. +func (u *Uninstall) Execute(_ Config) error { + return u.cmd.Run() +} + +// Prepare gets the Uninstall ready to execute. +func (u *Uninstall) Prepare(cfg Config) error { + if u.Release == "" { + return fmt.Errorf("release is required") + } + + args := []string{"--kubeconfig", cfg.KubeConfig} + + if cfg.Namespace != "" { + args = append(args, "--namespace", cfg.Namespace) + } + if cfg.Debug { + args = append(args, "--debug") + } + + args = append(args, "uninstall") + + if u.DryRun { + args = append(args, "--dry-run") + } + + args = append(args, u.Release) + + u.cmd = command(helmBin, args...) + u.cmd.Stdout(cfg.Stdout) + u.cmd.Stderr(cfg.Stderr) + + if cfg.Debug { + fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", u.cmd.String()) + } + + return nil +} diff --git a/internal/run/uninstall_test.go b/internal/run/uninstall_test.go new file mode 100644 index 0000000..9e826c9 --- /dev/null +++ b/internal/run/uninstall_test.go @@ -0,0 +1,141 @@ +package run + +import ( + "fmt" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/suite" + "strings" + "testing" +) + +type UninstallTestSuite struct { + suite.Suite + ctrl *gomock.Controller + mockCmd *Mockcmd + actualArgs []string + originalCommand func(string, ...string) cmd +} + +func (suite *UninstallTestSuite) BeforeTest(_, _ string) { + suite.ctrl = gomock.NewController(suite.T()) + suite.mockCmd = NewMockcmd(suite.ctrl) + + suite.originalCommand = command + command = func(path string, args ...string) cmd { + suite.actualArgs = args + return suite.mockCmd + } +} + +func (suite *UninstallTestSuite) AfterTest(_, _ string) { + command = suite.originalCommand +} + +func TestUninstallTestSuite(t *testing.T) { + suite.Run(t, new(UninstallTestSuite)) +} + +func (suite *UninstallTestSuite) TestPrepareAndExecute() { + defer suite.ctrl.Finish() + + u := Uninstall{ + Release: "zayde_wølf_king", + } + + actual := []string{} + command = func(path string, args ...string) cmd { + suite.Equal(helmBin, path) + actual = args + + return suite.mockCmd + } + + suite.mockCmd.EXPECT(). + Stdout(gomock.Any()) + suite.mockCmd.EXPECT(). + Stderr(gomock.Any()) + suite.mockCmd.EXPECT(). + Run(). + Times(1) + + cfg := Config{ + KubeConfig: "/root/.kube/config", + } + suite.NoError(u.Prepare(cfg)) + expected := []string{"--kubeconfig", "/root/.kube/config", "uninstall", "zayde_wølf_king"} + suite.Equal(expected, actual) + + u.Execute(cfg) +} + +func (suite *UninstallTestSuite) TestPrepareDryRunFlag() { + u := Uninstall{ + Release: "firefox_ak_wildfire", + DryRun: true, + } + cfg := Config{ + KubeConfig: "/root/.kube/config", + } + + suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() + suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() + + suite.NoError(u.Prepare(cfg)) + expected := []string{"--kubeconfig", "/root/.kube/config", "uninstall", "--dry-run", "firefox_ak_wildfire"} + suite.Equal(expected, suite.actualArgs) +} + +func (suite *UninstallTestSuite) TestPrepareNamespaceFlag() { + u := Uninstall{ + Release: "carly_simon_run_away_with_me", + } + cfg := Config{ + KubeConfig: "/root/.kube/config", + Namespace: "emotion", + } + + suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() + suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() + + suite.NoError(u.Prepare(cfg)) + expected := []string{"--kubeconfig", "/root/.kube/config", + "--namespace", "emotion", "uninstall", "carly_simon_run_away_with_me"} + suite.Equal(expected, suite.actualArgs) +} + +func (suite *UninstallTestSuite) TestPrepareDebugFlag() { + u := Uninstall{ + Release: "just_a_band_huff_and_puff", + } + stderr := strings.Builder{} + cfg := Config{ + KubeConfig: "/root/.kube/config", + Debug: true, + 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(&stderr).AnyTimes() + + suite.NoError(u.Prepare(cfg)) + suite.Equal(fmt.Sprintf("Generated command: '%s --kubeconfig /root/.kube/config "+ + "--debug uninstall just_a_band_huff_and_puff'\n", helmBin), stderr.String()) +} + +func (suite *UninstallTestSuite) TestPrepareRequiresRelease() { + // These aren't really expected, but allowing them gives clearer test-failure messages + suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() + suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() + + u := Uninstall{} + err := u.Prepare(Config{}) + suite.EqualError(err, "release is required", "Uninstall.Release should be mandatory") +}