refactor: retire simulator sets A and B; C and D are the fixed sets

Removes tracks/cka/bank_sim_a.py and bank_sim_b.py (34 tasks, ids 201-217 and
301-317). The previous commit holds their last versions, with every fix from
the audit applied and both sets verified end to end - restore with
`git show HEAD~1:tracks/cka/bank_sim_a.py` if one is ever wanted back.

The baseline keeps repairing what those sets could leave on a cluster that
ran them (the maintenance taint, cordons, Q317's controller flag, Q314's static
pod) - only its comments changed. `breaks` keys that existed solely to keep
pool tasks apart from set A/B tasks are gone. History keeps its raw entries;
weak-spots is regenerated without the retired tasks. A mistyped drill argument
now gets a usage message instead of a traceback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vumast3r 2026-09-22 22:02:21 -06:00
parent 55ab18f814
commit 228e4f790b
11 changed files with 59 additions and 2204 deletions

View File

@ -4,9 +4,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** (136 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 four fixed simulator sets A–D of 17–18 tasks each), CKAD and CKS (scaffolded — same 24 hard, plus two fixed simulator sets, C and D, of 18 and 17 tasks), CKAD and CKS (scaffolded —
format, add questions when you get there). same format, add questions when you get there).
## How a session is built ## How a session is built
@ -68,7 +68,7 @@ The same state is available from the terminal if you prefer:
./exam drill # next least-drilled domain, 5 tasks (add --hard) ./exam drill # next least-drilled domain, 5 tasks (add --hard)
./exam drill weak | drill 2 17 20 | drill TSHOOT NET | drill setd ./exam drill weak | drill 2 17 20 | drill TSHOOT NET | drill setd
./exam start [--hard] # full exam (gated until all domains drilled; --force) ./exam start [--hard] # full exam (gated until all domains drilled; --force)
./exam sim a|b|c|d # a fixed 17/18-task simulator set, exam rules, 120 min ./exam sim c|d # a fixed 18/17-task simulator set, exam rules, 120 min
./exam study [DOMAIN|weak|N..] # no clock, solutions visible, nothing recorded ./exam study [DOMAIN|weak|N..] # no clock, solutions visible, nothing recorded
./exam list | show N | timer | status # during ./exam list | show N | timer | status # during
./exam end # submit ./exam end # submit
@ -133,12 +133,12 @@ Before every session a baseline pass repairs anything a previous, unfinished ses
The kindnet CNI here **enforces** NetworkPolicy (it embeds a policy engine), so a wrong policy The kindnet CNI here **enforces** NetworkPolicy (it embeds a policy engine), so a wrong policy
really drops traffic; no ingress or Gateway controller runs, so those are graded on the objects. really drops traffic; no ingress or Gateway controller runs, so those are graded on the objects.
**Simulator sets** (`./exam sim a|b|c|d`, never drawn into random sessions): A and B are **Simulator sets** (`./exam sim c|d`, never drawn into random sessions): C is modelled on an
killer.sh-grade investigate-and-write-to-file sets, C is modelled on an 18-question commercial 18-question commercial mock sitting, and D is killer.sh-style — control-plane inventory, event
mock sitting, and D is killer.sh-style — control-plane inventory, event forensics with crictl, forensics with crictl, a broken kubelet unit, kubelet certificates, manual scheduling with the
a broken kubelet unit, kubelet certificates, manual scheduling with the scheduler stopped, Helm scheduler stopped, Helm housekeeping with a stuck release, and an enforced AND-vs-OR
housekeeping with a stuck release, and an enforced AND-vs-OR NetworkPolicy. All are original NetworkPolicy. Both are original scenarios, not copies of any commercial simulator. (Sets A and B
scenarios, not copies of any commercial simulator. were retired in favour of these two; their last versions are in git history.)
## Adding tasks or a new track ## Adding tasks or a new track

40
exam
View File

