TT Lab
Get started
Learn Learning paths Courses

Policy as Code

Writing a Mutation Policy That Fills In Defaults

Continue in TT Lab

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

  1. 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.
  2. 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().
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.

Notes

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.