Strategic Merge Patch in Kyverno#
Kyverno's patchStrategicMerge rule type applies overlay patterns to Kubernetes resources using kustomize's patchstrategicmerge.Filter. Before the patch reaches kustomize, Kyverno runs a preprocessing pass to resolve anchors, evaluate conditions, and strip control constructs from the pattern β producing a clean YAML overlay.
The two key files are:
strategicMergePatch.goβ entry point:ProcessStrategicMergePatchmarshals the overlay to JSON and callsstrategicMergePatch, which invokes preprocessing then delegates to kustomize.strategicPreprocessing.goβ the preprocessing engine containing all anchor and condition logic.
The call sequence is:
ProcessStrategicMergePatch
ββ strategicMergePatch
ββ preProcessStrategicMergePatch β PreProcessPattern
β ββ preProcessRecursive (walk maps / lists)
β ββ deleteConditionElements
ββ patchstrategicmerge.Filter (kustomize)
Preprocessing Pipeline#
PreProcessPattern is the public entry point for preprocessing. It calls preProcessRecursive on the pattern tree, then deleteConditionElements to remove any remaining condition-only fields.
preProcessRecursive dispatches on node type :
- MappingNode β
walkMap: processesaddIfNotPresentanchors, validates conditions, then recurses into non-anchor fields. - SequenceNode β
walkList: if the list contains maps, delegates toprocessListOfMaps.
Anchor types handled#
| Anchor | Syntax | Behavior |
|---|---|---|
| Conditional | (key) | Field must match resource; treated as a condition gate |
| Global condition | <(key)> | If any resource element matches, the rule passes globally |
| AddIfNotPresent | +(key) | Field is added only when absent from the resource; key is renamed in-place when the field doesn't exist, or removed when it does |
handleAddIfNotPresentAnchor removes the anchor wrapping from the key if the field is absent, or clears the field entirely if it already exists in the resource.
validateConditions checks global anchors first, then conditional anchors. A failed global condition wraps its error as GlobalConditionError; a failed regular condition wraps it as ConditionError. The caller in processListOfMaps catches both to skip unmatched list elements without failing the whole rule.
Anchor Handling in Lists#
List processing is the most complex part of preprocessing because kustomize uses the name field as a merge key for Kubernetes list types (containers, volumes, etc.).
processListOfMaps iterates over pattern elements. For each element that contains anchors, it copies the element and runs preProcessRecursive against every resource element. When conditions match, handlePatternName fires: it creates a copy of the matched pattern element, strips its anchors via deleteAnchors, injects the resource element's name field, and appends the new node to the pattern list. This ensures kustomize can locate the correct target element by name.
deleteAnchors and deleteAnchorsInList#
After processListOfMaps appends concretized elements, deleteConditionElements (called at the end of PreProcessPattern) drives anchor cleanup. It calls deleteAnchors which dispatches to:
deleteAnchorsInMapfor mapping nodesdeleteAnchorsInListfor sequence nodes
deleteAnchorsInList iterates elements and removes those that consist entirely of anchors. The return value is true (delete this list too) only if the list went from non-empty to empty during cleanup.
deleteListElement performs the actual removal by slicing YNode().Content in-place.
Known Bugs and Fixes#
Index-shift bug in deleteAnchorsInList (open, #16862)#
Issue #16862 (filed 2026-07-31, affects β₯1.16.4) documents a critical bug where deleteAnchorsInList silently deletes containers when a patchStrategicMerge policy targets two or more list elements with key anchors (e.g., two (name) entries).
Root cause: deleteAnchorsInList iterates over a snapshot of elements obtained at loop start. deleteListElement removes elements by slicing YNode().Content in-place, so every deletion shifts all subsequent indices left by one. The loop's stale indices then target the wrong elements on the next iteration.
Observed failure modes :
- Raw anchor templates (e.g.,
{"(name)": "b"}) survive β they should have been cleaned up. - Concretized mutation entries (appended by
handlePatternName) are incorrectly deleted. - kustomize's downstream merge collapses the container list, silently dropping containers at admission time.
A fix requires iterating deleteAnchorsInList in reverse order (highest index first) so that deletions do not shift the indices of remaining elements.
Container list ordering bug (fixed, PR #13397)#
PR #13397 fixed a separate ordering regression where foreach mutations with order: Ascending caused mutated containers to be prepended rather than appended, breaking the original container order. Closed issues #10727 and #12552.
The fix operates at two levels:
strategicMergePatch.go:reorderContainersandfixContainerListOrderparse base and patched JSON and reorderspec.containers/spec.initContainersin the patched output to match the base resource's name order.pkg/engine/utils/foreach.go:ReversePatchedListIfAscendingreverses the container slice in the patched resource whenforeach.Order == Ascending; called fromforEachMutator.mutateElementsinpkg/engine/handlers/mutation/common.go.
Key Source Files#
| File | Purpose |
|---|---|
pkg/engine/mutate/patch/strategicMergePatch.go | Entry point: ProcessStrategicMergePatch, kustomize integration, container reorder helpers |
pkg/engine/mutate/patch/strategicPreprocessing.go | Anchor/condition preprocessing: PreProcessPattern, deleteAnchorsInList, deleteListElement |
pkg/engine/mutate/patch/strategicMergePatch_test.go | Unit tests for SMP including container order preservation |
pkg/engine/utils/foreach.go | ReversePatchedListIfAscending for foreach order correction |
pkg/engine/handlers/mutation/common.go | forEachMutator.mutateElements β calls order correction after patching |
pkg/engine/anchor/ | Anchor type definitions and predicates: IsCondition, IsGlobal, IsAddIfNotPresent, ContainsCondition |