JSON Patch Mutation (patchesJson6902)#
Kyverno's patchesJson6902 mutation type implements RFC 6902 JSON Patch, allowing fine-grained add, remove, replace, move, copy, and test operations on Kubernetes resources. It works alongside patchStrategicMerge as one of two supported mutation strategies.
Execution Pipeline#
The pipeline goes: variable substitution β patch string normalization β JSON patch application.
1. Variable Substitution#
Variable resolution happens before the patcher is created.
- Regular rules (
Mutate):variables.SubstituteAllInRule()substitutes the entire rule β including bothpathandvaluefields of every patch operation β before thePatchesJSON6902string is handed to the patcher. forEachrules (ForEach):substituteAllInForEach()marshalspatchStrategicMergeandpatchesJson6902into amap[string]interface{}and callsvariables.SubstituteAll()on it, enabling per-iteration element variables in both patch paths and values.
Variables in the path field of patches (e.g., /spec/containers/{{ elementIndex }}/image) were previously forbidden but are now fully supported following PR #13898, which removed the validation restriction and extended substitution to cover the path field.
2. Patch String Normalization#
convertPatchesToJSON() accepts either YAML or JSON patch strings:
- If the string starts with
[, it is treated as a JSON array and returned as-is. - Otherwise, it is parsed through
yaml.YAMLToJSON()and converted to JSON.
This allows policy authors to write patches in either format.
3. Patch Application#
ProcessPatchJSON6902() applies the decoded patch using the evanphx/json-patch/v5 library with three non-default options set via applyPatchesWithOptions():
| Option | Effect |
|---|---|
SupportNegativeIndices: true | Allows e.g. -1 to reference the last array element |
AllowMissingPathOnRemove: true | Silently skips remove ops on non-existent paths |
EnsurePathExistsOnAdd: true | Auto-creates missing intermediate paths for add ops |
4. Patcher Interface#
The Patcher interface (Patch(logr.Logger, resource) (resource, error)) is implemented by patchesJSON6902Handler. NewPatcher() selects patchStrategicMerge over patchesJson6902 when both are present; only one is active per rule.
5. Patch Joining#
When multiple patch byte slices must be combined (e.g., from accumulated operations), JoinPatches() strips outer brackets from individual arrays and concatenates them into a single valid JSON array.
Known Fixes and Edge Cases#
Nil-Safety in forEach (PR #15887 / #15888)#
A bare type assertion fe["patchesJson6902"].(string) would panic if a forEach variable resolved to nil at runtime, sending the background controller into a CrashLoopBackOff. The fix in PR #15887 (backported to releases 1.16 and 1.17 via PR #15888) changed this to a safe assertion jsonPatch, _ := fe["patchesJson6902"].(string) , which gracefully returns an empty string when the variable is nil.
Stale Context Between Sequential Mutation Policies (PR #15452)#
When multiple mutation policies ran sequentially during admission, request.object in the JSON context was not refreshed after each policy applied its patches. This caused forEach rules using elementIndex to compute incorrect array indices against a stale pre-mutation snapshot. PR #15452 fixed this by calling JSONContext().AddResource() after WithNewResource() in the webhook mutation handler, keeping the context in sync. Note: WithNewResource() does not update the JSON context automatically β callers must sync explicitly.
Key Source Files#
| File | Role |
|---|---|
pkg/engine/mutate/mutation.go | Mutate(), ForEach(), substituteAllInForEach(), NewPatcher() |
pkg/engine/mutate/patch/patchers.go | Patcher interface, patchesJSON6902Handler |
pkg/engine/mutate/patch/patchJSON6902.go | ProcessPatchJSON6902(), applyPatchesWithOptions() |
pkg/engine/mutate/patch/patchesUtils.go | convertPatchesToJSON() (YAMLβJSON normalization) |
pkg/utils/json/utils.go | JoinPatches() (merge multiple patch arrays) |
pkg/engine/handlers/mutation/mutate_resource.go | High-level handler: routes to mutate.Mutate() (line 86) or forEachMutator.mutateForEach() (line 84) |