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