Study mode is gone: no more untimed sessions with the reference solutions open from the start. The path is now objective drills against the clock, then a mock, then the fixed sets, and an objective is done when every task passed on its latest timed attempt. In the lobby "Study mode" becomes "Drill an objective" (the objective list, with one "Against the clock" button per objective); ./ksim study is removed and ./ksim next always drills. study-<track>.json keeps focus, known and the done stamps; results of old study sessions stay in it, read by nothing. Also in this commit, from the work before it: - ./ksim skip / unskip, and links beside each domain in the lobby: mark a domain or an objective as known, so the path, the no-argument drill and drill all leave it out; a mock still draws from every domain - DOMAIN:N picks N tasks of a domain, spread over its objectives - a command with neither --track nor KSIM_TRACK is for the track whose session is under way; ./ksim show says where the task's host resolves - CKAD Q60: the check names which file holds which run's output Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JZPd3GGWyxTgAkL6a4gAJz
742 lines
37 KiB
Python
742 lines
37 KiB
Python
"""CKAD tasks 58-64: a second pool task for every curriculum objective that had only one.
|
|
|
|
curriculum.py turns the bank into a study path, one objective at a time, and an objective with a
|
|
single task is studied once and never drilled on anything new. Each task here takes its objective
|
|
from another side than the first one does: API deprecations (a warning, not a removal), CRDs (a
|
|
custom resource the API server rejects), images (ENTRYPOINT, CMD and USER), Kustomize (one that
|
|
does not build), probes (checks that are wrong, not missing), kubectl output (custom columns and
|
|
selectors), logs (a selector, a prefix and an init container).
|
|
|
|
Same format as questions.py; every grader reads the live cluster or the files the task asks for.
|
|
"""
|
|
from sim.bank import C, NS, ns_pick, name_pick
|
|
|
|
QUESTIONS = [
|
|
# ------------------------------------------------------------------- OBS ----
|
|
{
|
|
"id": 58, "domain": "OBS", "title": "Removed, and deprecated but still served",
|
|
"params": lambda r: {"ns": ns_pick(r, "migrate"), "app": name_pick(r, "catalog"), "pct": r.choice([10, 25, 50])},
|
|
"task": """
|
|
`{WS}/q58` holds two manifests written for an older cluster.
|
|
|
|
1. `deployment.yaml` no longer applies. Correct the file - same name, Namespace, replicas, labels
|
|
and container - so that it applies to this cluster, and apply it.
|
|
2. `flag.yaml` still applies, but the API server answers with a deprecation warning. Write the
|
|
warning to `{WS}/q58/warning.txt`, change the file to the version the warning points to, and
|
|
apply it.
|
|
3. Write the API version this cluster serves for each of the kinds `CronJob`,
|
|
`HorizontalPodAutoscaler`, `EndpointSlice` and `PriorityClass` to `{WS}/q58/served.txt`, one
|
|
per line as `<Kind> <group/version>`.
|
|
""",
|
|
"setup": NS + r"""
|
|
mkns %{ns}; rm -rf "$WS/q58"; mkdir -p "$WS/q58"
|
|
$K -n %{ns} delete deploy %{app} --ignore-not-found >/dev/null 2>&1
|
|
$K apply -f - >/dev/null <<'EOF'
|
|
apiVersion: apiextensions.k8s.io/v1
|
|
kind: CustomResourceDefinition
|
|
metadata: {name: featureflags.rollout.example.com}
|
|
spec:
|
|
group: rollout.example.com
|
|
scope: Namespaced
|
|
names: {plural: featureflags, singular: featureflag, kind: FeatureFlag, shortNames: [ff]}
|
|
versions:
|
|
- name: v1beta1
|
|
served: true
|
|
storage: false
|
|
deprecated: true
|
|
deprecationWarning: "rollout.example.com/v1beta1 FeatureFlag is deprecated and will stop being served; use rollout.example.com/v1 FeatureFlag"
|
|
schema:
|
|
openAPIV3Schema:
|
|
type: object
|
|
properties:
|
|
spec:
|
|
type: object
|
|
required: [enabled]
|
|
properties:
|
|
enabled: {type: boolean}
|
|
percent: {type: integer, minimum: 0, maximum: 100}
|
|
- name: v1
|
|
served: true
|
|
storage: true
|
|
schema:
|
|
openAPIV3Schema:
|
|
type: object
|
|
properties:
|
|
spec:
|
|
type: object
|
|
required: [enabled]
|
|
properties:
|
|
enabled: {type: boolean}
|
|
percent: {type: integer, minimum: 0, maximum: 100}
|
|
EOF
|
|
$K wait --for=condition=Established crd/featureflags.rollout.example.com --timeout=60s >/dev/null
|
|
$K -n %{ns} delete featureflags.rollout.example.com --all --wait=true >/dev/null 2>&1
|
|
cat > "$WS/q58/deployment.yaml" <<'EOF'
|
|
apiVersion: extensions/v1beta1
|
|
kind: Deployment
|
|
metadata:
|
|
name: %{app}
|
|
namespace: %{ns}
|
|
labels: {app: %{app}}
|
|
spec:
|
|
replicas: 2
|
|
template:
|
|
metadata:
|
|
labels: {app: %{app}, tier: web}
|
|
spec:
|
|
containers:
|
|
- name: web
|
|
image: nginx:1.27
|
|
ports: [{containerPort: 80}]
|
|
EOF
|
|
cat > "$WS/q58/flag.yaml" <<'EOF'
|
|
apiVersion: rollout.example.com/v1beta1
|
|
kind: FeatureFlag
|
|
metadata:
|
|
name: %{app}-checkout
|
|
namespace: %{ns}
|
|
spec:
|
|
enabled: true
|
|
percent: %{pct}
|
|
EOF
|
|
""",
|
|
"checks": [
|
|
C("deployment.yaml is apps/v1 and applies; Deployment %{app} has 2 available with its labels", 2, r"""
|
|
f="$WS/q58/deployment.yaml"
|
|
grep -Eq '^apiVersion:[[:space:]]*apps/v1[[:space:]]*$' "$f" 2>/dev/null || exit 1
|
|
$K apply --dry-run=server -f "$f" >/dev/null 2>&1 || exit 1
|
|
d="$K -n %{ns} get deploy %{app}"
|
|
eq "$($d -o jsonpath='{.spec.replicas}/{.spec.template.metadata.labels.app}/{.spec.template.metadata.labels.tier}/{.spec.template.spec.containers[0].name}/{.spec.template.spec.containers[0].image}' 2>/dev/null)" \
|
|
"2/%{app}/web/web/nginx:1.27" || exit 1
|
|
for i in $(seq 1 30); do [ "$($d -o jsonpath='{.status.availableReplicas}')" = 2 ] && exit 0; sleep 3; done; exit 1"""),
|
|
C("warning.txt holds the deprecation warning", 1, r"""
|
|
grep -q 'rollout.example.com/v1beta1 FeatureFlag is deprecated' "$WS/q58/warning.txt" 2>/dev/null"""),
|
|
C("flag.yaml uses rollout.example.com/v1, applies without a warning, and FeatureFlag %{app}-checkout exists", 2, r"""
|
|
f="$WS/q58/flag.yaml"
|
|
grep -Eq '^apiVersion:[[:space:]]*rollout\.example\.com/v1[[:space:]]*$' "$f" 2>/dev/null || exit 1
|
|
out=$($K apply --dry-run=server -f "$f" 2>&1) || exit 1
|
|
echo "$out" | grep -qi deprecated && exit 1
|
|
eq "$(j -n %{ns} featureflags.rollout.example.com %{app}-checkout '{.spec.enabled}/{.spec.percent}')" "true/%{pct}" """),
|
|
C("served.txt: the four kinds with the versions this cluster serves", 2, r"""
|
|
want=$($K api-resources --no-headers 2>/dev/null | awk '{k=$NF; if (k=="CronJob" || k=="HorizontalPodAutoscaler" || k=="EndpointSlice" || k=="PriorityClass") print k, $(NF-2)}' | sort)
|
|
[ "$(echo "$want" | grep -c .)" = 4 ] || exit 1
|
|
got=$(tr -s ' \t' ' ' < "$WS/q58/served.txt" 2>/dev/null | sed 's/^ *//; s/ *$//' | tr -d '\r' | grep . | sort)
|
|
eq "$got" "$want" """),
|
|
],
|
|
"solution": """
|
|
k apply -f {WS}/q58/deployment.yaml --dry-run=server # no matches for kind "Deployment" in version "extensions/v1beta1"
|
|
k api-resources | grep -w Deployment # apps/v1
|
|
vi {WS}/q58/deployment.yaml
|
|
# apiVersion: apps/v1
|
|
# spec:
|
|
# selector: {matchLabels: {app: %{app}}} <- apps/v1 requires it; the old API made it up from the template labels
|
|
k apply -f {WS}/q58/deployment.yaml
|
|
|
|
k apply -f {WS}/q58/flag.yaml --dry-run=server 2> {WS}/q58/warning.txt
|
|
cat {WS}/q58/warning.txt # Warning: rollout.example.com/v1beta1 FeatureFlag is deprecated ...; use rollout.example.com/v1 FeatureFlag
|
|
sed -i 's#rollout.example.com/v1beta1#rollout.example.com/v1#' {WS}/q58/flag.yaml
|
|
k apply -f {WS}/q58/flag.yaml # no warning any more
|
|
|
|
k api-resources | grep -w -E 'CronJob|HorizontalPodAutoscaler|EndpointSlice|PriorityClass' # APIVERSION column
|
|
cat > {WS}/q58/served.txt <<EOF
|
|
CronJob batch/v1
|
|
HorizontalPodAutoscaler autoscaling/v2
|
|
EndpointSlice discovery.k8s.io/v1
|
|
PriorityClass scheduling.k8s.io/v1
|
|
EOF
|
|
# Removed = "no matches for kind": nothing happens until the manifest is rewritten. Deprecated =
|
|
# a Warning on stderr while the object is still created - the time to migrate, before the version
|
|
# is removed. The warning is real API machinery (here a CRD version marked deprecated); built-in
|
|
# APIs print the same line, e.g. "batch/v1beta1 CronJob is deprecated in v1.21+, unavailable in v1.25+".
|
|
""",
|
|
},
|
|
{
|
|
"id": 62, "domain": "OBS", "title": "Health checks that check the wrong thing",
|
|
"params": lambda r: (lambda pair: {"ns": ns_pick(r, "health"), "dep": name_pick(r, "gateway"),
|
|
"port": r.choice([8080, 8081, 9090]), "live": pair[0], "ready": pair[1]})(
|
|
r.choice([("healthz", "ready"), ("livez", "readyz"), ("alive", "warm")])),
|
|
"task": """
|
|
Pods of Deployment `%{dep}` in Namespace `%{ns}` are restarted again and again and never become
|
|
Ready. The application is healthy - its health checks are wrong. ConfigMap `%{dep}-conf` shows how
|
|
the app listens.
|
|
|
|
1. Write the reason the kubelet puts on its events when a probe fails to `{WS}/q62/reason.txt`.
|
|
2. Correct the probes: liveness must request the app's liveness endpoint and readiness its
|
|
readiness endpoint, both on the port the app listens on. Keep them HTTP probes with their
|
|
timings.
|
|
3. Both replicas must be Ready and stay up without restarts. Do not change the ConfigMap.
|
|
""",
|
|
"setup": NS + r"""
|
|
mkns %{ns}; rm -rf "$WS/q62"; mkdir -p "$WS/q62"
|
|
$K -n %{ns} delete deploy %{dep} --ignore-not-found --wait=true >/dev/null 2>&1
|
|
$K apply -f - >/dev/null <<'EOF'
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata: {name: %{dep}-conf, namespace: %{ns}}
|
|
data:
|
|
default.conf: |
|
|
server {
|
|
listen %{port};
|
|
# liveness endpoint
|
|
location = /%{live} { return 200 "alive\n"; }
|
|
# readiness endpoint
|
|
location = /%{ready} { return 200 "ready\n"; }
|
|
location / { return 404; }
|
|
}
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata: {name: %{dep}, namespace: %{ns}, labels: {app: %{dep}}}
|
|
spec:
|
|
replicas: 2
|
|
selector: {matchLabels: {app: %{dep}}}
|
|
template:
|
|
metadata: {labels: {app: %{dep}}}
|
|
spec:
|
|
containers:
|
|
- name: gateway
|
|
image: nginx:1.27
|
|
ports: [{containerPort: %{port}}]
|
|
volumeMounts: [{name: conf, mountPath: /etc/nginx/conf.d}]
|
|
livenessProbe:
|
|
httpGet: {path: /%{live}, port: 80}
|
|
periodSeconds: 5
|
|
failureThreshold: 2
|
|
readinessProbe:
|
|
httpGet: {path: /status, port: %{port}}
|
|
periodSeconds: 5
|
|
volumes: [{name: conf, configMap: {name: %{dep}-conf}}]
|
|
EOF
|
|
$K -n %{ns} get cm %{dep}-conf -o jsonpath='{.data.default\.conf}' | md5sum | cut -d' ' -f1 > "$WS/.q62-conf.md5"
|
|
""",
|
|
"checks": [
|
|
C("reason.txt holds Unhealthy", 1, r"""eq "$(tr -d '[:space:]' < "$WS/q62/reason.txt" 2>/dev/null)" Unhealthy"""),
|
|
C("liveness: GET /%{live} on port %{port}, every 5 s, 2 failures", 2, r"""
|
|
$K -n %{ns} get deploy %{dep} -o json 2>/dev/null | python3 -c '
|
|
import sys, json
|
|
c = json.load(sys.stdin)["spec"]["template"]["spec"]["containers"][0]
|
|
p = c.get("livenessProbe") or {}; h = p.get("httpGet") or {}
|
|
named = {x.get("name"): x.get("containerPort") for x in c.get("ports", [])}
|
|
port = named.get(h.get("port"), h.get("port"))
|
|
sys.exit(0 if (h.get("path"), port, p.get("periodSeconds"), p.get("failureThreshold")) == ("/%{live}", %{port}, 5, 2) else 1)'"""),
|
|
C("readiness: GET /%{ready} on port %{port}, every 5 s", 2, r"""
|
|
$K -n %{ns} get deploy %{dep} -o json 2>/dev/null | python3 -c '
|
|
import sys, json
|
|
c = json.load(sys.stdin)["spec"]["template"]["spec"]["containers"][0]
|
|
p = c.get("readinessProbe") or {}; h = p.get("httpGet") or {}
|
|
named = {x.get("name"): x.get("containerPort") for x in c.get("ports", [])}
|
|
port = named.get(h.get("port"), h.get("port"))
|
|
sys.exit(0 if (h.get("path"), port, p.get("periodSeconds")) == ("/%{ready}", %{port}, 5) else 1)'"""),
|
|
C("2/2 Ready and still not restarted 12 s later; ConfigMap untouched", 2, r"""
|
|
d="$K -n %{ns} get deploy %{dep}"; ok=
|
|
for i in $(seq 1 20); do [ "$($d -o jsonpath='{.status.readyReplicas}/{.status.updatedReplicas}' 2>/dev/null)" = 2/2 ] && { ok=1; break; }; sleep 3; done
|
|
[ -n "$ok" ] || exit 1
|
|
sleep 12
|
|
$K -n %{ns} get pods -l app=%{dep} -o json | python3 -c '
|
|
import sys, json
|
|
pods = [p for p in json.load(sys.stdin)["items"] if not p["metadata"].get("deletionTimestamp")]
|
|
cs = [s for p in pods for s in p.get("status", {}).get("containerStatuses", [])]
|
|
sys.exit(0 if len(pods) == 2 and len(cs) == 2 and all(s["ready"] and s["restartCount"] == 0 for s in cs) else 1)' || exit 1
|
|
eq "$($K -n %{ns} get cm %{dep}-conf -o jsonpath='{.data.default\.conf}' | md5sum | cut -d' ' -f1)" "$(cat "$WS/.q62-conf.md5" 2>/dev/null)" """),
|
|
],
|
|
"solution": """
|
|
k -n %{ns} get pods # RESTARTS climbing, 0/1 READY
|
|
k -n %{ns} describe pod -l app=%{dep} --show-events | grep -E 'Liveness|Readiness|Unhealthy|Killing'
|
|
# Liveness: http-get http://:80/%{live} ... Readiness: http-get http://:%{port}/status ...
|
|
# Warning Unhealthy Liveness probe failed: Get "http://10.244.x.y:80/%{live}": ... connect: connection refused
|
|
# Warning Unhealthy Readiness probe failed: HTTP probe failed with statuscode: 404
|
|
# Normal Killing Container gateway failed liveness probe, will be restarted
|
|
# (kubectl 1.36 and later leave the Events out when a selector matches several Pods - hence --show-events)
|
|
echo Unhealthy > {WS}/q62/reason.txt
|
|
k -n %{ns} get cm %{dep}-conf -o yaml # listen %{port}; /%{live} = liveness, /%{ready} = readiness
|
|
k -n %{ns} edit deploy %{dep}
|
|
# livenessProbe.httpGet: {path: /%{live}, port: %{port}} (was port 80: connection refused -> restart)
|
|
# readinessProbe.httpGet: {path: /%{ready}, port: %{port}} (was /status: 404 -> never Ready)
|
|
k -n %{ns} rollout status deploy %{dep}
|
|
k -n %{ns} get pods # 1/1 Running, RESTARTS 0
|
|
# "connection refused" is a port problem, a status code is a path problem. A failing liveness
|
|
# probe restarts the container; a failing readiness probe only keeps the Pod out of the Service.
|
|
""",
|
|
},
|
|
{
|
|
"id": 63, "domain": "OBS", "title": "An inventory with kubectl alone",
|
|
"params": lambda r: {"ns": ns_pick(r, "stock"), "busy": r.choice(["W1", "W2", "W3"]), "tier": r.choice(["web", "api"])},
|
|
"task": """
|
|
Namespace `%{ns}` runs a handful of Pods; some of them never started.
|
|
|
|
1. Write a command into `{WS}/q63/inventory.sh` that prints one line per Pod of `%{ns}` in exactly
|
|
three columns, under the headers `POD`, `NODE` and `IMAGE`: the Pod's name, the node it runs on
|
|
and the image of its first container.
|
|
2. Write the names of the Pods labelled `tier=%{tier}` that are **not** in phase `Running` to
|
|
`{WS}/q63/not-running.txt`, one per line.
|
|
3. Write the name of the node that runs the most Pods of `%{ns}` to `{WS}/q63/busiest.txt`.
|
|
""",
|
|
"setup": NS + r"""
|
|
mkns %{ns}; rm -rf "$WS/q63"; mkdir -p "$WS/q63"
|
|
$K -n %{ns} delete pod --all --wait=true --grace-period=1 >/dev/null 2>&1
|
|
busy=$%{busy}; others=$(printf '%s\n' "$W1" "$W2" "$W3" | grep -vx "$busy")
|
|
o1=$(echo "$others" | sed -n 1p); o2=$(echo "$others" | sed -n 2p)
|
|
pod() { # name tier image nodeSelector-key nodeSelector-value
|
|
cat <<EOF
|
|
---
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata: {name: $1, namespace: %{ns}, labels: {app: stock, tier: $2}}
|
|
spec:
|
|
terminationGracePeriodSeconds: 1
|
|
nodeSelector: {"$4": "$5"}
|
|
containers: [{name: main, image: "$3", command: [sh, -c, "sleep 86400"]}]
|
|
EOF
|
|
}
|
|
{
|
|
pod stock-web-1 web busybox:1.36 kubernetes.io/hostname "$busy"
|
|
pod stock-web-2 web nginx:1.27 kubernetes.io/hostname "$busy"
|
|
pod stock-api-1 api redis:7-alpine kubernetes.io/hostname "$busy"
|
|
pod stock-web-3 web nginx:1.25 kubernetes.io/hostname "$o1"
|
|
pod stock-api-2 api busybox:1.36 kubernetes.io/hostname "$o2"
|
|
pod stock-web-4 web busybox:1.36 disktype nvme
|
|
pod stock-api-3 api nginx:1.27 disktype nvme
|
|
pod stock-api-4 api busybox:1.36 disktype nvme
|
|
} | $K apply -f - >/dev/null
|
|
$K -n %{ns} wait --for=condition=Ready pod/stock-web-1 pod/stock-web-2 pod/stock-api-1 pod/stock-web-3 pod/stock-api-2 --timeout=150s >/dev/null
|
|
""",
|
|
"checks": [
|
|
C("inventory.sh prints POD, NODE and IMAGE for every Pod of %{ns}", 3, r"""
|
|
f="$WS/q63/inventory.sh"; [ -s "$f" ] || exit 1
|
|
out=$(cd / && bash "$f" 2>/dev/null) || exit 1
|
|
want=$($K -n %{ns} get pods --no-headers -o 'custom-columns=POD:.metadata.name,NODE:.spec.nodeName,IMAGE:.spec.containers[0].image' | awk '{print $1, $2, $3}' | sort)
|
|
[ "$(echo "$want" | grep -c .)" -ge 8 ] || exit 1
|
|
eq "$(echo "$out" | head -1 | awk '{print $1, $2, $3, NF}')" "POD NODE IMAGE 3" || exit 1
|
|
echo "$out" | tail -n +2 | awk 'NF && NF != 3 {bad = 1} END {exit bad}' || exit 1
|
|
eq "$(echo "$out" | tail -n +2 | awk 'NF {print $1, $2, $3}' | sort)" "$want" """),
|
|
C("not-running.txt: the tier=%{tier} Pods that are not Running", 2, r"""
|
|
want=$($K -n %{ns} get pods -l tier=%{tier} --field-selector='status.phase!=Running' -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' | sort)
|
|
[ -n "$want" ] || exit 1
|
|
eq "$(tr -d ' \t\r' < "$WS/q63/not-running.txt" 2>/dev/null | sed 's#^pod/##' | grep . | sort)" "$want" """),
|
|
C("busiest.txt names the node with the most Pods", 2, r"""
|
|
want=$($K -n %{ns} get pods -o jsonpath='{range .items[*]}{.spec.nodeName}{"\n"}{end}' | grep . | sort | uniq -c | sort -rn | awk 'NR==1 {print $2}')
|
|
[ -n "$want" ] && eq "$(tr -d '[:space:]' < "$WS/q63/busiest.txt" 2>/dev/null)" "$want" """),
|
|
],
|
|
"solution": """
|
|
echo "kubectl -n %{ns} get pods -o custom-columns=POD:.metadata.name,NODE:.spec.nodeName,IMAGE:.spec.containers[0].image" > {WS}/q63/inventory.sh
|
|
bash {WS}/q63/inventory.sh # a Pod that was never scheduled shows <none> as its node
|
|
k -n %{ns} get pods -l tier=%{tier} --field-selector='status.phase!=Running' -o name | cut -d/ -f2 > {WS}/q63/not-running.txt
|
|
k -n %{ns} get pods -o wide --no-headers | awk '{print $7}' | sort | uniq -c | sort -rn # NODE is column 7 of -o wide
|
|
echo <the node on the first line> > {WS}/q63/busiest.txt
|
|
# -l filters on labels, --field-selector on a few fields of the object itself (status.phase,
|
|
# spec.nodeName, metadata.name ...), and both run on the server. custom-columns takes
|
|
# HEADER:jsonpath pairs without the {} that -o jsonpath needs.
|
|
""",
|
|
},
|
|
{
|
|
"id": 64, "domain": "OBS", "title": "One replica is logging errors",
|
|
"params": lambda r: {"ns": ns_pick(r, "trace"), "app": name_pick(r, "router"), "bad": r.choice([0, 1, 2]),
|
|
"code": r.choice([502, 503, 504]), "ver": r.choice(["2024.11", "2025.03", "2025.07"])},
|
|
"task": """
|
|
The three Pods of StatefulSet `%{app}` in Namespace `%{ns}` all log the requests they route, and
|
|
one of them logs errors as well.
|
|
|
|
1. Write the name of the Pod that logs `ERROR` lines to `{WS}/q64/bad-pod.txt`.
|
|
2. Save that Pod's `ERROR` lines, and nothing else, to `{WS}/q64/errors.log`.
|
|
3. Every Pod ran an init container `migrate` before it started. Write the schema version it
|
|
reported to `{WS}/q64/schema.txt`.
|
|
4. Write a command into `{WS}/q64/tail.sh` that prints the last **2** log lines of every Pod of
|
|
the StatefulSet in one go, each line prefixed with the Pod it comes from.
|
|
""",
|
|
"setup": NS + r"""
|
|
mkns %{ns}; rm -rf "$WS/q64"; mkdir -p "$WS/q64"
|
|
$K -n %{ns} delete statefulset %{app} --ignore-not-found --wait=true >/dev/null 2>&1
|
|
$K -n %{ns} delete pod -l app=%{app} --wait=true --grace-period=1 >/dev/null 2>&1
|
|
$K apply -f - >/dev/null <<'EOF'
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata: {name: %{app}, namespace: %{ns}}
|
|
spec: {clusterIP: None, selector: {app: %{app}}, ports: [{port: 80}]}
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: StatefulSet
|
|
metadata: {name: %{app}, namespace: %{ns}}
|
|
spec:
|
|
serviceName: %{app}
|
|
replicas: 3
|
|
podManagementPolicy: Parallel
|
|
selector: {matchLabels: {app: %{app}}}
|
|
template:
|
|
metadata: {labels: {app: %{app}}}
|
|
spec:
|
|
terminationGracePeriodSeconds: 2
|
|
initContainers:
|
|
- name: migrate
|
|
image: busybox:1.36
|
|
command: [sh, -c, "echo 'checking schema'; echo 'schema version %{ver} is current'"]
|
|
containers:
|
|
- name: router
|
|
image: busybox:1.36
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
trap 'exit 0' TERM
|
|
i=0
|
|
while true; do
|
|
i=$((i+1))
|
|
echo "INFO request $i routed"
|
|
if [ "${HOSTNAME##*-}" = "%{bad}" ] && [ $((i % 3)) = 0 ]; then
|
|
echo "ERROR upstream timeout code=%{code} request=$i"
|
|
fi
|
|
sleep 2 & wait $!
|
|
done
|
|
EOF
|
|
$K -n %{ns} rollout status statefulset %{app} --timeout=150s >/dev/null
|
|
""",
|
|
"checks": [
|
|
C("bad-pod.txt names %{app}-%{bad}", 2, r"""eq "$(tr -d '[:space:]' < "$WS/q64/bad-pod.txt" 2>/dev/null | sed 's#^pod/##')" %{app}-%{bad}"""),
|
|
C("errors.log holds its ERROR lines and nothing else", 2, r"""
|
|
f="$WS/q64/errors.log"; [ "$(grep -c . "$f" 2>/dev/null)" -ge 1 ] || exit 1
|
|
[ -z "$(grep . "$f" | grep -Ev 'ERROR upstream timeout code=%{code} request=[0-9]+[[:space:]]*$')" ]"""),
|
|
C("schema.txt holds %{ver}", 1, r"""eq "$(tr -d '[:space:]' < "$WS/q64/schema.txt" 2>/dev/null)" %{ver}"""),
|
|
C("tail.sh prints the last 2 lines of all three Pods, prefixed with the Pod", 2, r"""
|
|
f="$WS/q64/tail.sh"; [ -s "$f" ] || exit 1
|
|
out=$(cd / && bash "$f" 2>/dev/null) || exit 1
|
|
for n in 0 1 2; do [ "$(echo "$out" | grep -c "^\[pod/%{app}-$n/router\] ")" = 2 ] || exit 1; done"""),
|
|
],
|
|
"solution": """
|
|
k -n %{ns} logs -l app=%{app} --prefix --tail=-1 | grep ERROR | head -3 # [pod/%{app}-%{bad}/router] ERROR ...
|
|
echo %{app}-%{bad} > {WS}/q64/bad-pod.txt
|
|
k -n %{ns} logs %{app}-%{bad} | grep ERROR > {WS}/q64/errors.log
|
|
k -n %{ns} logs %{app}-0 -c migrate # schema version %{ver} is current
|
|
echo %{ver} > {WS}/q64/schema.txt
|
|
echo "kubectl -n %{ns} logs -l app=%{app} --prefix --tail=2" > {WS}/q64/tail.sh
|
|
bash {WS}/q64/tail.sh
|
|
# With a selector, kubectl logs shows only the last 10 lines of each Pod unless --tail says
|
|
# otherwise (-1 = everything), and --prefix says which Pod and container a line came from.
|
|
# An init container's log is read like any other container's: -c NAME.
|
|
""",
|
|
},
|
|
# ------------------------------------------------------------------- ENV ----
|
|
{
|
|
"id": 59, "domain": "ENV", "title": "A custom resource the API server rejects",
|
|
"params": lambda r: (lambda eng, pair: {
|
|
"ns": ns_pick(r, "cache"), "name": name_pick(r, "sessions"), "eng": eng, "engcap": eng.capitalize(),
|
|
"mem": r.choice([256, 512, 1024]), "maxr": r.choice([3, 5, 7]), "bad1": pair[0], "bad2": pair[1]})(
|
|
r.choice(["redis", "valkey", "memcached"]), r.sample(["catalog", "ratelimit", "search", "tokens"], 2)),
|
|
"task": """
|
|
An operator for cache clusters runs in this cluster. Its CustomResourceDefinition belongs to the
|
|
API group `cache.example.com`.
|
|
|
|
1. Write the short name kubectl accepts for that kind to `{WS}/q59/shortname.txt`.
|
|
2. The operator reports the state of every cache cluster in `status.phase`. Write each cache
|
|
cluster in the **whole cluster** whose phase is `Degraded` to `{WS}/q59/degraded.txt`, one per
|
|
line as `<namespace>/<name>`.
|
|
3. `{WS}/q59/cache.yaml` is meant to create cache cluster `%{name}` in Namespace `%{ns}`, but the
|
|
API server rejects it. Correct the manifest and apply it: engine `%{eng}`, `%{mem}` MB of
|
|
memory per node, and as many replicas as the CRD allows.
|
|
""",
|
|
"setup": NS + r"""
|
|
rm -rf "$WS/q59"; mkdir -p "$WS/q59"
|
|
$K apply -f - >/dev/null <<'EOF'
|
|
apiVersion: apiextensions.k8s.io/v1
|
|
kind: CustomResourceDefinition
|
|
metadata: {name: cacheclusters.cache.example.com}
|
|
spec:
|
|
group: cache.example.com
|
|
scope: Namespaced
|
|
names: {plural: cacheclusters, singular: cachecluster, kind: CacheCluster, shortNames: [cc]}
|
|
versions:
|
|
- name: v1
|
|
served: true
|
|
storage: true
|
|
subresources: {status: {}}
|
|
additionalPrinterColumns:
|
|
- {name: Engine, type: string, jsonPath: .spec.engine}
|
|
- {name: Replicas, type: integer, jsonPath: .spec.replicas}
|
|
- {name: Phase, type: string, jsonPath: .status.phase}
|
|
schema:
|
|
openAPIV3Schema:
|
|
type: object
|
|
properties:
|
|
spec:
|
|
type: object
|
|
required: [engine, replicas, memoryMb]
|
|
properties:
|
|
engine: {type: string, enum: [redis, valkey, memcached], description: "Cache engine"}
|
|
replicas: {type: integer, minimum: 1, maximum: %{maxr}, description: "Number of cache nodes"}
|
|
memoryMb: {type: integer, minimum: 64, maximum: 4096, description: "Memory per node, in MB"}
|
|
status:
|
|
type: object
|
|
properties:
|
|
phase: {type: string}
|
|
EOF
|
|
$K wait --for=condition=Established crd/cacheclusters.cache.example.com --timeout=60s >/dev/null
|
|
mkns %{ns}; mkns %{ns}-shared
|
|
$K delete cacheclusters.cache.example.com --all -A --wait=true >/dev/null 2>&1
|
|
mk() { # namespace name engine replicas memoryMb
|
|
phase=Ready; case "$2" in %{bad1}|%{bad2}) phase=Degraded ;; esac
|
|
printf 'apiVersion: cache.example.com/v1\nkind: CacheCluster\nmetadata: {name: %s, namespace: %s}\nspec: {engine: %s, replicas: %s, memoryMb: %s}\n' \
|
|
"$2" "$1" "$3" "$4" "$5" | $K apply -f - >/dev/null
|
|
$K -n "$1" patch cacheclusters.cache.example.com "$2" --subresource=status --type=merge -p "{\"status\":{\"phase\":\"$phase\"}}" >/dev/null
|
|
}
|
|
mk %{ns} catalog redis 2 256
|
|
mk %{ns}-shared ratelimit memcached 1 128
|
|
mk %{ns} search valkey 3 512
|
|
mk %{ns}-shared tokens redis 1 64
|
|
cat > "$WS/q59/cache.yaml" <<'EOF'
|
|
apiVersion: cache.example.com/v1alpha1
|
|
kind: CacheCluster
|
|
metadata:
|
|
name: %{name}
|
|
namespace: %{ns}
|
|
spec:
|
|
engine: %{engcap}
|
|
replicas: 12
|
|
memory: %{mem}
|
|
EOF
|
|
""",
|
|
"checks": [
|
|
C("shortname.txt holds cc", 1, r"""eq "$(tr -d '[:space:]' < "$WS/q59/shortname.txt" 2>/dev/null)" cc"""),
|
|
C("degraded.txt lists exactly the Degraded cache clusters", 2, r"""
|
|
want=$($K get cacheclusters.cache.example.com -A -o jsonpath='{range .items[?(@.status.phase=="Degraded")]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' 2>/dev/null | sort)
|
|
[ "$(echo "$want" | grep -c .)" = 2 ] || exit 1
|
|
eq "$(tr -d ' \t\r' < "$WS/q59/degraded.txt" 2>/dev/null | grep . | sort)" "$want" """),
|
|
C("CacheCluster %{name}: engine %{eng}, memoryMb %{mem}", 2, r"""
|
|
eq "$(j -n %{ns} cacheclusters.cache.example.com %{name} '{.spec.engine}/{.spec.memoryMb}')" "%{eng}/%{mem}" """),
|
|
C("it runs the most replicas the CRD allows", 1, r"""
|
|
max=$(j crd cacheclusters.cache.example.com '{.spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.replicas.maximum}')
|
|
[ -n "$max" ] && eq "$(j -n %{ns} cacheclusters.cache.example.com %{name} '{.spec.replicas}')" "$max" """),
|
|
C("cache.yaml itself now applies", 1, r"""
|
|
f="$WS/q59/cache.yaml"; grep -Eq '^[[:space:]]*name:[[:space:]]*%{name}[[:space:]]*$' "$f" 2>/dev/null \
|
|
&& $K apply --dry-run=server -f "$f" >/dev/null 2>&1"""),
|
|
],
|
|
"solution": """
|
|
k api-resources --api-group=cache.example.com # cacheclusters cc cache.example.com/v1 true CacheCluster
|
|
echo cc > {WS}/q59/shortname.txt
|
|
k get cc -A # the PHASE column is status.phase
|
|
k get cc -A --no-headers | awk '$NF == "Degraded" {print $1 "/" $2}' > {WS}/q59/degraded.txt
|
|
|
|
k apply -f {WS}/q59/cache.yaml --dry-run=server # one complaint after another:
|
|
# no matches for kind "CacheCluster" in version "cache.example.com/v1alpha1" -> only v1 is served
|
|
# unknown field "spec.memory" -> the field is memoryMb
|
|
# spec.engine: Unsupported value: "%{engcap}": supported values: "redis", "valkey", "memcached"
|
|
# spec.replicas: Invalid value: 12: spec.replicas in body should be less than or equal to %{maxr}
|
|
k explain cc.spec # engine (enum: redis, valkey, memcached), memoryMb, replicas - all required
|
|
k get crd cacheclusters.cache.example.com -o yaml | grep -n -A1 'maximum'
|
|
vi {WS}/q59/cache.yaml
|
|
# apiVersion: cache.example.com/v1
|
|
# spec: {engine: %{eng}, replicas: %{maxr}, memoryMb: %{mem}}
|
|
k apply -f {WS}/q59/cache.yaml
|
|
# A CRD's schema is the contract: required fields, enum values and bounds are all in it, and
|
|
# --dry-run=server shows every violation without creating anything. .status belongs to the
|
|
# operator - you read it, you do not write it.
|
|
""",
|
|
},
|
|
# ---------------------------------------------------------------- DESIGN ----
|
|
{
|
|
"id": 60, "domain": "DESIGN", "title": "Entrypoint, default argument and a non-root user",
|
|
"params": lambda r: {"img": name_pick(r, r.choice(["reporter", "exporter", "notifier"])), "arg": r.choice(["daily", "weekly", "hourly"]),
|
|
"arg2": r.choice(["audit", "billing", "usage"]), "team": r.choice(["payments", "search", "identity"])},
|
|
"task": """
|
|
`{WS}/q60/image` is the build context of `localhost/%{img}:1.0`, which is already built on this
|
|
machine (use `podman`). That image runs as root, and running it with an argument fails.
|
|
|
|
1. Change the **Dockerfile** so that
|
|
- `/app/report.sh` is the image's entrypoint and `%{arg}` its default argument,
|
|
- the image runs as the non-root user `10001`,
|
|
- the image carries the label `team=%{team}`,
|
|
|
|
and build it as `localhost/%{img}:1.1`.
|
|
2. Run `1.1` once with its defaults and write the output to `{WS}/q60/default.txt`. Run it once
|
|
more with the argument `%{arg2}` instead and write that output to `{WS}/q60/%{arg2}.txt`.
|
|
3. Remove the old image `localhost/%{img}:1.0` from this machine.
|
|
""",
|
|
"setup": r"""
|
|
command -v podman >/dev/null || { echo "podman is not installed on this machine - the task needs it"; exit 1; }
|
|
podman rmi -f localhost/%{img}:1.0 localhost/%{img}:1.1 >/dev/null 2>&1 || true
|
|
rm -rf "$WS/q60"; mkdir -p "$WS/q60/image"
|
|
cat > "$WS/q60/image/Dockerfile" <<'EOF'
|
|
FROM docker.io/library/busybox:1.36
|
|
COPY report.sh /app/report.sh
|
|
RUN chmod 0755 /app/report.sh
|
|
CMD ["/app/report.sh", "all"]
|
|
EOF
|
|
cat > "$WS/q60/image/report.sh" <<'EOF'
|
|
#!/bin/sh
|
|
echo "%{img} report: ${1:-none} (uid $(id -u))"
|
|
EOF
|
|
podman build -q -t localhost/%{img}:1.0 "$WS/q60/image" >/dev/null 2>&1 \
|
|
|| { echo "could not build localhost/%{img}:1.0 - is docker.io/library/busybox:1.36 available to podman?"; exit 1; }
|
|
""",
|
|
"checks": [
|
|
C("1.1 runs /app/report.sh with %{arg} by default, and with another argument when given one", 2, r"""
|
|
eq "$(podman run --rm localhost/%{img}:1.1 2>/dev/null | cut -d'(' -f1)" "%{img} report: %{arg} " || exit 1
|
|
eq "$(podman run --rm localhost/%{img}:1.1 probe 2>/dev/null | cut -d'(' -f1)" "%{img} report: probe " """),
|
|
C("1.1 runs as user 10001 and carries the label team=%{team}", 2, r"""
|
|
podman run --rm localhost/%{img}:1.1 2>/dev/null | grep -q '(uid 10001)$' || exit 1
|
|
eq "$(podman image inspect localhost/%{img}:1.1 --format '{{index .Config.Labels "team"}}' 2>/dev/null)" %{team}"""),
|
|
C("the Dockerfile itself sets ENTRYPOINT, USER and LABEL", 1, r"""
|
|
f="$WS/q60/image/Dockerfile"
|
|
grep -Eiq '^ENTRYPOINT' "$f" 2>/dev/null && grep -Eiq '^USER[[:space:]]+10001' "$f" && grep -Eiq '^LABEL.*team' "$f" """),
|
|
C("default.txt holds the output of the run with defaults (%{arg}), %{arg2}.txt that of the run with the argument %{arg2}", 1, r"""
|
|
eq "$(tr -d '\r' < "$WS/q60/default.txt" 2>/dev/null)" "%{img} report: %{arg} (uid 10001)" \
|
|
&& eq "$(tr -d '\r' < "$WS/q60/%{arg2}.txt" 2>/dev/null)" "%{img} report: %{arg2} (uid 10001)" """),
|
|
C("localhost/%{img}:1.0 is gone, 1.1 is there", 1, r"""
|
|
podman image exists localhost/%{img}:1.1 && ! podman image exists localhost/%{img}:1.0"""),
|
|
],
|
|
"solution": """
|
|
cd {WS}/q60/image
|
|
podman run --rm localhost/%{img}:1.0 %{arg2} # fails: with only a CMD, the argument replaces the whole command
|
|
vi Dockerfile
|
|
# FROM docker.io/library/busybox:1.36
|
|
# LABEL team=%{team}
|
|
# COPY report.sh /app/report.sh
|
|
# RUN chmod 0755 /app/report.sh
|
|
# USER 10001
|
|
# ENTRYPOINT ["/app/report.sh"]
|
|
# CMD ["%{arg}"]
|
|
podman build -t localhost/%{img}:1.1 .
|
|
podman run --rm localhost/%{img}:1.1 > {WS}/q60/default.txt # %{img} report: %{arg} (uid 10001)
|
|
podman run --rm localhost/%{img}:1.1 %{arg2} > {WS}/q60/%{arg2}.txt
|
|
podman rmi localhost/%{img}:1.0
|
|
podman inspect localhost/%{img}:1.1 --format '{{.Config.User}} {{.Config.Entrypoint}} {{.Config.Cmd}} {{.Config.Labels}}'
|
|
# ENTRYPOINT is the program, CMD its default arguments, and what follows the image name on the
|
|
# command line replaces CMD only. Both need the exec form (a JSON array): a shell-form ENTRYPOINT
|
|
# ignores CMD and arguments. In a Pod the same pair is `command` (ENTRYPOINT) and `args` (CMD).
|
|
# USER goes after the RUN steps that need root.
|
|
""",
|
|
},
|
|
# ---------------------------------------------------------------- DEPLOY ----
|
|
{
|
|
"id": 61, "domain": "DEPLOY", "title": "A kustomization that does not build",
|
|
"params": lambda r: {"ns": ns_pick(r, "shop"), "sfx": r.choice(["eu", "us", "apac"]), "tok": name_pick(r, "tok"), "n": r.choice([2, 3])},
|
|
"task": """
|
|
`{WS}/q61/app` holds a kustomization for the app `storefront`, but
|
|
`kubectl kustomize {WS}/q61/app` fails.
|
|
|
|
1. Make it build. Two things are wrong. The Deployment and the Service are fine as they are - do
|
|
not change `deployment.yaml` or `service.yaml`.
|
|
2. Extend `kustomization.yaml` so that the result
|
|
- goes into Namespace `%{ns}`,
|
|
- has every resource name end in `-%{sfx}`,
|
|
- runs image `nginx:1.27`,
|
|
- includes a generated Secret `storefront-token` whose key `token` is `%{tok}`, **without** a
|
|
hash at the end of its name.
|
|
3. Apply it. The Deployment must come up with the %{n} replicas its patch asks for.
|
|
""",
|
|
"setup": NS + r"""
|
|
mkns %{ns}
|
|
$K -n %{ns} delete deploy/storefront-%{sfx} svc/storefront-%{sfx} secret/storefront-token-%{sfx} --ignore-not-found >/dev/null 2>&1
|
|
rm -rf "$WS/q61"; mkdir -p "$WS/q61/app"
|
|
cat > "$WS/q61/app/deployment.yaml" <<'EOF'
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata: {name: storefront, labels: {app: storefront}}
|
|
spec:
|
|
replicas: 1
|
|
selector: {matchLabels: {app: storefront}}
|
|
template:
|
|
metadata: {labels: {app: storefront}}
|
|
spec:
|
|
containers:
|
|
- name: web
|
|
image: nginx:1.25
|
|
ports: [{containerPort: 80}]
|
|
env:
|
|
- name: API_TOKEN
|
|
valueFrom: {secretKeyRef: {name: storefront-token, key: token}}
|
|
EOF
|
|
cat > "$WS/q61/app/service.yaml" <<'EOF'
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata: {name: storefront, labels: {app: storefront}}
|
|
spec: {selector: {app: storefront}, ports: [{port: 80, targetPort: 80}]}
|
|
EOF
|
|
cat > "$WS/q61/app/replicas-patch.yaml" <<'EOF'
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: store-front
|
|
spec:
|
|
replicas: %{n}
|
|
EOF
|
|
cat > "$WS/q61/app/kustomization.yaml" <<'EOF'
|
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
kind: Kustomization
|
|
resources:
|
|
- deploy.yaml
|
|
- service.yaml
|
|
patches:
|
|
- path: replicas-patch.yaml
|
|
EOF
|
|
(cd "$WS/q61/app" && md5sum deployment.yaml service.yaml) > "$WS/.q61-app.md5"
|
|
""",
|
|
"checks": [
|
|
C("builds: Deployment and Service storefront-%{sfx} in %{ns}; %{n} replicas of nginx:1.27", 3, r"""
|
|
$K kustomize "$WS/q61/app" 2>/dev/null | $K create --dry-run=client -o json -f - 2>/dev/null | python3 -c '
|
|
import sys, json
|
|
s = sys.stdin.read(); dec = json.JSONDecoder(); i = 0; items = []
|
|
while i < len(s):
|
|
while i < len(s) and s[i].isspace(): i += 1
|
|
if i >= len(s): break
|
|
o, i = dec.raw_decode(s, i); items.extend(o.get("items", [o]))
|
|
dep = [x for x in items if x["kind"] == "Deployment"]; svc = [x for x in items if x["kind"] == "Service"]
|
|
ok = len(dep) == 1 and len(svc) == 1 and all(x["metadata"].get("namespace") == "%{ns}" for x in items) \
|
|
and dep[0]["metadata"]["name"] == "storefront-%{sfx}" and svc[0]["metadata"]["name"] == "storefront-%{sfx}" \
|
|
and dep[0]["spec"]["replicas"] == %{n} and dep[0]["spec"]["template"]["spec"]["containers"][0]["image"] == "nginx:1.27"
|
|
sys.exit(0 if ok else 1)'"""),
|
|
C("generated Secret storefront-token-%{sfx} (no hash) holds the token, and the Deployment refers to it", 2, r"""
|
|
$K kustomize "$WS/q61/app" 2>/dev/null | $K create --dry-run=client -o json -f - 2>/dev/null | python3 -c '
|
|
import sys, json, base64
|
|
s = sys.stdin.read(); dec = json.JSONDecoder(); i = 0; items = []
|
|
while i < len(s):
|
|
while i < len(s) and s[i].isspace(): i += 1
|
|
if i >= len(s): break
|
|
o, i = dec.raw_decode(s, i); items.extend(o.get("items", [o]))
|
|
sec = [x for x in items if x["kind"] == "Secret"]; dep = [x for x in items if x["kind"] == "Deployment"]
|
|
ok = len(sec) == 1 and len(dep) == 1 and sec[0]["metadata"]["name"] == "storefront-token-%{sfx}" \
|
|
and base64.b64decode(sec[0].get("data", {}).get("token", "")).decode() == "%{tok}" \
|
|
and dep[0]["spec"]["template"]["spec"]["containers"][0]["env"][0]["valueFrom"]["secretKeyRef"]["name"] == "storefront-token-%{sfx}"
|
|
sys.exit(0 if ok else 1)'"""),
|
|
C("applied: %{n} Pods available, and they see API_TOKEN=%{tok}", 2, r"""
|
|
d="$K -n %{ns} get deploy storefront-%{sfx}"; ok=
|
|
for i in $(seq 1 30); do [ "$($d -o jsonpath='{.status.availableReplicas}' 2>/dev/null)" = %{n} ] && { ok=1; break; }; sleep 3; done
|
|
[ -n "$ok" ] || exit 1
|
|
eq "$($K -n %{ns} exec deploy/storefront-%{sfx} -- printenv API_TOKEN 2>/dev/null | tr -d '\r')" %{tok}"""),
|
|
C("deployment.yaml and service.yaml left as they were", 1, r"""
|
|
[ -n "$($K kustomize "$WS/q61/app" 2>/dev/null)" ] && (cd "$WS/q61/app" && md5sum -c --quiet "$WS/.q61-app.md5" >/dev/null 2>&1)"""),
|
|
],
|
|
"solution": """
|
|
cd {WS}/q61/app
|
|
k kustomize . # ... deploy.yaml: no such file or directory -> the file is deployment.yaml
|
|
k kustomize . # no matches for Id Deployment...store-front; failed to find unique target for patch
|
|
sed -i 's/name: store-front/name: storefront/' replicas-patch.yaml # a patch finds its target by kind and name
|
|
# (or leave the file and name the target in kustomization.yaml: target: {kind: Deployment, name: storefront})
|
|
cat > kustomization.yaml <<EOF
|
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
kind: Kustomization
|
|
resources:
|
|
- deployment.yaml
|
|
- service.yaml
|
|
patches:
|
|
- path: replicas-patch.yaml
|
|
namespace: %{ns}
|
|
nameSuffix: -%{sfx}
|
|
images:
|
|
- {name: nginx, newTag: "1.27"}
|
|
secretGenerator:
|
|
- name: storefront-token
|
|
literals: [token=%{tok}]
|
|
generatorOptions:
|
|
disableNameSuffixHash: true
|
|
EOF
|
|
k kustomize . # read it: storefront-%{sfx} x3 kinds, and secretKeyRef.name became storefront-token-%{sfx}
|
|
k apply -k .
|
|
k -n %{ns} get deploy,svc,secret
|
|
# Patches, images and replicas use the names in the files; nameSuffix is applied afterwards, to
|
|
# the generated Secret as well, and every reference to a renamed object is rewritten for you.
|
|
# Without disableNameSuffixHash the Secret would be storefront-token-%{sfx}-<hash>.
|
|
""",
|
|
},
|
|
]
|
|
|
|
# written grouped by domain, listed by number
|
|
QUESTIONS.sort(key=lambda q: q["id"])
|