Task ids used to jump 1-43 / 101-124 / 401-418 / 501-517. They now run
contiguously in the same order:
1-43 unchanged (base and extra)
44-67 hard pool (was 101-124)
68-85 Simulator Set C (was 401-418)
86-102 Simulator Set D (was 501-517)
Every reference moved with its task: ids, solution keys, workspace paths
({WS}/qNN and the graders' hidden .qNN-* files), temp paths, the one
hardcoded id in code (Q56's derived canary weight), and Q-number
cross-references in the harness, README and the explainer. Each task's
paths were checked to carry only its own new number. History entries and
the local session state were remapped so past attempts stay attached to the
same tasks; retired set A/B ids keep their old numbers and match nothing.
weak-spots is regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhxm4rsUfuE5HoytYbu2YW
669 lines
38 KiB
Python
669 lines
38 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
|
|
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=<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/.*://'); 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 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} 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 <<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
|
|
# 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 <<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
|
|
# 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"""),
|
|
],
|
|
},
|
|
]
|