"""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 </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 ct=$(j -n kube-system secret bootstrap-token-$id '{.metadata.creationTimestamp}'); [ -n "$ct" ] || exit 1 secs=$(( $(date -d "$exp" +%s) - $(date -d "$ct" +%s) )); want=$(( $(echo %{ttl} | tr -d h) * 3600 )) [ $secs -ge $((want - 120)) ] && [ $secs -le $((want + 120)) ] && [ "$(date -d "$exp" +%s)" -gt "$(date +%s)" ]"""), ], }, { "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= etcd= kubelet= runtime= ``` 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/.*://'); ev=${etcd%-*} rt=$(j node %{node} '{.status.nodeInfo.containerRuntimeVersion}') grep -qxE "etcd=v?($etcd|$ev)" "$f" && grep -qx "runtime=$rt" "$f" """), ], }, # ---------------------------------------------------------------- WORK ---- { "id": 23, "domain": "WORK", "title": "metrics-server repair and HPA", "breaks": "metrics-server", "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 "$METRICS_SERVER_URL" | $K apply -f - >/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} delete deploy payments --ignore-not-found --wait=true >/dev/null 2>&1 $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""" for t in $(seq 1 15); do # the old ReplicaSet's pods linger for a moment while they terminate ok=1; eq "$(j -n %{ns} deploy payments '{.status.availableReplicas}')" 2 || ok=0 for p in $(j -n %{ns} pod -l app=payments '{.items[*].spec.priority}'); do eq "$p" %{val} || ok=0; done [ $ok = 1 ] && exit 0; sleep 2 done; exit 1"""), ], }, { "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", # needs every node schedulable: Q25/Q50 taint W1 and must keep the taint "breaks": ["w1-taint"], "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"""$K -n %{ns} get ds %{ds} -o json 2>/dev/null | python3 -c ' import sys,json; t=json.load(sys.stdin)["spec"]["template"]["spec"].get("tolerations") or [] sys.exit(0 if any("control-plane" in (x.get("key") or "") or (x.get("operator")=="Exists" and not x.get("key")) for x in t) else 1)'"""), 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 </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 # these fail harmlessly when the Gateway API CRDs are not installed yet - installing them is step 1 $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 || true """, "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 not in 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 pod 0's stable name `%{sts}-0.%{sts}.%{ns}.svc.cluster.local` and write the full nslookup output to `{WS}/q43/pod0.txt`. (busybox's nslookup applies no search domains, so a short name such as `%{sts}-0.%{sts}` fails even when DNS is healthy.) """, "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 < %{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 </dev/null # every PV that exists now; the "no extra PV" check compares against this, so Retain leftovers from # earlier sessions (this task's own, Q15's pv-data, Set C's) can no longer fail it $K get pv -o jsonpath='{.items[*].metadata.name}' > "$WS/.q35-pvs" """, "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""" before=" $(cat "$WS/.q35-pvs" 2>/dev/null) "; [ "$before" != " " ] || exit 1 for pv in $($K get pv -o jsonpath='{range .items[?(@.spec.storageClassName=="manual")]}{.metadata.name}{" "}{end}' 2>/dev/null); do case "$before" in *" $pv "*) ;; *) exit 1 ;; esac done"""), ], }, { "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.svc.cluster.local` and `nslookup kubernetes.io` from any pod must work again, and CoreDNS must be healthy. (Use the FQDN: busybox's nslookup does not apply the pod's `search` domains.) """, "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.svc.cluster.local >/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"""), ], }, ]