feat: inline environment values (#621)

Resolves #359
This commit is contained in:
KUOKA Yusuke 2019-05-28 16:26:00 +09:00 committed by GitHub
parent dd70c857a5
commit 681c866ce1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 109 additions and 27 deletions

View File

@ -159,11 +159,15 @@ environments:
# `{{ .Environment.Values.foo.bar }}` is evaluated to `1`.
values:
- environments/default/values.yaml
# Each entry in values can be either a file path or inline values.
# The below is an example of inline values, which is merged to the `.Environment.Values`
- myChartVer: 1.0.0-dev
# Any environment other than `default` is used only when `helmfile` is run with `--environment NAME`.
# That is, the "production" env below is used when and only when it is run like `helmfile --environment production sync`.
production:
values:
- environment/production/values.yaml
- myChartVer: 1.0.0
## `secrets.yaml` is decrypted by `helm-secrets` and available via `{{ .Environment.Secrets.KEY }}`
secrets:
- environment/production/secrets.yaml

View File

@ -983,6 +983,67 @@ foo: FOO
}
}
func TestLoadDesiredStateFromYaml_InlineEnvVals(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
- tillerNs: INLINE_TILLER_NS
`,
"/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,
fileExists: testFs.FileExists,
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 != "INLINE_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:

View File

@ -182,32 +182,49 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
envVals := map[string]interface{}{}
envSpec, ok := st.Environments[name]
if ok {
var envValuesFiles []string
for _, urlOrPath := range envSpec.Values {
resolved, skipped, err := st.resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath)
if err != nil {
return nil, err
}
if skipped {
for _, v := range envSpec.Values {
switch typedValue := v.(type) {
case string:
urlOrPath := typedValue
resolved, skipped, err := st.resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath)
if err != nil {
return nil, err
}
if skipped {
continue
}
for _, envvalFullPath := range resolved {
tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""}
r := tmpl.NewFileRenderer(readFile, filepath.Dir(envvalFullPath), tmplData)
bytes, err := r.RenderToBytes(envvalFullPath)
if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err)
}
m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err)
}
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to load \"%s\": %v", envvalFullPath, err)
}
}
case map[interface{}]interface{}:
m := map[string]interface{}{}
for k, v := range typedValue {
switch typedKey := k.(type) {
case string:
m[typedKey] = v
default:
return nil, fmt.Errorf("unexpected type of key in inline environment values %v: expected string, got %T", typedValue, typedKey)
}
}
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to merge %v: %v", typedValue, err)
}
continue
}
envValuesFiles = append(envValuesFiles, resolved...)
}
for _, envvalFullPath := range envValuesFiles {
tmplData := EnvironmentTemplateData{Environment: environment.EmptyEnvironment, Namespace: ""}
r := tmpl.NewFileRenderer(readFile, filepath.Dir(envvalFullPath), tmplData)
bytes, err := r.RenderToBytes(envvalFullPath)
if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err)
}
m := map[string]interface{}{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", envvalFullPath, err)
}
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to load \"%s\": %v", envvalFullPath, err)
default:
return nil, fmt.Errorf("unexpected type of values entry: %T", typedValue)
}
}

View File

@ -1,8 +1,8 @@
package state
type EnvironmentSpec struct {
Values []string `yaml:"values"`
Secrets []string `yaml:"secrets"`
Values []interface{} `yaml:"values"`
Secrets []string `yaml:"secrets"`
// MissingFileHandler instructs helmfile to fail when unable to find a environment values file listed
// under `environments.NAME.values`.