@ -212,8 +212,8 @@ def q_by_id(n: int) -> dict[str, Any]:
for q in QUESTIONS: for q in QUESTIONS:
if q["id"] == n: if q["id"] == n:
return q return q
# ids are not contiguous (1-43 base, 101-124 hard, 201-217 / 301-317 / 401-418 / 501-517 sim # ids are not contiguous (1-43 base, 101-124 hard, 401-418 / 501-517 sim sets), so print the
# sets), so print the real ranges rather than "1-<count>", which named ids that do not exist. # real ranges rather than "1-<count>", which named ids that do not exist.
ids = sorted(q["id"] for q in QUESTIONS) ids = sorted(q["id"] for q in QUESTIONS)
ranges, start, prev = [], ids[0], ids[0] ranges, start, prev = [], ids[0], ids[0]
for i in ids[1:] + [None]: for i in ids[1:] + [None]:
@ -375,7 +375,7 @@ def drilled_domains():
def breaks_of(q): def breaks_of(q):
"""A task's "breaks" as a set. It is a string or a list: Q118 damages the scheduler AND the """A task's "breaks" as a set. It is a string or a list: Q118 damages the scheduler AND the
controller-manager, and declaring only one let it share a session with Q38/Q317.""" controller-manager, and declaring only one let it share a session with Q38."""
b = q.get("breaks") b = q.get("breaks")
return set() if not b else ({b} if isinstance(b, str) else set(b)) return set() if not b else ({b} if isinstance(b, str) else set(b))
@ -653,11 +653,10 @@ f=/etc/kubernetes/manifests/kube-scheduler.yaml
sed -i "s|^\( *\)- kube-schedulerr$|\1- kube-scheduler|" $f 2>/dev/null sed -i "s|^\( *\)- kube-schedulerr$|\1- kube-scheduler|" $f 2>/dev/null
' </dev/null ' </dev/null
# controller-manager: undo Q38's injected flag, Q118's broken image tag and Q317's sabotage - both # controller-manager: undo Q38's injected flag and Q118's broken image tag. The last two lines
# its current form (the CSR signing controller switched off in --controllers) and the older one # undo the retired set B's Q317 (the CSR signing controller switched off in --controllers, or in
# (--cluster-signing-cert-file pointed at a missing file). That older form crash-looped the WHOLE # its older form --cluster-signing-cert-file pointed at a missing file, which crash-looped the WHOLE
# controller-manager, because a controller that fails to start is fatal there, so every later # controller-manager); a cluster that ever ran that set may still carry either.
# session silently lost deployments, jobs, endpoints and PV phases, not just CSR signing.
nx "$CP" ' nx "$CP" '
f=/etc/kubernetes/manifests/kube-controller-manager.yaml f=/etc/kubernetes/manifests/kube-controller-manager.yaml
sed -i "/--leader-elect-lease-duration=1s/d" $f 2>/dev/null sed -i "/--leader-elect-lease-duration=1s/d" $f 2>/dev/null
@ -711,9 +710,10 @@ for n in "$W1" "$W2" "$W3"; do
' </dev/null ' </dev/null
done done
# static pods the bank drops on nodes: Q05's static-web and Q314's edge-cache on W3 (edge-cache # static pods the bank drops on nodes: Q05's static-web on W3, Q103's relocated copies, Q509's
# sat in ContainerCreating for days, its hostPath deliberately missing), Q103's relocated copies, # ops-beacon on the control plane - and edge-cache, left on W3 by the retired set B (it sat in
# Q509's ops-beacon on the control plane. Each setup recreates what its own session needs. # ContainerCreating for days, its hostPath deliberately missing). Each setup recreates what its
# own session needs.
nx "$W3" 'rm -f /etc/kubernetes/manifests/static-web*.yaml /etc/kubernetes/manifests/edge-cache.yaml /etc/kubernetes/static/*.yaml' </dev/null nx "$W3" 'rm -f /etc/kubernetes/manifests/static-web*.yaml /etc/kubernetes/manifests/edge-cache.yaml /etc/kubernetes/static/*.yaml' </dev/null
nx "$CP" 'rm -f /etc/kubernetes/manifests/ops-beacon*.yaml' </dev/null nx "$CP" 'rm -f /etc/kubernetes/manifests/ops-beacon*.yaml' </dev/null
@ -729,8 +729,8 @@ if $K get sc standard >/dev/null 2>&1; then
fi fi
# w1-taint: clear every randomized demo taint key the bank can apply. Q25 draws from # w1-taint: clear every randomized demo taint key the bank can apply. Q25 draws from
# dedicated/team/workload, Q107 from pool/lane, and Q307 pins maintenance=inprogress - miss one # dedicated/team/workload and Q107 from pool/lane; `maintenance` was the retired set B's and may
# and that node silently stops accepting pods for every later session. # still be on a cluster that ran it. Miss one and that node silently stops accepting pods.
# One key per call: `kubectl taint node N a- b-` aborts on the first key that is not present and # One key per call: `kubectl taint node N a- b-` aborts on the first key that is not present and
# removes NOTHING, so the single taint that is actually set survives the "cleanup" and W1 keeps # removes NOTHING, so the single taint that is actually set survives the "cleanup" and W1 keeps
# rejecting pods in every later session - the exact failure this block exists to prevent. # rejecting pods in every later session - the exact failure this block exists to prevent.
@ -750,9 +750,8 @@ if $K -n kube-system get deploy metrics-server >/dev/null 2>&1 \
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' >/dev/null 2>&1 || true -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' >/dev/null 2>&1 || true
fi fi
# w1-block/w2-drain/w3-drain: Q307 cordons W1 and Q117 cordons W2, and Q208/Q309 have the # cordons: Q117 cordons W2, and a drain task left half-done leaves a node cordoned. A cordon left
# candidate drain W2/W3. A cordon left behind fails any later task that counts pods per node # behind fails any later task that needs every worker (Q26's DaemonSet coverage, Q107's spread).
# (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 for n in "$W1" "$W2" "$W3"; do $K uncordon "$n" >/dev/null 2>&1 || true; done
# Deliberately NOT restored, and why: # Deliberately NOT restored, and why:
@ -763,10 +762,6 @@ for n in "$W1" "$W2" "$W3"; do $K uncordon "$n" >/dev/null 2>&1 || true; done
# to tell "mid-attempt" from "correctly completed" without guessing, and guessing wrong here # 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 # 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. # ./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): def restore_baseline(log: Callable[..., Any] = _log_msg):
@ -904,8 +899,11 @@ def cmd_drill(args):
ids += [q["id"] for q in set_questions(a.lower().replace("set", ""))] ids += [q["id"] for q in set_questions(a.lower().replace("set", ""))]
elif a.upper() in DOMAINS: elif a.upper() in DOMAINS:
ids += [q["id"] for q in QUESTIONS if q["domain"] == a.upper()] ids += [q["id"] for q in QUESTIONS if q["domain"] == a.upper()]
else: elif a.isdigit():
ids.append(q_by_id(int(a))["id"]) ids.append(q_by_id(int(a))["id"])
else:
die(f"'{a}' is not a task id, a domain ({', '.join(DOMAINS)}) or a set "
f"({', '.join('set' + k for k in SETS) or 'none'}).")
ids = sorted(set(ids)) ids = sorted(set(ids))
qs, solo = split_solo([q for q in QUESTIONS if q["id"] in ids]) qs, solo = split_solo([q for q in QUESTIONS if q["id"] in ids])
if solo: if solo:

