From bd12fa1cc322491f6a1ef6a1c524b87848c84c58 Mon Sep 17 00:00:00 2001 From: yxxhero <11087727+yxxhero@users.noreply.github.com> Date: Mon, 9 Dec 2024 23:37:56 +0800 Subject: [PATCH] feat(state): add support for setString in ReleaseSpec and HelmState (#1821) * feat(state): add support for setString in ReleaseSpec and HelmState Signed-off-by: yxxhero * docs: add setString section to index.md for helm configuration Signed-off-by: yxxhero * tests: fix more tests Signed-off-by: yxxhero --------- Signed-off-by: yxxhero --- docs/index.md | 10 +++++ pkg/app/app_test.go | 77 ++++++++++++++++++++++++++++++++++++ pkg/state/state.go | 52 ++++++++++++++++++++---- pkg/state/state_exec_tmpl.go | 2 + pkg/state/temp_test.go | 12 +++--- 5 files changed, 140 insertions(+), 13 deletions(-) diff --git a/docs/index.md b/docs/index.md index a8c5a93e..fdc0fef1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -282,6 +282,16 @@ releases: domain: {{ requiredEnv "PLATFORM_ID" }}.my-domain.com scheme: {{ env "SCHEME" | default "https" }} # Use `values` whenever possible! + # `setString` translates to helm's `--set-string key=val` + setString: + # set a single array value in an array, translates to --set-string bar[0]={1,2} + - name: bar[0] + values: + - 1 + - 2 + # set a templated value + - name: namespace + value: {{ .Namespace }} # `set` translates to helm's `--set key=val`, that is known to suffer from type issues like https://github.com/roboll/helmfile/issues/608 set: # single value loaded from a local file, translates to --set-file foo.config=path/to/file diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index f6094f79..21860443 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -4006,6 +4006,73 @@ myrelease4 testNamespace true true id:myrelease1 mychart1 assert.Equal(t, expected, out) } +func testSetStringValuesTemplate(t *testing.T, goccyGoYaml bool) { + t.Helper() + + v := runtime.GoccyGoYaml + runtime.GoccyGoYaml = goccyGoYaml + t.Cleanup(func() { + runtime.GoccyGoYaml = v + }) + + files := map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: zipkin + chart: stable/zipkin + values: + - val2: "val2" + valuesTemplate: + - val1: '{{"{{ .Release.Name }}"}}' + setString: + - name: "name" + value: "val" +`, + } + expectedValues := []any{ + map[string]any{"val1": "zipkin"}, + map[string]any{"val2": "val2"}} + expectedSetValues := []state.SetValue{ + {Name: "name", Value: "val"}} + + app := appWithFs(&App{ + OverrideHelmBinary: DefaultHelmBinary, + OverrideKubeContext: "default", + Logger: newAppTestLogger(), + Env: "default", + FileOrDir: "helmfile.yaml", + }, files) + + expectNoCallsToHelm(app) + + var specs []state.ReleaseSpec + collectReleases := func(run *Run) (bool, []error) { + specs = append(specs, run.state.Releases...) + return false, nil + } + + err := app.ForEachState( + collectReleases, + false, + SetFilter(true), + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(specs) != 1 { + t.Fatalf("expected 1 release; got %d releases", len(specs)) + } + actualValues := specs[0].Values + actualSetStringValues := specs[0].SetStringValues + + if !reflect.DeepEqual(expectedValues, actualValues) { + t.Errorf("expected values: %v; got values: %v", expectedValues, actualValues) + } + if !reflect.DeepEqual(expectedSetValues, actualSetStringValues) { + t.Errorf("expected set-string: %v; got set: %v", expectedValues, actualValues) + } +} func testSetValuesTemplate(t *testing.T, goccyGoYaml bool) { t.Helper() @@ -4089,6 +4156,16 @@ func TestSetValuesTemplate(t *testing.T) { }) } +func TestSetStringValuesTemplate(t *testing.T) { + t.Run("with goccy/go-yaml", func(t *testing.T) { + testSetStringValuesTemplate(t, true) + }) + + t.Run("with gopkg.in/yaml.v2", func(t *testing.T) { + testSetStringValuesTemplate(t, false) + }) +} + func location() string { _, fn, line, _ := goruntime.Caller(1) return fmt.Sprintf("%s:%d", filepath.Base(fn), line) diff --git a/pkg/state/state.go b/pkg/state/state.go index 6c954faf..61ac599b 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -309,13 +309,14 @@ type ReleaseSpec struct { Hooks []event.Hook `yaml:"hooks,omitempty"` // Name is the name of this release - Name string `yaml:"name,omitempty"` - Namespace string `yaml:"namespace,omitempty"` - Labels map[string]string `yaml:"labels,omitempty"` - Values []any `yaml:"values,omitempty"` - Secrets []any `yaml:"secrets,omitempty"` - SetValues []SetValue `yaml:"set,omitempty"` - duration time.Duration + Name string `yaml:"name,omitempty"` + Namespace string `yaml:"namespace,omitempty"` + Labels map[string]string `yaml:"labels,omitempty"` + Values []any `yaml:"values,omitempty"` + Secrets []any `yaml:"secrets,omitempty"` + SetValues []SetValue `yaml:"set,omitempty"` + SetStringValues []SetValue `yaml:"setString,omitempty"` + duration time.Duration ValuesTemplate []any `yaml:"valuesTemplate,omitempty"` SetValuesTemplate []SetValue `yaml:"setTemplate,omitempty"` @@ -3341,6 +3342,15 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R flags = append(flags, setFlags...) } + if len(release.SetStringValues) > 0 { + setStringFlags, err := st.setStringFlags(release.SetStringValues) + if err != nil { + return nil, files, fmt.Errorf("Failed to render set string value entry in %s for release %s: %v", st.FilePath, release.Name, err) + } + + flags = append(flags, setStringFlags...) + } + /*********** * START 'env' section for backwards compatibility ***********/ @@ -3400,6 +3410,34 @@ func (st *HelmState) setFlags(setValues []SetValue) ([]string, error) { return flags, nil } +// setStringFlags is to generate the set-string flags for helm +func (st *HelmState) setStringFlags(setValues []SetValue) ([]string, error) { + var flags []string + + for _, set := range setValues { + if set.Value != "" { + renderedValue, err := renderValsSecrets(st.valsRuntime, set.Value) + if err != nil { + return nil, err + } + flags = append(flags, "--set-string", fmt.Sprintf("%s=%s", escape(set.Name), escape(renderedValue[0]))) + } else if len(set.Values) > 0 { + renderedValues, err := renderValsSecrets(st.valsRuntime, set.Values...) + if err != nil { + return nil, err + } + items := make([]string, len(renderedValues)) + for i, raw := range renderedValues { + items[i] = escape(raw) + } + v := strings.Join(items, ",") + flags = append(flags, "--set-string", fmt.Sprintf("%s={%s}", escape(set.Name), v)) + } + } + + return flags, nil +} + // renderValsSecrets helper function which renders 'ref+.*' secrets func renderValsSecrets(e vals.Evaluator, input ...string) ([]string, error) { output := make([]string, len(input)) diff --git a/pkg/state/state_exec_tmpl.go b/pkg/state/state_exec_tmpl.go index f4ec6dce..23684d4b 100644 --- a/pkg/state/state_exec_tmpl.go +++ b/pkg/state/state_exec_tmpl.go @@ -199,6 +199,8 @@ func (st *HelmState) releaseWithInheritedTemplate(r *ReleaseSpec, inheritancePat src.SetValuesTemplate = nil case "set": src.SetValues = nil + case "setString": + src.SetStringValues = nil case "secrets": src.Secrets = nil default: diff --git a/pkg/state/temp_test.go b/pkg/state/temp_test.go index 1fb59c3a..cc6769be 100644 --- a/pkg/state/temp_test.go +++ b/pkg/state/temp_test.go @@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) { run(testcase{ subject: "baseline", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, - want: "foo-values-5db58595d7", + want: "foo-values-5b58697694", }) run(testcase{ subject: "different bytes content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: []byte(`{"k":"v"}`), - want: "foo-values-78d88d86dd", + want: "foo-values-58bff47d77", }) run(testcase{ subject: "different map content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: map[string]any{"k": "v"}, - want: "foo-values-f9c8967cd", + want: "foo-values-5fb8948f75", }) run(testcase{ subject: "different chart", release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"}, - want: "foo-values-cdfb97444", + want: "foo-values-784b76684f", }) run(testcase{ subject: "different name", release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"}, - want: "bar-values-749bc4c6d4", + want: "bar-values-f48df5f49", }) run(testcase{ subject: "specific ns", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"}, - want: "myns-foo-values-7b74fbd6d6", + want: "myns-foo-values-6b68696b8c", }) for id, n := range ids {