From 124e5276d4c615336fd51a4aad2453c2e9e32e16 Mon Sep 17 00:00:00 2001 From: Abhinav Sinha Date: Mon, 15 Jun 2026 07:52:47 +0530 Subject: [PATCH] refactor(scenarios): split monolithic configuration into individual files - Split `scenarios/scenarios.json` into individual JSON files under `scenarios/data/` named `.json` - Split `scenarios/bundles.json` into individual JSON files under `scenarios/bundles/` named `.json` - Updated `backend/server.js` to dynamically load scenario and bundle files from their respective directories - Updated documentation in `scenarios/SCHEMA.md` and `README.md` to reflect the new repository layout and contributor workflow Signed-off-by: Abhinav Sinha --- README.md | 51 +- backend/server.js | 14 +- scenarios/SCHEMA.md | 16 +- scenarios/bundles.json | 156 - scenarios/bundles/k8s-admin.json | 41 + scenarios/bundles/k8s-appdev.json | 36 + scenarios/bundles/k8s-basics.json | 41 + scenarios/bundles/k8s-security.json | 36 + scenarios/data/annotate-resource-basics.json | 57 + scenarios/data/broken-deployment.json | 57 + scenarios/data/cks-apparmor-profile.json | 33 + scenarios/data/cks-audit-policy.json | 61 + scenarios/data/cks-automount-token.json | 33 + scenarios/data/cks-drop-capabilities.json | 45 + scenarios/data/cks-egress-namespace.json | 58 + scenarios/data/cks-image-pull-secret.json | 47 + scenarios/data/cks-immutable-secret.json | 33 + scenarios/data/cks-mcq-api-server.json | 33 + scenarios/data/cks-mcq-audit-policy.json | 39 + scenarios/data/cks-mcq-image-footprint.json | 33 + scenarios/data/cks-mcq-kubelet-auth.json | 33 + scenarios/data/cks-mcq-psp-replacement.json | 33 + scenarios/data/cks-mcq-runtime-security.json | 33 + scenarios/data/cks-mcq-sandboxing.json | 39 + .../data/cks-network-policy-metadata.json | 45 + scenarios/data/cks-network-policy.json | 39 + scenarios/data/cks-non-root-enforce.json | 57 + scenarios/data/cks-pod-security-context.json | 39 + scenarios/data/cks-psa-namespace.json | 37 + scenarios/data/cks-rbac-clusterrole.json | 49 + scenarios/data/cks-rbac-least-privilege.json | 51 + scenarios/data/cks-readonly-filesystem.json | 45 + scenarios/data/cks-runtime-class.json | 47 + scenarios/data/cks-seccomp-profile.json | 33 + scenarios/data/cks-tls-ingress.json | 78 + scenarios/data/cluster-upgrade-mcq.json | 39 + scenarios/data/configmap-volume.json | 65 + scenarios/data/container-logging-mcq.json | 39 + scenarios/data/crashloop-fix.json | 45 + scenarios/data/cronjob-task.json | 51 + scenarios/data/daemonset-mcq.json | 39 + scenarios/data/delete-by-label-basics.json | 58 + scenarios/data/deploy-nginx.json | 71 + scenarios/data/deployment-rollback.json | 43 + scenarios/data/dns-resolution-mcq.json | 39 + scenarios/data/dry-run-manifest-basics.json | 39 + scenarios/data/edit-deployment-basics.json | 40 + scenarios/data/emptydir-pod.json | 51 + scenarios/data/endpoint-fix-task.json | 59 + scenarios/data/env-vars-configmap.json | 59 + scenarios/data/etcd-backup-mcq.json | 39 + scenarios/data/exec-command-basics.json | 40 + scenarios/data/expose-service-basics.json | 52 + scenarios/data/extract-logs-basics.json | 42 + scenarios/data/ingress-task.json | 58 + .../data/init-container-task-basics.json | 45 + scenarios/data/init-containers-mcq.json | 39 + scenarios/data/job-task.json | 45 + scenarios/data/jobs-cronjobs-mcq.json | 39 + scenarios/data/jsonpath-basics.json | 37 + scenarios/data/kubectl-cp-basics.json | 49 + scenarios/data/kubectl-essentials-mcq.json | 39 + scenarios/data/kubectl-output-format-mcq.json | 39 + scenarios/data/labels-selectors-mcq.json | 39 + scenarios/data/limitrange-task.json | 49 + scenarios/data/liveness-probe-task.json | 51 + .../data/multi-container-pod-basics.json | 45 + scenarios/data/namespaces-basics.json | 56 + scenarios/data/network-policy.json | 75 + scenarios/data/networkpolicy-egress.json | 51 + scenarios/data/node-label-selector.json | 53 + scenarios/data/node-taint-toleration.json | 52 + scenarios/data/nodeport-task.json | 52 + scenarios/data/pod-affinity-mcq.json | 39 + scenarios/data/pod-basics-mcq.json | 44 + .../data/pod-disruption-budget-task.json | 66 + scenarios/data/pod-labeling-basics.json | 37 + scenarios/data/pod-security-context.json | 51 + scenarios/data/priority-class-task.json | 65 + scenarios/data/pv-pvc-mount.json | 59 + scenarios/data/pvc-dynamic-task.json | 59 + scenarios/data/rbac-clusterrole.json | 59 + scenarios/data/rbac-role.json | 79 + scenarios/data/readiness-probe-task.json | 45 + scenarios/data/resource-limits-task.json | 57 + scenarios/data/resource-quota-ns.json | 45 + scenarios/data/restart-policy-mcq.json | 39 + scenarios/data/rolling-update-task.json | 51 + scenarios/data/scale-deployment.json | 51 + scenarios/data/secrets-basics.json | 53 + scenarios/data/serviceaccount-pod.json | 53 + scenarios/data/services-mcq.json | 39 + scenarios/data/storageclass-mcq.json | 39 + scenarios/scenarios.json | 4042 ----------------- 94 files changed, 4256 insertions(+), 4217 deletions(-) delete mode 100644 scenarios/bundles.json create mode 100644 scenarios/bundles/k8s-admin.json create mode 100644 scenarios/bundles/k8s-appdev.json create mode 100644 scenarios/bundles/k8s-basics.json create mode 100644 scenarios/bundles/k8s-security.json create mode 100644 scenarios/data/annotate-resource-basics.json create mode 100644 scenarios/data/broken-deployment.json create mode 100644 scenarios/data/cks-apparmor-profile.json create mode 100644 scenarios/data/cks-audit-policy.json create mode 100644 scenarios/data/cks-automount-token.json create mode 100644 scenarios/data/cks-drop-capabilities.json create mode 100644 scenarios/data/cks-egress-namespace.json create mode 100644 scenarios/data/cks-image-pull-secret.json create mode 100644 scenarios/data/cks-immutable-secret.json create mode 100644 scenarios/data/cks-mcq-api-server.json create mode 100644 scenarios/data/cks-mcq-audit-policy.json create mode 100644 scenarios/data/cks-mcq-image-footprint.json create mode 100644 scenarios/data/cks-mcq-kubelet-auth.json create mode 100644 scenarios/data/cks-mcq-psp-replacement.json create mode 100644 scenarios/data/cks-mcq-runtime-security.json create mode 100644 scenarios/data/cks-mcq-sandboxing.json create mode 100644 scenarios/data/cks-network-policy-metadata.json create mode 100644 scenarios/data/cks-network-policy.json create mode 100644 scenarios/data/cks-non-root-enforce.json create mode 100644 scenarios/data/cks-pod-security-context.json create mode 100644 scenarios/data/cks-psa-namespace.json create mode 100644 scenarios/data/cks-rbac-clusterrole.json create mode 100644 scenarios/data/cks-rbac-least-privilege.json create mode 100644 scenarios/data/cks-readonly-filesystem.json create mode 100644 scenarios/data/cks-runtime-class.json create mode 100644 scenarios/data/cks-seccomp-profile.json create mode 100644 scenarios/data/cks-tls-ingress.json create mode 100644 scenarios/data/cluster-upgrade-mcq.json create mode 100644 scenarios/data/configmap-volume.json create mode 100644 scenarios/data/container-logging-mcq.json create mode 100644 scenarios/data/crashloop-fix.json create mode 100644 scenarios/data/cronjob-task.json create mode 100644 scenarios/data/daemonset-mcq.json create mode 100644 scenarios/data/delete-by-label-basics.json create mode 100644 scenarios/data/deploy-nginx.json create mode 100644 scenarios/data/deployment-rollback.json create mode 100644 scenarios/data/dns-resolution-mcq.json create mode 100644 scenarios/data/dry-run-manifest-basics.json create mode 100644 scenarios/data/edit-deployment-basics.json create mode 100644 scenarios/data/emptydir-pod.json create mode 100644 scenarios/data/endpoint-fix-task.json create mode 100644 scenarios/data/env-vars-configmap.json create mode 100644 scenarios/data/etcd-backup-mcq.json create mode 100644 scenarios/data/exec-command-basics.json create mode 100644 scenarios/data/expose-service-basics.json create mode 100644 scenarios/data/extract-logs-basics.json create mode 100644 scenarios/data/ingress-task.json create mode 100644 scenarios/data/init-container-task-basics.json create mode 100644 scenarios/data/init-containers-mcq.json create mode 100644 scenarios/data/job-task.json create mode 100644 scenarios/data/jobs-cronjobs-mcq.json create mode 100644 scenarios/data/jsonpath-basics.json create mode 100644 scenarios/data/kubectl-cp-basics.json create mode 100644 scenarios/data/kubectl-essentials-mcq.json create mode 100644 scenarios/data/kubectl-output-format-mcq.json create mode 100644 scenarios/data/labels-selectors-mcq.json create mode 100644 scenarios/data/limitrange-task.json create mode 100644 scenarios/data/liveness-probe-task.json create mode 100644 scenarios/data/multi-container-pod-basics.json create mode 100644 scenarios/data/namespaces-basics.json create mode 100644 scenarios/data/network-policy.json create mode 100644 scenarios/data/networkpolicy-egress.json create mode 100644 scenarios/data/node-label-selector.json create mode 100644 scenarios/data/node-taint-toleration.json create mode 100644 scenarios/data/nodeport-task.json create mode 100644 scenarios/data/pod-affinity-mcq.json create mode 100644 scenarios/data/pod-basics-mcq.json create mode 100644 scenarios/data/pod-disruption-budget-task.json create mode 100644 scenarios/data/pod-labeling-basics.json create mode 100644 scenarios/data/pod-security-context.json create mode 100644 scenarios/data/priority-class-task.json create mode 100644 scenarios/data/pv-pvc-mount.json create mode 100644 scenarios/data/pvc-dynamic-task.json create mode 100644 scenarios/data/rbac-clusterrole.json create mode 100644 scenarios/data/rbac-role.json create mode 100644 scenarios/data/readiness-probe-task.json create mode 100644 scenarios/data/resource-limits-task.json create mode 100644 scenarios/data/resource-quota-ns.json create mode 100644 scenarios/data/restart-policy-mcq.json create mode 100644 scenarios/data/rolling-update-task.json create mode 100644 scenarios/data/scale-deployment.json create mode 100644 scenarios/data/secrets-basics.json create mode 100644 scenarios/data/serviceaccount-pod.json create mode 100644 scenarios/data/services-mcq.json create mode 100644 scenarios/data/storageclass-mcq.json delete mode 100644 scenarios/scenarios.json diff --git a/README.md b/README.md index ce60b78..b1d217e 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ docker run -itd --name kubekosh --privileged -p 7554:80 \ -v :/data zeborg/kubekosh:latest ``` -Progress is stored in SQLite at `/data/progress.db` inside the container. You may mount your own custom directory to `/data` to persist the progress across container restarts. +Progress is stored in SQLite at `/data/progress.db` inside the container. Mount a local directory to `/data` to keep progress across container restarts. ### Build From Source @@ -78,13 +78,22 @@ The terminal comes pre-configured with: | Alias | Expands to | |---|---| | `k` | `kubectl` | +| `kg` | `kubectl get` | +| `kd` | `kubectl describe` | +| `krm` | `kubectl delete` | | `kgp` | `kubectl get pods` | | `kga` | `kubectl get pods --all-namespaces` | | `kgd` | `kubectl get deployments` | | `kgs` | `kubectl get services` | +| `kgn` | `kubectl get nodes` | +| `kgns` | `kubectl get namespaces` | +| `kdp` | `kubectl describe pod` | | `kaf` | `kubectl apply -f` | +| `kdf` | `kubectl delete -f` | | `kex` | `kubectl exec -it` | +| `klogs` | `kubectl logs` | | `kns ` | `kubectl config set-context --current --namespace=` | +| `kctx ` | `kubectl config use-context ` | --- @@ -102,13 +111,34 @@ Everything runs inside a **single Docker image** managed by `scripts/entrypoint. --- +## Repository Layout + +``` +scenarios/ +├── data/ # One JSON file per scenario → .json +├── bundles/ # One JSON file per bundle → .json +└── SCHEMA.md # Full schema reference + +backend/ +└── server.js # Express API + WebSocket PTY + +frontend/ +└── src/ # React + Vite SPA + +scripts/ +├── entrypoint.sh # Container startup (k3s → API → nginx) +└── nginx.conf # Reverse-proxy config +``` + +--- + ## Contributing Contributions are what make open-source projects like this one grow — and every contribution counts, big or small. Whether you're fixing a typo, polishing a scenario description, or building a completely new exercise from scratch, you're helping the next person learn Kubernetes in the best way possible. **Thank you for taking the time!** ### Adding Scenarios -Scenarios live in `scenarios/scenarios.json`; bundles in `scenarios/bundles.json`. See [`scenarios/SCHEMA.md`](scenarios/SCHEMA.md) for the full schema. +Each scenario is a single JSON file in `scenarios/data` directory; each bundle is a single JSON file in `scenarios/bundles` directory. See [`scenarios/SCHEMA.md`](scenarios/SCHEMA.md) for the full schema. **Task checklist:** - `validation.commands` — idempotent `kubectl` commands only @@ -128,15 +158,20 @@ cd kubekosh # 2. Create a branch git checkout -b feat/my-scenario -# 3. Edit scenarios/scenarios.json (and/or bundles.json) +# 3. Add a new scenario file (copy an existing scenario as a template or create a new one) +cp scenarios/data/deploy-nginx.json scenarios/data/my-new-scenario.json +vim scenarios/data/my-new-scenario.json # edit the new scenario as per [SCHEMA.md](scenarios/SCHEMA.md) -# 4. Build and test locally +# 4. Add the scenario ID to the relevant bundle +vim scenarios/bundles/k8s-basics.json # edit the bundle to include the new scenario ID + +# 5. Build and test locally docker build -t kubekosh . && docker run --rm -itd --privileged -p 7554:80 kubekosh -# 5. Commit and push to your fork -git add scenarios/scenarios.json -git commit -m "feat: add scenario" -git push -u origin feat/my-scenario +# 6. Commit and push to your fork (example for adding `my-new-scenario` to `k8s-basics` bundle) +git add scenarios/data/my-new-scenario.json scenarios/bundles/k8s-basics.json +git commit -m "feat: add my-new-scenario to k8s-basics bundle" +git push -u origin feat/my-new-scenario ``` Open a Pull Request from your fork's branch against `main`. diff --git a/backend/server.js b/backend/server.js index a644211..5775ce9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,8 +10,8 @@ const PORT = 4000; app.use(express.json()); app.use(express.static(path.join(__dirname, '../frontend/dist'))); -const SCENARIOS_FILE = path.join(__dirname, '../scenarios/scenarios.json'); -const BUNDLES_FILE = path.join(__dirname, '../scenarios/bundles.json'); +const SCENARIOS_DIR = path.join(__dirname, '../scenarios/data'); +const BUNDLES_DIR = path.join(__dirname, '../scenarios/bundles'); const DB_FILE = process.env.PROGRESS_DB || '/data/progress.db'; // ── SQLite progress store ───────────────────────────────────────────────────── @@ -82,12 +82,18 @@ function saveProgress(progress) { } +function loadJsonDir(dir) { + return fs.readdirSync(dir) + .filter(f => f.endsWith('.json')) + .map(f => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))); +} + function loadScenarios() { - return JSON.parse(fs.readFileSync(SCENARIOS_FILE, 'utf8')); + return loadJsonDir(SCENARIOS_DIR); } function loadBundles() { - return JSON.parse(fs.readFileSync(BUNDLES_FILE, 'utf8')); + return loadJsonDir(BUNDLES_DIR); } function runCommand(cmd, timeoutMs = 15000) { diff --git a/scenarios/SCHEMA.md b/scenarios/SCHEMA.md index 0cbea87..87cb483 100644 --- a/scenarios/SCHEMA.md +++ b/scenarios/SCHEMA.md @@ -1,14 +1,14 @@ # KubeKosh Configuration Schema Reference -KubeKosh uses two primary JSON files to define its curriculum, learning paths, and exam configurations: -1. **Bundles (`scenarios/bundles.json`)**: Defines the high-level study bundles (e.g., CKA, CKAD, CKS), active highlights, durations for exams, and lists of included scenarios. -2. **Scenarios (`scenarios/scenarios.json`)**: Defines individual exercises, hands-on tasks, multiple-choice questions (MCQs), environment preparations, and automated validation scripts. +KubeKosh uses two primary directories to define its curriculum, learning paths, and exam configurations: +1. **Bundles (`scenarios/bundles`)**: Each file defines one high-level study bundle (e.g., CKA, CKAD, CKS), including active highlights, exam duration, ordered list of included scenarios, and other fields. +2. **Scenarios (`scenarios/data`)**: Each file defines one individual exercise — a hands-on task or multiple-choice question (MCQ). Each scenario contains an environment setup script, cleanup script, hints, problem statement, automated validation script, and other fields. --- -## 1. Bundles Schema (`scenarios/bundles.json`) +## 1. Bundles Schema (`scenarios/bundles`) -Bundles are defined as a JSON array of objects. Each bundle organizes a learning track or mock exam. +Each file contains a **single bundle object**. The filename must match the bundle's `id` field (e.g., `k8s-basics.json`). ### Schema Fields * **`id`** *(string, required)*: A unique, kebab-case identifier for the bundle (e.g., `k8s-basics`). @@ -41,9 +41,11 @@ Bundles are defined as a JSON array of objects. Each bundle organizes a learning --- -## 2. Scenarios Schema (`scenarios/scenarios.json`) +## 2. Scenarios Schema (`scenarios/data`) -Scenarios are defined as a JSON array of objects. A scenario can be either a hands-on console challenge (`"task"`) or a multiple-choice question (`"mcq"`). +Each file contains a **single scenario object**. The filename must match the scenario's `id` field (e.g., `k8s-basics.json`). + +Scenarios are defined as JSON objects. A scenario can be either a hands-on console challenge (`"task"`) or a multiple-choice question (`"mcq"`). ### Common Fields (All Types) ```jsonc diff --git a/scenarios/bundles.json b/scenarios/bundles.json deleted file mode 100644 index 079fd2f..0000000 --- a/scenarios/bundles.json +++ /dev/null @@ -1,156 +0,0 @@ -[ - { - "id": "k8s-basics", - "name": "Kubernetes Basics", - "icon": "🌱", - "tagline": "Core concepts for beginners", - "color": "#3fb950", - "colorDim": "rgba(63,185,80,0.12)", - "exam_minutes": 60, - "scenario_ids": [ - "pod-basics-mcq", - "kubectl-essentials-mcq", - "labels-selectors-mcq", - "init-containers-mcq", - "namespaces-basics", - "deploy-nginx", - "scale-deployment", - "rolling-update-task", - "liveness-probe-task", - "jobs-cronjobs-mcq", - "daemonset-mcq", - "configmap-volume", - "resource-limits-task", - "env-vars-configmap", - "secrets-basics", - "services-mcq", - "multi-container-pod-basics", - "expose-service-basics", - "pod-labeling-basics", - "extract-logs-basics", - "edit-deployment-basics", - "jsonpath-basics", - "dry-run-manifest-basics", - "delete-by-label-basics", - "kubectl-cp-basics", - "exec-command-basics", - "annotate-resource-basics", - "init-container-task-basics", - "restart-policy-mcq", - "kubectl-output-format-mcq" - ] - }, - { - "id": "k8s-admin", - "name": "Kubernetes Administrator", - "icon": "🧑‍✈️", - "tagline": "CKA exam — cluster administration", - "color": "#58a6ff", - "colorDim": "rgba(88,166,255,0.12)", - "exam_minutes": 120, - "scenario_ids": [ - "rbac-role", - "rbac-clusterrole", - "serviceaccount-pod", - "node-label-selector", - "node-taint-toleration", - "resource-quota-ns", - "limitrange-task", - "etcd-backup-mcq", - "cluster-upgrade-mcq", - "deployment-rollback", - "cronjob-task", - "job-task", - "readiness-probe-task", - "pod-affinity-mcq", - "nodeport-task", - "ingress-task", - "dns-resolution-mcq", - "endpoint-fix-task", - "networkpolicy-egress", - "network-policy", - "storageclass-mcq", - "pvc-dynamic-task", - "emptydir-pod", - "pv-pvc-mount", - "pod-security-context", - "broken-deployment", - "crashloop-fix", - "container-logging-mcq", - "pod-disruption-budget-task", - "priority-class-task" - ] - }, - { - "id": "k8s-appdev", - "name": "Kubernetes Developer", - "icon": "🛠️", - "tagline": "CKAD exam — application development", - "color": "#bc8cff", - "colorDim": "rgba(188,140,255,0.12)", - "exam_minutes": 120, - "scenario_ids": [ - "pod-basics-mcq", - "kubectl-essentials-mcq", - "namespaces-basics", - "labels-selectors-mcq", - "deploy-nginx", - "scale-deployment", - "rolling-update-task", - "deployment-rollback", - "liveness-probe-task", - "readiness-probe-task", - "configmap-volume", - "env-vars-configmap", - "secrets-basics", - "resource-limits-task", - "cronjob-task", - "job-task", - "emptydir-pod", - "pvc-dynamic-task", - "services-mcq", - "nodeport-task", - "ingress-task", - "rbac-role", - "pod-security-context", - "crashloop-fix", - "container-logging-mcq" - ] - }, - { - "id": "k8s-security", - "name": "Kubernetes Security", - "icon": "🛡", - "tagline": "CKS exam — hardening and threats", - "color": "#f97316", - "colorDim": "rgba(249,115,22,0.12)", - "exam_minutes": 120, - "scenario_ids": [ - "cks-network-policy", - "cks-pod-security-context", - "cks-rbac-least-privilege", - "cks-seccomp-profile", - "cks-immutable-secret", - "cks-apparmor-profile", - "cks-automount-token", - "cks-network-policy-metadata", - "cks-rbac-clusterrole", - "cks-psa-namespace", - "cks-mcq-runtime-security", - "cks-mcq-api-server", - "cks-mcq-image-footprint", - "cks-mcq-psp-replacement", - "cks-mcq-kubelet-auth", - "cks-readonly-filesystem", - "cks-drop-capabilities", - "cks-tls-ingress", - "cks-image-pull-secret", - "cks-mcq-sandboxing", - "cks-mcq-audit-policy", - "cks-runtime-class", - "cks-audit-policy", - "cks-egress-namespace", - "cks-non-root-enforce" - ] - } -] \ No newline at end of file diff --git a/scenarios/bundles/k8s-admin.json b/scenarios/bundles/k8s-admin.json new file mode 100644 index 0000000..a9335f1 --- /dev/null +++ b/scenarios/bundles/k8s-admin.json @@ -0,0 +1,41 @@ +{ + "id": "k8s-admin", + "name": "Kubernetes Administrator", + "icon": "🧑‍✈️", + "tagline": "CKA exam — cluster administration", + "color": "#58a6ff", + "colorDim": "rgba(88,166,255,0.12)", + "exam_minutes": 120, + "scenario_ids": [ + "rbac-role", + "rbac-clusterrole", + "serviceaccount-pod", + "node-label-selector", + "node-taint-toleration", + "resource-quota-ns", + "limitrange-task", + "etcd-backup-mcq", + "cluster-upgrade-mcq", + "deployment-rollback", + "cronjob-task", + "job-task", + "readiness-probe-task", + "pod-affinity-mcq", + "nodeport-task", + "ingress-task", + "dns-resolution-mcq", + "endpoint-fix-task", + "networkpolicy-egress", + "network-policy", + "storageclass-mcq", + "pvc-dynamic-task", + "emptydir-pod", + "pv-pvc-mount", + "pod-security-context", + "broken-deployment", + "crashloop-fix", + "container-logging-mcq", + "pod-disruption-budget-task", + "priority-class-task" + ] +} diff --git a/scenarios/bundles/k8s-appdev.json b/scenarios/bundles/k8s-appdev.json new file mode 100644 index 0000000..f2a0674 --- /dev/null +++ b/scenarios/bundles/k8s-appdev.json @@ -0,0 +1,36 @@ +{ + "id": "k8s-appdev", + "name": "Kubernetes Developer", + "icon": "🛠️", + "tagline": "CKAD exam — application development", + "color": "#bc8cff", + "colorDim": "rgba(188,140,255,0.12)", + "exam_minutes": 120, + "scenario_ids": [ + "pod-basics-mcq", + "kubectl-essentials-mcq", + "namespaces-basics", + "labels-selectors-mcq", + "deploy-nginx", + "scale-deployment", + "rolling-update-task", + "deployment-rollback", + "liveness-probe-task", + "readiness-probe-task", + "configmap-volume", + "env-vars-configmap", + "secrets-basics", + "resource-limits-task", + "cronjob-task", + "job-task", + "emptydir-pod", + "pvc-dynamic-task", + "services-mcq", + "nodeport-task", + "ingress-task", + "rbac-role", + "pod-security-context", + "crashloop-fix", + "container-logging-mcq" + ] +} diff --git a/scenarios/bundles/k8s-basics.json b/scenarios/bundles/k8s-basics.json new file mode 100644 index 0000000..88044bc --- /dev/null +++ b/scenarios/bundles/k8s-basics.json @@ -0,0 +1,41 @@ +{ + "id": "k8s-basics", + "name": "Kubernetes Basics", + "icon": "🌱", + "tagline": "Core concepts for beginners", + "color": "#3fb950", + "colorDim": "rgba(63,185,80,0.12)", + "exam_minutes": 60, + "scenario_ids": [ + "pod-basics-mcq", + "kubectl-essentials-mcq", + "labels-selectors-mcq", + "init-containers-mcq", + "namespaces-basics", + "deploy-nginx", + "scale-deployment", + "rolling-update-task", + "liveness-probe-task", + "jobs-cronjobs-mcq", + "daemonset-mcq", + "configmap-volume", + "resource-limits-task", + "env-vars-configmap", + "secrets-basics", + "services-mcq", + "multi-container-pod-basics", + "expose-service-basics", + "pod-labeling-basics", + "extract-logs-basics", + "edit-deployment-basics", + "jsonpath-basics", + "dry-run-manifest-basics", + "delete-by-label-basics", + "kubectl-cp-basics", + "exec-command-basics", + "annotate-resource-basics", + "init-container-task-basics", + "restart-policy-mcq", + "kubectl-output-format-mcq" + ] +} diff --git a/scenarios/bundles/k8s-security.json b/scenarios/bundles/k8s-security.json new file mode 100644 index 0000000..5ee76b2 --- /dev/null +++ b/scenarios/bundles/k8s-security.json @@ -0,0 +1,36 @@ +{ + "id": "k8s-security", + "name": "Kubernetes Security", + "icon": "🛡", + "tagline": "CKS exam — hardening and threats", + "color": "#f97316", + "colorDim": "rgba(249,115,22,0.12)", + "exam_minutes": 120, + "scenario_ids": [ + "cks-network-policy", + "cks-pod-security-context", + "cks-rbac-least-privilege", + "cks-seccomp-profile", + "cks-immutable-secret", + "cks-apparmor-profile", + "cks-automount-token", + "cks-network-policy-metadata", + "cks-rbac-clusterrole", + "cks-psa-namespace", + "cks-mcq-runtime-security", + "cks-mcq-api-server", + "cks-mcq-image-footprint", + "cks-mcq-psp-replacement", + "cks-mcq-kubelet-auth", + "cks-readonly-filesystem", + "cks-drop-capabilities", + "cks-tls-ingress", + "cks-image-pull-secret", + "cks-mcq-sandboxing", + "cks-mcq-audit-policy", + "cks-runtime-class", + "cks-audit-policy", + "cks-egress-namespace", + "cks-non-root-enforce" + ] +} diff --git a/scenarios/data/annotate-resource-basics.json b/scenarios/data/annotate-resource-basics.json new file mode 100644 index 0000000..635dd28 --- /dev/null +++ b/scenarios/data/annotate-resource-basics.json @@ -0,0 +1,57 @@ +{ + "id": "annotate-resource-basics", + "title": "Annotating Kubernetes Resources", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "task", + "weight": 3, + "description": "## Annotating Kubernetes Resources\n\nAnnotations attach arbitrary non-identifying metadata to Kubernetes objects — such as contact info, tool versions, or documentation URLs. Unlike labels, annotations **cannot** be used as selectors.\n\n**Your task:**\n\nA Deployment named `myapp` is running in the `default` namespace. Add the following annotations to it:\n- `contact=ops@company.com`\n- `reviewed=true`\n\n```bash\n# Verify:\nkubectl get deployment myapp -o jsonpath='{.metadata.annotations}'\n```", + "hints": [ + { + "title": "kubectl annotate", + "body": "Use `kubectl annotate` to imperatively add or overwrite annotations on any resource.", + "command": "kubectl annotate deployment myapp contact=ops@company.com reviewed=true" + }, + { + "title": "Overwrite an existing annotation", + "body": "If the annotation key already exists, pass `--overwrite` to update it.", + "command": "kubectl annotate deployment myapp contact=ops@company.com reviewed=true --overwrite" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment myapp --image=nginx:alpine --replicas=1 2>/dev/null || true" + }, + { + "command": "kubectl rollout status deployment/myapp --timeout=60s" + } + ], + "validation": { + "commands": [ + { + "description": "Deployment 'myapp' exists", + "command": "kubectl get deployment myapp -o jsonpath='{.metadata.name}'", + "expected_output": "myapp", + "match": "exact" + }, + { + "description": "Annotation 'contact' is ops@company.com", + "command": "kubectl get deployment myapp -o jsonpath='{.metadata.annotations.contact}'", + "expected_output": "ops@company.com", + "match": "exact" + }, + { + "description": "Annotation 'reviewed' is true", + "command": "kubectl get deployment myapp -o jsonpath='{.metadata.annotations.reviewed}'", + "expected_output": "true", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment myapp --ignore-not-found" + } + ] +} diff --git a/scenarios/data/broken-deployment.json b/scenarios/data/broken-deployment.json new file mode 100644 index 0000000..9ca81d5 --- /dev/null +++ b/scenarios/data/broken-deployment.json @@ -0,0 +1,57 @@ +{ + "id": "broken-deployment", + "title": "Debug a Failing Deployment", + "category": "Troubleshooting", + "difficulty": "Medium", + "type": "task", + "weight": 8, + "description": "## Fix the Broken Deployment\n\nA deployment has been pre-created in the `debug` namespace but its pods are not running.\n\n**Your task:** Identify the problem and fix it so that the deployment has **all 3 pods in Ready state**.\n\n> Start by describing the deployment and its pods to find clues about what's wrong.", + "hints": [ + { + "title": "Inspect the deployment", + "body": "Start by listing pods in the debug namespace and describing the failing ones.", + "command": "kubectl get pods -n debug\nkubectl describe pod -n debug -l app=broken-app" + }, + { + "title": "Check events", + "body": "The Events section in `kubectl describe` usually tells you exactly what's wrong — look for ImagePullBackOff, OOMKilled, or probe failures.", + "command": "kubectl describe deployment broken-app -n debug" + }, + { + "title": "Fix the image tag", + "body": "If the image tag doesn't exist, update it to a valid one using `kubectl set image`.", + "command": "kubectl set image deployment/broken-app app=nginx:1.25 -n debug" + } + ], + "setup_commands": [ + { + "command": "kubectl create namespace debug" + }, + { + "command": "kubectl create deployment broken-app --image=nginx:invalid-tag-99999 --replicas=3 -n debug" + } + ], + "validation": { + "description": "Checks that the broken-app deployment in the debug namespace has 3 ready replicas.", + "commands": [ + { + "description": "Deployment 'broken-app' has 3 ready replicas", + "command": "kubectl get deployment broken-app -n debug -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", + "expected_output": "3", + "match": "exact" + }, + { + "description": "No pods in ImagePullBackOff state", + "command": "kubectl get pods -n debug --no-headers 2>/dev/null | awk '{print $3}' | grep -c 'ImagePullBackOff\\|ErrImagePull' || true", + "expected_output": "0", + "match": "exact" + } + ] + }, + "default_namespace": "debug", + "teardown_commands": [ + { + "command": "kubectl delete namespace debug --ignore-not-found --wait=false" + } + ] +} diff --git a/scenarios/data/cks-apparmor-profile.json b/scenarios/data/cks-apparmor-profile.json new file mode 100644 index 0000000..9a19007 --- /dev/null +++ b/scenarios/data/cks-apparmor-profile.json @@ -0,0 +1,33 @@ +{ + "id": "cks-apparmor-profile", + "title": "AppArmor Profiles", + "category": "Workload Security", + "difficulty": "Hard", + "type": "task", + "weight": 5, + "description": "## AppArmor Profiles\n\nAppArmor is a Linux kernel security module that restricts programs' capabilities. In Kubernetes 1.30+, it is supported natively in the securityContext.\n\n**Your task:**\n\nCreate a Pod named `apparmor-pod` using the `nginx:alpine` image. Configure its Pod-level `securityContext` to use the `RuntimeDefault` AppArmor profile.\n\n```bash\n# Verify the profile is set:\nkubectl get pod apparmor-pod -o yaml | grep appArmor\n```", + "hints": [ + { + "title": "Set AppArmor Profile", + "body": "Add `appArmorProfile: { type: RuntimeDefault }` under `spec.securityContext`.", + "command": "cat < /etc/kubernetes/audit-policy.yaml\napiVersion: audit.k8s.io/v1\nkind: Policy\nrules:\n- level: RequestResponse\n resources:\n - group: \"\"\n resources: [\"secrets\"]\n- level: Metadata\n verbs: [\"get\", \"list\", \"watch\"]\n resources:\n - group: \"\"\n resources: [\"pods\"]\n- level: None\n userGroups: [\"system:masters\"]\n- level: Metadata\nEOF" + } + ], + "setup_commands": [ + { + "command": "mkdir -p /etc/kubernetes" + } + ], + "validation": { + "commands": [ + { + "description": "Audit policy file is valid YAML with kind: Policy", + "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); print(p.get('kind',''))\"", + "expected_output": "Policy", + "match": "exact" + }, + { + "description": "Rule 1: level is RequestResponse and targets secrets", + "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); r=p['rules'][0]; print('ok' if r['level']=='RequestResponse' and any('secrets' in x.get('resources',[]) for x in r.get('resources',[])) else 'fail')\"", + "expected_output": "ok", + "match": "exact" + }, + { + "description": "Rule 2: level is Metadata, targets pods, verbs include get/list/watch", + "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); print('ok' if any(r.get('level')=='Metadata' and any('pods' in x.get('resources',[]) for x in r.get('resources',[])) and set(r.get('verbs',[])) >= {'get','list','watch'} for r in p['rules']) else 'fail')\"", + "expected_output": "ok", + "match": "exact" + }, + { + "description": "Rule 3: level is None for system:masters group", + "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); print('ok' if any(r.get('level')=='None' and 'system:masters' in r.get('userGroups',[]) for r in p['rules']) else 'fail')\"", + "expected_output": "ok", + "match": "exact" + }, + { + "description": "Rule 4: catch-all is level Metadata with no other selectors", + "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); last=p['rules'][-1]; print('ok' if set(last.keys())=={'level'} and last['level']=='Metadata' else 'fail')\"", + "expected_output": "ok", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "rm -f /etc/kubernetes/audit-policy.yaml" + } + ] +} diff --git a/scenarios/data/cks-automount-token.json b/scenarios/data/cks-automount-token.json new file mode 100644 index 0000000..e931758 --- /dev/null +++ b/scenarios/data/cks-automount-token.json @@ -0,0 +1,33 @@ +{ + "id": "cks-automount-token", + "title": "Disable ServiceAccount Token", + "category": "Workload Security", + "difficulty": "Easy", + "type": "task", + "weight": 3, + "description": "## Service Account Tokens\n\nBy default, Kubernetes automatically mounts a ServiceAccount API token into every Pod, which can be a significant security risk if the Pod is compromised.\n\n**Your task:**\n\nCreate a Pod named `no-token-pod` using the `alpine` image (with command `sleep 3600`). Explicitly disable the automatic mounting of the ServiceAccount token for this pod.\n\n```bash\n# Verify token is not mounted:\nkubectl get pod no-token-pod -o yaml | grep automount\n```", + "hints": [ + { + "title": "automountServiceAccountToken", + "body": "Set `automountServiceAccountToken: false` in the pod's spec.", + "command": "cat <`.", + "command": "kubectl explain runtimeclass\nkubectl explain pod.spec.runtimeClassName" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/cks-network-policy-metadata.json b/scenarios/data/cks-network-policy-metadata.json new file mode 100644 index 0000000..c06f472 --- /dev/null +++ b/scenarios/data/cks-network-policy-metadata.json @@ -0,0 +1,45 @@ +{ + "id": "cks-network-policy-metadata", + "title": "Block Cloud Metadata", + "category": "Network Security", + "difficulty": "Hard", + "type": "task", + "weight": 6, + "description": "## Restricting Cloud Metadata Access\n\nCloud providers expose sensitive instance metadata at `169.254.169.254`. An attacker exploiting an SSRF vulnerability can use this to steal cloud credentials.\n\n**Your task:**\n\nCreate a NetworkPolicy named `deny-metadata` in the `default` namespace that applies to all pods. It should explicitly **deny** all Egress traffic to the IP block `169.254.169.254/32`, while **allowing** all other Egress traffic.\n\n```bash\n# Tip: Use an ipBlock exception (except).\n```", + "hints": [ + { + "title": "Except IPBlock", + "body": "Create an Egress rule that allows all traffic (`0.0.0.0/0`) EXCEPT `169.254.169.254/32`.", + "command": "cat </dev/null", + "expected_output": "169.254.169.254/32", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete networkpolicy deny-metadata --ignore-not-found" + } + ] +} diff --git a/scenarios/data/cks-network-policy.json b/scenarios/data/cks-network-policy.json new file mode 100644 index 0000000..6dca53a --- /dev/null +++ b/scenarios/data/cks-network-policy.json @@ -0,0 +1,39 @@ +{ + "id": "cks-network-policy", + "title": "Default Deny Network Policy", + "category": "Network Security", + "difficulty": "Medium", + "type": "task", + "weight": 5, + "description": "## Network Policies\n\nIn a zero-trust architecture, you should deny all traffic by default and explicitly allow what is needed.\n\n**Your task:**\n\nCreate a NetworkPolicy named `default-deny-all` in the `default` namespace that denies all ingress and egress traffic for all pods in the namespace.\n\n```bash\n# Verify your policy:\nkubectl get networkpolicy default-deny-all\n```", + "hints": [ + { + "title": "Default Deny YAML", + "body": "Use a podSelector with an empty matchLabels `{}` to select all pods, and provide empty lists for ingress and egress.", + "command": "cat < **Note:** The pod may not reach `Running` state if `runsc` is not installed on the node — that is expected in this lab. Validation only checks the API object configuration.", + "hints": [ + { + "title": "Create the RuntimeClass", + "body": "Use a manifest with `apiVersion: node.k8s.io/v1`, `kind: RuntimeClass`, and a `handler` field.", + "command": "cat </dev/null" + }, + { + "command": "kubectl create deployment tls-app --image=nginx:alpine --replicas=1 2>/dev/null || true" + }, + { + "command": "kubectl expose deployment tls-app --name=tls-svc --port=80 --target-port=80 2>/dev/null || true" + } + ], + "validation": { + "commands": [ + { + "description": "Secret 'app-tls' is of type kubernetes.io/tls", + "command": "kubectl get secret app-tls -o jsonpath='{.type}'", + "expected_output": "kubernetes.io/tls", + "match": "exact" + }, + { + "description": "Ingress 'tls-ingress' exists", + "command": "kubectl get ingress tls-ingress -o jsonpath='{.metadata.name}'", + "expected_output": "tls-ingress", + "match": "exact" + }, + { + "description": "Ingress uses TLS secret 'app-tls'", + "command": "kubectl get ingress tls-ingress -o jsonpath='{.spec.tls[0].secretName}'", + "expected_output": "app-tls", + "match": "exact" + }, + { + "description": "Ingress routes host secure.lab.local", + "command": "kubectl get ingress tls-ingress -o jsonpath='{.spec.rules[0].host}'", + "expected_output": "secure.lab.local", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete ingress tls-ingress --ignore-not-found" + }, + { + "command": "kubectl delete secret app-tls --ignore-not-found" + }, + { + "command": "kubectl delete svc tls-svc --ignore-not-found" + }, + { + "command": "kubectl delete deployment tls-app --ignore-not-found" + }, + { + "command": "rm -f /tmp/tls.crt /tmp/tls.key" + } + ] +} diff --git a/scenarios/data/cluster-upgrade-mcq.json b/scenarios/data/cluster-upgrade-mcq.json new file mode 100644 index 0000000..d198d0b --- /dev/null +++ b/scenarios/data/cluster-upgrade-mcq.json @@ -0,0 +1,39 @@ +{ + "id": "cluster-upgrade-mcq", + "title": "Cluster Upgrade Order", + "category": "Cluster Administration", + "difficulty": "Hard", + "type": "mcq", + "weight": 5, + "description": "## Cluster Upgrade Order\n\nYou need to upgrade a kubeadm-managed cluster from v1.28 to v1.29. What is the **correct order** of operations?", + "options": [ + { + "id": "a", + "text": "1. Upgrade worker nodes → 2. Upgrade control plane → 3. Upgrade kubeadm → 4. Update kubelet/kubectl" + }, + { + "id": "b", + "text": "1. Upgrade kubeadm on control plane → 2. Run `kubeadm upgrade apply` → 3. Upgrade kubelet/kubectl on control plane → 4. Drain and upgrade worker nodes one by one" + }, + { + "id": "c", + "text": "1. `kubectl drain` all nodes → 2. Upgrade all nodes simultaneously → 3. `kubectl uncordon` all nodes" + }, + { + "id": "d", + "text": "1. Upgrade etcd first → 2. Upgrade API server → 3. Upgrade scheduler and controller-manager → 4. Upgrade kubelet" + } + ], + "correct_option": "b", + "explanation": "The correct kubeadm upgrade order is: **1)** Upgrade `kubeadm` on the control plane node → **2)** Run `kubeadm upgrade apply v1.29.x` (upgrades control-plane components) → **3)** Upgrade `kubelet` and `kubectl` on the control plane, then `systemctl daemon-reload && systemctl restart kubelet` → **4)** For each worker: `kubectl drain`, upgrade `kubeadm`/`kubelet`/`kubectl`, run `kubeadm upgrade node`, restart kubelet, `kubectl uncordon`. Never upgrade workers before the control plane.", + "hints": [ + { + "title": "kubeadm upgrade steps", + "body": "Key commands: `kubeadm upgrade plan` (preview), `kubeadm upgrade apply v1.29.x` (apply), `kubectl drain ` (before worker upgrade), `kubectl uncordon ` (after).", + "command": "kubeadm upgrade plan" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/configmap-volume.json b/scenarios/data/configmap-volume.json new file mode 100644 index 0000000..95d97e1 --- /dev/null +++ b/scenarios/data/configmap-volume.json @@ -0,0 +1,65 @@ +{ + "id": "configmap-volume", + "title": "Mount ConfigMap as Volume", + "category": "Configuration", + "difficulty": "Medium", + "type": "task", + "weight": 7, + "description": "## Inject Configuration via a ConfigMap Volume\n\nAn application reads its configuration from files on disk at `/etc/app-config/`.\n\n**Your tasks:**\n1. Create a **ConfigMap** named `app-config` in the `default` namespace with the following key-value pairs:\n - `environment`: `production`\n - `log_level`: `warn`\n - `max_connections`: `200`\n2. Create a **Pod** named `config-reader` using image `busybox:1.36`\n3. Mount the ConfigMap as a **volume** at `/etc/app-config` inside the container\n4. The pod should run the command: `sleep 3600`", + "hints": [ + { + "title": "Create the ConfigMap", + "body": "Use `--from-literal` for each key-value pair.", + "command": "kubectl create configmap app-config --from-literal=environment=production --from-literal=log_level=warn --from-literal=max_connections=200" + }, + { + "title": "Write the Pod manifest", + "body": "Use a heredoc piped directly to kubectl apply -f - to avoid writing to a temp file.", + "command": "cat < --previous` (or `-p`) retrieves logs from the **terminated previous container** instance in a pod. This is critical for debugging CrashLoopBackOff scenarios where the container restarts before you can inspect its logs. `--all-containers` shows logs from all containers in the current run. `kubectl describe` shows events and status but not stdout/stderr logs.", + "hints": [ + { + "title": "Useful kubectl logs flags", + "body": "`--previous`/`-p`: previous container | `--follow`/`-f`: stream | `--since=1h`: last hour | `--tail=50`: last 50 lines | `--timestamps`: show timestamps", + "command": "kubectl logs worker-abc --previous --tail=50" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/crashloop-fix.json b/scenarios/data/crashloop-fix.json new file mode 100644 index 0000000..fdd82c2 --- /dev/null +++ b/scenarios/data/crashloop-fix.json @@ -0,0 +1,45 @@ +{ + "id": "crashloop-fix", + "title": "Fix a CrashLoopBackOff Pod", + "category": "Troubleshooting", + "difficulty": "Medium", + "type": "task", + "weight": 7, + "description": "## Fix a CrashLoopBackOff\n\nA deployment `crash-app` is in `CrashLoopBackOff` because it was deployed with a broken command.\n\n**Your task:**\n\nFix the `crash-app` deployment so all pods run successfully. The container should run `nginx -g 'daemon off;'` (the default nginx command). Update the deployment to remove the broken command override.\n\n```bash\n# Check what's wrong:\nkubectl describe deployment crash-app\nkubectl logs -l app=crash-app\n```", + "hints": [ + { + "title": "Remove the broken command", + "body": "Use `kubectl patch` or `kubectl edit deployment crash-app`. Remove the `command` override so nginx uses its default entrypoint.", + "command": "kubectl patch deployment crash-app --type=json -p='[{\"op\":\"remove\",\"path\":\"/spec/template/spec/containers/0/command\"}]'" + }, + { + "title": "Alternative: kubectl set", + "body": "You can also edit the deployment directly and delete the command field.", + "command": "kubectl edit deployment crash-app" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment crash-app --image=nginx:alpine --replicas=1 2>/dev/null || true" + }, + { + "command": "kubectl patch deployment crash-app --type=json -p='[{\"op\":\"add\",\"path\":\"/spec/template/spec/containers/0/command\",\"value\":[\"/bin/sh\",\"-c\",\"exit 1\"]}]'" + } + ], + "validation": { + "commands": [ + { + "description": "Deployment 'crash-app' has 1 ready replica", + "command": "kubectl get deployment crash-app -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", + "expected_output": "1", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment crash-app --ignore-not-found" + } + ] +} diff --git a/scenarios/data/cronjob-task.json b/scenarios/data/cronjob-task.json new file mode 100644 index 0000000..64bc92e --- /dev/null +++ b/scenarios/data/cronjob-task.json @@ -0,0 +1,51 @@ +{ + "id": "cronjob-task", + "title": "Create a CronJob", + "category": "Workloads & Scheduling", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Create a CronJob\n\nCronJobs create Jobs on a repeating schedule, perfect for maintenance tasks like log rotation, backups, or report generation.\n\n**Your task:**\n\nCreate a CronJob named `date-printer` that:\n- Runs **every minute** (`* * * * *`)\n- Uses `busybox:1.36`\n- Executes: `date`\n- Has `successfulJobsHistoryLimit: 3`", + "hints": [ + { + "title": "CronJob manifest", + "body": "Use `kubectl create cronjob` and then patch with `successfulJobsHistoryLimit: 3`, or create a cronjob with a manifest. The schedule uses standard cron syntax.", + "command": "cat </dev/null | wc -l | tr -d ' '", + "expected_output": "0", + "match": "exact" + }, + { + "description": "Prod pods are still running", + "command": "kubectl get pods -l env=prod --no-headers 2>/dev/null | wc -l | tr -d ' '", + "expected_output": "2", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pods -l env=dev --ignore-not-found --grace-period=0 --force" + }, + { + "command": "kubectl delete pods -l env=prod --ignore-not-found --grace-period=0 --force" + } + ] +} diff --git a/scenarios/data/deploy-nginx.json b/scenarios/data/deploy-nginx.json new file mode 100644 index 0000000..3afe14b --- /dev/null +++ b/scenarios/data/deploy-nginx.json @@ -0,0 +1,71 @@ +{ + "id": "deploy-nginx", + "title": "Deploy and Expose Nginx", + "category": "Workloads", + "difficulty": "Easy", + "type": "task", + "weight": 5, + "description": "## Deploy and Expose an Nginx Application\n\nThe platform team needs a simple web server running in the cluster.\n\n**Your tasks:**\n1. Create a **Deployment** named `webserver` in the `default` namespace\n2. Use the image `nginx:1.25`\n3. Set replica count to **2**\n4. Expose it via a **ClusterIP Service** named `webserver-svc` on port **80**\n\n> 💡 Tip: You can use `kubectl create` for both resources imperatively, or write YAML manifests.", + "hints": [ + { + "title": "Create the Deployment", + "body": "Use `kubectl create deployment` with the `--image` and `--replicas` flags.", + "command": "kubectl create deployment webserver --image=nginx:1.25 --replicas=2" + }, + { + "title": "Expose the Deployment", + "body": "Use `kubectl expose` to create a ClusterIP service targeting port 80.", + "command": "kubectl expose deployment webserver --name=webserver-svc --port=80 --target-port=80" + }, + { + "title": "Verify", + "body": "Check that both resources are up and the service endpoints are populated.", + "command": "kubectl get deployment webserver && kubectl get svc webserver-svc && kubectl get endpoints webserver-svc" + } + ], + "setup_commands": [], + "validation": { + "description": "Checks that the deployment exists with 2 replicas, uses the correct image, and the service exists targeting port 80.", + "commands": [ + { + "description": "Deployment 'webserver' exists", + "command": "kubectl get deployment webserver -o jsonpath='{.metadata.name}'", + "expected_output": "webserver", + "match": "exact" + }, + { + "description": "Deployment has 2 ready replicas", + "command": "kubectl get deployment webserver -o jsonpath='{.status.readyReplicas}'", + "expected_output": "2", + "match": "exact" + }, + { + "description": "Deployment uses nginx:1.25 image", + "command": "kubectl get deployment webserver -o jsonpath='{.spec.template.spec.containers[0].image}'", + "expected_output": "nginx:1.25", + "match": "exact" + }, + { + "description": "Service 'webserver-svc' exists", + "command": "kubectl get svc webserver-svc -o jsonpath='{.metadata.name}'", + "expected_output": "webserver-svc", + "match": "exact" + }, + { + "description": "Service exposes port 80", + "command": "kubectl get svc webserver-svc -o jsonpath='{.spec.ports[0].port}'", + "expected_output": "80", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment webserver --ignore-not-found" + }, + { + "command": "kubectl delete svc webserver-svc --ignore-not-found" + } + ] +} diff --git a/scenarios/data/deployment-rollback.json b/scenarios/data/deployment-rollback.json new file mode 100644 index 0000000..3309e0e --- /dev/null +++ b/scenarios/data/deployment-rollback.json @@ -0,0 +1,43 @@ +{ + "id": "deployment-rollback", + "title": "Rolling Back a Deployment", + "category": "Workloads & Scheduling", + "difficulty": "Easy", + "type": "task", + "weight": 5, + "description": "## Rolling Back a Deployment\n\nA deployment `api-server` was mistakenly updated to a broken image. Your task is to roll it back to the previous working revision.\n\n**Your task:**\n\nRoll back the `api-server` deployment to its previous revision.\n\n```bash\n# Check rollout history\nkubectl rollout history deployment/api-server\n# Rollback\nkubectl rollout undo deployment/api-server\n```", + "hints": [ + { + "title": "Undo a rollout", + "body": "`kubectl rollout undo deployment/` reverts to the previous revision. Use `--to-revision=N` to go to a specific revision number.", + "command": "kubectl rollout undo deployment/api-server" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment api-server --image=nginx:1.24 --replicas=2 2>/dev/null || true" + }, + { + "command": "kubectl rollout status deployment/api-server --timeout=60s" + }, + { + "command": "kubectl set image deployment/api-server nginx=nginx:broken-image" + } + ], + "validation": { + "commands": [ + { + "description": "Deployment 'api-server' image rolled back to nginx:1.24", + "command": "kubectl get deployment api-server -o jsonpath='{.spec.template.spec.containers[0].image}'", + "expected_output": "nginx:1.24", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment api-server --ignore-not-found" + } + ] +} diff --git a/scenarios/data/dns-resolution-mcq.json b/scenarios/data/dns-resolution-mcq.json new file mode 100644 index 0000000..e23eafb --- /dev/null +++ b/scenarios/data/dns-resolution-mcq.json @@ -0,0 +1,39 @@ +{ + "id": "dns-resolution-mcq", + "title": "Kubernetes DNS Resolution", + "category": "Services & Networking", + "difficulty": "Easy", + "type": "mcq", + "weight": 3, + "description": "## Kubernetes DNS Resolution\n\nA pod in namespace `frontend` needs to reach a service named `db-service` in namespace `backend`. Which DNS name correctly resolves to that service from within the cluster?", + "options": [ + { + "id": "a", + "text": "`db-service`" + }, + { + "id": "b", + "text": "`db-service.backend`" + }, + { + "id": "c", + "text": "`db-service.backend.svc.cluster.local`" + }, + { + "id": "d", + "text": "`backend.db-service.cluster.local`" + } + ], + "correct_option": "c", + "explanation": "The full Kubernetes DNS format is `..svc.`. The default cluster domain is `cluster.local`, so the FQDN is `db-service.backend.svc.cluster.local`. When pods are in the **same namespace**, just `db-service` works. Across namespaces, you need at minimum `db-service.backend` (short form). Option C is the fully-qualified name that always works regardless of the caller's namespace.", + "hints": [ + { + "title": "Kubernetes DNS format", + "body": "CoreDNS resolves: `..svc.cluster.local`. You can verify with: `kubectl exec -- nslookup `.", + "command": "kubectl exec -it -- nslookup db-service.backend.svc.cluster.local" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/dry-run-manifest-basics.json b/scenarios/data/dry-run-manifest-basics.json new file mode 100644 index 0000000..491b024 --- /dev/null +++ b/scenarios/data/dry-run-manifest-basics.json @@ -0,0 +1,39 @@ +{ + "id": "dry-run-manifest-basics", + "title": "Generating YAML Manifests", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Generating and Applying Manifests with Dry Run\n\nInstead of writing YAML from scratch, you can use `kubectl` to generate templates using `--dry-run=client -o yaml` and then apply them.\n\n**Your task:**\n\nUse `kubectl run` with `--dry-run=client -o yaml` to generate a Pod manifest for `web-pod` using image `httpd:alpine`, then pipe it directly to `kubectl apply -f -` to create the pod.\n\n```bash\nkubectl run web-pod --image=httpd:alpine --dry-run=client -o yaml | kubectl apply -f -\n```", + "hints": [ + { + "title": "Combine dry-run with apply", + "body": "Chain kubectl run --dry-run=client -o yaml with kubectl apply -f - using a pipe.", + "command": "kubectl run web-pod --image=httpd:alpine --dry-run=client -o yaml | kubectl apply -f -" + } + ], + "setup_commands": [], + "validation": { + "commands": [ + { + "description": "Pod 'web-pod' exists", + "command": "kubectl get pod web-pod -o jsonpath='{.metadata.name}'", + "expected_output": "web-pod", + "match": "exact" + }, + { + "description": "Pod uses httpd:alpine image", + "command": "kubectl get pod web-pod -o jsonpath='{.spec.containers[0].image}'", + "expected_output": "httpd:alpine", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod web-pod --ignore-not-found --grace-period=0 --force" + } + ] +} diff --git a/scenarios/data/edit-deployment-basics.json b/scenarios/data/edit-deployment-basics.json new file mode 100644 index 0000000..4066f0e --- /dev/null +++ b/scenarios/data/edit-deployment-basics.json @@ -0,0 +1,40 @@ +{ + "id": "edit-deployment-basics", + "title": "Updating Deployments", + "category": "Workloads", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Updating a Deployment\n\nA deployment named `web-app` is currently running `nginx:1.24`. A security vulnerability was found in this version.\n\n**Your task:**\n\nUpdate the image of the `web-app` deployment to `nginx:1.25` to patch the vulnerability. Wait for the new pods to become ready.\n\n```bash\n# Verify your change:\nkubectl describe deployment web-app | grep Image\n```", + "hints": [ + { + "title": "Set image command", + "body": "You can use `kubectl set image` to update a deployment's container image immediately.", + "command": "kubectl set image deployment/web-app nginx=nginx:1.25" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment web-app --image=nginx:1.24" + }, + { + "command": "kubectl rollout status deployment/web-app --timeout=60s" + } + ], + "validation": { + "commands": [ + { + "description": "Deployment uses image nginx:1.25", + "command": "kubectl get deployment web-app -o jsonpath='{.spec.template.spec.containers[0].image}'", + "expected_output": "nginx:1.25", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment web-app --ignore-not-found" + } + ] +} diff --git a/scenarios/data/emptydir-pod.json b/scenarios/data/emptydir-pod.json new file mode 100644 index 0000000..9ed8c59 --- /dev/null +++ b/scenarios/data/emptydir-pod.json @@ -0,0 +1,51 @@ +{ + "id": "emptydir-pod", + "title": "Shared emptyDir Volume Between Containers", + "category": "Storage", + "difficulty": "Easy", + "type": "task", + "weight": 5, + "description": "## Shared emptyDir Volume\n\nAn `emptyDir` volume is created when a Pod is assigned to a node and exists as long as the Pod runs. It is ideal for sharing data between containers in the same pod (e.g., sidecar patterns).\n\n**Your task:**\n\nCreate a Pod named `shared-data` with two containers:\n1. **writer** (`busybox:1.36`) — writes `hello-kube` to `/shared/message.txt` then sleeps\n2. **reader** (`busybox:1.36`) — reads and prints `/shared/message.txt` then sleeps\n\nBoth containers mount an `emptyDir` volume at `/shared`.", + "hints": [ + { + "title": "emptyDir shared volume manifest", + "body": "Define a single volume of type `emptyDir: {}` and mount it in both containers under the same path.", + "command": "cat < /shared/message.txt && sleep 3600\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n - name: reader\n image: busybox:1.36\n command: [\"sh\",\"-c\",\"sleep 5 && cat /shared/message.txt && sleep 3600\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n volumes:\n - name: shared-vol\n emptyDir: {}\nEOF" + } + ], + "setup_commands": [], + "validation": { + "commands": [ + { + "description": "Pod 'shared-data' exists", + "command": "kubectl get pod shared-data -o jsonpath='{.metadata.name}'", + "expected_output": "shared-data", + "match": "exact" + }, + { + "description": "Pod has 2 containers", + "command": "kubectl get pod shared-data -o jsonpath='{range .spec.containers[*]}{.name}{\"\\n\"}{end}'", + "expected_output": "writer", + "match": "contains" + }, + { + "description": "Volume type is emptyDir", + "command": "kubectl get pod shared-data -o jsonpath='{.spec.volumes[0].emptyDir}'", + "expected_output": "{}", + "match": "contains" + }, + { + "description": "Writer container mounts the shared volume", + "command": "kubectl get pod shared-data -o jsonpath='{.spec.containers[0].volumeMounts[0].mountPath}'", + "expected_output": "/shared", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod shared-data --ignore-not-found --grace-period=0 --force" + } + ] +} diff --git a/scenarios/data/endpoint-fix-task.json b/scenarios/data/endpoint-fix-task.json new file mode 100644 index 0000000..00da91b --- /dev/null +++ b/scenarios/data/endpoint-fix-task.json @@ -0,0 +1,59 @@ +{ + "id": "endpoint-fix-task", + "title": "Fix a Broken Service Selector", + "category": "Troubleshooting", + "difficulty": "Medium", + "type": "task", + "weight": 7, + "description": "## Fix a Broken Service Selector\n\nA service `my-svc` has been created but no traffic reaches the pods. The pods are labelled `app=backend`, but the service selector is misconfigured.\n\n**Your task:**\n\nFix the `my-svc` service selector so it correctly targets the `backend` pods.\n\n```bash\n# Investigate:\nkubectl describe service my-svc\nkubectl get endpoints my-svc\nkubectl get pods --show-labels\n```", + "hints": [ + { + "title": "Identify the mismatch", + "body": "Check what label the pods actually carry, then compare it against the service selector.", + "command": "kubectl get pods --show-labels && kubectl get service my-svc -o jsonpath='{.spec.selector}'" + }, + { + "title": "Patch the service selector", + "body": "The pods carry `app=backend` but the service selector says `app=wrong-label`. Use `kubectl patch` to point the selector at the correct label.", + "command": "kubectl patch service my-svc --type=json -p='[{\"op\":\"replace\",\"path\":\"/spec/selector/app\",\"value\":\"backend\"}]'" + }, + { + "title": "Verify endpoints are populated", + "body": "After fixing the selector, Kubernetes will automatically update the endpoint slice. Confirm the service now has live pod addresses.", + "command": "kubectl get endpoints my-svc" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment backend --image=nginx:alpine --replicas=2 2>/dev/null || true" + }, + { + "command": "kubectl apply -f - </dev/null | grep -v '^$' || echo empty", + "expected_output": "empty", + "match": "not_contains" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment backend --ignore-not-found" + }, + { + "command": "kubectl delete svc my-svc --ignore-not-found" + } + ] +} diff --git a/scenarios/data/env-vars-configmap.json b/scenarios/data/env-vars-configmap.json new file mode 100644 index 0000000..d7986e6 --- /dev/null +++ b/scenarios/data/env-vars-configmap.json @@ -0,0 +1,59 @@ +{ + "id": "env-vars-configmap", + "title": "Environment Variables from ConfigMap", + "category": "Configuration", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Environment Variables from ConfigMap\n\nConfigMaps can inject configuration as environment variables into pods, keeping application images generic and portable.\n\n**Your task:**\n\n1. Create a ConfigMap named `app-env` with two keys:\n - `LOG_LEVEL=debug`\n - `APP_PORT=8080`\n\n2. Create a Pod named `env-pod` using `busybox:1.36` with command `sleep 3600` that loads **all keys** from `app-env` as environment variables using `envFrom`.\n\n**Verify:**\n```bash\nkubectl exec env-pod -- env | grep -E 'LOG_LEVEL|APP_PORT'\n```", + "hints": [ + { + "title": "Create the ConfigMap", + "body": "Use `kubectl create configmap` with multiple `--from-literal` flags.", + "command": "kubectl create configmap app-env --from-literal=LOG_LEVEL=debug --from-literal=APP_PORT=8080" + }, + { + "title": "Load all ConfigMap keys via envFrom", + "body": "Use `envFrom` with `configMapRef` to load all keys at once — simpler than mapping each key individually.", + "command": "cat </dev/null", + "expected_output": "/tmp/ready", + "match": "contains" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod worker-pod --ignore-not-found --grace-period=0 --force" + } + ] +} diff --git a/scenarios/data/expose-service-basics.json b/scenarios/data/expose-service-basics.json new file mode 100644 index 0000000..c41cf1c --- /dev/null +++ b/scenarios/data/expose-service-basics.json @@ -0,0 +1,52 @@ +{ + "id": "expose-service-basics", + "title": "Exposing an Application", + "category": "Services & Networking", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Exposing an Application\n\nA `redis` deployment is running, but other pods cannot reliably connect to it because pod IPs change when they restart.\n\n**Your task:**\n\nCreate a **ClusterIP** Service named `redis-svc` to expose the `redis` deployment on port `6379`.\n\n```bash\n# Verify your service:\nkubectl get svc redis-svc\n```", + "hints": [ + { + "title": "Use kubectl expose", + "body": "The imperative `expose` command is the fastest way to create a service for a deployment.", + "command": "kubectl expose deployment redis --name=redis-svc --port=6379 --target-port=6379" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment redis --image=redis:alpine" + } + ], + "validation": { + "commands": [ + { + "description": "Service 'redis-svc' exists", + "command": "kubectl get svc redis-svc -o jsonpath='{.metadata.name}'", + "expected_output": "redis-svc", + "match": "exact" + }, + { + "description": "Service targets port 6379", + "command": "kubectl get svc redis-svc -o jsonpath='{.spec.ports[0].port}'", + "expected_output": "6379", + "match": "exact" + }, + { + "description": "Service selector matches deployment", + "command": "kubectl get svc redis-svc -o jsonpath='{.spec.selector.app}'", + "expected_output": "redis", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment redis --ignore-not-found" + }, + { + "command": "kubectl delete svc redis-svc --ignore-not-found" + } + ] +} diff --git a/scenarios/data/extract-logs-basics.json b/scenarios/data/extract-logs-basics.json new file mode 100644 index 0000000..8dcb148 --- /dev/null +++ b/scenarios/data/extract-logs-basics.json @@ -0,0 +1,42 @@ +{ + "id": "extract-logs-basics", + "title": "Extract Container Logs", + "category": "Troubleshooting", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Extract Pod Logs\n\nA pod named `log-generator` is running and emitting log lines every second.\n\n**Your task:**\n\nUse `kubectl logs` to inspect the logs and confirm that the message `ERROR: Database connection failed` appears in the output.\n\n> Tip: Use `kubectl logs log-generator | grep ERROR` to filter.", + "hints": [ + { + "title": "View pod logs", + "body": "Use kubectl logs to stream the output of a running container.", + "command": "kubectl logs log-generator" + }, + { + "title": "Filter logs", + "body": "Pipe kubectl logs into grep to find specific messages.", + "command": "kubectl logs log-generator | grep ERROR" + } + ], + "setup_commands": [ + { + "command": "kubectl run log-generator --image=busybox:1.36 --command -- sh -c 'echo \"ERROR: Database connection failed\" && sleep 3600'" + } + ], + "validation": { + "commands": [ + { + "description": "Pod log-generator emits ERROR: Database connection failed", + "command": "kubectl logs log-generator 2>/dev/null", + "expected_output": "ERROR: Database connection failed", + "match": "contains" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod log-generator --ignore-not-found --grace-period=0 --force" + } + ] +} diff --git a/scenarios/data/ingress-task.json b/scenarios/data/ingress-task.json new file mode 100644 index 0000000..86332ab --- /dev/null +++ b/scenarios/data/ingress-task.json @@ -0,0 +1,58 @@ +{ + "id": "ingress-task", + "title": "Create an Ingress Resource", + "category": "Services & Networking", + "difficulty": "Medium", + "type": "task", + "weight": 7, + "description": "## Create an Ingress Resource\n\nIngress exposes HTTP/HTTPS routes from outside the cluster to services within it. k3s includes Traefik as its default Ingress controller.\n\n**Your task:**\n\nA deployment `web-app` and service `web-svc` (port 80) already exist. Create an **Ingress** named `web-ingress` that:\n- Routes traffic for host `app.lab.local`\n- Path `/` → service `web-svc` on port `80`\n- Uses `pathType: Prefix`", + "hints": [ + { + "title": "Ingress manifest", + "body": "Use `networking.k8s.io/v1` for the Ingress API. Define `spec.rules[].host` and `spec.rules[].http.paths[]`.", + "command": "cat </dev/null || true" + }, + { + "command": "kubectl expose deployment web-app --name=web-svc --port=80 --target-port=80 2>/dev/null || true" + } + ], + "validation": { + "commands": [ + { + "description": "Ingress 'web-ingress' exists", + "command": "kubectl get ingress web-ingress -o jsonpath='{.metadata.name}'", + "expected_output": "web-ingress", + "match": "exact" + }, + { + "description": "Ingress host is app.lab.local", + "command": "kubectl get ingress web-ingress -o jsonpath='{.spec.rules[0].host}'", + "expected_output": "app.lab.local", + "match": "exact" + }, + { + "description": "Backend service is web-svc", + "command": "kubectl get ingress web-ingress -o jsonpath='{.spec.rules[0].http.paths[0].backend.service.name}'", + "expected_output": "web-svc", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment web-app --ignore-not-found" + }, + { + "command": "kubectl delete svc web-svc --ignore-not-found" + }, + { + "command": "kubectl delete ingress web-ingress --ignore-not-found" + } + ] +} diff --git a/scenarios/data/init-container-task-basics.json b/scenarios/data/init-container-task-basics.json new file mode 100644 index 0000000..36d8096 --- /dev/null +++ b/scenarios/data/init-container-task-basics.json @@ -0,0 +1,45 @@ +{ + "id": "init-container-task-basics", + "title": "Pod with Init Container", + "category": "Core Concepts", + "difficulty": "Medium", + "type": "task", + "weight": 5, + "description": "## Pod with Init Container\n\nInit containers run **before** the main application containers start. Each init container must complete successfully before the next one begins, and all must finish before the main containers are started. They are ideal for setup tasks like seeding config files or waiting for a dependency.\n\n**Your task:**\n\nCreate a Pod named `init-demo` with:\n- An **init container** named `setup` using `busybox:1.36` that writes `ready` into `/shared/status.txt`\n- A **main container** named `app` using `busybox:1.36` with command `sleep 3600`\n- Both containers mount an `emptyDir` volume at `/shared`\n\n```bash\n# Verify the init container wrote the file:\nkubectl exec init-demo -- cat /shared/status.txt\n```", + "hints": [ + { + "title": "Pod manifest with initContainers", + "body": "Define `spec.initContainers[]` above `spec.containers[]`. Both containers share the emptyDir volume mounted at /shared.", + "command": "cat < /shared/status.txt\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n containers:\n - name: app\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n volumes:\n - name: shared-vol\n emptyDir: {}\nEOF" + } + ], + "setup_commands": [], + "validation": { + "commands": [ + { + "description": "Pod 'init-demo' is Running", + "command": "kubectl get pod init-demo -o jsonpath='{.status.phase}'", + "expected_output": "Running", + "match": "exact" + }, + { + "description": "Init container 'setup' completed with exit code 0", + "command": "kubectl get pod init-demo -o jsonpath='{.status.initContainerStatuses[0].state.terminated.exitCode}'", + "expected_output": "0", + "match": "exact" + }, + { + "description": "File /shared/status.txt contains 'ready'", + "command": "kubectl exec init-demo -- cat /shared/status.txt 2>/dev/null", + "expected_output": "ready", + "match": "contains" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod init-demo --ignore-not-found --grace-period=0 --force" + } + ] +} diff --git a/scenarios/data/init-containers-mcq.json b/scenarios/data/init-containers-mcq.json new file mode 100644 index 0000000..9e9b06e --- /dev/null +++ b/scenarios/data/init-containers-mcq.json @@ -0,0 +1,39 @@ +{ + "id": "init-containers-mcq", + "title": "Init Containers Behaviour", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "mcq", + "weight": 3, + "description": "## Init Containers Behaviour\n\nA pod spec defines two init containers (`init-db-check` and `init-config`) followed by one main app container (`web`).\n\nWhich statement **correctly** describes how Kubernetes runs these containers?", + "options": [ + { + "id": "a", + "text": "All three containers start simultaneously; init containers simply have lower priority" + }, + { + "id": "b", + "text": "Init containers run sequentially to completion before the main `web` container starts" + }, + { + "id": "c", + "text": "The `web` container starts first, and init containers run as sidecars alongside it" + }, + { + "id": "d", + "text": "Init containers cannot share volumes with the main container" + } + ], + "correct_option": "b", + "explanation": "Init containers always run **sequentially** and must each exit with a success (exit code 0) before the next one starts. Only after **all** init containers complete successfully does Kubernetes start the main application containers. They can share volumes with main containers, making them ideal for setup tasks like seeding configs, waiting for dependencies, or initialising databases.", + "hints": [ + { + "title": "Init container execution order", + "body": "Check `kubectl explain pod.spec.initContainers`. Each init container runs to completion before the next begins, and all must succeed before app containers start.", + "command": "kubectl explain pod.spec.initContainers" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/job-task.json b/scenarios/data/job-task.json new file mode 100644 index 0000000..1166c73 --- /dev/null +++ b/scenarios/data/job-task.json @@ -0,0 +1,45 @@ +{ + "id": "job-task", + "title": "Create a One-Shot Job", + "category": "Workloads & Scheduling", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Create a One-Shot Job\n\nA **Job** creates one or more pods and ensures a specified number of them successfully terminate. Unlike Deployments, Jobs are meant for finite tasks that run to completion.\n\n**Your task:**\n\nCreate a Job named `pi-job` that:\n- Uses `perl:5.34` image\n- Runs: `perl -Mbignum=bpi -wle 'print bpi(100)'` (computes π to 100 digits)\n- `completions: 1`\n- `restartPolicy: Never`", + "hints": [ + { + "title": "Job manifest", + "body": "Set `restartPolicy: Never` or `OnFailure` in the pod template spec (not at pod spec level).", + "command": "cat < Expected output: `nginx:1.25.3`", + "hints": [ + { + "title": "JSONPath syntax", + "body": "The image is located at `.spec.template.spec.containers[0].image`. Use -o jsonpath to extract it.", + "command": "kubectl get deployment hidden-app -o jsonpath='{.spec.template.spec.containers[0].image}'" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment hidden-app --image=nginx:1.25.3" + } + ], + "validation": { + "commands": [ + { + "description": "kubectl jsonpath returns the correct image", + "command": "kubectl get deployment hidden-app -o jsonpath='{.spec.template.spec.containers[0].image}'", + "expected_output": "nginx:1.25.3", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment hidden-app --ignore-not-found" + } + ] +} diff --git a/scenarios/data/kubectl-cp-basics.json b/scenarios/data/kubectl-cp-basics.json new file mode 100644 index 0000000..fdd6c60 --- /dev/null +++ b/scenarios/data/kubectl-cp-basics.json @@ -0,0 +1,49 @@ +{ + "id": "kubectl-cp-basics", + "title": "Copy Files to Containers", + "category": "Troubleshooting", + "difficulty": "Medium", + "type": "task", + "weight": 4, + "description": "## Copy Files Between Containers and the Cluster\n\nThe `kubectl cp` command allows you to copy files and directories to and from running containers.\n\n**Your task:**\n\nA pod named `config-pod` is running with a file at `/src/config.json`. Copy it **into** the same pod at `/app/config.json` using `kubectl cp`.\n\n> Tip: `kubectl cp` syntax is `kubectl cp :`", + "hints": [ + { + "title": "Use kubectl cp", + "body": "First copy the file out from /src, then copy it back to /app, or use kubectl exec to verify.", + "command": "kubectl cp config-pod:/src/config.json /tmp/config.json && kubectl cp /tmp/config.json config-pod:/app/config.json" + } + ], + "setup_commands": [ + { + "command": "kubectl run config-pod --image=busybox:1.36 --command -- sleep 3600" + }, + { + "command": "kubectl wait --for=condition=Ready pod/config-pod --timeout=60s" + }, + { + "command": "kubectl exec config-pod -- sh -c 'mkdir -p /src && echo eyJzdGF0dXMiOiAib2sifQo= | base64 -d > /src/config.json'" + }, + { + "command": "kubectl exec config-pod -- mkdir -p /app" + } + ], + "validation": { + "commands": [ + { + "description": "File /app/config.json exists inside pod", + "command": "kubectl exec config-pod -- cat /app/config.json 2>/dev/null", + "expected_output": "{\"status\": \"ok\"}", + "match": "contains" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod config-pod --ignore-not-found --grace-period=0 --force" + }, + { + "command": "rm -f /tmp/config.json" + } + ] +} diff --git a/scenarios/data/kubectl-essentials-mcq.json b/scenarios/data/kubectl-essentials-mcq.json new file mode 100644 index 0000000..12877e3 --- /dev/null +++ b/scenarios/data/kubectl-essentials-mcq.json @@ -0,0 +1,39 @@ +{ + "id": "kubectl-essentials-mcq", + "title": "Essential kubectl Commands", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "mcq", + "weight": 2, + "description": "## Essential kubectl Commands\n\nYou need to quickly view the **logs** of a container named `api` inside a pod named `backend-7d9f`. The pod has multiple containers.\n\nWhich command is correct?", + "options": [ + { + "id": "a", + "text": "`kubectl describe pod backend-7d9f`" + }, + { + "id": "b", + "text": "`kubectl logs backend-7d9f`" + }, + { + "id": "c", + "text": "`kubectl logs backend-7d9f -c api`" + }, + { + "id": "d", + "text": "`kubectl exec backend-7d9f -- cat /var/log/api.log`" + } + ], + "correct_option": "c", + "explanation": "When a pod has **multiple containers**, you must specify which container's logs you want using the `-c ` flag: `kubectl logs -c `. Without `-c`, kubectl returns an error if the pod has more than one container. `kubectl describe` shows metadata and events, not live logs. `kubectl exec` can work but is cumbersome and container-runtime dependent.", + "hints": [ + { + "title": "kubectl logs flags", + "body": "Key flags: `-c` (container name for multi-container pods), `-f` (follow/stream), `--previous` (crashed container logs), `--since=1h` (time filter).", + "command": "kubectl logs --help | head -30" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/kubectl-output-format-mcq.json b/scenarios/data/kubectl-output-format-mcq.json new file mode 100644 index 0000000..70fb9a6 --- /dev/null +++ b/scenarios/data/kubectl-output-format-mcq.json @@ -0,0 +1,39 @@ +{ + "id": "kubectl-output-format-mcq", + "title": "kubectl Output Formats", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "mcq", + "weight": 2, + "description": "## kubectl Output Formats\n\nYou need to retrieve the **full live manifest** of a running deployment named `frontend` in YAML format so you can save it to a file for version control.\n\nWhich command produces the correct output?", + "options": [ + { + "id": "a", + "text": "`kubectl describe deployment frontend` — prints a human-readable summary with events" + }, + { + "id": "b", + "text": "`kubectl get deployment frontend -o yaml` — prints the full manifest in YAML format" + }, + { + "id": "c", + "text": "`kubectl export deployment frontend` — exports a portable manifest (removed in k8s 1.18)" + }, + { + "id": "d", + "text": "`kubectl manifest deployment frontend` — a built-in command for generating manifests" + } + ], + "correct_option": "b", + "explanation": "`kubectl get -o yaml` (or `--output=yaml`) prints the full live manifest as YAML, including all managed fields. `kubectl describe` gives a human-readable summary with events — not a re-applicable YAML manifest. `kubectl export` was removed in Kubernetes 1.18. `kubectl manifest` does not exist. Use `-o json` for JSON output, or `-o jsonpath='...'` to extract specific fields. To save to a file: `kubectl get deployment frontend -o yaml > frontend.yaml`.", + "hints": [ + { + "title": "Output format flags", + "body": "The `-o` / `--output` flag controls format: `-o yaml`, `-o json`, `-o wide` (extra columns), `-o name` (just the resource name), `-o jsonpath='...'` (field extraction).", + "command": "kubectl get deployment frontend -o yaml" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/labels-selectors-mcq.json b/scenarios/data/labels-selectors-mcq.json new file mode 100644 index 0000000..109e2bd --- /dev/null +++ b/scenarios/data/labels-selectors-mcq.json @@ -0,0 +1,39 @@ +{ + "id": "labels-selectors-mcq", + "title": "Labels and Selectors", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "mcq", + "weight": 3, + "description": "## Labels and Selectors\n\nYou run the following command:\n\n```bash\nkubectl get pods -l env=prod,tier=frontend\n```\n\nWhat does this command return?", + "options": [ + { + "id": "a", + "text": "Pods that have BOTH `env=prod` AND `tier=frontend` labels" + }, + { + "id": "b", + "text": "Pods that have either `env=prod` OR `tier=frontend` labels" + }, + { + "id": "c", + "text": "Pods that do NOT have `env=prod` or `tier=frontend` labels" + }, + { + "id": "d", + "text": "All pods, sorted by the `env` and `tier` label values" + } + ], + "correct_option": "a", + "explanation": "When you specify multiple label selector expressions separated by commas, Kubernetes applies a logical **AND** — all conditions must be true. Only pods that have both `env=prod` **and** `tier=frontend` are returned. Use `-l 'env in (prod,staging)'` for OR-style matching.", + "hints": [ + { + "title": "How comma-separated selectors work", + "body": "The `-l` flag accepts a comma-separated list of `key=value` expressions. Multiple expressions are ANDed together — all must match for a pod to be included in the result.", + "command": "kubectl get pods -l env=prod,tier=frontend" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/limitrange-task.json b/scenarios/data/limitrange-task.json new file mode 100644 index 0000000..0d8e18f --- /dev/null +++ b/scenarios/data/limitrange-task.json @@ -0,0 +1,49 @@ +{ + "id": "limitrange-task", + "title": "LimitRange for Default Container Limits", + "category": "Workloads & Scheduling", + "difficulty": "Medium", + "type": "task", + "weight": 6, + "description": "## LimitRange\n\nA **LimitRange** sets default resource requests/limits for containers in a namespace. Any container created without explicit limits will automatically get the LimitRange defaults.\n\n**Your task:**\n\nCreate a namespace `bounded-ns` and a LimitRange named `default-limits` that sets:\n- Default CPU limit: `200m`\n- Default CPU request: `100m`\n- Default Memory limit: `256Mi`\n- Default Memory request: `128Mi`", + "hints": [ + { + "title": "LimitRange manifest", + "body": "Use `type: Container` and the `default`/`defaultRequest` fields inside `spec.limits[]`.", + "command": "cat </dev/null || true" + } + ], + "validation": { + "commands": [ + { + "description": "LimitRange 'default-limits' exists in bounded-ns", + "command": "kubectl get limitrange default-limits -n bounded-ns -o jsonpath='{.metadata.name}'", + "expected_output": "default-limits", + "match": "exact" + }, + { + "description": "Default CPU limit is 200m", + "command": "kubectl get limitrange default-limits -n bounded-ns -o jsonpath='{.spec.limits[0].default.cpu}'", + "expected_output": "200m", + "match": "exact" + }, + { + "description": "Default memory limit is 256Mi", + "command": "kubectl get limitrange default-limits -n bounded-ns -o jsonpath='{.spec.limits[0].default.memory}'", + "expected_output": "256Mi", + "match": "exact" + } + ] + }, + "default_namespace": "bounded-ns", + "teardown_commands": [ + { + "command": "kubectl delete namespace bounded-ns --ignore-not-found --wait=false" + } + ] +} diff --git a/scenarios/data/liveness-probe-task.json b/scenarios/data/liveness-probe-task.json new file mode 100644 index 0000000..8649cb8 --- /dev/null +++ b/scenarios/data/liveness-probe-task.json @@ -0,0 +1,51 @@ +{ + "id": "liveness-probe-task", + "title": "Configure a Liveness Probe", + "category": "Workloads", + "difficulty": "Medium", + "type": "task", + "weight": 6, + "description": "## Configure a Liveness Probe\n\nKubernetes uses **liveness probes** to know when to restart a container. If a container's liveness probe fails repeatedly, the kubelet kills the container and the pod's restart policy takes effect.\n\n**Your task:**\n\nCreate a Deployment named `probed-app` with:\n- Image: `nginx:alpine`\n- 1 replica\n- A **liveness probe** that:\n - Checks `HTTP GET /` on port `80`\n - `initialDelaySeconds: 5`\n - `periodSeconds: 10`", + "hints": [ + { + "title": "Liveness probe structure", + "body": "Add `livenessProbe` under `spec.containers[]`. Use `httpGet` with `path` and `port` fields.", + "command": "cat <` to create a new namespace.", + "command": "kubectl create namespace team-alpha" + }, + { + "title": "Deploy into a specific namespace", + "body": "Use the `-n` or `--namespace` flag with `kubectl create deployment`. Alternatively use `kubectl apply -f` with `namespace` set in the manifest metadata.", + "command": "kubectl create deployment web --image=nginx:alpine --replicas=2 -n team-alpha" + } + ], + "setup_commands": [], + "validation": { + "commands": [ + { + "description": "Namespace 'team-alpha' exists", + "command": "kubectl get namespace team-alpha -o jsonpath='{.metadata.name}'", + "expected_output": "team-alpha", + "match": "exact" + }, + { + "description": "Deployment 'web' exists in team-alpha", + "command": "kubectl get deployment web -n team-alpha -o jsonpath='{.metadata.name}'", + "expected_output": "web", + "match": "exact" + }, + { + "description": "Deployment 'web' has 2 replicas", + "command": "kubectl get deployment web -n team-alpha -o jsonpath='{.spec.replicas}'", + "expected_output": "2", + "match": "exact" + }, + { + "description": "Deployment uses nginx image", + "command": "kubectl get deployment web -n team-alpha -o jsonpath='{.spec.template.spec.containers[0].image}'", + "expected_output": "nginx", + "match": "contains" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete namespace team-alpha --ignore-not-found --wait=false" + } + ] +} diff --git a/scenarios/data/network-policy.json b/scenarios/data/network-policy.json new file mode 100644 index 0000000..7a321bd --- /dev/null +++ b/scenarios/data/network-policy.json @@ -0,0 +1,75 @@ +{ + "id": "network-policy", + "title": "Isolate Traffic with NetworkPolicy", + "category": "Networking", + "difficulty": "Hard", + "type": "task", + "weight": 9, + "description": "## Implement Network Isolation\n\nA pre-created `database` pod in the `netpol` namespace must only accept traffic from pods labeled `role=backend`.\n\n**Your tasks:**\n1. Create a **NetworkPolicy** named `db-isolate` in the `netpol` namespace that:\n - Targets pods with label `app=database`\n - Allows **ingress** only from pods with label `role=backend` in the **same namespace**\n - Denies all other ingress traffic\n2. Verify the policy is applied correctly\n\n> The `database` and `backend` pods are pre-created for you.", + "hints": [ + { + "title": "Understand NetworkPolicy selectors", + "body": "A NetworkPolicy uses `podSelector` to pick which pods it applies to, and `ingress.from` to define allowed sources.", + "command": "kubectl explain networkpolicy.spec.ingress.from" + }, + { + "title": "Write the NetworkPolicy", + "body": "An empty ingress rule (no `from`) denies everything. Specifying a `from` allows only those sources.", + "command": "cat < key=value`.", + "command": "kubectl label node k8s-lab disk=ssd --overwrite" + }, + { + "title": "Use nodeSelector in pod spec", + "body": "Add `nodeSelector: disk: ssd` under `spec` in the pod manifest.", + "command": "cat </dev/null || true" + }, + { + "command": "kubectl delete pod ssd-pod --ignore-not-found --grace-period=0 --force" + } + ] +} diff --git a/scenarios/data/node-taint-toleration.json b/scenarios/data/node-taint-toleration.json new file mode 100644 index 0000000..66616db --- /dev/null +++ b/scenarios/data/node-taint-toleration.json @@ -0,0 +1,52 @@ +{ + "id": "node-taint-toleration", + "title": "Taints and Tolerations", + "category": "Cluster Administration", + "difficulty": "Medium", + "type": "task", + "weight": 7, + "description": "## Taints and Tolerations\n\nTaints allow a node to **repel** pods. A toleration on a pod allows it to be scheduled onto a tainted node.\n\nThe node `k8s-lab` has been tainted with `env=gpu:NoSchedule`.\n\n**Your task:**\n\nCreate a Pod named `gpu-pod` using `nginx:alpine` that tolerates the taint `env=gpu:NoSchedule` so it can be scheduled on this node.\n\n```bash\nkubectl describe node k8s-lab | grep -A5 Taints\n```", + "hints": [ + { + "title": "Toleration structure", + "body": "Add a `tolerations` block under `spec`. Match `key`, `operator`, `value`, and `effect` to the node taint.", + "command": "cat </dev/null || true" + } + ], + "validation": { + "commands": [ + { + "description": "Node has taint env=gpu:NoSchedule", + "command": "kubectl get node k8s-lab -o jsonpath='{.spec.taints[*].key}'", + "expected_output": "env", + "match": "contains" + }, + { + "description": "Pod 'gpu-pod' has toleration for key 'env'", + "command": "kubectl get pod gpu-pod -o jsonpath='{.spec.tolerations[*].key}'", + "expected_output": "env", + "match": "contains" + }, + { + "description": "Pod 'gpu-pod' toleration effect is NoSchedule", + "command": "kubectl get pod gpu-pod -o jsonpath='{.spec.tolerations[*].effect}'", + "expected_output": "NoSchedule", + "match": "contains" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl taint nodes k8s-lab env=gpu:NoSchedule- 2>/dev/null || true" + }, + { + "command": "kubectl delete pod gpu-pod --ignore-not-found --grace-period=0 --force" + } + ] +} diff --git a/scenarios/data/nodeport-task.json b/scenarios/data/nodeport-task.json new file mode 100644 index 0000000..cff937a --- /dev/null +++ b/scenarios/data/nodeport-task.json @@ -0,0 +1,52 @@ +{ + "id": "nodeport-task", + "title": "Expose a Deployment via NodePort", + "category": "Services & Networking", + "difficulty": "Easy", + "type": "task", + "weight": 5, + "description": "## Expose a Deployment via NodePort\n\nA **NodePort** service exposes an application on a static port on every node, making it accessible outside the cluster without a cloud load balancer.\n\n**Your task:**\n\nA deployment `frontend` is already running. Expose it as a **NodePort** service named `frontend-np` on:\n- Service port `80`\n- NodePort `30080`\n- Target port `80`", + "hints": [ + { + "title": "Create NodePort service", + "body": "Use `kubectl expose` or apply a Service manifest with `type: NodePort` and the `nodePort` field.", + "command": "cat </dev/null || true" + } + ], + "validation": { + "commands": [ + { + "description": "Service 'frontend-np' exists", + "command": "kubectl get service frontend-np -o jsonpath='{.metadata.name}'", + "expected_output": "frontend-np", + "match": "exact" + }, + { + "description": "Service type is NodePort", + "command": "kubectl get service frontend-np -o jsonpath='{.spec.type}'", + "expected_output": "NodePort", + "match": "exact" + }, + { + "description": "NodePort is 30080", + "command": "kubectl get service frontend-np -o jsonpath='{.spec.ports[0].nodePort}'", + "expected_output": "30080", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment frontend --ignore-not-found" + }, + { + "command": "kubectl delete svc frontend-np --ignore-not-found" + } + ] +} diff --git a/scenarios/data/pod-affinity-mcq.json b/scenarios/data/pod-affinity-mcq.json new file mode 100644 index 0000000..ec79c48 --- /dev/null +++ b/scenarios/data/pod-affinity-mcq.json @@ -0,0 +1,39 @@ +{ + "id": "pod-affinity-mcq", + "title": "Pod Affinity vs Anti-Affinity", + "category": "Workloads & Scheduling", + "difficulty": "Medium", + "type": "mcq", + "weight": 4, + "description": "## Pod Affinity vs Anti-Affinity\n\nYou want to ensure that **cache** pods are always scheduled on the **same node** as the **app** pods they serve (to reduce latency). Which scheduling rule should you use in the cache pod spec?", + "options": [ + { + "id": "a", + "text": "`spec.affinity.podAntiAffinity` with `requiredDuringSchedulingIgnoredDuringExecution` matching `app=app-pod`" + }, + { + "id": "b", + "text": "`spec.affinity.podAffinity` with `requiredDuringSchedulingIgnoredDuringExecution` matching `app=app-pod`" + }, + { + "id": "c", + "text": "`spec.affinity.nodeAffinity` with a `matchExpressions` for the app pod's node label" + }, + { + "id": "d", + "text": "`spec.tolerations` with `key=app-pod` and `effect=NoSchedule`" + } + ], + "correct_option": "b", + "explanation": "**Pod Affinity** (`spec.affinity.podAffinity`) attracts pods to nodes where matching pods are already running. Use `requiredDuringSchedulingIgnoredDuringExecution` for a hard constraint (cache pod MUST co-locate with app pod) vs `preferredDuringSchedulingIgnoredDuringExecution` for a soft preference. **Pod Anti-Affinity** does the opposite — spreads pods apart. Tolerations are unrelated to co-location and work with taints.", + "hints": [ + { + "title": "Affinity types", + "body": "podAffinity = attract to same node. podAntiAffinity = repel from same node. nodeAffinity = attract to nodes with specific labels. The `topologyKey: kubernetes.io/hostname` means 'same node'.", + "command": "kubectl explain pod.spec.affinity.podAffinity" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/pod-basics-mcq.json b/scenarios/data/pod-basics-mcq.json new file mode 100644 index 0000000..48d99fa --- /dev/null +++ b/scenarios/data/pod-basics-mcq.json @@ -0,0 +1,44 @@ +{ + "id": "pod-basics-mcq", + "title": "Pod Lifecycle States", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "mcq", + "weight": 3, + "description": "## What does the `CrashLoopBackOff` status mean for a Pod?\n\nYou observe the following when running `kubectl get pods`:\n\n```\nNAME READY STATUS RESTARTS AGE\nmy-app-xyz 0/1 CrashLoopBackOff 5 3m\n```\n\nWhat is the Kubernetes control plane communicating with this status?", + "options": [ + { + "id": "a", + "text": "The pod image could not be pulled from the container registry" + }, + { + "id": "b", + "text": "The container starts, crashes, and Kubernetes keeps restarting it with exponential backoff delay" + }, + { + "id": "c", + "text": "The pod is waiting for a PersistentVolume to become available" + }, + { + "id": "d", + "text": "The pod has been evicted from the node due to resource pressure" + } + ], + "correct_option": "b", + "explanation": "`CrashLoopBackOff` means the container is repeatedly crashing after startup. Kubernetes restarts it automatically but introduces increasing delays (backoff) between attempts to avoid overwhelming the system. Common causes include a bad entrypoint command, missing environment variables, or application errors on startup.", + "hints": [ + { + "title": "Interpreting pod status", + "body": "Use `kubectl describe pod ` to see the Events section — it shows exactly why the container is failing.", + "command": "kubectl describe pod my-app-xyz" + }, + { + "title": "Reading container logs", + "body": "Even a crashed container leaves logs behind. Use `--previous` to read the logs from the last crash.", + "command": "kubectl logs my-app-xyz --previous" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/pod-disruption-budget-task.json b/scenarios/data/pod-disruption-budget-task.json new file mode 100644 index 0000000..7f87de7 --- /dev/null +++ b/scenarios/data/pod-disruption-budget-task.json @@ -0,0 +1,66 @@ +{ + "id": "pod-disruption-budget-task", + "title": "Configure a PodDisruptionBudget", + "category": "Cluster Administration", + "difficulty": "Medium", + "type": "task", + "weight": 6, + "description": "## PodDisruptionBudget\n\nA **PodDisruptionBudget (PDB)** limits the number of pods of a replicated application that are down simultaneously during voluntary disruptions (node drains, cluster upgrades). It is a critical CKA topic.\n\n**Your task:**\n\nA Deployment named `pdb-app` with **3 replicas** is already running. Create a **PodDisruptionBudget** named `web-pdb` in the `default` namespace that:\n- Applies to pods with the label `app=pdb-app`\n- Ensures **at least 2 pods** are always available (`minAvailable: 2`)\n\n```bash\n# Verify:\nkubectl get pdb web-pdb\n```", + "hints": [ + { + "title": "PodDisruptionBudget manifest", + "body": "Use `spec.minAvailable` (integer or percentage) and `spec.selector.matchLabels` to target the right pods.", + "command": "cat </dev/null || true" + }, + { + "command": "kubectl rollout status deployment/pdb-app --timeout=90s" + } + ], + "validation": { + "commands": [ + { + "description": "PDB 'web-pdb' exists", + "command": "kubectl get pdb web-pdb -o jsonpath='{.metadata.name}'", + "expected_output": "web-pdb", + "match": "exact" + }, + { + "description": "minAvailable is 2", + "command": "kubectl get pdb web-pdb -o jsonpath='{.spec.minAvailable}'", + "expected_output": "2", + "match": "exact" + }, + { + "description": "PDB selector targets app=pdb-app", + "command": "kubectl get pdb web-pdb -o jsonpath='{.spec.selector.matchLabels.app}'", + "expected_output": "pdb-app", + "match": "exact" + }, + { + "description": "Deployment 'pdb-app' has 3 replicas", + "command": "kubectl get deployment pdb-app -o jsonpath='{.spec.replicas}'", + "expected_output": "3", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pdb web-pdb --ignore-not-found" + }, + { + "command": "kubectl delete deployment pdb-app --ignore-not-found" + } + ] +} diff --git a/scenarios/data/pod-labeling-basics.json b/scenarios/data/pod-labeling-basics.json new file mode 100644 index 0000000..8c3029e --- /dev/null +++ b/scenarios/data/pod-labeling-basics.json @@ -0,0 +1,37 @@ +{ + "id": "pod-labeling-basics", + "title": "Labelling Resources", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "task", + "weight": 3, + "description": "## Labelling Resources\n\nLabels are key-value pairs attached to objects, used to organize and select subsets of objects. A pod named `app-worker` is currently running, but it's missing a required label.\n\n**Your task:**\n\nAdd the label `tier=frontend` to the existing pod `app-worker`.\n\n```bash\n# Verify labels\nkubectl get pod app-worker --show-labels\n```", + "hints": [ + { + "title": "Use kubectl label", + "body": "You can label resources imperatively without editing the YAML.", + "command": "kubectl label pod app-worker tier=frontend" + } + ], + "setup_commands": [ + { + "command": "kubectl run app-worker --image=nginx:alpine" + } + ], + "validation": { + "commands": [ + { + "description": "Pod has label tier=frontend", + "command": "kubectl get pod app-worker -o jsonpath='{.metadata.labels.tier}'", + "expected_output": "frontend", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod app-worker --ignore-not-found --grace-period=0 --force" + } + ] +} diff --git a/scenarios/data/pod-security-context.json b/scenarios/data/pod-security-context.json new file mode 100644 index 0000000..72bd9d2 --- /dev/null +++ b/scenarios/data/pod-security-context.json @@ -0,0 +1,51 @@ +{ + "id": "pod-security-context", + "title": "Pod Security Context", + "category": "Security", + "difficulty": "Medium", + "type": "task", + "weight": 6, + "description": "## Pod Security Context\n\nA security context defines privilege and access control settings for a pod or container.\n\n**Your task:**\n\nCreate a Pod named `secure-pod` using `busybox:1.36` (command: `sleep 3600`) with the following security settings:\n- `runAsUser: 1000`\n- `runAsNonRoot: true`\n- `allowPrivilegeEscalation: false`\n\n```bash\n# Verify after creation:\nkubectl get pod secure-pod -o jsonpath='{.spec.containers[0].securityContext}'\n```", + "hints": [ + { + "title": "securityContext structure", + "body": "Security context can be set at Pod level (`spec.securityContext`) or container level (`spec.containers[].securityContext`). Container-level settings override pod-level.", + "command": "cat < /data/hello.txt'" + } + ], + "setup_commands": [], + "validation": { + "description": "Validates PV, PVC binding, pod running, and file written to the volume.", + "commands": [ + { + "description": "PVC local-pvc is Bound", + "command": "kubectl get pvc local-pvc -o jsonpath='{.status.phase}'", + "expected_output": "Bound", + "match": "exact" + }, + { + "description": "Pod 'storage-pod' is Running", + "command": "kubectl get pod storage-pod -o jsonpath='{.status.phase}'", + "expected_output": "Running", + "match": "exact" + }, + { + "description": "File /data/hello.txt contains 'hello-k8s'", + "command": "kubectl exec storage-pod -- cat /data/hello.txt", + "expected_output": "hello-k8s", + "match": "contains" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod storage-pod --ignore-not-found --grace-period=0 --force" + }, + { + "command": "kubectl delete pvc local-pvc --ignore-not-found" + } + ] +} diff --git a/scenarios/data/pvc-dynamic-task.json b/scenarios/data/pvc-dynamic-task.json new file mode 100644 index 0000000..bd1ec8a --- /dev/null +++ b/scenarios/data/pvc-dynamic-task.json @@ -0,0 +1,59 @@ +{ + "id": "pvc-dynamic-task", + "title": "Dynamic PVC Provisioning", + "category": "Storage", + "difficulty": "Medium", + "type": "task", + "weight": 6, + "description": "## Dynamic PVC Provisioning\n\nDynamic provisioning automatically creates a PersistentVolume when a PVC is created, using a StorageClass. k3s ships with a built-in `local-path` StorageClass.\n\n**Your task:**\n\n1. Create a **PersistentVolumeClaim** named `app-data` that requests:\n - Storage: `1Gi`\n - Access mode: `ReadWriteOnce`\n - StorageClass: `local-path`\n2. Create a Pod named `data-pod` using `nginx:alpine` that mounts `app-data` at `/data`\n\n```bash\nkubectl get storageclass # verify local-path exists\n```", + "hints": [ + { + "title": "PVC manifest", + "body": "Set `storageClassName: local-path` (k3s default). Dynamic provisioning creates the PV automatically.", + "command": "cat < =\n```", + "hints": [ + { + "title": "Set a new image on a Deployment", + "body": "Use `kubectl set image deployment/ =` to trigger a rolling update.", + "command": "kubectl set image deployment/app-v1 nginx=nginx:1.25" + }, + { + "title": "Check rollout progress", + "body": "Monitor the rollout with `kubectl rollout status`. Once complete, all pods will run the new image.", + "command": "kubectl get deployment app-v1 -o jsonpath='{.spec.template.spec.containers[0].image}'" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment app-v1 --image=nginx:1.24 --replicas=2" + }, + { + "command": "kubectl rollout status deployment/app-v1 --timeout=60s" + } + ], + "validation": { + "commands": [ + { + "description": "Deployment 'app-v1' image is nginx:1.25", + "command": "kubectl get deployment app-v1 -o jsonpath='{.spec.template.spec.containers[0].image}'", + "expected_output": "nginx:1.25", + "match": "exact" + }, + { + "description": "Deployment has 2 ready replicas", + "command": "kubectl get deployment app-v1 -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", + "expected_output": "2", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment app-v1 --ignore-not-found" + } + ] +} diff --git a/scenarios/data/scale-deployment.json b/scenarios/data/scale-deployment.json new file mode 100644 index 0000000..02154a1 --- /dev/null +++ b/scenarios/data/scale-deployment.json @@ -0,0 +1,51 @@ +{ + "id": "scale-deployment", + "title": "Scaling a Deployment", + "category": "Workloads", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Scaling a Deployment\n\nA deployment named `webapp` is currently running with **1 replica**. Traffic has increased and you need to scale it up.\n\n**Your task:**\n\nScale the `webapp` deployment to **4 replicas** and verify all pods become Ready.\n\n```bash\n# You can use the imperative command\nkubectl scale deployment webapp --replicas=4\n# Or edit the manifest\nkubectl edit deployment webapp\n```", + "hints": [ + { + "title": "Scale imperatively", + "body": "`kubectl scale` is the quickest way to change replica count without editing a YAML file.", + "command": "kubectl scale deployment webapp --replicas=4" + }, + { + "title": "Verify the scale", + "body": "Check READY column in `kubectl get deployment webapp` — it should show 4/4.", + "command": "kubectl get deployment webapp" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment webapp --image=nginx:alpine --replicas=1" + }, + { + "command": "kubectl rollout status deployment/webapp --timeout=60s" + } + ], + "validation": { + "commands": [ + { + "description": "Deployment 'webapp' has 4 replicas specified", + "command": "kubectl get deployment webapp -o jsonpath='{.spec.replicas}'", + "expected_output": "4", + "match": "exact" + }, + { + "description": "Deployment 'webapp' has 4 ready replicas", + "command": "kubectl get deployment webapp -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", + "expected_output": "4", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment webapp --ignore-not-found" + } + ] +} diff --git a/scenarios/data/secrets-basics.json b/scenarios/data/secrets-basics.json new file mode 100644 index 0000000..eec0409 --- /dev/null +++ b/scenarios/data/secrets-basics.json @@ -0,0 +1,53 @@ +{ + "id": "secrets-basics", + "title": "Creating and Using Secrets", + "category": "Configuration", + "difficulty": "Easy", + "type": "task", + "weight": 5, + "description": "## Creating and Using Secrets\n\nSecrets let you store sensitive data (passwords, tokens, keys) separately from your pod specs.\n\n**Your task:**\n\n1. Create a Secret named `app-secret` with the key `api-key` and value `supersecret`\n2. Create a Pod named `secret-reader` using `busybox:1.36` that:\n - Mounts the secret key `api-key` as an environment variable named `API_KEY`\n - Runs: `sleep 3600`\n\n**Verify:**\n```bash\nkubectl exec secret-reader -- env | grep API_KEY\n```", + "hints": [ + { + "title": "Create the Secret", + "body": "Use `kubectl create secret generic` with `--from-literal`.", + "command": "kubectl create secret generic app-secret --from-literal=api-key=supersecret" + }, + { + "title": "Reference secret in a Pod env var", + "body": "Use `env[].valueFrom.secretKeyRef` in the container spec to map a secret key to an env var.", + "command": "cat <` to see the Events section — it shows exactly why the container is failing.", - "command": "kubectl describe pod my-app-xyz" - }, - { - "title": "Reading container logs", - "body": "Even a crashed container leaves logs behind. Use `--previous` to read the logs from the last crash.", - "command": "kubectl logs my-app-xyz --previous" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "deploy-nginx", - "title": "Deploy and Expose Nginx", - "category": "Workloads", - "difficulty": "Easy", - "type": "task", - "weight": 5, - "description": "## Deploy and Expose an Nginx Application\n\nThe platform team needs a simple web server running in the cluster.\n\n**Your tasks:**\n1. Create a **Deployment** named `webserver` in the `default` namespace\n2. Use the image `nginx:1.25`\n3. Set replica count to **2**\n4. Expose it via a **ClusterIP Service** named `webserver-svc` on port **80**\n\n> 💡 Tip: You can use `kubectl create` for both resources imperatively, or write YAML manifests.", - "hints": [ - { - "title": "Create the Deployment", - "body": "Use `kubectl create deployment` with the `--image` and `--replicas` flags.", - "command": "kubectl create deployment webserver --image=nginx:1.25 --replicas=2" - }, - { - "title": "Expose the Deployment", - "body": "Use `kubectl expose` to create a ClusterIP service targeting port 80.", - "command": "kubectl expose deployment webserver --name=webserver-svc --port=80 --target-port=80" - }, - { - "title": "Verify", - "body": "Check that both resources are up and the service endpoints are populated.", - "command": "kubectl get deployment webserver && kubectl get svc webserver-svc && kubectl get endpoints webserver-svc" - } - ], - "setup_commands": [], - "validation": { - "description": "Checks that the deployment exists with 2 replicas, uses the correct image, and the service exists targeting port 80.", - "commands": [ - { - "description": "Deployment 'webserver' exists", - "command": "kubectl get deployment webserver -o jsonpath='{.metadata.name}'", - "expected_output": "webserver", - "match": "exact" - }, - { - "description": "Deployment has 2 ready replicas", - "command": "kubectl get deployment webserver -o jsonpath='{.status.readyReplicas}'", - "expected_output": "2", - "match": "exact" - }, - { - "description": "Deployment uses nginx:1.25 image", - "command": "kubectl get deployment webserver -o jsonpath='{.spec.template.spec.containers[0].image}'", - "expected_output": "nginx:1.25", - "match": "exact" - }, - { - "description": "Service 'webserver-svc' exists", - "command": "kubectl get svc webserver-svc -o jsonpath='{.metadata.name}'", - "expected_output": "webserver-svc", - "match": "exact" - }, - { - "description": "Service exposes port 80", - "command": "kubectl get svc webserver-svc -o jsonpath='{.spec.ports[0].port}'", - "expected_output": "80", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment webserver --ignore-not-found" - }, - { - "command": "kubectl delete svc webserver-svc --ignore-not-found" - } - ] - }, - { - "id": "configmap-volume", - "title": "Mount ConfigMap as Volume", - "category": "Configuration", - "difficulty": "Medium", - "type": "task", - "weight": 7, - "description": "## Inject Configuration via a ConfigMap Volume\n\nAn application reads its configuration from files on disk at `/etc/app-config/`.\n\n**Your tasks:**\n1. Create a **ConfigMap** named `app-config` in the `default` namespace with the following key-value pairs:\n - `environment`: `production`\n - `log_level`: `warn`\n - `max_connections`: `200`\n2. Create a **Pod** named `config-reader` using image `busybox:1.36`\n3. Mount the ConfigMap as a **volume** at `/etc/app-config` inside the container\n4. The pod should run the command: `sleep 3600`", - "hints": [ - { - "title": "Create the ConfigMap", - "body": "Use `--from-literal` for each key-value pair.", - "command": "kubectl create configmap app-config --from-literal=environment=production --from-literal=log_level=warn --from-literal=max_connections=200" - }, - { - "title": "Write the Pod manifest", - "body": "Use a heredoc piped directly to kubectl apply -f - to avoid writing to a temp file.", - "command": "cat < Start by describing the deployment and its pods to find clues about what's wrong.", - "hints": [ - { - "title": "Inspect the deployment", - "body": "Start by listing pods in the debug namespace and describing the failing ones.", - "command": "kubectl get pods -n debug\nkubectl describe pod -n debug -l app=broken-app" - }, - { - "title": "Check events", - "body": "The Events section in `kubectl describe` usually tells you exactly what's wrong — look for ImagePullBackOff, OOMKilled, or probe failures.", - "command": "kubectl describe deployment broken-app -n debug" - }, - { - "title": "Fix the image tag", - "body": "If the image tag doesn't exist, update it to a valid one using `kubectl set image`.", - "command": "kubectl set image deployment/broken-app app=nginx:1.25 -n debug" - } - ], - "setup_commands": [ - { - "command": "kubectl create namespace debug" - }, - { - "command": "kubectl create deployment broken-app --image=nginx:invalid-tag-99999 --replicas=3 -n debug" - } - ], - "validation": { - "description": "Checks that the broken-app deployment in the debug namespace has 3 ready replicas.", - "commands": [ - { - "description": "Deployment 'broken-app' has 3 ready replicas", - "command": "kubectl get deployment broken-app -n debug -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", - "expected_output": "3", - "match": "exact" - }, - { - "description": "No pods in ImagePullBackOff state", - "command": "kubectl get pods -n debug --no-headers 2>/dev/null | awk '{print $3}' | grep -c 'ImagePullBackOff\\|ErrImagePull' || true", - "expected_output": "0", - "match": "exact" - } - ] - }, - "default_namespace": "debug", - "teardown_commands": [ - { - "command": "kubectl delete namespace debug --ignore-not-found --wait=false" - } - ] - }, - { - "id": "pv-pvc-mount", - "title": "Persistent Storage with PV and PVC", - "category": "Storage", - "difficulty": "Medium", - "type": "task", - "weight": 7, - "description": "## Attach Persistent Storage to a Pod\n\nA stateful application needs data to survive pod restarts.\n\n**Your tasks:**\n1. Create a **PersistentVolumeClaim** named `local-pvc` in the `default` namespace requesting `200Mi` with:\n - Access mode: `ReadWriteOnce`\n - StorageClass: `local-path` (available in this cluster via k3s)\n2. Create a **Pod** named `storage-pod` using image `nginx:1.25` that mounts the PVC at `/data`\n3. Write a file inside the pod at `/data/hello.txt` with content `hello-k8s`", - "hints": [ - { - "title": "Create the PVC", - "body": "Use storageClassName: local-path to use the k3s built-in dynamic provisioner. No PV needs to be created manually.", - "command": "cat < /data/hello.txt'" - } - ], - "setup_commands": [], - "validation": { - "description": "Validates PV, PVC binding, pod running, and file written to the volume.", - "commands": [ - { - "description": "PVC local-pvc is Bound", - "command": "kubectl get pvc local-pvc -o jsonpath='{.status.phase}'", - "expected_output": "Bound", - "match": "exact" - }, - { - "description": "Pod 'storage-pod' is Running", - "command": "kubectl get pod storage-pod -o jsonpath='{.status.phase}'", - "expected_output": "Running", - "match": "exact" - }, - { - "description": "File /data/hello.txt contains 'hello-k8s'", - "command": "kubectl exec storage-pod -- cat /data/hello.txt", - "expected_output": "hello-k8s", - "match": "contains" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete pod storage-pod --ignore-not-found --grace-period=0 --force" - }, - { - "command": "kubectl delete pvc local-pvc --ignore-not-found" - } - ] - }, - { - "id": "network-policy", - "title": "Isolate Traffic with NetworkPolicy", - "category": "Networking", - "difficulty": "Hard", - "type": "task", - "weight": 9, - "description": "## Implement Network Isolation\n\nA pre-created `database` pod in the `netpol` namespace must only accept traffic from pods labeled `role=backend`.\n\n**Your tasks:**\n1. Create a **NetworkPolicy** named `db-isolate` in the `netpol` namespace that:\n - Targets pods with label `app=database`\n - Allows **ingress** only from pods with label `role=backend` in the **same namespace**\n - Denies all other ingress traffic\n2. Verify the policy is applied correctly\n\n> The `database` and `backend` pods are pre-created for you.", - "hints": [ - { - "title": "Understand NetworkPolicy selectors", - "body": "A NetworkPolicy uses `podSelector` to pick which pods it applies to, and `ingress.from` to define allowed sources.", - "command": "kubectl explain networkpolicy.spec.ingress.from" - }, - { - "title": "Write the NetworkPolicy", - "body": "An empty ingress rule (no `from`) denies everything. Specifying a `from` allows only those sources.", - "command": "cat <` to create a new namespace.", - "command": "kubectl create namespace team-alpha" - }, - { - "title": "Deploy into a specific namespace", - "body": "Use the `-n` or `--namespace` flag with `kubectl create deployment`. Alternatively use `kubectl apply -f` with `namespace` set in the manifest metadata.", - "command": "kubectl create deployment web --image=nginx:alpine --replicas=2 -n team-alpha" - } - ], - "setup_commands": [], - "validation": { - "commands": [ - { - "description": "Namespace 'team-alpha' exists", - "command": "kubectl get namespace team-alpha -o jsonpath='{.metadata.name}'", - "expected_output": "team-alpha", - "match": "exact" - }, - { - "description": "Deployment 'web' exists in team-alpha", - "command": "kubectl get deployment web -n team-alpha -o jsonpath='{.metadata.name}'", - "expected_output": "web", - "match": "exact" - }, - { - "description": "Deployment 'web' has 2 replicas", - "command": "kubectl get deployment web -n team-alpha -o jsonpath='{.spec.replicas}'", - "expected_output": "2", - "match": "exact" - }, - { - "description": "Deployment uses nginx image", - "command": "kubectl get deployment web -n team-alpha -o jsonpath='{.spec.template.spec.containers[0].image}'", - "expected_output": "nginx", - "match": "contains" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete namespace team-alpha --ignore-not-found --wait=false" - } - ] - }, - { - "id": "labels-selectors-mcq", - "title": "Labels and Selectors", - "category": "Core Concepts", - "difficulty": "Easy", - "type": "mcq", - "weight": 3, - "description": "## Labels and Selectors\n\nYou run the following command:\n\n```bash\nkubectl get pods -l env=prod,tier=frontend\n```\n\nWhat does this command return?", - "options": [ - { - "id": "a", - "text": "Pods that have BOTH `env=prod` AND `tier=frontend` labels" - }, - { - "id": "b", - "text": "Pods that have either `env=prod` OR `tier=frontend` labels" - }, - { - "id": "c", - "text": "Pods that do NOT have `env=prod` or `tier=frontend` labels" - }, - { - "id": "d", - "text": "All pods, sorted by the `env` and `tier` label values" - } - ], - "correct_option": "a", - "explanation": "When you specify multiple label selector expressions separated by commas, Kubernetes applies a logical **AND** — all conditions must be true. Only pods that have both `env=prod` **and** `tier=frontend` are returned. Use `-l 'env in (prod,staging)'` for OR-style matching.", - "hints": [ - { - "title": "How comma-separated selectors work", - "body": "The `-l` flag accepts a comma-separated list of `key=value` expressions. Multiple expressions are ANDed together — all must match for a pod to be included in the result.", - "command": "kubectl get pods -l env=prod,tier=frontend" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "resource-limits-task", - "title": "Resource Requests and Limits", - "category": "Configuration", - "difficulty": "Easy", - "type": "task", - "weight": 5, - "description": "## Resource Requests and Limits\n\nProper resource management prevents noisy-neighbour issues and enables the Kubernetes scheduler to make good placement decisions.\n\n**Your task:**\n\nCreate a Pod named `limited-pod` using the `nginx:alpine` image with the following resource configuration:\n\n| | CPU | Memory |\n|---|---|---|\n| **Request** | `100m` | `64Mi` |\n| **Limit** | `200m` | `128Mi` |\n\n```bash\n# Tip: write a manifest and apply it\nkubectl apply -f limited-pod.yaml\n```", - "hints": [ - { - "title": "Pod manifest with resources", - "body": "Add a `resources` block under `spec.containers[].resources` with `requests` and `limits` sub-keys.", - "command": "cat < =\n```", - "hints": [ - { - "title": "Set a new image on a Deployment", - "body": "Use `kubectl set image deployment/ =` to trigger a rolling update.", - "command": "kubectl set image deployment/app-v1 nginx=nginx:1.25" - }, - { - "title": "Check rollout progress", - "body": "Monitor the rollout with `kubectl rollout status`. Once complete, all pods will run the new image.", - "command": "kubectl get deployment app-v1 -o jsonpath='{.spec.template.spec.containers[0].image}'" - } - ], - "setup_commands": [ - { - "command": "kubectl create deployment app-v1 --image=nginx:1.24 --replicas=2" - }, - { - "command": "kubectl rollout status deployment/app-v1 --timeout=60s" - } - ], - "validation": { - "commands": [ - { - "description": "Deployment 'app-v1' image is nginx:1.25", - "command": "kubectl get deployment app-v1 -o jsonpath='{.spec.template.spec.containers[0].image}'", - "expected_output": "nginx:1.25", - "match": "exact" - }, - { - "description": "Deployment has 2 ready replicas", - "command": "kubectl get deployment app-v1 -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", - "expected_output": "2", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment app-v1 --ignore-not-found" - } - ] - }, - { - "id": "init-containers-mcq", - "title": "Init Containers Behaviour", - "category": "Core Concepts", - "difficulty": "Easy", - "type": "mcq", - "weight": 3, - "description": "## Init Containers Behaviour\n\nA pod spec defines two init containers (`init-db-check` and `init-config`) followed by one main app container (`web`).\n\nWhich statement **correctly** describes how Kubernetes runs these containers?", - "options": [ - { - "id": "a", - "text": "All three containers start simultaneously; init containers simply have lower priority" - }, - { - "id": "b", - "text": "Init containers run sequentially to completion before the main `web` container starts" - }, - { - "id": "c", - "text": "The `web` container starts first, and init containers run as sidecars alongside it" - }, - { - "id": "d", - "text": "Init containers cannot share volumes with the main container" - } - ], - "correct_option": "b", - "explanation": "Init containers always run **sequentially** and must each exit with a success (exit code 0) before the next one starts. Only after **all** init containers complete successfully does Kubernetes start the main application containers. They can share volumes with main containers, making them ideal for setup tasks like seeding configs, waiting for dependencies, or initialising databases.", - "hints": [ - { - "title": "Init container execution order", - "body": "Check `kubectl explain pod.spec.initContainers`. Each init container runs to completion before the next begins, and all must succeed before app containers start.", - "command": "kubectl explain pod.spec.initContainers" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "env-vars-configmap", - "title": "Environment Variables from ConfigMap", - "category": "Configuration", - "difficulty": "Easy", - "type": "task", - "weight": 4, - "description": "## Environment Variables from ConfigMap\n\nConfigMaps can inject configuration as environment variables into pods, keeping application images generic and portable.\n\n**Your task:**\n\n1. Create a ConfigMap named `app-env` with two keys:\n - `LOG_LEVEL=debug`\n - `APP_PORT=8080`\n\n2. Create a Pod named `env-pod` using `busybox:1.36` with command `sleep 3600` that loads **all keys** from `app-env` as environment variables using `envFrom`.\n\n**Verify:**\n```bash\nkubectl exec env-pod -- env | grep -E 'LOG_LEVEL|APP_PORT'\n```", - "hints": [ - { - "title": "Create the ConfigMap", - "body": "Use `kubectl create configmap` with multiple `--from-literal` flags.", - "command": "kubectl create configmap app-env --from-literal=LOG_LEVEL=debug --from-literal=APP_PORT=8080" - }, - { - "title": "Load all ConfigMap keys via envFrom", - "body": "Use `envFrom` with `configMapRef` to load all keys at once — simpler than mapping each key individually.", - "command": "cat </dev/null | grep -v '^$' || echo 0", - "expected_output": "4", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment webapp --ignore-not-found" - } - ] - }, - { - "id": "kubectl-essentials-mcq", - "title": "Essential kubectl Commands", - "category": "Core Concepts", - "difficulty": "Easy", - "type": "mcq", - "weight": 2, - "description": "## Essential kubectl Commands\n\nYou need to quickly view the **logs** of a container named `api` inside a pod named `backend-7d9f`. The pod has multiple containers.\n\nWhich command is correct?", - "options": [ - { - "id": "a", - "text": "`kubectl describe pod backend-7d9f`" - }, - { - "id": "b", - "text": "`kubectl logs backend-7d9f`" - }, - { - "id": "c", - "text": "`kubectl logs backend-7d9f -c api`" - }, - { - "id": "d", - "text": "`kubectl exec backend-7d9f -- cat /var/log/api.log`" - } - ], - "correct_option": "c", - "explanation": "When a pod has **multiple containers**, you must specify which container's logs you want using the `-c ` flag: `kubectl logs -c `. Without `-c`, kubectl returns an error if the pod has more than one container. `kubectl describe` shows metadata and events, not live logs. `kubectl exec` can work but is cumbersome and container-runtime dependent.", - "hints": [ - { - "title": "kubectl logs flags", - "body": "Key flags: `-c` (container name for multi-container pods), `-f` (follow/stream), `--previous` (crashed container logs), `--since=1h` (time filter).", - "command": "kubectl logs --help | head -30" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "rbac-clusterrole", - "title": "ClusterRole and ClusterRoleBinding", - "category": "Cluster Administration", - "difficulty": "Medium", - "type": "task", - "weight": 8, - "description": "## ClusterRole and ClusterRoleBinding\n\nRBAC **ClusterRoles** grant permissions cluster-wide (across all namespaces), unlike Roles which are namespace-scoped.\n\n**Your task:**\n\n1. Create a **ClusterRole** named `pod-reader` that allows `get`, `list`, `watch` on `pods`\n2. Create a **ClusterRoleBinding** named `pod-reader-binding` that binds `pod-reader` to the ServiceAccount `default` in the `default` namespace\n\n```bash\nkubectl create clusterrole --help\nkubectl create clusterrolebinding --help\n```", - "hints": [ - { - "title": "Create the ClusterRole", - "body": "Use `kubectl create clusterrole` with `--verb` and `--resource` flags.", - "command": "kubectl create clusterrole pod-reader --verb=get,list,watch --resource=pods" - }, - { - "title": "Create the ClusterRoleBinding", - "body": "Bind the ClusterRole to a ServiceAccount using `--serviceaccount=namespace:name`.", - "command": "kubectl create clusterrolebinding pod-reader-binding --clusterrole=pod-reader --serviceaccount=default:default" - } - ], - "setup_commands": [], - "validation": { - "commands": [ - { - "description": "ClusterRole 'pod-reader' exists", - "command": "kubectl get clusterrole pod-reader -o jsonpath='{.metadata.name}'", - "expected_output": "pod-reader", - "match": "exact" - }, - { - "description": "ClusterRole allows 'get' on pods", - "command": "kubectl get clusterrole pod-reader -o jsonpath='{.rules[0].verbs[*]}'", - "expected_output": "get", - "match": "contains" - }, - { - "description": "ClusterRoleBinding 'pod-reader-binding' exists", - "command": "kubectl get clusterrolebinding pod-reader-binding -o jsonpath='{.metadata.name}'", - "expected_output": "pod-reader-binding", - "match": "exact" - }, - { - "description": "Binding references the 'pod-reader' ClusterRole", - "command": "kubectl get clusterrolebinding pod-reader-binding -o jsonpath='{.roleRef.name}'", - "expected_output": "pod-reader", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete clusterrole pod-reader --ignore-not-found" - }, - { - "command": "kubectl delete clusterrolebinding pod-reader-binding --ignore-not-found" - } - ] - }, - { - "id": "node-label-selector", - "title": "Node Labels and nodeSelector", - "category": "Cluster Administration", - "difficulty": "Easy", - "type": "task", - "weight": 5, - "description": "## Node Labels and nodeSelector\n\nLabelling nodes lets you constrain which nodes a pod can be scheduled on, using `nodeSelector` in the pod spec.\n\n**Your task:**\n\n1. Add the label `disk=ssd` to node `k8s-lab`\n2. Create a Pod named `ssd-pod` using `nginx:alpine` that uses `nodeSelector` to target nodes with `disk=ssd`", - "hints": [ - { - "title": "Label the node", - "body": "Use `kubectl label node key=value`.", - "command": "kubectl label node k8s-lab disk=ssd --overwrite" - }, - { - "title": "Use nodeSelector in pod spec", - "body": "Add `nodeSelector: disk: ssd` under `spec` in the pod manifest.", - "command": "cat </dev/null || true" - }, - { - "command": "kubectl delete pod ssd-pod --ignore-not-found --grace-period=0 --force" - } - ] - }, - { - "id": "node-taint-toleration", - "title": "Taints and Tolerations", - "category": "Cluster Administration", - "difficulty": "Medium", - "type": "task", - "weight": 7, - "description": "## Taints and Tolerations\n\nTaints allow a node to **repel** pods. A toleration on a pod allows it to be scheduled onto a tainted node.\n\nThe node `k8s-lab` has been tainted with `env=gpu:NoSchedule`.\n\n**Your task:**\n\nCreate a Pod named `gpu-pod` using `nginx:alpine` that tolerates the taint `env=gpu:NoSchedule` so it can be scheduled on this node.\n\n```bash\nkubectl describe node k8s-lab | grep -A5 Taints\n```", - "hints": [ - { - "title": "Toleration structure", - "body": "Add a `tolerations` block under `spec`. Match `key`, `operator`, `value`, and `effect` to the node taint.", - "command": "cat </dev/null || true" - } - ], - "validation": { - "commands": [ - { - "description": "Node has taint env=gpu:NoSchedule", - "command": "kubectl get node k8s-lab -o jsonpath='{.spec.taints[*].key}'", - "expected_output": "env", - "match": "contains" - }, - { - "description": "Pod 'gpu-pod' has toleration for key 'env'", - "command": "kubectl get pod gpu-pod -o jsonpath='{.spec.tolerations[*].key}'", - "expected_output": "env", - "match": "contains" - }, - { - "description": "Pod 'gpu-pod' toleration effect is NoSchedule", - "command": "kubectl get pod gpu-pod -o jsonpath='{.spec.tolerations[*].effect}'", - "expected_output": "NoSchedule", - "match": "contains" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl taint nodes k8s-lab env=gpu:NoSchedule- 2>/dev/null || true" - }, - { - "command": "kubectl delete pod gpu-pod --ignore-not-found --grace-period=0 --force" - } - ] - }, - { - "id": "etcd-backup-mcq", - "title": "etcd Backup and Restore", - "category": "Cluster Administration", - "difficulty": "Hard", - "type": "mcq", - "weight": 5, - "description": "## etcd Backup and Restore\n\nIn the CKA exam, you may need to back up and restore an etcd cluster. Which command correctly creates an etcd snapshot?\n\n```bash\n# The etcdctl binary is available on the control-plane node.\n# Assume certificates are at /etc/kubernetes/pki/etcd/\n```", - "options": [ - { - "id": "a", - "text": "`etcdctl snapshot save /backup/etcd.db` (no flags needed — etcdctl auto-detects certs)" - }, - { - "id": "b", - "text": "`ETCDCTL_API=3 etcdctl snapshot save /backup/etcd.db --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key`" - }, - { - "id": "c", - "text": "`kubectl exec etcd-controlplane -- etcdctl backup --data-dir=/var/lib/etcd`" - }, - { - "id": "d", - "text": "`etcdctl snapshot save /backup/etcd.db --kubeconfig=/root/.kube/config`" - } - ], - "correct_option": "b", - "explanation": "etcd requires **TLS certificates** and explicit endpoint specification. You must set `ETCDCTL_API=3`, point to the etcd endpoint (`https://127.0.0.1:2379` on control-plane), and provide the CA cert, server cert, and key from `/etc/kubernetes/pki/etcd/`. To restore: `ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd.db --data-dir=/var/lib/etcd-restored`, then update the etcd static pod manifest to point to the new data dir.", - "hints": [ - { - "title": "etcdctl API version", - "body": "Always set `ETCDCTL_API=3`. etcdctl v2 and v3 APIs are different — the CKA exam uses v3. The certs live at `/etc/kubernetes/pki/etcd/`.", - "command": "ETCDCTL_API=3 etcdctl snapshot status /backup/etcd.db --write-out=table" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "resource-quota-ns", - "title": "ResourceQuota for a Namespace", - "category": "Cluster Administration", - "difficulty": "Medium", - "type": "task", - "weight": 7, - "description": "## ResourceQuota for a Namespace\n\nResourceQuotas enforce aggregate resource constraints per namespace, preventing any single team from consuming all cluster resources.\n\n**Your task:**\n\n1. Create a namespace named `team-quota`\n2. Create a **ResourceQuota** named `compute-quota` in `team-quota` that limits:\n - `pods`: `5`\n - `requests.cpu`: `\"1\"`\n - `requests.memory`: `\"500Mi\"`", - "hints": [ - { - "title": "Create the ResourceQuota", - "body": "Use `kubectl create resourcequota` with `--hard` flag or apply a YAML manifest.", - "command": "cat <` reverts to the previous revision. Use `--to-revision=N` to go to a specific revision number.", - "command": "kubectl rollout undo deployment/api-server" - } - ], - "setup_commands": [ - { - "command": "kubectl create deployment api-server --image=nginx:1.24 --replicas=2 2>/dev/null || true" - }, - { - "command": "kubectl rollout status deployment/api-server --timeout=60s" - }, - { - "command": "kubectl set image deployment/api-server nginx=nginx:broken-image" - } - ], - "validation": { - "commands": [ - { - "description": "Deployment 'api-server' image rolled back to nginx:1.24", - "command": "kubectl get deployment api-server -o jsonpath='{.spec.template.spec.containers[0].image}'", - "expected_output": "nginx:1.24", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment api-server --ignore-not-found" - } - ] - }, - { - "id": "cronjob-task", - "title": "Create a CronJob", - "category": "Workloads & Scheduling", - "difficulty": "Easy", - "type": "task", - "weight": 4, - "description": "## Create a CronJob\n\nCronJobs create Jobs on a repeating schedule, perfect for maintenance tasks like log rotation, backups, or report generation.\n\n**Your task:**\n\nCreate a CronJob named `date-printer` that:\n- Runs **every minute** (`* * * * *`)\n- Uses `busybox:1.36`\n- Executes: `date`\n- Has `successfulJobsHistoryLimit: 3`", - "hints": [ - { - "title": "CronJob manifest", - "body": "Use `kubectl create cronjob` and then patch with `successfulJobsHistoryLimit: 3`, or create a cronjob with a manifest. The schedule uses standard cron syntax.", - "command": "cat </dev/null || true" - } - ], - "validation": { - "commands": [ - { - "description": "Service 'frontend-np' exists", - "command": "kubectl get service frontend-np -o jsonpath='{.metadata.name}'", - "expected_output": "frontend-np", - "match": "exact" - }, - { - "description": "Service type is NodePort", - "command": "kubectl get service frontend-np -o jsonpath='{.spec.type}'", - "expected_output": "NodePort", - "match": "exact" - }, - { - "description": "NodePort is 30080", - "command": "kubectl get service frontend-np -o jsonpath='{.spec.ports[0].nodePort}'", - "expected_output": "30080", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment frontend --ignore-not-found" - }, - { - "command": "kubectl delete svc frontend-np --ignore-not-found" - } - ] - }, - { - "id": "dns-resolution-mcq", - "title": "Kubernetes DNS Resolution", - "category": "Services & Networking", - "difficulty": "Easy", - "type": "mcq", - "weight": 3, - "description": "## Kubernetes DNS Resolution\n\nA pod in namespace `frontend` needs to reach a service named `db-service` in namespace `backend`. Which DNS name correctly resolves to that service from within the cluster?", - "options": [ - { - "id": "a", - "text": "`db-service`" - }, - { - "id": "b", - "text": "`db-service.backend`" - }, - { - "id": "c", - "text": "`db-service.backend.svc.cluster.local`" - }, - { - "id": "d", - "text": "`backend.db-service.cluster.local`" - } - ], - "correct_option": "c", - "explanation": "The full Kubernetes DNS format is `..svc.`. The default cluster domain is `cluster.local`, so the FQDN is `db-service.backend.svc.cluster.local`. When pods are in the **same namespace**, just `db-service` works. Across namespaces, you need at minimum `db-service.backend` (short form). Option C is the fully-qualified name that always works regardless of the caller's namespace.", - "hints": [ - { - "title": "Kubernetes DNS format", - "body": "CoreDNS resolves: `..svc.cluster.local`. You can verify with: `kubectl exec -- nslookup `.", - "command": "kubectl exec -it -- nslookup db-service.backend.svc.cluster.local" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "storageclass-mcq", - "title": "StorageClass Reclaim Policies", - "category": "Storage", - "difficulty": "Easy", - "type": "mcq", - "weight": 3, - "description": "## StorageClass Reclaim Policies\n\nA PersistentVolumeClaim bound to a PV is deleted. The StorageClass has `reclaimPolicy: Retain`.\n\nWhat happens to the underlying PersistentVolume?", - "options": [ - { - "id": "a", - "text": "The PV is immediately deleted along with all data" - }, - { - "id": "b", - "text": "The PV remains but enters a `Released` state; the data is preserved and must be manually reclaimed" - }, - { - "id": "c", - "text": "The PV is automatically rebound to the next PVC that requests the same storage class" - }, - { - "id": "d", - "text": "The PV is converted to an emptyDir volume attached to the node" - } - ], - "correct_option": "b", - "explanation": "With `reclaimPolicy: Retain`, when a PVC is deleted the PV moves to `Released` state — the data is **preserved** on the storage backend, but the PV is not available for automatic rebinding. An admin must manually reclaim it (delete the PV, clean the storage, recreate the PV). Contrast with `reclaimPolicy: Delete`, which deletes both the PV object and the underlying storage asset automatically.", - "hints": [ - { - "title": "Reclaim policies", - "body": "Three policies exist: `Retain` (manual reclaim), `Delete` (auto-delete storage), `Recycle` (deprecated, basic scrub). Check with: `kubectl get storageclass`.", - "command": "kubectl get storageclass -o jsonpath='{range .items[*]}{.metadata.name}={.reclaimPolicy}{\"\\n\"}{end}'" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "emptydir-pod", - "title": "Shared emptyDir Volume Between Containers", - "category": "Storage", - "difficulty": "Easy", - "type": "task", - "weight": 5, - "description": "## Shared emptyDir Volume\n\nAn `emptyDir` volume is created when a Pod is assigned to a node and exists as long as the Pod runs. It is ideal for sharing data between containers in the same pod (e.g., sidecar patterns).\n\n**Your task:**\n\nCreate a Pod named `shared-data` with two containers:\n1. **writer** (`busybox:1.36`) — writes `hello-kube` to `/shared/message.txt` then sleeps\n2. **reader** (`busybox:1.36`) — reads and prints `/shared/message.txt` then sleeps\n\nBoth containers mount an `emptyDir` volume at `/shared`.", - "hints": [ - { - "title": "emptyDir shared volume manifest", - "body": "Define a single volume of type `emptyDir: {}` and mount it in both containers under the same path.", - "command": "cat < /shared/message.txt && sleep 3600\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n - name: reader\n image: busybox:1.36\n command: [\"sh\",\"-c\",\"sleep 5 && cat /shared/message.txt && sleep 3600\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n volumes:\n - name: shared-vol\n emptyDir: {}\nEOF" - } - ], - "setup_commands": [], - "validation": { - "commands": [ - { - "description": "Pod 'shared-data' exists", - "command": "kubectl get pod shared-data -o jsonpath='{.metadata.name}'", - "expected_output": "shared-data", - "match": "exact" - }, - { - "description": "Pod has 2 containers", - "command": "kubectl get pod shared-data -o jsonpath='{range .spec.containers[*]}{.name}{\"\\n\"}{end}'", - "expected_output": "writer", - "match": "contains" - }, - { - "description": "Volume type is emptyDir", - "command": "kubectl get pod shared-data -o jsonpath='{.spec.volumes[0].emptyDir}'", - "expected_output": "{}", - "match": "contains" - }, - { - "description": "Writer container mounts the shared volume", - "command": "kubectl get pod shared-data -o jsonpath='{.spec.containers[0].volumeMounts[0].mountPath}'", - "expected_output": "/shared", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete pod shared-data --ignore-not-found --grace-period=0 --force" - } - ] - }, - { - "id": "pod-security-context", - "title": "Pod Security Context", - "category": "Security", - "difficulty": "Medium", - "type": "task", - "weight": 6, - "description": "## Pod Security Context\n\nA security context defines privilege and access control settings for a pod or container.\n\n**Your task:**\n\nCreate a Pod named `secure-pod` using `busybox:1.36` (command: `sleep 3600`) with the following security settings:\n- `runAsUser: 1000`\n- `runAsNonRoot: true`\n- `allowPrivilegeEscalation: false`\n\n```bash\n# Verify after creation:\nkubectl get pod secure-pod -o jsonpath='{.spec.containers[0].securityContext}'\n```", - "hints": [ - { - "title": "securityContext structure", - "body": "Security context can be set at Pod level (`spec.securityContext`) or container level (`spec.containers[].securityContext`). Container-level settings override pod-level.", - "command": "cat </dev/null || true" - }, - { - "command": "kubectl patch deployment crash-app --type=json -p='[{\"op\":\"add\",\"path\":\"/spec/template/spec/containers/0/command\",\"value\":[\"/bin/sh\",\"-c\",\"exit 1\"]}]'" - } - ], - "validation": { - "commands": [ - { - "description": "Deployment 'crash-app' has 1 ready replica", - "command": "kubectl get deployment crash-app -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", - "expected_output": "1", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment crash-app --ignore-not-found" - } - ] - }, - { - "id": "endpoint-fix-task", - "title": "Fix a Broken Service Selector", - "category": "Troubleshooting", - "difficulty": "Medium", - "type": "task", - "weight": 7, - "description": "## Fix a Broken Service Selector\n\nA service `my-svc` has been created but no traffic reaches the pods. The pods are labelled `app=backend`, but the service selector is misconfigured.\n\n**Your task:**\n\nFix the `my-svc` service selector so it correctly targets the `backend` pods.\n\n```bash\n# Investigate:\nkubectl describe service my-svc\nkubectl get endpoints my-svc\nkubectl get pods --show-labels\n```", - "hints": [ - { - "title": "Identify the mismatch", - "body": "Check what label the pods actually carry, then compare it against the service selector.", - "command": "kubectl get pods --show-labels && kubectl get service my-svc -o jsonpath='{.spec.selector}'" - }, - { - "title": "Patch the service selector", - "body": "The pods carry `app=backend` but the service selector says `app=wrong-label`. Use `kubectl patch` to point the selector at the correct label.", - "command": "kubectl patch service my-svc --type=json -p='[{\"op\":\"replace\",\"path\":\"/spec/selector/app\",\"value\":\"backend\"}]'" - }, - { - "title": "Verify endpoints are populated", - "body": "After fixing the selector, Kubernetes will automatically update the endpoint slice. Confirm the service now has live pod addresses.", - "command": "kubectl get endpoints my-svc" - } - ], - "setup_commands": [ - { - "command": "kubectl create deployment backend --image=nginx:alpine --replicas=2 2>/dev/null || true" - }, - { - "command": "kubectl apply -f - </dev/null | grep -v '^$' || echo empty", - "expected_output": "empty", - "match": "not_contains" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment backend --ignore-not-found" - }, - { - "command": "kubectl delete svc my-svc --ignore-not-found" - } - ] - }, - { - "id": "ingress-task", - "title": "Create an Ingress Resource", - "category": "Services & Networking", - "difficulty": "Medium", - "type": "task", - "weight": 7, - "description": "## Create an Ingress Resource\n\nIngress exposes HTTP/HTTPS routes from outside the cluster to services within it. k3s includes Traefik as its default Ingress controller.\n\n**Your task:**\n\nA deployment `web-app` and service `web-svc` (port 80) already exist. Create an **Ingress** named `web-ingress` that:\n- Routes traffic for host `app.lab.local`\n- Path `/` → service `web-svc` on port `80`\n- Uses `pathType: Prefix`", - "hints": [ - { - "title": "Ingress manifest", - "body": "Use `networking.k8s.io/v1` for the Ingress API. Define `spec.rules[].host` and `spec.rules[].http.paths[]`.", - "command": "cat </dev/null || true" - }, - { - "command": "kubectl expose deployment web-app --name=web-svc --port=80 --target-port=80 2>/dev/null || true" - } - ], - "validation": { - "commands": [ - { - "description": "Ingress 'web-ingress' exists", - "command": "kubectl get ingress web-ingress -o jsonpath='{.metadata.name}'", - "expected_output": "web-ingress", - "match": "exact" - }, - { - "description": "Ingress host is app.lab.local", - "command": "kubectl get ingress web-ingress -o jsonpath='{.spec.rules[0].host}'", - "expected_output": "app.lab.local", - "match": "exact" - }, - { - "description": "Backend service is web-svc", - "command": "kubectl get ingress web-ingress -o jsonpath='{.spec.rules[0].http.paths[0].backend.service.name}'", - "expected_output": "web-svc", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment web-app --ignore-not-found" - }, - { - "command": "kubectl delete svc web-svc --ignore-not-found" - }, - { - "command": "kubectl delete ingress web-ingress --ignore-not-found" - } - ] - }, - { - "id": "networkpolicy-egress", - "title": "Egress NetworkPolicy", - "category": "Security", - "difficulty": "Hard", - "type": "task", - "weight": 8, - "description": "## Egress NetworkPolicy\n\nEgress NetworkPolicies control **outbound** traffic from pods. By default, all egress is allowed; once you apply an egress policy to a pod, only explicitly allowed egress is permitted.\n\n**Your task:**\n\nCreate a NetworkPolicy named `restrict-egress` in the `default` namespace that:\n- Applies to pods with label `role=isolated`\n- Allows **egress only to pods with label `role=allowed`** on port `80`\n- Blocks all other egress", - "hints": [ - { - "title": "Egress NetworkPolicy structure", - "body": "Set `spec.policyTypes: [Egress]` and define `spec.egress[]` with `to` and `ports`. Omitting a type means it's not affected.", - "command": "cat </dev/null || true" - } - ], - "validation": { - "commands": [ - { - "description": "LimitRange 'default-limits' exists in bounded-ns", - "command": "kubectl get limitrange default-limits -n bounded-ns -o jsonpath='{.metadata.name}'", - "expected_output": "default-limits", - "match": "exact" - }, - { - "description": "Default CPU limit is 200m", - "command": "kubectl get limitrange default-limits -n bounded-ns -o jsonpath='{.spec.limits[0].default.cpu}'", - "expected_output": "200m", - "match": "exact" - }, - { - "description": "Default memory limit is 256Mi", - "command": "kubectl get limitrange default-limits -n bounded-ns -o jsonpath='{.spec.limits[0].default.memory}'", - "expected_output": "256Mi", - "match": "exact" - } - ] - }, - "default_namespace": "bounded-ns", - "teardown_commands": [ - { - "command": "kubectl delete namespace bounded-ns --ignore-not-found --wait=false" - } - ] - }, - { - "id": "job-task", - "title": "Create a One-Shot Job", - "category": "Workloads & Scheduling", - "difficulty": "Easy", - "type": "task", - "weight": 4, - "description": "## Create a One-Shot Job\n\nA **Job** creates one or more pods and ensures a specified number of them successfully terminate. Unlike Deployments, Jobs are meant for finite tasks that run to completion.\n\n**Your task:**\n\nCreate a Job named `pi-job` that:\n- Uses `perl:5.34` image\n- Runs: `perl -Mbignum=bpi -wle 'print bpi(100)'` (computes π to 100 digits)\n- `completions: 1`\n- `restartPolicy: Never`", - "hints": [ - { - "title": "Job manifest", - "body": "Set `restartPolicy: Never` or `OnFailure` in the pod template spec (not at pod spec level).", - "command": "cat <` (before worker upgrade), `kubectl uncordon ` (after).", - "command": "kubeadm upgrade plan" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "container-logging-mcq", - "title": "Container Logging and Monitoring", - "category": "Troubleshooting", - "difficulty": "Easy", - "type": "mcq", - "weight": 3, - "description": "## Container Logging and Monitoring\n\nA pod `worker-abc` has crashed and restarted. You want to see the logs from the **previous** (crashed) container instance, not the current one.\n\nWhich command retrieves logs from the previously crashed container?", - "options": [ - { - "id": "a", - "text": "`kubectl logs worker-abc --all-containers`" - }, - { - "id": "b", - "text": "`kubectl logs worker-abc --previous`" - }, - { - "id": "c", - "text": "`kubectl logs worker-abc --restart`" - }, - { - "id": "d", - "text": "`kubectl describe pod worker-abc | grep log`" - } - ], - "correct_option": "b", - "explanation": "`kubectl logs --previous` (or `-p`) retrieves logs from the **terminated previous container** instance in a pod. This is critical for debugging CrashLoopBackOff scenarios where the container restarts before you can inspect its logs. `--all-containers` shows logs from all containers in the current run. `kubectl describe` shows events and status but not stdout/stderr logs.", - "hints": [ - { - "title": "Useful kubectl logs flags", - "body": "`--previous`/`-p`: previous container | `--follow`/`-f`: stream | `--since=1h`: last hour | `--tail=50`: last 50 lines | `--timestamps`: show timestamps", - "command": "kubectl logs worker-abc --previous --tail=50" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "multi-container-pod-basics", - "title": "Create a Multi-Container Pod", - "category": "Core Concepts", - "difficulty": "Easy", - "type": "task", - "weight": 5, - "description": "## Multi-Container Pods\n\nPods can contain multiple containers that share the same network namespace and storage volumes. This is often used for the 'sidecar' pattern.\n\n**Your task:**\n\nCreate a Pod named `web-sidecar` in the `default` namespace with two containers:\n1. Name: `app`, Image: `nginx:alpine`\n2. Name: `sidecar`, Image: `busybox:1.36`, Command: `sleep 3600`\n\nVerify it is running:\n```bash\nkubectl get pod web-sidecar\n```", - "hints": [ - { - "title": "Manifest Structure", - "body": "Your `spec.containers` array needs two items.", - "command": "cat < Tip: Use `kubectl logs log-generator | grep ERROR` to filter.", - "hints": [ - { - "title": "View pod logs", - "body": "Use kubectl logs to stream the output of a running container.", - "command": "kubectl logs log-generator" - }, - { - "title": "Filter logs", - "body": "Pipe kubectl logs into grep to find specific messages.", - "command": "kubectl logs log-generator | grep ERROR" - } - ], - "setup_commands": [ - { - "command": "kubectl run log-generator --image=busybox:1.36 --command -- sh -c 'echo \"ERROR: Database connection failed\" && sleep 3600'" - } - ], - "validation": { - "commands": [ - { - "description": "Pod log-generator emits ERROR: Database connection failed", - "command": "kubectl logs log-generator 2>/dev/null", - "expected_output": "ERROR: Database connection failed", - "match": "contains" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete pod log-generator --ignore-not-found --grace-period=0 --force" - } - ] - }, - { - "id": "edit-deployment-basics", - "title": "Updating Deployments", - "category": "Workloads", - "difficulty": "Easy", - "type": "task", - "weight": 4, - "description": "## Updating a Deployment\n\nA deployment named `web-app` is currently running `nginx:1.24`. A security vulnerability was found in this version.\n\n**Your task:**\n\nUpdate the image of the `web-app` deployment to `nginx:1.25` to patch the vulnerability. Wait for the new pods to become ready.\n\n```bash\n# Verify your change:\nkubectl describe deployment web-app | grep Image\n```", - "hints": [ - { - "title": "Set image command", - "body": "You can use `kubectl set image` to update a deployment's container image immediately.", - "command": "kubectl set image deployment/web-app nginx=nginx:1.25" - } - ], - "setup_commands": [ - { - "command": "kubectl create deployment web-app --image=nginx:1.24" - }, - { - "command": "kubectl rollout status deployment/web-app --timeout=60s" - } - ], - "validation": { - "commands": [ - { - "description": "Deployment uses image nginx:1.25", - "command": "kubectl get deployment web-app -o jsonpath='{.spec.template.spec.containers[0].image}'", - "expected_output": "nginx:1.25", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment web-app --ignore-not-found" - } - ] - }, - { - "id": "jsonpath-basics", - "title": "JSONPath Data Extraction", - "category": "Core Concepts", - "difficulty": "Medium", - "type": "task", - "weight": 5, - "description": "## Extracting Data with JSONPath\n\nKubectl allows you to extract specific fields from resource manifests using JSONPath, which is incredibly useful for scripting and automation.\n\n**Your task:**\n\nA deployment named `hidden-app` is running in the `default` namespace. Use `kubectl` with a JSONPath expression to output **only the container image name** used by that deployment.\n\n> Expected output: `nginx:1.25.3`", - "hints": [ - { - "title": "JSONPath syntax", - "body": "The image is located at `.spec.template.spec.containers[0].image`. Use -o jsonpath to extract it.", - "command": "kubectl get deployment hidden-app -o jsonpath='{.spec.template.spec.containers[0].image}'" - } - ], - "setup_commands": [ - { - "command": "kubectl create deployment hidden-app --image=nginx:1.25.3" - } - ], - "validation": { - "commands": [ - { - "description": "kubectl jsonpath returns the correct image", - "command": "kubectl get deployment hidden-app -o jsonpath='{.spec.template.spec.containers[0].image}'", - "expected_output": "nginx:1.25.3", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment hidden-app --ignore-not-found" - } - ] - }, - { - "id": "dry-run-manifest-basics", - "title": "Generating YAML Manifests", - "category": "Core Concepts", - "difficulty": "Easy", - "type": "task", - "weight": 4, - "description": "## Generating and Applying Manifests with Dry Run\n\nInstead of writing YAML from scratch, you can use `kubectl` to generate templates using `--dry-run=client -o yaml` and then apply them.\n\n**Your task:**\n\nUse `kubectl run` with `--dry-run=client -o yaml` to generate a Pod manifest for `web-pod` using image `httpd:alpine`, then pipe it directly to `kubectl apply -f -` to create the pod.\n\n```bash\nkubectl run web-pod --image=httpd:alpine --dry-run=client -o yaml | kubectl apply -f -\n```", - "hints": [ - { - "title": "Combine dry-run with apply", - "body": "Chain kubectl run --dry-run=client -o yaml with kubectl apply -f - using a pipe.", - "command": "kubectl run web-pod --image=httpd:alpine --dry-run=client -o yaml | kubectl apply -f -" - } - ], - "setup_commands": [], - "validation": { - "commands": [ - { - "description": "Pod 'web-pod' exists", - "command": "kubectl get pod web-pod -o jsonpath='{.metadata.name}'", - "expected_output": "web-pod", - "match": "exact" - }, - { - "description": "Pod uses httpd:alpine image", - "command": "kubectl get pod web-pod -o jsonpath='{.spec.containers[0].image}'", - "expected_output": "httpd:alpine", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete pod web-pod --ignore-not-found --grace-period=0 --force" - } - ] - }, - { - "id": "delete-by-label-basics", - "title": "Bulk Deletion by Label", - "category": "Core Concepts", - "difficulty": "Easy", - "type": "task", - "weight": 3, - "description": "## Bulk Deletion by Label\n\nYou can use label selectors (`-l`) to perform bulk operations on resources.\n\n**Your task:**\n\nThere are 5 pods running in the default namespace. Delete **only** the pods that have the label `env=dev`. Leave the `env=prod` pods running.\n\n```bash\n# Verify which pods are left:\nkubectl get pods --show-labels\n```", - "hints": [ - { - "title": "Delete with selector", - "body": "Use `kubectl delete pods` with the `-l` flag.", - "command": "kubectl delete pods -l env=dev" - } - ], - "setup_commands": [ - { - "command": "kubectl run dev-1 --image=nginx:alpine -l env=dev" - }, - { - "command": "kubectl run dev-2 --image=nginx:alpine -l env=dev" - }, - { - "command": "kubectl run dev-3 --image=nginx:alpine -l env=dev" - }, - { - "command": "kubectl run prod-1 --image=nginx:alpine -l env=prod" - }, - { - "command": "kubectl run prod-2 --image=nginx:alpine -l env=prod" - } - ], - "validation": { - "commands": [ - { - "description": "No 'env=dev' pods exist", - "command": "kubectl get pods -l env=dev --no-headers 2>/dev/null | wc -l | tr -d ' '", - "expected_output": "0", - "match": "exact" - }, - { - "description": "Prod pods are still running", - "command": "kubectl get pods -l env=prod --no-headers 2>/dev/null | wc -l | tr -d ' '", - "expected_output": "2", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete pods -l env=dev --ignore-not-found --grace-period=0 --force" - }, - { - "command": "kubectl delete pods -l env=prod --ignore-not-found --grace-period=0 --force" - } - ] - }, - { - "id": "kubectl-cp-basics", - "title": "Copy Files to Containers", - "category": "Troubleshooting", - "difficulty": "Medium", - "type": "task", - "weight": 4, - "description": "## Copy Files Between Containers and the Cluster\n\nThe `kubectl cp` command allows you to copy files and directories to and from running containers.\n\n**Your task:**\n\nA pod named `config-pod` is running with a file at `/src/config.json`. Copy it **into** the same pod at `/app/config.json` using `kubectl cp`.\n\n> Tip: `kubectl cp` syntax is `kubectl cp :`", - "hints": [ - { - "title": "Use kubectl cp", - "body": "First copy the file out from /src, then copy it back to /app, or use kubectl exec to verify.", - "command": "kubectl cp config-pod:/src/config.json /tmp/config.json && kubectl cp /tmp/config.json config-pod:/app/config.json" - } - ], - "setup_commands": [ - { - "command": "kubectl run config-pod --image=busybox:1.36 --command -- sleep 3600" - }, - { - "command": "kubectl wait --for=condition=Ready pod/config-pod --timeout=60s" - }, - { - "command": "kubectl exec config-pod -- sh -c 'mkdir -p /src && echo eyJzdGF0dXMiOiAib2sifQo= | base64 -d > /src/config.json'" - }, - { - "command": "kubectl exec config-pod -- mkdir -p /app" - } - ], - "validation": { - "commands": [ - { - "description": "File /app/config.json exists inside pod", - "command": "kubectl exec config-pod -- cat /app/config.json 2>/dev/null", - "expected_output": "{\"status\": \"ok\"}", - "match": "contains" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete pod config-pod --ignore-not-found --grace-period=0 --force" - }, - { - "command": "rm -f /tmp/config.json" - } - ] - }, - { - "id": "exec-command-basics", - "title": "Execute Commands in Pods", - "category": "Troubleshooting", - "difficulty": "Easy", - "type": "task", - "weight": 3, - "description": "## Execute Commands in Pods\n\nSometimes you need to run arbitrary commands inside a running container to debug or change state.\n\n**Your task:**\n\nA pod named `worker-pod` is running. Use `kubectl exec` to create an empty file at `/tmp/ready` inside the container.\n\n```bash\n# Verify the file was created:\nkubectl exec worker-pod -- ls /tmp/ready\n```", - "hints": [ - { - "title": "Use kubectl exec", - "body": "You can pass commands to the container by adding `--` after the pod name.", - "command": "kubectl exec worker-pod -- touch /tmp/ready" - } - ], - "setup_commands": [ - { - "command": "kubectl run worker-pod --image=busybox:1.36 --command -- sleep 3600" - }, - { - "command": "kubectl wait --for=condition=Ready pod/worker-pod --timeout=30s" - } - ], - "validation": { - "commands": [ - { - "description": "File /tmp/ready exists inside worker-pod", - "command": "kubectl exec worker-pod -- ls /tmp/ready 2>/dev/null", - "expected_output": "/tmp/ready", - "match": "contains" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete pod worker-pod --ignore-not-found --grace-period=0 --force" - } - ] - }, - { - "id": "cks-network-policy", - "title": "Default Deny Network Policy", - "category": "Network Security", - "difficulty": "Medium", - "type": "task", - "weight": 5, - "description": "## Network Policies\n\nIn a zero-trust architecture, you should deny all traffic by default and explicitly allow what is needed.\n\n**Your task:**\n\nCreate a NetworkPolicy named `default-deny-all` in the `default` namespace that denies all ingress and egress traffic for all pods in the namespace.\n\n```bash\n# Verify your policy:\nkubectl get networkpolicy default-deny-all\n```", - "hints": [ - { - "title": "Default Deny YAML", - "body": "Use a podSelector with an empty matchLabels `{}` to select all pods, and provide empty lists for ingress and egress.", - "command": "cat </dev/null", - "expected_output": "169.254.169.254/32", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete networkpolicy deny-metadata --ignore-not-found" - } - ] - }, - { - "id": "cks-rbac-clusterrole", - "title": "Cluster-Level RBAC", - "category": "Cluster Security", - "difficulty": "Medium", - "type": "task", - "weight": 5, - "description": "## Cluster Roles and Bindings\n\nSome resources, like Nodes, are cluster-scoped and cannot be accessed using normal Roles and RoleBindings.\n\n**Your task:**\n\n1. Create a ServiceAccount named `monitor-sa` in the `monitoring` namespace.\n2. Create a ClusterRole named `node-viewer` that grants `get`, `list`, and `watch` permissions on `nodes`.\n3. Create a ClusterRoleBinding named `monitor-node-binding` to bind the ClusterRole to the ServiceAccount.\n\n```bash\n# Verify your permissions:\nkubectl auth can-i list nodes --as=system:serviceaccount:monitoring:monitor-sa\n```", - "hints": [ - { - "title": "Imperative Commands", - "body": "Create the SA, then the ClusterRole, then the ClusterRoleBinding.", - "command": "kubectl create sa monitor-sa -n monitoring && kubectl create clusterrole node-viewer --verb=get,list,watch --resource=nodes && kubectl create clusterrolebinding monitor-node-binding --clusterrole=node-viewer --serviceaccount=monitoring:monitor-sa" - } - ], - "setup_commands": [ - { - "command": "kubectl create namespace monitoring --dry-run=client -o yaml | kubectl apply -f -" - } - ], - "validation": { - "commands": [ - { - "description": "monitor-sa exists in monitoring namespace", - "command": "kubectl get sa monitor-sa -n monitoring -o jsonpath='{.metadata.name}'", - "expected_output": "monitor-sa", - "match": "exact" - }, - { - "description": "ServiceAccount can list nodes", - "command": "kubectl auth can-i list nodes --as=system:serviceaccount:monitoring:monitor-sa", - "expected_output": "yes", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete sa monitor-sa -n monitoring --ignore-not-found" - }, - { - "command": "kubectl delete clusterrole node-viewer --ignore-not-found" - }, - { - "command": "kubectl delete clusterrolebinding monitor-node-binding --ignore-not-found" - } - ] - }, - { - "id": "cks-psa-namespace", - "title": "Pod Security Admission", - "category": "Cluster Security", - "difficulty": "Easy", - "type": "task", - "weight": 4, - "description": "## Pod Security Admission (PSA)\n\nPod Security Admission replaced PodSecurityPolicies (PSP) to enforce security standards at the namespace level.\n\n**Your task:**\n\nA namespace named `secure-workloads` already exists. Add the necessary label to this namespace to **enforce** the `restricted` pod security standard.\n\n```bash\n# Verify your label:\nkubectl get ns secure-workloads --show-labels\n```", - "hints": [ - { - "title": "Namespace Label", - "body": "Label the namespace with the `pod-security.kubernetes.io/enforce=restricted` key-value pair.", - "command": "kubectl label ns secure-workloads pod-security.kubernetes.io/enforce=restricted" - } - ], - "setup_commands": [ - { - "command": "kubectl create namespace secure-workloads --dry-run=client -o yaml | kubectl apply -f -" - } - ], - "validation": { - "commands": [ - { - "description": "Namespace secure-workloads is labeled", - "command": "kubectl get ns secure-workloads -o jsonpath='{.metadata.labels.pod-security\\.kubernetes\\.io/enforce}'", - "expected_output": "restricted", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete namespace secure-workloads --ignore-not-found --wait=false" - } - ] - }, - { - "id": "cks-mcq-runtime-security", - "title": "Runtime Security Tools", - "category": "System Hardening", - "difficulty": "Medium", - "type": "mcq", - "weight": 2, - "description": "Which of the following is an open-source tool specifically designed for **runtime security** in Kubernetes (e.g., detecting unexpected behavior or shell execution in running containers)?", - "options": [ - { - "id": "a", - "text": "Trivy" - }, - { - "id": "b", - "text": "Kube-bench" - }, - { - "id": "c", - "text": "Falco" - }, - { - "id": "d", - "text": "SonarQube" - } - ], - "correct_option": "c", - "explanation": "Falco is a CNCF incubating project that acts as a runtime security tool. It parses Linux system calls at runtime and asserts the stream against a powerful rules engine. Trivy is for image scanning, and kube-bench checks CIS benchmarks.", - "hints": [], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "cks-mcq-api-server", - "title": "API Server Authentication", - "category": "Cluster Setup", - "difficulty": "Easy", - "type": "mcq", - "weight": 2, - "description": "To secure the `kube-apiserver`, you should prevent unauthorized users from interacting with the cluster anonymously. Which flag is used to disable anonymous authentication on the API server?", - "options": [ - { - "id": "a", - "text": "--disable-anonymous=true" - }, - { - "id": "b", - "text": "--anonymous-auth=false" - }, - { - "id": "c", - "text": "--auth-mode=Node,RBAC" - }, - { - "id": "d", - "text": "--secure-port=6443" - } - ], - "correct_option": "b", - "explanation": "The `--anonymous-auth=false` flag instructs the kube-apiserver to reject any request that is not associated with a known user or service account. By default, it is enabled.", - "hints": [], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "cks-mcq-image-footprint", - "title": "Minimizing Image Footprint", - "category": "Supply Chain Security", - "difficulty": "Easy", - "type": "mcq", - "weight": 2, - "description": "Why is it highly recommended in the CKS exam to use base images like **Alpine Linux** or **Distroless** for your containerized applications?", - "options": [ - { - "id": "a", - "text": "They automatically encrypt data at rest" - }, - { - "id": "b", - "text": "They have a significantly reduced attack surface with fewer packages" - }, - { - "id": "c", - "text": "They automatically configure NetworkPolicies for the pod" - }, - { - "id": "d", - "text": "They guarantee that the container will not run as root" - } - ], - "correct_option": "b", - "explanation": "Minimal images like Alpine or Distroless contain only the bare minimum files and dependencies needed to run the application. This drastically reduces the attack surface and the number of potential CVEs.", - "hints": [], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "cks-mcq-psp-replacement", - "title": "Pod Security Evolution", - "category": "Cluster Security", - "difficulty": "Easy", - "type": "mcq", - "weight": 2, - "description": "PodSecurityPolicies (PSP) were completely removed from Kubernetes in version 1.25. What is the built-in native replacement that enforces Pod Security Standards at the namespace level?", - "options": [ - { - "id": "a", - "text": "NetworkPolicies" - }, - { - "id": "b", - "text": "OPA Gatekeeper" - }, - { - "id": "c", - "text": "Pod Security Admission (PSA)" - }, - { - "id": "d", - "text": "AppArmor Profiles" - } - ], - "correct_option": "c", - "explanation": "Pod Security Admission (PSA) is the built-in admission controller that evaluates Pods against the predefined Pod Security Standards (Privileged, Baseline, and Restricted) based on namespace labels.", - "hints": [], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "cks-mcq-kubelet-auth", - "title": "Securing the Kubelet", - "category": "Cluster Setup", - "difficulty": "Medium", - "type": "mcq", - "weight": 2, - "description": "The kubelet exposes its own API on port 10250, which can allow an attacker to run `exec` commands on pods if left unsecured. Which configuration setting in the kubelet config file ensures unauthenticated requests are rejected?", - "options": [ - { - "id": "a", - "text": "authorization.mode: Webhook" - }, - { - "id": "b", - "text": "readOnlyPort: 0" - }, - { - "id": "c", - "text": "authentication.anonymous.enabled: false" - }, - { - "id": "d", - "text": "protectKernelDefaults: true" - } - ], - "correct_option": "c", - "explanation": "Setting `authentication.anonymous.enabled: false` ensures that the kubelet will reject any API requests that do not present a valid client certificate or bearer token.", - "hints": [], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "cks-readonly-filesystem", - "title": "Read-Only Root Filesystem", - "category": "Workload Security", - "difficulty": "Easy", - "type": "task", - "weight": 4, - "description": "## Read-Only Root Filesystem\n\nSetting `readOnlyRootFilesystem: true` prevents any process inside the container from writing to the container's root filesystem layer, significantly reducing the blast radius of a compromised container.\n\n**Your task:**\n\nCreate a Pod named `readonly-pod` using `busybox:1.36` (command: `sleep 3600`) with:\n- `readOnlyRootFilesystem: true` in the container's `securityContext`\n- An `emptyDir` volume mounted at `/tmp` to provide writable scratch space\n\n```bash\n# Verify:\nkubectl get pod readonly-pod -o jsonpath='{.spec.containers[0].securityContext.readOnlyRootFilesystem}'\n```", - "hints": [ - { - "title": "readOnlyRootFilesystem with emptyDir", - "body": "Set `readOnlyRootFilesystem: true` in the container's `securityContext`. Mount an `emptyDir` volume at `/tmp` so the process has a writable location if needed.", - "command": "cat </dev/null" - }, - { - "command": "kubectl create deployment tls-app --image=nginx:alpine --replicas=1 2>/dev/null || true" - }, - { - "command": "kubectl expose deployment tls-app --name=tls-svc --port=80 --target-port=80 2>/dev/null || true" - } - ], - "validation": { - "commands": [ - { - "description": "Secret 'app-tls' is of type kubernetes.io/tls", - "command": "kubectl get secret app-tls -o jsonpath='{.type}'", - "expected_output": "kubernetes.io/tls", - "match": "exact" - }, - { - "description": "Ingress 'tls-ingress' exists", - "command": "kubectl get ingress tls-ingress -o jsonpath='{.metadata.name}'", - "expected_output": "tls-ingress", - "match": "exact" - }, - { - "description": "Ingress uses TLS secret 'app-tls'", - "command": "kubectl get ingress tls-ingress -o jsonpath='{.spec.tls[0].secretName}'", - "expected_output": "app-tls", - "match": "exact" - }, - { - "description": "Ingress routes host secure.lab.local", - "command": "kubectl get ingress tls-ingress -o jsonpath='{.spec.rules[0].host}'", - "expected_output": "secure.lab.local", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete ingress tls-ingress --ignore-not-found" - }, - { - "command": "kubectl delete secret app-tls --ignore-not-found" - }, - { - "command": "kubectl delete svc tls-svc --ignore-not-found" - }, - { - "command": "kubectl delete deployment tls-app --ignore-not-found" - }, - { - "command": "rm -f /tmp/tls.crt /tmp/tls.key" - } - ] - }, - { - "id": "cks-image-pull-secret", - "title": "Private Registry Image Pull Secret", - "category": "Supply Chain Security", - "difficulty": "Easy", - "type": "task", - "weight": 4, - "description": "## Private Registry Image Pull Secret\n\nIn production, images are stored in private registries that require authentication. Kubernetes uses `imagePullSecrets` to securely store registry credentials and inject them at image pull time.\n\n**Your task:**\n\n1. Create a Docker registry Secret named `registry-creds` for registry `registry.company.com` with:\n - Username: `ci-bot`\n - Password: `s3cr3t-token`\n - Email: `ci@company.com`\n2. Create a Pod named `private-pod` using `nginx:alpine` that references `registry-creds` as an `imagePullSecret`\n\n```bash\n# Verify:\nkubectl get pod private-pod -o jsonpath='{.spec.imagePullSecrets[0].name}'\n```", - "hints": [ - { - "title": "Create a docker-registry Secret", - "body": "Use `kubectl create secret docker-registry` with the four required flags.", - "command": "kubectl create secret docker-registry registry-creds \\\n --docker-server=registry.company.com \\\n --docker-username=ci-bot \\\n --docker-password=s3cr3t-token \\\n --docker-email=ci@company.com" - }, - { - "title": "Reference imagePullSecrets in a Pod", - "body": "Add `spec.imagePullSecrets[].name` to the pod spec with the name of your secret.", - "command": "cat <`.", - "command": "kubectl explain runtimeclass\nkubectl explain pod.spec.runtimeClassName" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "cks-mcq-audit-policy", - "title": "Kubernetes Audit Logging Levels", - "category": "Cluster Setup", - "difficulty": "Medium", - "type": "mcq", - "weight": 3, - "description": "## Kubernetes Audit Logging\n\nYou are writing an audit policy for the kube-apiserver. You need to capture the **complete request AND response body** for all write operations on `secrets` in any namespace, so you have a full record of every secret modification.\n\nWhich audit level must you specify for these events?", - "options": [ - { - "id": "a", - "text": "`None` — discard all events matching this rule; nothing is logged" - }, - { - "id": "b", - "text": "`Metadata` — logs request metadata (user, verb, resource, timestamp) but not the request or response body" - }, - { - "id": "c", - "text": "`Request` — logs metadata and the request body, but not the response body" - }, - { - "id": "d", - "text": "`RequestResponse` — logs metadata, the full request body, and the full response body" - } - ], - "correct_option": "d", - "explanation": "Kubernetes audit levels in ascending verbosity: **None → Metadata → Request → RequestResponse**. `RequestResponse` is required when you need the complete picture — what was sent and what the API server returned. This is most important for sensitive resources like Secrets. The audit policy file is passed to kube-apiserver via `--audit-policy-file`. Note that `RequestResponse` is expensive; use `Metadata` for most resources.", - "hints": [ - { - "title": "Audit policy levels", - "body": "The four levels: `None` (discard), `Metadata` (headers only), `Request` (+ request body), `RequestResponse` (+ response body). Configure via `--audit-policy-file` and `--audit-log-path` on the kube-apiserver.", - "command": "# Example rule targeting Secrets at RequestResponse level:\n# - level: RequestResponse\n# resources:\n# - group: \"\"\n# resources: [\"secrets\"]" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "cks-runtime-class", - "title": "RuntimeClass for Sandboxed Workloads", - "category": "System Hardening", - "difficulty": "Medium", - "type": "task", - "weight": 5, - "description": "## RuntimeClass for Stronger Workload Isolation\n\nKubernetes `RuntimeClass` lets you select an alternative container runtime (such as gVisor's `runsc`) for specific pods, providing stronger kernel-level isolation than the default `runc`.\n\n**Your task:**\n\n1. Create a `RuntimeClass` named `gvisor` with `handler: runsc`.\n2. Create a Pod named `sandbox-pod` using the `nginx:alpine` image that references this RuntimeClass via `spec.runtimeClassName: gvisor`.\n\n```bash\n# Verify the RuntimeClass:\nkubectl get runtimeclass gvisor\n# Verify the pod spec:\nkubectl get pod sandbox-pod -o jsonpath='{.spec.runtimeClassName}'\n```\n\n> **Note:** The pod may not reach `Running` state if `runsc` is not installed on the node — that is expected in this lab. Validation only checks the API object configuration.", - "hints": [ - { - "title": "Create the RuntimeClass", - "body": "Use a manifest with `apiVersion: node.k8s.io/v1`, `kind: RuntimeClass`, and a `handler` field.", - "command": "cat < /etc/kubernetes/audit-policy.yaml\napiVersion: audit.k8s.io/v1\nkind: Policy\nrules:\n- level: RequestResponse\n resources:\n - group: \"\"\n resources: [\"secrets\"]\n- level: Metadata\n verbs: [\"get\", \"list\", \"watch\"]\n resources:\n - group: \"\"\n resources: [\"pods\"]\n- level: None\n userGroups: [\"system:masters\"]\n- level: Metadata\nEOF" - } - ], - "setup_commands": [ - { - "command": "mkdir -p /etc/kubernetes" - } - ], - "validation": { - "commands": [ - { - "description": "Audit policy file is valid YAML with kind: Policy", - "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); print(p.get('kind',''))\"", - "expected_output": "Policy", - "match": "exact" - }, - { - "description": "Rule 1: level is RequestResponse and targets secrets", - "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); r=p['rules'][0]; print('ok' if r['level']=='RequestResponse' and any('secrets' in x.get('resources',[]) for x in r.get('resources',[])) else 'fail')\"", - "expected_output": "ok", - "match": "exact" - }, - { - "description": "Rule 2: level is Metadata, targets pods, verbs include get/list/watch", - "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); print('ok' if any(r.get('level')=='Metadata' and any('pods' in x.get('resources',[]) for x in r.get('resources',[])) and set(r.get('verbs',[])) >= {'get','list','watch'} for r in p['rules']) else 'fail')\"", - "expected_output": "ok", - "match": "exact" - }, - { - "description": "Rule 3: level is None for system:masters group", - "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); print('ok' if any(r.get('level')=='None' and 'system:masters' in r.get('userGroups',[]) for r in p['rules']) else 'fail')\"", - "expected_output": "ok", - "match": "exact" - }, - { - "description": "Rule 4: catch-all is level Metadata with no other selectors", - "command": "python3 -c \"import yaml; p=yaml.safe_load(open('/etc/kubernetes/audit-policy.yaml')); last=p['rules'][-1]; print('ok' if set(last.keys())=={'level'} and last['level']=='Metadata' else 'fail')\"", - "expected_output": "ok", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "rm -f /etc/kubernetes/audit-policy.yaml" - } - ] - }, - { - "id": "cks-egress-namespace", - "title": "Restrict Egress to Namespace", - "category": "Network Security", - "difficulty": "Hard", - "type": "task", - "weight": 6, - "description": "## Namespace-Scoped Egress Network Policy\n\nFine-grained network policies can restrict pod traffic so that only pods within a specific namespace can communicate with each other.\n\n**Your task:**\n\nA namespace `frontend` and a namespace `backend` already exist. Create a NetworkPolicy named `allow-backend-only` in the `frontend` namespace that:\n\n- Applies to **all pods** in the `frontend` namespace\n- **Allows egress only** to pods in the `backend` namespace (matched by `namespaceSelector`)\n- **Denies all other egress** traffic\n\n```bash\n# Verify:\nkubectl get networkpolicy allow-backend-only -n frontend\n```", - "hints": [ - { - "title": "namespaceSelector in Egress rule", - "body": "Use `spec.policyTypes: [Egress]` with an empty egress rule except for a `namespaceSelector` that matches the `backend` namespace label.", - "command": "cat </dev/null || true" - }, - { - "command": "kubectl rollout status deployment/myapp --timeout=60s" - } - ], - "validation": { - "commands": [ - { - "description": "Deployment 'myapp' exists", - "command": "kubectl get deployment myapp -o jsonpath='{.metadata.name}'", - "expected_output": "myapp", - "match": "exact" - }, - { - "description": "Annotation 'contact' is ops@company.com", - "command": "kubectl get deployment myapp -o jsonpath='{.metadata.annotations.contact}'", - "expected_output": "ops@company.com", - "match": "exact" - }, - { - "description": "Annotation 'reviewed' is true", - "command": "kubectl get deployment myapp -o jsonpath='{.metadata.annotations.reviewed}'", - "expected_output": "true", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete deployment myapp --ignore-not-found" - } - ] - }, - { - "id": "init-container-task-basics", - "title": "Pod with Init Container", - "category": "Core Concepts", - "difficulty": "Medium", - "type": "task", - "weight": 5, - "description": "## Pod with Init Container\n\nInit containers run **before** the main application containers start. Each init container must complete successfully before the next one begins, and all must finish before the main containers are started. They are ideal for setup tasks like seeding config files or waiting for a dependency.\n\n**Your task:**\n\nCreate a Pod named `init-demo` with:\n- An **init container** named `setup` using `busybox:1.36` that writes `ready` into `/shared/status.txt`\n- A **main container** named `app` using `busybox:1.36` with command `sleep 3600`\n- Both containers mount an `emptyDir` volume at `/shared`\n\n```bash\n# Verify the init container wrote the file:\nkubectl exec init-demo -- cat /shared/status.txt\n```", - "hints": [ - { - "title": "Pod manifest with initContainers", - "body": "Define `spec.initContainers[]` above `spec.containers[]`. Both containers share the emptyDir volume mounted at /shared.", - "command": "cat < /shared/status.txt\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n containers:\n - name: app\n image: busybox:1.36\n command: [\"sleep\", \"3600\"]\n volumeMounts:\n - name: shared-vol\n mountPath: /shared\n volumes:\n - name: shared-vol\n emptyDir: {}\nEOF" - } - ], - "setup_commands": [], - "validation": { - "commands": [ - { - "description": "Pod 'init-demo' is Running", - "command": "kubectl get pod init-demo -o jsonpath='{.status.phase}'", - "expected_output": "Running", - "match": "exact" - }, - { - "description": "Init container 'setup' completed with exit code 0", - "command": "kubectl get pod init-demo -o jsonpath='{.status.initContainerStatuses[0].state.terminated.exitCode}'", - "expected_output": "0", - "match": "exact" - }, - { - "description": "File /shared/status.txt contains 'ready'", - "command": "kubectl exec init-demo -- cat /shared/status.txt 2>/dev/null", - "expected_output": "ready", - "match": "contains" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete pod init-demo --ignore-not-found --grace-period=0 --force" - } - ] - }, - { - "id": "restart-policy-mcq", - "title": "Pod Restart Policies", - "category": "Core Concepts", - "difficulty": "Easy", - "type": "mcq", - "weight": 3, - "description": "## Pod Restart Policies\n\nA pod runs a one-time database migration script. If the script **fails** (non-zero exit), the container should be restarted. If it **succeeds** (exit 0), it should **not** be restarted.\n\nWhich `restartPolicy` should be set in the pod spec?", - "options": [ - { - "id": "a", - "text": "`Always` — restarts the container regardless of exit code (this is the default)" - }, - { - "id": "b", - "text": "`OnFailure` — restarts only if the container exits with a non-zero code" - }, - { - "id": "c", - "text": "`Never` — the container is never restarted, even on failure" - }, - { - "id": "d", - "text": "`OnCompletion` — restarts the container only after it completes successfully" - } - ], - "correct_option": "b", - "explanation": "`OnFailure` is the right policy for batch workloads like migration scripts — Kubernetes restarts the container on any non-zero exit code but leaves it alone after a clean exit (code 0). `Always` (the default for Deployment pods) would keep restarting even after success, creating an infinite restart loop. `Never` means no automatic recovery on failure. `OnCompletion` does not exist in Kubernetes — the three valid values are `Always`, `OnFailure`, and `Never`. Jobs automatically use `OnFailure` by default.", - "hints": [ - { - "title": "restartPolicy values", - "body": "Three valid values: `Always` (default for Pods), `OnFailure` (for Jobs/batch), `Never` (run-once, no retry). Check with `kubectl explain pod.spec.restartPolicy`.", - "command": "kubectl explain pod.spec.restartPolicy" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "kubectl-output-format-mcq", - "title": "kubectl Output Formats", - "category": "Core Concepts", - "difficulty": "Easy", - "type": "mcq", - "weight": 2, - "description": "## kubectl Output Formats\n\nYou need to retrieve the **full live manifest** of a running deployment named `frontend` in YAML format so you can save it to a file for version control.\n\nWhich command produces the correct output?", - "options": [ - { - "id": "a", - "text": "`kubectl describe deployment frontend` — prints a human-readable summary with events" - }, - { - "id": "b", - "text": "`kubectl get deployment frontend -o yaml` — prints the full manifest in YAML format" - }, - { - "id": "c", - "text": "`kubectl export deployment frontend` — exports a portable manifest (removed in k8s 1.18)" - }, - { - "id": "d", - "text": "`kubectl manifest deployment frontend` — a built-in command for generating manifests" - } - ], - "correct_option": "b", - "explanation": "`kubectl get -o yaml` (or `--output=yaml`) prints the full live manifest as YAML, including all managed fields. `kubectl describe` gives a human-readable summary with events — not a re-applicable YAML manifest. `kubectl export` was removed in Kubernetes 1.18. `kubectl manifest` does not exist. Use `-o json` for JSON output, or `-o jsonpath='...'` to extract specific fields. To save to a file: `kubectl get deployment frontend -o yaml > frontend.yaml`.", - "hints": [ - { - "title": "Output format flags", - "body": "The `-o` / `--output` flag controls format: `-o yaml`, `-o json`, `-o wide` (extra columns), `-o name` (just the resource name), `-o jsonpath='...'` (field extraction).", - "command": "kubectl get deployment frontend -o yaml" - } - ], - "setup_commands": [], - "default_namespace": "default", - "teardown_commands": [] - }, - { - "id": "pod-disruption-budget-task", - "title": "Configure a PodDisruptionBudget", - "category": "Cluster Administration", - "difficulty": "Medium", - "type": "task", - "weight": 6, - "description": "## PodDisruptionBudget\n\nA **PodDisruptionBudget (PDB)** limits the number of pods of a replicated application that are down simultaneously during voluntary disruptions (node drains, cluster upgrades). It is a critical CKA topic.\n\n**Your task:**\n\nA Deployment named `pdb-app` with **3 replicas** is already running. Create a **PodDisruptionBudget** named `web-pdb` in the `default` namespace that:\n- Applies to pods with the label `app=pdb-app`\n- Ensures **at least 2 pods** are always available (`minAvailable: 2`)\n\n```bash\n# Verify:\nkubectl get pdb web-pdb\n```", - "hints": [ - { - "title": "PodDisruptionBudget manifest", - "body": "Use `spec.minAvailable` (integer or percentage) and `spec.selector.matchLabels` to target the right pods.", - "command": "cat </dev/null || true" - }, - { - "command": "kubectl rollout status deployment/pdb-app --timeout=90s" - } - ], - "validation": { - "commands": [ - { - "description": "PDB 'web-pdb' exists", - "command": "kubectl get pdb web-pdb -o jsonpath='{.metadata.name}'", - "expected_output": "web-pdb", - "match": "exact" - }, - { - "description": "minAvailable is 2", - "command": "kubectl get pdb web-pdb -o jsonpath='{.spec.minAvailable}'", - "expected_output": "2", - "match": "exact" - }, - { - "description": "PDB selector targets app=pdb-app", - "command": "kubectl get pdb web-pdb -o jsonpath='{.spec.selector.matchLabels.app}'", - "expected_output": "pdb-app", - "match": "exact" - }, - { - "description": "Deployment 'pdb-app' has 3 replicas", - "command": "kubectl get deployment pdb-app -o jsonpath='{.spec.replicas}'", - "expected_output": "3", - "match": "exact" - } - ] - }, - "default_namespace": "default", - "teardown_commands": [ - { - "command": "kubectl delete pdb web-pdb --ignore-not-found" - }, - { - "command": "kubectl delete deployment pdb-app --ignore-not-found" - } - ] - }, - { - "id": "priority-class-task", - "title": "Pod Priority and PriorityClass", - "category": "Workloads & Scheduling", - "difficulty": "Medium", - "type": "task", - "weight": 6, - "description": "## Pod Priority and PriorityClass\n\n**PriorityClasses** assign a numeric priority to pods. When cluster resources are scarce, the scheduler preempts lower-priority pods to make room for higher-priority ones. This is a CKA exam topic.\n\n**Your task:**\n\n1. Create a **PriorityClass** named `high-priority` with:\n - `value: 1000000`\n - `globalDefault: false`\n - `description: \"High priority workloads\"`\n2. Create a Pod named `critical-pod` using `nginx:alpine` that references the `high-priority` PriorityClass via `spec.priorityClassName`\n\n```bash\n# Verify:\nkubectl get priorityclass high-priority\nkubectl get pod critical-pod -o jsonpath='{.spec.priorityClassName}'\n```", - "hints": [ - { - "title": "Create the PriorityClass", - "body": "PriorityClass is a cluster-scoped resource (not namespaced). Higher value = higher priority.", - "command": "cat <