View File

@ -282,8 +282,8 @@ $K -n %{ns} delete deploy %{dep} --ignore-not-found >/dev/null
}, },
{ {
"id": 26, "domain": "WORK", "title": "DaemonSet on every node", "id": 26, "domain": "WORK", "title": "DaemonSet on every node",
# needs every node schedulable: Q25/Q107 taint W1 and must keep the taint, Q307 blocks it # needs every node schedulable: Q25/Q107 taint W1 and must keep the taint
"breaks": ["w1-taint", "w1-block"], "breaks": ["w1-taint"],
"params": lambda r: {"ns": ns_pick(r, "agents"), "ds": name_pick(r, "node-agent")}, "params": lambda r: {"ns": ns_pick(r, "agents"), "ds": name_pick(r, "node-agent")},
"task": """ "task": """
Create a DaemonSet `%{ds}` in namespace `%{ns}` (image `busybox:1.36`, command `sleep 3600`) Create a DaemonSet `%{ds}` in namespace `%{ns}` (image `busybox:1.36`, command `sleep 3600`)

View File

@ -670,7 +670,7 @@ echo "$pv" > "$WS/.q116-pv"
}, },
# ============================================================== TSHOOT ==== # ============================================================== TSHOOT ====
{ {
"id": 117, "domain": "TSHOOT", "breaks": ["w2-kubelet", "w2-drain"], "difficulty": "hard", "title": "Worker node: three things wrong", "disruptive": True, "id": 117, "domain": "TSHOOT", "breaks": "w2-kubelet", "difficulty": "hard", "title": "Worker node: three things wrong", "disruptive": True,
"params": lambda r: {"ctx": "k8s-c1-H"}, "params": lambda r: {"ctx": "k8s-c1-H"},
"task": CTX + """ "task": CTX + """
Node `{W2}` is unusable. Make it fully usable again: `Ready`, schedulable, kubelet running and Node `{W2}` is unusable. Make it fully usable again: `Ready`, schedulable, kubelet running and

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
"""Simulator Set D - 17 tasks, fixed set, killer.sh-style. """Simulator Set D - 17 tasks, fixed set, killer.sh-style.
Original scenarios, not copies of any commercial simulator. They exercise the techniques the Original scenarios, not copies of any commercial simulator. They exercise the techniques the
killer.sh CKA environment is known for that sets A-C do not already cover: saying how every killer.sh CKA environment is known for that the rest of the bank did not cover: saying how every
control-plane piece is run, cluster facts to an exact file, readiness gated on a Service, control-plane piece is run, cluster facts to an exact file, readiness gated on a Service,
`--sort-by` scripts, event forensics around kube-proxy with crictl, API-resource and RBAC census, `--sort-by` scripts, event forensics around kube-proxy with crictl, API-resource and RBAC census,
a kubelet whose systemd unit points at the wrong binary, Secrets mounted read-only and renamed into a kubelet whose systemd unit points at the wrong binary, Secrets mounted read-only and renamed into

View File

@ -6,7 +6,7 @@ mkns() {
# Wait for the namespace's `default` ServiceAccount before returning. The controller creates it # Wait for the namespace's `default` ServiceAccount before returning. The controller creates it
# asynchronously, and a Pod applied in the gap is rejected outright: # asynchronously, and a Pod applied in the gap is rejected outright:
# error looking up service account <ns>/default: serviceaccount "default" not found # 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 (Q204), # 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. # which fails the seed and costs the candidate a whole task. It belongs here, not in each task.
for _ in $(seq 1 30); do $K -n "$1" get sa default >/dev/null 2>&1 && return 0; sleep 1; done for _ in $(seq 1 30); do $K -n "$1" get sa default >/dev/null 2>&1 && return 0; sleep 1; done
} }

View File

@ -23,8 +23,9 @@ TRACK = {
# tasks per domain drawn into a session (weights follow the exam blueprint) # tasks per domain drawn into a session (weights follow the exam blueprint)
"quota": {"ARCH": 5, "WORK": 4, "NET": 4, "STOR": 2, "TSHOOT": 5}, # 20 tasks "quota": {"ARCH": 5, "WORK": 4, "NET": 4, "STOR": 2, "TSHOOT": 5}, # 20 tasks
"quota_hard": {"ARCH": 6, "WORK": 5, "NET": 5, "STOR": 3, "TSHOOT": 6}, # 25 tasks, hard ones preferred "quota_hard": {"ARCH": 6, "WORK": 5, "NET": 5, "STOR": 3, "TSHOOT": 6}, # 25 tasks, hard ones preferred
# fixed simulator sets (./exam sim a|b|c|d) - excluded from random sampling # fixed simulator sets (./exam sim c|d) - excluded from random sampling. Sets A and B were
"sets": {"a": "Simulator Set A", "b": "Simulator Set B", "c": "Simulator Set C", "d": "Simulator Set D (killer.sh-style)"}, # retired on 2026-09-22; their last (fixed, verified) versions are in git history.
"sets": {"c": "Simulator Set C", "d": "Simulator Set D (killer.sh-style)"},
"domains": { "domains": {
"ARCH": "Cluster Architecture, Installation & Configuration (25%)", "ARCH": "Cluster Architecture, Installation & Configuration (25%)",
"WORK": "Workloads & Scheduling (15%)", "WORK": "Workloads & Scheduling (15%)",
@ -631,10 +632,10 @@ $K -n default delete pod sched-probe --wait=false >/dev/null 2>&1
] ]
# ---- aggregate the banks: 1-20 here, 21-43 in bank_extra, 101-124 (hard) in bank_hard, # ---- aggregate the banks: 1-20 here, 21-43 in bank_extra, 101-124 (hard) in bank_hard,
# ---- 201-217 / 301-317 / 401-418 / 501-517 in the four fixed simulator sets # ---- 401-418 / 501-517 in the two fixed simulator sets
from tracks.cka import bank_extra, bank_hard, bank_sim_a, bank_sim_b, bank_sim_c, bank_sim_d from tracks.cka import bank_extra, bank_hard, bank_sim_c, bank_sim_d
QUESTIONS = (QUESTIONS + bank_extra.QUESTIONS + bank_hard.QUESTIONS QUESTIONS = (QUESTIONS + bank_extra.QUESTIONS + bank_hard.QUESTIONS
+ bank_sim_a.QUESTIONS + bank_sim_b.QUESTIONS + bank_sim_c.QUESTIONS + bank_sim_d.QUESTIONS) + bank_sim_c.QUESTIONS + bank_sim_d.QUESTIONS)
QUESTIONS.sort(key=lambda q: q["id"]) QUESTIONS.sort(key=lambda q: q["id"])
_ids = [q["id"] for q in QUESTIONS] _ids = [q["id"] for q in QUESTIONS]
assert len(_ids) == len(set(_ids)), "duplicate question ids" assert len(_ids) == len(set(_ids)), "duplicate question ids"

View File

@ -100,27 +100,6 @@ _Generated by exam from 10 attempt(s); regenerated on every `end`._
- ✗ Service ledger-21 port 9000 in the same namespace - ✗ Service ledger-21 port 9000 in the same namespace
- ✗ ns.txt has the namespace - ✗ ns.txt has the namespace
### Q202 · ARCH · Helm chart with CRDs, then a custom resource (1/1 failed, last 3/8 pts, last failed 2026-09-19T12:27)
- ✗ watchAllNamespaces=true reached the pod env
- ✗ Ledger main-ledger has currency USD and retentionDays 30
### Q204 · WORK · Which Pods get evicted first (1/1 failed, last 0/8 pts, last failed 2026-09-19T12:27)
- ✗ evicted-first.txt lists exactly the BestEffort pods
- ✗ api-qos.txt says Burstable
- ✗ guaranteed.txt names orion-ledger
### Q217 · TSHOOT · Inspect a container with crictl (1/1 failed, last 0/10 pts, last failed 2026-09-19T12:27)
- ✗ Pod tiger-probe Running with both labels
- ✗ container.txt holds a real container ID for that Pod
- ✗ container.txt records the runtimeType
- ✗ container.log holds the httpd startup output
### Q313 · NET · Two hosts, TLS, and a default backend (1/1 failed, last 0/11 pts, last failed 2026-09-19T12:27)
- ✗ Secret classic-tls is a TLS secret with CN classic.r700.io
- ✗ both host rules route to the right Services on /
- ✗ TLS covers both hosts with classic-tls
- ✗ defaultBackend points at web-classic:80, class is nginx
## Attempt matrix ## Attempt matrix
| Q | Domain | Title | D1 | E2 | D3 | D4 | D5 | D6 | D7 | D8 | D9 | D10 | | Q | Domain | Title | D1 | E2 | D3 | D4 | D5 | D6 | D7 | D8 | D9 | D10 |
@ -192,40 +171,6 @@ _Generated by exam from 10 attempt(s); regenerated on every `end`._
| 122 | ARCH | Renew control-plane certificates | · | · | · | · | · | · | · | · | · | · | | 122 | ARCH | Renew control-plane certificates | · | · | · | · | · | · | · | · | · | · |
| 123 | TSHOOT | Nothing works: kube-apiserver itself is down | · | · | · | · | · | · | · | · | · | · | | 123 | TSHOOT | Nothing works: kube-apiserver itself is down | · | · | · | · | · | · | · | · | · | · |
| 124 | ARCH | Prepare a node join without shell access to the control plane | · | · | · | · | · | · | · | · | · | · | | 124 | ARCH | Prepare a node join without shell access to the control plane | · | · | · | · | · | · | · | · | · | · |
| 201 | ARCH | kubeconfig forensics | · | · | · | · | · | · | · | · | · | · |
| 202 | ARCH | Helm chart with CRDs, then a custom resource | · | · | · | · | · | · | · | · | · | 3/8 |
| 203 | WORK | Find the controller, then scale it | · | · | · | · | · | · | · | · | · | · |
| 204 | WORK | Which Pods get evicted first | · | · | · | · | · | · | · | · | · | 0/8 |
| 205 | ARCH | Kustomize: drop a ConfigMap, add an HPA, override in prod | · | · | · | · | · | · | · | · | · | · |
| 206 | STOR | PV, PVC and a Deployment that mounts it | · | · | · | · | · | · | · | · | · | · |
| 207 | TSHOOT | Resource usage scripts | · | · | · | · | · | · | · | · | · | · |
| 208 | ARCH | Drain a node for maintenance | · | · | · | · | · | · | · | · | · | · |
| 209 | ARCH | Query the API from inside a Pod | · | · | · | · | · | · | · | · | · | · |
| 210 | ARCH | ServiceAccount limited to two resources | · | · | · | · | · | · | · | · | · | · |
| 211 | WORK | DaemonSet on every node | · | · | · | · | · | · | · | · | · | · |
| 212 | WORK | One Pod per node, on purpose | · | · | · | · | · | · | · | · | · | · |
| 213 | NET | Convert an Ingress to Gateway API, then split on a header | · | · | · | · | · | · | · | · | · | · |
| 214 | ARCH | Certificate expiry, two ways | · | · | · | · | · | · | · | · | · | · |
| 215 | NET | Egress policy with paired destinations | · | · | · | · | · | · | · | · | · | · |
| 216 | NET | CoreDNS: back up, extend, verify | · | · | · | · | · | · | · | · | · | · |
| 217 | TSHOOT | Inspect a container with crictl | · | · | · | · | · | · | · | · | · | 0/10 |
| 301 | ARCH | etcd configuration facts | · | · | · | · | · | · | · | · | · | ✓ |
| 302 | NET | Service rules on the node | · | · | · | · | · | · | · | · | · | · |
| 303 | NET | A second Service CIDR | · | · | · | · | · | · | · | · | · | · |
| 304 | ARCH | Bound tokens versus legacy tokens | · | · | · | · | · | · | · | · | · | · |
| 305 | WORK | Schedule without the scheduler | · | · | · | · | · | · | · | · | · | · |
| 306 | STOR | Grow a volume in place | · | · | · | · | · | · | · | · | · | · |
| 307 | TSHOOT | Node accepts nothing new | · | · | · | · | · | · | · | · | · | · |
| 308 | ARCH | API resource reference card | · | · | · | · | · | · | · | · | · | · |
| 309 | WORK | A PodDisruptionBudget that blocks maintenance | · | · | · | · | · | · | · | · | · | · |
| 310 | NET | Route to something outside the cluster | · | · | · | · | · | · | · | · | · | · |
| 311 | ARCH | Image inventory across the cluster | · | · | · | · | · | · | · | · | · | · |
| 312 | WORK | A Job that fails, then one that doesn't | · | · | · | · | · | · | · | · | · | · |
| 313 | NET | Two hosts, TLS, and a default backend | · | · | · | · | · | · | · | · | · | 0/11 |
| 314 | TSHOOT | A static pod that never starts | · | · | · | · | · | · | · | · | · | · |
| 315 | STOR | Keep the data when the claim goes | · | · | · | · | · | · | · | · | · | · |
| 316 | ARCH | Plan a control-plane upgrade | · | · | · | · | · | · | · | · | · | · |
| 317 | TSHOOT | Certificates stop being issued | · | · | · | · | · | · | · | · | · | · |
| 401 | STOR | Provision geo-fence persistent storage | · | · | · | · | · | · | · | · | · | · | | 401 | STOR | Provision geo-fence persistent storage | · | · | · | · | · | · | · | · | · | · |
| 402 | ARCH | Render a chart, then install it | · | · | · | · | · | · | · | · | · | · | | 402 | ARCH | Render a chart, then install it | · | · | · | · | · | · | · | · | · | · |
| 403 | TSHOOT | Fix the driver onboarding app | · | · | · | · | · | · | · | · | · | · | | 403 | TSHOOT | Fix the driver onboarding app | · | · | · | · | · | · | · | · | · | · |
@ -244,5 +189,22 @@ _Generated by exam from 10 attempt(s); regenerated on every `end`._
| 416 | NET | NetworkPolicy for the safety monitor | · | · | · | · | · | · | · | · | · | · | | 416 | NET | NetworkPolicy for the safety monitor | · | · | · | · | · | · | · | · | · | · |
| 417 | ARCH | Enforce TLS 1.3 on the booking portal | · | · | · | · | · | · | · | · | · | · | | 417 | ARCH | Enforce TLS 1.3 on the booking portal | · | · | · | · | · | · | · | · | · | · |
| 418 | TSHOOT | Investigate vehicle tracker health | · | · | · | · | · | · | · | · | · | · | | 418 | TSHOOT | Investigate vehicle tracker health | · | · | · | · | · | · | · | · | · | · |
| 501 | ARCH | How the control plane is run | · | · | · | · | · | · | · | · | · | · |
| 502 | TSHOOT | Cluster facts to an exact file | · | · | · | · | · | · | · | · | · | · |
| 503 | NET | Ready only when its dependency answers | · | · | · | · | · | · | · | · | · | · |
| 504 | WORK | Sorting done by kubectl | · | · | · | · | · | · | · | · | · | · |
| 505 | TSHOOT | Event forensics around kube-proxy | · | · | · | · | · | · | · | · | · | · |
| 506 | ARCH | API resources and a Role census | · | · | · | · | · | · | · | · | · | · |
| 507 | TSHOOT | Kubelet unit points at nothing | · | · | · | · | · | · | · | · | · | · |
| 508 | WORK | Secrets as a read-only volume and renamed env vars | · | · | · | · | · | · | · | · | · | · |
| 509 | ARCH | Static Pod on the control plane behind a Service | · | · | · | · | · | · | · | · | · | · |
| 510 | ARCH | The kubelet's two certificates | · | · | · | · | · | · | · | · | · | · |
| 511 | WORK | A Pod for the control plane only | · | · | · | · | · | · | · | · | · | · |
| 512 | TSHOOT | Stop the scheduler, schedule by hand | · | · | · | · | · | · | · | · | · | · |
| 513 | ARCH | Helm release housekeeping | · | · | · | · | · | · | · | · | · | · |
| 514 | STOR | Per-replica storage for a StatefulSet | · | · | · | · | · | · | · | · | · | · |
| 515 | NET | Namespace AND pod selector, enforced | · | · | · | · | · | · | · | · | · | · |
| 516 | NET | Resolves everything except the cluster | · | · | · | · | · | · | · | · | · | · |
| 517 | STOR | A claim that can never bind | · | · | · | · | · | · | · | · | · | · |
_E = full exam, D = drill, · = not in that session, n/m = partial credit._ _E = full exam, D = drill, · = not in that session, n/m = partial credit._

View File

@ -347,8 +347,8 @@
const eligible = S.bank.filter(q => hard || q.difficulty !== 'hard'), seenN = eligible.filter(q => (S.seen || []).includes(q.id)).length; const eligible = S.bank.filter(q => hard || q.difficulty !== 'hard'), seenN = eligible.filter(q => (S.seen || []).includes(q.id)).length;
$('#allDesc').textContent = `${seenN}/${eligible.length} tasks attempted. 5 unseen tasks per session, round-robin across domains${hard ? ', hard first' : ''}; then whatever you failed.`; $('#allDesc').textContent = `${seenN}/${eligible.length} tasks attempted. 5 unseen tasks per session, round-robin across domains${hard ? ', hard first' : ''}; then whatever you failed.`;
$('#nextDesc').textContent = `${S.track.domains[nxt]} — 5 tasks, ${hard ? 5 : 6} min each. Drills rotate through all domains before the exam.`; $('#nextDesc').textContent = `${S.track.domains[nxt]} — 5 tasks, ${hard ? 5 : 6} min each. Drills rotate through all domains before the exam.`;
// one button per fixed set the track defines (Set C and later were unreachable from here when // one button per fixed set the track defines - never hard-code the letters: newer sets used to
// the buttons were hard-coded to A and B) // be unreachable from here
$('#simOpts').innerHTML = Object.entries(S.sets || {}).map(([k, info]) => $('#simOpts').innerHTML = Object.entries(S.sets || {}).map(([k, info]) =>
`<button class="opt" data-sim="${k}"><b>${esc(info.name)}</b><span>${info.count} fixed tasks, ${S.track.minutes} min, exam rules, no feedback until you submit.</span></button>`).join(''); `<button class="opt" data-sim="${k}"><b>${esc(info.name)}</b><span>${info.count} fixed tasks, ${S.track.minutes} min, exam rules, no feedback until you submit.</span></button>`).join('');
$('#simOpts').querySelectorAll('[data-sim]').forEach(el => el.onclick = () => startSim(el.dataset.sim)); $('#simOpts').querySelectorAll('[data-sim]').forEach(el => el.onclick = () => startSim(el.dataset.sim));