diff --git a/Dockerfile b/Dockerfile index 6f10062..492dd54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM alpine/helm MAINTAINER Erin Call COPY build/drone-helm /bin/drone-helm +COPY kubeconfig /root/.kube/config.tpl LABEL description="Helm 3 plugin for Drone 3" LABEL base="alpine/helm" diff --git a/README.md b/README.md index 192d697..a39eb59 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ TODO: * [x] Make `cmd/drone-helm/main.go` actually invoke `helm` * [x] Make `golint` part of the build process (and make it pass) * [x] Implement debug output -* [ ] Flesh out `helm upgrade` until it's capable of working +* [x] Flesh out `helm upgrade` until it's capable of working * [ ] Implement config settings for `upgrade` * [ ] Implement `helm lint` * [ ] Implement `helm delete` diff --git a/internal/helm/config.go b/internal/helm/config.go index e2513e8..9795cd2 100644 --- a/internal/helm/config.go +++ b/internal/helm/config.go @@ -25,7 +25,7 @@ type Config struct { StringValues string `split_words:"true"` ValuesFiles []string `split_words:"true"` Namespace string `` - Token string `envconfig:"KUBERNETES_TOKEN"` + KubeToken string `envconfig:"KUBERNETES_TOKEN"` SkipTLSVerify bool `envconfig:"SKIP_TLS_VERIFY"` Certificate string `envconfig:"KUBERNETES_CERTIFICATE"` APIServer string `envconfig:"API_SERVER"` diff --git a/internal/helm/mock_step_test.go b/internal/helm/mock_step_test.go new file mode 100644 index 0000000..5387162 --- /dev/null +++ b/internal/helm/mock_step_test.go @@ -0,0 +1,62 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/helm/plan.go + +// Package mock_helm is a generated GoMock package. +package helm + +import ( + gomock "github.com/golang/mock/gomock" + run "github.com/pelotech/drone-helm3/internal/run" + reflect "reflect" +) + +// MockStep is a mock of Step interface +type MockStep struct { + ctrl *gomock.Controller + recorder *MockStepMockRecorder +} + +// MockStepMockRecorder is the mock recorder for MockStep +type MockStepMockRecorder struct { + mock *MockStep +} + +// NewMockStep creates a new mock instance +func NewMockStep(ctrl *gomock.Controller) *MockStep { + mock := &MockStep{ctrl: ctrl} + mock.recorder = &MockStepMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockStep) EXPECT() *MockStepMockRecorder { + return m.recorder +} + +// Prepare mocks base method +func (m *MockStep) Prepare(arg0 run.Config) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Prepare", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Prepare indicates an expected call of Prepare +func (mr *MockStepMockRecorder) Prepare(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockStep)(nil).Prepare), arg0) +} + +// Execute mocks base method +func (m *MockStep) Execute(arg0 run.Config) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Execute", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Execute indicates an expected call of Execute +func (mr *MockStepMockRecorder) Execute(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockStep)(nil).Execute), arg0) +} diff --git a/internal/helm/plan.go b/internal/helm/plan.go index 285c7b6..1364112 100644 --- a/internal/helm/plan.go +++ b/internal/helm/plan.go @@ -1,89 +1,108 @@ package helm import ( - "errors" "fmt" "github.com/pelotech/drone-helm3/internal/run" "os" ) +const kubeConfigTemplate = "/root/.kube/config.tpl" + // A Step is one step in the plan. type Step interface { Prepare(run.Config) error - Execute() error + Execute(run.Config) error } // A Plan is a series of steps to perform. type Plan struct { - steps []Step + steps []Step + cfg Config + runCfg run.Config } // 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{ + cfg: cfg, + runCfg: run.Config{ + Debug: cfg.Debug, + KubeConfig: cfg.KubeConfig, + Values: cfg.Values, + StringValues: cfg.StringValues, + ValuesFiles: cfg.ValuesFiles, + Namespace: cfg.Namespace, + Stdout: os.Stdout, + Stderr: os.Stderr, + }, } - p := Plan{} - switch cfg.Command { - case "upgrade": - steps, err := upgrade(cfg, runCfg) - if err != nil { - return nil, err + p.steps = (*determineSteps(cfg))(cfg) + + for i, step := range p.steps { + if cfg.Debug { + fmt.Fprintf(os.Stderr, "calling %T.Prepare (step %d)\n", step, i) } - p.steps = steps - case "delete": - return nil, errors.New("not implemented") - case "lint": - return nil, errors.New("not implemented") - case "help": - steps, err := help(cfg, runCfg) - if err != nil { + + if err := step.Prepare(p.runCfg); err != nil { + err = fmt.Errorf("while preparing %T step: %w", step, err) return nil, err } - p.steps = steps - default: - switch cfg.DroneEvent { - case "push", "tag", "deployment", "pull_request", "promote", "rollback": - steps, err := upgrade(cfg, runCfg) - if err != nil { - return nil, err - } - p.steps = steps - default: - return nil, errors.New("not implemented") - } } return &p, nil } +// determineSteps is primarily for the tests' convenience: it allows testing the "which stuff should +// we do" logic without building a config that meets all the steps' requirements. +func determineSteps(cfg Config) *func(Config) []Step { + switch cfg.Command { + case "upgrade": + return &upgrade + case "delete": + panic("not implemented") + case "lint": + panic("not implemented") + case "help": + return &help + default: + switch cfg.DroneEvent { + case "push", "tag", "deployment", "pull_request", "promote", "rollback": + return &upgrade + default: + panic("not implemented") + } + } +} + // 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.Execute(); err != nil { - return err + for i, step := range p.steps { + if p.cfg.Debug { + fmt.Fprintf(os.Stderr, "calling %T.Execute (step %d)\n", step, i) + } + + if err := step.Execute(p.runCfg); err != nil { + return fmt.Errorf("in execution step %d: %w", i, err) } } return nil } -func upgrade(cfg Config, runCfg run.Config) ([]Step, error) { +var upgrade = func(cfg Config) []Step { steps := make([]Step, 0) - upgrade := &run.Upgrade{ + + steps = append(steps, &run.InitKube{ + SkipTLSVerify: cfg.SkipTLSVerify, + Certificate: cfg.Certificate, + APIServer: cfg.APIServer, + ServiceAccount: cfg.ServiceAccount, + Token: cfg.KubeToken, + TemplateFile: kubeConfigTemplate, + }) + + steps = append(steps, &run.Upgrade{ Chart: cfg.Chart, Release: cfg.Release, ChartVersion: cfg.ChartVersion, @@ -91,23 +110,12 @@ func upgrade(cfg Config, runCfg run.Config) ([]Step, error) { 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 + return steps } -func help(cfg Config, runCfg run.Config) ([]Step, error) { +var help = func(cfg Config) []Step { 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 + return []Step{help} } diff --git a/internal/helm/plan_test.go b/internal/helm/plan_test.go index 22bbd3b..c34bc48 100644 --- a/internal/helm/plan_test.go +++ b/internal/helm/plan_test.go @@ -2,7 +2,9 @@ package helm import ( "fmt" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/suite" + "os" "testing" "github.com/pelotech/drone-helm3/internal/run" @@ -16,48 +18,147 @@ func TestPlanTestSuite(t *testing.T) { suite.Run(t, new(PlanTestSuite)) } -func (suite *PlanTestSuite) TestNewPlanUpgradeCommand() { - cfg := Config{ - Command: "upgrade", - Chart: "billboard_top_100", - Release: "post_malone_circles", +func (suite *PlanTestSuite) TestNewPlan() { + ctrl := gomock.NewController(suite.T()) + stepOne := NewMockStep(ctrl) + stepTwo := NewMockStep(ctrl) + + origHelp := help + help = func(cfg Config) []Step { + return []Step{stepOne, stepTwo} } + defer func() { help = origHelp }() + + cfg := Config{ + Command: "help", + Debug: false, + KubeConfig: "/branch/.sfere/profig", + Values: "steadfastness,forthrightness", + StringValues: "tensile_strength,flexibility", + ValuesFiles: []string{"/root/price_inventory.yml"}, + Namespace: "outer", + } + + runCfg := run.Config{ + Debug: false, + KubeConfig: "/branch/.sfere/profig", + Values: "steadfastness,forthrightness", + StringValues: "tensile_strength,flexibility", + ValuesFiles: []string{"/root/price_inventory.yml"}, + Namespace: "outer", + Stdout: os.Stdout, + Stderr: os.Stderr, + } + + stepOne.EXPECT(). + Prepare(runCfg) + stepTwo.EXPECT(). + Prepare(runCfg) plan, err := NewPlan(cfg) suite.Require().Nil(err) - suite.Require().Equal(1, len(plan.steps)) - - 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) + suite.Equal(cfg, plan.cfg) + suite.Equal(runCfg, plan.runCfg) } -func (suite *PlanTestSuite) TestNewPlanUpgradeFromDroneEvent() { - cfg := Config{ - Chart: "billboard_top_100", - Release: "lizzo_good_as_hell", - } +func (suite *PlanTestSuite) TestNewPlanAbortsOnError() { + ctrl := gomock.NewController(suite.T()) + stepOne := NewMockStep(ctrl) + stepTwo := NewMockStep(ctrl) - upgradeEvents := []string{"push", "tag", "deployment", "pull_request", "promote", "rollback"} - for _, event := range upgradeEvents { - cfg.DroneEvent = event - plan, err := NewPlan(cfg) - suite.Require().Nil(err) - suite.Require().Equal(1, len(plan.steps), fmt.Sprintf("for event type '%s'", event)) - suite.IsType(&run.Upgrade{}, plan.steps[0], fmt.Sprintf("for event type '%s'", event)) + origHelp := help + help = func(cfg Config) []Step { + return []Step{stepOne, stepTwo} } -} + defer func() { help = origHelp }() -func (suite *PlanTestSuite) TestNewPlanHelpCommand() { cfg := Config{ Command: "help", } - plan, err := NewPlan(cfg) - suite.Require().Nil(err) - suite.Equal(1, len(plan.steps)) + stepOne.EXPECT(). + Prepare(gomock.Any()). + Return(fmt.Errorf("I'm starry Dave, aye, cat blew that")) - suite.Require().IsType(&run.Help{}, plan.steps[0]) + _, err := NewPlan(cfg) + suite.Require().NotNil(err) + suite.EqualError(err, "while preparing *helm.MockStep step: I'm starry Dave, aye, cat blew that") +} + +func (suite *PlanTestSuite) TestUpgrade() { + cfg := Config{ + KubeToken: "cXVlZXIgY2hhcmFjdGVyCg==", + SkipTLSVerify: true, + Certificate: "b2Ygd29rZW5lc3MK", + APIServer: "123.456.78.9", + ServiceAccount: "helmet", + ChartVersion: "seventeen", + 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().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{ + Chart: cfg.Chart, + Release: cfg.Release, + ChartVersion: cfg.ChartVersion, + Wait: cfg.Wait, + ReuseValues: cfg.ReuseValues, + Timeout: cfg.Timeout, + Force: cfg.Force, + } + + suite.Equal(expected, upgrade) +} + +func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() { + cfg := Config{ + Command: "upgrade", + } + stepsMaker := determineSteps(cfg) + suite.Same(&upgrade, stepsMaker) +} + +func (suite *PlanTestSuite) TestDeterminePlanUpgradeFromDroneEvent() { + cfg := Config{} + + upgradeEvents := []string{"push", "tag", "deployment", "pull_request", "promote", "rollback"} + for _, event := range upgradeEvents { + cfg.DroneEvent = event + stepsMaker := determineSteps(cfg) + suite.Same(&upgrade, stepsMaker, fmt.Sprintf("for event type '%s'", event)) + } +} + +func (suite *PlanTestSuite) TestDeterminePlanHelpCommand() { + cfg := Config{ + Command: "help", + } + + stepsMaker := determineSteps(cfg) + suite.Same(&help, stepsMaker) } diff --git a/internal/run/config.go b/internal/run/config.go index 59b9429..09b9642 100644 --- a/internal/run/config.go +++ b/internal/run/config.go @@ -6,17 +6,12 @@ import ( // 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 + Debug bool + KubeConfig string + Values string + StringValues string + ValuesFiles []string + Namespace string + Stdout io.Writer + Stderr io.Writer } diff --git a/internal/run/help.go b/internal/run/help.go index 03805f6..8597815 100644 --- a/internal/run/help.go +++ b/internal/run/help.go @@ -10,7 +10,7 @@ type Help struct { } // Execute executes the `helm help` command. -func (h *Help) Execute() error { +func (h *Help) Execute(_ Config) error { return h.cmd.Run() } diff --git a/internal/run/help_test.go b/internal/run/help_test.go index 7339c69..1824578 100644 --- a/internal/run/help_test.go +++ b/internal/run/help_test.go @@ -50,7 +50,7 @@ func (suite *HelpTestSuite) TestPrepare() { h := Help{} err := h.Prepare(cfg) suite.Require().Nil(err) - h.Execute() + h.Execute(cfg) } func (suite *HelpTestSuite) TestPrepareDebugFlag() { diff --git a/internal/run/initkube.go b/internal/run/initkube.go new file mode 100644 index 0000000..4af29af --- /dev/null +++ b/internal/run/initkube.go @@ -0,0 +1,93 @@ +package run + +import ( + "errors" + "fmt" + "io" + "os" + "text/template" +) + +// InitKube is a step in a helm Plan that initializes the kubernetes config file. +type InitKube struct { + SkipTLSVerify bool + Certificate string + APIServer string + ServiceAccount string + Token string + TemplateFile string + + template *template.Template + configFile io.WriteCloser + values kubeValues +} + +type kubeValues struct { + SkipTLSVerify bool + Certificate string + APIServer string + Namespace string + ServiceAccount string + Token string +} + +// Execute generates a kubernetes config file from drone-helm3's template. +func (i *InitKube) Execute(cfg Config) error { + if cfg.Debug { + fmt.Fprintf(cfg.Stderr, "writing kubeconfig file to %s\n", cfg.KubeConfig) + } + defer i.configFile.Close() + return i.template.Execute(i.configFile, i.values) +} + +// Prepare ensures all required configuration is present and that the config file is writable. +func (i *InitKube) Prepare(cfg Config) error { + var err error + + if i.APIServer == "" { + return errors.New("an API Server is needed to deploy") + } + if i.Token == "" { + return errors.New("token is needed to deploy") + } + if i.Certificate == "" && !i.SkipTLSVerify { + return errors.New("certificate is needed to deploy") + } + + if i.ServiceAccount == "" { + i.ServiceAccount = "helm" + } + + if cfg.Debug { + fmt.Fprintf(cfg.Stderr, "loading kubeconfig template from %s\n", i.TemplateFile) + } + i.template, err = template.ParseFiles(i.TemplateFile) + if err != nil { + return fmt.Errorf("could not load kubeconfig template: %w", err) + } + + i.values = kubeValues{ + SkipTLSVerify: i.SkipTLSVerify, + Certificate: i.Certificate, + APIServer: i.APIServer, + ServiceAccount: i.ServiceAccount, + Token: i.Token, + Namespace: cfg.Namespace, + } + + if cfg.Debug { + if _, err := os.Stat(cfg.KubeConfig); err != nil { + // non-nil err here isn't an actual error state; the kubeconfig just doesn't exist + fmt.Fprint(cfg.Stderr, "creating ") + } else { + fmt.Fprint(cfg.Stderr, "truncating ") + } + fmt.Fprintf(cfg.Stderr, "kubeconfig file at %s\n", cfg.KubeConfig) + } + + i.configFile, err = os.Create(cfg.KubeConfig) + if err != nil { + return fmt.Errorf("could not open kubeconfig file for writing: %w", err) + } + return nil +} diff --git a/internal/run/initkube_test.go b/internal/run/initkube_test.go new file mode 100644 index 0000000..fb32b15 --- /dev/null +++ b/internal/run/initkube_test.go @@ -0,0 +1,184 @@ +package run + +import ( + "io/ioutil" + "os" + "text/template" + // "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "testing" +) + +type InitKubeTestSuite struct { + suite.Suite +} + +func TestInitKubeTestSuite(t *testing.T) { + suite.Run(t, new(InitKubeTestSuite)) +} + +func (suite *InitKubeTestSuite) TestPrepareExecute() { + templateFile, err := tempfile("kubeconfig********.yml.tpl", ` +certificate: {{ .Certificate }} +namespace: {{ .Namespace }} +`) + defer os.Remove(templateFile.Name()) + suite.Require().Nil(err) + + configFile, err := tempfile("kubeconfig********.yml", "") + defer os.Remove(configFile.Name()) + suite.Require().Nil(err) + + init := InitKube{ + APIServer: "Sysadmin", + Certificate: "CCNA", + Token: "Aspire virtual currency", + TemplateFile: templateFile.Name(), + } + cfg := Config{ + Namespace: "Cisco", + KubeConfig: configFile.Name(), + } + err = init.Prepare(cfg) + suite.Require().Nil(err) + + suite.IsType(&template.Template{}, init.template) + suite.NotNil(init.configFile) + + err = init.Execute(cfg) + suite.Require().Nil(err) + + conf, err := ioutil.ReadFile(configFile.Name()) + suite.Require().Nil(err) + + want := ` +certificate: CCNA +namespace: Cisco +` + suite.Equal(want, string(conf)) +} + +func (suite *InitKubeTestSuite) TestPrepareParseError() { + templateFile, err := tempfile("kubeconfig********.yml.tpl", `{{ NonexistentFunction }}`) + defer os.Remove(templateFile.Name()) + suite.Require().Nil(err) + + init := InitKube{ + APIServer: "Sysadmin", + Certificate: "CCNA", + Token: "Aspire virtual currency", + TemplateFile: templateFile.Name(), + } + err = init.Prepare(Config{}) + suite.Error(err) + suite.Regexp("could not load kubeconfig .* function .* not defined", err) +} + +func (suite *InitKubeTestSuite) TestPrepareNonexistentTemplateFile() { + init := InitKube{ + APIServer: "Sysadmin", + Certificate: "CCNA", + Token: "Aspire virtual currency", + TemplateFile: "/usr/foreign/exclude/kubeprofig.tpl", + } + err := init.Prepare(Config{}) + suite.Error(err) + suite.Regexp("could not load kubeconfig .* no such file or directory", err) +} + +func (suite *InitKubeTestSuite) TestPrepareCannotOpenDestinationFile() { + templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity") + defer os.Remove(templateFile.Name()) + suite.Require().Nil(err) + init := InitKube{ + APIServer: "Sysadmin", + Certificate: "CCNA", + Token: "Aspire virtual currency", + TemplateFile: templateFile.Name(), + } + + cfg := Config{ + KubeConfig: "/usr/foreign/exclude/kubeprofig", + } + err = init.Prepare(cfg) + suite.Error(err) + suite.Regexp("could not open .* for writing: .* no such file or directory", err) +} + +func (suite *InitKubeTestSuite) TestPrepareRequiredConfig() { + templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity") + defer os.Remove(templateFile.Name()) + suite.Require().Nil(err) + + configFile, err := tempfile("kubeconfig********.yml", "") + defer os.Remove(configFile.Name()) + suite.Require().Nil(err) + + // initial config with all required fields present + init := InitKube{ + APIServer: "Sysadmin", + Certificate: "CCNA", + Token: "Aspire virtual currency", + TemplateFile: templateFile.Name(), + } + + cfg := Config{ + KubeConfig: configFile.Name(), + } + + suite.NoError(init.Prepare(cfg)) // consistency check; we should be starting in a happy state + + init.APIServer = "" + suite.Error(init.Prepare(cfg), "APIServer should be required.") + + init.APIServer = "Sysadmin" + init.Token = "" + suite.Error(init.Prepare(cfg), "Token should be required.") + + init.Token = "Aspire virtual currency" + init.Certificate = "" + suite.Error(init.Prepare(cfg), "Certificate should be required.") + + init.SkipTLSVerify = true + suite.NoError(init.Prepare(cfg), "Certificate should not be required if SkipTLSVerify is true") +} + +func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() { + templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity") + defer os.Remove(templateFile.Name()) + suite.Require().Nil(err) + + configFile, err := tempfile("kubeconfig********.yml", "") + defer os.Remove(configFile.Name()) + suite.Require().Nil(err) + + init := InitKube{ + APIServer: "Sysadmin", + Certificate: "CCNA", + Token: "Aspire virtual currency", + TemplateFile: templateFile.Name(), + } + + cfg := Config{ + KubeConfig: configFile.Name(), + } + + init.Prepare(cfg) + suite.Equal("helm", init.ServiceAccount) +} + +func tempfile(name, contents string) (*os.File, error) { + file, err := ioutil.TempFile("", name) + if err != nil { + return nil, err + } + _, err = file.Write([]byte(contents)) + if err != nil { + return nil, err + } + err = file.Close() + if err != nil { + return nil, err + } + return file, nil +} diff --git a/internal/run/upgrade.go b/internal/run/upgrade.go index 1a76b46..9fa6180 100644 --- a/internal/run/upgrade.go +++ b/internal/run/upgrade.go @@ -19,13 +19,19 @@ type Upgrade struct { } // Execute executes the `helm upgrade` command. -func (u *Upgrade) Execute() error { +func (u *Upgrade) Execute(_ Config) error { return u.cmd.Run() } // Prepare gets the Upgrade ready to execute. func (u *Upgrade) Prepare(cfg Config) error { - args := []string{"upgrade", "--install", u.Release, u.Chart} + args := []string{"--kubeconfig", cfg.KubeConfig} + + if cfg.Namespace != "" { + args = append(args, "--namespace", cfg.Namespace) + } + + args = append(args, "upgrade", "--install", u.Release, u.Chart) if cfg.Debug { args = append([]string{"--debug"}, args...) diff --git a/internal/run/upgrade_test.go b/internal/run/upgrade_test.go index 48c670d..19b84b9 100644 --- a/internal/run/upgrade_test.go +++ b/internal/run/upgrade_test.go @@ -41,7 +41,8 @@ func (suite *UpgradeTestSuite) TestPrepare() { command = func(path string, args ...string) cmd { suite.Equal(helmBin, path) - suite.Equal([]string{"upgrade", "--install", "jonas_brothers_only_human", "at40"}, args) + suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "upgrade", "--install", + "jonas_brothers_only_human", "at40"}, args) return suite.mockCmd } @@ -54,9 +55,44 @@ func (suite *UpgradeTestSuite) TestPrepare() { Run(). Times(1) - err := u.Prepare(Config{}) + cfg := Config{ + KubeConfig: "/root/.kube/config", + } + err := u.Prepare(cfg) suite.Require().Nil(err) - u.Execute() + u.Execute(cfg) +} + +func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() { + defer suite.ctrl.Finish() + + u := Upgrade{ + Chart: "at40", + Release: "shaed_trampoline", + } + + command = func(path string, args ...string) cmd { + suite.Equal(helmBin, path) + suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "--namespace", "melt", "upgrade", + "--install", "shaed_trampoline", "at40"}, args) + + return suite.mockCmd + } + + suite.mockCmd.EXPECT(). + Stdout(gomock.Any()) + suite.mockCmd.EXPECT(). + Stderr(gomock.Any()) + suite.mockCmd.EXPECT(). + Run() + + cfg := Config{ + Namespace: "melt", + KubeConfig: "/root/.kube/config", + } + err := u.Prepare(cfg) + suite.Require().Nil(err) + u.Execute(cfg) } func (suite *UpgradeTestSuite) TestPrepareDebugFlag() { @@ -68,9 +104,10 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() { stdout := strings.Builder{} stderr := strings.Builder{} cfg := Config{ - Debug: true, - Stdout: &stdout, - Stderr: &stderr, + Debug: true, + KubeConfig: "/root/.kube/config", + Stdout: &stdout, + Stderr: &stderr, } command = func(path string, args ...string) cmd { @@ -88,7 +125,8 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() { u.Prepare(cfg) - want := fmt.Sprintf("Generated command: '%s --debug upgrade --install lewis_capaldi_someone_you_loved at40'\n", helmBin) + want := fmt.Sprintf("Generated command: '%s --debug --kubeconfig /root/.kube/config upgrade "+ + "--install lewis_capaldi_someone_you_loved at40'\n", helmBin) suite.Equal(want, stderr.String()) suite.Equal("", stdout.String()) } diff --git a/kubeconfig b/kubeconfig new file mode 100644 index 0000000..c7b2025 --- /dev/null +++ b/kubeconfig @@ -0,0 +1,39 @@ +apiVersion: v1 +clusters: +- cluster: +{{- if eq .SkipTLSVerify true }} + insecure-skip-tls-verify: true +{{- else }} + certificate-authority-data: {{ .Certificate }} +{{- end}} + server: {{ .APIServer }} + name: helm +contexts: +- context: + cluster: helm +{{- if .Namespace }} + namespace: {{ .Namespace }} +{{- end }} + user: {{ .ServiceAccount }} + name: helm +current-context: "helm" +kind: Config +preferences: {} +users: +- name: {{ .ServiceAccount }} + user: +{{- if .Token }} + token: {{ .Token }} +{{- else if .EKSCluster }} + exec: + apiVersion: client.authentication.k8s.io/v1alpha1 + command: aws-iam-authenticator + args: + - "token" + - "-i" + - "{{ .EKSCluster }}" + {{- if .EKSRoleARN }} + - "-r" + - "{{ .EKSRoleARN }}" + {{- end }} +{{- end }}