"""CKAD tasks 77-84: Application Design and Build - the pool tasks that take the domain from 9 to 17. 1.1 Images 77 a Dockerfile that does not build 78 what is inside this image? 1.2 Workloads 79 a StatefulSet with storage per replica 80 a Job that retries, then cleans up 1.3 Multi-container 81 an ambassador in front of a backend 82 init containers in the wrong order 1.4 Volumes 83 a claim that stays Pending 84 one file into a directory that has others Images are built and run on the task's host for real (its podman is the store the grader looks in); everything else is checked in the running Pods - what a container serves, what a file holds, whether a name resolves. Every task has a scripted solution in solve.py. """ from ckad.common import C, IMG, ONE, PY, NS, ns_pick, name_pick WORDS = ["amber", "basalt", "cobalt", "dune", "ember", "fjord", "garnet", "harbor"] QUESTIONS = [ # ========================================================================= 1.1 Images ==== { "id": 77, "domain": "DESIGN", "title": "A Dockerfile that does not build", "params": lambda r: {"img": name_pick(r, "ticker"), "word": r.choice(WORDS)}, "task": """ `{WS}/q77/image/` holds a script `run.sh` and a Dockerfile for it that does not build. Fix the **Dockerfile** — leave `run.sh` alone — so that: - the base image is `busybox:1.36`; - `run.sh` ends up in the image as `/app/run.sh`, executable; - the image's ENTRYPOINT runs `/app/run.sh`, in exec form (a JSON array). Build it on this host as `localhost/%{img}:1.0`, give the same image the second tag `localhost/%{img}:stable`, and save what a container of it prints to `{WS}/q77/out.txt`. """, "setup": r""" d="$WS/q77/image"; rm -rf "$WS/q77"; mkdir -p "$d" podman rmi -f localhost/%{img}:1.0 localhost/%{img}:stable >/dev/null 2>&1; true cat > "$d/run.sh" <<'EOF' #!/bin/sh echo "%{img} says %{word}" EOF cat > "$d/Dockerfile" <<'EOF' FROM docker.io/library/busybox:1.99-nope WORKDIR /app COPY app.sh /app/run.sh RUN chmod +x /app/start.sh ENTRYPOINT /app/run.sh EOF sha256sum "$d/run.sh" | cut -d' ' -f1 > "$PRIV/q77.sha" """, "checks": [ C("localhost/%{img}:1.0 and :stable exist and are the same image", 3, IMG + r""" a=$(img localhost/%{img}:1.0 '{{.Id}}'); b=$(img localhost/%{img}:stable '{{.Id}}'); [ -n "$a" ] && [ "$a" = "$b" ]"""), C("a container of it prints the script's line", 3, r""" podman run --rm localhost/%{img}:1.0 2>/dev/null | tr -d '\r' | grep -qx '%{img} says %{word}'"""), C("built from busybox:1.36, with an exec-form ENTRYPOINT, run.sh untouched", 2, IMG + r""" eq "$(img localhost/%{img}:1.0 '{{json .Config.Entrypoint}}')" '["/app/run.sh"]' || exit 1 grep -Eiq '^FROM +(docker\.io/)?(library/)?busybox:1\.36( |$)' "$WS/q77/image/Dockerfile" \ && eq "$(sha256sum "$WS/q77/image/run.sh" | cut -d' ' -f1)" "$(cat "$PRIV/q77.sha")" """), C("out.txt holds the output", 2, r"""grep -qx '%{img} says %{word}' "$WS/q77/out.txt" 2>/dev/null"""), ], "solution": """ cd {WS}/q77/image podman build -t localhost/%{img}:1.0 . # read the error, fix, build again - three times: # 1. the base tag does not exist -> FROM busybox:1.36 # 2. COPY names a file that is not there -> COPY run.sh /app/run.sh # 3. chmod names a file that is not there -> RUN chmod +x /app/run.sh cat > Dockerfile < workdir= port= tier= 2. Run a container from the image with the environment variable `MODE` set to `debug` and with the argument `check` in place of its default argument. Save its output to `{WS}/q78/run.txt`. """, "setup": r""" d="$WS/q78"; rm -rf "$d"; mkdir -p "$d/src" podman rmi -f localhost/%{img}:%{tag} >/dev/null 2>&1; true cat > "$d/src/start.sh" <<'EOF' echo "mode=$MODE port=$PORT user=$(id -u) action=$1" EOF cat > "$d/src/Dockerfile" <<'EOF' FROM docker.io/library/busybox:1.36 LABEL maintainer="platform@example.com" tier="%{tier}" ENV MODE=production PORT=%{port} WORKDIR /srv/%{img} EXPOSE %{port} COPY start.sh /usr/local/bin/start.sh USER %{uid} ENTRYPOINT ["/bin/sh", "/usr/local/bin/start.sh"] CMD ["serve"] EOF podman build -q -t localhost/%{img}:%{tag} "$d/src" >/dev/null 2>&1 || { echo "the image could not be built on the host" >&2; exit 1; } rm -rf "$d/src" podman image inspect localhost/%{img}:%{tag} --format '{{.Id}}' | tr -d '\r' > "$PRIV/q78.id"; [ -s "$PRIV/q78.id" ] """, "checks": [ C("facts.txt: user and workdir", 3, r""" grep -qx 'user=%{uid}' <(tr -d ' \r' < "$WS/q78/facts.txt" 2>/dev/null) && grep -qx 'workdir=/srv/%{img}/\?' <(tr -d ' \r' < "$WS/q78/facts.txt")"""), C("facts.txt: port and label", 2, r""" grep -qx 'port=%{port}\(/tcp\)\?' <(tr -d ' \r' < "$WS/q78/facts.txt" 2>/dev/null) && grep -qx 'tier=%{tier}' <(tr -d ' \r' < "$WS/q78/facts.txt")"""), C("run.txt is the output of a container with MODE=debug and the argument check", 4, r""" grep -qx 'mode=debug port=%{port} user=%{uid} action=check' <(tr -d '\r' < "$WS/q78/run.txt" 2>/dev/null)"""), C("the image is still the one that was there", 1, IMG + r""" [ -s "$WS/q78/run.txt" ] && eq "$(img localhost/%{img}:%{tag} '{{.Id}}')" "$(cat "$PRIV/q78.id")" """), ], "solution": """ podman image inspect localhost/%{img}:%{tag} --format \\ 'user={{.Config.User}} workdir={{.Config.WorkingDir}} ports={{.Config.ExposedPorts}} tier={{index .Config.Labels "tier"}}' # user=%{uid} workdir=/srv/%{img} ports=map[%{port}/tcp:{}] tier=%{tier} -> the four lines of {WS}/q78/facts.txt podman image inspect localhost/%{img}:%{tag} --format '{{.Config.Entrypoint}} {{.Config.Cmd}} {{.Config.Env}}' # [/bin/sh /usr/local/bin/start.sh] [serve] [... MODE=production PORT=%{port}] podman run --rm -e MODE=debug localhost/%{img}:%{tag} check | tee {WS}/q78/run.txt # Everything a Dockerfile declares is in the image's configuration: `inspect` (or `podman history`) # reads it back. Arguments after the image name replace CMD and are appended to ENTRYPOINT; -e sets # or overrides a variable. In a Pod the same two knobs are `args` and `env`. """, }, # ====================================================================== 1.2 Workloads ==== { "id": 79, "domain": "DESIGN", "title": "A StatefulSet with storage per replica", "params": lambda r: {"ns": ns_pick(r, "data"), "app": name_pick(r, "ledger")}, "task": """ In Namespace `%{ns}`, run a small replicated store whose replicas each keep their own data and their own name: 1. A **headless** Service `%{app}` for Pods labelled `app=%{app}`, port 80. 2. StatefulSet `%{app}`: 2 replicas, governed by that Service, image `busybox:1.36`, command sh -c 'hostname >> /data/started; exec httpd -f -p 80 -h /data' with a volume claim template `data` — `50Mi`, `ReadWriteOnce`, StorageClass `standard` — mounted at `/data`. 3. When both Pods run, delete Pod `%{app}-0` once. Its replacement must come back with the same name and the same volume: `/data/started` in it then has two lines. """, "setup": NS + r"""mkns %{ns}""", "checks": [ C("Service %{app} is headless and governs the StatefulSet", 2, r""" eq "$(j -n %{ns} svc %{app} '{.spec.clusterIP}')" None && eq "$(j -n %{ns} sts %{app} '{.spec.serviceName}')" %{app}"""), C("2 replicas are ready, each with its own bound claim from the template", 3, r""" eq "$(j -n %{ns} sts %{app} '{.status.readyReplicas}')" 2 && eq "$(j -n %{ns} sts %{app} '{.spec.volumeClaimTemplates[0].metadata.name}')" data || exit 1 for i in 0 1; do eq "$(j -n %{ns} pvc data-%{app}-$i '{.status.phase}')" Bound && eq "$(j -n %{ns} pvc data-%{app}-$i '{.spec.resources.requests.storage}')" 50Mi || exit 1; done"""), C("each replica has a DNS name of its own", 2, r""" eq "$(j -n %{ns} sts %{app} '{.status.readyReplicas}')" 2 || exit 1 $K -n %{ns} exec %{app}-1 -- nslookup %{app}-0.%{app}.%{ns}.svc.cluster.local >/dev/null 2>&1 \ && $K -n %{ns} exec %{app}-0 -- wget -T 3 -qO- http://%{app}-1.%{app}/started 2>/dev/null | grep -q '%{app}-1'"""), C("%{app}-0 came back with its volume: two starts on record, one for %{app}-1", 3, r""" [ "$($K -n %{ns} exec %{app}-0 -- sh -c 'grep -c %{app}-0 /data/started' 2>/dev/null)" -ge 2 ] \ && eq "$($K -n %{ns} exec %{app}-1 -- sh -c 'grep -c . /data/started' 2>/dev/null)" 1"""), ], "solution": """ cat <> /data/started; exec httpd -f -p 80 -h /data"] volumeMounts: [{name: data, mountPath: /data}] volumeClaimTemplates: - metadata: {name: data} spec: accessModes: [ReadWriteOnce] storageClassName: standard resources: {requests: {storage: 50Mi}} EOF k -n %{ns} rollout status sts %{app} k -n %{ns} delete pod %{app}-0 && k -n %{ns} rollout status sts %{app} k -n %{ns} exec %{app}-0 -- cat /data/started # two lines # There is no generator for a StatefulSet: write it from a Deployment's YAML (kind, serviceName, # volumeClaimTemplates; no strategy). The claims are named