Writing a Mutation Policy That Fills In Defaults
Goal
Write a mutation policy that fills in missing values in three ways, and check for yourself that it does not touch values that are already specified. At the end, you build a comparison report of what changed and how.
Why it matters
A policy that only blocks pushes the problem onto users. If you reject every Pod without limits, the rule is followed, but each team raises the question "how much should I put in?" A mutation policy fills in the organization's defaults on their behalf, following the rule while blocking nobody. But there is a line that must not be crossed here — never overwriting a value the user specified. If a team wrote cpu: 500m and the policy quietly changes it, that team can no longer trust its own manifest, and from then on the policy becomes an object not of trust but of fear. That is why the add anchor +() is not syntax decoration but a contract. Note that this environment runs no admission webhook, so you evaluate mutation locally with the kyverno CLI and check the resulting manifest.
Steps
- In
/root/policy/mutate/add-team-label.yaml, create akind: ClusterPolicy. Statespec.backgroundexplicitly, and put+(team): unassignedunderspec.rules[0].mutate.patchStrategicMerge.metadata.labels. The rule name isadd-default-team, and the match isPodin thepol-labnamespace. - In
/root/policy/mutate/add-defaults.yaml, create a second policy and write a rule that fills thecpuandmemorydefaults in the container'sresources.limitswith the+(...)add anchor. And in/root/policy/mutate/out/anchors.md(at least 150 bytes), sum up the differences among the conditional anchor(), the add anchor+(), and=()orX(). - In the same
add-defaults.yaml, add one more rule that usesmutate.patchesJson6902. It takes the formop: add,path: /spec/containers/0/imagePullPolicy, andvalue: IfNotPresent. And in/root/policy/mutate/out/json6902-note.txt, write why this method is needed when handling arrays and indexes. - Add a
mutate.foreachrule toadd-defaults.yaml. Receive the container list withlist: "request.object.spec.containers", and inside it you must point to the current item with theelementvariable. - Apply the two policies to
/opt/lab/fixtures/policy/resources/bad-pod.yaml, and save the mutated Pod manifest to/root/policy/mutate/out/mutated.yaml. That file must have all three valuesmetadata.labels.team,spec.containers[0].resources.limits.memory, andspec.containers[0].imagePullPolicyfilled in. - Apply the same two policies to
/opt/lab/fixtures/policy/resources/good-pod.yamland save it to/root/policy/mutate/out/preserved.yaml. This Pod originally has theteam: platformlabel and acpu: 500mlimit, and they must stay as they are after the mutation. - Create
/root/policy/mutate/mutate-existing.yaml. Setspec.mutateExistingOnPolicyUpdate: true, and in the rule'smutate.targetswritekind: Podandnamespace: pol-labso that it modifies resources that are not the trigger. And in/root/policy/mutate/out/existing-note.txt, write that because this behavior runs in the background, extra RBAC permissions are needed. - Create
/root/policy/mutate/out/mutate-report.json. Put at least 3 entries in the top-levelmutationsarray, and each entry must have four keys:field,before,after, andrule. Do not include an entry wherebeforeandafterare the same. Theaftervalue of the entry whosefieldcontainsteammust be exactly equal to themetadata.labels.teamvalue of themutated.yamlyou created in step 5.
Notes
- You can pass several policy files at once:
kyverno apply <정책1> <정책2> --resource <매니페스트>(policy 1, policy 2, and the manifest). - You can receive the mutated manifest with an output option, or cut out only the Pod document part of the output and save it. Grading checks only whether that file is valid Pod YAML.
+()is "put it in if absent," and()is "apply only when the condition matches." The two do different things.- Common mistake 1: writing the
teamlabel plainly without an anchor. Thenplatformis overwritten in step 6 and it fails. - Common mistake 2: filling the step 8 report with fields whose values did not change. What is not a mutation must not go into the report.
Create a mutation policy that adds a label
In /root/policy/mutate/add-team-label.yaml, create a kind: ClusterPolicy. State spec.background explicitly, and put +(team): unassigned under spec.rules[0].mutate.patchStrategicMerge.metadata.labels. The rule name is add-default-team, and the match is Pod in the pol-lab namespace.
You write a mutation under mutate. For adding fields to a map, writing in the same shape as the object is the easiest to read. You will soon handle Pods that already have values, so use a notation that does not overwrite from the start.
Fill in resource defaults with the add anchor
In /root/policy/mutate/add-defaults.yaml, create a second policy and write a rule that fills the cpu and memory defaults in the container's resources.limits with the +(...) add anchor. And in /root/policy/mutate/out/anchors.md (at least 150 bytes), sum up the differences among the conditional anchor (), the add anchor +(), and =() or X().
There is a separate anchor that means "put it in if absent, leave it if present." You pass only if you write up the differences among the three kinds of anchors in a document.
Fix an array item with a JSON patch
In the same add-defaults.yaml, add one more rule that uses mutate.patchesJson6902. It takes the form op: add, path: /spec/containers/0/imagePullPolicy, and value: IfNotPresent. And in /root/policy/mutate/out/json6902-note.txt, write why this method is needed when handling arrays and indexes.
With Strategic Merge, it is hard to pinpoint "which container." There is a method that specifies the position with three things: op, path, and value.
Write a rule that loops over the container list
Add a mutate.foreach rule to add-defaults.yaml. Receive the container list with list: "request.object.spec.containers", and inside it you must point to the current item with the element variable.
When you do not know the number of containers, you cannot use an index. Check the key that receives the list and the variable name that points to the current item.
Run the policies and save the mutation result
Apply the two policies to /opt/lab/fixtures/policy/resources/bad-pod.yaml, and save the mutated Pod manifest to /root/policy/mutate/out/mutated.yaml. That file must have all three values metadata.labels.team, spec.containers[0].resources.limits.memory, and spec.containers[0].imagePullPolicy filled in.
You can pass the two policy files at once. What you save is not the execution summary but the mutated Pod manifest.
Check that existing values are preserved
Apply the same two policies to /opt/lab/fixtures/policy/resources/good-pod.yaml and save it to /root/policy/mutate/out/preserved.yaml. This Pod originally has the team: platform label and a cpu: 500m limit, and they must stay as they are after the mutation.
You run the same policy on a Pod whose values are already filled in. If the original values changed, you either did not use an anchor or used it wrongly.
Write a policy that modifies already existing resources
Create /root/policy/mutate/mutate-existing.yaml. Set spec.mutateExistingOnPolicyUpdate: true, and in the rule's mutate.targets write kind: Pod and namespace: pol-lab so that it modifies resources that are not the trigger. And in /root/policy/mutate/out/existing-note.txt, write that because this behavior runs in the background, extra RBAC permissions are needed.
To modify another resource rather than the trigger, you must write the target separately. There is also a switch that decides whether existing resources are reworked when you change the policy.
Build a before-and-after mutation comparison report
Create /root/policy/mutate/out/mutate-report.json. Put at least 3 entries in the top-level mutations array, and each entry must have four keys: field, before, after, and rule. Do not include an entry where before and after are the same. The after value of the entry whose field contains team must be exactly equal to the metadata.labels.team value of the mutated.yaml you created in step 5.
For each field, record the before and after values and which rule did it. An entry whose value did not change is not a mutation, so it must not be included.