diff --git a/.gitignore b/.gitignore index 6fc0e56..e02c0d6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,9 @@ __pycache__/ .DS_Store # workspace/ is where task setup scripts write scratch files for whatever question you're -# currently drilling - regenerated (and wiped) every time that question is re-seeded, so it -# doesn't belong in the repo. q03 and q21 are the two exceptions already tracked as seed -# templates. -/workspace/* -!/workspace/q03 -!/workspace/q21 +# currently drilling - regenerated (and wiped) every time that question is re-seeded, so none +# of it belongs in the repo. q03 and q21 used to be excepted as "seed templates", but both +# setups start with `rm -rf "$WS/qNN"` and rewrite every file from heredocs, so they are +# generated output too: tracking them only produced phantom diffs (a re-seed reverted any edit +# to base/ and deleted any answer left in overlays/). +/workspace/ diff --git a/exam b/exam index add7986..ec7a903 100755 --- a/exam +++ b/exam @@ -207,7 +207,16 @@ def q_by_id(n: int) -> dict[str, Any]: for q in QUESTIONS: if q["id"] == n: return q - die(f"No question {n}. Valid: 1-{len(QUESTIONS)}") + # ids are not contiguous (1-43 base, 101-124 hard, 201-217 / 301-317 sim sets), so print the + # real ranges rather than "1-", which named ids that do not exist. + ids = sorted(q["id"] for q in QUESTIONS) + ranges, start, prev = [], ids[0], ids[0] + for i in ids[1:] + [None]: + if i != prev + 1: + ranges.append(str(start) if start == prev else f"{start}-{prev}") + start = i + prev = i + die(f"No question {n}. Valid: {', '.join(ranges)}") return {} def render(text, params): @@ -552,6 +561,15 @@ def _log_msg(*args: Any, **kwargs: Any) -> None: # restores every known "breaks" target to a good baseline before each session seeds, so # leftover damage never leaks across sessions regardless of which questions get sampled. RESTORE_BASELINE = r""" +# apiserver FIRST: Q123 renames the command to "kube-apiserverr", which takes the whole API +# down - every kubectl-based step below would fail until this is undone. The repair goes over +# podman/ssh (nx), not kubectl, so it works precisely when the cluster is unreachable. +if nx "$CP" 'grep -q "^ *- kube-apiserverr$" /etc/kubernetes/manifests/kube-apiserver.yaml' /dev/null; then + nx "$CP" 'sed -i "s|^\( *\)- kube-apiserverr$|\1- kube-apiserver|" /etc/kubernetes/manifests/kube-apiserver.yaml' /dev/null 2>&1 && break; sleep 2; done +fi + # scheduler: undo the "kube-scheduler" -> "kube-schedulerr" command typo (Q20/Q118) nx "$CP" ' f=/etc/kubernetes/manifests/kube-scheduler.yaml @@ -573,10 +591,19 @@ if [ -n "$cf" ] && ! printf '%s' "$cf" | grep -Eq '^[[:space:]]*forward \. /etc/ $K -n kube-system rollout restart deploy coredns >/dev/null 2>&1 fi -# w1/w2/w3 kubelet: undo Q17/Q39-style tampering, then make sure it's enabled and running +# w1/w2/w3 kubelet: undo Q17/Q39-style tampering, then make sure it's enabled and running. +# staticPodPath is normalised unconditionally: Q103 relocates it to /etc/kubernetes/static, and +# the config.yaml.orig copy below only helps if something made that backup first - nothing does, +# so a relocated path used to survive every later session and silently break Q05 (whose check +# looks in /etc/kubernetes/manifests). Only restart the kubelet if we actually changed the file. for n in "$W1" "$W2" "$W3"; do nx "$n" ' [ -f /var/lib/kubelet/config.yaml.orig ] && cp -f /var/lib/kubelet/config.yaml.orig /var/lib/kubelet/config.yaml + if grep -q "^staticPodPath:" /var/lib/kubelet/config.yaml 2>/dev/null \ + && ! grep -q "^staticPodPath: /etc/kubernetes/manifests$" /var/lib/kubelet/config.yaml; then + sed -i "s|^staticPodPath: .*|staticPodPath: /etc/kubernetes/manifests|" /var/lib/kubelet/config.yaml + systemctl restart kubelet >/dev/null 2>&1 + fi systemctl enable kubelet >/dev/null 2>&1 systemctl start kubelet >/dev/null 2>&1 ' /dev/nu # (Q26/Q211 DaemonSet coverage, Q212's one-pod-per-node spread). for n in "$W1" "$W2" "$W3"; do $K uncordon "$n" >/dev/null 2>&1 || true; done -# NOTE: etcd/certs/apiserver corruption (hard-mode only: Q102, Q118 apiserver half, Q122, -# Q123) isn't restored here yet - those are rarer and each needs a more bespoke repair. -# Q103's staticPodPath relocation also only reverts if /var/lib/kubelet/config.yaml.orig -# exists, which it does not unless something made that backup first. +# Deliberately NOT restored, and why: +# etcd (Q102) and certs (Q122). Neither setup damages anything - Q102 only takes a snapshot +# and deletes a ConfigMap, Q122 only records the current notBefore. Any damage comes from a +# half-finished candidate attempt (etcd.yaml left pointing at a restored data-dir, static pod +# manifests moved to /tmp and never moved back), which is not deterministic: there is no way +# to tell "mid-attempt" from "correctly completed" without guessing, and guessing wrong here +# rewrites etcd's data-dir or re-issues certs on a healthy cluster. Re-run ./setup.sh (or +# ./teardown.sh && ./setup.sh) if one of those leaves the control plane wedged. +# +# proxy-rules (Q302) needs nothing: its setup only creates a namespace and deletes a pod and +# service. The "breaks" key is there for no_conflicts() bookkeeping, not because it damages +# shared state. """ def restore_baseline(log: Callable[..., Any] = _log_msg): diff --git a/teardown.sh b/teardown.sh index e9c4f1f..81cdc45 100755 --- a/teardown.sh +++ b/teardown.sh @@ -5,5 +5,7 @@ cd "$(dirname "$0")" RT="${CONTAINER_RUNTIME:-$(cat .state/runtime 2>/dev/null || echo podman)}" [ "$RT" = podman ] && export KIND_EXPERIMENTAL_PROVIDER=podman kind delete cluster --name cka-sim || true +# Safe to wipe wholesale: everything under workspace/ is generated by task setup scripts and +# nothing there is tracked (see .gitignore). Anything you want to keep, copy out first. rm -rf .state workspace echo "cka-sim cluster and local state removed." diff --git a/tracks/cka/bank_hard.py b/tracks/cka/bank_hard.py index 77c7b5f..8e6b5ad 100644 --- a/tracks/cka/bank_hard.py +++ b/tracks/cka/bank_hard.py @@ -233,7 +233,7 @@ want=$(nx "$CP" "openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl r got=$(grep -oE '^CA_HASH=sha256:[0-9a-f]{64}' "$WS/q124/node-join.env" 2>/dev/null | cut -d= -f2 | cut -d: -f2) [ -n "$want" ] && eq "$got" "$want" """), C("API_ENDPOINT is the real in-cluster address, not the local kubeconfig's", 2, r""" -want=$($K get endpoints kubernetes -o jsonpath='{.subsets[0].addresses[0].ip}:{.subsets[0].ports[0].port}' 2>/dev/null) +want=$($K get endpointslices -l kubernetes.io/service-name=kubernetes -o jsonpath='{.items[0].endpoints[0].addresses[0]}:{.items[0].ports[0].port}' 2>/dev/null) got=$(grep -oE '^API_ENDPOINT=.*' "$WS/q124/node-join.env" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]') [ -n "$want" ] && eq "$got" "$want" """), ], diff --git a/tracks/cka/solutions.py b/tracks/cka/solutions.py index 5b5b6ce..ea7346e 100644 --- a/tracks/cka/solutions.py +++ b/tracks/cka/solutions.py @@ -456,7 +456,7 @@ openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \\ # kind maps the API server to 127.0.0.1: for YOUR machine only; a node # joining over the cluster network needs the address the apiserver actually advertises. exit -kubectl get endpoints kubernetes -o jsonpath='{.subsets[0].addresses[0].ip}:{.subsets[0].ports[0].port}{"\\n"}' +kubectl get endpointslices -l kubernetes.io/service-name=kubernetes -o jsonpath='{.items[0].endpoints[0].addresses[0]}:{.items[0].ports[0].port}{"\\n"}' # Step 4: Assemble the handoff file cat > {WS}/q124/node-join.env <