From d42aa779b128d532a964a4909dd9a8763c21c95c Mon Sep 17 00:00:00 2001 From: Abhinav Sinha Date: Sat, 20 Jun 2026 23:29:59 +0530 Subject: [PATCH] feat(scenarios): add 10 new `Kubernetes Basics` scenarios Signed-off-by: Abhinav Sinha --- scenarios/bundles/1-k8s-basics.json | 12 +++- scenarios/data/deployment-strategy-mcq.json | 44 ++++++++++++++ scenarios/data/field-selector-basics.json | 60 +++++++++++++++++++ scenarios/data/kubectl-diff-basics.json | 65 +++++++++++++++++++++ scenarios/data/kubectl-explain-basics.json | 50 ++++++++++++++++ scenarios/data/kubectl-patch-basics.json | 51 ++++++++++++++++ scenarios/data/node-cordon-basics.json | 44 ++++++++++++++ scenarios/data/pod-env-vars-inline.json | 50 ++++++++++++++++ scenarios/data/pod-node-assignment-mcq.json | 44 ++++++++++++++ scenarios/data/sort-by-basics.json | 63 ++++++++++++++++++++ scenarios/data/static-pods-mcq.json | 44 ++++++++++++++ 11 files changed, 526 insertions(+), 1 deletion(-) create mode 100644 scenarios/data/deployment-strategy-mcq.json create mode 100644 scenarios/data/field-selector-basics.json create mode 100644 scenarios/data/kubectl-diff-basics.json create mode 100644 scenarios/data/kubectl-explain-basics.json create mode 100644 scenarios/data/kubectl-patch-basics.json create mode 100644 scenarios/data/node-cordon-basics.json create mode 100644 scenarios/data/pod-env-vars-inline.json create mode 100644 scenarios/data/pod-node-assignment-mcq.json create mode 100644 scenarios/data/sort-by-basics.json create mode 100644 scenarios/data/static-pods-mcq.json diff --git a/scenarios/bundles/1-k8s-basics.json b/scenarios/bundles/1-k8s-basics.json index 180a41a..9274db2 100644 --- a/scenarios/bundles/1-k8s-basics.json +++ b/scenarios/bundles/1-k8s-basics.json @@ -36,6 +36,16 @@ "annotate-resource-basics", "init-container-task-basics", "restart-policy-mcq", - "kubectl-output-format-mcq" + "kubectl-output-format-mcq", + "kubectl-explain-basics", + "kubectl-patch-basics", + "field-selector-basics", + "sort-by-basics", + "kubectl-diff-basics", + "node-cordon-basics", + "static-pods-mcq", + "deployment-strategy-mcq", + "pod-node-assignment-mcq", + "pod-env-vars-inline" ] } diff --git a/scenarios/data/deployment-strategy-mcq.json b/scenarios/data/deployment-strategy-mcq.json new file mode 100644 index 0000000..0c6aa2c --- /dev/null +++ b/scenarios/data/deployment-strategy-mcq.json @@ -0,0 +1,44 @@ +{ + "id": "deployment-strategy-mcq", + "title": "Deployment Update Strategies", + "category": "Workloads", + "difficulty": "Medium", + "type": "mcq", + "weight": 3, + "description": "## Deployment Update Strategies\n\nA production Deployment is being updated. The team requires that **at least 3 pods are always available** during the rollout, and at most **5 pods can exist at any one time** (the Deployment has 4 replicas).\n\nWhich `strategy` configuration achieves this?\n\n```yaml\nspec:\n replicas: 4\n strategy:\n type: RollingUpdate\n rollingUpdate:\n maxUnavailable: ???\n maxSurge: ???\n```", + "options": [ + { + "id": "a", + "text": "`maxUnavailable: 1`, `maxSurge: 1` — at most 1 pod unavailable (3 always up), at most 5 pods total" + }, + { + "id": "b", + "text": "`maxUnavailable: 0`, `maxSurge: 0` — ensures zero disruption with no extra pods" + }, + { + "id": "c", + "text": "`maxUnavailable: 2`, `maxSurge: 2` — allows 2 pods down (2 available) and 6 total pods" + }, + { + "id": "d", + "text": "`maxUnavailable: 4`, `maxSurge: 1` — replaces all pods at once before adding new ones" + } + ], + "correct_option": "a", + "explanation": "With 4 replicas, `maxUnavailable: 1` means at most 1 pod can be unavailable → minimum 3 pods always running. `maxSurge: 1` means at most 1 extra pod can be created → maximum 5 pods total (4+1). This satisfies both constraints. Option B (`maxUnavailable: 0, maxSurge: 0`) is invalid — at least one of them must be non-zero. Option C allows only 2 available pods (violates the minimum-3 requirement). Option D would take all existing pods offline before any new ones are ready.", + "hints": [ + { + "title": "Understanding maxUnavailable and maxSurge", + "body": "maxUnavailable: how many pods can be unavailable during an update (can be a count or %). maxSurge: how many extra pods above the desired count can exist simultaneously.", + "command": "kubectl explain deployment.spec.strategy.rollingUpdate" + }, + { + "title": "Check the current strategy on a deployment", + "body": "Use jsonpath to inspect the rolling update config of any existing deployment.", + "command": "kubectl get deployment -o jsonpath='{.spec.strategy.rollingUpdate}'" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/field-selector-basics.json b/scenarios/data/field-selector-basics.json new file mode 100644 index 0000000..f16562e --- /dev/null +++ b/scenarios/data/field-selector-basics.json @@ -0,0 +1,60 @@ +{ + "id": "field-selector-basics", + "title": "Filtering Resources with Field Selectors", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Filtering Resources with Field Selectors\n\nField selectors let you filter Kubernetes resources by the **value of specific object fields** — similar to how label selectors work but targeting any field in the resource spec or status.\n\nCommon use cases:\n- Find all pods in a specific phase: `kubectl get pods --field-selector=status.phase=Running`\n- Find resources in a specific namespace: `kubectl get pods --field-selector=metadata.namespace=kube-system`\n\n**Your task:**\n\nSeveral pods have been created for you — some Running, some in a Failed state.\n\n1. Use a field selector to list only the **Running** pods and count them. Save the count to `/tmp/running-count.txt`\n2. Use a field selector to list all pods **not** in the `default` namespace (filter by `metadata.namespace!=default`)\n\n```bash\n# Count running pods:\nkubectl get pods --field-selector=status.phase=Running --no-headers | wc -l\n```", + "hints": [ + { + "title": "Field selector syntax", + "body": "Use `--field-selector=field.path=value` to filter. Multiple conditions are comma-separated. Supported operators: `=`, `==`, `!=`.", + "command": "kubectl get pods --field-selector=status.phase=Running" + }, + { + "title": "Save running pod count to a file", + "body": "Pipe the result through wc -l and redirect to the file.", + "command": "kubectl get pods --field-selector=status.phase=Running --no-headers | wc -l | tr -d ' ' > /tmp/running-count.txt" + } + ], + "setup_commands": [ + { + "command": "kubectl run alpha --image=nginx:alpine 2>/dev/null || true" + }, + { + "command": "kubectl run beta --image=nginx:alpine 2>/dev/null || true" + }, + { + "command": "kubectl run crash-pod --image=busybox:1.36 --restart=Never -- /bin/false 2>/dev/null || true" + }, + { + "command": "kubectl rollout status deployment/alpha --timeout=30s 2>/dev/null || true" + } + ], + "validation": { + "commands": [ + { + "description": "File /tmp/running-count.txt exists and has a numeric value", + "command": "cat /tmp/running-count.txt", + "expected_output": "^[0-9]+$", + "match": "regex" + }, + { + "description": "The saved count matches the actual number of Running pods", + "command": "LIVE=$(kubectl get pods --field-selector=status.phase=Running --no-headers 2>/dev/null | wc -l | tr -d ' '); SAVED=$(cat /tmp/running-count.txt 2>/dev/null | tr -d ' '); [ \"$LIVE\" = \"$SAVED\" ] && echo 'match' || echo 'mismatch'", + "expected_output": "match", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod alpha beta crash-pod --ignore-not-found --grace-period=0 --force" + }, + { + "command": "rm -f /tmp/running-count.txt" + } + ] +} diff --git a/scenarios/data/kubectl-diff-basics.json b/scenarios/data/kubectl-diff-basics.json new file mode 100644 index 0000000..3f1a24b --- /dev/null +++ b/scenarios/data/kubectl-diff-basics.json @@ -0,0 +1,65 @@ +{ + "id": "kubectl-diff-basics", + "title": "Previewing Changes with kubectl diff", + "category": "Core Concepts", + "difficulty": "Medium", + "type": "task", + "weight": 4, + "description": "## Previewing Changes with `kubectl diff`\n\n`kubectl diff` compares a local manifest file against the **live state** of a resource in the cluster, showing exactly what would change if you applied it — without actually making any changes. This is a safe way to review updates before rolling them out.\n\n**Your task:**\n\nA Deployment named `diffme` is running with 1 replica and image `nginx:1.24`.\n\n1. Write a updated manifest for the same Deployment with **3 replicas** and image `nginx:1.25` to `/tmp/diffme-updated.yaml`\n2. Run `kubectl diff` against it to preview the changes\n3. Then **apply** the updated manifest to make the changes live\n\n```bash\n# Preview changes:\nkubectl diff -f /tmp/diffme-updated.yaml\n\n# Apply changes:\nkubectl apply -f /tmp/diffme-updated.yaml\n```", + "hints": [ + { + "title": "Write the updated manifest", + "body": "Create a YAML file with the updated replicas and image values.", + "command": "cat < /tmp/diffme-updated.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: diffme\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: diffme\n template:\n metadata:\n labels:\n app: diffme\n spec:\n containers:\n - name: app\n image: nginx:1.25\nEOF" + }, + { + "title": "Run kubectl diff", + "body": "kubectl diff exits with code 1 if there are differences (expected), code 0 if nothing changed.", + "command": "kubectl diff -f /tmp/diffme-updated.yaml; echo \"Exit code: $?\"" + }, + { + "title": "Apply the manifest", + "body": "Once you have reviewed the diff output, apply the manifest to update the live cluster state.", + "command": "kubectl apply -f /tmp/diffme-updated.yaml" + } + ], + "setup_commands": [ + { + "command": "kubectl create deployment diffme --image=nginx:1.24 --replicas=1 2>/dev/null || true" + }, + { + "command": "kubectl rollout status deployment/diffme --timeout=60s" + } + ], + "validation": { + "commands": [ + { + "description": "Deployment 'diffme' has 3 replicas", + "command": "kubectl get deployment diffme -o jsonpath='{.spec.replicas}'", + "expected_output": "3", + "match": "exact" + }, + { + "description": "Deployment uses nginx:1.25 image", + "command": "kubectl get deployment diffme -o jsonpath='{.spec.template.spec.containers[0].image}'", + "expected_output": "nginx:1.25", + "match": "exact" + }, + { + "description": "Manifest file exists at /tmp/diffme-updated.yaml", + "command": "test -f /tmp/diffme-updated.yaml && echo 'exists'", + "expected_output": "exists", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment diffme --ignore-not-found" + }, + { + "command": "rm -f /tmp/diffme-updated.yaml" + } + ] +} diff --git a/scenarios/data/kubectl-explain-basics.json b/scenarios/data/kubectl-explain-basics.json new file mode 100644 index 0000000..a80cab5 --- /dev/null +++ b/scenarios/data/kubectl-explain-basics.json @@ -0,0 +1,50 @@ +{ + "id": "kubectl-explain-basics", + "title": "Exploring the API with kubectl explain", + "category": "Core Concepts", + "difficulty": "Easy", + "type": "task", + "weight": 3, + "description": "## Exploring the API with `kubectl explain`\n\n`kubectl explain` is a built-in reference tool that describes the fields of any Kubernetes resource — directly from the live API server. It is invaluable during exams when you need to recall the exact field name or understand what a field accepts.\n\n**Your task:**\n\nA ConfigMap has been created for you. Use `kubectl explain` to answer the following, then:\n\n1. Create a Pod named `explain-demo` using image `nginx:alpine`\n2. Set the pod's `spec.terminationGracePeriodSeconds` to **5** (use `kubectl explain pod.spec.terminationGracePeriodSeconds` to understand the field)\n\n```bash\n# Explore the pod spec:\nkubectl explain pod.spec\n\n# Inspect a specific field:\nkubectl explain pod.spec.terminationGracePeriodSeconds\n```", + "hints": [ + { + "title": "kubectl explain syntax", + "body": "Use dot-notation to drill into nested fields. For example: `kubectl explain pod.spec.containers.resources`.", + "command": "kubectl explain pod.spec.terminationGracePeriodSeconds" + }, + { + "title": "Create the pod with the field set", + "body": "Use a heredoc to pipe a YAML manifest with terminationGracePeriodSeconds set to 5 directly to kubectl apply.", + "command": "cat </dev/null || true" + }, + { + "command": "kubectl rollout status deployment/patchme --timeout=60s" + } + ], + "validation": { + "commands": [ + { + "description": "Deployment 'patchme' has 3 replicas", + "command": "kubectl get deployment patchme -o jsonpath='{.spec.replicas}'", + "expected_output": "3", + "match": "exact" + }, + { + "description": "Pod template has label env=staging", + "command": "kubectl get deployment patchme -o jsonpath='{.spec.template.metadata.labels.env}'", + "expected_output": "staging", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete deployment patchme --ignore-not-found" + } + ] +} diff --git a/scenarios/data/node-cordon-basics.json b/scenarios/data/node-cordon-basics.json new file mode 100644 index 0000000..2b720b6 --- /dev/null +++ b/scenarios/data/node-cordon-basics.json @@ -0,0 +1,44 @@ +{ + "id": "node-cordon-basics", + "title": "Cordoning and Uncordoning a Node", + "category": "Cluster Management", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Cordoning and Uncordoning a Node\n\n`kubectl cordon` marks a node as **unschedulable** — the node continues running existing workloads but the scheduler will not place new pods on it. This is useful before maintenance windows.\n\n`kubectl uncordon` reverses the cordon, making the node schedulable again.\n\n**Your task:**\n\n1. Find the name of the cluster node using `kubectl get nodes`\n2. **Cordon** the node\n3. Verify the node shows `SchedulingDisabled` in its status\n4. **Uncordon** the node to restore it to normal\n\n```bash\n# Check node status:\nkubectl get nodes\n\n# Cordon:\nkubectl cordon \n\n# Uncordon:\nkubectl uncordon \n```", + "hints": [ + { + "title": "Find the node name", + "body": "In this single-node cluster, there is exactly one node. Use kubectl get nodes to find its name.", + "command": "kubectl get nodes -o jsonpath='{.items[0].metadata.name}'" + }, + { + "title": "Cordon and uncordon using a variable", + "body": "Store the node name in a variable for convenience, then cordon and uncordon it.", + "command": "NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') && kubectl cordon $NODE && kubectl uncordon $NODE" + } + ], + "setup_commands": [], + "validation": { + "commands": [ + { + "description": "Node is schedulable (not cordoned)", + "command": "kubectl get nodes -o jsonpath='{.items[0].spec.unschedulable}' 2>/dev/null || echo 'false'", + "expected_output": "false", + "match": "contains" + }, + { + "description": "Node is in Ready state", + "command": "kubectl get nodes -o jsonpath='{.items[0].status.conditions[?(@.type==\"Ready\")].status}'", + "expected_output": "True", + "match": "exact" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl uncordon $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') 2>/dev/null || true" + } + ] +} diff --git a/scenarios/data/pod-env-vars-inline.json b/scenarios/data/pod-env-vars-inline.json new file mode 100644 index 0000000..335d2f9 --- /dev/null +++ b/scenarios/data/pod-env-vars-inline.json @@ -0,0 +1,50 @@ +{ + "id": "pod-env-vars-inline", + "title": "Pod with Inline Environment Variables", + "category": "Configuration", + "difficulty": "Easy", + "type": "task", + "weight": 4, + "description": "## Pod with Inline Environment Variables\n\nContainers can receive configuration through environment variables from three sources:\n1. **Inline literals** — hardcoded directly in the pod spec (`env[].value`)\n2. **ConfigMap** — loaded from a ConfigMap key\n3. **Secret** — loaded from a Secret key\n\nThis scenario focuses on source **#1**: setting environment variables directly in the pod spec.\n\n**Your task:**\n\nCreate a Pod named `env-demo` using image `busybox:1.36` with command `sleep 3600` and the following environment variables set **inline** (not from a ConfigMap or Secret):\n\n| Name | Value |\n|---|---|\n| `APP_COLOR` | `blue` |\n| `APP_MODE` | `production` |\n\n```bash\n# Verify the env vars are set inside the container:\nkubectl exec env-demo -- env | grep APP_\n```", + "hints": [ + { + "title": "Setting env vars with kubectl run", + "body": "The quickest way: use `kubectl run` with one `--env` flag per variable.", + "command": "kubectl run env-demo --image=busybox:1.36 --command --env=APP_COLOR=blue --env=APP_MODE=production -- sleep 3600" + }, + { + "title": "Setting env vars in a manifest", + "body": "In a YAML manifest, use the `env` array under the container spec. Each entry has a `name` and `value` field.", + "command": "cat < operator.", + "command": "kubectl get pods --sort-by=.metadata.creationTimestamp > /tmp/pods-sorted.txt && kubectl get pods --sort-by=.metadata.name > /tmp/pods-by-name.txt" + } + ], + "setup_commands": [ + { + "command": "kubectl run sort-pod-c --image=nginx:alpine 2>/dev/null || true && sleep 1" + }, + { + "command": "kubectl run sort-pod-b --image=nginx:alpine 2>/dev/null || true && sleep 0.5" + }, + { + "command": "kubectl run sort-pod-a --image=nginx:alpine 2>/dev/null || true && sleep 0.5" + } + ], + "validation": { + "commands": [ + { + "description": "File /tmp/pods-sorted.txt exists and contains pod output", + "command": "cat /tmp/pods-sorted.txt", + "expected_output": "sort-pod", + "match": "contains" + }, + { + "description": "File /tmp/pods-by-name.txt exists and contains pod output", + "command": "cat /tmp/pods-by-name.txt", + "expected_output": "sort-pod", + "match": "contains" + }, + { + "description": "pods-by-name.txt is sorted alphabetically (sort-pod-a appears before sort-pod-c)", + "command": "grep -n 'sort-pod-a' /tmp/pods-by-name.txt | cut -d: -f1", + "expected_output": "^[0-9]+$", + "match": "regex" + } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { + "command": "kubectl delete pod sort-pod-a sort-pod-b sort-pod-c --ignore-not-found --grace-period=0 --force" + }, + { + "command": "rm -f /tmp/pods-sorted.txt /tmp/pods-by-name.txt" + } + ] +} \ No newline at end of file diff --git a/scenarios/data/static-pods-mcq.json b/scenarios/data/static-pods-mcq.json new file mode 100644 index 0000000..e4918fd --- /dev/null +++ b/scenarios/data/static-pods-mcq.json @@ -0,0 +1,44 @@ +{ + "id": "static-pods-mcq", + "title": "Static Pods in Kubernetes", + "category": "Core Concepts", + "difficulty": "Medium", + "type": "mcq", + "weight": 3, + "description": "## Static Pods in Kubernetes\n\nA cluster administrator wants to run a monitoring agent on every node without relying on the Kubernetes scheduler or API server. The agent must start automatically even if the API server is unavailable.\n\nWhich approach should they use, and where should the manifest be placed?", + "options": [ + { + "id": "a", + "text": "Create a DaemonSet — the scheduler places one pod per node automatically" + }, + { + "id": "b", + "text": "Create a Static Pod by placing a manifest in the kubelet's static pod directory (typically `/etc/kubernetes/manifests/`)" + }, + { + "id": "c", + "text": "Use a CronJob with `--concurrencyPolicy=Forbid` to schedule one pod per node" + }, + { + "id": "d", + "text": "Annotate a Deployment with `node-placement: static` to pin it to every node" + } + ], + "correct_option": "b", + "explanation": "Static Pods are managed directly by the kubelet daemon on a specific node, **without the API server scheduling them**. The kubelet watches a directory (e.g., `/etc/kubernetes/manifests/`) and automatically creates any pods defined there — even if the API server is down. They are ideal for bootstrapping control-plane components (etcd, kube-apiserver, etc.) and for workloads that must survive API server failures. DaemonSets are managed by the scheduler/API server and won't work if the API server is unavailable. There is no `node-placement: static` annotation.", + "hints": [ + { + "title": "What makes a pod 'static'?", + "body": "Static pods are defined as YAML files on the node's filesystem. The kubelet monitors the staticPodPath directory and reconciles the pods itself — no scheduler, no API server required.", + "command": "ls /etc/kubernetes/manifests/ 2>/dev/null || echo 'Static pod dir may differ per distro'" + }, + { + "title": "Identifying static pods in the cluster", + "body": "Static pods always have the node name appended to their pod name (e.g., `kube-apiserver-controlplane`). You can also look at the pod's ownerReferences — static pods have no ownerReference.", + "command": "kubectl get pods -n kube-system" + } + ], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +}