Skip to content

Commit 2beb703

Browse files
committed
feat: state values
This adds `values` to state files as proposed in #640. ```yaml values: - key1: val1 - defaults.yaml environments: default: - values: - environments/default.yaml production: - values: - environments/production.yaml ``` Resolves #640
1 parent 2d2b3e4 commit 2beb703

File tree

11 files changed

+329
-28
lines changed

11 files changed

+329
-28
lines changed

pkg/app/app_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,130 @@ bar: "bar1"
900900
}
901901
}
902902

903+
func TestVisitDesiredStatesWithReleasesFiltered_StateValueOverrides(t *testing.T) {
904+
envTmplExpr := "{{ .Values.foo }}-{{ .Values.bar }}-{{ .Values.baz }}-{{ .Values.hoge }}-{{ .Values.fuga }}-{{ .Values.a | first | pluck \"b\" | first | first | pluck \"c\" | first }}"
905+
relTmplExpr := "\"{{`{{ .Values.foo }}-{{ .Values.bar }}-{{ .Values.baz }}-{{ .Values.hoge }}-{{ .Values.fuga }}-{{ .Values.a | first | pluck \\\"b\\\" | first | first | pluck \\\"c\\\" | first }}`}}\""
906+
907+
testcases := []struct {
908+
expr, env, expected string
909+
}{
910+
{
911+
expr: envTmplExpr,
912+
env: "default",
913+
expected: "foo-bar_default-baz_override-hoge_set-fuga_set-C",
914+
},
915+
{
916+
expr: envTmplExpr,
917+
env: "production",
918+
expected: "foo-bar_production-baz_override-hoge_set-fuga_set-C",
919+
},
920+
{
921+
expr: relTmplExpr,
922+
env: "default",
923+
expected: "foo-bar_default-baz_override-hoge_set-fuga_set-C",
924+
},
925+
{
926+
expr: relTmplExpr,
927+
env: "production",
928+
expected: "foo-bar_production-baz_override-hoge_set-fuga_set-C",
929+
},
930+
}
931+
for i := range testcases {
932+
testcase := testcases[i]
933+
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
934+
files := map[string]string{
935+
"/path/to/helmfile.yaml": fmt.Sprintf(`
936+
# The top-level "values" are "base" values has inherited to state values with the lowest priority.
937+
# The lowest priority results in environment-specific values to override values defined in the base.
938+
values:
939+
- values.yaml
940+
941+
environments:
942+
default:
943+
values:
944+
- default.yaml
945+
production:
946+
values:
947+
- production.yaml
948+
---
949+
releases:
950+
- name: %s
951+
chart: %s
952+
namespace: %s
953+
`, testcase.expr, testcase.expr, testcase.expr),
954+
"/path/to/values.yaml": `
955+
foo: foo
956+
bar: bar
957+
baz: baz
958+
hoge: hoge
959+
fuga: fuga
960+
961+
a: []
962+
`,
963+
"/path/to/default.yaml": `
964+
bar: "bar_default"
965+
baz: "baz_default"
966+
967+
a:
968+
- b: []
969+
`,
970+
"/path/to/production.yaml": `
971+
bar: "bar_production"
972+
baz: "baz_production"
973+
974+
a:
975+
- b: []
976+
`,
977+
"/path/to/overrides.yaml": `
978+
baz: baz_override
979+
hoge: hoge_override
980+
981+
a:
982+
- b:
983+
- c: C
984+
`,
985+
}
986+
987+
actual := []state.ReleaseSpec{}
988+
989+
collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
990+
for _, r := range st.Releases {
991+
actual = append(actual, r)
992+
}
993+
return []error{}
994+
}
995+
app := appWithFs(&App{
996+
KubeContext: "default",
997+
Logger: helmexec.NewLogger(os.Stderr, "debug"),
998+
Reverse: false,
999+
Namespace: "",
1000+
Selectors: []string{},
1001+
Env: testcase.env,
1002+
ValuesFiles: []string{"overrides.yaml"},
1003+
Set: map[string]interface{}{"hoge": "hoge_set", "fuga": "fuga_set"},
1004+
}, files)
1005+
err := app.VisitDesiredStatesWithReleasesFiltered(
1006+
"helmfile.yaml", collectReleases,
1007+
)
1008+
if err != nil {
1009+
t.Fatalf("unexpected error: %v", err)
1010+
}
1011+
if len(actual) != 1 {
1012+
t.Errorf("unexpected number of processed releases: expected=1, got=%d", len(actual))
1013+
}
1014+
if actual[0].Name != testcase.expected {
1015+
t.Errorf("unexpected name: expected=%s, got=%s", testcase.expected, actual[0].Name)
1016+
}
1017+
if actual[0].Chart != testcase.expected {
1018+
t.Errorf("unexpected chart: expected=%s, got=%s", testcase.expected, actual[0].Chart)
1019+
}
1020+
if actual[0].Namespace != testcase.expected {
1021+
t.Errorf("unexpected namespace: expected=%s, got=%s", testcase.expected, actual[0].Namespace)
1022+
}
1023+
})
1024+
}
1025+
}
1026+
9031027
func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
9041028
yamlFile := "example/path/to/yaml/file"
9051029
yamlContent := []byte(`releases:

pkg/app/desired_state_file_loader.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
8383
func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
8484
return ld.loadFileWithOverrides(inheritedEnv, nil, baseDir, file, evaluateBases)
8585
}
86+
8687
func (ld *desiredStateLoader) loadFileWithOverrides(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
8788
var f string
8889
if filepath.IsAbs(file) {

pkg/app/two_pass_renderer.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ func prependLineNumbers(text string) string {
1919
return buf.String()
2020
}
2121

22-
func (r *desiredStateLoader) renderEnvironment(firstPassEnv *environment.Environment, baseDir, filename string, content []byte) *environment.Environment {
23-
tmplData := state.EnvironmentTemplateData{Environment: *firstPassEnv, Namespace: r.namespace}
22+
func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environment, baseDir, filename string, content []byte) (*environment.Environment, *state.HelmState) {
23+
tmplData := state.EnvironmentTemplateData{
24+
Environment: *firstPassEnv,
25+
Namespace: r.namespace,
26+
Values: map[string]interface{}{},
27+
}
2428
firstPassRenderer := tmpl.NewFirstPassRenderer(baseDir, tmplData)
2529

2630
// parse as much as we can, tolerate errors, this is a preparse
@@ -29,7 +33,7 @@ func (r *desiredStateLoader) renderEnvironment(firstPassEnv *environment.Environ
2933
r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", filename, prependLineNumbers(string(content)))
3034
if yamlBuf == nil { // we have a template syntax error, let the second parse report
3135
r.logger.Debugf("template syntax error: %v", err)
32-
return firstPassEnv
36+
return firstPassEnv, nil
3337
}
3438
}
3539
yamlData := yamlBuf.String()
@@ -57,7 +61,8 @@ func (r *desiredStateLoader) renderEnvironment(firstPassEnv *environment.Environ
5761
if prestate != nil {
5862
firstPassEnv = &prestate.Env
5963
}
60-
return firstPassEnv
64+
65+
return firstPassEnv, prestate
6166
}
6267

6368
type RenderOpts struct {
@@ -88,13 +93,18 @@ func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *en
8893
r.logger.Debugf("first-pass uses: %v", initEnv)
8994
}
9095

91-
renderedEnv := r.renderEnvironment(initEnv, baseDir, filename, content)
96+
renderedEnv, prestate := r.renderPrestate(initEnv, baseDir, filename, content)
9297

9398
if r.logger != nil {
9499
r.logger.Debugf("first-pass produced: %v", renderedEnv)
95100
}
96101

97-
finalEnv, err := renderedEnv.Merge(overrode)
102+
finalEnv, err := inherited.Merge(renderedEnv)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
finalEnv, err = finalEnv.Merge(overrode)
98108
if err != nil {
99109
return nil, err
100110
}
@@ -103,7 +113,23 @@ func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *en
103113
r.logger.Debugf("first-pass rendering result of \"%s\": %v", filename, *finalEnv)
104114
}
105115

106-
tmplData := state.EnvironmentTemplateData{Environment: *finalEnv, Namespace: r.namespace}
116+
vals := map[string]interface{}{}
117+
if prestate != nil {
118+
prestate.Env = *finalEnv
119+
vals, err = prestate.Values()
120+
if err != nil {
121+
return nil, err
122+
}
123+
}
124+
if prestate != nil {
125+
r.logger.Debugf("vals:\n%v\ndefaultVals:%v", vals, prestate.DefaultValues)
126+
}
127+
128+
tmplData := state.EnvironmentTemplateData{
129+
Environment: *finalEnv,
130+
Namespace: r.namespace,
131+
Values: vals,
132+
}
107133
secondPassRenderer := tmpl.NewFileRenderer(r.readFile, baseDir, tmplData)
108134
yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content)
109135
if err != nil {

pkg/environment/environment.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,44 @@ import (
77
)
88

99
type Environment struct {
10-
Name string
11-
Values map[string]interface{}
10+
Name string
11+
Values map[string]interface{}
12+
Defaults map[string]interface{}
1213
}
1314

1415
var EmptyEnvironment Environment
1516

1617
func (e Environment) DeepCopy() Environment {
17-
bytes, err := yaml.Marshal(e.Values)
18+
valuesBytes, err := yaml.Marshal(e.Values)
1819
if err != nil {
1920
panic(err)
2021
}
2122
var values map[string]interface{}
22-
if err := yaml.Unmarshal(bytes, &values); err != nil {
23+
if err := yaml.Unmarshal(valuesBytes, &values); err != nil {
2324
panic(err)
2425
}
2526
values, err = maputil.CastKeysToStrings(values)
2627
if err != nil {
2728
panic(err)
2829
}
30+
31+
defaultsBytes, err := yaml.Marshal(e.Defaults)
32+
if err != nil {
33+
panic(err)
34+
}
35+
var defaults map[string]interface{}
36+
if err := yaml.Unmarshal(defaultsBytes, &defaults); err != nil {
37+
panic(err)
38+
}
39+
defaults, err = maputil.CastKeysToStrings(defaults)
40+
if err != nil {
41+
panic(err)
42+
}
43+
2944
return Environment{
30-
Name: e.Name,
31-
Values: values,
45+
Name: e.Name,
46+
Values: values,
47+
Defaults: defaults,
3248
}
3349
}
3450

pkg/maputil/maputil_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package maputil
2+
3+
import "testing"
4+
5+
func TestMapUtil_StrKeys(t *testing.T) {
6+
m := map[string]interface{}{
7+
"a": []interface{}{
8+
map[string]interface{}{
9+
"b": []interface{}{
10+
map[string]interface{}{
11+
"c": "C",
12+
},
13+
},
14+
},
15+
},
16+
}
17+
18+
r, err := CastKeysToStrings(m)
19+
if err != nil {
20+
t.Fatalf("unexpected error: %v", err)
21+
}
22+
23+
a := r["a"].([]interface{})
24+
a0 := a[0].(map[string]interface{})
25+
b := a0["b"].([]interface{})
26+
b0 := b[0].(map[string]interface{})
27+
c := b0["c"]
28+
29+
if c != "C" {
30+
t.Errorf("unexpected c: expected=C, got=%s", c)
31+
}
32+
}
33+
34+
func TestMapUtil_IFKeys(t *testing.T) {
35+
m := map[interface{}]interface{}{
36+
"a": []interface{}{
37+
map[interface{}]interface{}{
38+
"b": []interface{}{
39+
map[interface{}]interface{}{
40+
"c": "C",
41+
},
42+
},
43+
},
44+
},
45+
}
46+
47+
r, err := CastKeysToStrings(m)
48+
if err != nil {
49+
t.Fatalf("unexpected error: %v", err)
50+
}
51+
52+
a := r["a"].([]interface{})
53+
a0 := a[0].(map[string]interface{})
54+
b := a0["b"].([]interface{})
55+
b0 := b[0].(map[string]interface{})
56+
c := b0["c"]
57+
58+
if c != "C" {
59+
t.Errorf("unexpected c: expected=C, got=%s", c)
60+
}
61+
}

pkg/state/create.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *envi
114114
if err != nil {
115115
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
116116
}
117+
118+
e.Defaults, err = state.loadValuesEntries(nil, state.DefaultValues)
119+
if err != nil {
120+
return nil, err
121+
}
122+
117123
state.Env = *e
118124

119125
return &state, nil
@@ -137,7 +143,12 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
137143
return nil, err
138144
}
139145

140-
return c.LoadEnvValues(state, envName, envValues)
146+
state, err = c.LoadEnvValues(state, envName, envValues)
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
return state, nil
141152
}
142153

143154
func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
@@ -164,13 +175,8 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
164175
envVals := map[string]interface{}{}
165176
envSpec, ok := st.Environments[name]
166177
if ok {
167-
envValues := append([]interface{}{}, envSpec.Values...)
168-
ld := &EnvironmentValuesLoader{
169-
storage: st.storage(),
170-
readFile: st.readFile,
171-
}
172178
var err error
173-
envVals, err = ld.LoadEnvironmentValues(envSpec.MissingFileHandler, envValues)
179+
envVals, err = st.loadValuesEntries(envSpec.MissingFileHandler, envSpec.Values)
174180
if err != nil {
175181
return nil, err
176182
}
@@ -237,3 +243,20 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
237243

238244
return newEnv, nil
239245
}
246+
247+
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []interface{}) (map[string]interface{}, error) {
248+
envVals := map[string]interface{}{}
249+
250+
valuesEntries := append([]interface{}{}, entries...)
251+
ld := &EnvironmentValuesLoader{
252+
storage: st.storage(),
253+
readFile: st.readFile,
254+
}
255+
var err error
256+
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries)
257+
if err != nil {
258+
return nil, err
259+
}
260+
261+
return envVals, nil
262+
}

0 commit comments

Comments
 (0)