From e35f6bb3124615d724858690926de9a85804e5b2 Mon Sep 17 00:00:00 2001 From: waterWang Date: Tue, 11 Aug 2026 01:35:31 +0800 Subject: [PATCH] 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 --- pkg/chart/common/util/coalesce.go | 16 ++++++++----- pkg/chart/common/util/coalesce_test.go | 33 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/pkg/chart/common/util/coalesce.go b/pkg/chart/common/util/coalesce.go index c1f73b16d..a96a54e4f 100644 --- a/pkg/chart/common/util/coalesce.go +++ b/pkg/chart/common/util/coalesce.go @@ -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(). - 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(). 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 } diff --git a/pkg/chart/common/util/coalesce_test.go b/pkg/chart/common/util/coalesce_test.go index e058bd996..f9a50a26a 100644 --- a/pkg/chart/common/util/coalesce_test.go +++ b/pkg/chart/common/util/coalesce_test.go @@ -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") +}