mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-18 19:01:50 +02:00
Add MergeMapsWithArrayMerge and IsCLIOverride flag - WIP fixing array merging issue
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
This commit is contained in:
co-authored by
yxxhero
parent
66751a845b
commit
1802fd4888
+31
-2
@@ -243,6 +243,35 @@ func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Arrays are replaced, not merged (documented behavior)
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MergeMapsWithArrayMerge merges two maps, merging arrays element-by-element.
|
||||
// This is used specifically for CLI overrides (--state-values-set) where sparse arrays
|
||||
// with specific indices set should merge with base arrays element-by-element.
|
||||
// For general helmfile composition, use MergeMaps which replaces arrays.
|
||||
func MergeMapsWithArrayMerge(a, b map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(a))
|
||||
// fill the out map with the first map
|
||||
for k, v := range a {
|
||||
out[k] = v
|
||||
}
|
||||
for k, v := range b {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if v, ok := v.(map[string]interface{}); ok {
|
||||
if bv, ok := out[k]; ok {
|
||||
if bv, ok := bv.(map[string]interface{}); ok {
|
||||
// if b and out map has a map value, merge it too
|
||||
out[k] = MergeMapsWithArrayMerge(bv, v)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle array merging element-by-element
|
||||
vSlice := toInterfaceSlice(v)
|
||||
if vSlice != nil {
|
||||
@@ -292,11 +321,11 @@ func mergeSlices(base, override []any) []any {
|
||||
continue
|
||||
}
|
||||
|
||||
// If both are maps, merge them recursively
|
||||
// If both are maps, merge them recursively with array merging
|
||||
if overrideMap, ok := overrideVal.(map[string]any); ok {
|
||||
if i < len(base) {
|
||||
if baseMap, ok := base[i].(map[string]any); ok {
|
||||
result[i] = MergeMaps(baseMap, overrideMap)
|
||||
result[i] = MergeMapsWithArrayMerge(baseMap, overrideMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +474,9 @@ func TestMapUtil_Issue2281_EmptyMapScenario(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestMapUtil_Issue2281_MergeArrays tests that MergeMaps should merge arrays element-by-element
|
||||
// TestMapUtil_Issue2281_MergeArrays tests that MergeMapsWithArrayMerge should merge arrays element-by-element
|
||||
// This is used for CLI overrides (--state-values-set) where sparse arrays should merge with base arrays.
|
||||
// For general helmfile composition, MergeMaps replaces arrays entirely (documented behavior).
|
||||
func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
t.Run("merging arrays should preserve elements from base that aren't in override", func(t *testing.T) {
|
||||
// Base values from helmfile
|
||||
@@ -491,7 +493,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := MergeMaps(base, override)
|
||||
result := MergeMapsWithArrayMerge(base, override)
|
||||
|
||||
// Expected: array should be ["cmdlinething1", "thing2"]
|
||||
// array[0] is overridden, array[1] is preserved from base
|
||||
@@ -532,7 +534,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := MergeMaps(base, override)
|
||||
result := MergeMapsWithArrayMerge(base, override)
|
||||
|
||||
// Expected: complexArray[0] should be unchanged, complexArray[1] should have merged fields
|
||||
resultArray := result["top"].(map[string]any)["complexArray"].([]any)
|
||||
@@ -558,7 +560,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("complete issue #2281 scenario with MergeMaps", func(t *testing.T) {
|
||||
t.Run("complete issue #2281 scenario with MergeMapsWithArrayMerge", func(t *testing.T) {
|
||||
// Base values from helmfile
|
||||
base := map[string]interface{}{
|
||||
"top": map[string]any{
|
||||
@@ -591,7 +593,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
result := MergeMaps(base, override)
|
||||
result := MergeMapsWithArrayMerge(base, override)
|
||||
|
||||
// Check array
|
||||
resultArray := result["top"].(map[string]any)["array"].([]any)
|
||||
@@ -617,3 +619,90 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMapUtil_MergeMaps_ArrayReplacement tests that MergeMaps replaces arrays
|
||||
// This is the documented behavior for helmfile composition and layer merging
|
||||
func TestMapUtil_MergeMaps_ArrayReplacement(t *testing.T) {
|
||||
t.Run("arrays should be replaced not merged", func(t *testing.T) {
|
||||
base := map[string]interface{}{
|
||||
"list": []any{
|
||||
map[string]any{"name": "dummy", "values": []any{1, 2}},
|
||||
},
|
||||
}
|
||||
|
||||
override := map[string]interface{}{
|
||||
"list": []any{
|
||||
map[string]any{"name": "a"},
|
||||
},
|
||||
}
|
||||
|
||||
result := MergeMaps(base, override)
|
||||
|
||||
// Expected: list should be completely replaced
|
||||
resultList := result["list"].([]any)
|
||||
if len(resultList) != 1 {
|
||||
t.Fatalf("Expected list length 1, got %d", len(resultList))
|
||||
}
|
||||
|
||||
elem := resultList[0].(map[string]any)
|
||||
if elem["name"] != "a" {
|
||||
t.Errorf("Expected name 'a', got %v", elem["name"])
|
||||
}
|
||||
// values field should not exist since it was not in override
|
||||
if _, exists := elem["values"]; exists {
|
||||
t.Errorf("values field should not exist in result, but got: %v", elem["values"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty array should replace non-empty array", func(t *testing.T) {
|
||||
base := map[string]interface{}{
|
||||
"list": []any{
|
||||
map[string]any{"name": "dummy", "values": []any{1, 2}},
|
||||
},
|
||||
}
|
||||
|
||||
override := map[string]interface{}{
|
||||
"list": []any{},
|
||||
}
|
||||
|
||||
result := MergeMaps(base, override)
|
||||
|
||||
// Expected: list should be empty
|
||||
resultList := result["list"].([]any)
|
||||
if len(resultList) != 0 {
|
||||
t.Errorf("Expected empty list, got length %d: %+v", len(resultList), resultList)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nested empty array should replace non-empty nested array", func(t *testing.T) {
|
||||
base := map[string]interface{}{
|
||||
"list": []any{
|
||||
map[string]any{"name": "dummy", "values": []any{1, 2}},
|
||||
},
|
||||
}
|
||||
|
||||
override := map[string]interface{}{
|
||||
"list": []any{
|
||||
map[string]any{"name": "a", "values": []any{}},
|
||||
},
|
||||
}
|
||||
|
||||
result := MergeMaps(base, override)
|
||||
|
||||
// Expected: list should be replaced with one element that has empty values
|
||||
resultList := result["list"].([]any)
|
||||
if len(resultList) != 1 {
|
||||
t.Fatalf("Expected list length 1, got %d", len(resultList))
|
||||
}
|
||||
|
||||
elem := resultList[0].(map[string]any)
|
||||
if elem["name"] != "a" {
|
||||
t.Errorf("Expected name 'a', got %v", elem["name"])
|
||||
}
|
||||
|
||||
values := elem["values"].([]any)
|
||||
if len(values) != 0 {
|
||||
t.Errorf("Expected empty values array, got length %d: %+v", len(values), values)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user