"""CKAD track. Starter bank - same format as the CKA banks, extend it the same way. CKAD is a build-things exam: less cluster surgery, more "produce the right object, fast". The clock is the adversary, so these are mostly single-object tasks graded exactingly. """ from tracks.cka.common import C, NS, ns_pick, name_pick TRACK = { "id": "ckad", "name": "Certified Kubernetes Application Developer", "minutes": 120, "pass_pct": 66, "domains": { "DESIGN": "Application Design and Build (20%)", "DEPLOY": "Application Deployment (20%)", "OBS": "Application Observability and Maintenance (15%)", "ENV": "Application Environment, Configuration and Security (25%)", "NET": "Services and Networking (20%)", }, "quota": {"DESIGN": 3, "DEPLOY": 3, "OBS": 2, "ENV": 2, "NET": 2}, "quota_hard": {"DESIGN": 3, "DEPLOY": 3, "OBS": 2, "ENV": 2, "NET": 2}, } QUESTIONS = [ # ---------------------------------------------------------------- DESIGN ---- { "id": 1, "domain": "DESIGN", "title": "Adapter sidecar", "params": lambda r: {"ns": ns_pick(r, "app"), "pod": name_pick(r, "adapter")}, "task": """ Create Pod `%{pod}` in Namespace `%{ns}` with two containers sharing an `emptyDir` named `spool` mounted at `/spool` in both: - `writer` (busybox:1.36): every 2 seconds appends a line `metric 1` to `/spool/raw.txt` - `adapter` (busybox:1.36): runs `tail -f /spool/raw.txt` `kubectl logs %{pod} -c adapter` must show the writer's lines. """, "setup": NS + r"""mkns %{ns}; $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null""", "checks": [ C("pod Running with containers writer and adapter", 2, r""" p="$K -n %{ns} get pod %{pod}" eq "$($p -o jsonpath='{.status.phase}' 2>/dev/null)" Running && $p -o jsonpath='{.spec.containers[*].name}' | grep -qw writer && $p -o jsonpath='{.spec.containers[*].name}' | grep -qw adapter"""), C("emptyDir 'spool' mounted at /spool in both containers", 2, r""" p="$K -n %{ns} get pod %{pod}" [ -n "$($p -o jsonpath='{.spec.volumes[?(@.name=="spool")].emptyDir}')" ] \ && eq "$($p -o jsonpath='{.spec.containers[?(@.name=="writer")].volumeMounts[?(@.name=="spool")].mountPath}')" /spool \ && eq "$($p -o jsonpath='{.spec.containers[?(@.name=="adapter")].volumeMounts[?(@.name=="spool")].mountPath}')" /spool"""), C("adapter logs show the writer's output", 2, r"""sleep 3; $K -n %{ns} logs %{pod} -c adapter --tail=3 2>/dev/null | grep -q 'metric 1'"""), ], "solution": """ cat <> /spool/raw.txt; sleep 2; done"] volumeMounts: [{name: spool, mountPath: /spool}] - name: adapter image: busybox:1.36 command: [sh, -c, "tail -f /spool/raw.txt"] volumeMounts: [{name: spool, mountPath: /spool}] EOF k -n %{ns} logs %{pod} -c adapter """, }, { "id": 2, "domain": "DESIGN", "title": "Init container waits for a dependency", "params": lambda r: {"ns": ns_pick(r, "boot"), "pod": name_pick(r, "consumer")}, "task": """ Namespace `%{ns}` has a Service `backend` with no endpoints yet. 1. Create Pod `%{pod}` with an init container `wait-for-backend` (busybox:1.36) that blocks until Service `backend` actually **answers on port 80**, and a main container `app` (nginx:1.27). It will sit in `Init:0/1`. 2. Then create Deployment `backend` (nginx:1.27, 1 replica, Pod label `app=backend`) so the Service gets an endpoint and your Pod starts. Two traps here: a ClusterIP Service gets a DNS record the moment it is created, endpoints or not, so waiting on name resolution alone never blocks. And busybox `nslookup` exits non-zero even when it succeeds, because the search-domain attempts after the hit return NXDOMAIN. Wait on a real connection instead. """, "setup": NS + r""" mkns %{ns} $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null $K -n %{ns} delete deploy backend --ignore-not-found >/dev/null $K apply -f - >/dev/null </dev/null 2>&1; do echo waiting; sleep 2; done"] containers: [{name: app, image: nginx:1.27}] EOF k -n %{ns} get pod %{pod} # Init:0/1 k -n %{ns} create deploy backend --image=nginx:1.27 k -n %{ns} patch deploy backend -p '{"spec":{"template":{"metadata":{"labels":{"app":"backend"}}}}}' # A ClusterIP Service resolves in DNS from the moment it exists - endpoints change nothing about # that - so `until nslookup backend` would not block even before you add the Deployment. Worse, # busybox nslookup returns 1 even on a successful lookup, because it walks the whole search list # and the trailing NXDOMAINs set the exit status, so that loop never terminates either way. # With no endpoints, kube-proxy REJECTs the connection, so wget fails fast and the loop is real. """, }, { "id": 3, "domain": "DESIGN", "title": "CronJob with deadlines", "params": lambda r: {"ns": ns_pick(r, "batch"), "cj": name_pick(r, "digest"), "m": r.choice([3, 5, 7])}, "task": """ In `%{ns}` create CronJob `%{cj}`: - schedule: every %{m} minutes - image `busybox:1.36`, command prints `digest` - a run that cannot start within 10 seconds of its schedule is skipped - a run that takes longer than 20 seconds is killed - keep 2 successful and 2 failed Jobs - restartPolicy `Never` """, "setup": NS + r"""mkns %{ns}; $K -n %{ns} delete cj %{cj} --ignore-not-found >/dev/null""", "checks": [ C("schedule and history limits", 2, r""" c="$K -n %{ns} get cj %{cj}" eq "$($c -o jsonpath='{.spec.schedule}' 2>/dev/null)" "*/%{m} * * * *" && eq "$($c -o jsonpath='{.spec.successfulJobsHistoryLimit}')" 2 && eq "$($c -o jsonpath='{.spec.failedJobsHistoryLimit}')" 2"""), C("startingDeadlineSeconds 10 and activeDeadlineSeconds 20", 3, r""" c="$K -n %{ns} get cj %{cj}" eq "$($c -o jsonpath='{.spec.startingDeadlineSeconds}' 2>/dev/null)" 10 && eq "$($c -o jsonpath='{.spec.jobTemplate.spec.activeDeadlineSeconds}')" 20"""), C("restartPolicy Never", 1, r"""eq "$(j -n %{ns} cj %{cj} '{.spec.jobTemplate.spec.template.spec.restartPolicy}')" Never"""), ], "solution": """ k -n %{ns} create cj %{cj} --image=busybox:1.36 --schedule='*/%{m} * * * *' $do -- sh -c "echo digest" > cj.yaml # spec.startingDeadlineSeconds: 10 (skip a late start) # spec.successfulJobsHistoryLimit: 2 / failedJobsHistoryLimit: 2 # spec.jobTemplate.spec.activeDeadlineSeconds: 20 (kill an overrunning run) # spec.jobTemplate.spec.template.spec.restartPolicy: Never k apply -f cj.yaml # startingDeadlineSeconds lives on the CronJob, activeDeadlineSeconds on the Job template. """, }, # ---------------------------------------------------------------- DEPLOY ---- { "id": 4, "domain": "DEPLOY", "title": "Roll forward, then back", "params": lambda r: {"ns": ns_pick(r, "rel"), "dep": name_pick(r, "frontend")}, "task": """ Deployment `%{dep}` in `%{ns}` runs `nginx:1.25` with 3 replicas. 1. Update the image to `nginx:1.27` and record a change-cause annotation of `pick 1.27`. 2. Then update it to `nginx:9.99-nope`, which will never become ready. 3. Roll back so all 3 replicas are Available on `nginx:1.27` again. """, "setup": NS + r""" mkns %{ns} $K -n %{ns} delete deploy %{dep} --ignore-not-found --wait=true >/dev/null $K -n %{ns} create deploy %{dep} --image=nginx:1.25 --replicas=3 >/dev/null $K -n %{ns} rollout status deploy %{dep} --timeout=120s >/dev/null 2>&1 """, "checks": [ C("running image is nginx:1.27 with 3 available", 4, r""" eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].image}')" nginx:1.27 && eq "$(j -n %{ns} deploy %{dep} '{.status.availableReplicas}')" 3"""), C("rollout history has at least three revisions", 2, r"""[ "$($K -n %{ns} rollout history deploy %{dep} 2>/dev/null | grep -c '^[0-9]')" -ge 3 ]"""), C("a change-cause annotation is recorded", 2, r"""j -n %{ns} deploy %{dep} '{.metadata.annotations.kubernetes\.io/change-cause}' | grep -q ."""), ], "solution": """ k -n %{ns} set image deploy %{dep} nginx=nginx:1.27 k -n %{ns} annotate deploy %{dep} kubernetes.io/change-cause='pick 1.27' --overwrite k -n %{ns} rollout status deploy %{dep} k -n %{ns} set image deploy %{dep} nginx=nginx:9.99-nope k -n %{ns} rollout status deploy %{dep} --timeout=30s # never completes k -n %{ns} rollout history deploy %{dep} k -n %{ns} rollout undo deploy %{dep} k -n %{ns} rollout status deploy %{dep} """, }, { "id": 5, "domain": "DEPLOY", "title": "Blue/green cutover", "params": lambda r: {"ns": ns_pick(r, "bg"), "svc": name_pick(r, "shop")}, "task": """ Namespace `%{ns}` has Deployment `%{svc}-blue` (Pod labels `app=%{svc}`, `version=blue`) behind Service `%{svc}`. 1. Create Deployment `%{svc}-green`: 2 replicas, image `nginx:1.27`, Pod labels `app=%{svc}` and `version=green`. 2. When it is fully Available, switch Service `%{svc}` to serve green only. 3. Leave blue running untouched so the switch can be reversed. """, "setup": NS + r""" mkns %{ns} $K -n %{ns} delete deploy %{svc}-blue %{svc}-green --ignore-not-found >/dev/null $K -n %{ns} delete svc %{svc} --ignore-not-found >/dev/null $K apply -f - >/dev/null < green.yaml # rename to %{svc}-green, change every version: blue -> green, image -> nginx:1.27, # strip status/uid/resourceVersion/creationTimestamp k apply -f green.yaml k -n %{ns} rollout status deploy %{svc}-green k -n %{ns} patch svc %{svc} -p '{"spec":{"selector":{"app":"%{svc}","version":"green"}}}' k -n %{ns} get endpointslices -l kubernetes.io/service-name=%{svc} # Rolling back is the same patch with version: blue - which is why blue stays up. """, }, { "id": 6, "domain": "DEPLOY", "title": "Helm release lifecycle", "params": lambda r: {"ns": ns_pick(r, "helm"), "rel": name_pick(r, "site")}, "task": """ A chart is at `{WS}/q6/chart`. 1. Install it into Namespace `%{ns}` (create it) as release `%{rel}` with `replicaCount=2`. 2. Upgrade the release to `replicaCount=4`. 3. Roll the release back so the Deployment is at 2 replicas again — without uninstalling it. 4. Write the release's current revision number to `{WS}/q6/revision.txt`. """, "setup": r""" rm -rf "$WS/q6"; mkdir -p "$WS/q6/chart/templates" cat > "$WS/q6/chart/Chart.yaml" <<'EOF' apiVersion: v2 name: site version: 0.1.0 EOF cat > "$WS/q6/chart/values.yaml" <<'EOF' replicaCount: 1 image: nginx:1.27 EOF cat > "$WS/q6/chart/templates/deploy.yaml" <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: {name: {{ .Release.Name }}-site} spec: replicas: {{ .Values.replicaCount }} selector: {matchLabels: {app: {{ .Release.Name }}-site}} template: metadata: {labels: {app: {{ .Release.Name }}-site}} spec: {containers: [{name: web, image: "{{ .Values.image }}"}]} EOF $K create ns %{ns} --dry-run=client -o yaml | $K apply -f - >/dev/null helm -n %{ns} uninstall %{rel} >/dev/null 2>&1 || true command -v helm >/dev/null || echo "warn: helm not installed" """, "checks": [ C("release deployed with at least three revisions of history", 3, r""" helm -n %{ns} status %{rel} -o json 2>/dev/null | python3 -c 'import sys,json;sys.exit(0 if json.load(sys.stdin)["info"]["status"]=="deployed" else 1)' || exit 1 [ "$(helm -n %{ns} history %{rel} -o json 2>/dev/null | python3 -c 'import sys,json;print(len(json.load(sys.stdin)))')" -ge 3 ]"""), C("Deployment is back to 2 replicas", 3, r"""eq "$(j -n %{ns} deploy %{rel}-site '{.spec.replicas}')" 2"""), C("revision.txt matches the release's current revision", 2, r""" cur=$(helm -n %{ns} history %{rel} -o json 2>/dev/null | python3 -c 'import sys,json;print(max(r["revision"] for r in json.load(sys.stdin)))' 2>/dev/null) [ -n "$cur" ] && eq "$(tr -dc '0-9' < "$WS/q6/revision.txt" 2>/dev/null)" "$cur" """), ], "solution": """ k create ns %{ns} helm -n %{ns} install %{rel} {WS}/q6/chart --set replicaCount=2 helm -n %{ns} upgrade %{rel} {WS}/q6/chart --set replicaCount=4 helm -n %{ns} history %{rel} helm -n %{ns} rollback %{rel} 1 # creates a NEW revision identical to revision 1 helm -n %{ns} history %{rel} | grep '^[0-9]' | tail -1 | awk '{print $1}' > {WS}/q6/revision.txt """, }, # ------------------------------------------------------------------- OBS ---- { "id": 7, "domain": "OBS", "title": "Three kinds of probe", "params": lambda r: {"ns": ns_pick(r, "probe"), "dep": name_pick(r, "api")}, "task": """ Deployment `%{dep}` in `%{ns}` (nginx:1.27, 2 replicas) needs health checks: - **startup**: HTTP GET `/` on port 80, `periodSeconds: 5`, allowed a total of 60 seconds to come up - **readiness**: HTTP GET `/` on port 80, every 5 seconds, a single failure removes it from endpoints - **liveness**: TCP on port 80, every 10 seconds, initial delay 15 seconds All replicas must end up Ready. """, "setup": NS + r""" mkns %{ns} $K -n %{ns} delete deploy %{dep} --ignore-not-found >/dev/null $K -n %{ns} create deploy %{dep} --image=nginx:1.27 --replicas=2 >/dev/null """, "checks": [ C("startupProbe: httpGet / :80, period 5, failureThreshold 12", 3, r""" c='{.spec.template.spec.containers[0].startupProbe' eq "$(j -n %{ns} deploy %{dep} "$c.httpGet.path}")" / && eq "$(j -n %{ns} deploy %{dep} "$c.periodSeconds}")" 5 && eq "$(j -n %{ns} deploy %{dep} "$c.failureThreshold}")" 12"""), C("readinessProbe: httpGet, period 5, failureThreshold 1", 3, r""" c='{.spec.template.spec.containers[0].readinessProbe' eq "$(j -n %{ns} deploy %{dep} "$c.httpGet.path}")" / && eq "$(j -n %{ns} deploy %{dep} "$c.periodSeconds}")" 5 && eq "$(j -n %{ns} deploy %{dep} "$c.failureThreshold}")" 1"""), C("livenessProbe: tcpSocket 80, period 10, initialDelay 15", 3, r""" c='{.spec.template.spec.containers[0].livenessProbe' eq "$(j -n %{ns} deploy %{dep} "$c.tcpSocket.port}")" 80 && eq "$(j -n %{ns} deploy %{dep} "$c.periodSeconds}")" 10 && eq "$(j -n %{ns} deploy %{dep} "$c.initialDelaySeconds}")" 15"""), C("all replicas Ready", 2, r"""eq "$(j -n %{ns} deploy %{dep} '{.status.readyReplicas}')" 2"""), ], "solution": """ k -n %{ns} edit deploy %{dep} # add to the container: # startupProbe: {httpGet: {path: /, port: 80}, periodSeconds: 5, failureThreshold: 12} # readinessProbe: {httpGet: {path: /, port: 80}, periodSeconds: 5, failureThreshold: 1} # livenessProbe: {tcpSocket: {port: 80}, periodSeconds: 10, initialDelaySeconds: 15} k -n %{ns} rollout status deploy %{dep} # 12 x 5s = 60s. While a startupProbe runs, liveness and readiness are suspended, which is why # it beats a long initialDelaySeconds on liveness for slow starters. """, }, { "id": 8, "domain": "OBS", "title": "Diagnose and fix a broken Pod", "params": lambda r: {"ns": ns_pick(r, "debug"), "pod": name_pick(r, "worker")}, "task": """ Pod `%{pod}` in `%{ns}` is not running. 1. Write the container's waiting **reason**, as reported in the Pod's status, to `{WS}/q8/reason.txt`. 2. Fix the Pod so it runs. Keep the name, Namespace and command — correct only what is wrong. """, "setup": NS + r""" mkns %{ns}; rm -rf "$WS/q8"; mkdir -p "$WS/q8" $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=true >/dev/null $K apply -f - >/dev/null </dev/null"""), C("pod Running with its original command", 4, r""" eq "$(j -n %{ns} pod %{pod} '{.status.phase}')" Running && j -n %{ns} pod %{pod} '{.spec.containers[0].command}' | grep -q sleep"""), ], "solution": """ k -n %{ns} get pod %{pod} k -n %{ns} get pod %{pod} -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}' # ImagePullBackOff -> {WS}/q8/reason.txt k -n %{ns} describe pod %{pod} | tail -15 # "Failed to pull image ... not found" k -n %{ns} set image pod/%{pod} worker=busybox:1.36 # `image` is one of the very few mutable fields on a running Pod, so no delete/recreate is needed # here - the kubelet just pulls the new image and restarts the container. Most other spec fields # (command, volumes, securityContext) really are immutable, and for those the fix is: # k -n %{ns} get pod %{pod} -o yaml > fix.yaml # edit, strip status/uid/resourceVersion # k -n %{ns} delete pod %{pod} --now && k apply -f fix.yaml """, }, # ------------------------------------------------------------------- ENV ---- { "id": 9, "domain": "ENV", "title": "SecurityContext and capabilities", "params": lambda r: {"ns": ns_pick(r, "sec"), "pod": name_pick(r, "hardened"), "uid": r.choice([1001, 2000, 10001])}, "task": """ Create Pod `%{pod}` in `%{ns}` (image `busybox:1.36`, command `sleep 86400`) that: - runs as user %{uid} and group %{uid}, and must not run as root, - has a read-only root filesystem, - drops **all** capabilities and adds back only `NET_BIND_SERVICE`, - cannot gain new privileges. """, "setup": NS + r"""mkns %{ns}; $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null""", "checks": [ C("runs as UID %{uid} with runAsNonRoot", 3, r""" p="$K -n %{ns} get pod %{pod}" u=$($p -o jsonpath='{.spec.securityContext.runAsUser}' 2>/dev/null); [ -z "$u" ] && u=$($p -o jsonpath='{.spec.containers[0].securityContext.runAsUser}') eq "$u" %{uid} && $p -o jsonpath='{.spec.securityContext.runAsNonRoot}{.spec.containers[0].securityContext.runAsNonRoot}' | grep -q true"""), C("readOnlyRootFilesystem and allowPrivilegeEscalation=false", 3, r""" c='{.spec.containers[0].securityContext' eq "$(j -n %{ns} pod %{pod} "$c.readOnlyRootFilesystem}")" true && eq "$(j -n %{ns} pod %{pod} "$c.allowPrivilegeEscalation}")" false"""), C("capabilities drop ALL, add NET_BIND_SERVICE", 3, r""" c='{.spec.containers[0].securityContext.capabilities' j -n %{ns} pod %{pod} "$c.drop[*]}" | grep -q ALL && j -n %{ns} pod %{pod} "$c.add[*]}" | grep -q NET_BIND_SERVICE"""), C("pod Running as that UID", 2, r""" eq "$(j -n %{ns} pod %{pod} '{.status.phase}')" Running && $K -n %{ns} exec %{pod} -- id 2>/dev/null | grep -q 'uid=%{uid}'"""), ], "solution": """ cat </dev/null $K -n %{ns} delete cm app-cfg --ignore-not-found >/dev/null; $K -n %{ns} delete secret app-sec --ignore-not-found >/dev/null """, "checks": [ C("ConfigMap and Secret hold the right values", 2, r""" eq "$(j -n %{ns} cm app-cfg '{.data.mode}')" live && eq "$(j -n %{ns} secret app-sec '{.data.apikey}' | base64 -d)" xyz789"""), C("one projected volume 'bundle' with all three sources", 3, r""" s=$(j -n %{ns} pod %{pod} '{.spec.volumes[?(@.name=="bundle")].projected.sources}') echo "$s" | grep -q configMap && echo "$s" | grep -q secret && echo "$s" | grep -q serviceAccountToken"""), C("token source: audience vault, expiry 3600", 3, r""" s=$(j -n %{ns} pod %{pod} '{.spec.volumes[?(@.name=="bundle")].projected.sources[*].serviceAccountToken}') echo "$s" | grep -q '"audience":"vault"' && echo "$s" | grep -q '"expirationSeconds":3600'"""), C("all three files readable at /etc/bundle", 3, r""" eq "$($K -n %{ns} exec %{pod} -- cat /etc/bundle/mode 2>/dev/null)" live \ && eq "$($K -n %{ns} exec %{pod} -- cat /etc/bundle/apikey 2>/dev/null)" xyz789 \ && [ -n "$($K -n %{ns} exec %{pod} -- cat /etc/bundle/token 2>/dev/null)" ]"""), ], "solution": """ k -n %{ns} create cm app-cfg --from-literal=mode=live k -n %{ns} create secret generic app-sec --from-literal=apikey=xyz789 cat </dev/null $K -n %{ns} delete svc %{dep} %{dep}-ext --ignore-not-found >/dev/null $K -n %{ns} delete ingress %{dep} --ignore-not-found >/dev/null $K -n %{ns} create deploy %{dep} --image=nginx:1.27 --replicas=2 --port=80 >/dev/null """, "checks": [ C("ClusterIP Service 8080 -> 80 with endpoints", 3, r""" s="$K -n %{ns} get svc %{dep}" eq "$($s -o jsonpath='{.spec.type}' 2>/dev/null)" ClusterIP && eq "$($s -o jsonpath='{.spec.ports[0].port}')" 8080 && eq "$($s -o jsonpath='{.spec.ports[0].targetPort}')" 80 \ && [ -n "$(j -n %{ns} endpointslices -l kubernetes.io/service-name=%{dep} '{.items[*].endpoints[*].addresses[*]}')" ]"""), C("NodePort Service on %{np}", 2, r""" s="$K -n %{ns} get svc %{dep}-ext" eq "$($s -o jsonpath='{.spec.type}' 2>/dev/null)" NodePort && eq "$($s -o jsonpath='{.spec.ports[0].nodePort}')" %{np}"""), C("Ingress routes the host to the ClusterIP Service on 8080", 3, r""" i="$K -n %{ns} get ingress %{dep}" eq "$($i -o jsonpath='{.spec.ingressClassName}' 2>/dev/null)" nginx && eq "$($i -o jsonpath='{.spec.rules[0].host}')" %{dep}.example.com \ && eq "$($i -o jsonpath='{.spec.rules[0].http.paths[0].backend.service.name}')" %{dep} \ && eq "$($i -o jsonpath='{.spec.rules[0].http.paths[0].backend.service.port.number}')" 8080"""), ], "solution": """ k -n %{ns} expose deploy %{dep} --port=8080 --target-port=80 k -n %{ns} expose deploy %{dep} --name=%{dep}-ext --type=NodePort --port=80 --target-port=80 $do > ext.yaml # set spec.ports[0].nodePort: %{np} k apply -f ext.yaml k -n %{ns} create ingress %{dep} --class=nginx --rule="%{dep}.example.com/*=%{dep}:8080" """, }, { "id": 12, "domain": "NET", "title": "Restrict a namespace with NetworkPolicy", "params": lambda r: {"ns": ns_pick(r, "np"), "client": r.choice(["frontend", "gateway", "portal"])}, "task": """ Namespace `%{ns}` runs Pods labelled `app=db` and `app=%{client}`. Create NetworkPolicy `db-allow` so `app=db` Pods accept ingress **only** from `app=%{client}` Pods in the same Namespace on TCP 5432, and nothing else. Do not restrict egress. (kindnet enforces NetworkPolicy here, so a wrong policy really blocks traffic; you are graded on the object.) """, "setup": NS + r""" mkns %{ns} for a in db %{client}; do $K -n %{ns} create deploy $a --image=nginx:1.27 --dry-run=client -o yaml | $K apply -f - >/dev/null $K -n %{ns} patch deploy $a -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"app\":\"$a\"}}}}}" >/dev/null done $K -n %{ns} delete netpol db-allow --ignore-not-found >/dev/null """, "checks": [ C("selects app=db, Ingress only", 2, r""" n="$K -n %{ns} get netpol db-allow" eq "$($n -o jsonpath='{.spec.podSelector.matchLabels.app}' 2>/dev/null)" db && eq "$($n -o jsonpath='{.spec.policyTypes}')" '["Ingress"]'"""), C("exactly one rule: from app=%{client} on TCP 5432", 4, r""" $K -n %{ns} get netpol db-allow -o json 2>/dev/null | python3 -c ' import sys,json i=json.load(sys.stdin)["spec"].get("ingress",[]) ok=len(i)==1 and len(i[0].get("from",[]))==1 and i[0]["from"][0].get("podSelector",{}).get("matchLabels",{}).get("app")=="%{client}" \ and len(i[0].get("ports",[]))==1 and str(i[0]["ports"][0]["port"])=="5432" sys.exit(0 if ok else 1)'"""), ], "solution": """ cat <