From 37f6ae8557e93f3d45be3989acf67b28e7265772 Mon Sep 17 00:00:00 2001 From: Mike Eves Date: Tue, 22 May 2018 09:12:48 +0100 Subject: [PATCH] Add helmfile test sub-command (#150) **Feature** An additional sub-command to the helmfile binary; helmfile test **Why** Helm provides helm test (https://github.com/kubernetes/helm/blob/master/docs/chart_tests.md) as a method to run automated tests post chart install to ensure things are working as they should be. It would be nice to be able to run something like helmfile test against a particular helmfile in order to run helm tests against all charts/releases defined in the file. Either as part of the sync (i.e. helmfile sync --test) to be ran immediately after the corresponding chart is installed, or as a separate command ran after a sync (i.e. helmfile test). A chart without tests will exit with a 0 status, so it can be safely ran against any charts. **Notes** `--cleanup` (bool) & `--timeout` (default: 300) are available as first class arguments. Additional arguments can be passed to the helm binary as with other sub commands using `--args=` Resolves #144 --- README.md | 7 +++++++ helmexec/exec.go | 6 ++++++ helmexec/exec_test.go | 19 +++++++++++++++++++ helmexec/helmexec.go | 1 + main.go | 37 +++++++++++++++++++++++++++++++++++++ state/state.go | 29 +++++++++++++++++++++++++++++ state/state_test.go | 3 +++ 7 files changed, 102 insertions(+) diff --git a/README.md b/README.md index 0b7b1a10..62728ecc 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ COMMANDS: sync sync all resources from state file (repos, charts and local chart deps) status retrieve status of releases in state file delete delete charts from state file (helm delete) + test tets releases from state file (helm test) GLOBAL OPTIONS: --file FILE, -f FILE load config from FILE (default: "helmfile.yaml") @@ -200,6 +201,12 @@ To supply the secret functionality Helmfile needs the `helm secrets` plugin inst you should be able to simply execute `helm plugin install https://github.com/futuresimple/helm-secrets `. +### test + +The `helmfile test` sub-command runs a `helm test` against specified releases in the manifest, default to all + +Use `--cleanup` to delete pods upon completion. + ## Paths Overview Using manifest files in conjunction with command line argument can be a bit confusing. diff --git a/helmexec/exec.go b/helmexec/exec.go index 13c9cbb3..1d9bff6b 100644 --- a/helmexec/exec.go +++ b/helmexec/exec.go @@ -85,6 +85,12 @@ func (helm *execer) DeleteRelease(name string, flags ...string) error { return err } +func (helm *execer) TestRelease(name string, flags ...string) error { + out, err := helm.exec(append([]string{"test", name}, flags...)...) + helm.write(out) + return err +} + func (helm *execer) exec(args ...string) ([]byte, error) { cmdargs := args if len(helm.extra) > 0 { diff --git a/helmexec/exec_test.go b/helmexec/exec_test.go index 088761d9..2142e87e 100644 --- a/helmexec/exec_test.go +++ b/helmexec/exec_test.go @@ -164,6 +164,25 @@ func Test_DeleteRelease_Flags(t *testing.T) { } } +func Test_TestRelease(t *testing.T) { + var buffer bytes.Buffer + helm := MockExecer(&buffer, "dev") + helm.TestRelease("release") + expected := "exec: helm test release --kube-context dev\n" + if buffer.String() != expected { + t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected) + } +} +func Test_TestRelease_Flags(t *testing.T) { + var buffer bytes.Buffer + helm := MockExecer(&buffer, "dev") + helm.TestRelease("release", "--cleanup", "--timeout", "60") + expected := "exec: helm test release --cleanup --timeout 60 --kube-context dev\n" + if buffer.String() != expected { + t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected) + } +} + func Test_ReleaseStatus(t *testing.T) { var buffer bytes.Buffer helm := MockExecer(&buffer, "dev") diff --git a/helmexec/helmexec.go b/helmexec/helmexec.go index 4bfd1817..0b0a556b 100644 --- a/helmexec/helmexec.go +++ b/helmexec/helmexec.go @@ -11,6 +11,7 @@ type Interface interface { DiffRelease(name, chart string, flags ...string) error ReleaseStatus(name string) error DeleteRelease(name string, flags ...string) error + TestRelease(name string, flags ...string) error DecryptSecret(name string) (string, error) } diff --git a/main.go b/main.go index 91357cf8..7bb34f97 100644 --- a/main.go +++ b/main.go @@ -271,6 +271,43 @@ func main() { return clean(state, errs) }, }, + { + Name: "test", + Usage: "test releases from state file (helm test)", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "cleanup", + Usage: "delete test pods upon completion", + }, + cli.StringFlag{ + Name: "args", + Value: "", + Usage: "pass additional args to helm exec", + }, + cli.IntFlag{ + Name: "timeout", + Value: 300, + Usage: "maximum time for tests to run before being considered failed", + }, + }, + Action: func(c *cli.Context) error { + state, helm, err := before(c) + if err != nil { + return err + } + + cleanup := c.Bool("cleanup") + timeout := c.Int("timeout") + + args := c.String("args") + if len(args) > 0 { + helm.SetExtraArgs(strings.Split(args, " ")...) + } + + errs := state.TestReleases(helm, cleanup, timeout) + return clean(state, errs) + }, + }, } err := app.Run(os.Args) diff --git a/state/state.go b/state/state.go index f4ba33b1..5d79c50f 100644 --- a/state/state.go +++ b/state/state.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strconv" "strings" "sync" "text/template" @@ -382,6 +383,34 @@ func (state *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []er return nil } +// TestReleases wrapper for executing helm test on the releases +func (state *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout int) []error { + var wg sync.WaitGroup + errs := []error{} + + for _, release := range state.Releases { + wg.Add(1) + go func(wg *sync.WaitGroup, release ReleaseSpec) { + flags := []string{} + if cleanup { + flags = append(flags, "--cleanup") + } + flags = append(flags, "--timeout", strconv.Itoa(timeout)) + if err := helm.TestRelease(release.Name, flags...); err != nil { + errs = append(errs, err) + } + wg.Done() + }(&wg, release) + } + wg.Wait() + + if len(errs) != 0 { + return errs + } + + return nil +} + // Clean will remove any generated secrets func (state *HelmState) Clean() []error { errs := []error{} diff --git a/state/state_test.go b/state/state_test.go index 88a539c8..b488df33 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -552,6 +552,9 @@ func (helm *mockHelmExec) DeleteRelease(name string, flags ...string) error { func (helm *mockHelmExec) DecryptSecret(name string) (string, error) { return "", nil } +func (helm *mockHelmExec) TestRelease(name string, flags ...string) error { + return nil +} func TestHelmState_SyncRepos(t *testing.T) { tests := []struct {