Skip to content

Commit abe83c3

Browse files
committed
Fix helm valuesInline merge failure when types differ
When using valuesInline with helm charts, kustomize would fail with "wrong node kind" errors when the chart's values.yaml had a different type than what the user provided. For example: - Chart has `topologySpreadConstraints: {}` but user provides a list - Chart has `githubConfigSecret: {github_token: ""}` but user provides a string This adds an AllowKindChange option to MergeOptions that allows the source value to completely replace the destination when their YAML node kinds differ (e.g., map vs list, map vs scalar). The HelmChartInflationGenerator now enables this option when merging values, fixing the regression introduced in kustomize 5.4.0. Fixes #5766 Fixes actions/actions-runner-controller#3819
1 parent 17a06a7 commit abe83c3

File tree

4 files changed

+237
-2
lines changed

4 files changed

+237
-2
lines changed

kyaml/yaml/merge2/merge2_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,178 @@ type testCase struct {
5151
infer bool
5252
mergeOptions yaml.MergeOptions
5353
}
54+
55+
// TestMergeWithKindChange tests the AllowKindChange option which allows
56+
// merging nodes of different kinds (e.g., map to list, map to scalar).
57+
// This is needed for Helm values merging where charts may have placeholder
58+
// types that don't match what users want to provide.
59+
// Reference: https://github.com/kubernetes-sigs/kustomize/issues/5766
60+
// Reference: https://github.com/actions/actions-runner-controller/issues/3819
61+
func TestMergeWithKindChange(t *testing.T) {
62+
testCases := []struct {
63+
name string
64+
source string
65+
dest string
66+
expected string
67+
allowKindChange bool
68+
expectError bool
69+
}{
70+
{
71+
// Issue #5766: Chart has empty map {}, user wants to provide a list
72+
name: "map to list with AllowKindChange=true (issue #5766)",
73+
source: `
74+
topologySpreadConstraints:
75+
- maxSkew: 1
76+
topologyKey: topology.kubernetes.io/zone
77+
`,
78+
dest: `
79+
topologySpreadConstraints: {}
80+
`,
81+
expected: `
82+
topologySpreadConstraints:
83+
- maxSkew: 1
84+
topologyKey: topology.kubernetes.io/zone
85+
`,
86+
allowKindChange: true,
87+
expectError: false,
88+
},
89+
{
90+
name: "map to list without AllowKindChange should fail",
91+
source: `
92+
topologySpreadConstraints:
93+
- maxSkew: 1
94+
`,
95+
dest: `
96+
topologySpreadConstraints: {}
97+
`,
98+
allowKindChange: false,
99+
expectError: true,
100+
},
101+
{
102+
// Issue #3819: Chart has map structure, user wants to provide a scalar string
103+
name: "map to scalar with AllowKindChange=true (issue #3819)",
104+
source: `
105+
githubConfigSecret: my-custom-secret
106+
`,
107+
dest: `
108+
githubConfigSecret:
109+
github_token: ""
110+
`,
111+
expected: `
112+
githubConfigSecret: my-custom-secret
113+
`,
114+
allowKindChange: true,
115+
expectError: false,
116+
},
117+
{
118+
name: "map to scalar without AllowKindChange should fail",
119+
source: `
120+
githubConfigSecret: my-custom-secret
121+
`,
122+
dest: `
123+
githubConfigSecret:
124+
github_token: ""
125+
`,
126+
allowKindChange: false,
127+
expectError: true,
128+
},
129+
{
130+
// Reverse case: list to map
131+
name: "list to map with AllowKindChange=true",
132+
source: `
133+
tls:
134+
termination: edge
135+
insecureEdgeTerminationPolicy: Redirect
136+
`,
137+
dest: `
138+
tls: []
139+
`,
140+
expected: `
141+
tls:
142+
termination: edge
143+
insecureEdgeTerminationPolicy: Redirect
144+
`,
145+
allowKindChange: true,
146+
expectError: false,
147+
},
148+
{
149+
// Scalar to map
150+
name: "scalar to map with AllowKindChange=true",
151+
source: `
152+
config:
153+
enabled: true
154+
value: 42
155+
`,
156+
dest: `
157+
config: "default"
158+
`,
159+
expected: `
160+
config:
161+
enabled: true
162+
value: 42
163+
`,
164+
allowKindChange: true,
165+
expectError: false,
166+
},
167+
{
168+
// Nested kind change
169+
name: "nested kind change with AllowKindChange=true",
170+
source: `
171+
route:
172+
tls:
173+
termination: edge
174+
`,
175+
dest: `
176+
route:
177+
tls: []
178+
`,
179+
expected: `
180+
route:
181+
tls:
182+
termination: edge
183+
`,
184+
allowKindChange: true,
185+
expectError: false,
186+
},
187+
}
188+
189+
for _, tc := range testCases {
190+
t.Run(tc.name, func(t *testing.T) {
191+
mergeOpts := yaml.MergeOptions{AllowKindChange: tc.allowKindChange}
192+
actual, err := MergeStrings(tc.source, tc.dest, false, mergeOpts)
193+
194+
if tc.expectError {
195+
assert.Error(t, err, "expected error but got none")
196+
return
197+
}
198+
199+
if !assert.NoError(t, err, tc.name) {
200+
t.FailNow()
201+
}
202+
203+
// Parse and format both expected and actual for comparison
204+
src, err := yaml.Parse(tc.source)
205+
if !assert.NoError(t, err) {
206+
t.FailNow()
207+
}
208+
srcMap, err := src.Map()
209+
if !assert.NoError(t, err) {
210+
t.FailNow()
211+
}
212+
213+
act, err := yaml.Parse(actual)
214+
if !assert.NoError(t, err) {
215+
t.FailNow()
216+
}
217+
actMap, err := act.Map()
218+
if !assert.NoError(t, err) {
219+
t.FailNow()
220+
}
221+
222+
// The source values should be present in the result
223+
for key := range srcMap {
224+
assert.Contains(t, actMap, key, "result should contain key from source: %s", key)
225+
}
226+
})
227+
}
228+
}

