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 <aiopsclub@163.com>

* docs: add setString section to index.md for helm configuration

Signed-off-by: yxxhero <aiopsclub@163.com>

* tests: fix more tests

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero 2024-12-09 23:37:56 +08:00 committed by GitHub
parent b1f827394c
commit bd12fa1cc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 140 additions and 13 deletions

View File

@ -282,6 +282,16 @@ releases:
domain: {{ requiredEnv "PLATFORM_ID" }}.my-domain.com domain: {{ requiredEnv "PLATFORM_ID" }}.my-domain.com
scheme: {{ env "SCHEME" | default "https" }} scheme: {{ env "SCHEME" | default "https" }}
# Use `values` whenever possible! # 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` translates to helm's `--set key=val`, that is known to suffer from type issues like https://github.com/roboll/helmfile/issues/608
set: set:
# single value loaded from a local file, translates to --set-file foo.config=path/to/file # single value loaded from a local file, translates to --set-file foo.config=path/to/file

View File

@ -4006,6 +4006,73 @@ myrelease4 testNamespace true true id:myrelease1 mychart1
assert.Equal(t, expected, out) 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) { func testSetValuesTemplate(t *testing.T, goccyGoYaml bool) {
t.Helper() 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 { func location() string {
_, fn, line, _ := goruntime.Caller(1) _, fn, line, _ := goruntime.Caller(1)
return fmt.Sprintf("%s:%d", filepath.Base(fn), line) return fmt.Sprintf("%s:%d", filepath.Base(fn), line)

View File

@ -309,13 +309,14 @@ type ReleaseSpec struct {
Hooks []event.Hook `yaml:"hooks,omitempty"` Hooks []event.Hook `yaml:"hooks,omitempty"`
// Name is the name of this release // Name is the name of this release
Name string `yaml:"name,omitempty"` Name string `yaml:"name,omitempty"`
Namespace string `yaml:"namespace,omitempty"` Namespace string `yaml:"namespace,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"` Labels map[string]string `yaml:"labels,omitempty"`
Values []any `yaml:"values,omitempty"` Values []any `yaml:"values,omitempty"`
Secrets []any `yaml:"secrets,omitempty"` Secrets []any `yaml:"secrets,omitempty"`
SetValues []SetValue `yaml:"set,omitempty"` SetValues []SetValue `yaml:"set,omitempty"`
duration time.Duration SetStringValues []SetValue `yaml:"setString,omitempty"`
duration time.Duration
ValuesTemplate []any `yaml:"valuesTemplate,omitempty"` ValuesTemplate []any `yaml:"valuesTemplate,omitempty"`
SetValuesTemplate []SetValue `yaml:"setTemplate,omitempty"` SetValuesTemplate []SetValue `yaml:"setTemplate,omitempty"`
@ -3341,6 +3342,15 @@ func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *R
flags = append(flags, setFlags...) 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 * START 'env' section for backwards compatibility
***********/ ***********/
@ -3400,6 +3410,34 @@ func (st *HelmState) setFlags(setValues []SetValue) ([]string, error) {
return flags, nil 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 // renderValsSecrets helper function which renders 'ref+.*' secrets
func renderValsSecrets(e vals.Evaluator, input ...string) ([]string, error) { func renderValsSecrets(e vals.Evaluator, input ...string) ([]string, error) {
output := make([]string, len(input)) output := make([]string, len(input))

View File

@ -199,6 +199,8 @@ func (st *HelmState) releaseWithInheritedTemplate(r *ReleaseSpec, inheritancePat
src.SetValuesTemplate = nil src.SetValuesTemplate = nil
case "set": case "set":
src.SetValues = nil src.SetValues = nil
case "setString":
src.SetStringValues = nil
case "secrets": case "secrets":
src.Secrets = nil src.Secrets = nil
default: default:

View File

@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
run(testcase{ run(testcase{
subject: "baseline", subject: "baseline",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
want: "foo-values-5db58595d7", want: "foo-values-5b58697694",
}) })
run(testcase{ run(testcase{
subject: "different bytes content", subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`), data: []byte(`{"k":"v"}`),
want: "foo-values-78d88d86dd", want: "foo-values-58bff47d77",
}) })
run(testcase{ run(testcase{
subject: "different map content", subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"}, data: map[string]any{"k": "v"},
want: "foo-values-f9c8967cd", want: "foo-values-5fb8948f75",
}) })
run(testcase{ run(testcase{
subject: "different chart", subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"}, release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-cdfb97444", want: "foo-values-784b76684f",
}) })
run(testcase{ run(testcase{
subject: "different name", subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"}, release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-749bc4c6d4", want: "bar-values-f48df5f49",
}) })
run(testcase{ run(testcase{
subject: "specific ns", subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"}, release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-7b74fbd6d6", want: "myns-foo-values-6b68696b8c",
}) })
for id, n := range ids { for id, n := range ids {