feat: "base" helmfile state gotmpl is rendered with the envvals inherited from the parent (#613)
Resolves #611
This commit is contained in:
		
							parent
							
								
									90390492a3
								
							
						
					
					
						commit
						1012256f16
					
				|  | @ -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" | ||||
| ``` | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 		} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue