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 {