kubestronaut-sim/cka/common.py
vumas3r f3d0e536b6 Exam simulator for CKA, CKAD and CKS with per-task hosts and a remote desktop
kubestronaut-sim runs each exam task on a host of its own - a kind (kubeadm)
cluster in rootless podman, or a QEMU/KVM Ubuntu VM for the CKS tasks that
need their own kernel (Falco, AppArmor) - and puts a browser desktop in front
of them, the way killer.sh and sailor.sh do.

- ksim, sim/: the engine (sessions, drills, study mode, grading, coverage,
  audit), host and VM lifecycle, the desktop
- images/: exam host images (common, CKS variant with its tools, VM guest)
- cka/ (131 tasks), ckad/ (105), cks/ (110): task banks, curricula with
  study notes, reference solutions and scripted solutions

Every domain of every track has at least 17 pool tasks. `./ksim audit` proves
a task three ways: it seeds cleanly on its own host, scores nothing before
anyone has worked on it, and reaches full marks when its scripted solution is
run on the host as `candidate`. All 346 tasks pass (CKA audited 2026-10-13,
CKAD and CKS 2026-10-12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPd3GGWyxTgAkL6a4gAJz
2026-10-13 16:29:33 -06:00

47 lines
2.5 KiB
Python

"""Building blocks shared by the CKA task banks written for the per-task hosts (114 onwards): bash
for setups and checks. The generic ones - NET, PODS, PY, CAN - are sim/bank.py's.
The two rules of the CKS and CKAD banks hold here as well: a task scores nothing before anyone has
worked on it, and what can be tested for real - a file on a node, a name resolved from a Pod, a
request, a volume that gets provisioned - is tested for real rather than read off the object.
"""
from sim.bank import C, CAN, NET, NS, PODS, PY, ns_pick, name_pick # noqa: F401 (the banks import these from here)
# one FILE: the file's content with blanks and line ends removed - for a one-word answer
ONE = r"""
one() { tr -d ' \t\n\r' < "$1" 2>/dev/null; }
"""
# Volumes and claims.
STOR = ONE + r"""
# pvnode PV: the node a PersistentVolume is tied to (its node affinity)
pvnode() { j pv "$1" '{.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}'; }
# pvpath PV: its directory on that node (hostPath or local)
pvpath() { local p; p=$(j pv "$1" '{.spec.hostPath.path}'); [ -n "$p" ] || p=$(j pv "$1" '{.spec.local.path}'); echo "$p"; }
# bound NS CLAIM [SECONDS]: the claim is Bound, given a moment
bound() {
local i
for i in $(seq 1 $(( ${3:-45} / 3 ))); do [ "$(j -n "$1" pvc "$2" '{.status.phase}')" = Bound ] && return 0; sleep 3; done
return 1
}
# running NS POD [SECONDS]: the Pod is Running, given a moment
running() {
local i
for i in $(seq 1 $(( ${3:-60} / 3 ))); do [ "$(j -n "$1" pod "$2" '{.status.phase}')" = Running ] && return 0; sleep 3; done
return 1
}
# gone KIND NAME [-n NS]: the object does not exist (and the API server said so)
gone() { local out; out=$($K get "$@" 2>&1); case "$out" in *NotFound*) return 0 ;; esac; return 1; }
# newest NS SELECTOR: the name of the newest Running Pod the selector matches
newest() { $K -n "$1" get pod -l "$2" --field-selector status.phase=Running --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].metadata.name}' 2>/dev/null; }
"""
# Name lookups from a Pod.
DNS = r"""
# look NS POD NAME: the address the name resolves to from that Pod ("" when it does not)
look() { $K -n "$1" exec "$2" -- nslookup "$3" 2>/dev/null | awk '/^Name:/ {n=1} n && /^Address/ {print $NF; exit}'; }
# resolves NS POD NAME ADDRESS: it resolves to that address, given a moment (CoreDNS reloads its
# configuration by itself, but not at once)
resolves() { local i; for i in $(seq 1 10); do [ "$(look "$1" "$2" "$3")" = "$4" ] && return 0; sleep 3; done; return 1; }
"""