Compare commits
2 Commits
f4f2a6bea9
...
e6ee219dfc
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ee219dfc | |||
| 1cd963e8b5 |
24
README.md
24
README.md
@ -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.
|
||||||
|
|||||||
43
exam
43
exam
@ -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):
|
||||||
@ -644,11 +658,22 @@ def _log_msg(*args: Any, **kwargs: Any) -> None:
|
|||||||
# restores every known "breaks" target to a good baseline before each session seeds, so
|
# 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.
|
# leftover damage never leaks across sessions regardless of which questions get sampled.
|
||||||
RESTORE_BASELINE = r"""
|
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
|
# 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
|
# 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.
|
# 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
|
if nx "$CP" 'grep -q "^ *- kube-apiserverr$" /etc/kubernetes/manifests/kube-apiserver.yaml' </dev/null 2>/dev/null; then
|
||||||
nx "$CP" 'sed -i "s|^\( *\)- kube-apiserverr$|\1- kube-apiserver|" /etc/kubernetes/manifests/kube-apiserver.yaml' </dev/null
|
nx "$CP" "$MEDIT"'
|
||||||
|
medit /etc/kubernetes/manifests/kube-apiserver.yaml -e "s|^\( *\)- kube-apiserverr$|\1- kube-apiserver|"' </dev/null
|
||||||
# the kubelet re-reads the manifest on its own; wait for the API to answer again
|
# 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
|
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
|
fi
|
||||||
@ -656,22 +681,22 @@ fi
|
|||||||
# scheduler: undo the "kube-scheduler" -> "kube-schedulerr" command typo (Q20/Q61), and put the
|
# 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
|
# 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.
|
# never moved it back - that setup keeps a pristine copy outside the static pod directory.
|
||||||
nx "$CP" '
|
nx "$CP" "$MEDIT"'
|
||||||
f=/etc/kubernetes/manifests/kube-scheduler.yaml
|
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; }
|
[ -f $f ] || { [ -f /etc/kubernetes/.cka-sim/kube-scheduler.yaml ] && cp /etc/kubernetes/.cka-sim/kube-scheduler.yaml $f; }
|
||||||
sed -i "s|^\( *\)- kube-schedulerr$|\1- kube-scheduler|" $f 2>/dev/null
|
medit $f -e "s|^\( *\)- kube-schedulerr$|\1- kube-scheduler|"
|
||||||
' </dev/null
|
' </dev/null
|
||||||
|
|
||||||
# controller-manager: undo Q38's injected flag and Q61's broken image tag. The last two lines
|
# 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
|
# 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
|
# 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.
|
# controller-manager); a cluster that ever ran that set may still carry either.
|
||||||
nx "$CP" '
|
nx "$CP" "$MEDIT"'
|
||||||
f=/etc/kubernetes/manifests/kube-controller-manager.yaml
|
medit /etc/kubernetes/manifests/kube-controller-manager.yaml \
|
||||||
sed -i "/--leader-elect-lease-duration=1s/d" $f 2>/dev/null
|
-e "/--leader-elect-lease-duration=1s/d" \
|
||||||
sed -i "s|\(image: registry.k8s.io/kube-controller-manager:v[0-9.]*\)-broken|\1|" $f 2>/dev/null
|
-e "s|\(image: registry.k8s.io/kube-controller-manager:v[0-9.]*\)-broken|\1|" \
|
||||||
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
|
-e "s|--cluster-signing-cert-file=/etc/kubernetes/pki/ca-signing.crt|--cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt|" \
|
||||||
sed -i "s|,-certificatesigningrequest-signing-controller||; s|,-csrsigning||" $f 2>/dev/null
|
-e "s|,-certificatesigningrequest-signing-controller||; s|,-csrsigning||"
|
||||||
' </dev/null
|
' </dev/null
|
||||||
|
|
||||||
# coredns: restore the default upstream forward (Q37/Q57 point it somewhere bogus)
|
# coredns: restore the default upstream forward (Q37/Q57 point it somewhere bogus)
|
||||||
|
|||||||
@ -8,7 +8,11 @@ mkns() {
|
|||||||
# 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,
|
# 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
|
# 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
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
1595
tracks/ckad/bank_extra.py
Normal file
1595
tracks/ckad/bank_extra.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user