From 88c1bb7f22bed6e078b554cb2a37d3ae63260066 Mon Sep 17 00:00:00 2001 From: Yusuke Kuoka Date: Thu, 16 Jul 2020 08:48:43 +0900 Subject: [PATCH] feat: `helmfile template --output-dir-template` for customizing output dirs This is useful for e.g. removing state file names and their hash values out of output dirs so that it can be used easily in a gitops setup. For example, `--output-dir-template mybasedir/{{.Release.Name}}` produces `mybasedir/RELEASE/CHART/templates/*.yaml` for each release in your helmfile.yaml. --- main.go | 8 +++++++ pkg/app/app.go | 3 ++- pkg/app/app_test.go | 4 ++++ pkg/app/config.go | 1 + pkg/state/state.go | 54 ++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 2f14ce36..0aed3772 100644 --- a/main.go +++ b/main.go @@ -235,6 +235,10 @@ func main() { Name: "output-dir", Usage: "output directory to pass to helm template (helm template --output-dir)", }, + cli.StringFlag{ + Name: "output-dir-template", + Usage: "go text template for generating the output directory. Default: {{ .OutputDir }}/{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}-{{ .Release.Name}}", + }, cli.IntFlag{ Name: "concurrency", Value: 0, @@ -552,6 +556,10 @@ func (c configImpl) OutputDir() string { return c.c.String("output-dir") } +func (c configImpl) OutputDirTemplate() string { + return c.c.String("output-dir-template") +} + func (c configImpl) Validate() bool { return c.c.Bool("validate") } diff --git a/pkg/app/app.go b/pkg/app/app.go index 817fb748..235e9646 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1282,7 +1282,8 @@ func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) { args := argparser.GetArgs(c.Args(), st) opts := &state.TemplateOpts{ - Set: c.Set(), + Set: c.Set(), + OutputDirTemplate: c.OutputDirTemplate(), } return subst.TemplateReleases(helm, c.OutputDir(), c.Values(), args, c.Concurrency(), c.Validate(), opts) })) diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 884e7bcd..4a0f43f2 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2178,6 +2178,10 @@ func (c configImpl) OutputDir() string { return "output/subdir" } +func (c configImpl) OutputDirTemplate() string { + return "" +} + func (c configImpl) Concurrency() int { return 1 } diff --git a/pkg/app/config.go b/pkg/app/config.go index d64a8091..5ae6078e 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -129,6 +129,7 @@ type TemplateConfigProvider interface { Values() []string Set() []string + OutputDirTemplate() string Validate() bool SkipDeps() bool OutputDir() string diff --git a/pkg/state/state.go b/pkg/state/state.go index 04643f1e..13040f9c 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -1,6 +1,7 @@ package state import ( + "bytes" "crypto/sha1" "encoding/hex" "errors" @@ -16,6 +17,7 @@ import ( "strconv" "strings" "sync" + "text/template" "github.com/roboll/helmfile/pkg/environment" "github.com/roboll/helmfile/pkg/event" @@ -828,7 +830,8 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre } type TemplateOpts struct { - Set []string + Set []string + OutputDirTemplate string } type TemplateOpt interface{ Apply(*TemplateOpts) } @@ -890,8 +893,8 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string, } } - if len(outputDir) > 0 { - releaseOutputDir, err := st.GenerateOutputDir(outputDir, release) + if len(outputDir) > 0 || len(opts.OutputDirTemplate) > 0 { + releaseOutputDir, err := st.GenerateOutputDir(outputDir, release, opts.OutputDirTemplate) if err != nil { errs = append(errs, err) } @@ -2193,7 +2196,7 @@ func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) erro return nil } -func (st *HelmState) GenerateOutputDir(outputDir string, release *ReleaseSpec) (string, error) { +func (st *HelmState) GenerateOutputDir(outputDir string, release *ReleaseSpec, outputDirTemplate string) (string, error) { // get absolute path of state file to generate a hash // use this hash to write helm output in a specific directory by state file and release name // ie. in a directory named stateFileName-stateFileHash-releaseName @@ -2208,14 +2211,53 @@ func (st *HelmState) GenerateOutputDir(outputDir string, release *ReleaseSpec) ( var stateFileExtension = filepath.Ext(st.FilePath) var stateFileName = st.FilePath[0 : len(st.FilePath)-len(stateFileExtension)] + sha1sum := hex.EncodeToString(hasher.Sum(nil))[:8] + var sb strings.Builder sb.WriteString(stateFileName) sb.WriteString("-") - sb.WriteString(hex.EncodeToString(hasher.Sum(nil))[:8]) + sb.WriteString(sha1sum) sb.WriteString("-") sb.WriteString(release.Name) - return path.Join(outputDir, sb.String()), nil + if outputDirTemplate == "" { + outputDirTemplate = filepath.Join("{{ .OutputDir }}", "{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}-{{ .Release.Name}}") + } + + t, err := template.New("output-dir").Parse(outputDirTemplate) + if err != nil { + return "", fmt.Errorf("parsing output-dir templmate") + } + + buf := &bytes.Buffer{} + + type state struct { + BaseName string + Path string + AbsPath string + AbsPathSHA1 string + } + + data := struct { + OutputDir string + State state + Release *ReleaseSpec + }{ + OutputDir: outputDir, + State: state{ + BaseName: stateFileName, + Path: st.FilePath, + AbsPath: stateAbsPath, + AbsPathSHA1: sha1sum, + }, + Release: release, + } + + if err := t.Execute(buf, data); err != nil { + return "", fmt.Errorf("executing output-dir template: %w", err) + } + + return buf.String(), nil } func (st *HelmState) ToYaml() (string, error) {