diff --git a/pkg/chartutil/coalesce.go b/pkg/chartutil/coalesce.go index 68bc9aa6f..a1cfb8ef0 100644 --- a/pkg/chartutil/coalesce.go +++ b/pkg/chartutil/coalesce.go @@ -153,6 +153,25 @@ func copyMap(src map[string]interface{}) map[string]interface{} { return m } +// stringCollectionsDeepCopy makes deep copy for string maps and lists. +// For other types performs shallow copy. +func stringCollectionsDeepCopy(src any) any { + switch t := src.(type) { + case map[string]any: + r := make(map[string]interface{}, len(t)) + for k, v := range t { + r[k] = stringCollectionsDeepCopy(v) + } + return r + case []string: + r := make([]string, len(t)) + copy(r, t) + return r + default: + return t + } +} + // coalesceValues builds up a values map for a particular chart. // // Values in v will override the values in the chart. @@ -212,7 +231,7 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, pref if dv, ok := dst[key]; ok && dv == nil { delete(dst, key) } else if !ok { - dst[key] = val + dst[key] = stringCollectionsDeepCopy(val) } else if istable(val) { if istable(dv) { coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey) diff --git a/pkg/chartutil/coalesce_test.go b/pkg/chartutil/coalesce_test.go index 3fe93f5ff..b1d634a5a 100644 --- a/pkg/chartutil/coalesce_test.go +++ b/pkg/chartutil/coalesce_test.go @@ -17,6 +17,7 @@ limitations under the License. package chartutil import ( + "bytes" "encoding/json" "fmt" "testing" @@ -63,6 +64,16 @@ func withDeps(c *chart.Chart, deps ...*chart.Chart) *chart.Chart { return c } +func assertIsEqualToJSON(t *testing.T, name string, val any, expectedJson []byte) { + j, err := json.Marshal(val) + if err != nil { + t.Fatalf("JSON marshal failed: %v", err) + } + if !bytes.Equal(j, expectedJson) { + t.Errorf("%s contents changed, got: %v", name, string(j)) + } +} + func TestCoalesceValues(t *testing.T) { is := assert.New(t) @@ -233,12 +244,21 @@ func TestCoalesceTables(t *testing.T) { "state": "MA", "street": "234 Spouter Inn Ct.", "country": "US", + "weather": map[string]interface{}{ + "clouds": true, + }, }, "details": "empty", "boat": map[string]interface{}{ "mast": true, }, - "hole": "black", + "hole": "black", + "grass": []string{"green", "yellow"}, + } + + srcJson, err := json.Marshal(src) + if err != nil { + t.Fatalf("JSON marshal failed: %v", err) } // What we expect is that anything in dst overrides anything in src, but that @@ -287,6 +307,11 @@ func TestCoalesceTables(t *testing.T) { t.Error("The hole still exists.") } + // change in dst shouldn't make a change in src + dst["grass"].([]string)[1] = "blue" + dst["address"].(map[string]interface{})["weather"].(map[string]interface{})["clouds"] = "no" + assertIsEqualToJSON(t, "src", src, srcJson) + dst2 := map[string]interface{}{ "name": "Ishmael", "address": map[string]interface{}{ @@ -407,3 +432,34 @@ func TestConcatPrefix(t *testing.T) { assert.Equal(t, "b", concatPrefix("", "b")) assert.Equal(t, "a.b", concatPrefix("a", "b")) } + +func TestTwoFollowingCoalesceTables(t *testing.T) { + update := map[string]interface{}{ + "mango": map[string]interface{}{ + "fruit": "cool", + }, + } + main := map[string]interface{}{ + "mango": map[string]interface{}{ + "fruit": "super", + "color": "orange", + "taste": "ok", + }, + } + + updateJson, err := json.Marshal(update) + if err != nil { + t.Fatalf("JSON marshal failed: %v", err) + } + mainJson, err := json.Marshal(main) + if err != nil { + t.Fatalf("JSON marshal failed: %v", err) + } + + vals := CoalesceTables(make(map[string]interface{}, len(update)), update) + vals = CoalesceTables(vals, main) + _ = vals + + assertIsEqualToJSON(t, "update", update, updateJson) + assertIsEqualToJSON(t, "main", main, mainJson) +}