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

393 lines
22 KiB
Python

"""CKAD tasks 100-105: Application Environment, Configuration and Security - the pool tasks that take the domain from 11 to 17.
4.2 Access 100 one may read, one may change
4.3 Quotas 101 a quota that stops a Deployment halfway
4.4 ConfigMaps 102 an env file, and a file under another name
4.6 Secrets 105 every key as a variable, one key as a file
4.7 ServiceAccounts 104 an identity for an app that calls the API
4.8 Security 103 user, group and a volume the group may write
Permissions are asked of the API server as the subject, quotas are read from their own status, and
what a Pod is given - variables, files, modes, ids - is read inside the running container. Every
task has a scripted solution in solve.py.
"""
from ckad.common import C, CAN, ONE, PY, ROLL, NS, ns_pick, name_pick
PEOPLE = ["dana", "omar", "priya", "lucas", "mei", "tariq", "ines", "kofi"]
QUESTIONS = [
# ========================================================================= 4.2 Access ====
{
"id": 100, "domain": "ENV", "title": "One may read, one may change",
"params": lambda r: (lambda p: {"ns": ns_pick(r, "team"), "dev": p[0], "lead": p[1]})(r.sample(PEOPLE, 2)),
"task": """
Give two colleagues access to Namespace `%{ns}` — and to nothing else in the cluster. Use the
built-in ClusterRoles where one fits.
- User `%{dev}`: read-only. RoleBinding `%{dev}-view` to the built-in ClusterRole `view`.
- `%{dev}` also restarts Pods by deleting them: Role `pod-restarter` (delete on Pods, nothing
more) and RoleBinding `%{dev}-restart`.
- User `%{lead}`: may change workloads. RoleBinding `%{lead}-edit` to the built-in ClusterRole
`edit`.
Then answer three questions in `{WS}/q100/answers.txt`, one `yes` or `no` per line, in this order:
1. Can `%{dev}` read the logs of Pods in `%{ns}`?
2. Can `%{dev}` read Secrets in `%{ns}`?
3. Can `%{lead}` create RoleBindings in `%{ns}`?
""",
"setup": NS + r"""mkns %{ns}; rm -rf "$WS/q100"; mkdir -p "$WS/q100" """,
"checks": [
C("%{dev} has view in %{ns} through RoleBinding %{dev}-view", 3, CAN + r"""
eq "$(j -n %{ns} rolebinding %{dev}-view '{.roleRef.kind}/{.roleRef.name}')" ClusterRole/view || exit 1
can --as=%{dev} -- list pods -n %{ns} && can --as=%{dev} -- get pods/log -n %{ns} && cannot --as=%{dev} -- get secrets -n %{ns} && cannot --as=%{dev} -- create deployments -n %{ns}"""),
C("and may delete Pods there, through Role pod-restarter - and nothing else", 2, CAN + PY + r"""
py -n %{ns} role pod-restarter 'len(o["rules"]) == 1 and o["rules"][0]["resources"] == ["pods"] and o["rules"][0]["verbs"] == ["delete"]' || exit 1
eq "$(j -n %{ns} rolebinding %{dev}-restart '{.roleRef.kind}/{.roleRef.name}')" Role/pod-restarter \
&& can --as=%{dev} -- delete pods -n %{ns} && cannot --as=%{dev} -- delete deployments -n %{ns} && cannot --as=%{dev} -- delete configmaps -n %{ns}"""),
C("%{lead} has edit in %{ns} through RoleBinding %{lead}-edit", 3, CAN + r"""
eq "$(j -n %{ns} rolebinding %{lead}-edit '{.roleRef.kind}/{.roleRef.name}')" ClusterRole/edit || exit 1
can --as=%{lead} -- create deployments -n %{ns} && can --as=%{lead} -- get secrets -n %{ns} && cannot --as=%{lead} -- create rolebindings -n %{ns}"""),
C("neither has anything in another Namespace, and answers.txt is right", 2, CAN + r"""
can --as=%{lead} -- create deployments -n %{ns} && cannot --as=%{lead} -- list pods -n default && cannot --as=%{dev} -- list pods -n default && cannot --as=%{dev} -- list nodes || exit 1
eq "$(tr 'A-Z' 'a-z' < "$WS/q100/answers.txt" 2>/dev/null | tr -cd 'a-z\n' | grep . | tr '\n' ',')" yes,no,no,"""),
],
"solution": """
k -n %{ns} create rolebinding %{dev}-view --clusterrole=view --user=%{dev}
k -n %{ns} create role pod-restarter --verb=delete --resource=pods
k -n %{ns} create rolebinding %{dev}-restart --role=pod-restarter --user=%{dev}
k -n %{ns} create rolebinding %{lead}-edit --clusterrole=edit --user=%{lead}
k -n %{ns} auth can-i get pods --subresource=log --as=%{dev} # yes
k -n %{ns} auth can-i get secrets --as=%{dev} # no - `view` leaves Secrets out
k -n %{ns} auth can-i create rolebindings --as=%{lead} # no - `edit` cannot hand out permissions; `admin` can
printf 'yes\\nno\\nno\\n' > {WS}/q100/answers.txt
# A RoleBinding may point at a ClusterRole: the rules then hold in the binding's Namespace only.
# Permissions add up across bindings - %{dev} has view plus one verb more. To `auth can-i`, a
# subresource is --subresource=log; `can-i get pods/log` would ask about a Pod named "log".
""",
},
# ========================================================================= 4.3 Quotas ====
{
"id": 101, "domain": "ENV", "title": "A quota that stops a Deployment halfway",
"params": lambda r: {"ns": ns_pick(r, "squad"), "dep": name_pick(r, "worker")},
"task": """
Deployment `%{dep}` in Namespace `%{ns}` asks for 4 replicas and gets 2. The Namespace has a
ResourceQuota, `team-quota`.
1. Find out which resource of the quota is exhausted, and write its name as the quota lists it
(for example `limits.memory`) to `{WS}/q101/resource.txt`.
2. The application needs only half of what it requests of that resource. Change the
**Deployment** — the quota stays as it is — so that all 4 replicas run.
""",
"setup": NS + r"""
mkns %{ns}; rm -rf "$WS/q101"; mkdir -p "$WS/q101"
$K -n %{ns} create quota team-quota --hard=requests.cpu=1,requests.memory=1Gi,pods=10 >/dev/null
$K apply -f - >/dev/null <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: %{dep}, namespace: %{ns}}
spec:
replicas: 4
# Recreate, not the default: under a quota this tight a rolling update cannot make room for its
# first new Pods while the old ones still count, and stalls (see the solution's note)
strategy: {type: Recreate}
selector: {matchLabels: {app: %{dep}}}
template:
metadata: {labels: {app: %{dep}}}
spec:
terminationGracePeriodSeconds: 2
containers:
- name: worker
image: busybox:1.36
command: ["sleep", "86400"]
resources:
requests: {cpu: 400m, memory: 64Mi}
EOF
for i in $(seq 1 60); do [ "$(j -n %{ns} deploy %{dep} '{.status.availableReplicas}')" = 2 ] && break; sleep 2; done
eq "$(j -n %{ns} deploy %{dep} '{.status.availableReplicas}')" 2 && j -n %{ns} quota team-quota '{.metadata.uid}' > "$PRIV/q101.uid"
""",
"checks": [
C("resource.txt names the exhausted resource", 3, ONE + r"""eq "$(one "$WS/q101/resource.txt")" requests.cpu"""),
C("the quota is the one that was there, unchanged", 2, r"""
eq "$(j -n %{ns} quota team-quota '{.metadata.uid}')" "$(cat "$PRIV/q101.uid")" && eq "$(j -n %{ns} quota team-quota '{.spec.hard.requests\.cpu}')" 1 \
&& eq "$(j -n %{ns} quota team-quota '{.spec.hard.pods}')" 10 && eq "$(j -n %{ns} deploy %{dep} '{.spec.replicas}')" 4 \
&& [ "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].resources.requests.cpu}')" != 400m ]"""),
C("each replica requests 200m CPU, and all 4 run", 5, ROLL + r"""
eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].resources.requests.cpu}')" 200m \
&& eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.containers[0].resources.requests.memory}')" 64Mi && rolled %{ns} %{dep} 4 120"""),
],
"solution": """
k -n %{ns} get deploy,rs # 2/4
k -n %{ns} describe rs | grep -A2 Warning # FailedCreate ... exceeded quota: team-quota, requested: requests.cpu=400m,
# # used: requests.cpu=800m, limited: requests.cpu=1
k -n %{ns} describe quota team-quota # Used / Hard per resource
echo requests.cpu > {WS}/q101/resource.txt
k -n %{ns} set resources deploy/%{dep} --requests=cpu=200m,memory=64Mi
k -n %{ns} rollout status deploy/%{dep}
# A Pod that would exceed the quota is not created at all - so there is no Pending Pod to describe:
# the refusal is an event on the ReplicaSet. A quota on requests or limits also makes every Pod in
# the Namespace declare that resource (or a LimitRange must supply a default).
# This Deployment uses the Recreate strategy for a reason: during a rolling update old and new Pods
# count against the quota together, and with the quota already full the first new Pods cannot be
# created while the old ones may not yet be removed - the rollout would hang until you scale down.
""",
},
# ===================================================================== 4.4 ConfigMaps ====
{
"id": 102, "domain": "ENV", "title": "An env file, and a file under another name",
"params": lambda r: {"ns": ns_pick(r, "mail"), "cm": name_pick(r, "relay"), "pod": name_pick(r, "sender"), "level": r.choice(["debug", "info", "warn"])},
"task": """
`{WS}/q102/` holds two files of settings: `logging.env` (one `KEY=value` per line) and
`app.properties`.
In Namespace `%{ns}`:
1. Create ConfigMap `%{cm}-env` in which every line of `logging.env` is a key of its own.
2. Create ConfigMap `%{cm}-files` holding the file `app.properties` under the key
`application.properties` — not under its file name.
3. Create Pod `%{pod}` (`busybox:1.36`, command `sleep 86400`) that
- gets every key of `%{cm}-env` as an environment variable with the prefix `LOG_`
(so `LEVEL` becomes `LOG_LEVEL`), without naming the keys one by one;
- mounts the key `application.properties` as the file `/etc/app/conf/app.properties` — the
only thing under `/etc/app` — with file mode `0440`.
""",
"setup": NS + r"""
mkns %{ns}; rm -rf "$WS/q102"; mkdir -p "$WS/q102"
printf 'LEVEL=%{level}\nFORMAT=json\n' > "$WS/q102/logging.env"
printf 'smtp.host=relay.%{ns}.svc\nsmtp.port=2525\nretry.max=5\n' > "$WS/q102/app.properties"
cp "$WS/q102/app.properties" "$PRIV/q102.properties"
""",
"checks": [
C("ConfigMap %{cm}-env has one key per line of logging.env", 2, PY + r"""
py -n %{ns} cm %{cm}-env 'o.get("data") == {"LEVEL": "%{level}", "FORMAT": "json"}'"""),
C("ConfigMap %{cm}-files holds the file under the key application.properties", 2, r"""
eq "$(j -n %{ns} cm %{cm}-files '{.data.application\.properties}')" "$(cat "$PRIV/q102.properties")" && [ -z "$(j -n %{ns} cm %{cm}-files '{.data.app\.properties}')" ]"""),
C("the Pod has LOG_LEVEL and LOG_FORMAT, taken with envFrom and a prefix", 3, PY + r"""
py -n %{ns} pod %{pod} 'any(e.get("prefix") == "LOG_" and e.get("configMapRef", {}).get("name") == "%{cm}-env" for e in o["spec"]["containers"][0].get("envFrom", []))' || exit 1
e=$($K -n %{ns} exec %{pod} -- env 2>/dev/null); echo "$e" | grep -qx 'LOG_LEVEL=%{level}' && echo "$e" | grep -qx 'LOG_FORMAT=json'"""),
C("/etc/app holds only conf/app.properties, with the file's content and mode 440", 3, r"""
eq "$($K -n %{ns} exec %{pod} -- cat /etc/app/conf/app.properties 2>/dev/null)" "$(cat "$PRIV/q102.properties")" \
&& eq "$($K -n %{ns} exec %{pod} -- stat -L -c %a /etc/app/conf/app.properties 2>/dev/null)" 440 \
&& eq "$($K -n %{ns} exec %{pod} -- sh -c 'ls /etc/app; ls /etc/app/conf' 2>/dev/null | tr '\n' ' ')" "conf app.properties " """),
],
"solution": """
cd {WS}/q102
k -n %{ns} create configmap %{cm}-env --from-env-file=logging.env
k -n %{ns} create configmap %{cm}-files --from-file=application.properties=app.properties
cat <<EOF | k apply -f -
apiVersion: v1
kind: Pod
metadata: {name: %{pod}, namespace: %{ns}}
spec:
volumes:
- name: files
configMap:
name: %{cm}-files
defaultMode: 0440
items:
- {key: application.properties, path: conf/app.properties}
containers:
- name: sender
image: busybox:1.36
command: [sleep, "86400"]
envFrom:
- prefix: LOG_
configMapRef: {name: %{cm}-env}
volumeMounts: [{name: files, mountPath: /etc/app}]
EOF
k -n %{ns} exec %{pod} -- sh -c 'env | grep LOG_; ls -lL /etc/app/conf'
# --from-env-file makes a key of every line; --from-file makes ONE key of the whole file, named
# after the file unless you write key=path. `items` picks keys and says where each one lands under
# the mount point - a path may contain directories. Keys not listed are not mounted.
""",
},
# ======================================================================= 4.8 Security ====
{
"id": 103, "domain": "ENV", "title": "User, group and a volume the group may write",
"params": lambda r: {"ns": ns_pick(r, "upload"), "pod": name_pick(r, "scanner"), "uid": r.choice([1000, 1500, 2000]), "gid": r.choice([3000, 3500]), "fsg": r.choice([4000, 4500, 5000])},
"task": """
Create Pod `%{pod}` in Namespace `%{ns}` (image `busybox:1.36`, command `sleep 86400`) with an
`emptyDir` volume named `work` mounted at `/work`, and this security configuration:
- every process runs as user `%{uid}` and primary group `%{gid}`;
- files created in the volume belong to group `%{fsg}`, and that group may write there;
- the root filesystem is read-only, privilege escalation is not allowed, all capabilities are
dropped.
Then, in the running container, save the output of `id` to `{WS}/q103/id.txt` and create the file
`/work/proof`.
""",
"setup": NS + r"""mkns %{ns}; rm -rf "$WS/q103"; mkdir -p "$WS/q103" """,
"checks": [
C("the Pod's security context has the ids and the three restrictions", 3, PY + r"""
py -n %{ns} pod %{pod} '
p = o["spec"].get("securityContext", {}); c = o["spec"]["containers"][0].get("securityContext", {})
g = lambda k: c.get(k, p.get(k))
g("runAsUser") == %{uid} and g("runAsGroup") == %{gid} and p.get("fsGroup") == %{fsg} and c.get("readOnlyRootFilesystem") is True and c.get("allowPrivilegeEscalation") is False and c.get("capabilities", {}).get("drop") == ["ALL"]'"""),
C("in the container: that user and group, the volume's group among its groups, nothing writable but the volume", 4, r"""
eq "$(j -n %{ns} pod %{pod} '{.status.phase}')" Running || exit 1
x() { $K -n %{ns} exec %{pod} -- sh -c "$1" 2>/dev/null; }
eq "$(x 'id -u')" %{uid} && eq "$(x 'id -g')" %{gid} && x 'id -G' | tr ' ' '\n' | grep -qx %{fsg} \
&& eq "$(x 'stat -c %g /work/proof')" %{fsg} && eq "$(x 'stat -c %u /work/proof')" %{uid} && ! x 'touch /ksim-probe' && x 'touch /work/ksim-probe && rm /work/ksim-probe'"""),
C("id.txt is the output of id in that container", 3, r"""
grep -q 'uid=%{uid}' "$WS/q103/id.txt" 2>/dev/null && grep -q 'gid=%{gid}' "$WS/q103/id.txt" && grep -q '%{fsg}' "$WS/q103/id.txt" """),
],
"solution": """
cat <<EOF | k apply -f -
apiVersion: v1
kind: Pod
metadata: {name: %{pod}, namespace: %{ns}}
spec:
securityContext:
runAsUser: %{uid}
runAsGroup: %{gid}
fsGroup: %{fsg}
volumes: [{name: work, emptyDir: {}}]
containers:
- name: scanner
image: busybox:1.36
command: [sleep, "86400"]
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
volumeMounts: [{name: work, mountPath: /work}]
EOF
k -n %{ns} exec %{pod} -- id | tee {WS}/q103/id.txt # uid=%{uid} gid=%{gid} groups=%{fsg},%{gid}
k -n %{ns} exec %{pod} -- sh -c 'touch /work/proof; ls -ln /work'
# runAsUser, runAsGroup and fsGroup are Pod-level (the first two can be overridden per container);
# readOnlyRootFilesystem, allowPrivilegeEscalation and capabilities exist only per container.
# fsGroup is added to the process's groups and made the owner group of the volume, with the setgid
# bit - which is how a non-root user gets to write into it.
""",
},
# ================================================================ 4.7 ServiceAccounts ====
{
"id": 104, "domain": "ENV", "title": "An identity for an app that calls the API",
"params": lambda r: {"ns": ns_pick(r, "flags"), "dep": name_pick(r, "toggler")},
"task": """
Deployment `%{dep}` in Namespace `%{ns}` asks the Kubernetes API for ConfigMap `feature-flags`
every few seconds, and logs the HTTP status it gets: `status=403`. It runs as the Namespace's
`default` ServiceAccount, which may not read anything.
Give the application an identity of its own, with exactly the permission it needs:
1. ServiceAccount `%{dep}`.
2. Role `%{dep}` that allows `get` on the ConfigMap named `feature-flags` — that one object, not
ConfigMaps in general — and RoleBinding `%{dep}`.
3. Make the Deployment run as that ServiceAccount.
Its log must then show `status=200`.
""",
"setup": NS + ROLL + r"""
mkns %{ns}
$K -n %{ns} create configmap feature-flags --from-literal=beta=on >/dev/null
$K -n %{ns} create configmap other-settings --from-literal=a=b >/dev/null
$K apply -f - >/dev/null <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: %{dep}, namespace: %{ns}}
spec:
replicas: 1
selector: {matchLabels: {app: %{dep}}}
template:
metadata: {labels: {app: %{dep}}}
spec:
terminationGracePeriodSeconds: 2
containers:
- name: app
image: curlimages/curl:8.10.1
command: ["sh", "-c", "while true; do echo status=$(curl -sk -m 3 -o /dev/null -w '%{http_code}' -H \"Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\" https://kubernetes.default.svc/api/v1/namespaces/%{ns}/configmaps/feature-flags); sleep 5; done"]
EOF
rolled %{ns} %{dep} 1 240 || exit 1
for i in $(seq 1 20); do $K -n %{ns} logs deploy/%{dep} --tail=1 2>/dev/null | grep -qx 'status=403' && exit 0; sleep 2; done
echo "the app does not log status=403" >&2; exit 1
""",
"checks": [
C("the Deployment runs as ServiceAccount %{dep}", 3, ROLL + r"""
eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.serviceAccountName}')" %{dep} && [ -n "$(j -n %{ns} sa %{dep} '{.metadata.name}')" ] && rolled %{ns} %{dep} 1 60"""),
C("which may get ConfigMap feature-flags, and no other ConfigMap, nor list them", 3, CAN + r"""
S=system:serviceaccount:%{ns}:%{dep}
eq "$(j -n %{ns} rolebinding %{dep} '{.roleRef.kind}/{.roleRef.name}')" Role/%{dep} || exit 1
can --as=$S -- get configmap/feature-flags -n %{ns} && cannot --as=$S -- get configmap/other-settings -n %{ns} && cannot --as=$S -- list configmaps -n %{ns} && cannot --as=$S -- get secrets -n %{ns}"""),
C("the application now logs status=200", 4, ROLL + r"""
eq "$(j -n %{ns} deploy %{dep} '{.spec.template.spec.serviceAccountName}')" %{dep} && rolled %{ns} %{dep} 1 60 || exit 1
p=$(newest %{ns} app=%{dep}); for i in 1 2 3 4 5 6; do $K -n %{ns} logs "$p" --tail=1 2>/dev/null | grep -qx 'status=200' && exit 0; sleep 3; done; exit 1"""),
],
"solution": """
k -n %{ns} logs deploy/%{dep} --tail=2 # status=403
k -n %{ns} create sa %{dep}
k -n %{ns} create role %{dep} --verb=get --resource=configmaps --resource-name=feature-flags
k -n %{ns} create rolebinding %{dep} --role=%{dep} --serviceaccount=%{ns}:%{dep}
k -n %{ns} set serviceaccount deploy/%{dep} %{dep}
k -n %{ns} rollout status deploy/%{dep}
k -n %{ns} logs deploy/%{dep} --tail=2 # status=200
# The token a Pod finds under /var/run/secrets/kubernetes.io/serviceaccount is its ServiceAccount's:
# that is who the API server sees. --resource-name narrows the rule to one object (and works for
# get, not for list). In --serviceaccount the form is <namespace>:<name>.
""",
},
# ======================================================================== 4.6 Secrets ====
{
"id": 105, "domain": "ENV", "title": "Every key as a variable, one key as a file",
"params": lambda r: {"ns": ns_pick(r, "ledger"), "sec": name_pick(r, "db"), "pod": name_pick(r, "client"),
"pw": "pw-" + "".join(r.choice("abcdefghjkmnpqrstuvwxyz23456789") for _ in range(12))},
"task": """
Namespace `%{ns}` has two Secrets: `%{sec}` with the keys `USER` and `PASSWORD`, and `%{sec}-ca` with
the key `ca.crt`.
Create Pod `%{pod}` in `%{ns}` (`busybox:1.36`, command `sleep 86400`) that
- gets **every** key of `%{sec}` as an environment variable with the prefix `DB_` — `DB_USER`,
`DB_PASSWORD` — without naming the keys one by one;
- mounts the key `ca.crt` of `%{sec}-ca`, read-only, as the single file `/etc/ssl/db/ca.crt`.
""",
"setup": NS + r"""
mkns %{ns}
$K -n %{ns} create secret generic %{sec} --from-literal=USER=ledger_rw --from-literal=PASSWORD=%{pw} >/dev/null
printf -- '-----BEGIN CERTIFICATE-----\nMIIBksim%{pw}\nQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=\n-----END CERTIFICATE-----\n' > "$PRIV/q105.ca"
$K -n %{ns} create secret generic %{sec}-ca --from-file=ca.crt="$PRIV/q105.ca" --from-literal=ca.key=must-not-be-mounted >/dev/null
""",
"checks": [
C("the Pod takes Secret %{sec} with envFrom and the prefix DB_", 3, PY + r"""
py -n %{ns} pod %{pod} '
c = o["spec"]["containers"][0]
any(e.get("prefix") == "DB_" and e.get("secretRef", {}).get("name") == "%{sec}" for e in c.get("envFrom", [])) and not any("secretKeyRef" in e.get("valueFrom", {}) for e in c.get("env", []))'"""),
C("in the container DB_USER and DB_PASSWORD have the Secret's values", 3, r"""
e=$($K -n %{ns} exec %{pod} -- env 2>/dev/null); echo "$e" | grep -qx 'DB_USER=ledger_rw' && echo "$e" | grep -qx 'DB_PASSWORD=%{pw}'"""),
C("/etc/ssl/db holds ca.crt and nothing else, read-only", 4, PY + r"""
eq "$($K -n %{ns} exec %{pod} -- cat /etc/ssl/db/ca.crt 2>/dev/null)" "$(cat "$PRIV/q105.ca")" && eq "$($K -n %{ns} exec %{pod} -- ls /etc/ssl/db 2>/dev/null | tr '\n' ' ')" "ca.crt " || exit 1
py -n %{ns} pod %{pod} '
v = {x["name"] for x in o["spec"]["volumes"] if x.get("secret", {}).get("secretName") == "%{sec}-ca"}
any(m["name"] in v and m.get("readOnly") is True for m in o["spec"]["containers"][0].get("volumeMounts", []))'"""),
],
"solution": """
cat <<EOF | k apply -f -
apiVersion: v1
kind: Pod
metadata: {name: %{pod}, namespace: %{ns}}
spec:
volumes:
- name: ca
secret:
secretName: %{sec}-ca
items:
- {key: ca.crt, path: ca.crt}
containers:
- name: client
image: busybox:1.36
command: [sleep, "86400"]
envFrom:
- prefix: DB_
secretRef: {name: %{sec}}
volumeMounts: [{name: ca, mountPath: /etc/ssl/db, readOnly: true}]
EOF
k -n %{ns} exec %{pod} -- sh -c 'env | grep DB_; ls /etc/ssl/db'
# envFrom takes every key of the Secret (or ConfigMap) as a variable; `prefix` goes in front of
# each name. The Secret %{sec}-ca has a second key, ca.key: without `items` it would be mounted
# too. For a ConfigMap the field is `name`, for a Secret volume it is `secretName` - and under
# envFrom both are `name` again.
""",
},
]