diff --git a/docs/writing-helmfile.md b/docs/writing-helmfile.md index e4eab93d..2e7fa56a 100644 --- a/docs/writing-helmfile.md +++ b/docs/writing-helmfile.md @@ -91,7 +91,9 @@ releases: See the [issue 428](https://github.com/roboll/helmfile/issues/428) for more context on how this is supposed to work. -## Layering +## Layering State Files + +> See **Layering State Template Files** if you're layering templates. You may occasionally end up with many helmfiles that shares common parts like which repositories to use, and whichi release to be bundled by default. @@ -164,3 +166,117 @@ Great! Now, repeat the above steps for each your `helmfile.yaml`, so that all your helmfiles becomes DRY. Please also see [the discussion in the issue 388](https://github.com/roboll/helmfile/issues/388#issuecomment-491710348) for more advanced layering examples. + +## Layering State Template Files + +Do you need to make your state file even more DRY? + +Turned out layering state files wasn't enough for you? + +Helmfile supports an advanced feature that allows you to compose state "template" files to generate the final state to be processed. + +In the following example `helmfile.yaml.gotmpl`, each `---` separated part of the file is a go template. + +`helmfile.yaml.gotmpl`: + +```yaml +# Part 1: Reused Enviroment Values +bases: + - myenv.yaml +--- +# Part 2: Reused Defaults +bases: + - mydefaults.yaml +--- +# Part 3: Dynamic Releases +releases: + - name: test1 + chart: mychart-{{ .Environment.Values.myname }} + values: + replicaCount: 1 + image: + repository: "nginx" + tag: "latest" +``` + +Suppose the `myenv.yaml` and `test.env.yaml` loaded in the first part looks like: + +`myenv.yaml`: + +```yaml +environments: + test: + values: + - test.env.yaml +``` + +`test.env.yaml`: + +```yaml +kubeContext: test +wait: false +cvOnly: false +myname: "dog" +``` + +Where the gotmpl file loaded in the second part looks like: + +`mydefaults.yaml.gotmpl`: + +```yaml +helmDefaults: + tillerNamespace: kube-system + kubeContext: {{ .Environment.Values.kubeContext }} + verify: false + {{ if .Environment.Values.wait }} + wait: true + {{ else }} + wait: false + {{ end }} + timeout: 600 + recreatePods: false + force: true +``` + +Each go template is rendered in the context where `.Environment.Values` is inherited from the previous part. + +So in `mydefaults.yaml.gotmpl`, both `.Environment.Values.kubeContext` and `.Environment.Values.wait` are valid as they do exist in the environment values inherited from the previous part(=the first part) of your `helmfile.yaml.gotmpl`, and therefore the template is rendered to: + +```yaml +helmDefaults: + tillerNamespace: kube-system + kubeContext: test + verify: false + wait: false + timeout: 600 + recreatePods: false + force: true +``` + +Similarly, the third part of the top-level `helmfile.yaml.gotmpl`, `.Environment.Values.myname` is valid as it is included in the environment values inherited from the previous parts: + +```yaml +# Part 3: Dynamic Releases +releases: + - name: test1 + chart: mychart-{{ .Environment.Values.myname }} + values: + replicaCount: 1 + image: + repository: "nginx" + tag: "latest" +```` + +hence rendered to: + +```yaml +# Part 3: Dynamic Releases +releases: + - name: test1 + chart: mychart-dog + values: + replicaCount: 1 + image: + repository: "nginx" + tag: "latest" +``` diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 3f90be0c..97e48bc4 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -865,6 +865,65 @@ helmDefaults: } } +func TestLoadDesiredStateFromYaml_EnvvalsInheritanceToBaseTemplate(t *testing.T) { + yamlFile := "/path/to/yaml/file" + yamlContent := `bases: +- ../base.yaml +--- +bases: +# "envvals inheritance" +# base.gotmpl should be able to reference environment values defined in the base.yaml and default/1.yaml +- ../base.gotmpl +--- +releases: +- name: myrelease0 + chart: mychart0 +` + testFs := state.NewTestFs(map[string]string{ + yamlFile: yamlContent, + "/path/to/base.yaml": `environments: + default: + values: + - environments/default/1.yaml +`, + "/path/to/base.gotmpl": `helmDefaults: + kubeContext: {{ .Environment.Values.foo }} + tillerNamespace: {{ .Environment.Values.tillerNs }} +`, + "/path/to/yaml/environments/default/1.yaml": `tillerNs: TILLER_NS +foo: FOO +`, + "/path/to/yaml/templates.yaml": `templates: + default: &default + missingFileHandler: Warn + values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"] +`, + }) + app := &App{ + readFile: testFs.ReadFile, + glob: testFs.Glob, + abs: testFs.Abs, + Env: "default", + Logger: helmexec.NewLogger(os.Stderr, "debug"), + } + st, err := app.loadDesiredStateFromYaml(yamlFile) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if st.HelmDefaults.TillerNamespace != "TILLER_NS" { + t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace) + } + + if st.Releases[0].Name != "myrelease0" { + t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[0].Name) + } + + if st.HelmDefaults.KubeContext != "FOO" { + t.Errorf("unexpected helmDefaults.kubeContext: expected=FOO, got=%s", st.HelmDefaults.KubeContext) + } +} + func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithNonDefaultEnv(t *testing.T) { yamlFile := "/path/to/yaml/file" yamlContent := `bases: diff --git a/pkg/app/desired_state_file_loader.go b/pkg/app/desired_state_file_loader.go index e101aba7..b82310fb 100644 --- a/pkg/app/desired_state_file_loader.go +++ b/pkg/app/desired_state_file_loader.go @@ -27,7 +27,7 @@ type desiredStateLoader struct { } func (ld *desiredStateLoader) Load(f string) (*state.HelmState, error) { - st, err := ld.loadFile(filepath.Dir(f), filepath.Base(f), true) + st, err := ld.loadFile(nil, filepath.Dir(f), filepath.Base(f), true) if err != nil { return nil, err } @@ -57,7 +57,7 @@ func (ld *desiredStateLoader) Load(f string) (*state.HelmState, error) { return st, nil } -func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool) (*state.HelmState, error) { +func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) { var f string if filepath.IsAbs(file) { f = file @@ -76,6 +76,7 @@ func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool) if !experimentalModeEnabled() || ext == ".gotmpl" { self, err = ld.renderAndLoad( + inheritedEnv, baseDir, f, fileBytes, @@ -87,7 +88,7 @@ func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool) baseDir, file, evaluateBases, - nil, + inheritedEnv, ) } @@ -123,11 +124,10 @@ func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBas return st, nil } -func (ld *desiredStateLoader) renderAndLoad(baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) { +func (ld *desiredStateLoader) renderAndLoad(env *environment.Environment, baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) { parts := bytes.Split(content, []byte("\n---\n")) var finalState *state.HelmState - var env *environment.Environment for i, part := range parts { var yamlBuf *bytes.Buffer diff --git a/state/create.go b/state/create.go index d5ef68a7..7fec9f09 100644 --- a/state/create.go +++ b/state/create.go @@ -42,7 +42,7 @@ type StateCreator struct { Strict bool - LoadFile func(baseDir, file string, evaluateBases bool) (*HelmState, error) + LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error) } func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), abs func(string) (string, error), glob func(string) ([]string, error)) *StateCreator { @@ -143,7 +143,7 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam } } - state, err = c.loadBases(state, baseDir) + state, err = c.loadBases(envValues, state, baseDir) if err != nil { return nil, err } @@ -151,10 +151,10 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam return c.LoadEnvValues(state, envName, envValues) } -func (c *StateCreator) loadBases(st *HelmState, baseDir string) (*HelmState, error) { +func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) { layers := []*HelmState{} for _, b := range st.Bases { - base, err := c.LoadFile(baseDir, b, false) + base, err := c.LoadFile(envValues, baseDir, b, false) if err != nil { return nil, err }