fix: support array of maps in set/setTemplate values (#2615)

Changed SetValue.Values type from []string to []any to allow passing
maps (not just strings) in the values field of set/setTemplate.

Previously, YAML like:
  setTemplate:
    - name: source.helm.parameters
      values:
        - name: demo
        - version: v2

would fail with 'cannot unmarshal !!map into string'. Map values are
now serialized to JSON when generating --set flags.

Fixes #1021

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero 2026-05-31 09:57:35 +08:00 committed by GitHub
parent fbf4f31c02
commit 7391453cbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 78 additions and 16 deletions

View File

@ -193,13 +193,18 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
}
result.SetValuesTemplate[i].File = s.String()
}
for j, ts := range val.Values {
for j, tv := range val.Values {
// values
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].values[%d] = \"%s\": %v", r.Name, i, j, ts, err)
switch ts := tv.(type) {
case string:
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].values[%d] = \"%s\": %v", r.Name, i, j, ts, err)
}
result.SetValuesTemplate[i].Values[j] = s.String()
default:
result.SetValuesTemplate[i].Values[j] = ts
}
result.SetValuesTemplate[i].Values[j] = s.String()
}
}

View File

@ -576,10 +576,10 @@ type Release struct {
// SetValue are the key values to set on a helm release
type SetValue struct {
Name string `yaml:"name,omitempty"`
Value string `yaml:"value,omitempty"`
File string `yaml:"file,omitempty"`
Values []string `yaml:"values,omitempty"`
Name string `yaml:"name,omitempty"`
Value string `yaml:"value,omitempty"`
File string `yaml:"file,omitempty"`
Values []any `yaml:"values,omitempty"`
}
// AffectedReleases hold the list of released that where updated, deleted, or in error
@ -4570,7 +4570,7 @@ func (st *HelmState) setFlags(setValues []SetValue) ([]string, error) {
} else if set.File != "" {
flags = append(flags, "--set-file", fmt.Sprintf("%s=%s", escape(set.Name), st.storage().normalizeSetFilePath(set.File, runtime.GOOS)))
} else if len(set.Values) > 0 {
renderedValues, err := renderValsSecrets(st.valsRuntime, set.Values...)
renderedValues, err := renderValsSecretsAny(st.valsRuntime, set.Values)
if err != nil {
return nil, err
}
@ -4598,7 +4598,7 @@ func (st *HelmState) setStringFlags(setValues []SetValue) ([]string, error) {
}
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...)
renderedValues, err := renderValsSecretsAny(st.valsRuntime, set.Values)
if err != nil {
return nil, err
}
@ -4635,6 +4635,43 @@ func renderValsSecrets(e vals.Evaluator, input ...string) ([]string, error) {
return output, nil
}
// renderValsSecretsAny renders 'ref+.*' secrets in a slice of any-typed values.
// Map values are serialized to JSON; string values are rendered via vals.
func renderValsSecretsAny(e vals.Evaluator, input []any) ([]string, error) {
output := make([]string, len(input))
if len(input) == 0 {
return output, nil
}
strInputs := make([]string, 0, len(input))
strIndexMap := make([]int, 0, len(input))
for i, v := range input {
switch tv := v.(type) {
case string:
strInputs = append(strInputs, tv)
strIndexMap = append(strIndexMap, i)
default:
jsonBytes, err := json.Marshal(tv)
if err != nil {
return nil, fmt.Errorf("failed to marshal set value at index %d: %w", i, err)
}
output[i] = string(jsonBytes)
}
}
if len(strInputs) > 0 {
rendered, err := renderValsSecrets(e, strInputs...)
if err != nil {
return nil, err
}
for idx, renderedIdx := range strIndexMap {
output[renderedIdx] = rendered[idx]
}
}
return output, nil
}
func hideChartCredentials(chartCredentials string) (string, error) {
u, err := url.Parse(chartCredentials)
if err != nil {

View File

@ -96,7 +96,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
SetValuesTemplate: []SetValue{
{Name: "val1", Value: "{{ .Release.Name }}-val1"},
{Name: "val2", File: "{{ .Release.Name }}.yml"},
{Name: "val3", Values: []string{"{{ .Release.Name }}-val2", "{{ .Release.Name }}-val3"}},
{Name: "val3", Values: []any{"{{ .Release.Name }}-val2", "{{ .Release.Name }}-val3"}},
{Name: "val4", Value: "{{ .Release.Chart }}-{{ .Release.ChartVersion}}"},
},
},
@ -108,7 +108,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
SetValues: []SetValue{
{Name: "val1", Value: "test-app-val1"},
{Name: "val2", File: "test-app.yml"},
{Name: "val3", Values: []string{"test-app-val2", "test-app-val3"}},
{Name: "val3", Values: []any{"test-app-val2", "test-app-val3"}},
{Name: "val4", Value: "test-charts/chart-1.5"},
},
},

View File

@ -2096,7 +2096,7 @@ func TestHelmState_SyncReleases(t *testing.T) {
SetValues: []SetValue{
{
Name: "foo.bar[0]",
Values: []string{
Values: []any{
"A",
"B",
},
@ -2107,6 +2107,26 @@ func TestHelmState_SyncReleases(t *testing.T) {
helm: &exectest.Helm{},
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "foo.bar[0]={A,B}", "--reset-values"}}},
},
{
name: "set array of map values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "source.helm.parameters",
Values: []any{
map[string]any{"name": "demo"},
map[string]any{"version": "v2"},
},
},
},
},
},
helm: &exectest.Helm{},
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "source.helm.parameters={\\{\"name\":\"demo\"\\},\\{\"version\":\"v2\"\\}}", "--reset-values"}}},
},
{
name: "post renderer helm 3",
releases: []ReleaseSpec{
@ -2709,7 +2729,7 @@ func TestHelmState_DiffReleases(t *testing.T) {
SetValues: []SetValue{
{
Name: "foo.bar[0]",
Values: []string{
Values: []any{
"A",
"B",
},
@ -5698,7 +5718,7 @@ func TestHelmState_setStringFlags(t *testing.T) {
setStringValues: []SetValue{
{
Name: "key",
Values: []string{"value1", "value2"},
Values: []any{"value1", "value2"},
},
},
want: []string{"--set-string", "key={value1,value2}"},