""" CKA track for the exam simulator. TRACK - metadata (name, duration, pass mark, domain table) QUESTIONS - list of tasks. Each task: id, domain, title, task (markdown shown to candidate), setup : bash that seeds the scenario checks : list of (name, points, bash) - each check is graded independently (exit 0 = pass). Points earned = sum of passing checks, so partial credit works the way it does on the real exam. disruptive: True => setup runs last, after everything else is scheduled. Scripts get a prelude: $K (kubectl), $WS (workspace), $CP/$W1/$W2/$W3 (node names), nx NODE 'cmd' (run a command on a node). {WS}, {CP}, {W2}, {W3}, {SHELL_CP} etc. are substituted in task text. """ TRACK = { "id": "cka", "name": "Certified Kubernetes Administrator", "minutes": 120, "pass_pct": 66, # tasks per domain drawn into a session (weights follow the exam blueprint) "quota": {"ARCH": 5, "WORK": 4, "NET": 4, "STOR": 2, "TSHOOT": 5}, # 20 tasks "quota_hard": {"ARCH": 6, "WORK": 5, "NET": 5, "STOR": 3, "TSHOOT": 6}, # 25 tasks, hard ones preferred # fixed simulator sets (./exam sim a|b|c|d) - excluded from random sampling "sets": {"a": "Simulator Set A", "b": "Simulator Set B", "c": "Simulator Set C", "d": "Simulator Set D (killer.sh-style)"}, "domains": { "ARCH": "Cluster Architecture, Installation & Configuration (25%)", "WORK": "Workloads & Scheduling (15%)", "NET": "Services & Networking (20%)", "STOR": "Storage (10%)", "TSHOOT": "Troubleshooting (30%)", }, } from tracks.cka.common import C, NS QUESTIONS = [ # ================================================================ ARCH ==== { "id": 1, "domain": "ARCH", "title": "RBAC for a deployment bot", "task": """ Namespace `rbac-app` already exists. 1. Create a ServiceAccount `deploy-bot` in namespace `rbac-app`. 2. Create a Role `deploy-manager` in `rbac-app` that grants get, list, watch, create, update, patch, delete on **deployments** (apps API group) only. 3. Bind it to the ServiceAccount with a RoleBinding named `deploy-bot-binding`. The ServiceAccount must NOT be able to delete pods, and must have no access outside the `rbac-app` namespace. Verify with `kubectl auth can-i`. """, "setup": NS + r""" mkns rbac-app $K -n rbac-app delete sa deploy-bot --ignore-not-found >/dev/null $K -n rbac-app delete role deploy-manager --ignore-not-found >/dev/null $K -n rbac-app delete rolebinding deploy-bot-binding --ignore-not-found >/dev/null """, "checks": [ C("ServiceAccount rbac-app/deploy-bot exists", 1, r"""$K -n rbac-app get sa deploy-bot >/dev/null 2>&1"""), C("Role deploy-manager and RoleBinding deploy-bot-binding exist", 1, r"""$K -n rbac-app get role deploy-manager >/dev/null 2>&1 && $K -n rbac-app get rolebinding deploy-bot-binding >/dev/null 2>&1"""), C("deploy-bot can get/list/watch/create/update/patch/delete deployments", 2, r""" for v in get list watch create update patch delete; do [ "$($K auth can-i $v deployments -n rbac-app --as=system:serviceaccount:rbac-app:deploy-bot 2>/dev/null)" = yes ] || exit 1 done"""), C("deploy-bot cannot delete pods", 1, r"""[ "$($K auth can-i delete pods -n rbac-app --as=system:serviceaccount:rbac-app:deploy-bot 2>/dev/null || true)" = no ]"""), C("deploy-bot has no access in namespace default", 1, r"""[ "$($K auth can-i create deployments -n default --as=system:serviceaccount:rbac-app:deploy-bot 2>/dev/null || true)" = no ]"""), ], }, { "id": 2, "domain": "ARCH", "title": "etcd snapshot", "task": """ Create a snapshot of the cluster's etcd database and save it on the **control-plane node** at `/opt/cka/etcd-snapshot.db`. Details you need: - etcd listens on `https://127.0.0.1:2379` - certificates are under `/etc/kubernetes/pki/etcd/` (`ca.crt`, `server.crt`, `server.key`) - "ssh to the control-plane node" is: `{SHELL_CP}` - `etcdctl` and `etcdutl` are installed on that node, in `/usr/local/bin` Verify the snapshot with `etcdutl snapshot status` (or `etcdctl snapshot status`). """, "setup": r""" nx "$CP" 'mkdir -p /opt/cka; rm -f /opt/cka/etcd-snapshot.db' ensure_etcd_tools || { echo "no etcdctl/etcdutl on $CP - fix with ./etcd-tools.sh, then re-seed"; exit 1; } """, "checks": [ C("/opt/cka/etcd-snapshot.db exists on the control-plane node", 3, r"""nx "$CP" 'test -f /opt/cka/etcd-snapshot.db'"""), C("snapshot is a real etcd backup (> 100 KB)", 4, r"""[ "$(nx "$CP" 'stat -c %s /opt/cka/etcd-snapshot.db' 2>/dev/null || echo 0)" -gt 100000 ]"""), ], }, { "id": 3, "domain": "ARCH", "title": "Kustomize overlay", "task": """ A Kustomize base lives in `{WS}/q03/base` (Deployment `store-api` + Service `store-api`). Create a production overlay at `{WS}/q03/overlays/prod` that: - deploys everything into namespace `store-prod` (create the Namespace as part of the overlay), - adds the common label `env=prod` to all resources, - sets the `store-api` Deployment to **3** replicas. Apply it with `kubectl apply -k`. """, "setup": r""" rm -rf "$WS/q03"; mkdir -p "$WS/q03/base" cat > "$WS/q03/base/kustomization.yaml" <<'EOF' apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - deployment.yaml - service.yaml EOF cat > "$WS/q03/base/deployment.yaml" <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: store-api spec: replicas: 1 selector: matchLabels: app: store-api template: metadata: labels: app: store-api spec: containers: - name: api image: nginx:1.25 ports: - containerPort: 80 EOF cat > "$WS/q03/base/service.yaml" <<'EOF' apiVersion: v1 kind: Service metadata: name: store-api spec: selector: app: store-api ports: - port: 80 targetPort: 80 EOF $K delete ns store-prod --ignore-not-found --wait=false >/dev/null """, "checks": [ C("overlay kustomization.yaml exists", 1, r"""[ -f "$WS/q03/overlays/prod/kustomization.yaml" ]"""), C("resources deployed into namespace store-prod", 1, r"""$K -n store-prod get deploy store-api >/dev/null 2>&1 && $K -n store-prod get svc store-api >/dev/null 2>&1"""), C("store-api has 3 replicas", 1, r"""[ "$($K -n store-prod get deploy store-api -o jsonpath='{.spec.replicas}')" = 3 ]"""), C("Deployment and Service carry label env=prod", 2, r"""[ "$($K -n store-prod get deploy store-api -o jsonpath='{.metadata.labels.env}')" = prod ] && [ "$($K -n store-prod get svc store-api -o jsonpath='{.metadata.labels.env}')" = prod ]"""), ], }, { "id": 4, "domain": "ARCH", "title": "CustomResourceDefinition", "task": """ 1. Create a CRD with: - group `data.example.com`, version `v1` (served + storage) - kind `Backup`, plural `backups`, singular `backup`, shortName `bk` - scope `Namespaced` - an OpenAPI v3 schema where `spec` has string fields `schedule` and `target` 2. Create a `Backup` named `nightly` in namespace `default` with `spec.schedule: "0 2 * * *"` and `spec.target: "s3://backups/prod"`. `kubectl get bk` must list it. """, "setup": r"""$K delete crd backups.data.example.com --ignore-not-found >/dev/null""", "checks": [ C("CRD backups.data.example.com exists and is Namespaced", 1, r"""[ "$($K get crd backups.data.example.com -o jsonpath='{.spec.scope}' 2>/dev/null)" = Namespaced ]"""), C("shortName bk works", 1, r"""$K get crd backups.data.example.com -o jsonpath='{.spec.names.shortNames}' | grep -q '"bk"'"""), C("Backup nightly has the right schedule", 1, r"""[ "$($K -n default get bk nightly -o jsonpath='{.spec.schedule}' 2>/dev/null)" = "0 2 * * *" ]"""), C("Backup nightly has the right target", 1, r"""[ "$($K -n default get bk nightly -o jsonpath='{.spec.target}' 2>/dev/null)" = "s3://backups/prod" ]"""), ], }, { "id": 5, "domain": "ARCH", "title": "Static pod", "breaks": ["w3-staticpath"], # Q103 relocates W3's staticPodPath "task": """ Create a **static pod** named `static-web` running image `nginx:1.27` on node `{W3}` (namespace `default`). You must place the manifest in the kubelet's static pod path on that node. Reach the node with: `{SHELL_W3}` (check `/var/lib/kubelet/config.yaml` for `staticPodPath`). The mirror pod `static-web-{W3}` must reach Running. """, "setup": r"""nx "$W3" 'rm -f /etc/kubernetes/manifests/static-web*.yaml' || true""", "checks": [ C("manifest containing static-web is in /etc/kubernetes/manifests on the node", 2, r"""nx "$W3" 'grep -ls static-web /etc/kubernetes/manifests/*.yaml' >/dev/null 2>&1"""), C("mirror pod static-web- is Running", 2, r"""[ "$($K -n default get pod "static-web-$W3" -o jsonpath='{.status.phase}' 2>/dev/null)" = Running ]"""), C("image is nginx:1.27", 1, r"""[ "$($K -n default get pod "static-web-$W3" -o jsonpath='{.spec.containers[0].image}' 2>/dev/null)" = nginx:1.27 ]"""), ], }, { "id": 6, "domain": "ARCH", "title": "Certificates: expiry check and user CSR", "task": """ Part A — On the control-plane node (`{SHELL_CP}`) determine the expiry date of the **kube-apiserver** serving certificate (use `kubeadm certs check-expiration` or `openssl`). Write the expiry line to `{WS}/q06/apiserver-expiry.txt` on your machine. Part B — Onboard a new user `jane`: 1. Generate a private key and a CSR with CN=jane (openssl). 2. Create a Kubernetes `CertificateSigningRequest` named `jane` (signer `kubernetes.io/kube-apiserver-client`, usage `client auth`). 3. Approve it so a certificate is issued. 4. Create a RoleBinding `jane-view` in namespace `default` giving user `jane` the built-in ClusterRole `view`. """, "setup": r""" rm -rf "$WS/q06"; mkdir -p "$WS/q06" $K delete csr jane --ignore-not-found >/dev/null $K -n default delete rolebinding jane-view --ignore-not-found >/dev/null """, "checks": [ C("apiserver-expiry.txt records the real expiry month and year", 2, r""" [ -s "$WS/q06/apiserver-expiry.txt" ] || exit 1 exp=$(nx "$CP" 'openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -enddate' | sed 's/notAfter=//') yr=$(echo "$exp" | awk '{print $4}'); mon=$(echo "$exp" | awk '{print $1}') grep -q "$yr" "$WS/q06/apiserver-expiry.txt" && grep -qi "$mon" "$WS/q06/apiserver-expiry.txt" """), C("CSR jane exists with signer kubernetes.io/kube-apiserver-client", 1, r"""[ "$($K get csr jane -o jsonpath='{.spec.signerName}' 2>/dev/null)" = kubernetes.io/kube-apiserver-client ]"""), C("CSR jane approved and certificate issued", 2, r"""$K get csr jane -o jsonpath='{.status.conditions[*].type}' 2>/dev/null | grep -q Approved && [ -n "$($K get csr jane -o jsonpath='{.status.certificate}')" ]"""), C("jane can list pods in default (view) but not delete them", 1, r"""[ "$($K auth can-i list pods -n default --as=jane 2>/dev/null || true)" = yes ] && [ "$($K auth can-i delete pods -n default --as=jane 2>/dev/null || true)" = no ]"""), ], }, # ================================================================ WORK ==== { "id": 7, "domain": "WORK", "title": "Rolling update & scaling", "task": """ Deployment `web-frontend` in namespace `apps` runs `nginx:1.25` with 2 replicas. - Update the image to `nginx:1.27`. - Scale to **4** replicas. - Configure the rolling update strategy: `maxSurge: 1`, `maxUnavailable: 0`. - All 4 replicas must be available when you are done. """, "setup": NS + r""" mkns apps $K -n apps delete deploy web-frontend --ignore-not-found >/dev/null $K apply -f - >/dev/null <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: web-frontend namespace: apps spec: replicas: 2 selector: matchLabels: {app: web-frontend} template: metadata: labels: {app: web-frontend} spec: containers: - name: nginx image: nginx:1.25 resources: requests: {cpu: 50m, memory: 32Mi} EOF """, "checks": [ C("image is nginx:1.27", 1, r"""[ "$($K -n apps get deploy web-frontend -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null)" = nginx:1.27 ]"""), C("replicas = 4", 1, r"""[ "$($K -n apps get deploy web-frontend -o jsonpath='{.spec.replicas}' 2>/dev/null)" = 4 ]"""), C("strategy maxSurge=1 maxUnavailable=0", 1, r"""[ "$($K -n apps get deploy web-frontend -o jsonpath='{.spec.strategy.rollingUpdate.maxSurge}')" = 1 ] && [ "$($K -n apps get deploy web-frontend -o jsonpath='{.spec.strategy.rollingUpdate.maxUnavailable}')" = 0 ]"""), C("all 4 replicas available", 1, r"""[ "$($K -n apps get deploy web-frontend -o jsonpath='{.status.availableReplicas}' 2>/dev/null)" = 4 ]"""), ], }, { "id": 8, "domain": "WORK", "title": "Multi-container pod with shared volume", "task": """ Create a pod `log-shipper` in namespace `apps` with two containers: - `app` (image `busybox:1.36`): appends the current date to `/var/log/app.log` every 2 seconds. - `shipper` (image `busybox:1.36`): runs `tail -f /var/log/app.log`. Both containers share an `emptyDir` volume named `logs` mounted at `/var/log`. `kubectl logs log-shipper -c shipper` must show the dates written by `app`. """, "setup": NS + r""" mkns apps $K -n apps delete pod log-shipper --ignore-not-found --wait=false >/dev/null """, "checks": [ C("pod log-shipper Running with containers app and shipper", 1, r""" p="$K -n apps get pod log-shipper" [ "$($p -o jsonpath='{.status.phase}' 2>/dev/null)" = Running ] || exit 1 n=$($p -o jsonpath='{.spec.containers[*].name}'); echo "$n" | grep -qw app && echo "$n" | grep -qw shipper"""), C("volume logs is an emptyDir mounted at /var/log in both containers", 2, r""" p="$K -n apps get pod log-shipper" [ -n "$($p -o jsonpath='{.spec.volumes[?(@.name=="logs")].emptyDir}' 2>/dev/null)" ] || exit 1 [ "$($p -o jsonpath='{.spec.containers[?(@.name=="app")].volumeMounts[?(@.name=="logs")].mountPath}')" = /var/log ] || exit 1 [ "$($p -o jsonpath='{.spec.containers[?(@.name=="shipper")].volumeMounts[?(@.name=="logs")].mountPath}')" = /var/log ]"""), C("shipper's logs show lines written by app", 1, r"""sleep 3; [ -n "$($K -n apps logs log-shipper -c shipper --tail=2 2>/dev/null)" ]"""), ], }, { "id": 9, "domain": "WORK", "title": "Scheduling with node affinity and resources", "task": """ One node in the cluster is labelled `disktype=ssd`. Create a Deployment `cache` in namespace `apps`, 2 replicas, image `redis:7-alpine`, that: - uses **required** node affinity (`requiredDuringSchedulingIgnoredDuringExecution`) so pods only run on nodes with `disktype=ssd` (do not use nodeSelector), - sets resource requests `cpu: 100m`, `memory: 64Mi` and limits `cpu: 200m`, `memory: 128Mi`. Both pods must be Running. """, "setup": NS + r""" mkns apps $K label node "$W3" disktype=ssd --overwrite >/dev/null $K label node "$W1" disktype- >/dev/null 2>&1 || true $K label node "$W2" disktype- >/dev/null 2>&1 || true $K -n apps delete deploy cache --ignore-not-found >/dev/null """, "checks": [ C("deployment cache with 2 replicas", 1, r"""[ "$($K -n apps get deploy cache -o jsonpath='{.spec.replicas}' 2>/dev/null)" = 2 ]"""), C("required node affinity set, no nodeSelector", 1, r"""d="$K -n apps get deploy cache"; [ -n "$($d -o jsonpath='{.spec.template.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution}' 2>/dev/null)" ] && [ -z "$($d -o jsonpath='{.spec.template.spec.nodeSelector}')" ]"""), C("requests 100m/64Mi and limits 200m/128Mi", 1, r""" c='{.spec.template.spec.containers[0].resources'; d="$K -n apps get deploy cache" [ "$($d -o jsonpath="$c.requests.cpu}")" = 100m ] && [ "$($d -o jsonpath="$c.requests.memory}")" = 64Mi ] && [ "$($d -o jsonpath="$c.limits.cpu}")" = 200m ] && [ "$($d -o jsonpath="$c.limits.memory}")" = 128Mi ]"""), C("both pods available and on the ssd node", 1, r""" [ "$($K -n apps get deploy cache -o jsonpath='{.status.availableReplicas}')" = 2 ] || exit 1 for n in $($K -n apps get pod -l app=cache -o jsonpath='{.items[*].spec.nodeName}'); do [ "$n" = "$W3" ] || exit 1; done"""), ], }, { "id": 10, "domain": "WORK", "title": "ConfigMap and Secret consumption", "task": """ In namespace `apps`: 1. Create ConfigMap `app-config` with keys `APP_MODE=production` and `LOG_LEVEL=debug`. 2. Create Secret `db-creds` with keys `username=admin` and `password=S3cr3t!`. 3. Create pod `config-pod` (image `busybox:1.36`, command `sleep 3600`) that: - exposes `APP_MODE` as an env var sourced from the ConfigMap key, - exposes **all** keys of `db-creds` as env vars (envFrom), - mounts the ConfigMap as a volume at `/etc/config`. """, "setup": NS + r""" mkns apps $K -n apps delete pod config-pod --ignore-not-found --wait=false >/dev/null $K -n apps delete cm app-config --ignore-not-found >/dev/null $K -n apps delete secret db-creds --ignore-not-found >/dev/null """, "checks": [ C("ConfigMap app-config and Secret db-creds with correct values", 1, r"""[ "$($K -n apps get cm app-config -o jsonpath='{.data.LOG_LEVEL}' 2>/dev/null)" = debug ] && [ "$($K -n apps get secret db-creds -o jsonpath='{.data.password}' 2>/dev/null | base64 -d)" = 'S3cr3t!' ]"""), C("APP_MODE injected via configMapKeyRef", 1, r"""[ "$($K -n apps get pod config-pod -o jsonpath='{.status.phase}' 2>/dev/null)" = Running ] && [ -n "$($K -n apps get pod config-pod -o jsonpath='{.spec.containers[0].env[?(@.name=="APP_MODE")].valueFrom.configMapKeyRef}')" ] && $K -n apps exec config-pod -- env 2>/dev/null | grep -q '^APP_MODE=production$'"""), C("all db-creds keys injected via envFrom", 1, r"""[ -n "$($K -n apps get pod config-pod -o jsonpath='{.spec.containers[0].envFrom[*].secretRef}' 2>/dev/null)" ] && $K -n apps exec config-pod -- env 2>/dev/null | grep -q '^password=S3cr3t!$'"""), C("ConfigMap mounted at /etc/config", 1, r"""[ "$($K -n apps exec config-pod -- cat /etc/config/LOG_LEVEL 2>/dev/null)" = debug ]"""), ], }, # ================================================================= NET ==== { "id": 11, "domain": "NET", "title": "NetworkPolicy", "task": """ Namespace `netpol` has deployments `frontend`, `backend` and `db` (labels `app=frontend|backend|db`). Create a NetworkPolicy `backend-policy` in `netpol` so that pods with `app=backend` - accept ingress **only** from pods labelled `app=frontend` in the same namespace, on TCP port **8080**, - reject all other ingress. Do not restrict egress. (This cluster's kindnet enforces NetworkPolicy, so a wrong policy really blocks traffic; you are graded on the policy spec.) """, "setup": NS + r""" mkns netpol for a in frontend backend db; do $K -n netpol create deploy $a --image=nginx:1.25 --dry-run=client -o yaml | $K apply -f - >/dev/null done $K -n netpol delete netpol backend-policy --ignore-not-found >/dev/null """, "checks": [ C("policy selects app=backend, policyTypes Ingress only", 2, r"""n="$K -n netpol get netpol backend-policy"; [ "$($n -o jsonpath='{.spec.podSelector.matchLabels.app}' 2>/dev/null)" = backend ] && $n -o jsonpath='{.spec.policyTypes}' | grep -q Ingress && ! $n -o jsonpath='{.spec.policyTypes}' | grep -q Egress"""), C("single ingress rule from podSelector app=frontend only", 2, r"""n="$K -n netpol get netpol backend-policy"; [ "$($n -o jsonpath='{.spec.ingress[0].from[0].podSelector.matchLabels.app}' 2>/dev/null)" = frontend ] && [ -z "$($n -o jsonpath='{.spec.ingress[1]}')" ] && [ -z "$($n -o jsonpath='{.spec.ingress[0].from[1]}')" ]"""), C("allowed port is TCP 8080", 2, r"""n="$K -n netpol get netpol backend-policy"; [ "$($n -o jsonpath='{.spec.ingress[0].ports[0].port}' 2>/dev/null)" = 8080 ] && { p=$($n -o jsonpath='{.spec.ingress[0].ports[0].protocol}'); [ -z "$p" ] || [ "$p" = TCP ]; }"""), ], }, { "id": 12, "domain": "NET", "title": "NodePort Service", "task": """ Deployment `shop` (namespace `svc`) serves HTTP on container port 80. Expose it with a Service `shop-svc` of type **NodePort**: port 80, targetPort 80, nodePort **30080**. The Service must have ready endpoints. """, "setup": NS + r""" mkns svc $K -n svc create deploy shop --image=nginx:1.25 --port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null $K -n svc delete svc shop-svc --ignore-not-found >/dev/null """, "checks": [ C("Service shop-svc is NodePort, port 80 -> targetPort 80", 1, r"""s="$K -n svc get svc shop-svc"; [ "$($s -o jsonpath='{.spec.type}' 2>/dev/null)" = NodePort ] && [ "$($s -o jsonpath='{.spec.ports[0].port}')" = 80 ] && [ "$($s -o jsonpath='{.spec.ports[0].targetPort}')" = 80 ]"""), C("nodePort is 30080", 1, r"""[ "$($K -n svc get svc shop-svc -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null)" = 30080 ]"""), C("Service has endpoints", 2, r"""[ -n "$($K -n svc get endpointslices -l kubernetes.io/service-name=shop-svc -o jsonpath='{.items[*].endpoints[*].addresses[*]}' 2>/dev/null)" ]"""), ], }, { "id": 13, "domain": "NET", "title": "Ingress routing", "task": """ Namespace `ingress-app` has Services `web` (port 80) and `api` (port 8080). Create an Ingress `app-ingress` in `ingress-app`: - `ingressClassName: nginx` - host `shop.example.com` - path `/api` (Prefix) -> service `api` port 8080 - path `/` (Prefix) -> service `web` port 80 (No ingress controller is installed; you are graded on the object.) """, "setup": NS + r""" mkns ingress-app $K -n ingress-app create deploy web --image=nginx:1.25 --port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null $K -n ingress-app create deploy api --image=nginx:1.25 --port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null $K -n ingress-app expose deploy web --port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null $K -n ingress-app expose deploy api --port=8080 --target-port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null $K -n ingress-app delete ingress app-ingress --ignore-not-found >/dev/null """, "checks": [ C("ingressClassName nginx and host shop.example.com", 1, r"""i="$K -n ingress-app get ingress app-ingress"; [ "$($i -o jsonpath='{.spec.ingressClassName}' 2>/dev/null)" = nginx ] && [ "$($i -o jsonpath='{.spec.rules[0].host}')" = shop.example.com ]"""), C("/api (Prefix) -> api:8080", 2, r"""i="$K -n ingress-app get ingress app-ingress"; P='{.spec.rules[0].http.paths[?(@.path=="/api")]'; [ "$($i -o jsonpath="$P.backend.service.name}" 2>/dev/null)" = api ] && [ "$($i -o jsonpath="$P.backend.service.port.number}")" = 8080 ] && [ "$($i -o jsonpath="$P.pathType}")" = Prefix ]"""), C("/ (Prefix) -> web:80", 2, r"""i="$K -n ingress-app get ingress app-ingress"; P='{.spec.rules[0].http.paths[?(@.path=="/")]'; [ "$($i -o jsonpath="$P.backend.service.name}" 2>/dev/null)" = web ] && [ "$($i -o jsonpath="$P.backend.service.port.number}")" = 80 ] && [ "$($i -o jsonpath="$P.pathType}")" = Prefix ]"""), ], }, { "id": 14, "domain": "NET", "title": "Cluster DNS", "task": """ Service `hello` exists in namespace `dns-check`. 1. Run a pod `dns-test` (image `busybox:1.36`) in namespace `dns-check`. 2. From inside it, resolve the Service using its **fully-qualified** DNS name. 3. Save the FQDN you used (one line) to `{WS}/q14/svc-fqdn.txt`. 4. Save the full `nslookup` output to `{WS}/q14/nslookup.txt`. """, "setup": NS + r""" mkns dns-check rm -rf "$WS/q14"; mkdir -p "$WS/q14" $K -n dns-check create deploy hello --image=nginx:1.25 --port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null $K -n dns-check expose deploy hello --port=80 --dry-run=client -o yaml | $K apply -f - >/dev/null $K -n dns-check delete pod dns-test --ignore-not-found --wait=false >/dev/null """, "checks": [ C("pod dns-test exists in dns-check", 1, r"""$K -n dns-check get pod dns-test >/dev/null 2>&1"""), C("svc-fqdn.txt contains hello.dns-check.svc.cluster.local", 2, r"""grep -qx 'hello.dns-check.svc.cluster.local\.\?' "$WS/q14/svc-fqdn.txt" 2>/dev/null"""), C("nslookup.txt contains the service ClusterIP", 1, r"""ip=$($K -n dns-check get svc hello -o jsonpath='{.spec.clusterIP}'); grep -q "$ip" "$WS/q14/nslookup.txt" 2>/dev/null"""), ], }, # ================================================================ STOR ==== { "id": 15, "domain": "STOR", "title": "PersistentVolume, claim and pod", "task": """ 1. Create a PersistentVolume `pv-data`: 1Gi, `hostPath` `/mnt/data`, accessMode `ReadWriteOnce`, `storageClassName: manual`, reclaim policy `Retain`. 2. Create a PersistentVolumeClaim `pvc-data` in namespace `storage` requesting 500Mi with `storageClassName: manual` so it binds to `pv-data`. 3. Create pod `data-pod` (image `nginx:1.25`) in `storage` mounting the claim at `/usr/share/data`. """, "setup": NS + r""" mkns storage $K -n storage delete pod data-pod --ignore-not-found --wait=false >/dev/null $K -n storage delete pvc pvc-data --ignore-not-found >/dev/null $K delete pv pv-data --ignore-not-found >/dev/null """, "checks": [ C("PV pv-data: 1Gi hostPath /mnt/data, RWO, Retain, class manual", 2, r""" pv="$K get pv pv-data" [ "$($pv -o jsonpath='{.spec.capacity.storage}' 2>/dev/null)" = 1Gi ] && [ "$($pv -o jsonpath='{.spec.hostPath.path}')" = /mnt/data ] && [ "$($pv -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')" = Retain ] && [ "$($pv -o jsonpath='{.spec.storageClassName}')" = manual ] && $pv -o jsonpath='{.spec.accessModes}' | grep -q ReadWriteOnce"""), C("PVC pvc-data Bound to pv-data", 2, r"""[ "$($K -n storage get pvc pvc-data -o jsonpath='{.status.phase}' 2>/dev/null)" = Bound ] && [ "$($K -n storage get pvc pvc-data -o jsonpath='{.spec.volumeName}')" = pv-data ]"""), C("data-pod Running with claim mounted at /usr/share/data", 2, r"""[ "$($K -n storage get pod data-pod -o jsonpath='{.status.phase}' 2>/dev/null)" = Running ] && [ -n "$($K -n storage get pod data-pod -o jsonpath='{.spec.volumes[?(@.persistentVolumeClaim.claimName=="pvc-data")].name}')" ] && $K -n storage get pod data-pod -o jsonpath='{.spec.containers[0].volumeMounts[*].mountPath}' | grep -q /usr/share/data"""), ], }, { "id": 16, "domain": "STOR", "title": "Default StorageClass and dynamic provisioning", "task": """ 1. Create a StorageClass `fast-local`: provisioner `rancher.io/local-path`, `volumeBindingMode: WaitForFirstConsumer`, `reclaimPolicy: Delete`. 2. Make `fast-local` the **default** StorageClass and ensure the existing `standard` class is no longer default. 3. Create PVC `fast-claim` (200Mi, RWO) in namespace `storage` **without** specifying a storageClassName. 4. Create pod `fast-pod` (image `nginx:1.25`) in `storage` that mounts `fast-claim` at `/data`. The claim must end up Bound. """, "setup": NS + r""" mkns storage $K -n storage delete pod fast-pod --ignore-not-found --wait=false >/dev/null $K -n storage delete pvc fast-claim --ignore-not-found >/dev/null $K delete sc fast-local --ignore-not-found >/dev/null $K annotate sc standard storageclass.kubernetes.io/is-default-class=true --overwrite >/dev/null """, "checks": [ C("StorageClass fast-local: local-path, WaitForFirstConsumer, Delete", 1, r"""sc="$K get sc fast-local"; [ "$($sc -o jsonpath='{.provisioner}' 2>/dev/null)" = rancher.io/local-path ] && [ "$($sc -o jsonpath='{.volumeBindingMode}')" = WaitForFirstConsumer ] && [ "$($sc -o jsonpath='{.reclaimPolicy}')" = Delete ]"""), C("fast-local is default and standard is not", 2, r"""[ "$($K get sc fast-local -o jsonpath='{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}' 2>/dev/null)" = true ] && [ "$($K get sc standard -o jsonpath='{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}')" != true ]"""), C("fast-claim picked up fast-local and is Bound", 1, r"""[ "$($K -n storage get pvc fast-claim -o jsonpath='{.spec.storageClassName}' 2>/dev/null)" = fast-local ] && [ "$($K -n storage get pvc fast-claim -o jsonpath='{.status.phase}')" = Bound ]"""), C("fast-pod Running with claim at /data", 1, r"""[ "$($K -n storage get pod fast-pod -o jsonpath='{.status.phase}' 2>/dev/null)" = Running ] && $K -n storage get pod fast-pod -o jsonpath='{.spec.containers[0].volumeMounts[*].mountPath}' | grep -qw /data"""), ], }, # ============================================================== TSHOOT ==== { "id": 17, "domain": "TSHOOT", "breaks": "w2-kubelet", "title": "Node NotReady", "disruptive": True, "task": """ Node `{W2}` is `NotReady`. Investigate and fix it so the node returns to `Ready`. The fix must survive a reboot of the node (i.e. the responsible service must be enabled). Reach the node with: `{SHELL_W2}` """, "setup": r"""nx "$W2" 'systemctl disable kubelet >/dev/null 2>&1; systemctl stop kubelet'""", "checks": [ C("node is Ready", 4, r"""[ "$($K get node "$W2" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null)" = True ]"""), C("kubelet is active", 2, r"""[ "$(nx "$W2" 'systemctl is-active kubelet')" = active ]"""), C("kubelet is enabled (survives reboot)", 2, r"""[ "$(nx "$W2" 'systemctl is-enabled kubelet')" = enabled ]"""), ], }, { "id": 18, "domain": "TSHOOT", "title": "Broken deployment and service", "task": """ The `payments` application in namespace `troubleshoot` is down. Users report that the Service `payments` returns no backends and the pods are not healthy. Fix everything necessary so that: - the `payments` Deployment has all replicas Available (the app is a plain nginx web server), and - the `payments` Service has endpoints. Do not delete the Deployment or the Service; fix them in place. """, "setup": NS + r""" mkns troubleshoot $K apply -f - >/dev/null <<'EOF' apiVersion: apps/v1 kind: Deployment metadata: name: payments namespace: troubleshoot spec: replicas: 2 selector: matchLabels: {app: payments} template: metadata: labels: {app: payments} spec: containers: - name: payments image: nginx:1.99-nope ports: [{containerPort: 80}] --- apiVersion: v1 kind: Service metadata: name: payments namespace: troubleshoot spec: selector: app: payment ports: - port: 80 targetPort: 8080 EOF """, "checks": [ C("all payments replicas available", 3, r"""d="$K -n troubleshoot get deploy payments"; r=$($d -o jsonpath='{.spec.replicas}' 2>/dev/null); a=$($d -o jsonpath='{.status.availableReplicas}'); [ -n "$a" ] && [ "$a" = "$r" ]"""), C("Service payments has endpoints (selector fixed)", 2, r"""[ -n "$($K -n troubleshoot get endpointslices -l kubernetes.io/service-name=payments -o jsonpath='{.items[*].endpoints[*].addresses[*]}' 2>/dev/null)" ]"""), C("Service targetPort matches container port 80", 2, r"""[ "$($K -n troubleshoot get svc payments -o jsonpath='{.spec.ports[0].targetPort}' 2>/dev/null)" = 80 ]"""), ], }, { "id": 19, "domain": "TSHOOT", "title": "CrashLoopBackOff diagnosis", "task": """ Pod `crasher` in namespace `troubleshoot` keeps restarting. 1. Find out why. Save the pod's (current or previous) container logs to `{WS}/q19/crasher.log`. 2. Fix the pod so it stays Running. The application must keep the same name, namespace, image and command; it only needs the configuration it is complaining about (use the value `db.internal` if a value is required). """, "setup": NS + r""" mkns troubleshoot rm -rf "$WS/q19"; mkdir -p "$WS/q19" $K -n troubleshoot delete pod crasher --ignore-not-found --wait=true >/dev/null $K apply -f - >/dev/null </dev/null"""), C("pod crasher Running with DB_HOST set", 3, r"""[ "$($K -n troubleshoot get pod crasher -o jsonpath='{.status.phase}' 2>/dev/null)" = Running ] && [ -n "$($K -n troubleshoot exec crasher -- sh -c 'echo $DB_HOST' 2>/dev/null)" ]"""), C("image unchanged (busybox:1.36)", 1, r"""[ "$($K -n troubleshoot get pod crasher -o jsonpath='{.spec.containers[0].image}' 2>/dev/null)" = busybox:1.36 ]"""), ], }, { "id": 20, "domain": "TSHOOT", "breaks": "scheduler", "title": "Control plane component failure", "disruptive": True, "task": """ New pods in the cluster are stuck in `Pending` and are never assigned to a node. Find the failing control-plane component and repair it on the control-plane node (`{SHELL_CP}`). Afterwards, newly created pods must be scheduled normally. """, "setup": r""" nx "$CP" ' f=/etc/kubernetes/manifests/kube-scheduler.yaml grep -q "^ *- kube-scheduler$" $f && sed -i "s|^\( *\)- kube-scheduler$|\1- kube-schedulerr|" $f ' || true """, "checks": [ C("kube-scheduler static pod manifest repaired", 3, r"""nx "$CP" 'grep -q "^ *- kube-scheduler$" /etc/kubernetes/manifests/kube-scheduler.yaml'"""), C("new pods get scheduled", 5, r""" $K -n default delete pod sched-probe --ignore-not-found --wait=true >/dev/null 2>&1 $K -n default run sched-probe --image=busybox:1.36 --restart=Never -- sleep 300 >/dev/null 2>&1 || exit 1 n=""; for i in $(seq 1 30); do n=$($K -n default get pod sched-probe -o jsonpath='{.spec.nodeName}' 2>/dev/null); [ -n "$n" ] && break; sleep 2; done $K -n default delete pod sched-probe --wait=false >/dev/null 2>&1 [ -n "$n" ]"""), ], }, ] # ---- aggregate the banks: 1-20 here, 21-43 in bank_extra, 101-124 (hard) in bank_hard, # ---- 201-217 / 301-317 / 401-418 / 501-517 in the four fixed simulator sets from tracks.cka import bank_extra, bank_hard, bank_sim_a, bank_sim_b, bank_sim_c, bank_sim_d QUESTIONS = (QUESTIONS + bank_extra.QUESTIONS + bank_hard.QUESTIONS + bank_sim_a.QUESTIONS + bank_sim_b.QUESTIONS + bank_sim_c.QUESTIONS + bank_sim_d.QUESTIONS) QUESTIONS.sort(key=lambda q: q["id"]) _ids = [q["id"] for q in QUESTIONS] assert len(_ids) == len(set(_ids)), "duplicate question ids" from tracks.cka.solutions import S as _SOL for _q in QUESTIONS: _q["solution"] = (_q.get("solution") or _SOL.get(_q["id"], "")).strip() _q.setdefault("set", None) _missing = [q["id"] for q in QUESTIONS if not q["solution"]] assert not _missing, f"tasks without a solution: {_missing}"