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

@ -825,3 +825,36 @@ func TestCoalesceValuesSubchartNilCleanedWhenUserPartiallyOverrides(t *testing.T
_, ok = keyMapping["password"]
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