cka-sim/tracks/cka/bank_extra.py
vumast3r 595003e377 fix: repair stale reference solutions and Q33's grading check
tracks/cka/solutions.py had drifted out of sync with the actual current
task text for several questions (Q06, 11, 12, 13, 14, 22, 32, 40, 109,
110, 120, 122) - wrong namespaces/resource names, missing required steps,
or output files that were never actually written. Rewrote each to match
its task's real requirements.

tracks/cka/bank_extra.py's Q33 grading check queried
`.spec.aggregationRule` on a ClusterRole, but ClusterRole has no `.spec`
field - `aggregationRule` and `rules` are top-level. Fixed the jsonpath
so a correct answer actually passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fqcio9TXkEXH35UHebTgoL
2026-09-14 19:23:54 -06:00

647 lines
37 KiB
Python

"""Extra normal-difficulty CKA tasks (ids 21-43). Parameters (%{...}) are drawn per session."""
from tracks.cka.common import C, NS, ns_pick, name_pick
QUESTIONS = [
# ---------------------------------------------------------------- ARCH ----
{
"id": 21, "domain": "ARCH", "title": "Helm release install and upgrade",
"params": lambda r: {"ns": ns_pick(r, "helm"), "rel": name_pick(r, "web"), "n": r.choice([3, 4, 5])},
"task": """
A Helm chart is at `{WS}/q21/chart` (values: `replicaCount`, `image.*`, `service.type`, `service.port`).
1. Install it as release `%{rel}` into namespace `%{ns}` (create the namespace) with
`replicaCount=%{n}` overridden.
2. Then **upgrade** the same release so the Service type becomes `NodePort`
(value `service.type`). The release must be at revision 2 or higher.
""",
"setup": NS + r"""
mkns %{ns}
rm -rf "$WS/q21"; mkdir -p "$WS/q21/chart/templates"
cat > "$WS/q21/chart/Chart.yaml" <<'EOF'
apiVersion: v2
name: chart
description: A small web chart for the exam simulator
type: application
version: 0.1.0
appVersion: "1.27"
EOF
cat > "$WS/q21/chart/values.yaml" <<'EOF'
replicaCount: 1
image:
repository: nginx
tag: "1.27"
service:
type: ClusterIP
port: 80
EOF
cat > "$WS/q21/chart/templates/_helpers.tpl" <<'EOF'
{{- define "chart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
EOF
cat > "$WS/q21/chart/templates/deployment.yaml" <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "chart.fullname" . }}
labels: {app: {{ include "chart.fullname" . }}}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels: {app: {{ include "chart.fullname" . }}}
template:
metadata:
labels: {app: {{ include "chart.fullname" . }}}
spec:
containers:
- name: web
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports: [{containerPort: 80}]
EOF
cat > "$WS/q21/chart/templates/service.yaml" <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: {{ include "chart.fullname" . }}
spec:
type: {{ .Values.service.type }}
selector: {app: {{ include "chart.fullname" . }}}
ports: [{port: {{ .Values.service.port }}, targetPort: 80}]
EOF
command -v helm >/dev/null || echo "warn: helm is not installed on this machine - Q21 cannot be completed or graded without it"
helm -n %{ns} uninstall %{rel} >/dev/null 2>&1 || true
""",
"checks": [
C("release %{rel} deployed in %{ns}", 1, 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)'"""),
C("replicaCount=%{n} applied", 1, r"""eq "$(j -n %{ns} deploy %{rel}-chart '{.spec.replicas}')" %{n}"""),
C("upgraded: revision >= 2 and Service is NodePort", 2, r"""
rev=$(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 "$rev" ] && [ "$rev" -ge 2 ] && eq "$(j -n %{ns} svc %{rel}-chart '{.spec.type}')" NodePort"""),
],
},
{
"id": 33, "domain": "ARCH", "title": "Aggregated ClusterRole",
"params": lambda r: {"cr": name_pick(r, "ops"), "user": r.choice(["dana", "lee", "sam"])},
"task": """
1. Create a ClusterRole `%{cr}` that has **no rules of its own** but aggregates every
ClusterRole labelled `rbac.example.com/aggregate-to-%{cr}=true`.
2. Create two ClusterRoles carrying that label: `%{cr}-pods` (get/list/watch pods) and
`%{cr}-services` (get/list/watch services).
3. Bind `%{cr}` cluster-wide to user `%{user}` with ClusterRoleBinding `%{cr}-binding`.
`kubectl auth can-i list pods --as=%{user} -A` must say yes; deleting pods must say no.
""",
"setup": r"""
for x in %{cr} %{cr}-pods %{cr}-services; do $K delete clusterrole $x --ignore-not-found >/dev/null; done
$K delete clusterrolebinding %{cr}-binding --ignore-not-found >/dev/null
""",
"checks": [
C("%{cr} has an aggregationRule with the right label", 1, r"""j clusterrole %{cr} '{.aggregationRule.clusterRoleSelectors[*].matchLabels}' | grep -q 'aggregate-to-%{cr}":"true'"""),
C("aggregation populated rules from the two labelled roles", 2, r"""j clusterrole %{cr} '{.rules[*].resources}' | grep -q pods && j clusterrole %{cr} '{.rules[*].resources}' | grep -q services"""),
C("%{user} can list pods and services cluster-wide", 1, r"""eq "$($K auth can-i list pods -A --as=%{user} 2>/dev/null)" yes && eq "$($K auth can-i list services -A --as=%{user} 2>/dev/null)" yes"""),
C("%{user} cannot delete pods", 1, r"""eq "$($K auth can-i delete pods -A --as=%{user} 2>/dev/null || true)" no"""),
],
},
{
"id": 34, "domain": "ARCH", "title": "Discover a CRD field with kubectl explain",
"params": lambda r: {"kind": r.choice(["Widget", "Gadget", "Gizmo"]), "field": r.choice(["retries", "attempts", "tries"]), "n": r.randint(2, 7)},
"task": """
A CRD for kind `%{kind}` (group `demo.io`) is installed. Somewhere in its schema is an
integer field whose description is exactly **"Number of retry attempts"**.
1. Using `kubectl explain`, find the field's full path (e.g. `spec.foo.bar`) and write it
on one line to `{WS}/q34/path.txt`.
2. Create a `%{kind}` named `sample` in namespace `default` with that field set to `%{n}`.
""",
"setup": r"""
rm -rf "$WS/q34"; mkdir -p "$WS/q34"
K_LOWER=$(echo %{kind} | tr A-Z a-z)
$K delete crd ${K_LOWER}s.demo.io --ignore-not-found >/dev/null
$K apply -f - >/dev/null <<EOF
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: {name: ${K_LOWER}s.demo.io}
spec:
group: demo.io
scope: Namespaced
names: {kind: %{kind}, plural: ${K_LOWER}s, singular: ${K_LOWER}}
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
mode: {type: string, description: "Operating mode"}
tuning:
type: object
description: "Performance tuning knobs"
properties:
%{field}: {type: integer, description: "Number of retry attempts"}
backoffSeconds: {type: integer, description: "Delay between attempts"}
EOF
""",
"checks": [
C("path.txt has the correct field path", 2, r"""[ "$(tr -d '[:space:]' < "$WS/q34/path.txt" 2>/dev/null)" = "spec.tuning.%{field}" ]"""),
C("%{kind}/sample created with the field = %{n}", 2, r"""K_LOWER=$(echo %{kind} | tr A-Z a-z); eq "$(j -n default ${K_LOWER}s.demo.io sample '{.spec.tuning.%{field}}')" %{n}"""),
],
},
{
"id": 40, "domain": "ARCH", "title": "kubeadm join token",
"params": lambda r: {"ttl": r.choice(["2h", "6h", "24h"])},
"task": """
A new worker is going to be added to the cluster.
On the control-plane node (`{SHELL_CP}`), create a **new** bootstrap token with TTL `%{ttl}`
and save the complete `kubeadm join` command for it (with `--discovery-token-ca-cert-hash`)
to `{WS}/q40/join.sh`.
""",
"setup": r"""
rm -rf "$WS/q40"; mkdir -p "$WS/q40"
for t in $($K -n kube-system get secrets -o jsonpath='{.items[?(@.type=="bootstrap.kubernetes.io/token")].metadata.name}'); do $K -n kube-system delete secret $t >/dev/null; done
""",
"checks": [
C("join.sh contains a kubeadm join with token and CA hash", 2, r"""grep -q 'kubeadm join' "$WS/q40/join.sh" 2>/dev/null && grep -q -- '--token' "$WS/q40/join.sh" && grep -q -- '--discovery-token-ca-cert-hash sha256:' "$WS/q40/join.sh" """),
C("the token in the file exists in the cluster with TTL ~%{ttl}", 3, r"""
tok=$(grep -o -- '--token [a-z0-9]\{6\}\.[a-z0-9]\{16\}' "$WS/q40/join.sh" 2>/dev/null | awk '{print $2}'); [ -n "$tok" ] || exit 1
id=${tok%%.*}; exp=$(j -n kube-system secret bootstrap-token-$id '{.data.expiration}' | base64 -d 2>/dev/null); [ -n "$exp" ] || exit 1
secs=$(( $(date -d "$exp" +%s) - $(date +%s) )); want=$(( $(echo %{ttl} | tr -d h) * 3600 ))
[ $secs -gt $((want - 900)) ] && [ $secs -le $((want + 60)) ]"""),
],
},
{
"id": 41, "domain": "ARCH", "title": "Component versions",
"params": lambda r: {"node": r.choice(["{W1}", "{W2}", "{W3}"])},
"task": """
Write the following to `{WS}/q41/versions.txt`, one per line, in this exact format:
```
apiserver=<version>
etcd=<version>
kubelet=<version>
runtime=<containerRuntimeVersion of node %{node}>
```
Use the values the cluster itself reports (`kubectl`, not what you remember).
""",
"setup": r"""rm -rf "$WS/q41"; mkdir -p "$WS/q41" """,
"checks": [
C("apiserver and kubelet versions correct", 2, r"""
f="$WS/q41/versions.txt"; [ -f "$f" ] || exit 1
api=$($K version -o json 2>/dev/null | python3 -c 'import sys,json;print(json.load(sys.stdin)["serverVersion"]["gitVersion"])')
kub=$(j node %{node} '{.status.nodeInfo.kubeletVersion}')
grep -qx "apiserver=$api" "$f" && grep -qx "kubelet=$kub" "$f" """),
C("etcd and runtime versions correct", 2, r"""
f="$WS/q41/versions.txt"
etcd=$(j -n kube-system pod -l component=etcd '{.items[0].spec.containers[0].image}' | sed 's/.*://')
rt=$(j node %{node} '{.status.nodeInfo.containerRuntimeVersion}')
grep -qx "etcd=$etcd" "$f" && grep -qx "runtime=$rt" "$f" """),
],
},
# ---------------------------------------------------------------- WORK ----
{
"id": 23, "domain": "WORK", "title": "metrics-server repair and HPA",
"params": lambda r: {"ns": ns_pick(r, "scale"), "dep": name_pick(r, "api"), "max": r.choice([5, 6, 8]), "cpu": r.choice([60, 70, 75])},
"task": """
`metrics-server` was installed in `kube-system` but `kubectl top nodes` does not work.
1. Fix metrics-server so `kubectl top nodes` returns data (hint: in this cluster the kubelet
serving certificates are not signed by the cluster CA).
2. Create an HPA `%{dep}` for Deployment `%{dep}` in namespace `%{ns}`:
min 2, max %{max}, target average CPU utilisation %{cpu}%.
""",
"setup": NS + r"""
mkns %{ns}
$K -n %{ns} create deploy %{dep} --image=nginx:1.25 --dry-run=client -o yaml | $K apply -f - >/dev/null
$K -n %{ns} set resources deploy %{dep} --requests=cpu=50m >/dev/null
$K -n %{ns} delete hpa %{dep} --ignore-not-found >/dev/null
if ! $K -n kube-system get deploy metrics-server >/dev/null 2>&1; then
curl -sfL --max-time 60 https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml -o /tmp/ms.yaml && $K apply -f /tmp/ms.yaml >/dev/null || echo "warn: could not download metrics-server manifest"
fi
# make it broken (kind kubelets use self-signed serving certs)
$K -n kube-system get deploy metrics-server -o json 2>/dev/null | python3 -c '
import sys,json; d=json.load(sys.stdin); c=d["spec"]["template"]["spec"]["containers"][0]
c["args"]=[a for a in c["args"] if a!="--kubelet-insecure-tls"]; print(json.dumps(d))' | $K apply -f - >/dev/null 2>&1 || true
""",
"checks": [
C("kubectl top nodes works", 2, r"""for i in $(seq 1 12); do $K top nodes >/dev/null 2>&1 && exit 0; sleep 5; done; exit 1"""),
C("HPA %{dep}: min 2, max %{max}", 1, r"""eq "$(j -n %{ns} hpa %{dep} '{.spec.minReplicas}')" 2 && eq "$(j -n %{ns} hpa %{dep} '{.spec.maxReplicas}')" %{max}"""),
C("HPA targets CPU %{cpu}% on the deployment", 1, r"""eq "$(j -n %{ns} hpa %{dep} '{.spec.scaleTargetRef.name}')" %{dep} && j -n %{ns} hpa %{dep} '{.spec.metrics[*].resource}' | grep -q '"averageUtilization":%{cpu}'"""),
],
},
{
"id": 24, "domain": "WORK", "title": "PriorityClass",
"params": lambda r: {"ns": ns_pick(r, "prio"), "pc": name_pick(r, "critical"), "val": r.choice([100000, 500000, 900000])},
"task": """
1. Create a PriorityClass `%{pc}` with value %{val}, not the global default, description "%{pc} workloads".
2. In namespace `%{ns}`, Deployment `payments` already exists. Make its pods use `%{pc}` and
ensure a rollout completes (pods must show priority %{val}).
""",
"setup": NS + r"""
mkns %{ns}
$K delete priorityclass %{pc} --ignore-not-found >/dev/null
$K -n %{ns} create deploy payments --image=nginx:1.25 --replicas=2 --dry-run=client -o yaml | $K apply -f - >/dev/null
""",
"checks": [
C("PriorityClass %{pc} value %{val}, not default", 2, r"""eq "$(j priorityclass %{pc} '{.value}')" %{val} && [ "$(j priorityclass %{pc} '{.globalDefault}')" != true ]"""),
C("payments pods running with priority %{val}", 2, r"""eq "$(j -n %{ns} deploy payments '{.status.availableReplicas}')" 2 && for p in $(j -n %{ns} pod -l app=payments '{.items[*].spec.priority}'); do eq "$p" %{val} || exit 1; done"""),
],
},
{
"id": 25, "domain": "WORK", "breaks": "w1-taint", "title": "Taints and tolerations",
"params": lambda r: {"ns": ns_pick(r, "sched"), "key": r.choice(["dedicated", "workload", "team"]), "val": r.choice(["gpu", "batch", "finance"]), "dep": name_pick(r, "worker")},
"task": """
1. Taint node `{W1}` with `%{key}=%{val}:NoSchedule`.
2. Create Deployment `%{dep}` in namespace `%{ns}` (image `busybox:1.36`, command `sleep 3600`,
2 replicas) that tolerates that taint **and** is pinned to `{W1}` with a nodeSelector on
`kubernetes.io/hostname`.
Both pods must be Running on `{W1}`. Do not remove the taint.
""",
"setup": NS + r"""
mkns %{ns}
$K taint node "$W1" %{key}- >/dev/null 2>&1 || true
$K -n %{ns} delete deploy %{dep} --ignore-not-found >/dev/null
""",
"checks": [
C("{W1} carries %{key}=%{val}:NoSchedule", 1, r"""j node "$W1" '{.spec.taints}' | grep -q '"key":"%{key}"' && j node "$W1" '{.spec.taints}' | grep -q '"value":"%{val}"' """),
C("deployment tolerates the taint", 1, r"""j -n %{ns} deploy %{dep} '{.spec.template.spec.tolerations}' | grep -q '"key":"%{key}"' """),
C("both pods Running on {W1}", 2, r"""eq "$(j -n %{ns} deploy %{dep} '{.status.availableReplicas}')" 2 && for n in $(j -n %{ns} pod -l app=%{dep} '{.items[*].spec.nodeName}'); do eq "$n" "$W1" || exit 1; done"""),
],
},
{
"id": 26, "domain": "WORK", "title": "DaemonSet on every node",
"params": lambda r: {"ns": ns_pick(r, "agents"), "ds": name_pick(r, "node-agent")},
"task": """
Create a DaemonSet `%{ds}` in namespace `%{ns}` (image `busybox:1.36`, command `sleep 3600`)
that runs on **every** node, including the control-plane.
Requests: cpu 10m, memory 16Mi.
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete ds %{ds} --ignore-not-found >/dev/null""",
"checks": [
C("DaemonSet exists with requests set", 1, r"""eq "$(j -n %{ns} ds %{ds} '{.spec.template.spec.containers[0].resources.requests.cpu}')" 10m"""),
C("tolerates the control-plane taint", 1, r"""j -n %{ns} ds %{ds} '{.spec.template.spec.tolerations[*].key}' | grep -q control-plane"""),
C("one ready pod per node (including control-plane)", 2, r"""n=$($K get nodes --no-headers 2>/dev/null | wc -l); eq "$(j -n %{ns} ds %{ds} '{.status.numberReady}')" "$n" && eq "$(j -n %{ns} ds %{ds} '{.status.desiredNumberScheduled}')" "$n" """),
],
},
{
"id": 27, "domain": "WORK", "title": "CronJob with a manual run",
"params": lambda r: {"ns": ns_pick(r, "jobs"), "cj": name_pick(r, "report"), "every": r.choice([5, 10, 15])},
"task": """
1. Create CronJob `%{cj}` in `%{ns}`: runs every %{every} minutes, image `busybox:1.36`,
command `date`, `concurrencyPolicy: Forbid`, keep 3 successful and 1 failed job,
each run limited to 30 seconds (`activeDeadlineSeconds`), restartPolicy Never.
2. Trigger it manually once as a Job named `%{cj}-manual` and let it complete.
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete cj %{cj} --ignore-not-found >/dev/null; $K -n %{ns} delete job %{cj}-manual --ignore-not-found >/dev/null""",
"checks": [
C("schedule every %{every} minutes, Forbid, history limits", 2, r"""c="$K -n %{ns} get cj %{cj}"; eq "$($c -o jsonpath='{.spec.schedule}' 2>/dev/null)" "*/%{every} * * * *" && eq "$($c -o jsonpath='{.spec.concurrencyPolicy}')" Forbid && eq "$($c -o jsonpath='{.spec.successfulJobsHistoryLimit}')" 3 && eq "$($c -o jsonpath='{.spec.failedJobsHistoryLimit}')" 1"""),
C("activeDeadlineSeconds 30, restartPolicy Never", 1, r"""eq "$(j -n %{ns} cj %{cj} '{.spec.jobTemplate.spec.activeDeadlineSeconds}')" 30 && eq "$(j -n %{ns} cj %{cj} '{.spec.jobTemplate.spec.template.spec.restartPolicy}')" Never"""),
C("manual job %{cj}-manual completed", 1, r"""for i in $(seq 1 10); do [ "$(j -n %{ns} job %{cj}-manual '{.status.succeeded}')" = 1 ] && exit 0; sleep 3; done; exit 1"""),
],
},
{
"id": 28, "domain": "WORK", "title": "Init container prepares content",
"params": lambda r: {"ns": ns_pick(r, "web"), "pod": name_pick(r, "site"), "text": r.choice(["hello from init", "prepared by init", "init was here"])},
"task": """
Create pod `%{pod}` in namespace `%{ns}`:
- init container `setup` (busybox:1.36) writes the text `%{text}` into `/work/index.html`,
- main container `web` (nginx:1.25) serves that file from `/usr/share/nginx/html`,
- both share an `emptyDir` volume named `content`.
`curl` from inside the cluster to the pod on port 80 must return the text.
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null""",
"checks": [
C("pod Running, init container named setup completed", 1, r"""eq "$(j -n %{ns} pod %{pod} '{.status.phase}')" Running && eq "$(j -n %{ns} pod %{pod} '{.spec.initContainers[0].name}')" setup"""),
C("emptyDir 'content' shared by both containers", 1, r"""[ -n "$(j -n %{ns} pod %{pod} '{.spec.volumes[?(@.name=="content")].emptyDir}')" ] && j -n %{ns} pod %{pod} '{.spec.containers[0].volumeMounts[*].name}' | grep -qw content"""),
C("nginx serves the init-written content", 2, r"""$K -n %{ns} exec %{pod} -c web -- cat /usr/share/nginx/html/index.html 2>/dev/null | grep -q '%{text}'"""),
],
},
{
"id": 29, "domain": "WORK", "title": "Immutable ConfigMap rotation",
"params": lambda r: {"ns": ns_pick(r, "cfg"), "dep": name_pick(r, "app"), "cm": name_pick(r, "settings")},
"task": """
Deployment `%{dep}` in `%{ns}` mounts ConfigMap `%{cm}` at `/etc/cfg`. The ConfigMap is
**immutable** and its key `mode` is `old`.
The application must now read `mode=new`. Without deleting `%{cm}` and without editing it
(it cannot be edited), make the running pods see `new` at `/etc/cfg/mode`.
Hint: version the ConfigMap.
""",
"setup": NS + r"""
mkns %{ns}
$K -n %{ns} delete deploy %{dep} --ignore-not-found >/dev/null
$K -n %{ns} delete cm -l q=29 --ignore-not-found >/dev/null; $K -n %{ns} delete cm %{cm} --ignore-not-found >/dev/null
$K apply -f - >/dev/null <<EOF
apiVersion: v1
kind: ConfigMap
metadata: {name: %{cm}, namespace: %{ns}, labels: {q: "29"}}
immutable: true
data: {mode: old}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: %{dep}, namespace: %{ns}}
spec:
replicas: 2
selector: {matchLabels: {app: %{dep}}}
template:
metadata: {labels: {app: %{dep}}}
spec:
containers:
- name: app
image: busybox:1.36
command: [sleep, "3600"]
volumeMounts: [{name: cfg, mountPath: /etc/cfg}]
volumes: [{name: cfg, configMap: {name: %{cm}}}]
EOF
""",
"checks": [
C("original ConfigMap %{cm} still exists and is immutable", 1, r"""eq "$(j -n %{ns} cm %{cm} '{.immutable}')" true && eq "$(j -n %{ns} cm %{cm} '{.data.mode}')" old"""),
C("deployment now references a different ConfigMap", 1, r"""n=$(j -n %{ns} deploy %{dep} '{.spec.template.spec.volumes[0].configMap.name}'); [ -n "$n" ] && [ "$n" != %{cm} ]"""),
C("all pods available and see mode=new", 2, r"""eq "$(j -n %{ns} deploy %{dep} '{.status.availableReplicas}')" 2 && for p in $(j -n %{ns} pod -l app=%{dep} '{.items[*].metadata.name}'); do eq "$($K -n %{ns} exec $p -- cat /etc/cfg/mode 2>/dev/null)" new || exit 1; done"""),
],
},
{
"id": 31, "domain": "WORK", "title": "Parallel Job",
"params": lambda r: {"ns": ns_pick(r, "batch"), "job": name_pick(r, "crunch"), "c": r.choice([6, 8, 9]), "p": r.choice([2, 3])},
"task": """
Create Job `%{job}` in `%{ns}`: image `busybox:1.36`, command `sh -c "echo done; sleep 2"`,
%{c} completions, %{p} in parallel, `backoffLimit: 2`, restartPolicy Never. Let it finish.
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete job %{job} --ignore-not-found >/dev/null""",
"checks": [
C("completions %{c}, parallelism %{p}, backoffLimit 2", 2, r"""eq "$(j -n %{ns} job %{job} '{.spec.completions}')" %{c} && eq "$(j -n %{ns} job %{job} '{.spec.parallelism}')" %{p} && eq "$(j -n %{ns} job %{job} '{.spec.backoffLimit}')" 2"""),
C("job succeeded %{c} times", 2, r"""for i in $(seq 1 20); do [ "$(j -n %{ns} job %{job} '{.status.succeeded}')" = %{c} ] && exit 0; sleep 3; done; exit 1"""),
],
},
{
"id": 32, "domain": "WORK", "title": "ResourceQuota and LimitRange",
"params": lambda r: {"ns": ns_pick(r, "team"), "pods": r.choice([4, 5, 6]), "dcpu": r.choice(["100m", "150m", "200m"])},
"task": """
In namespace `%{ns}`:
1. ResourceQuota `%{ns}-quota`: at most %{pods} pods, total CPU requests 1, total memory limits 2Gi.
2. LimitRange `%{ns}-defaults`: for containers, default request cpu %{dcpu} / memory 64Mi and
default limit cpu 500m / memory 256Mi.
3. Create Deployment `worker` (nginx:1.25, 2 replicas) **without** specifying resources.
Its pods must be Running with the defaults applied.
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete quota,limitrange --all >/dev/null 2>&1; $K -n %{ns} delete deploy worker --ignore-not-found >/dev/null""",
"checks": [
C("ResourceQuota limits pods/%{pods}, requests.cpu 1, limits.memory 2Gi", 2, r"""q="$K -n %{ns} get quota %{ns}-quota"; eq "$($q -o jsonpath='{.spec.hard.pods}' 2>/dev/null)" %{pods} && eq "$($q -o jsonpath='{.spec.hard.requests\.cpu}')" 1 && eq "$($q -o jsonpath='{.spec.hard.limits\.memory}')" 2Gi"""),
C("LimitRange defaults set", 1, r"""l="$K -n %{ns} get limitrange %{ns}-defaults"; eq "$($l -o jsonpath='{.spec.limits[0].defaultRequest.cpu}' 2>/dev/null)" %{dcpu} && eq "$($l -o jsonpath='{.spec.limits[0].default.memory}')" 256Mi"""),
C("worker pods Running with defaulted requests", 1, r"""eq "$(j -n %{ns} deploy worker '{.status.availableReplicas}')" 2 && eq "$(j -n %{ns} pod -l app=worker '{.items[0].spec.containers[0].resources.requests.cpu}')" %{dcpu}"""),
],
},
# ----------------------------------------------------------------- NET ----
{
"id": 22, "domain": "NET", "title": "Gateway API HTTPRoute",
"params": lambda r: {"ns": ns_pick(r, "gw"), "gc": name_pick(r, "lab-gc"), "gw": name_pick(r, "edge"), "host": r.choice(["shop", "api", "app"])},
"task": """
1. Install the Gateway API **standard** channel CRDs (v1.2.1 or newer) into the cluster.
2. Create a GatewayClass `%{gc}` (controllerName `example.com/gateway`).
3. In namespace `%{ns}` create Gateway `%{gw}` using that class, one HTTP listener on port 80
for hostname `%{host}.example.com`.
4. Create HTTPRoute `%{host}-route` attached to `%{gw}` that sends `/api` (PathPrefix) to
Service `api` port 8080 and everything else to Service `web` port 80.
(No controller runs here; you are graded on the objects.)
""",
"setup": NS + r"""
mkns %{ns}
$K -n %{ns} create deploy web --image=nginx:1.25 --port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null
$K -n %{ns} create deploy api --image=nginx:1.25 --port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null
$K -n %{ns} expose deploy web --port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null
$K -n %{ns} expose deploy api --port=8080 --target-port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null
$K delete gatewayclass %{gc} --ignore-not-found >/dev/null 2>&1; $K -n %{ns} delete gateway %{gw} --ignore-not-found >/dev/null 2>&1; $K -n %{ns} delete httproute %{host}-route --ignore-not-found >/dev/null 2>&1
""",
"checks": [
C("Gateway API CRDs installed (gateways, httproutes)", 1, r"""$K get crd gateways.gateway.networking.k8s.io httproutes.gateway.networking.k8s.io >/dev/null 2>&1"""),
C("GatewayClass %{gc} with the controllerName", 1, r"""eq "$(j gatewayclass %{gc} '{.spec.controllerName}')" example.com/gateway"""),
C("Gateway %{gw}: class, HTTP listener :80 for %{host}.example.com", 2, r"""g="$K -n %{ns} get gateway %{gw}"; eq "$($g -o jsonpath='{.spec.gatewayClassName}' 2>/dev/null)" %{gc} && eq "$($g -o jsonpath='{.spec.listeners[0].port}')" 80 && eq "$($g -o jsonpath='{.spec.listeners[0].hostname}')" %{host}.example.com"""),
C("HTTPRoute: parentRef %{gw}, /api -> api:8080, default -> web:80", 2, r"""
h="$K -n %{ns} get httproute %{host}-route"; $h >/dev/null 2>&1 || exit 1
eq "$($h -o jsonpath='{.spec.parentRefs[0].name}')" %{gw} || exit 1
$h -o json | python3 -c '
import sys,json; r=json.load(sys.stdin)["spec"]["rules"]
api=[x for x in r if any(m.get("path",{}).get("value")=="/api" for m in x.get("matches",[]))]
web=[x for x in r if x is not api and x.get("backendRefs",[{}])[0].get("name")=="web"]
ok=api and api[0]["backendRefs"][0]["name"]=="api" and api[0]["backendRefs"][0]["port"]==8080 and web and web[0]["backendRefs"][0]["port"]==80
sys.exit(0 if ok else 1)'"""),
],
},
{
"id": 30, "domain": "NET", "title": "Named ports",
"params": lambda r: {"ns": ns_pick(r, "ports"), "dep": name_pick(r, "catalog"), "pname": r.choice(["http-web", "app-port", "svc-http"])},
"task": """
1. In `%{ns}`, Deployment `%{dep}` (nginx:1.25, 1 replica) must declare its container port 80
with the **name** `%{pname}`.
2. Create Service `%{dep}` (ClusterIP, port 8080) whose `targetPort` references the port
**by name**, not by number.
3. Add a readiness probe on the container that also references the port by name (HTTP GET /).
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete deploy,svc %{dep} --ignore-not-found >/dev/null""",
"checks": [
C("container port 80 named %{pname}", 1, r"""eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].ports[?(@.containerPort==80)].name}')" %{pname}"""),
C("Service port 8080 -> targetPort %{pname} (by name)", 2, r"""eq "$(j -n %{ns} svc %{dep} '{.spec.ports[0].port}')" 8080 && eq "$(j -n %{ns} svc %{dep} '{.spec.ports[0].targetPort}')" %{pname}"""),
C("readiness probe uses the named port; endpoints ready", 1, r"""eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].readinessProbe.httpGet.port}')" %{pname} && [ -n "$(j -n %{ns} endpointslices -l kubernetes.io/service-name=%{dep} '{.items[*].endpoints[?(@.conditions.ready==true)].addresses[*]}')" ]"""),
],
},
{
"id": 42, "domain": "NET", "title": "Egress-only NetworkPolicy",
"params": lambda r: {"ns": ns_pick(r, "egress"), "app": name_pick(r, "client"), "cidr": r.choice(["10.96.0.0/12", "172.16.0.0/16", "192.168.100.0/24"])},
"task": """
Namespace `%{ns}` has a Deployment `%{app}` (label `app=%{app}`).
Create NetworkPolicy `%{app}-egress` that applies only to those pods and allows **egress** to:
- DNS (UDP and TCP 53) to pods in `kube-system` labelled `k8s-app=kube-dns`,
- TCP 443 to the CIDR `%{cidr}`,
and denies all other egress. Do not restrict ingress.
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} create deploy %{app} --image=busybox:1.36 --dry-run=client -o yaml -- sleep 3600 | $K apply -f - >/dev/null; $K -n %{ns} delete netpol %{app}-egress --ignore-not-found >/dev/null""",
"checks": [
C("applies to app=%{app}, Egress only", 1, r"""n="$K -n %{ns} get netpol %{app}-egress"; eq "$($n -o jsonpath='{.spec.podSelector.matchLabels.app}' 2>/dev/null)" %{app} && eq "$($n -o jsonpath='{.spec.policyTypes}')" '["Egress"]'"""),
C("DNS rule: kube-system namespace + k8s-app=kube-dns, ports 53 UDP and TCP", 2, r"""
$K -n %{ns} get netpol %{app}-egress -o json 2>/dev/null | python3 -c '
import sys,json; e=json.load(sys.stdin)["spec"].get("egress",[])
dns=[r for r in e if any((p.get("port")==53 or p.get("port")=="53") for p in r.get("ports",[]))]
ok=False
for r in dns:
protos={p.get("protocol","TCP") for p in r["ports"] if str(p.get("port"))=="53"}
to=r.get("to",[])
nsok=any("kube-system" in json.dumps(t.get("namespaceSelector",{})) for t in to)
podok=any(t.get("podSelector",{}).get("matchLabels",{}).get("k8s-app")=="kube-dns" for t in to)
if protos>={"UDP","TCP"} and nsok and podok: ok=True
sys.exit(0 if ok else 1)'"""),
C("TCP 443 to %{cidr} only", 1, r"""
$K -n %{ns} get netpol %{app}-egress -o json 2>/dev/null | python3 -c '
import sys,json; e=json.load(sys.stdin)["spec"].get("egress",[])
ok=any(any(t.get("ipBlock",{}).get("cidr")=="%{cidr}" for t in r.get("to",[])) and any(str(p.get("port"))=="443" for p in r.get("ports",[])) for r in e)
sys.exit(0 if ok else 1)'"""),
],
},
{
"id": 43, "domain": "NET", "title": "Headless and ExternalName services",
"params": lambda r: {"ns": ns_pick(r, "svc2"), "sts": name_pick(r, "db"), "ext": r.choice(["api.example.com", "db.corp.example.org", "cache.internal.example.net"])},
"task": """
1. In `%{ns}`, StatefulSet `%{sts}` (2 replicas) exists but has no Service. Create the **headless**
Service `%{sts}` it needs (port 5432) so that `%{sts}-0.%{sts}.%{ns}.svc.cluster.local` resolves.
2. Create Service `legacy` of type ExternalName pointing at `%{ext}`.
3. From a pod in the namespace, resolve `%{sts}-0.%{sts}` and write the full nslookup output
to `{WS}/q43/pod0.txt`.
""",
"setup": NS + r"""
mkns %{ns}; rm -rf "$WS/q43"; mkdir -p "$WS/q43"
$K -n %{ns} delete svc %{sts} legacy --ignore-not-found >/dev/null
$K apply -f - >/dev/null <<EOF
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: %{sts}, namespace: %{ns}}
spec:
serviceName: %{sts}
replicas: 2
selector: {matchLabels: {app: %{sts}}}
template:
metadata: {labels: {app: %{sts}}}
spec:
containers: [{name: db, image: busybox:1.36, command: [sleep, "3600"], ports: [{containerPort: 5432}]}]
EOF
""",
"checks": [
C("headless Service %{sts} (clusterIP None) selecting app=%{sts}, port 5432", 2, r"""eq "$(j -n %{ns} svc %{sts} '{.spec.clusterIP}')" None && eq "$(j -n %{ns} svc %{sts} '{.spec.selector.app}')" %{sts} && eq "$(j -n %{ns} svc %{sts} '{.spec.ports[0].port}')" 5432"""),
C("ExternalName Service legacy -> %{ext}", 1, r"""eq "$(j -n %{ns} svc legacy '{.spec.type}')" ExternalName && eq "$(j -n %{ns} svc legacy '{.spec.externalName}')" %{ext}"""),
C("pod0.txt shows %{sts}-0 resolving to its pod IP", 1, r"""ip=$(j -n %{ns} pod %{sts}-0 '{.status.podIP}'); [ -n "$ip" ] && grep -q "$ip" "$WS/q43/pod0.txt" 2>/dev/null"""),
],
},
# ---------------------------------------------------------------- STOR ----
{
"id": 35, "domain": "STOR", "title": "Re-bind a Released PersistentVolume",
"params": lambda r: {"ns": ns_pick(r, "store"), "pv": name_pick(r, "pv-archive"), "pvc": name_pick(r, "archive-claim")},
"task": """
PersistentVolume `%{pv}` (1Gi, hostPath, class `manual`) is in status **Released**: the claim that used
it was deleted, but its data must be kept and reused.
Make `%{pv}` bindable again without deleting it, then create PVC `%{pvc}` in `%{ns}`
(class `manual`, 500Mi, RWO) so it binds to **exactly** that PV. Do not create any other PV.
""",
"setup": NS + r"""
mkns %{ns}
$K -n %{ns} delete pvc %{pvc} --ignore-not-found >/dev/null; $K delete pv %{pv} --ignore-not-found >/dev/null
$K apply -f - >/dev/null <<EOF
apiVersion: v1
kind: PersistentVolume
metadata: {name: %{pv}}
spec:
capacity: {storage: 1Gi}
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath: {path: /mnt/%{pv}}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: old-claim, namespace: %{ns}}
spec: {accessModes: [ReadWriteOnce], storageClassName: manual, resources: {requests: {storage: 500Mi}}}
EOF
for i in $(seq 1 20); do [ "$(j -n %{ns} pvc old-claim '{.status.phase}')" = Bound ] && break; sleep 1; done
$K -n %{ns} delete pvc old-claim >/dev/null
""",
"checks": [
C("PV %{pv} still exists and was not recreated", 2, r"""[ -n "$(j pv %{pv} '{.metadata.uid}')" ] && eq "$(j pv %{pv} '{.spec.persistentVolumeReclaimPolicy}')" Retain"""),
C("PVC %{pvc} Bound to %{pv}", 3, r"""eq "$(j -n %{ns} pvc %{pvc} '{.status.phase}')" Bound && eq "$(j -n %{ns} pvc %{pvc} '{.spec.volumeName}')" %{pv}"""),
C("no extra PVs were created", 1, r"""[ "$($K get pv --no-headers 2>/dev/null | grep -c manual)" -le 2 ]"""),
],
},
{
"id": 36, "domain": "STOR", "title": "Data survives the pod",
"params": lambda r: {"ns": ns_pick(r, "persist"), "pvc": name_pick(r, "notes"), "text": r.choice(["order-9981", "ticket-4471", "batch-2026"])},
"task": """
1. In `%{ns}` create PVC `%{pvc}` (200Mi, RWO) using the cluster's default StorageClass.
2. Run pod `writer` (busybox:1.36) that mounts it at `/data` and writes the text `%{text}`
into `/data/marker.txt`, then exits (restartPolicy Never).
3. After `writer` has completed, run pod `reader` (busybox:1.36, `sleep 3600`) mounting the
same claim at `/data`. `cat /data/marker.txt` inside `reader` must show the text.
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete pod writer reader --ignore-not-found --wait=false >/dev/null; $K -n %{ns} delete pvc %{pvc} --ignore-not-found >/dev/null""",
"checks": [
C("PVC %{pvc} Bound with the default StorageClass", 1, r"""eq "$(j -n %{ns} pvc %{pvc} '{.status.phase}')" Bound && [ -n "$(j -n %{ns} pvc %{pvc} '{.spec.storageClassName}')" ]"""),
C("writer completed (Succeeded)", 1, r"""eq "$(j -n %{ns} pod writer '{.status.phase}')" Succeeded"""),
C("reader sees the marker written by writer", 2, r"""eq "$($K -n %{ns} exec reader -- cat /data/marker.txt 2>/dev/null | tr -d '[:space:]')" %{text}"""),
],
},
# -------------------------------------------------------------- TSHOOT ----
{
"id": 37, "domain": "TSHOOT", "breaks": "coredns", "title": "Cluster DNS is broken", "disruptive": True,
"params": lambda r: {"bad": r.choice(["forward . 10.255.255.1", "forward . 192.0.2.53"])},
"task": """
Pods across the cluster can no longer resolve names (in-cluster or external).
Find the cause in CoreDNS and fix it. `nslookup kubernetes.default` from any pod must work
again, and CoreDNS must be healthy.
""",
"setup": r"""
$K -n kube-system get cm coredns -o jsonpath='{.data.Corefile}' > /tmp/Corefile.orig
sed -i 's|forward \. /etc/resolv.conf|%{bad}|' /tmp/Corefile.orig
$K -n kube-system create cm coredns --from-file=Corefile=/tmp/Corefile.orig --dry-run=client -o yaml | $K apply -f - >/dev/null
$K -n kube-system rollout restart deploy coredns >/dev/null
""",
"checks": [
C("CoreDNS Corefile forwards to the node resolver again", 2, r"""j -n kube-system cm coredns '{.data.Corefile}' | grep -q 'forward \. /etc/resolv.conf'"""),
C("CoreDNS pods available", 1, r"""[ "$(j -n kube-system deploy coredns '{.status.availableReplicas}')" -ge 1 ]"""),
C("in-cluster and external names resolve from a pod", 3, r"""
$K -n default delete pod dns-probe --ignore-not-found --wait=true >/dev/null 2>&1
$K -n default run dns-probe --image=busybox:1.36 --restart=Never -- sh -c 'for i in 1 2 3 4 5 6; do nslookup kubernetes.default >/dev/null 2>&1 && nslookup kubernetes.io >/dev/null 2>&1 && echo OK && exit 0; sleep 5; done; exit 1' >/dev/null 2>&1
for i in $(seq 1 20); do ph=$(j -n default pod dns-probe '{.status.phase}'); [ "$ph" = Succeeded ] && { $K -n default delete pod dns-probe --wait=false >/dev/null; exit 0; }; [ "$ph" = Failed ] && break; sleep 3; done
$K -n default delete pod dns-probe --wait=false >/dev/null; exit 1"""),
],
},
{
"id": 38, "domain": "TSHOOT", "breaks": "controller-manager", "title": "Deployments never create pods", "disruptive": True,
"task": """
Creating a Deployment results in **no** ReplicaSet or pods, and nothing is reported in
`kubectl describe deploy`. Existing pods still run.
Find the failing control-plane component, repair it on the control-plane node
(`{SHELL_CP}`) and make sure new Deployments get their pods.
""",
"setup": r"""
nx "$CP" '
f=/etc/kubernetes/manifests/kube-controller-manager.yaml
grep -q "^ *- kube-controller-manager$" $f && sed -i "s|^\( *\)- kube-controller-manager$|\1- kube-controller-manager\n\1- --leader-elect-lease-duration=1s|" $f
'
""",
"checks": [
C("kube-controller-manager manifest repaired and pod Running", 3, r"""! nx "$CP" 'grep -q "leader-elect-lease-duration=1s" /etc/kubernetes/manifests/kube-controller-manager.yaml' && [ "$(j -n kube-system pod -l component=kube-controller-manager '{.items[0].status.phase}')" = Running ]"""),
C("a new Deployment gets a ReplicaSet and running pods", 4, r"""
$K -n default delete deploy cm-probe --ignore-not-found --wait=true >/dev/null 2>&1
$K -n default create deploy cm-probe --image=busybox:1.36 -- sleep 300 >/dev/null 2>&1
ok=1; for i in $(seq 1 24); do [ "$(j -n default deploy cm-probe '{.status.availableReplicas}')" = 1 ] && { ok=0; break; }; sleep 5; done
$K -n default delete deploy cm-probe --wait=false >/dev/null 2>&1; exit $ok"""),
],
},
{
"id": 39, "domain": "TSHOOT", "breaks": "w1-kubelet", "title": "kubelet misconfiguration", "disruptive": True,
"task": """
Node `{W1}` is `NotReady`. Someone "tuned" the kubelet on it last night.
Bring the node back to `Ready`; the kubelet must be running and enabled.
Reach the node with `{SHELL_W1}`. `journalctl -u kubelet` is your friend.
""",
"setup": r"""
nx "$W1" '
cp -n /var/lib/kubelet/config.yaml /var/lib/kubelet/config.yaml.orig 2>/dev/null
sed -i "s|clientCAFile: /etc/kubernetes/pki/ca.crt|clientCAFile: /etc/kubernetes/pki/ca-bundle.crt|" /var/lib/kubelet/config.yaml
systemctl restart kubelet
'
""",
"checks": [
C("kubelet config points at a CA file that exists", 2, r"""f=$(nx "$W1" 'grep clientCAFile /var/lib/kubelet/config.yaml' | awk '{print $2}'); [ -n "$f" ] && nx "$W1" "test -f $f" """),
C("node Ready", 4, r"""for i in $(seq 1 12); do [ "$(j node "$W1" '{.status.conditions[?(@.type=="Ready")].status}')" = True ] && exit 0; sleep 5; done; exit 1"""),
C("kubelet active and enabled", 1, r"""eq "$(nx "$W1" 'systemctl is-active kubelet')" active && eq "$(nx "$W1" 'systemctl is-enabled kubelet')" enabled"""),
],
},
]