kubestronaut-sim/cka/solve.py
vumas3r e761d53a96 feat: show every CKA reference solution the way the exam is done
105 of the 147 CKA reference solutions had you type what nobody types
in the exam: heredoc manifests, sed, JSON patches, kubectl wait, loops,
command substitution, jsonpath into files. Each is rewritten as the
lessons are: a generator and the lines to add in vim, a manifest pasted
from the docs page it names, kubectl edit, ssh NODE and sudo -i, or a
value read off the screen and echoed. The other 42 were commands typed
as they stand already.

A solution can now be parts (sim/bank.py cmd, hand) as a lesson step's
show is. The page and ./ksim solution N render it as Markdown, each
thing to type a block of its own that a click copies, with the edit
beneath it. A solution in parts is its own audit script: load_track
runs its parts, and for each edit by hand the script beside it, so the
solution you read is the one proven. Their separate scripts in
cka/solve.py are dropped, and a task that has both is refused.

./ksim solution lint [N|DOMAIN..] reads what the solutions have you
type against TYPING_TRAPS: the CKA bank is clean. Q49's params gain
the plural stem and shortName the solution spells out (no new draws);
Q103, ported from CKAD Q23, takes a solution of its own.

Audited from a copy with renamed hosts: Q3-Q67 (52 tasks) all solved by
their parts, after two waits were added to the audit scripts (Q43's
DNS record, Q53's Pods); Q68-Q147 still running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNSZLLsGXTMBsw17kDunkQ
2026-10-27 20:56:51 -06:00

284 lines
17 KiB
Python

"""Scripted solutions: what a candidate would type on the task's host, as a script.
`./ksim audit` runs a task's script as `candidate` in an interactive shell on its host (so the
host's aliases apply) after grading the task untouched, and grades it again: full marks prove the
task can be solved as its text describes, on a cluster of its own. %{param}, {WS} and {CP}.. are
substituted as in the task. A task without a script is only audited for a clean seed.
A command that reads stdin (ssh, kubectl run -i) would swallow the rest of the script: ssh -n,
</dev/null.
A task whose reference solution is in parts (sim/bank.py: cmd, hand) has no script here: the audit
runs the parts - what is typed as it stands, and for each edit by hand its script - so the solution
the candidate reads is the one that is proven.
SOLVE: task id -> bash
"""
# cpwait COMPONENT: the control-plane component's static Pod on this host is ready (again)
CPWAIT = r"""
cpwait() {
local i
for i in $(seq 1 80); do
[ "$(kubectl -n kube-system get pod $1-{CP} -o jsonpath='{.status.containerStatuses[0].ready}' 2>/dev/null)" = true ] && return 0
sleep 3
done
return 1
}
"""
SOLVE = {
# ---- 114-124: bank_storage.py
121: r"""
kubectl -n %{ns} get pvc %{pvc} -o jsonpath='{.metadata.finalizers[0]}' > {WS}/q121/finalizer.txt
echo deployment/%{dep} > {WS}/q121/used-by.txt
kubectl -n %{ns} patch deploy %{dep} --type=json -p '[{"op":"replace","path":"/spec/template/spec/volumes/0","value":{"name":"files","emptyDir":{}}}]'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=240s
""",
122: r"""
kubectl -n local-path-storage scale deploy local-path-provisioner --replicas=1
kubectl get sc %{sc} -o json | jq 'del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, .metadata.managedFields, .metadata.annotations) | .provisioner = "rancher.io/local-path"' > /tmp/sc.json
kubectl delete sc %{sc}
kubectl apply -f /tmp/sc.json
for i in $(seq 1 40); do [ "$(kubectl -n %{ns} get pvc %{pvc} -o jsonpath='{.status.phase}')" = Bound ] && break; sleep 3; done
kubectl -n %{ns} wait --for=condition=Ready pod/%{pod} --timeout=240s
echo rancher.io/local-path > {WS}/q122/provisioner.txt
""",
# ---- 125-128: bank_net.py
125: r"""
kubectl -n kube-system get cm coredns -o yaml > {WS}/q125/coredns-backup.yaml
ADD=$' hosts {\n %{ip} %{host}.dc.internal\n fallthrough\n }\n rewrite name %{svc}.dc.internal %{svc}.%{ns}.svc.cluster.local\n'
kubectl -n kube-system get cm coredns -o json | jq --arg add "$ADD" '.data.Corefile |= sub(" kubernetes "; $add + " kubernetes ")' | kubectl apply -f -
kubectl -n kube-system rollout restart deploy coredns
kubectl -n kube-system rollout status deploy coredns --timeout=240s
""",
127: r"""
kubectl annotate ingressclass %{old} ingressclass.kubernetes.io/is-default-class-
kubectl annotate ingressclass %{cls} ingressclass.kubernetes.io/is-default-class=true
kubectl -n %{ns} patch ingress %{ing} --type=json -p '[
{"op": "add", "path": "/spec/ingressClassName", "value": "%{cls}"},
{"op": "replace", "path": "/spec/rules/0/http/paths/0/backend/service/port", "value": {"name": "http"}},
{"op": "replace", "path": "/spec/rules/0/http/paths/1/pathType", "value": "Prefix"}]'
kubectl apply -f {WS}/q127/blog-ingress.yaml
""",
# ---- 129-131: bank_tshoot.py
129: r"""
ssh -n {W1} 'sudo systemctl enable --now containerd; echo "runtime-endpoint: unix:///run/containerd/containerd.sock" | sudo tee /etc/crictl.yaml >/dev/null'
echo containerd > {WS}/q129/unit.txt
for i in $(seq 1 40); do [ "$(kubectl get node {W1} -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" = True ] && break; sleep 3; done
""",
130: r"""
for i in $(seq 1 60); do
U=$(kubectl -n %{ns} top pod query --no-headers 2>/dev/null | awk '{gsub("Mi","",$3); print $3}')
[ "${U:-0}" -ge 50 ] 2>/dev/null && [ "$(kubectl -n %{ns} top pod --no-headers 2>/dev/null | wc -l)" -ge 3 ] && break
sleep 5
done
kubectl -n %{ns} top pod query --containers --no-headers | awk '{gsub("Mi","",$4); print $4, $2}' | sort -rn | head -1 | awk '{print $2}' > {WS}/q130/container.txt
for p in ingest index query; do
use=$(kubectl -n %{ns} top pod $p --no-headers | awk '{gsub("Mi","",$3); print $3}')
req=$(kubectl -n %{ns} get pod $p -o json | jq '[.spec.containers[].resources.requests.memory | sub("Mi"; "") | tonumber] | add')
[ "$use" -gt "$req" ] && echo $p
done > {WS}/q130/over.txt
echo 'kubectl -n %{ns} top pod --containers' > {WS}/q130/top.sh
""",
131: r"""
kubectl -n %{ns} logs %{pod} -c api --previous > {WS}/q131/previous.log
U=$(kubectl -n %{ns} get pod %{pod} -o jsonpath='{.metadata.uid}')
N=$(kubectl -n %{ns} get pod %{pod} -o jsonpath='{.status.containerStatuses[?(@.name=="api")].restartCount}')
NODE=$(kubectl -n %{ns} get pod %{pod} -o jsonpath='{.spec.nodeName}')
F=/var/log/pods/%{ns}_%{pod}_$U/api/$N.log
echo $F > {WS}/q131/logfile.txt
sleep 20
ssh -n $NODE "sudo grep ' stderr F ' $F" | cut -d' ' -f4- > {WS}/q131/stderr.log
echo 'kubectl -n %{ns} logs %{pod} --all-containers --prefix --timestamps' > {WS}/q131/all.sh
""",
# ---- 1-113, where the reference solution is not a script as it stands: it opens a shell on a
# ---- node, edits in vi, or leaves a value to be read off the screen. The same steps, scripted.
# ---- (The host is the control-plane node: what a solution does after "ssh <host>" is sudo here.)
2: r"""
sudo mkdir -p /opt/cka
sudo ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key snapshot save /opt/cka/etcd-snapshot.db
""",
17: r"""
ssh -n {W2} 'sudo systemctl enable --now kubelet'
for i in $(seq 1 40); do [ "$(kubectl get node {W2} -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" = True ] && break; sleep 3; done
""",
20: CPWAIT + r"""
sudo sed -i 's/kube-schedulerr/kube-scheduler/' /etc/kubernetes/manifests/kube-scheduler.yaml
cpwait kube-scheduler
""",
23: r"""
kubectl -n kube-system patch deploy metrics-server --type=json -p '[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}]'
kubectl -n kube-system rollout status deploy metrics-server --timeout=240s
for i in $(seq 1 40); do kubectl top nodes >/dev/null 2>&1 && break; sleep 5; done
kubectl -n %{ns} autoscale deploy %{dep} --min=2 --max=%{max} --cpu-percent=%{cpu} --name=%{dep}
""",
37: r"""
kubectl -n kube-system get cm coredns -o json | jq '.data.Corefile |= sub("forward \\. [0-9.]+"; "forward . /etc/resolv.conf")' | kubectl apply -f -
kubectl -n kube-system rollout restart deploy coredns
kubectl -n kube-system rollout status deploy coredns --timeout=240s
""",
38: CPWAIT + r"""
sudo sed -i '/--leader-elect-lease-duration=1s/d' /etc/kubernetes/manifests/kube-controller-manager.yaml
cpwait kube-controller-manager
""",
57: r"""
kubectl -n kube-system delete netpol maintenance-lockdown
kubectl -n kube-system scale deploy coredns --replicas=2
kubectl -n kube-system get cm coredns -o json | jq '.data.Corefile |= sub("forward \\. [0-9.]+"; "forward . /etc/resolv.conf")' | kubectl apply -f -
kubectl -n kube-system rollout restart deploy coredns
kubectl -n kube-system rollout status deploy coredns --timeout=240s
""",
61: CPWAIT + r"""
sudo sed -i 's/kube-schedulerr/kube-scheduler/' /etc/kubernetes/manifests/kube-scheduler.yaml
sudo sed -i 's|\(image: registry.k8s.io/kube-controller-manager:v[0-9.]*\)-broken|\1|' /etc/kubernetes/manifests/kube-controller-manager.yaml
cpwait kube-scheduler; cpwait kube-controller-manager
""",
65: CPWAIT + r"""
sudo kubeadm certs renew all
sudo sh -c 'mkdir -p /tmp/manifests && mv /etc/kubernetes/manifests/*.yaml /tmp/manifests/ && sleep 25 && mv /tmp/manifests/*.yaml /etc/kubernetes/manifests/'
for i in $(seq 1 80); do kubectl get nodes >/dev/null 2>&1 && break; sleep 3; done
for c in etcd kube-apiserver kube-scheduler kube-controller-manager; do cpwait $c; done
sudo kubeadm certs check-expiration 2>/dev/null | grep '^apiserver ' > {WS}/q65/expiry.txt
""",
70: r"""
kubectl -n driver-onboard set image deploy/onboard-app onboard=nginx:1.25
kubectl -n driver-onboard patch deploy onboard-app --type=json -p '[
{"op": "replace", "path": "/spec/template/spec/containers/0/livenessProbe/httpGet/port", "value": 80},
{"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/httpGet/path", "value": "/"}]'
kubectl -n driver-onboard rollout status deploy onboard-app --timeout=240s
""",
76: r"""
kubectl -n trip-tracker get deploy trip-logger -o json | jq '
.spec.template.spec.volumes += [{"name": "config-volume", "emptyDir": {}}]
| .spec.template.spec.initContainers += [{"name": "config-init", "image": "busybox:1.36",
"command": ["sh", "-c", "echo '\''{\"region\":\"us-east\",\"mode\":\"production\"}'\'' > /config/app-config.json"],
"volumeMounts": [{"name": "config-volume", "mountPath": "/config"}]}]
| .spec.template.spec.containers[0].volumeMounts += [{"name": "config-volume", "mountPath": "/config"}]' | kubectl apply -f -
kubectl -n trip-tracker rollout status deploy trip-logger --timeout=240s
""",
79: r"""
kubectl -n surge-pricing create cm surge-config --from-literal=PRICING_MODE=dynamic --from-literal=MAX_MULTIPLIER=3.5 --from-literal=REGION=us-east-1
kubectl -n surge-pricing create secret generic surge-db-secret --from-literal=DB_HOST=pricing-db.surge-pricing.svc.cluster.local \
--from-literal=DB_USER=surge_admin --from-literal=DB_PASSWORD='S3cur3Pr1c1ng!'
kubectl -n surge-pricing get deploy surge-engine -o json | jq '
.spec.template.spec.containers[0].envFrom += [{"configMapRef": {"name": "surge-config"}}, {"secretRef": {"name": "surge-db-secret"}}]
| .spec.template.spec.containers[0].volumeMounts += [{"name": "config-vol", "mountPath": "/etc/surge-config"}]
| .spec.template.spec.volumes += [{"name": "config-vol", "configMap": {"name": "surge-config"}}]' | kubectl apply -f -
kubectl -n surge-pricing rollout status deploy surge-engine --timeout=240s
""",
84: r"""
kubectl -n booking-tls get cm booking-nginx-config -o json | jq '.data |= with_entries(.value |= sub("ssl_protocols TLSv1.2 TLSv1.3;"; "ssl_protocols TLSv1.3;"))' | kubectl apply -f -
kubectl -n booking-tls rollout restart deploy booking-nginx
kubectl -n booking-tls rollout status deploy booking-nginx --timeout=240s
echo 'ssl_protocols TLSv1.3;' > {WS}/q84/ssl-line.txt
""",
88: r"""
kubectl -n oslo-web run gatekeeper --image=busybox:1.36 --dry-run=client -o json -- sleep 86400 | jq '.spec.containers[0].readinessProbe = {"exec": {"command": ["sh", "-c", "wget -T2 -qO- http://ledger-api:8080"]}, "initialDelaySeconds": 5, "periodSeconds": 5}' | kubectl apply -f -
sleep 12
kubectl -n oslo-web expose deploy ledger --name=ledger-api --port=8080 --target-port=80
kubectl -n oslo-web wait --for=condition=Ready pod/gatekeeper --timeout=180s
""",
92: r"""
ssh -n {W1} "sudo sed -i 's|^ExecStart=/usr/local/bin/kubelet |ExecStart=/usr/bin/kubelet |' /etc/systemd/system/kubelet.service.d/10-kubeadm.conf; sudo systemctl daemon-reload; sudo systemctl restart kubelet"
echo /etc/systemd/system/kubelet.service.d/10-kubeadm.conf > {WS}/q92/fixed-file.txt
for i in $(seq 1 40); do [ "$(kubectl get node {W1} -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" = True ] && break; sleep 3; done
""",
95: r"""
CI=$(ssh -n {W2} 'sudo openssl x509 -in /var/lib/kubelet/pki/kubelet-client-current.pem -noout -issuer' | sed 's/^issuer= *//')
SI=$(ssh -n {W2} 'sudo openssl x509 -in /var/lib/kubelet/pki/kubelet.crt -noout -issuer' | sed 's/^issuer= *//')
printf 'client-issuer: %s\nclient-eku: TLS Web Client Authentication\nserver-issuer: %s\nserver-eku: TLS Web Server Authentication\n' "$CI" "$SI" > {WS}/q95/kubelet-certs.txt
""",
96: r"""
kubectl -n default run cp-watch --image=httpd:2-alpine --dry-run=client -o json | jq '.spec.containers[0].name = "watch"
| .spec.nodeSelector = {"node-role.kubernetes.io/control-plane": ""}
| .spec.tolerations = [{"key": "node-role.kubernetes.io/control-plane", "operator": "Exists", "effect": "NoSchedule"}]' | kubectl apply -f -
kubectl -n default wait --for=condition=Ready pod/cp-watch --timeout=180s
""",
97: CPWAIT + r"""
sudo mv /etc/kubernetes/manifests/kube-scheduler.yaml /etc/kubernetes/
for i in $(seq 1 40); do [ -z "$(kubectl -n kube-system get pod -l component=kube-scheduler -o name 2>/dev/null)" ] && break; sleep 3; done
kubectl -n default run manual-1 --image=busybox:1.36 -- sleep 86400
sleep 5
kubectl -n default get pod manual-1 -o json | jq 'del(.metadata.resourceVersion, .metadata.uid, .metadata.creationTimestamp, .status) | .spec.nodeName = "{CP}"' | kubectl replace --force -f -
kubectl -n default wait --for=condition=Ready pod/manual-1 --timeout=180s
sudo mv /etc/kubernetes/kube-scheduler.yaml /etc/kubernetes/manifests/
cpwait kube-scheduler
kubectl -n default run manual-2 --image=busybox:1.36 -- sleep 86400
kubectl -n default wait --for=condition=Ready pod/manual-2 --timeout=180s
""",
105: r"""
kubectl -n %{ns} set resources deploy %{dep} -c web --requests=cpu=100m
cat <<EOF | kubectl apply -f -
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: %{dep}, namespace: %{ns}}
spec:
scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: %{dep}}
minReplicas: %{min}
maxReplicas: %{max}
metrics:
- type: Resource
resource: {name: cpu, target: {type: Utilization, averageUtilization: %{cpu}}}
behavior: {scaleDown: {stabilizationWindowSeconds: %{win}, policies: [{type: Pods, value: 1, periodSeconds: 60}]}}
EOF
for i in $(seq 1 40); do [ "$(kubectl -n %{ns} get deploy %{dep} -o jsonpath='{.status.readyReplicas}')" -ge %{min} ] 2>/dev/null && break; sleep 3; done
""",
106: r"""
echo Unhealthy > {WS}/q106/reason.txt
kubectl -n %{ns} patch deploy %{dep} --type=json -p '[
{"op": "replace", "path": "/spec/template/spec/containers/0/livenessProbe/httpGet/port", "value": %{port}},
{"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/httpGet/path", "value": "/%{ready}"}]'
kubectl -n %{ns} rollout status deploy %{dep} --timeout=240s
""",
110: r"""
kubectl -n kube-system get cm kubeadm-config -o jsonpath='{.data.ClusterConfiguration}' | awk '/^controlPlaneEndpoint:/{print $2}' | tr -d '"' > {WS}/q110/endpoint.txt
KEY=$(sudo kubeadm init phase upload-certs --upload-certs 2>/dev/null | tail -1)
sudo kubeadm token create --print-join-command --certificate-key $KEY 2>/dev/null | grep 'kubeadm join' > {WS}/q110/join-control-plane.sh
""",
19: r"""
for i in $(seq 1 40); do kubectl -n troubleshoot logs crasher --previous 2>/dev/null | grep -q DB_HOST && break; sleep 3; done
kubectl -n troubleshoot logs crasher --previous > {WS}/q19/crasher.log
kubectl -n troubleshoot get pod crasher -o json | jq 'del(.metadata.resourceVersion, .metadata.uid, .metadata.creationTimestamp, .status)
| .spec.containers[0].env = [{"name": "DB_HOST", "value": "db.internal"}]' > /tmp/crasher.json
kubectl -n troubleshoot delete pod crasher --now
kubectl apply -f /tmp/crasher.json
kubectl -n troubleshoot wait --for=condition=Ready pod/crasher --timeout=180s
""",
41: r"""
A=$(kubectl version -o json | jq -r .serverVersion.gitVersion)
E=$(kubectl -n kube-system get pod -l component=etcd -o jsonpath='{.items[0].spec.containers[0].image}' | sed 's/.*://')
KV=$(kubectl get node %{node} -o jsonpath='{.status.nodeInfo.kubeletVersion}')
R=$(kubectl get node %{node} -o jsonpath='{.status.nodeInfo.containerRuntimeVersion}')
printf 'apiserver=%s\netcd=%s\nkubelet=%s\nruntime=%s\n' "$A" "$E" "$KV" "$R" > {WS}/q41/versions.txt
""",
93: r"""
kubectl -n oslo-secure create secret generic bundle --from-file={WS}/q93/tls-bundle.pem
kubectl -n oslo-secure create secret generic db-login --from-literal=user=svc-ledger --from-literal=pass='%{pw}'
kubectl -n oslo-secure run vault-reader --image=busybox:1.36 --dry-run=client -o json -- sleep 86400 | jq '
.spec.volumes = [{"name": "bundle", "secret": {"secretName": "bundle"}}]
| .spec.containers[0].volumeMounts = [{"name": "bundle", "mountPath": "/etc/bundle", "readOnly": true}]
| .spec.containers[0].env = [{"name": "APP_USER", "valueFrom": {"secretKeyRef": {"name": "db-login", "key": "user"}}},
{"name": "APP_PASS", "valueFrom": {"secretKeyRef": {"name": "db-login", "key": "pass"}}}]' | kubectl apply -f -
kubectl -n oslo-secure wait --for=condition=Ready pod/vault-reader --timeout=180s
""",
}
# ---- 1-113: the banks carried over from cka-sim.
# For these tasks the reference solution is a script as it stands, so the audit runs exactly the
# text the candidate is shown - and a solution that stops working is found the next time it runs.
AS_WRITTEN = [1, 21, 40, 55, 69, 75, 85, 98, 104, 109,
# bank_course.py: written to run as they stand
133]
def _as_written():
from cka.questions import QUESTIONS
text = {q["id"]: q["solution"] for q in QUESTIONS}
# $do is the shorthand the solutions use for kubectl's dry run (export it yourself in the exam)
return {i: 'export do="--dry-run=client -o yaml"\n' + text[i] for i in AS_WRITTEN if i not in SOLVE}
SOLVE.update(_as_written())