kyaml/yaml/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,14 @@ type MergeOptions struct {
246246
// ListIncreaseDirection indicates should merge function prepend the items from
247247
// source list to destination or append.
248248
ListIncreaseDirection MergeOptionsListIncreaseDirection
249+
250+
// AllowKindChange if set to true will allow the merge to replace a field with
251+
// a different type. For example, if the destination has a map and the source
252+
// has a list (or scalar), the source will replace the destination instead of
253+
// returning an error. This is useful for merging Helm values where the chart's
254+
// values.yaml may have a placeholder type that needs to be overridden.
255+
// See https://github.com/kubernetes-sigs/kustomize/issues/5766
256+
AllowKindChange bool
249257
}
250258

251259
// Since ObjectMeta and TypeMeta are stable, we manually create DeepCopy funcs for ResourceMeta and ObjectMeta.

kyaml/yaml/walk/walk.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,25 @@ func (l Walker) Walk() (*yaml.RNode, error) {
6060
switch l.Kind() {
6161
case yaml.MappingNode:
6262
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.MappingNode, l.Sources...); err != nil {
63+
// If AllowKindChange is set and there's a type mismatch, allow the
64+
// origin (source) to replace dest entirely instead of erroring.
65+
if l.MergeOptions.AllowKindChange {
66+
if replaced := l.replaceOnKindMismatch(); replaced != nil {
67+
return replaced, nil
68+
}
69+
}
6370
return nil, err
6471
}
6572
return l.walkMap()
6673
case yaml.SequenceNode:
6774
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.SequenceNode, l.Sources...); err != nil {
75+
// If AllowKindChange is set and there's a type mismatch, allow the
76+
// origin (source) to replace dest entirely instead of erroring.
77+
if l.MergeOptions.AllowKindChange {
78+
if replaced := l.replaceOnKindMismatch(); replaced != nil {
79+
return replaced, nil
80+
}
81+
}
6882
return nil, err
6983
}
7084
// AssociativeSequence means the items in the sequence are associative. They can be merged
@@ -76,6 +90,13 @@ func (l Walker) Walk() (*yaml.RNode, error) {
7690

7791
case yaml.ScalarNode:
7892
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.ScalarNode, l.Sources...); err != nil {
93+
// If AllowKindChange is set and there's a type mismatch, allow the
94+
// origin (source) to replace dest entirely instead of erroring.
95+
if l.MergeOptions.AllowKindChange {
96+
if replaced := l.replaceOnKindMismatch(); replaced != nil {
97+
return replaced, nil
98+
}
99+
}
79100
return nil, err
80101
}
81102
return l.walkScalar()
@@ -87,6 +108,32 @@ func (l Walker) Walk() (*yaml.RNode, error) {
87108
}
88109
}
89110

111+
// replaceOnKindMismatch returns the origin node when there's a kind mismatch
112+
// between dest and origin. This allows the origin to completely replace the
113+
// dest when they have different types (e.g., map vs list, map vs scalar).
114+
// Returns nil if no replacement should occur.
115+
func (l Walker) replaceOnKindMismatch() *yaml.RNode {
116+
dest := l.Sources.Dest()
117+
origin := l.Sources.Origin()
118+
119+
// If origin is nil or null, we can't do a replacement
120+
if yaml.IsMissingOrNull(origin) {
121+
return nil
122+
}
123+
124+
// If dest is nil or null, origin should be used directly
125+
if yaml.IsMissingOrNull(dest) {
126+
return origin
127+
}
128+
129+
// Check if origin has a different kind than dest - if so, origin replaces dest
130+
if origin.YNode().Kind != dest.YNode().Kind {
131+
return origin
132+
}
133+
134+
return nil
135+
}
136+
90137
func (l Walker) GetSchema() *openapi.ResourceSchema {
91138
for i := range l.Sources {
92139
r := l.Sources[i]

plugin/builtin/helmchartinflationgenerator/HelmChartInflationGenerator.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,20 @@ func (p *plugin) replaceValuesInline() error {
226226
return errors.WrapPrefixf(err, "could not parse values inline into rnode")
227227
}
228228
var outValues *kyaml.RNode
229+
// Enable AllowKindChange to handle cases where the chart's values.yaml has
230+
// a different type (e.g., empty map {}) than what the user provides in valuesInline
231+
// (e.g., a list []). This allows the user's inline values to override the type.
232+
// See https://github.com/kubernetes-sigs/kustomize/issues/5766
233+
mergeOpts := kyaml.MergeOptions{AllowKindChange: true}
229234
switch p.ValuesMerge {
230235
// Function `merge2.Merge` overrides values in dest with values from src.
231236
// To achieve override or merge behavior, we pass parameters in different order.
232237
// Object passed as dest will be modified, so we copy it just in case someone
233238
// decides to use it after this is called.
234239
case valuesMergeOptionOverride:
235-
outValues, err = merge2.Merge(inlineValues, chValues.Copy(), kyaml.MergeOptions{})
240+
outValues, err = merge2.Merge(inlineValues, chValues.Copy(), mergeOpts)
236241
case valuesMergeOptionMerge:
237-
outValues, err = merge2.Merge(chValues, inlineValues.Copy(), kyaml.MergeOptions{})
242+
outValues, err = merge2.Merge(chValues, inlineValues.Copy(), mergeOpts)
238243
}
239244
if err != nil {
240245
return errors.WrapPrefixf(err, "could not merge values")

0 commit comments

Comments
 (0)