A rejection with no network hop in it¶
Artifact — a ValidatingAdmissionPolicy and its binding that refuse a specific field value, on the hand-started apiserver, with the CEL expression that does it and the message the user sees. This is the object the comparison exercise puts beside a webhook doing the identical job.
Rests on — the plugin metrics — a policy shows up in a different place from a plugin and a different place again from a webhook, and locating each is half of what makes the comparison meaningful. Module 3.2's last reading pair is the reading behind it.
Topology — none. forge, the running apiserver.
Do
- Write the policy. The rule: no container may run without a memory limit, and the message must name the container.
kubectl --context=admin apply -f - <<'YAML'
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: {name: require-memory-limit}
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: >-
object.spec.containers.all(c,
has(c.resources) && has(c.resources.limits) && has(c.resources.limits.memory))
message: "every container needs resources.limits.memory"
reason: Invalid
YAML
- Bind it — a policy with no binding does nothing at all, which is the first thing to be wrong about:
kubectl --context=admin apply -f - <<'YAML'
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: {name: require-memory-limit}
spec:
policyName: require-memory-limit
validationActions: [Deny]
matchResources:
namespaceSelector: {}
YAML
- Test both directions:
kubectl --context=admin run nolimit --image=busybox --restart=Never
kubectl --context=admin run withlimit --image=busybox --restart=Never \
--overrides='{"spec":{"containers":[{"name":"withlimit","image":"busybox","resources":{"limits":{"memory":"32Mi"}}}]}}'
-
Change
validationActionsto[Warn, Audit]and re-run the first command. Then set it back to[Deny]. -
Break the expression on purpose — reference a field that does not exist — and try to
applythe policy.
Observe
kubectl --context=admin get validatingadmissionpolicy require-memory-limit -o jsonpath='{.status}{"\n"}' | jq .
kubectl --context=admin get --raw /metrics | grep -E 'validating_admission_policy|admission_webhook' | head
jq -r 'select(.annotations["validation.policy.admission.k8s.io/validation_failure"]) | .annotations' /tmp/audit.log | tail -3
Expect — the first pod is refused with your message, prefixed by the policy name. The second is admitted. validationActions: [Warn, Audit] turns the same evaluation into a Warning: line on the client and an annotation in the audit log while the object is still created — the same policy, three different consequences, chosen at bind time rather than at write time. No webhook can do that, because a webhook's answer is the consequence.
Step 5 fails at apply, not at evaluation: the expression is type-checked against the matched resource's schema when the policy is written, and .status.typeChecking on the object holds the warnings. That is the mechanical difference the comparison exercise is built on — a broken CEL policy is caught when you write it, a broken webhook is caught when someone else's write fails.
The metrics grep is deliberately wide: expect series for policy evaluation and none at all for webhooks, because no webhook exists yet. Note the zero; it is the baseline.
Write down — the CEL expression, the exact refusal message, the three validationActions behaviours, and what .status.typeChecking said when you broke it. Keep the two manifests — the comparison reapplies them on a real cluster.
Teardown — delete the pods, the binding and the policy, in that order (deleting the policy first leaves a binding pointing at nothing, which is legal and silently does nothing — worth doing once to see). The apiserver and etcd stay up for one more exercise. The CA exercise is where they stop. No topology is up.