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.
|
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.
|
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.
|
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.
|
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) {
|
func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithNonDefaultEnv(t *testing.T) {
|
||||||
yamlFile := "/path/to/yaml/file"
|
yamlFile := "/path/to/yaml/file"
|
||||||
yamlContent := `bases:
|
yamlContent := `bases:
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ type desiredStateLoader struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ld *desiredStateLoader) Load(f string) (*state.HelmState, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +57,7 @@ func (ld *desiredStateLoader) Load(f string) (*state.HelmState, error) {
|
||||||
return st, nil
|
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
|
var f string
|
||||||
if filepath.IsAbs(file) {
|
if filepath.IsAbs(file) {
|
||||||
f = file
|
f = file
|
||||||
|
|
@ -76,6 +76,7 @@ func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool)
|
||||||
|
|
||||||
if !experimentalModeEnabled() || ext == ".gotmpl" {
|
if !experimentalModeEnabled() || ext == ".gotmpl" {
|
||||||
self, err = ld.renderAndLoad(
|
self, err = ld.renderAndLoad(
|
||||||
|
inheritedEnv,
|
||||||
baseDir,
|
baseDir,
|
||||||
f,
|
f,
|
||||||
fileBytes,
|
fileBytes,
|
||||||
|
|
@ -87,7 +88,7 @@ func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool)
|
||||||
baseDir,
|
baseDir,
|
||||||
file,
|
file,
|
||||||
evaluateBases,
|
evaluateBases,
|
||||||
nil,
|
inheritedEnv,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,11 +124,10 @@ func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBas
|
||||||
return st, nil
|
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"))
|
parts := bytes.Split(content, []byte("\n---\n"))
|
||||||
|
|
||||||
var finalState *state.HelmState
|
var finalState *state.HelmState
|
||||||
var env *environment.Environment
|
|
||||||
|
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
var yamlBuf *bytes.Buffer
|
var yamlBuf *bytes.Buffer
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ type StateCreator struct {
|
||||||
|
|
||||||
Strict bool
|
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 {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -151,10 +151,10 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
|
||||||
return c.LoadEnvValues(state, envName, envValues)
|
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{}
|
layers := []*HelmState{}
|
||||||
for _, b := range st.Bases {
|
for _, b := range st.Bases {
|
||||||
base, err := c.LoadFile(baseDir, b, false)
|
base, err := c.LoadFile(envValues, baseDir, b, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue