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
This commit is contained in:
Mike Eves 2018-05-22 09:12:48 +01:00 committed by KUOKA Yusuke
parent 5735efa8c0
commit 37f6ae8557
7 changed files with 102 additions and 0 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -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")

View File

@ -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)
}

37
main.go
View File

@ -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)

View File

@ -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{}

View File

@ -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 {