diff --git a/pkg/chart/common/util/coalesce.go b/pkg/chart/common/util/coalesce.go index 07794a04a..6c72b3d56 100644 --- a/pkg/chart/common/util/coalesce.go +++ b/pkg/chart/common/util/coalesce.go @@ -302,6 +302,15 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, pref if dst == nil { return src } + // Track original non-nil src keys before modifying src + // This lets us distinguish between user nullifying a chart default vs + // user setting nil for a key not in chart defaults. + srcOriginalNonNil := make(map[string]bool) + for key, val := range src { + if val != nil { + srcOriginalNonNil[key] = true + } + } for key, val := range dst { if val == nil { src[key] = nil @@ -311,9 +320,13 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, pref // values. for key, val := range src { fullkey := concatPrefix(prefix, key) - if dv, ok := dst[key]; ok && !merge && dv == nil { + if dv, ok := dst[key]; ok && !merge && dv == nil && srcOriginalNonNil[key] { + // When coalescing (not merging), if dst has nil and src has a non-nil + // value, the user is nullifying a chart default - remove the key. + // But if src also has nil (or key not in src), preserve the nil delete(dst, key) } else if !ok { + // key not in user values, preserve src value (including nil) dst[key] = val } else if istable(val) { if istable(dv) { diff --git a/pkg/chart/common/util/coalesce_test.go b/pkg/chart/common/util/coalesce_test.go index 871bfa8da..4eaf4be2b 100644 --- a/pkg/chart/common/util/coalesce_test.go +++ b/pkg/chart/common/util/coalesce_test.go @@ -731,3 +731,37 @@ func TestConcatPrefix(t *testing.T) { assert.Equal(t, "b", concatPrefix("", "b")) assert.Equal(t, "a.b", concatPrefix("a", "b")) } + +// TestCoalesceValuesEmptyMapWithNils tests the full CoalesceValues scenario +// from issue #31643 where chart has data: {} and user provides data: {foo: bar, baz: ~} +func TestCoalesceValuesEmptyMapWithNils(t *testing.T) { + is := assert.New(t) + + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + Values: map[string]any{ + "data": map[string]any{}, // empty map in chart defaults + }, + } + + vals := map[string]any{ + "data": map[string]any{ + "foo": "bar", + "baz": nil, // explicit nil from user + }, + } + + v, err := CoalesceValues(c, vals) + is.NoError(err) + + data, ok := v["data"].(map[string]any) + is.True(ok, "data is not a map") + + // "foo" should be preserved + is.Equal("bar", data["foo"]) + + // "baz" should be preserved with nil value since it wasn't in chart defaults + _, ok = data["baz"] + is.True(ok, "Expected data.baz key to be present but it was removed") + is.Nil(data["baz"], "Expected data.baz key to be nil but it is not") +}