Fix panic related to env values from files and sprig dict funcs (#625)
* fix: 0.68.0: go panic w/ multiple bases/layers Fixes #623 * fix: 0.64.1+: getOrNil/hasKey fails on Environment.Values with nested maps Fixes #624
This commit is contained in:
parent
591086dac9
commit
b3f4586ed8
|
|
@ -1,8 +1,9 @@
|
|||
package environment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Environment struct {
|
||||
|
|
@ -13,12 +14,16 @@ type Environment struct {
|
|||
var EmptyEnvironment Environment
|
||||
|
||||
func (e Environment) DeepCopy() Environment {
|
||||
bytes, err := json.Marshal(e.Values)
|
||||
bytes, err := yaml.Marshal(e.Values)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var values map[string]interface{}
|
||||
if err := json.Unmarshal(bytes, &values); err != nil {
|
||||
if err := yaml.Unmarshal(bytes, &values); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
values, err = maputil.CastKeysToStrings(values)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return Environment{
|
||||
|
|
|
|||
|
|
@ -1356,3 +1356,138 @@ releases:
|
|||
t.Errorf("unexpected releases[0].chart: expected=BAR, got=%s", st.Releases[0].Chart)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/623
|
||||
func TestLoadDesiredStateFromYaml_MultiPartTemplate_MergeMapsVariousKeys(t *testing.T) {
|
||||
type testcase struct {
|
||||
overrideValues interface{}
|
||||
expected string
|
||||
}
|
||||
testcases := []testcase{
|
||||
{map[interface{}]interface{}{"foo": "FOO"}, `FOO`},
|
||||
{map[interface{}]interface{}{"foo": map[interface{}]interface{}{"foo": "FOO"}}, `map[foo:FOO]`},
|
||||
{map[interface{}]interface{}{"foo": map[string]interface{}{"foo": "FOO"}}, `map[foo:FOO]`},
|
||||
{map[interface{}]interface{}{"foo": []interface{}{"foo"}}, `[foo]`},
|
||||
{map[interface{}]interface{}{"foo": "FOO"}, `FOO`},
|
||||
}
|
||||
for i := range testcases {
|
||||
tc := testcases[i]
|
||||
statePath := "/path/to/helmfile.yaml"
|
||||
stateContent := `
|
||||
environments:
|
||||
default:
|
||||
values:
|
||||
- 1.yaml
|
||||
- 2.yaml
|
||||
---
|
||||
releases:
|
||||
- name: {{ .Environment.Values.foo | quote }}
|
||||
chart: {{ .Environment.Values.bar | quote }}
|
||||
`
|
||||
testFs := state.NewTestFs(map[string]string{
|
||||
statePath: stateContent,
|
||||
"/path/to/1.yaml": `bar: ["bar"]`,
|
||||
"/path/to/2.yaml": `bar: ["BAR"]`,
|
||||
})
|
||||
app := &App{
|
||||
readFile: testFs.ReadFile,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
Reverse: true,
|
||||
}
|
||||
opts := LoadOpts{
|
||||
CalleePath: statePath,
|
||||
Environment: state.SubhelmfileEnvironmentSpec{
|
||||
OverrideValues: []interface{}{tc.overrideValues},
|
||||
},
|
||||
}
|
||||
st, err := app.loadDesiredStateFromYaml(statePath, opts)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if st.Releases[0].Name != tc.expected {
|
||||
t.Errorf("unexpected releases[0].name: expected=%s, got=%s", tc.expected, st.Releases[0].Name)
|
||||
}
|
||||
if st.Releases[0].Chart != "[BAR]" {
|
||||
t.Errorf("unexpected releases[0].chart: expected=BAR, got=%s", st.Releases[0].Chart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDesiredStateFromYaml_MultiPartTemplate_SprigDictFuncs(t *testing.T) {
|
||||
type testcase struct {
|
||||
state string
|
||||
expr string
|
||||
expected string
|
||||
}
|
||||
stateInline := `
|
||||
environments:
|
||||
default:
|
||||
values:
|
||||
- foo: FOO
|
||||
bar: { "baz": "BAZ" }
|
||||
---
|
||||
releases:
|
||||
- name: %s
|
||||
chart: stable/nginx
|
||||
`
|
||||
stateExternal := `
|
||||
environments:
|
||||
default:
|
||||
values:
|
||||
- 1.yaml
|
||||
- 2.yaml
|
||||
---
|
||||
releases:
|
||||
- name: %s
|
||||
chart: stable/nginx
|
||||
`
|
||||
testcases := []testcase{
|
||||
{stateInline, `{{ getOrNil "foo" .Environment.Values }}`, `FOO`},
|
||||
{stateInline, `{{ getOrNil "baz" (getOrNil "bar" .Environment.Values) }}`, `BAZ`},
|
||||
{stateInline, `{{ if hasKey .Environment.Values "foo" }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
|
||||
{stateInline, `{{ if hasKey .Environment.Values "bar" }}{{ .Environment.Values.bar.baz }}{{ end }}`, `BAZ`},
|
||||
{stateInline, `{{ if (keys .Environment.Values | has "foo") }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
|
||||
// See https://github.com/roboll/helmfile/issues/624
|
||||
// This fails when .Environment.Values.bar is not map[string]interface{}. At the time of #624 it was map[interface{}]interface{}, which sprig's dict funcs don't support.
|
||||
{stateInline, `{{ if (keys .Environment.Values | has "bar") }}{{ if (keys .Environment.Values.bar | has "baz") }}{{ .Environment.Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
|
||||
{stateExternal, `{{ getOrNil "foo" .Environment.Values }}`, `FOO`},
|
||||
{stateExternal, `{{ getOrNil "baz" (getOrNil "bar" .Environment.Values) }}`, `BAZ`},
|
||||
{stateExternal, `{{ if hasKey .Environment.Values "foo" }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
|
||||
{stateExternal, `{{ if hasKey .Environment.Values "bar" }}{{ .Environment.Values.bar.baz }}{{ end }}`, `BAZ`},
|
||||
{stateExternal, `{{ if (keys .Environment.Values | has "foo") }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
|
||||
// See https://github.com/roboll/helmfile/issues/624
|
||||
{stateExternal, `{{ if (keys .Environment.Values | has "bar") }}{{ if (keys .Environment.Values.bar | has "baz") }}{{ .Environment.Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
|
||||
}
|
||||
for i := range testcases {
|
||||
tc := testcases[i]
|
||||
statePath := "/path/to/helmfile.yaml"
|
||||
stateContent := fmt.Sprintf(tc.state, tc.expr)
|
||||
testFs := state.NewTestFs(map[string]string{
|
||||
statePath: stateContent,
|
||||
"/path/to/1.yaml": `foo: FOO`,
|
||||
"/path/to/2.yaml": `bar: { "baz": "BAZ" }`,
|
||||
})
|
||||
app := &App{
|
||||
readFile: testFs.ReadFile,
|
||||
glob: testFs.Glob,
|
||||
abs: testFs.Abs,
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
Reverse: true,
|
||||
}
|
||||
st, err := app.loadDesiredStateFromYaml(statePath)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error at %d: %v", i, err)
|
||||
}
|
||||
|
||||
if st.Releases[0].Name != tc.expected {
|
||||
t.Errorf("unexpected releases[0].name at %d: expected=%s, got=%s", i, tc.expected, st.Releases[0].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *en
|
|||
renderedEnv := r.renderEnvironment(initEnv, baseDir, filename, content)
|
||||
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("first-pass produced: %v", initEnv)
|
||||
r.logger.Debugf("first-pass produced: %v", renderedEnv)
|
||||
}
|
||||
|
||||
finalEnv, err := renderedEnv.Merge(overrode)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package maputil
|
||||
|
||||
import "fmt"
|
||||
|
||||
func CastKeysToStrings(s interface{}) (map[string]interface{}, error) {
|
||||
new := map[string]interface{}{}
|
||||
switch src := s.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
for k, v := range src {
|
||||
var str_k string
|
||||
switch typed_k := k.(type) {
|
||||
case string:
|
||||
str_k = typed_k
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of key in map: expected string, got %T: value=%v, map=%v", typed_k, typed_k, src)
|
||||
}
|
||||
|
||||
var casted_v interface{}
|
||||
switch typed_v := v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
tmp, err := CastKeysToStrings(typed_v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
casted_v = tmp
|
||||
default:
|
||||
casted_v = typed_v
|
||||
}
|
||||
|
||||
new[str_k] = casted_v
|
||||
}
|
||||
case map[string]interface{}:
|
||||
for k, v := range src {
|
||||
var casted_v interface{}
|
||||
switch typed_v := v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
tmp, err := CastKeysToStrings(typed_v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
casted_v = tmp
|
||||
default:
|
||||
casted_v = typed_v
|
||||
}
|
||||
|
||||
new[k] = casted_v
|
||||
}
|
||||
}
|
||||
return new, nil
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/environment"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"github.com/roboll/helmfile/tmpl"
|
||||
"gopkg.in/yaml.v2"
|
||||
"path/filepath"
|
||||
|
|
@ -53,21 +54,16 @@ func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *str
|
|||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
m, err := maputil.CastKeysToStrings(typedValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mergo.Merge(&envVals, &m, mergo.WithOverride); err != nil {
|
||||
return nil, fmt.Errorf("failed to merge %v: %v", typedValue, err)
|
||||
}
|
||||
continue
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of values entry: %T", typedValue)
|
||||
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", typedValue, typedValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1319,7 +1319,7 @@ func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingF
|
|||
}
|
||||
generatedFiles = append(generatedFiles, valfile.Name())
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of values entry: %T", typedValue)
|
||||
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", typedValue, typedValue)
|
||||
}
|
||||
}
|
||||
return generatedFiles, nil
|
||||
|
|
|
|||
Loading…
Reference in New Issue