# Label every Pod with its team using a Mutating Admission Policy

> Copy each namespace's team label onto its Pods with a CEL Mutating Admission Policy, stable in Kubernetes 1.36, instead of running a mutating webhook.

Level: Advanced · Topic: Operations · About 15 minutes
Try it in the browser: https://k8wiz.com/playground/label-pods-with-a-mutating-admission-policy

The playground is a simulated Kubernetes cluster that runs in the browser. kubectl output and errors follow the real ones.

## The situation

Finance reports cost per team from a `team` label on every Pod. The namespaces carry it (shop is team=payments, search is team=discovery), but Pods don't, and teams forget to add it. The tools namespace has no team at all.

Mutating Admission Policies are stable since Kubernetes 1.36: the API server runs CEL mutations itself, so there's no webhook server to build and keep running. Write one that copies the namespace's team label onto every new Pod.

## Objectives

1. New Pods in shop and search get their namespace's team label
2. Pods in tools, which has no team, are still created, unlabelled
3. Preview the change with a server-side dry run (`kubectl run test -n shop --image=nginx:1.29 --dry-run=server -o yaml`)
4. Every running Pod in shop and search carries the label

## Hints

1. In the expression, `namespaceObject` is the namespace of the Pod being created. `Object{metadata: Object.metadata{labels: {"team": namespaceObject.metadata.labels["team"]}}}` builds the change, and ApplyConfiguration merges it into the Pod. team-label.yaml in the Editor has the rest.
2. tools has no team label, so that expression fails there, and with `failurePolicy: Fail` the Pod is rejected. Add a `matchConditions` entry with the expression `namespaceObject.metadata.?labels.?team.hasValue()`, or limit the binding with `matchResources.namespaceSelector`.
3. `kubectl run test -n shop --image=nginx:1.29 --dry-run=server -o yaml` shows what the API server would store, mutations included, without creating anything.
4. A policy only changes objects as they're admitted. `kubectl rollout restart deployment -n shop` and `-n search` replace the Pods that were already running.

## Solution

```bash
cat <<EOF | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
  name: team-label
spec:
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      resources: ["pods"]
      operations: ["CREATE"]
  matchConditions:
  - name: namespace-has-a-team
    expression: namespaceObject.metadata.?labels.?team.hasValue()
  failurePolicy: Fail
  mutations:
  - patchType: ApplyConfiguration
    applyConfiguration:
      expression: >
        Object{
          metadata: Object.metadata{
            labels: {"team": namespaceObject.metadata.labels["team"]}
          }
        }
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
  name: team-label
spec:
  policyName: team-label
EOF
```
```bash
kubectl run test -n shop --image=nginx:1.29 --dry-run=server -o yaml | grep -A3 labels
```
```bash
kubectl rollout restart deployment -n shop
```
```bash
kubectl rollout restart deployment -n search
```
```bash
sleep 20
```
```bash
kubectl get pods -A -l 'app in (web,api,toolbox)' -L team
```

## What it teaches

- A Mutating Admission Policy changes objects inside the API server with CEL: no webhook to deploy, scale or secure. The binding decides where it applies.
- `ApplyConfiguration` merges a partial object, the way server-side apply does; `JSONPatch` gives exact operations. `object`, `oldObject` and `namespaceObject` are the expression's inputs.
- With `failurePolicy: Fail`, an expression that errors blocks the request. Guard it with `matchConditions` or optional fields such as `.?team`.
- `--dry-run=server` runs admission without storing anything: the quickest way to see what a policy does.
