Compare commits

..

No commits in common. "e6ee219dfcc26f23121285bd9dc29900dcb8ef4c" and "f4f2a6bea99c7d593f61de58049d15af1d40a232" have entirely different histories.

5 changed files with 21 additions and 1670 deletions

View File

@ -5,9 +5,8 @@ Entry point is `./exam` (renamed from `cka-sim` so it can't collide with the dir
Hands-on exam tasks graded **per check** against a real cluster, an exam clock, a browser
front end modelled on the real PSI exam, attempt history, and drills that rotate through the
domains before you sit a full exam. Tracks: **CKA** (102 tasks: a random pool of 43 normal +
24 hard, plus two fixed simulator sets, C and D, of 18 and 17 tasks), **CKAD** (37 tasks, at
least one for every competency in the curriculum) and CKS (scaffolded — same format, add
questions when you get there).
24 hard, plus two fixed simulator sets, C and D, of 18 and 17 tasks), CKAD and CKS (scaffolded —
same format, add questions when you get there).
## How a session is built
@ -74,25 +73,10 @@ The same state is available from the terminal if you prefer:
./exam list | show N | timer | status # during
./exam end # submit
./exam weak | history # afterwards
./exam --track ckad web # other tracks (or CKA_TRACK=ckad)
./exam --track ckad web # other tracks
./teardown.sh # delete cluster + state (history kept)
```
## CKAD track
`./exam --track ckad ...` (every command takes it). A mock draws **16** tasks in 120 minutes,
weighted like the exam: Design and Build 3, Deployment 3, Observability 3, Environment and
Security 4, Networking 3, from a pool of 37 (8/7/7/10/5), so no two mocks are the same set. Every
line of the curriculum has at least one task: image builds, Jobs, DaemonSets, native sidecars,
volumes; blue/green, canary, rolling-update limits, Helm, Kustomize; API deprecations, probes,
`kubectl top`, logs, debugging (CrashLoopBackOff, OOMKilled, config errors); CRDs, RBAC, Pod
Security admission, quotas and LimitRanges, QoS, ConfigMaps, Secrets, ServiceAccounts;
NetworkPolicy (tested with real traffic), Services, Ingress with TLS.
A few tasks need more than kubectl on this machine: `podman` (Q13 builds an image), `helm`
(Q6, Q21), and network access the first time Q23 runs on a fresh cluster (it installs
metrics-server). Drill a domain with `./exam --track ckad drill ENV`, as for CKA.
## Scoring, history, drills
Each task is a list of named, weighted checks (`tracks/cka/questions.py`). You get the points
@ -174,5 +158,5 @@ Banks live in `tracks/cka/{questions,bank_extra,bank_hard,bank_sim_*}.py`; a tas
Scripts get `$K` (pinned to the grading context), `$WS`, `$CP/$W1/$W2/$W3`, `nx NODE 'cmd'`,
and helpers `j <get args> '{jsonpath}'` and `eq a b`. Task text may use `{WS}`, `{CP}`, `{W2}`,
`{W3}`, `{SHELL_CP}` etc. `TRACK["quota"]` / `["quota_hard"]` set how many tasks per domain a
session draws. For CKS, fill in `QUESTIONS` in the
session draws. For CKAD/CKS, fill in `QUESTIONS` in the
existing placeholder track; `TRACK` already carries the domain table and pass mark.

43
exam
View File

@ -136,20 +136,6 @@ ensure_gateway_crds() {
&& $K wait --for=condition=Established --timeout=60s crd/gatewayclasses.gateway.networking.k8s.io \
crd/gateways.gateway.networking.k8s.io crd/httproutes.gateway.networking.k8s.io >/dev/null 2>&1
}
# ensure_metrics_server -> a working metrics API for `kubectl top` (CKAD Q23). kind ships none, and
# kind's kubelet serving certs have no IP SANs, so it only becomes Ready with --kubelet-insecure-tls.
# CKA Q23 strips that flag on purpose; the baseline puts it back before every session.
ensure_metrics_server() {
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 2>&1 || return 1
fi
if ! $K -n kube-system get deploy metrics-server -o jsonpath='{.spec.template.spec.containers[0].args}' | grep -q kubelet-insecure-tls; then
$K -n kube-system patch deploy metrics-server --type=json \
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' >/dev/null 2>&1
fi
$K -n kube-system rollout status deploy metrics-server --timeout=150s >/dev/null 2>&1
}
"""
def _pick_track_arg(argv):
@ -658,22 +644,11 @@ 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"""
# Static-pod manifests are only ever rewritten when an edit actually changes them, and through a
# hidden temp file the kubelet ignores. `sed -i` writes its temp file (sedXXXXXX) into the SAME
# directory, the kubelet loads that as one more static pod and kills the running one: every session
# start used to knock the scheduler and controller-manager into CrashLoopBackOff for up to minutes
# while the session's tasks were being seeded (new Namespaces got no ServiceAccount, so bare Pods
# were rejected; Pods sat unscheduled). The user's cluster had 10 such restarts by 2026-09-24.
MEDIT='medit() { f=$1; shift; t=$(mktemp "${f%/*}/.medit.XXXXXX") || return 1
if sed "$@" "$f" > "$t" 2>/dev/null && [ "$(md5sum < "$t")" != "$(md5sum < "$f")" ]; then
chmod --reference="$f" "$t" 2>/dev/null; mv -f "$t" "$f"; else rm -f "$t"; fi; }'
# apiserver FIRST: Q66 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 2>/dev/null; then
nx "$CP" "$MEDIT"'
medit /etc/kubernetes/manifests/kube-apiserver.yaml -e "s|^\( *\)- kube-apiserverr$|\1- kube-apiserver|"' </dev/null
nx "$CP" 'sed -i "s|^\( *\)- kube-apiserverr$|\1- kube-apiserver|" /etc/kubernetes/manifests/kube-apiserver.yaml' </dev/null
# the kubelet re-reads the manifest on its own; wait for the API to answer again
for _ in $(seq 1 30); do kubectl --context "${CKA_CONTEXT:-kind-cka-sim}" get --raw=/readyz >/dev/null 2>&1 && break; sleep 2; done
fi
@ -681,22 +656,22 @@ fi
# scheduler: undo the "kube-scheduler" -> "kube-schedulerr" command typo (Q20/Q61), and put the
# manifest back if a task had the candidate stop the scheduler by moving it out (Q97) and they
# never moved it back - that setup keeps a pristine copy outside the static pod directory.
nx "$CP" "$MEDIT"'
nx "$CP" '
f=/etc/kubernetes/manifests/kube-scheduler.yaml
[ -f $f ] || { [ -f /etc/kubernetes/.cka-sim/kube-scheduler.yaml ] && cp /etc/kubernetes/.cka-sim/kube-scheduler.yaml $f; }
medit $f -e "s|^\( *\)- kube-schedulerr$|\1- kube-scheduler|"
sed -i "s|^\( *\)- kube-schedulerr$|\1- kube-scheduler|" $f 2>/dev/null
' </dev/null
# controller-manager: undo Q38's injected flag and Q61's broken image tag. The last two lines
# undo the retired set B's Q317 (the CSR signing controller switched off in --controllers, or in
# its older form --cluster-signing-cert-file pointed at a missing file, which crash-looped the WHOLE
# controller-manager); a cluster that ever ran that set may still carry either.
nx "$CP" "$MEDIT"'
medit /etc/kubernetes/manifests/kube-controller-manager.yaml \
-e "/--leader-elect-lease-duration=1s/d" \
-e "s|\(image: registry.k8s.io/kube-controller-manager:v[0-9.]*\)-broken|\1|" \
-e "s|--cluster-signing-cert-file=/etc/kubernetes/pki/ca-signing.crt|--cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt|" \
-e "s|,-certificatesigningrequest-signing-controller||; s|,-csrsigning||"
nx "$CP" '
f=/etc/kubernetes/manifests/kube-controller-manager.yaml
sed -i "/--leader-elect-lease-duration=1s/d" $f 2>/dev/null
sed -i "s|\(image: registry.k8s.io/kube-controller-manager:v[0-9.]*\)-broken|\1|" $f 2>/dev/null
sed -i "s|--cluster-signing-cert-file=/etc/kubernetes/pki/ca-signing.crt|--cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt|" $f 2>/dev/null
sed -i "s|,-certificatesigningrequest-signing-controller||; s|,-csrsigning||" $f 2>/dev/null
' </dev/null
# coredns: restore the default upstream forward (Q37/Q57 point it somewhere bogus)

View File

@ -8,11 +8,7 @@ mkns() {
# error looking up service account <ns>/default: serviceaccount "default" not found
# Setups that create a namespace and immediately apply Pods lose that race under load,
# which fails the seed and costs the candidate a whole task. It belongs here, not in each task.
# 90 s, not 30: the ServiceAccount controller runs in kube-controller-manager, and a manager that
# is restarting (it takes ~60 s to be back and leading) left bare Pods rejected with no trace.
for _ in $(seq 1 90); do $K -n "$1" get sa default >/dev/null 2>&1 && return 0; sleep 1; done
echo "namespace $1 still has no default ServiceAccount after 90 s - is kube-controller-manager running?" >&2
return 1
for _ in $(seq 1 30); do $K -n "$1" get sa default >/dev/null 2>&1 && return 0; sleep 1; done
}
"""

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
"""CKAD track: 37 tasks, at least one per competency in the CKAD curriculum (1-12 here, 13-37 in
bank_extra.py). Same format as the CKA banks.
"""CKAD track. Starter bank - same format as the CKA banks, extend it the same way.
CKAD is a build-things exam: less cluster surgery, more "produce the right object, fast".
The clock is the adversary, so these are mostly single-object tasks graded exactingly.
@ -18,10 +17,8 @@ TRACK = {
"ENV": "Application Environment, Configuration and Security (25%)",
"NET": "Services and Networking (20%)",
},
# 16 tasks in 120 minutes, weighted like the exam (20/20/15/25/20 %). The pool per domain is
# 8/7/7/10/5, so no two mocks are the same set.
"quota": {"DESIGN": 3, "DEPLOY": 3, "OBS": 3, "ENV": 4, "NET": 3},
"quota_hard": {"DESIGN": 3, "DEPLOY": 3, "OBS": 3, "ENV": 4, "NET": 3},
"quota": {"DESIGN": 3, "DEPLOY": 3, "OBS": 2, "ENV": 2, "NET": 2},
"quota_hard": {"DESIGN": 3, "DEPLOY": 3, "OBS": 2, "ENV": 2, "NET": 2},
}
QUESTIONS = [
@ -38,7 +35,7 @@ mounted at `/spool` in both:
`kubectl logs %{pod} -c adapter` must show the writer's lines.
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=true --grace-period=1 >/dev/null""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null""",
"checks": [
C("pod Running with containers writer and adapter", 2, r"""
p="$K -n %{ns} get pod %{pod}"
@ -89,7 +86,7 @@ connection instead.
""",
"setup": NS + r"""
mkns %{ns}
$K -n %{ns} delete pod %{pod} --ignore-not-found --wait=true --grace-period=1 >/dev/null
$K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null
$K -n %{ns} delete deploy backend --ignore-not-found >/dev/null
$K apply -f - >/dev/null <<EOF
apiVersion: v1
@ -366,7 +363,6 @@ spec:
image: busybox:1.99-doesnotexist
command: [sh, -c, "echo working; sleep 86400"]
EOF
$K -n %{ns} get pod %{pod} >/dev/null
""",
"checks": [
C("reason.txt records the image pull failure", 3, r"""grep -Eqi 'ImagePull|ErrImagePull|ImagePullBackOff' "$WS/q8/reason.txt" 2>/dev/null"""),
@ -398,7 +394,7 @@ Create Pod `%{pod}` in `%{ns}` (image `busybox:1.36`, command `sleep 86400`) tha
- drops **all** capabilities and adds back only `NET_BIND_SERVICE`,
- cannot gain new privileges.
""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=true --grace-period=1 >/dev/null""",
"setup": NS + r"""mkns %{ns}; $K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null""",
"checks": [
C("runs as UID %{uid} with runAsNonRoot", 3, r"""
p="$K -n %{ns} get pod %{pod}"
@ -448,7 +444,7 @@ In `%{ns}`:
""",
"setup": NS + r"""
mkns %{ns}
$K -n %{ns} delete pod %{pod} --ignore-not-found --wait=true --grace-period=1 >/dev/null
$K -n %{ns} delete pod %{pod} --ignore-not-found --wait=false >/dev/null
$K -n %{ns} delete cm app-cfg --ignore-not-found >/dev/null; $K -n %{ns} delete secret app-sec --ignore-not-found >/dev/null
""",
"checks": [
@ -577,8 +573,3 @@ EOF
""",
},
]
# 13-37: the rest of the curriculum, one or more tasks per competency the starter bank left out.
from tracks.ckad import bank_extra # noqa: E402
QUESTIONS = QUESTIONS + bank_extra.QUESTIONS