Show me how gave the commands the audit runs: sed, awk and yq edits, heredoc manifests, JSON patches, --overrides, sleeps and wait loops - nothing anyone types in the exam. A step's show is now a list of parts (sim/bank.py): cmd() is what you type as it stands; hand() is what you do by hand - vim on a generated skeleton or on a manifest pasted from the docs page it names, kubectl edit, ssh NODE and sudo -i, a value you read and echo - with a script that does the same for the audit. All 135 CKA steps are rewritten so, with k for kubectl; their checks, titles and texts are unchanged. Add ./ksim lesson lint, which reads what every step has you type and flags scripts. The lesson audit runs it first. Record the hosts whose setup failed when a session started. The page warns above that task or lesson and offers Rebuild this host; the CLI names ./ksim reset N on each step. Render *italics* in lessons and tasks, outside code spans. Audited on v1.37: CKA lessons 27/27, 135/135 steps, done the exam way; lint clean. kubectl edit --subresource resize, the one edit the audit replaces with a patch, was proven on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
5.4 KiB
Python
113 lines
5.4 KiB
Python
"""Helpers shared by every track's task bank."""
|
|
|
|
NS = r"""
|
|
mkns() {
|
|
$K create ns "$1" --dry-run=client -o yaml | $K apply -f - >/dev/null
|
|
# Wait for the namespace's `default` ServiceAccount before returning. The controller creates it
|
|
# asynchronously, and a Pod applied in the gap is rejected outright:
|
|
# 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
|
|
}
|
|
"""
|
|
|
|
def C(name, pts, script):
|
|
return {"name": name, "pts": pts, "script": script}
|
|
|
|
def step(title, text, check, hint="", show=""):
|
|
"""One step of a lesson (<track>/lessons.py): what to do (Markdown), the check that proves it
|
|
(bash, exit 0 = done, run like a task's checks), a hint, and how to do it as you would in the
|
|
exam - shown on request, and done by ./ksim lesson audit to prove the step works as shown.
|
|
`show` is commands you type as they stand, or a list of cmd() and hand() parts."""
|
|
return {"title": title, "text": text, "check": check, "hint": hint, "show": show}
|
|
|
|
def cmd(text, how=""):
|
|
"""Commands you type as they stand, and what to look for (Markdown); the audit runs them as
|
|
they stand."""
|
|
return {"type": text, "how": how, "run": text}
|
|
|
|
def hand(type_, how, run):
|
|
"""What you do by hand: what you type to begin (`vim f`, `k edit ...`, `echo NAME > f`), what
|
|
to do there (Markdown), and a script that does the same, which the audit runs instead - an
|
|
editor needs a person, and so does a value read off the screen."""
|
|
return {"type": type_, "how": how, "run": run}
|
|
|
|
def ns_pick(r, base):
|
|
"""Namespace name with a random suffix so the same task never lands on the same namespace twice."""
|
|
return f"{base}-{r.choice(['alpha', 'bravo', 'delta', 'echo', 'kilo', 'nova', 'omega', 'zulu'])}"
|
|
|
|
def name_pick(r, base):
|
|
return f"{base}-{r.randint(10, 99)}"
|
|
|
|
|
|
# ---- bash building blocks for setups and checks, shared by the tracks' banks ----------------
|
|
# Prepend what a script needs: "setup": PODS + r"""...""", C("...", 3, NET + r"""...""")
|
|
|
|
# Traffic probes for checks.
|
|
NET = r"""
|
|
# ok NS POD URL: one request from a Pod succeeds. okr: for a path that should work, given a
|
|
# moment. blocked: the request fails - at once, or once a policy applied a moment ago has landed.
|
|
ok() { $K -n "$1" exec "$2" -- wget -T 3 -qO- "$3" >/dev/null 2>&1; }
|
|
okr() { local i; for i in 1 2 3 4; do ok "$@" && return 0; sleep 2; done; return 1; }
|
|
blocked() { local i; for i in 1 2 3 4 5 6; do ok "$@" || return 0; sleep 3; done; return 1; }
|
|
# has -n NS KIND NAME: the object exists
|
|
has() { [ -n "$(j "$@" '{.metadata.name}')" ]; }
|
|
"""
|
|
|
|
# Scenario building blocks for setups (busybox Pods: each can be the client of a test as well).
|
|
PODS = NS + r"""
|
|
# srv NS NAME [LABELS]: a Pod that serves its own name on port 80, with a Service of its name. It
|
|
# has wget and nslookup too, so it can be the client of a test as well.
|
|
srv() {
|
|
$K -n "$1" run "$2" --image=busybox:1.36 --port=80 --labels="${3:-app=$2}" --command -- \
|
|
sh -c 'mkdir -p /www && echo "$HOSTNAME" > /www/index.html && exec httpd -f -p 80 -h /www' >/dev/null
|
|
$K -n "$1" expose pod "$2" --port=80 >/dev/null
|
|
}
|
|
# cli NS NAME [LABELS]: a Pod to send requests from
|
|
cli() { $K -n "$1" run "$2" --image=busybox:1.36 --labels="${3:-app=$2}" --command -- sleep 86400 >/dev/null; }
|
|
ready() { $K -n "$1" wait --for=condition=Ready pod --all --timeout=240s >/dev/null; }
|
|
"""
|
|
|
|
# An object as JSON, for a Python expression.
|
|
PY = r"""
|
|
# py KUBECTL-GET-ARGS.. 'PYTHON': the object as `o` (a dict) for a Python expression or statements;
|
|
# the check passes when the last line, an expression, is true. Fails when the object is missing.
|
|
py() {
|
|
local n=$#; local code="${!n}"
|
|
$K get "${@:1:n-1}" -o json 2>/dev/null | CODE="$code" python3 -c '
|
|
import json, os, sys
|
|
try:
|
|
o = json.load(sys.stdin)
|
|
except Exception:
|
|
sys.exit(1)
|
|
src = os.environ["CODE"].strip().splitlines()
|
|
env = {"o": o, "json": json, "os": os}
|
|
exec("\n".join(src[:-1]), env)
|
|
sys.exit(0 if eval(src[-1], env) else 1)'
|
|
}
|
|
"""
|
|
|
|
# Permissions, asked the way a Role spells them.
|
|
# can SUBJECT-FLAGS.. -- VERB RESOURCE [-n NS]: kubectl auth can-i says yes. cannot: it says no.
|
|
# A subresource is written here as in a Role - pods/exec - and handed to kubectl the way it wants
|
|
# it: to `can-i`, "pods/exec" is a Pod named exec, and the subresource is --subresource=exec.
|
|
CAN = r"""
|
|
_cani() {
|
|
local out=() a
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
*/exec|*/attach|*/log|*/portforward|*/proxy|*/status|*/scale|*/eviction|*/ephemeralcontainers|*/token) out+=("${a%/*}" "--subresource=${a##*/}") ;;
|
|
*) out+=("$a") ;;
|
|
esac
|
|
done
|
|
$K auth can-i "${out[@]}" 2>/dev/null | head -1
|
|
}
|
|
can() { local a=(); while [ "$1" != -- ]; do a+=("$1"); shift; done; shift; [ "$(_cani "$@" "${a[@]}")" = yes ]; }
|
|
cannot() { local a=(); while [ "$1" != -- ]; do a+=("$1"); shift; done; shift; [ "$(_cani "$@" "${a[@]}")" = no ]; }
|
|
"""
|