From 93c5d4c219036b02a751769bd0e48f2808f06bee Mon Sep 17 00:00:00 2001 From: Karl Stoney Date: Tue, 4 Sep 2018 03:31:43 +0100 Subject: [PATCH] feat: `helmfile template` (#284) `helmfile template` runs `helm template` over releases within the helmfiles, and provide you a stream of generated yaml documents of Kubernetes resources via stdout. Resolves #283 --- helmexec/exec.go | 9 +++- helmexec/helmexec.go | 1 + main.go | 48 +++++++++++++++++- state/state.go | 117 ++++++++++++++++++++++++++++++++++++++++++- state/state_test.go | 7 ++- 5 files changed, 177 insertions(+), 5 deletions(-) diff --git a/helmexec/exec.go b/helmexec/exec.go index 663fb92c..eb92a6b9 100644 --- a/helmexec/exec.go +++ b/helmexec/exec.go @@ -6,9 +6,10 @@ import ( "os" "strings" + "sync" + "go.uber.org/zap" "go.uber.org/zap/zapcore" - "sync" ) const ( @@ -143,6 +144,12 @@ func (helm *execer) DecryptSecret(name string) (string, error) { return tmpFile.Name(), err } +func (helm *execer) TemplateRelease(chart string, flags ...string) error { + out, err := helm.exec(append([]string{"template", chart}, flags...)...) + helm.write(out) + return err +} + func (helm *execer) DiffRelease(name, chart string, flags ...string) error { helm.logger.Infof("Comparing %v %v", name, chart) out, err := helm.exec(append([]string{"diff", "upgrade", "--allow-unreleased", name, chart}, flags...)...) diff --git a/helmexec/helmexec.go b/helmexec/helmexec.go index 68e1b92c..821f38ea 100644 --- a/helmexec/helmexec.go +++ b/helmexec/helmexec.go @@ -10,6 +10,7 @@ type Interface interface { UpdateDeps(chart string) error SyncRelease(name, chart string, flags ...string) error DiffRelease(name, chart string, flags ...string) error + TemplateRelease(chart string, flags ...string) error Fetch(chart string, flags ...string) error Lint(chart string, flags ...string) error ReleaseStatus(name string) error diff --git a/main.go b/main.go index 884c80bf..e6c47ffc 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,8 @@ import ( "os/exec" + "io/ioutil" + "github.com/roboll/helmfile/args" "github.com/roboll/helmfile/environment" "github.com/roboll/helmfile/helmexec" @@ -19,7 +21,6 @@ import ( "github.com/urfave/cli" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "io/ioutil" ) const ( @@ -197,6 +198,31 @@ func main() { }) }, }, + { + Name: "template", + Usage: "template releases from state file against env (helm template)", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "args", + Value: "", + Usage: "pass args to helm template", + }, + cli.StringSliceFlag{ + Name: "values", + Usage: "additional value files to be merged into the command", + }, + cli.IntFlag{ + Name: "concurrency", + Value: 0, + Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", + }, + }, + Action: func(c *cli.Context) error { + return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error { + return executeTemplateCommand(c, state, helm) + }) + }, + }, { Name: "lint", Usage: "lint charts from state file (helm lint)", @@ -449,6 +475,26 @@ func executeSyncCommand(c *cli.Context, state *state.HelmState, helm helmexec.In return state.SyncReleases(helm, values, workers) } +func executeTemplateCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error { + if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 { + return errs + } + + if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 { + return errs + } + + if c.GlobalString("helm-binary") != "" { + helm.SetHelmBinary(c.GlobalString("helm-binary")) + } + + args := args.GetArgs(c.String("args"), state) + values := c.StringSlice("values") + workers := c.Int("concurrency") + + return state.TemplateReleases(helm, values, workers, args) +} + func executeDiffCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) []error { args := args.GetArgs(c.String("args"), state) if len(args) > 0 { diff --git a/state/state.go b/state/state.go index cc987fa6..c4f9a626 100644 --- a/state/state.go +++ b/state/state.go @@ -3,7 +3,6 @@ package state import ( "errors" "fmt" - "github.com/roboll/helmfile/helmexec" "io/ioutil" "os" "path" @@ -12,6 +11,8 @@ import ( "strings" "sync" + "github.com/roboll/helmfile/helmexec" + "regexp" "github.com/roboll/helmfile/environment" @@ -270,6 +271,111 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues [ return nil } +// TemplateReleases wrapper for executing helm template on the releases +func (state *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, args []string) []error { + var wgRelease sync.WaitGroup + var wgError sync.WaitGroup + errs := []error{} + jobQueue := make(chan *ReleaseSpec, len(state.Releases)) + errQueue := make(chan error) + + if workerLimit < 1 { + workerLimit = len(state.Releases) + } + + wgRelease.Add(len(state.Releases)) + + // Create tmp directory and bail immediately if it fails + dir, err := ioutil.TempDir("", "") + if err != nil { + errs = append(errs, err) + return errs + } + defer os.RemoveAll(dir) + + for w := 1; w <= workerLimit; w++ { + go func() { + for release := range jobQueue { + errs := []error{} + flags, err := state.flagsForTemplate(helm, release) + if err != nil { + errs = append(errs, err) + } + for _, value := range additionalValues { + valfile, err := filepath.Abs(value) + if err != nil { + errs = append(errs, err) + } + + if _, err := os.Stat(valfile); os.IsNotExist(err) { + errs = append(errs, err) + } + flags = append(flags, "--values", valfile) + } + + chartPath := "" + if pathExists(normalizeChart(state.basePath, release.Chart)) { + chartPath = normalizeChart(state.basePath, release.Chart) + } else { + fetchFlags := []string{} + if release.Version != "" { + chartPath = path.Join(dir, release.Name, release.Version, release.Chart) + fetchFlags = append(fetchFlags, "--version", release.Version) + } else { + chartPath = path.Join(dir, release.Name, "latest", release.Chart) + } + + // only fetch chart if it is not already fetched + if _, err := os.Stat(chartPath); os.IsNotExist(err) { + fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath) + if err := helm.Fetch(release.Chart, fetchFlags...); err != nil { + errs = append(errs, err) + } + } + chartPath = path.Join(chartPath, chartNameWithoutRepository(release.Chart)) + } + + if len(args) > 0 { + helm.SetExtraArgs(args...) + } + + if len(errs) == 0 { + if err := helm.TemplateRelease(chartPath, flags...); err != nil { + errs = append(errs, err) + } + } + for _, err := range errs { + errQueue <- err + } + wgRelease.Done() + } + }() + } + wgError.Add(1) + go func() { + for err := range errQueue { + errs = append(errs, err) + } + wgError.Done() + }() + + for i := 0; i < len(state.Releases); i++ { + jobQueue <- &state.Releases[i] + } + + close(jobQueue) + wgRelease.Wait() + + close(errQueue) + wgError.Wait() + + if len(errs) != 0 { + return errs + } + + return nil +} + // DiffReleases wrapper for executing helm diff on the releases func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool) []error { var wgRelease sync.WaitGroup @@ -691,6 +797,15 @@ func (state *HelmState) flagsForUpgrade(helm helmexec.Interface, release *Releas return append(flags, common...), nil } +func (state *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { + flags := []string{} + common, err := state.namespaceAndValuesFlags(helm, release) + if err != nil { + return nil, err + } + return append(flags, common...), nil +} + func (state *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec) ([]string, error) { flags := []string{} if release.Version != "" { diff --git a/state/state_test.go b/state/state_test.go index bad3c1c9..820adbb0 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -6,8 +6,9 @@ import ( "testing" "errors" - "github.com/roboll/helmfile/helmexec" "strings" + + "github.com/roboll/helmfile/helmexec" ) var logger = helmexec.NewLogger(os.Stdout, "warn") @@ -537,7 +538,9 @@ func (helm *mockHelmExec) Fetch(chart string, flags ...string) error { func (helm *mockHelmExec) Lint(chart string, flags ...string) error { return nil } - +func (helm *mockHelmExec) TemplateRelease(chart string, flags ...string) error { + return nil +} func TestHelmState_SyncRepos(t *testing.T) { tests := []struct { name string