fix: separate list traversal from deduplication in SSA dedup

Rename nameKeyedFields -> dedupFields and remove container lists
(containers, initContainers, ephemeralContainers). Container lists are
now always traversed to reach nested env/volumes, but are never
deduplicated themselves — duplicate container names are invalid and
must not be silently dropped with last-value-wins semantics.

Add a test case asserting duplicate container names are preserved.

Addresses Copilot review comment on PR #32061.

Signed-off-by: Kunal Jain <qlapon@gmail.com>
pull/32061/head
Kunal Jain 6 months ago
parent dc0197c98d
commit 1c9f24adf0

@ -1299,24 +1299,25 @@ func copyRequestStreamToWriter(request *rest.Request, podName, containerName str
return nil return nil
} }
// nameKeyedFields is the set of Kubernetes list fields that are keyed by the // dedupFields is the set of Kubernetes list fields that should be deduplicated
// "name" field under server-side apply merge semantics. Only these fields are // by "name" under server-side apply merge semantics. Container lists
// candidates for deduplication; other lists that incidentally contain a "name" // (containers, initContainers, ephemeralContainers) are intentionally excluded:
// field (e.g. volumeMounts, which is keyed by mountPath) must not be touched. // duplicate container names are invalid and must not be silently dropped.
var nameKeyedFields = map[string]bool{ // Other lists that happen to carry a "name" field (e.g. volumeMounts, keyed by
"containers": true, // mountPath) are also excluded. All list fields are still traversed so that
"initContainers": true, // nested dedupFields (e.g. env inside a container) are processed.
"ephemeralContainers": true, var dedupFields = map[string]bool{
"env": true, "env": true,
"volumes": true, "volumes": true,
"imagePullSecrets": true, "imagePullSecrets": true,
} }
// deduplicateListMaps walks an unstructured Kubernetes object and deduplicates // deduplicateListMaps walks an unstructured Kubernetes object and deduplicates
// list fields that are keyed by "name" under server-side apply merge semantics // list fields in dedupFields under server-side apply merge semantics. When
// (see nameKeyedFields). When duplicates are found the last occurrence wins, // duplicates are found the last occurrence wins, matching Kubernetes
// matching Kubernetes client-side-apply semantics. Returns true if any // client-side-apply semantics. All lists are traversed (to reach nested fields)
// deduplication occurred. // but only lists whose key is in dedupFields are themselves deduplicated.
// Returns true if any deduplication occurred.
func deduplicateListMaps(obj map[string]interface{}) bool { func deduplicateListMaps(obj map[string]interface{}) bool {
deduped := false deduped := false
for key, val := range obj { for key, val := range obj {
@ -1326,10 +1327,7 @@ func deduplicateListMaps(obj map[string]interface{}) bool {
deduped = true deduped = true
} }
case []interface{}: case []interface{}:
if !nameKeyedFields[key] { newList, changed := processListItems(v, dedupFields[key])
continue
}
newList, changed := processNamedList(v)
if changed { if changed {
obj[key] = newList obj[key] = newList
deduped = true deduped = true
@ -1339,14 +1337,15 @@ func deduplicateListMaps(obj map[string]interface{}) bool {
return deduped return deduped
} }
// processNamedList recurses into list items and deduplicates the list if every // processListItems recurses into list items and, when dedup is true,
// item is a map with a "name" key. The last occurrence of each name is kept and // deduplicates the list if every item is a map with a non-empty string "name".
// the relative order of surviving items is preserved. Returns the (possibly // The last occurrence of each name is kept and relative order of surviving
// modified) list and whether any change was made. // items is preserved. Returns the (possibly modified) list and whether any
func processNamedList(list []interface{}) ([]interface{}, bool) { // change was made.
func processListItems(list []interface{}, dedup bool) ([]interface{}, bool) {
changed := false changed := false
// Recurse into each map item first. // Always recurse into map items to reach nested dedupFields.
for i, item := range list { for i, item := range list {
if m, ok := item.(map[string]interface{}); ok { if m, ok := item.(map[string]interface{}); ok {
if deduplicateListMaps(m) { if deduplicateListMaps(m) {
@ -1356,7 +1355,7 @@ func processNamedList(list []interface{}) ([]interface{}, bool) {
} }
} }
if len(list) < 2 { if !dedup || len(list) < 2 {
return list, changed return list, changed
} }

@ -2053,6 +2053,25 @@ func TestDeduplicateListMaps(t *testing.T) {
}, },
changed: false, changed: false,
}, },
{
// Duplicate container names are an invalid manifest; they must not be
// silently dropped. We traverse the list to reach nested env vars but
// do not deduplicate the containers list itself.
name: "duplicate container names not deduplicated",
input: map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{"name": "app", "image": "nginx:1"},
map[string]interface{}{"name": "app", "image": "nginx:2"},
},
},
expected: map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{"name": "app", "image": "nginx:1"},
map[string]interface{}{"name": "app", "image": "nginx:2"},
},
},
changed: false,
},
} }
for _, tt := range tests { for _, tt := range tests {

Loading…
Cancel
Save