feat(ckad): cover the whole curriculum - 37 tasks, 16-task mocks

The CKAD track was a 12-task starter bank whose quota (3/3/2/2/2) drew the
whole bank, so every mock was the same 12 tasks reshuffled, and about half
of the curriculum had no task at all.

Tasks 13-37 (tracks/ckad/bank_extra.py) give every missing competency at
least one task: build and export an image with podman (OCI archive), Job
completions and parallelism, a DaemonSet on every node, a native sidecar
that lets a Job finish, a PVC and an in-memory emptyDir, a canary by replica
ratio, rolling-update limits, a Kustomize overlay, a Helm values file and an
upgrade that keeps values, manifests on removed APIs, kubectl top, logs of a
crashing container, two stacked faults, OOMKilled, a custom resource, RBAC
for a ServiceAccount, Pod Security admission, ResourceQuota with LimitRange
defaults, Guaranteed QoS, ConfigMaps, Secrets, ServiceAccount tokens, an
egress NetworkPolicy tested with real traffic, a Service with two faults and
an Ingress with TLS.

A mock now draws 16 tasks weighted like the exam (DESIGN 3, DEPLOY 3, OBS 3,
ENV 4, NET 3) from a pool of 8/7/7/10/5. New ensure_metrics_server helper so
`kubectl top` works on a freshly built cluster. Starter tasks now delete
their Pods with --wait=true, so a re-seed cannot apply onto a Pod that is
still terminating, and setups that create bare Pods fail loudly if one is
missing. README gains a CKAD section.

Verified on a throwaway cluster: all 12 starter and 25 new tasks score 0
untouched and full when solved; four full 16-task mocks that together
include all 37 tasks scored 122/122, 118/118, 124/124 and 119/119.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mhxm4rsUfuE5HoytYbu2YW
This commit is contained in:
vumast3r 2026-09-24 12:39:12 -06:00
parent 1cd963e8b5
commit e6ee219dfc
4 changed files with 1645 additions and 11 deletions

View File

@ -5,8 +5,9 @@ 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 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 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 + 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 and CKS (scaffolded — 24 hard, plus two fixed simulator sets, C and D, of 18 and 17 tasks), **CKAD** (37 tasks, at
same format, add questions when you get there). least one for every competency in the curriculum) and CKS (scaffolded — same format, add
questions when you get there).
## How a session is built ## How a session is built
@ -73,10 +74,25 @@ The same state is available from the terminal if you prefer:
./exam list | show N | timer | status # during ./exam list | show N | timer | status # during
./exam end # submit ./exam end # submit
./exam weak | history # afterwards ./exam weak | history # afterwards
./exam --track ckad web # other tracks ./exam --track ckad web # other tracks (or CKA_TRACK=ckad)
./teardown.sh # delete cluster + state (history kept) ./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 ## Scoring, history, drills
Each task is a list of named, weighted checks (`tracks/cka/questions.py`). You get the points Each task is a list of named, weighted checks (`tracks/cka/questions.py`). You get the points
@ -158,5 +174,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'`, 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}`, 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 `{W3}`, `{SHELL_CP}` etc. `TRACK["quota"]` / `["quota_hard"]` set how many tasks per domain a
session draws. For CKAD/CKS, fill in `QUESTIONS` in the session draws. For CKS, fill in `QUESTIONS` in the
existing placeholder track; `TRACK` already carries the domain table and pass mark. existing placeholder track; `TRACK` already carries the domain table and pass mark.

14
exam
View File

@ -136,6 +136,20 @@ ensure_gateway_crds() {
&& $K wait --for=condition=Established --timeout=60s crd/gatewayclasses.gateway.networking.k8s.io \ && $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 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): def _pick_track_arg(argv):

1595
tracks/ckad/bank_extra.py Normal file

File diff suppressed because it is too large Load Diff

View File

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