kubestronaut-sim/ckad/bank_deploy.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

599 lines
32 KiB
Python

"""CKAD tasks 85-93: Application Deployment - the pool tasks that take the domain from 8 to 17.
2.1 Strategies 85 promote a canary 86 never two versions at once
2.2 Rolling updates 87 three changes, one rollout 88 which revision ran that image?
89 a rotated Secret the Pods have not seen
2.3 Helm 90 a release you did not install 91 a chart from a repository
92 a release that failed
2.4 Kustomize 93 a generated ConfigMap that rolls the Deployment
Rollouts are judged by what runs afterwards (images, environment, revisions), Helm by the release's
own history, Kustomize by building the directory and by the objects in the cluster. The Helm tasks
are seeded with the helm on this machine and solved with the one on the host. Every task has a
scripted solution in solve.py.
"""
from ckad.common import C, ONE, PY, ROLL, NS, ns_pick, name_pick
# A chart "webapp" in two versions, as task 21 has it: a Deployment <release>-webapp and a Service.
WEBAPP = r"""
# webapp_chart DIR VERSION TAG: write the chart's source into DIR/webapp
webapp_chart() {
local d="$1/webapp"; mkdir -p "$d/templates"
printf 'apiVersion: v2\nname: webapp\nversion: %s\nappVersion: "%s"\n' "$2" "$3" > "$d/Chart.yaml"
printf 'replicaCount: 1\nimage:\n repository: nginx\n tag: "%s"\nservice:\n type: ClusterIP\n port: 80\n' "$3" > "$d/values.yaml"
cat > "$d/templates/deployment.yaml" <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-webapp
labels: {app.kubernetes.io/instance: {{ .Release.Name }}, chart: {{ .Chart.Name }}-{{ .Chart.Version }}}
spec:
replicas: {{ .Values.replicaCount }}
selector: {matchLabels: {app.kubernetes.io/instance: {{ .Release.Name }}}}
template:
metadata: {labels: {app.kubernetes.io/instance: {{ .Release.Name }}}}
spec:
containers:
- name: web
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports: [{containerPort: 80}]
EOF
cat > "$d/templates/service.yaml" <<'EOF'
apiVersion: v1
kind: Service
metadata: {name: {{ .Release.Name }}-webapp}
spec:
type: {{ .Values.service.type }}
selector: {app.kubernetes.io/instance: {{ .Release.Name }}}
ports: [{port: {{ .Values.service.port }}, targetPort: 80}]
EOF
}
# hist NS RELEASE 'PYTHON EXPRESSION over h': the release's history (a list of revisions, oldest first) satisfies it
hist() { helm -n "$1" history "$2" -o json 2>/dev/null | CODE="$3" python3 -c '
import json, os, sys
try:
h = json.load(sys.stdin)
except Exception:
sys.exit(1)
sys.exit(0 if eval(os.environ["CODE"], {"h": h}) else 1)'; }
# vals NS RELEASE 'PYTHON EXPRESSION over v': the values the release was given satisfy it
vals() { helm -n "$1" get values "$2" -o json 2>/dev/null | CODE="$3" python3 -c '
import json, os, sys
try:
v = json.load(sys.stdin) or {}
except Exception:
sys.exit(1)
sys.exit(0 if eval(os.environ["CODE"], {"v": v}) else 1)'; }
"""
QUESTIONS = [
# ===================================================================== 2.1 Strategies ====
{
"id": 85, "domain": "DEPLOY", "title": "Promote a canary",
"params": lambda r: {"ns": ns_pick(r, "shop"), "app": name_pick(r, "cart")},
"task": """
Namespace `%{ns}` runs a canary release behind Service `%{app}`: Deployment `%{app}-stable`
(4 replicas) and Deployment `%{app}-canary` (1 replica) with a newer image. The canary has done
well for a week. Promote it:
1. Write the image the canary runs to `{WS}/q85/image.txt`.
2. Bring `%{app}-stable` to that image, on all 4 replicas, with a normal rolling update.
3. When that rollout is finished, delete `%{app}-canary`.
Do not touch the Service. It must end up with exactly 4 endpoints, all on the new image.
""",
"setup": NS + ROLL + r"""
mkns %{ns}; rm -rf "$WS/q85"; mkdir -p "$WS/q85"
for t in stable:4:nginx:1.25 canary:1:nginx:1.27; do
IFS=: read -r track n repo tag <<< "$t"
$K apply -f - >/dev/null <<EOF
apiVersion: apps/v1
kind: Deployment
metadata: {name: %{app}-$track, namespace: %{ns}}
spec:
replicas: $n
selector: {matchLabels: {app: %{app}, track: $track}}
template:
metadata: {labels: {app: %{app}, track: $track}}
spec:
containers:
- {name: web, image: "$repo:$tag", ports: [{containerPort: 80}]}
EOF
done
$K -n %{ns} create service clusterip %{app} --tcp=80:80 >/dev/null
$K -n %{ns} patch svc %{app} -p '{"spec":{"selector":{"app":"%{app}"}}}' >/dev/null
rolled %{ns} %{app}-stable 4 240 && rolled %{ns} %{app}-canary 1 240 || exit 1
j -n %{ns} svc %{app} '{.metadata.uid}' > "$PRIV/q85.uid"
""",
"checks": [
C("image.txt names the canary's image", 2, ONE + r"""eq "$(one "$WS/q85/image.txt")" nginx:1.27"""),
C("%{app}-stable runs that image on all 4 replicas, the rollout finished", 4, ROLL + r"""
eq "$(j -n %{ns} deploy %{app}-stable '{.spec.template.spec.containers[0].image}')" nginx:1.27 && rolled %{ns} %{app}-stable 4 60"""),
C("%{app}-canary is deleted", 2, r"""
eq "$(j -n %{ns} deploy %{app}-stable '{.spec.template.spec.containers[0].image}')" nginx:1.27 && [ -z "$(j -n %{ns} deploy %{app}-canary '{.metadata.name}')" ]"""),
C("the same Service has 4 endpoints, all on the new image", 2, PY + r"""
eq "$(j -n %{ns} svc %{app} '{.metadata.uid}')" "$(cat "$PRIV/q85.uid")" && eq "$(j -n %{ns} svc %{app} '{.spec.selector}')" '{"app":"%{app}"}' || exit 1
py -n %{ns} pod -l app=%{app} '
p = [x for x in o["items"] if "deletionTimestamp" not in x["metadata"]]
len(p) == 4 and all(x["spec"]["containers"][0]["image"] == "nginx:1.27" and any(c["type"] == "Ready" and c["status"] == "True" for c in x["status"]["conditions"]) for x in p)'"""),
],
"solution": """
k -n %{ns} get deploy -o wide # the IMAGES column: stable 1.25, canary 1.27
echo nginx:1.27 > {WS}/q85/image.txt
k -n %{ns} set image deploy/%{app}-stable web=nginx:1.27
k -n %{ns} rollout status deploy/%{app}-stable # wait: only then is the canary redundant
k -n %{ns} delete deploy %{app}-canary
k -n %{ns} get endpointslices -l kubernetes.io/service-name=%{app}
# A canary by replica count is two Deployments under one Service selector: the share of traffic is
# the share of Pods. Promoting it is an ordinary rolling update of the stable Deployment - the
# Service keeps answering throughout - and the canary goes last, not first.
""",
},
{
"id": 86, "domain": "DEPLOY", "title": "Never two versions at once",
"params": lambda r: {"ns": ns_pick(r, "ledger"), "dep": name_pick(r, "posting")},
"task": """
The application of Deployment `%{dep}` in Namespace `%{ns}` (3 replicas, `nginx:1.25`) migrates a
shared database when it starts: an old and a new version must **never run side by side**, not even
for a moment. A short outage during an update is accepted.
1. Change the Deployment's update strategy accordingly.
2. Then update its image to `nginx:1.27`.
3. Write the name of the strategy type you chose to `{WS}/q86/strategy.txt`.
""",
"setup": NS + ROLL + r"""
mkns %{ns}; rm -rf "$WS/q86"; mkdir -p "$WS/q86"
$K -n %{ns} create deploy %{dep} --image=nginx:1.25 --replicas=3 >/dev/null
rolled %{ns} %{dep} 3 240
""",
"checks": [
C("the Deployment's strategy is Recreate", 4, r"""
eq "$(j -n %{ns} deploy %{dep} '{.spec.strategy.type}')" Recreate && [ -z "$(j -n %{ns} deploy %{dep} '{.spec.strategy.rollingUpdate}')" ]"""),
C("it runs nginx:1.27 on all 3 replicas", 4, ROLL + r"""
eq "$(j -n %{ns} deploy %{dep} '{.spec.strategy.type}')" Recreate && eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].image}')" nginx:1.27 && rolled %{ns} %{dep} 3 90"""),
C("strategy.txt says Recreate", 2, ONE + r"""eq "$(one "$WS/q86/strategy.txt")" Recreate"""),
],
"solution": """
k -n %{ns} patch deploy %{dep} --type=json -p '[{"op":"replace","path":"/spec/strategy","value":{"type":"Recreate"}}]'
# with `kubectl edit`: set type: Recreate AND delete the rollingUpdate block, or the change is refused
k -n %{ns} set image deploy/%{dep} nginx=nginx:1.27
k -n %{ns} get pod -w # all three terminate, then three new ones start
echo Recreate > {WS}/q86/strategy.txt
# RollingUpdate (the default) keeps the application up by overlapping old and new Pods; Recreate
# scales the old ReplicaSet to zero first and trades availability for never mixing versions.
# maxSurge 0 would still leave old and new Pods running together during the update.
""",
},
# ================================================================ 2.2 Rolling updates ====
{
"id": 87, "domain": "DEPLOY", "title": "Three changes, one rollout",
"params": lambda r: {"ns": ns_pick(r, "api"), "dep": name_pick(r, "orders")},
"task": """
Deployment `%{dep}` in Namespace `%{ns}` (3 replicas, `nginx:1.25`) is due for three changes:
- image `nginx:1.27`,
- environment variable `LOG_LEVEL=debug`,
- resource requests of `50m` CPU and `64Mi` memory.
Each change on its own would restart every Pod. Make all three reach the Pods in **one** rollout:
afterwards the Deployment has exactly one more revision than it has now.
""",
"setup": NS + ROLL + r"""
mkns %{ns}
$K -n %{ns} create deploy %{dep} --image=nginx:1.25 --replicas=3 >/dev/null
rolled %{ns} %{dep} 3 240 && eq "$(j -n %{ns} deploy %{dep} '{.metadata.annotations.deployment\.kubernetes\.io/revision}')" 1
""",
"checks": [
C("the Pod template has all three changes and the Deployment is not paused", 3, PY + r"""
py -n %{ns} deploy %{dep} '
c = o["spec"]["template"]["spec"]["containers"][0]; r = c.get("resources", {}).get("requests", {})
c["image"] == "nginx:1.27" and any(e["name"] == "LOG_LEVEL" and e.get("value") == "debug" for e in c.get("env", [])) and r.get("cpu") == "50m" and r.get("memory") == "64Mi" and not o["spec"].get("paused")'"""),
C("all 3 replicas run the new template", 3, ROLL + r"""
eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].image}')" nginx:1.27 && rolled %{ns} %{dep} 3 90"""),
C("it took one rollout: the Deployment is at revision 2, with two ReplicaSets", 4, r"""
eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].image}')" nginx:1.27 || exit 1
eq "$(j -n %{ns} deploy %{dep} '{.metadata.annotations.deployment\.kubernetes\.io/revision}')" 2 && eq "$($K -n %{ns} get rs -l app=%{dep} -o name | wc -l)" 2"""),
],
"solution": """
k -n %{ns} rollout pause deploy/%{dep}
k -n %{ns} set image deploy/%{dep} nginx=nginx:1.27
k -n %{ns} set env deploy/%{dep} LOG_LEVEL=debug
k -n %{ns} set resources deploy/%{dep} --requests=cpu=50m,memory=64Mi
k -n %{ns} rollout resume deploy/%{dep}
k -n %{ns} rollout status deploy/%{dep}; k -n %{ns} rollout history deploy/%{dep} # revisions 1 and 2
# A paused Deployment records changes to its template without acting on them; resume starts one
# rollout for the lot. A single `kubectl edit` or one `kubectl apply` of the three changes does the
# same - the point is that every separate change of the template is a revision and a restart.
""",
},
{
"id": 88, "domain": "DEPLOY", "title": "Which revision ran that image?",
"params": lambda r: (lambda rev: {"ns": ns_pick(r, "media"), "dep": name_pick(r, "encoder"), "rev": rev, "tag": "1.3%d" % (3 + rev)})(r.choice([1, 2, 3])),
"task": """
Deployment `%{dep}` in Namespace `%{ns}` has been through four revisions. Since the last one it
leaks memory, and the release notes say the last version without the leak used image
`busybox:%{tag}`.
1. Find the revision that ran that image and write its number to `{WS}/q88/revision.txt`.
2. Roll the Deployment back to exactly that revision.
3. Set the change cause of the revision this produces to `rollback to busybox:%{tag}`.
""",
"setup": NS + ROLL + r"""
mkns %{ns}; rm -rf "$WS/q88"; mkdir -p "$WS/q88"
$K -n %{ns} create deploy %{dep} --image=busybox:1.34 --replicas=2 -- sleep 86400 >/dev/null
$K -n %{ns} annotate deploy %{dep} kubernetes.io/change-cause="initial release" >/dev/null
rolled %{ns} %{dep} 2 240 || exit 1
for t in 1.35 1.36 1.37; do
$K -n %{ns} set image deploy/%{dep} busybox=busybox:$t >/dev/null
$K -n %{ns} annotate deploy %{dep} kubernetes.io/change-cause="update to $t" --overwrite >/dev/null
rolled %{ns} %{dep} 2 240 || exit 1
done
eq "$(j -n %{ns} deploy %{dep} '{.metadata.annotations.deployment\.kubernetes\.io/revision}')" 4
""",
"checks": [
C("revision.txt holds the number of the revision that ran busybox:%{tag}", 3, ONE + r"""eq "$(one "$WS/q88/revision.txt")" %{rev}"""),
C("the Deployment is rolled back to it: busybox:%{tag} on both replicas, as revision 5", 4, ROLL + r"""
eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].image}')" busybox:%{tag} && rolled %{ns} %{dep} 2 90 \
&& eq "$(j -n %{ns} deploy %{dep} '{.metadata.annotations.deployment\.kubernetes\.io/revision}')" 5"""),
C("its change cause is the one asked for", 3, r"""
eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].image}')" busybox:%{tag} \
&& eq "$(j -n %{ns} deploy %{dep} '{.metadata.annotations.kubernetes\.io/change-cause}')" "rollback to busybox:%{tag}" """),
],
"solution": """
k -n %{ns} rollout history deploy/%{dep} # REVISION and CHANGE-CAUSE, no images
for i in 1 2 3 4; do echo "== $i"; k -n %{ns} rollout history deploy/%{dep} --revision=$i | grep -i image; done
# revision %{rev} is busybox:%{tag} -> {WS}/q88/revision.txt
k -n %{ns} rollout undo deploy/%{dep} --to-revision=%{rev}
k -n %{ns} rollout status deploy/%{dep}
k -n %{ns} annotate deploy %{dep} kubernetes.io/change-cause="rollback to busybox:%{tag}" --overwrite
k -n %{ns} rollout history deploy/%{dep} # revision %{rev} is gone: it has become revision 5
# `rollout undo` without --to-revision goes back one step only. A rollback does not rewind the
# counter: the old ReplicaSet is reused under a new revision number. The CHANGE-CAUSE column is the
# annotation kubernetes.io/change-cause of the Deployment at the time of the rollout.
""",
},
{
"id": 89, "domain": "DEPLOY", "title": "A rotated Secret the Pods have not seen",
"params": lambda r: {"ns": ns_pick(r, "billing"), "app": name_pick(r, "invoicer"),
"pw": "pw-" + "".join(r.choice("abcdefghjkmnpqrstuvwxyz23456789") for _ in range(10))},
"task": """
Secret `%{app}-db` in Namespace `%{ns}` was rotated an hour ago, but the Pods of Deployment `%{app}`
still run with the old password: they read it into the environment variable `DB_PASSWORD` when
they started.
1. Make every Pod of the Deployment pick up the current value **without** changing the
Deployment's image, command or replica count, and without deleting Pods by hand: restart the
rollout.
2. Record the reason: set the Deployment's change cause to `secret rotation`.
3. Save the first line one of the new Pods logged to `{WS}/q89/started.txt`.
""",
"setup": NS + ROLL + r"""
mkns %{ns}; rm -rf "$WS/q89"; mkdir -p "$WS/q89"
$K -n %{ns} create secret generic %{app}-db --from-literal=password=old-%{pw} >/dev/null
$K apply -f - >/dev/null <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: %{app}, namespace: %{ns}}
spec:
replicas: 2
selector: {matchLabels: {app: %{app}}}
template:
metadata: {labels: {app: %{app}}}
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo \"started with $DB_PASSWORD\"; exec sleep 2147483647"]
env:
- {name: DB_PASSWORD, valueFrom: {secretKeyRef: {name: %{app}-db, key: password}}}
EOF
rolled %{ns} %{app} 2 240 || exit 1
$K -n %{ns} create secret generic %{app}-db --from-literal=password=%{pw} --dry-run=client -o yaml | $K apply -f - >/dev/null 2>&1
""",
"checks": [
C("every Pod of the Deployment now has the current password", 4, ROLL + r"""
rolled %{ns} %{app} 2 60 || exit 1
n=0; for p in $($K -n %{ns} get pod -l app=%{app} -o json | python3 -c '
import json, sys
[print(x["metadata"]["name"]) for x in json.load(sys.stdin)["items"] if "deletionTimestamp" not in x["metadata"]]'); do
eq "$($K -n %{ns} exec "$p" -- printenv DB_PASSWORD 2>/dev/null)" %{pw} || exit 1; n=$((n+1))
done; [ $n -ge 2 ]"""),
C("through a restart of the rollout: revision 2, image, command and replicas unchanged", 3, PY + r"""
py -n %{ns} deploy %{app} '
t = o["spec"]["template"]; c = t["spec"]["containers"][0]
"kubectl.kubernetes.io/restartedAt" in t["metadata"].get("annotations", {}) and o["metadata"]["annotations"].get("deployment.kubernetes.io/revision") == "2" and c["image"] == "busybox:1.36" and "started with" in c["command"][-1] and o["spec"]["replicas"] == 2'"""),
C("the change cause is recorded", 2, r"""eq "$(j -n %{ns} deploy %{app} '{.metadata.annotations.kubernetes\.io/change-cause}')" "secret rotation" """),
C("started.txt is a new Pod's first line", 1, r"""grep -qx 'started with %{pw}' <(tr -d '\r' < "$WS/q89/started.txt" 2>/dev/null)"""),
],
"solution": """
k -n %{ns} exec deploy/%{app} -- printenv DB_PASSWORD # old-...: the value from when the Pod started
k -n %{ns} rollout restart deploy/%{app}
k -n %{ns} rollout status deploy/%{app}
k -n %{ns} annotate deploy %{app} kubernetes.io/change-cause="secret rotation" --overwrite
k -n %{ns} logs deploy/%{app} | head -1 | tee {WS}/q89/started.txt
# Environment variables from a Secret or ConfigMap are copied in when the container starts; nothing
# updates them afterwards (mounted files are updated, env never is). `rollout restart` stamps an
# annotation into the Pod template - which makes it a new revision and a normal rolling update,
# with no other change to the Deployment.
""",
},
# =========================================================================== 2.3 Helm ====
{
"id": 90, "domain": "DEPLOY", "title": "A Helm release you did not install",
"params": lambda r: {"ns": ns_pick(r, "tracking"), "rel": name_pick(r, "fleet"), "mode": r.choice(["strict", "relaxed", "audit"]), "region": r.choice(["eu", "us", "ap"])},
"task": """
A Helm release named `%{rel}` runs somewhere in this cluster. Nobody on the team installed it.
Find out what it is, and write to `{WS}/q90/`:
1. `namespace.txt` — the Namespace the release is in;
2. `chart.txt` — its chart and chart version, as `helm list` shows them (for example `nginx-1.4.2`);
3. `values.yaml` — the values that were **supplied** when it was installed or upgraded (not the
chart's defaults), as YAML;
4. `manifest.yaml` — the manifest Helm rendered and applied for it.
Change nothing.
""",
"setup": NS + r"""
command -v helm >/dev/null || { echo "helm is not installed on this machine - the task needs it"; exit 1; }
mkns %{ns}; mkns %{ns}-old; rm -rf "$WS/q90"; mkdir -p "$WS/q90"
d=$(mktemp -d)/tracker; mkdir -p "$d/templates"
printf 'apiVersion: v2\nname: tracker\nversion: 0.4.2\nappVersion: "2.1"\n' > "$d/Chart.yaml"
printf 'replicaCount: 1\nimage:\n tag: "1.27"\nconfig:\n mode: standard\n region: eu\n retention: 30\n' > "$d/values.yaml"
cat > "$d/templates/all.yaml" <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata: {name: {{ .Release.Name }}-tracker}
data:
MODE: {{ .Values.config.mode | quote }}
REGION: {{ .Values.config.region | quote }}
RETENTION: {{ .Values.config.retention | quote }}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: {{ .Release.Name }}-tracker}
spec:
replicas: {{ .Values.replicaCount }}
selector: {matchLabels: {app: {{ .Release.Name }}-tracker}}
template:
metadata: {labels: {app: {{ .Release.Name }}-tracker}}
spec:
containers:
- name: tracker
image: "nginx:{{ .Values.image.tag }}"
envFrom: [{configMapRef: {name: {{ .Release.Name }}-tracker}}]
EOF
helm -n %{ns} install %{rel} "$d" --set replicaCount=3 --set config.mode=%{mode} --set config.region=%{region} >/dev/null || exit 1
helm -n %{ns}-old install %{rel}-legacy "$d" >/dev/null
rm -rf "$(dirname "$d")"
""",
"checks": [
C("namespace.txt and chart.txt", 4, ONE + r"""eq "$(one "$WS/q90/namespace.txt")" %{ns} && eq "$(one "$WS/q90/chart.txt")" tracker-0.4.2"""),
C("values.yaml holds the supplied values, and only those", 3, r"""
nx "$CP" 'yq -o=json /opt/course/q90/values.yaml' </dev/null 2>/dev/null | python3 -c '
import json, sys
v = json.load(sys.stdin); v.pop("USER-SUPPLIED VALUES", None)
sys.exit(0 if v == {"replicaCount": 3, "config": {"mode": "%{mode}", "region": "%{region}"}} else 1)'"""),
C("manifest.yaml is the release's rendered manifest", 3, r"""
f="$WS/q90/manifest.yaml"
grep -q '^# Source: tracker/templates/all.yaml' "$f" 2>/dev/null && grep -q 'name: %{rel}-tracker' "$f" && grep -q 'kind: Deployment' "$f" && grep -q 'MODE: "%{mode}"' "$f" """),
],
"solution": """
helm list -A | grep %{rel} # NAMESPACE %{ns} ... CHART tracker-0.4.2
cd {WS}/q90
echo %{ns} > namespace.txt; echo tracker-0.4.2 > chart.txt
helm -n %{ns} get values %{rel} -o yaml > values.yaml # supplied values only; --all adds the defaults
helm -n %{ns} get manifest %{rel} > manifest.yaml
helm -n %{ns} status %{rel}; helm -n %{ns} history %{rel}
# Helm commands are per Namespace: without -n (or -A for list) a release elsewhere does not show.
# `helm get` reads back what was stored with the release: values, manifest, notes, hooks, all.
# `helm show values CHART` is the other direction - a chart's defaults, before anything is installed.
""",
},
{
"id": 91, "domain": "DEPLOY", "title": "A chart from a repository",
"params": lambda r: {"ns": ns_pick(r, "portal"), "rel": name_pick(r, "front")},
"task": """
The team publishes its charts in a Helm repository that is reachable from this host at
`http://localhost:30080`.
1. Add it to helm on this host under the name `internal`.
2. Write the versions of chart `webapp` it offers to `{WS}/q91/versions.txt`, one per line.
3. Install release `%{rel}` into Namespace `%{ns}` (create it) from that repository, at chart
version **1.2.0** — not the newest — with `replicaCount=2`.
4. Then upgrade the release to the newest version of the chart, keeping its values.
""",
"setup": NS + WEBAPP + r"""
command -v helm >/dev/null || { echo "helm is not installed on this machine - the task needs it"; exit 1; }
rm -rf "$WS/q91"; mkdir -p "$WS/q91"; mkns chart-repo
src=$(mktemp -d); mkdir -p "$src/repo"
webapp_chart "$src/1.2.0" 1.2.0 1.25; webapp_chart "$src/1.3.0" 1.3.0 1.27
helm package "$src/1.2.0/webapp" -d "$src/repo" >/dev/null && helm package "$src/1.3.0/webapp" -d "$src/repo" >/dev/null && helm repo index "$src/repo" >/dev/null || exit 1
$K -n chart-repo create configmap charts --from-file="$src/repo" >/dev/null || exit 1
rm -rf "$src"
$K apply -f - >/dev/null <<'EOF'
apiVersion: v1
kind: Pod
metadata: {name: chart-repo, namespace: chart-repo, labels: {app: chart-repo}}
spec:
volumes: [{name: charts, configMap: {name: charts}}]
containers:
- name: nginx
image: nginx:1.27
volumeMounts: [{name: charts, mountPath: /usr/share/nginx/html}]
---
apiVersion: v1
kind: Service
metadata: {name: chart-repo, namespace: chart-repo}
spec:
type: NodePort
selector: {app: chart-repo}
ports: [{port: 80, targetPort: 80, nodePort: 30080}]
EOF
$K -n chart-repo wait --for=condition=Ready pod/chart-repo --timeout=240s >/dev/null || exit 1
for i in $(seq 1 20); do nx "$CP" 'curl -s -m 3 http://localhost:30080/index.yaml' </dev/null 2>/dev/null | grep -q 'webapp-1.3.0.tgz' && exit 0; sleep 2; done
echo "the chart repository does not answer on the host" >&2; exit 1
""",
"checks": [
C("versions.txt lists the two versions of webapp", 2, r"""
[ -s "$WS/q91/versions.txt" ] && diff <(tr -d ' \rv' < "$WS/q91/versions.txt" | grep . | sort -u) <(printf '1.2.0\n1.3.0\n') >/dev/null"""),
C("release %{rel} was installed at chart 1.2.0 and upgraded to 1.3.0, and is deployed", 4, WEBAPP + r"""
hist %{ns} %{rel} 'len(h) >= 2 and h[0]["chart"] == "webapp-1.2.0" and h[-1]["chart"] == "webapp-1.3.0" and h[-1]["status"] == "deployed"'"""),
C("it kept replicaCount=2 through the upgrade", 2, WEBAPP + r"""
hist %{ns} %{rel} 'h[-1]["chart"] == "webapp-1.3.0"' && vals %{ns} %{rel} 'v.get("replicaCount") == 2' && eq "$(j -n %{ns} deploy %{rel}-webapp '{.spec.replicas}')" 2"""),
C("the repository is configured on the host as internal", 2, r"""
for u in candidate root; do
nx "$CP" "su - $u -c 'helm repo list -o json'" </dev/null 2>/dev/null | python3 -c '
import json, sys
try:
r = json.load(sys.stdin)
except Exception:
sys.exit(1)
sys.exit(0 if any(x["name"] == "internal" and x["url"].rstrip("/") == "http://localhost:30080" for x in r) else 1)' && exit 0
done; exit 1"""),
],
"solution": """
helm repo add internal http://localhost:30080
helm repo update
helm search repo internal/webapp --versions # 1.3.0 and 1.2.0 -> {WS}/q91/versions.txt
helm install %{rel} internal/webapp --version 1.2.0 -n %{ns} --create-namespace --set replicaCount=2
helm upgrade %{rel} internal/webapp -n %{ns} --reuse-values # no --version: the newest
helm -n %{ns} history %{rel}; helm -n %{ns} get values %{rel}
# `search repo` shows only the newest version of each chart unless you ask for --versions, and it
# searches the local copy of the index: `repo update` first. An upgrade without --reuse-values (or
# the same --set again) puts every value back to the new chart's default.
""",
},
{
"id": 92, "domain": "DEPLOY", "title": "A release that failed",
"params": lambda r: {"ns": ns_pick(r, "search"), "rel": name_pick(r, "indexer")},
"task": """
Helm release `%{rel}` in Namespace `%{ns}` is in status `failed` since its last upgrade. Its chart
is in `{WS}/q92/chart`.
1. Find the value that broke it, and write its name — as `--set` would spell it, for example
`service.type` — to `{WS}/q92/key.txt`.
2. Bring the release back to `deployed` with another **upgrade** that corrects that one value to
`1.27` and keeps every other value the release has. Do not roll back and do not reinstall.
3. Release `%{rel}-old` in Namespace `%{ns}-archive` is no longer used: uninstall it.
""",
"setup": NS + WEBAPP + ROLL + r"""
command -v helm >/dev/null || { echo "helm is not installed on this machine - the task needs it"; exit 1; }
mkns %{ns}; mkns %{ns}-archive; rm -rf "$WS/q92"; mkdir -p "$WS/q92"
webapp_chart "$WS/q92" 1.3.0 1.27 && mv "$WS/q92/webapp" "$WS/q92/chart"
helm -n %{ns} install %{rel} "$WS/q92/chart" --set replicaCount=2 --set service.port=8080 >/dev/null || exit 1
rolled %{ns} %{rel}-webapp 2 240 || exit 1
helm -n %{ns}-archive install %{rel}-old "$WS/q92/chart" >/dev/null
helm -n %{ns} upgrade %{rel} "$WS/q92/chart" --reuse-values --set image.tag=1.99-nope --wait --timeout 25s >/dev/null 2>&1
hist %{ns} %{rel} 'len(h) == 2 and h[-1]["status"] == "failed"' || { echo "the upgrade did not leave the release failed" >&2; exit 1; }
""",
"checks": [
C("key.txt names the value", 2, ONE + r"""eq "$(one "$WS/q92/key.txt")" image.tag"""),
C("the release is deployed again, on nginx:1.27, with its other values kept", 4, WEBAPP + ROLL + r"""
hist %{ns} %{rel} 'h[-1]["status"] == "deployed"' && vals %{ns} %{rel} 'v.get("replicaCount") == 2 and v.get("service", {}).get("port") == 8080 and str(v.get("image", {}).get("tag")) == "1.27"' || exit 1
eq "$(j -n %{ns} deploy %{rel}-webapp '{.spec.template.spec.containers[0].image}')" nginx:1.27 && rolled %{ns} %{rel}-webapp 2 60"""),
C("by a further upgrade: the failed revision is still in its history", 2, WEBAPP + r"""
hist %{ns} %{rel} 'len(h) >= 3 and h[-1]["status"] == "deployed" and any(x["status"] == "failed" for x in h) and "ollback" not in h[-1]["description"]'"""),
C("release %{rel}-old is uninstalled", 2, r"""
! helm -n %{ns}-archive status %{rel}-old >/dev/null 2>&1 && [ -z "$(j -n %{ns}-archive deploy %{rel}-old-webapp '{.metadata.name}')" ] && helm -n %{ns} status %{rel} >/dev/null 2>&1"""),
],
"solution": """
helm -n %{ns} list -a; helm -n %{ns} history %{rel} # revision 2: failed
helm -n %{ns} get values %{rel} # image.tag: 1.99-nope <- no such image
k -n %{ns} get pod # ImagePullBackOff says the same
echo image.tag > {WS}/q92/key.txt
helm -n %{ns} upgrade %{rel} {WS}/q92/chart --reuse-values --set image.tag=1.27 --wait
helm -n %{ns} history %{rel} # 1 superseded, 2 failed, 3 deployed
helm -n %{ns}-archive uninstall %{rel}-old
# A failed upgrade leaves the release at the failed revision with the old Pods still running (the
# rolling update never finished). `helm rollback` would have gone back to revision 1; an upgrade
# with --reuse-values takes the failed revision's values and changes only what you --set.
""",
},
# ====================================================================== 2.4 Kustomize ====
{
"id": 93, "domain": "DEPLOY", "title": "A generated ConfigMap that rolls the Deployment",
"params": lambda r: {"ns": ns_pick(r, "render"), "app": name_pick(r, "painter")},
"task": """
`{WS}/q93/app/` is a Kustomize directory with a Deployment `%{app}` that reads its settings from a
ConfigMap named `%{app}-config` — which nothing creates yet. `settings.env` beside it holds the
settings, one `KEY=value` per line.
1. Extend `kustomization.yaml`: everything goes into Namespace `%{ns}`, and a ConfigMap
`%{app}-config` is **generated** from `settings.env`, each line becoming a key. Keep the hash
that Kustomize appends to generated names.
2. Apply the directory with kubectl's built-in Kustomize.
3. Change `MODE` in `settings.env` from `blue` to `green` and apply again. The Deployment must
roll to the new settings by itself — that is what the hash is for.
""",
"setup": NS + r"""
mkns %{ns}; d="$WS/q93/app"; rm -rf "$WS/q93"; mkdir -p "$d"
cat > "$d/deployment.yaml" <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: %{app}
spec:
replicas: 2
selector:
matchLabels:
app: %{app}
template:
metadata:
labels:
app: %{app}
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "86400"]
envFrom:
- configMapRef:
name: %{app}-config
EOF
printf 'apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n - deployment.yaml\n' > "$d/kustomization.yaml"
printf 'MODE=blue\nREGION=eu\n' > "$d/settings.env"
""",
"checks": [
C("the directory builds: a ConfigMap generated from settings.env with a hashed name, in %{ns}", 3, r"""
kubectl kustomize "$WS/q93/app" 2>/dev/null | python3 -c '
import re, sys
t = sys.stdin.read(); docs = t.split("\n---\n")
cm = [d for d in docs if re.search(r"^kind: ConfigMap$", d, re.M)]
dep = [d for d in docs if re.search(r"^kind: Deployment$", d, re.M)]
ok = len(cm) == 1 and len(dep) == 1
m = ok and re.search(r"^ name: (%{app}-config-[a-z0-9]{6,})$", cm[0], re.M)
ok = bool(m) and "MODE: green" in cm[0] and "REGION: eu" in cm[0] and "namespace: %{ns}" in cm[0] and "namespace: %{ns}" in dep[0] and ("name: " + m.group(1)) in dep[0]
sys.exit(0 if ok else 1)'"""),
C("in %{ns} the Deployment runs with MODE=green from a generated ConfigMap", 4, ROLL + r"""
n=$(j -n %{ns} deploy %{app} '{.spec.template.spec.containers[0].envFrom[0].configMapRef.name}')
echo "$n" | grep -Eq '^%{app}-config-[a-z0-9]{6,}$' && eq "$(j -n %{ns} cm "$n" '{.data.MODE}')" green && rolled %{ns} %{app} 2 90 || exit 1
eq "$($K -n %{ns} exec "$(newest %{ns} app=%{app})" -- printenv MODE 2>/dev/null)" green"""),
C("it rolled once, from the ConfigMap that said blue", 3, r"""
eq "$(j -n %{ns} deploy %{app} '{.metadata.annotations.deployment\.kubernetes\.io/revision}')" 2 || exit 1
[ "$($K -n %{ns} get cm -o name | grep -c '/%{app}-config-')" -ge 2 ] && $K -n %{ns} get cm -o jsonpath='{range .items[*]}{.metadata.name}={.data.MODE}{"\n"}{end}' | grep -q '^%{app}-config-.*=blue$'"""),
],
"solution": """
cd {WS}/q93/app
cat > kustomization.yaml <<EOF
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: %{ns}
resources:
- deployment.yaml
configMapGenerator:
- name: %{app}-config
envs:
- settings.env
EOF
k kustomize . # the ConfigMap is %{app}-config-<hash>, and the Deployment refers to that name
k apply -k . && k -n %{ns} rollout status deploy/%{app}
sed -i 's/^MODE=blue/MODE=green/' settings.env
k apply -k . && k -n %{ns} rollout status deploy/%{app}
k -n %{ns} exec deploy/%{app} -- printenv MODE # green
# The hash is of the content: new content, new name - and Kustomize rewrites every reference to it,
# so the Pod template changes and the Deployment rolls. `envs:` makes each line a key; `files:`
# would make the whole file one key. The old ConfigMap stays behind until somebody prunes it.
""",
},
]