Fix #32530: preserve null values in top-level chart values.yaml

Since Helm 4.2.0, null-valued keys defined directly in the top-level chart's
own values.yaml were being removed from `.Values` during coalescing. For
example:

```yaml
stuff:
  foo: bar
  baz: null
foobar: null
```

would render `{"stuff":{"foo":"bar"}}` instead of
`{"foobar":null,"stuff":{"baz":null,"foo":"bar"}}`.

The root cause is in `coalesceValues`: the nil-skipping and `cleanNilValues`
cleanup applied to `val == nil` keys was being applied unconditionally to the
top-level chart's own defaults, not just to subchart defaults.

This change restricts the nil cleanup to subchart coalescing (`prefix != ""`),
so null values defined directly in the top-level chart's values.yaml are now
preserved. Subchart default nils are still cleaned as before, retaining the
in-place delete behavior from the `cleanNilValues` path.

Adds a regression test covering both a top-level null key and a nested null
key. Restores the pre-4.2.0 behavior described in the issue.

Signed-off-by: waterWang
pull/32531/head
waterWang 1 month ago
parent f3d68cdbea
commit e35f6bb312

@ -251,9 +251,11 @@ func coalesceValues(printf printFn, c chart.Charter, v map[string]any, prefix st
// If the key is a child chart, coalesce tables with Merge set to true // If the key is a child chart, coalesce tables with Merge set to true
merge := childChartMergeTrue(c, key, merge) merge := childChartMergeTrue(c, key, merge)
// When coalescing, clean nils from chart defaults before merging // When coalescing a subchart default, clean nils from the chart
// so they don't leak into the result. // defaults before merging so they don't leak into the result.
if !merge { // Null values defined directly in the top-level chart's own
// values.yaml are preserved (see helm#32530).
if !merge && prefix != "" {
cleanNilValues(src) cleanNilValues(src)
} }
@ -264,9 +266,11 @@ func coalesceValues(printf printFn, c chart.Charter, v map[string]any, prefix st
} }
} else { } else {
// If the key is not in v, copy it from nv. // If the key is not in v, copy it from nv.
// When coalescing, skip chart default nils and clean nils from // When coalescing a subchart default, skip chart default nils and
// nested maps so they don't shadow globals or produce %!s(<nil>). // clean nils from nested maps so they don't shadow globals or
if !merge { // produce %!s(<nil>). Null values defined directly in the top-level
// chart's own values.yaml are preserved (see helm#32530).
if !merge && prefix != "" {
if val == nil { if val == nil {
continue continue
} }

@ -825,3 +825,36 @@ func TestCoalesceValuesSubchartNilCleanedWhenUserPartiallyOverrides(t *testing.T
_, ok = keyMapping["password"] _, ok = keyMapping["password"]
is.False(ok, "Expected keyMapping.password (nil from chart defaults) to be removed even when user partially overrides the map") is.False(ok, "Expected keyMapping.password (nil from chart defaults) to be removed even when user partially overrides the map")
} }
// TestCoalesceValuesTopLevelNullsPreserved tests that null values defined directly in
// the top-level chart's own values.yaml are preserved in the coalesced result.
// Regression test for issue #32530.
func TestCoalesceValuesTopLevelNullsPreserved(t *testing.T) {
is := assert.New(t)
req := require.New(t)
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "test"},
Values: map[string]any{
"stuff": map[string]any{
"foo": "bar",
"baz": nil,
},
"foobar": nil,
},
}
v, err := CoalesceValues(c, map[string]any{})
req.NoError(err)
// The top-level null key should be preserved.
is.Contains(v, "foobar", "Expected top-level null key foobar to be preserved")
is.Nil(v["foobar"], "Expected foobar to be nil")
// The nested null key should be preserved.
stuff, ok := v["stuff"].(map[string]any)
is.True(ok, "stuff should be a map")
is.Contains(stuff, "baz", "Expected nested null key stuff.baz to be preserved")
is.Nil(stuff["baz"], "Expected stuff.baz to be nil")
is.Equal("bar", stuff["foo"], "Expected stuff.foo to be preserved")
}

Loading…
Cancel
Save