diff --git a/README.md b/README.md index e13e360..83524de 100644 --- a/README.md +++ b/README.md @@ -4,5 +4,10 @@ TODO: * [x] Make a `.drone.yml` that's sufficient for building an image * [x] Make a `Dockerfile` that's sufficient for launching the built image -* [ ] Make `cmd/drone-helm/main.go` actually invoke `helm` +* [x] Make `cmd/drone-helm/main.go` actually invoke `helm` +* [ ] Flesh out `helm upgrade` until it's capable of working +* [ ] Implement `helm lint` +* [ ] Implement `helm delete` +* [ ] Implement all config settings +* [ ] EKS support * [ ] Change `.drone.yml` to use a real docker registry diff --git a/cmd/drone-helm/main.go b/cmd/drone-helm/main.go index 2490e98..3fbf3a4 100644 --- a/cmd/drone-helm/main.go +++ b/cmd/drone-helm/main.go @@ -2,44 +2,34 @@ package main import ( "fmt" - "github.com/urfave/cli" + "github.com/kelseyhightower/envconfig" "os" - "github.com/pelotech/drone-helm3/internal/run" + "github.com/pelotech/drone-helm3/internal/helm" ) func main() { - app := cli.NewApp() - app.Name = "helm plugin" - app.Usage = "helm plugin" - app.Action = execute - app.Version = "0.0.1α" - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "helm_command", - Usage: "Helm command to execute", - EnvVar: "PLUGIN_HELM_COMMAND,HELM_COMMAND", - }, - } + var c helm.Config - if err := app.Run(os.Args); err != nil { + if err := envconfig.Process("plugin", &c); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + return + } + + // Make the plan + plan, err := helm.NewPlan(c) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + return + } + + // Execute the plan + err = plan.Execute() + + // Expect the plan to go off the rails + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + // Throw away the plan + os.Exit(1) } } - - -func execute(c *cli.Context) error { - switch c.String("helm_command") { - case "upgrade": - run.Upgrade() - case "help": - run.Help() - default: - switch os.Getenv("DRONE_BUILD_EVENT") { - case "push", "tag", "deployment", "pull_request", "promote", "rollback": - run.Upgrade() - default: - run.Help() - } - return nil -} diff --git a/go.mod b/go.mod index 4d30c41..709639b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,6 @@ go 1.13 require ( github.com/golang/mock v1.3.1 + github.com/kelseyhightower/envconfig v1.4.0 github.com/stretchr/testify v1.4.0 - github.com/urfave/cli v1.22.0 ) diff --git a/go.sum b/go.sum index 59ac194..901a1c3 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,14 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/urfave/cli v1.22.0 h1:8nz/RUUotroXnOpYzT/Fy3sBp+2XEbXaY641/s3nbFI= -github.com/urfave/cli v1.22.0/go.mod h1:b3D7uWrF2GilkNgYpgcg6J+JMUw7ehmNkE8sZdliGLc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -21,6 +16,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262 h1:qsl9y/CJx34tuA7QCPNp86JNJe4spst6Ff8MjvPUdPg= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/helm/config.go b/internal/helm/config.go new file mode 100644 index 0000000..cffa2f8 --- /dev/null +++ b/internal/helm/config.go @@ -0,0 +1,58 @@ +package helm + +import ( + "fmt" + "strings" +) + +type Config struct { + // Configuration for drone-helm itself + Command HelmCommand `envconfig:"HELM_COMMAND"` // 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 + Repos []string `envconfig:"HELM_REPOS"` // call `helm repo add` before the main command + Prefix string `` // Prefix to use when looking up secret env vars + + // Global helm config + Debug bool `` // global helm flag (also applies to drone-helm itself) + KubeConfig string `split_words:"true" default:"/root/.kube/config"` // path to the kube config file + Values string `` + StringValues string `split_words:"true"` + ValuesFiles []string `split_words:"true"` + Namespace string `` + Token string `envconfig:"KUBERNETES_TOKEN"` + SkipTLSVerify bool `envconfig:"SKIP_TLS_VERIFY"` + Certificate string `envconfig:"KUBERNETES_CERTIFICATE"` + APIServer string `envconfig:"API_SERVER"` + ServiceAccount string `envconfig:"SERVICE_ACCOUNT"` // Can't just use split_words; need envconfig to find the non-prefixed form + + // Config specifically for `helm upgrade` + ChartVersion string `split_words:"true"` // + DryRun bool `split_words:"true"` // also available for `delete` + Wait bool `` // + ReuseValues bool `split_words:"true"` // + Timeout string `` // + Chart string `` // Also available for `lint`, in which case it must be a path to a chart directory + Release string `` + Force bool `` // +} + +type HelmCommand string + +// HelmCommand.Decode checks the given value against the list of known commands and generates a helpful error if the command is unknown. +func (cmd *HelmCommand) Decode(value string) error { + known := []string{"upgrade", "delete", "lint", "help"} + for _, c := range known { + if value == c { + *cmd = HelmCommand(value) + return nil + } + } + + if value == "" { + return nil + } + known[len(known)-1] = fmt.Sprintf("or %s", known[len(known)-1]) + return fmt.Errorf("Unknown command '%s'. If specified, command must be %s.", + value, strings.Join(known, ", ")) +} diff --git a/internal/helm/config_test.go b/internal/helm/config_test.go new file mode 100644 index 0000000..d4ff7dd --- /dev/null +++ b/internal/helm/config_test.go @@ -0,0 +1,28 @@ +package helm + +import ( + "github.com/stretchr/testify/suite" + "testing" +) + +type ConfigTestSuite struct { + suite.Suite +} + +func TestConfigTestSuite(t *testing.T) { + suite.Run(t, new(ConfigTestSuite)) +} + +func (suite *ConfigTestSuite) TestHelmCommandDecodeSuccess() { + cmd := HelmCommand("") + err := cmd.Decode("upgrade") + suite.Require().Nil(err) + + suite.EqualValues(cmd, "upgrade") +} + +func (suite *ConfigTestSuite) TestHelmCommandDecodeFailure() { + cmd := HelmCommand("") + err := cmd.Decode("execute order 66") + suite.EqualError(err, "Unknown command 'execute order 66'. If specified, command must be upgrade, delete, lint, or help.") +} diff --git a/internal/helm/plan.go b/internal/helm/plan.go new file mode 100644 index 0000000..91bffbb --- /dev/null +++ b/internal/helm/plan.go @@ -0,0 +1,62 @@ +package helm + +import ( + "errors" + "github.com/pelotech/drone-helm3/internal/run" +) + +type Step interface { + Run() error +} + +type Plan struct { + steps []Step +} + +func NewPlan(cfg Config) (*Plan, error) { + p := Plan{} + switch cfg.Command { + case "upgrade": + steps, err := upgrade(cfg) + if err != nil { + return nil, err + } + p.steps = steps + case "delete": + return nil, errors.New("not implemented") + case "lint": + return nil, errors.New("not implemented") + case "help": + return nil, errors.New("not implemented") + default: + switch cfg.DroneEvent { + case "push", "tag", "deployment", "pull_request", "promote", "rollback": + steps, err := upgrade(cfg) + if err != nil { + return nil, err + } + p.steps = steps + default: + return nil, errors.New("not implemented") + } + } + + return &p, nil +} + +func (p *Plan) Execute() error { + for _, step := range p.steps { + if err := step.Run(); err != nil { + return err + } + } + + return nil +} + +func upgrade(cfg Config) ([]Step, error) { + steps := make([]Step, 0) + steps = append(steps, run.NewUpgrade(cfg.Release, cfg.Chart)) + + return steps, nil +} diff --git a/internal/helm/plan_test.go b/internal/helm/plan_test.go new file mode 100644 index 0000000..017a8fe --- /dev/null +++ b/internal/helm/plan_test.go @@ -0,0 +1,53 @@ +package helm + +import ( + "fmt" + "github.com/stretchr/testify/suite" + "testing" + + "github.com/pelotech/drone-helm3/internal/run" +) + +type PlanTestSuite struct { + suite.Suite +} + +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", + } + + plan, err := NewPlan(cfg) + suite.Require().Nil(err) + suite.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) + } +} + +func (suite *PlanTestSuite) TestNewPlanUpgradeFromDroneEvent() { + cfg := Config{ + Chart: "billboard_top_100", + Release: "lizzo_good_as_hell", + } + + 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)) + } +} diff --git a/internal/run/help.go b/internal/run/help.go index ee92a2d..49b57e4 100644 --- a/internal/run/help.go +++ b/internal/run/help.go @@ -4,12 +4,20 @@ import ( "os" ) -func Help(args ...string) error { - args = append([]string{"help"}, args...) - - cmd := Command(HELM_BIN, args...) - cmd.Stdout(os.Stdout) - cmd.Stderr(os.Stderr) - - return cmd.Run() +type Help struct { + cmd cmd +} + +func (h *Help) Run() error { + return h.cmd.Run() +} + +func NewHelp() *Help { + h := Help{} + + h.cmd = Command(HELM_BIN, "help") + h.cmd.Stdout(os.Stdout) + h.cmd.Stderr(os.Stderr) + + return &h } diff --git a/internal/run/help_test.go b/internal/run/help_test.go index 0e9a25a..8e99b93 100644 --- a/internal/run/help_test.go +++ b/internal/run/help_test.go @@ -15,7 +15,7 @@ func TestHelp(t *testing.T) { Command = func(path string, args ...string) cmd { assert.Equal(t, HELM_BIN, path) - assert.Equal(t, []string{"help", "arg1", "arg2"}, args) + assert.Equal(t, []string{"help"}, args) return mCmd } defer func() { Command = originalCommand }() @@ -28,5 +28,6 @@ func TestHelp(t *testing.T) { Run(). Times(1) - Help("arg1", "arg2") + h := NewHelp() + h.Run() } diff --git a/internal/run/upgrade.go b/internal/run/upgrade.go index 37c3633..2b4c9fb 100644 --- a/internal/run/upgrade.go +++ b/internal/run/upgrade.go @@ -4,12 +4,25 @@ import ( "os" ) -func Upgrade(args ...string) error { - args = append([]string{"upgrade"}, args...) - cmd := Command(HELM_BIN, args...) - - cmd.Stdout(os.Stdout) - cmd.Stderr(os.Stderr) - - return cmd.Run() +type Upgrade struct { + Chart string + Release string + cmd cmd +} + +func (u *Upgrade) Run() error { + return u.cmd.Run() +} + +func NewUpgrade(release, chart string) *Upgrade { + u := Upgrade{ + Chart: chart, + Release: release, + cmd: Command(HELM_BIN, "upgrade", "--install", release, chart), + } + + u.cmd.Stdout(os.Stdout) + u.cmd.Stderr(os.Stderr) + + return &u } diff --git a/internal/run/upgrade_test.go b/internal/run/upgrade_test.go index 0b70654..f8ad66f 100644 --- a/internal/run/upgrade_test.go +++ b/internal/run/upgrade_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestUpgrade(t *testing.T) { +func TestNewUpgrade(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -15,7 +15,7 @@ func TestUpgrade(t *testing.T) { Command = func(path string, args ...string) cmd { assert.Equal(t, HELM_BIN, path) - assert.Equal(t, []string{"upgrade", "arg1", "arg2"}, args) + assert.Equal(t, []string{"upgrade", "--install", "jonas_brothers_only_human", "at40"}, args) return mCmd } @@ -29,5 +29,6 @@ func TestUpgrade(t *testing.T) { Run(). Times(1) - Upgrade("arg1", "arg2") + u := NewUpgrade("jonas_brothers_only_human", "at40") + u.Run() }