From f7cdb488337cf2e1db2fa12167b5ebac0acae38b Mon Sep 17 00:00:00 2001 From: Chase Dumphord Date: Sun, 21 Jun 2026 03:27:46 -0500 Subject: [PATCH] feat: rebrand to RootNode Academy + add Real Ops scenario bundle - Rename app title, header, tagline, footer, terminal tab to RootNode Academy - Apply RootNode color palette (#39FF14 green, #00D4FF blue, GitHub-style darks) - Add rootnode-runaway-monitor scenario (homelab NOC incident: oversized memory request) - Add rootnode-real-ops bundle in terminal-green accent - Update package name to rootnode-academy-ui --- README.md | 236 +++++++++---------- frontend/index.html | 2 +- frontend/package.json | 2 +- frontend/src/App.jsx | 4 +- frontend/src/components/Header.jsx | 8 +- frontend/src/components/Terminal.jsx | 2 +- frontend/src/index.css | 26 +- scenarios/bundles/rootnode-real-ops.json | 13 + scenarios/data/rootnode-runaway-monitor.json | 58 +++++ 9 files changed, 205 insertions(+), 146 deletions(-) create mode 100644 scenarios/bundles/rootnode-real-ops.json create mode 100644 scenarios/data/rootnode-runaway-monitor.json diff --git a/README.md b/README.md index 11c6b34..fcf7866 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,41 @@
- KubeKosh Logo -

KubeKosh

+KubeKosh -

Self-hosted Kubernetes Lab for Hands-on Learning

+# KubeKosh — RootNode Academy Edition + +**A browser-based Kubernetes lab for hands-on learners.** +Real K3s. Real `kubectl`. Instant validation. No cloud account, no local cluster. + +[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](LICENSE) +[![Platforms](https://img.shields.io/badge/platforms-amd64%20%7C%20arm64-lightgrey?style=flat-square)](#) +[![Kubernetes](https://img.shields.io/badge/Kubernetes-K3s-326CE5?style=flat-square&logo=kubernetes&logoColor=white)](https://k3s.io) +[![Docker](https://img.shields.io/badge/Docker-required-2496ED?style=flat-square&logo=docker&logoColor=white)](https://docs.docker.com/get-docker/) +[![Maintained by ced4568](https://img.shields.io/badge/maintained%20by-ced4568-181717?style=flat-square&logo=github)](https://github.com/ced4568) -

- Docker Hub - License - Platforms -

--- -KubeKosh runs a real [K3s](https://k3s.io/) Kubernetes cluster inside a single Docker container and pairs it with a browser-based terminal and automated scenario validation — no cloud account or local cluster required. +> **Attribution.** This is a branded, modified fork of the excellent +> [`zeborg/kubekosh`](https://github.com/zeborg/kubekosh), licensed under Apache 2.0. +> RootNode Academy adds custom scenarios, branding, and multi-student deployment tooling on +> top of the upstream project. Original copyright and license notices are preserved in +> [`LICENSE`](LICENSE). Modified files are marked per Apache 2.0 §4. Full credit to +> the original author for the core platform. -## Screenshots +--- -| | | | -|---|---|---| -| ![Scenario browser with live terminal](screenshots/1.png) | ![Task scenario with problem statement](screenshots/2.png) | ![Contextual hints with copy-ready commands](screenshots/3.png) | -| ![Automated validation — all checks passed](screenshots/4.png) | ![Exam mode — start with custom duration and scenario count](screenshots/5.png) | ![Exam mode — live exam with timer](screenshots/6.png) | -| ![Exam MCQ — selecting an answer](screenshots/7.png) | ![Exam MCQ — correct answer with explanation](screenshots/8.png) | ![Exam history with per-scenario score report](screenshots/9.png) | +## What It Is + +KubeKosh runs a real single-node [K3s](https://k3s.io) cluster inside one Docker +container and pairs it with a browser terminal and automated scenario validation. +A learner opens a tab, gets a live cluster, runs real `kubectl`, and clicks **Validate** +to check their work against actual cluster state. + +In the RootNode Academy curriculum this is the **capstone rung** — the Kubernetes & DevOps +track that sits on top of the Linux / Git / Docker foundations. It is aimed at the +advanced learner (roughly 14+), not the absolute beginner. --- @@ -31,23 +44,25 @@ KubeKosh runs a real [K3s](https://k3s.io/) Kubernetes cluster inside a single D **Prerequisite:** [Docker](https://docs.docker.com/get-docker/) ```bash -docker run -itd --name kubekosh --privileged -p 7554:80 zeborg/kubekosh:latest +docker run -itd --name kubekosh --privileged -p 7554:80 ced4568/kubekosh:latest ``` -Open **http://localhost:7554** — wait ~30 seconds for the *Cluster Ready* indicator to turn green. +Open **http://localhost:7554** and wait ~30s for the *Cluster Ready* indicator to turn green. -> `--privileged` is required — K3s needs access to kernel namespaces and cgroups. - -> **Security Warning:** Do **not** expose this container publicly. Use it only on your local machine as it is meant for educational purposes only. +> `--privileged` is required — K3s needs kernel namespaces and cgroups. +> **Do not expose this container directly to the public internet.** It is a teaching +> sandbox. Multi-student exposure is handled by the deployment layer below, not by +> publishing the raw container. ### Persist Progress ```bash docker run -itd --name kubekosh --privileged -p 7554:80 \ - -v :/data zeborg/kubekosh:latest + -v :/data ced4568/kubekosh:latest ``` -Progress is stored in SQLite at `/data/progress.db` inside the container. Mount a local directory to `/data` to keep progress across container restarts. +Progress is stored in SQLite at `/data/progress.db`. Mount a host directory to `/data` +to keep progress across restarts — one volume per student. ### Build From Source @@ -62,53 +77,72 @@ docker buildx build --platform linux/amd64,linux/arm64 -t kubekosh . ## What's Inside | Bundle | Focus | Exam Mode | -|---|---|---| +| ------ | ----- | --------- | | 🌱 Kubernetes Basics | Core concepts | 60 min | | 🧑‍✈️ Kubernetes Administrator | CKA | 120 min | | 🛠️ Kubernetes Developer | CKAD | 120 min | | 🛡️ Kubernetes Security | CKS | 120 min | -**Scenario types:** -- **Task** — Hands-on challenge in the live terminal. Click **Validate** for automated cluster-state checking. -- **MCQ** — Multiple-choice question with a detailed explanation on submission. +**Scenario types** -### Shell Aliases +- **Task** — hands-on challenge in the live terminal; **Validate** auto-checks cluster state. +- **MCQ** — multiple choice with a detailed explanation on submission. -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 ` | +Custom RootNode Academy scenarios live alongside upstream ones in `scenarios/data/` and are +grouped into bundles in `scenarios/bundles/`. See [`scenarios/SCHEMA.md`](scenarios/SCHEMA.md). --- ## Architecture -| Component | Technology | -|---|---| -| Frontend | React + Vite, `xterm.js` | -| Backend | Node.js / Express, `node-pty` WebSocket PTY | -| Cluster | K3s (single-node, in-container) | -| Proxy | nginx on container port `80`, mapped to host port `7554` | -| Storage | SQLite (`better-sqlite3`) at `/data/progress.db` | +```mermaid +flowchart TD + subgraph Container["Single Docker Image (--privileged)"] + NGINX["nginx :80 — reverse proxy"] + FE["Frontend
React + Vite + xterm.js"] + BE["Backend
Node / Express + node-pty (WebSocket PTY)"] + K3S["K3s
single-node in-container cluster"] + DB[("SQLite
/data/progress.db")] -Everything runs inside a **single Docker image** managed by `scripts/entrypoint.sh`. + NGINX --> FE + NGINX --> BE + BE -->|kubectl / PTY| K3S + BE --> DB + end + + Student["Learner browser"] -->|:7554| NGINX +``` + +| Component | Technology | +| --------- | ---------- | +| Frontend | React + Vite, `xterm.js` | +| Backend | Node.js / Express, `node-pty` WebSocket PTY | +| Cluster | K3s (single-node, in-container) | +| Proxy | nginx on port `80`, mapped to host `7554` | +| Storage | SQLite (`better-sqlite3`) at `/data/progress.db` | + +Everything runs inside one image managed by `scripts/entrypoint.sh`. + +--- + +## Multi-Student Deployment (RootNode Academy addition) + +The upstream project assumes one learner, one container. RootNode Academy runs it across three +delivery modes. **Never publish the raw privileged container** — front it with isolation. + +| Mode | Provisioning | Isolation | +| ---- | ------------ | --------- | +| In-person camp | Pre-spun container per seat, torn down after | Lab VLAN, no public ingress | +| Live cohort | Container per enrolled student for the session window | Per-student auth at the proxy | +| Self-paced | On-demand spin-up per session, idle-timeout teardown | Per-user volume + auth + resource limits | + +Hardening checklist for shared infra: + +- One container **per student** — never share a cluster between learners. +- Apply CPU/memory limits so one runaway pod can't starve a host. +- Put authentication in front (proxy-level), never expose `:7554` to the internet. +- Persist each student's `/data` to a dedicated volume. +- Idle-timeout + auto-teardown for self-paced sessions to control cost. --- @@ -116,88 +150,42 @@ Everything runs inside a **single Docker image** managed by `scripts/entrypoint. ``` scenarios/ -├── data/ # One JSON file per scenario -> .json -├── bundles/ # One JSON file per bundle -> .json -└── SCHEMA.md # Full schema reference +├── 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 +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 +## Authoring Scenarios -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!** +Each scenario is one JSON file in `scenarios/data/`; each bundle is one JSON file in +`scenarios/bundles/`. See [`scenarios/SCHEMA.md`](scenarios/SCHEMA.md) for the full schema. -### Adding Scenarios +**Task checklist** +- `validation.commands` — idempotent `kubectl` only +- `setup_commands` / `teardown_commands` — `kubectl` or native Ubuntu only -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. +**MCQ checklist** +- `correct_option` must match an `options[].id` +- always include an `explanation` -**Task checklist:** -- `validation.commands` — idempotent `kubectl` commands only -- `setup_commands` / `teardown_commands` — `kubectl` or native Ubuntu commands only - -**MCQ checklist:** -- `correct_option` must match one of the `options[].id` values -- Always include an `explanation` - -### In-Memory Cache & Hot Reloading - -To ensure high performance and zero disk-I/O bottlenecking, scenarios and bundles are cached in memory on backend startup. When developing or updating scenarios, you can hot-reload the definitions without rebuilding the image or restarting the container: - -1. **Mount Scenarios Directory:** Run the container with the local `scenarios/` directory mounted to `/app/scenarios`: - ```bash - docker run --rm -itd --privileged -p 7554:80 --name kubekosh -v :/app/scenarios zeborg/kubekosh:latest - ``` -2. **Reload Cache:** Click the **Reload Scenario Cache** (↻) button in the top right corner of the header in the web user interface, or send an API request: - ```bash - curl -X POST http://localhost:7554/api/cache/reload - ``` - -> **NOTE:** -> The content in `` should be the path to the local `scenarios/` directory of the cloned repository with your updates, i.e., it should contain the updated `scenarios/data` and `scenarios/bundles` directories. - -### Workflow +Hot-reload while authoring (no rebuild): ```bash -# 1. Fork the repo on GitHub, then clone your fork -git clone https://github.com//kubekosh.git -cd kubekosh - -# 2. Create a branch -git checkout -b feat/my-scenario - -# 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. 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 -# Run the built container directly: -docker build -t kubekosh . && docker run --rm -itd --privileged -p 7554:80 --name kubekosh kubekosh -# Or mount the scenarios folder for hot-reloading: -docker run --rm -itd --privileged -p 7554:80 -v $PWD/scenarios:/app/scenarios --name kubekosh zeborg/kubekosh:dev - -# 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 +docker run --rm -itd --privileged -p 7554:80 --name kubekosh \ + -v $PWD/scenarios:/app/scenarios ced4568/kubekosh:latest +curl -X POST http://localhost:7554/api/cache/reload ``` -Open a Pull Request from your fork's branch against `main`. - --- ## License -Apache 2.0 License — see [LICENSE](LICENSE). +Apache 2.0 — see [`LICENSE`](LICENSE). Original work © the `zeborg/kubekosh` authors. +Modifications © RootNode Academy / Chase Dumphord (ced4568). \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 6e93cfc..f32f392 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - KubeKosh — Interactive Kubernetes Playground + RootNode Academy — Build From the Root diff --git a/frontend/package.json b/frontend/package.json index c2f1153..2e5ebaa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "kubekosh-ui", + "name": "rootnode-academy-ui", "version": "0.1.0", "private": true, "scripts": { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 872f868..a51b8dd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -410,9 +410,9 @@ export default function App() { )}
-
© {new Date().getFullYear()} The KubeKosh Project • All rights reserved
+
© {new Date().getFullYear()} RootNode Academy • All rights reserved
- Made with ❤️ by zeborg + Built on the open-source KubeKosh project by zeborg
diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 958c761..0b6cfd9 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -98,18 +98,18 @@ export default function Header({ clusterReady, onShowHistory }) {
- KubeKosh Logo - KubeKosh + RootNode Academy Logo + RootNode Academy {import.meta.env.VITE_APP_VERSION}
- Interactive Kubernetes Playground + Build From the Root
{/* GitHub link */}
diff --git a/frontend/src/index.css b/frontend/src/index.css index d75870c..fd9d068 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,17 +2,17 @@ /* ── Dark theme (default) ────────────────────────────────────────────────── */ :root { - --bg: #0c0f14; - --surface: #131720; - --surface2: #1a2030; - --surface3: #212840; - --border: #252d42; - --border2: #2e3a55; + --bg: #0D1117; + --surface: #161B22; + --surface2: #1C232E; + --surface3: #232C3A; + --border: #21262D; + --border2: #30363D; - --green: #39d98a; - --green-dim: rgba(57,217,138,0.12); - --blue: #4dabf7; - --blue-dim: rgba(77,171,247,0.10); + --green: #39FF14; + --green-dim: rgba(57,255,20,0.12); + --blue: #00D4FF; + --blue-dim: rgba(0,212,255,0.10); --amber: #fcc419; --amber-dim: rgba(252,196,25,0.10); --red: #ff6b6b; @@ -22,9 +22,9 @@ --teal: #2dd4bf; --teal-dim: rgba(45,212,191,0.11); - --text: #e2e8f4; - --text-2: #8899b8; - --text-3: #4d5f80; + --text: #E6EDF3; + --text-2: #8B949E; + --text-3: #586069; --mono: 'IBM Plex Mono', monospace; --sans: 'Epilogue', sans-serif; diff --git a/scenarios/bundles/rootnode-real-ops.json b/scenarios/bundles/rootnode-real-ops.json new file mode 100644 index 0000000..7ebf742 --- /dev/null +++ b/scenarios/bundles/rootnode-real-ops.json @@ -0,0 +1,13 @@ +{ + "id": "rootnode-real-ops", + "name": "RootNode: Real Ops", + "icon": "🌳", + "tagline": "Real-world cluster failures, drawn from a working homelab NOC.", + "color": "#39FF14", + "colorDim": "rgba(57,255,20,0.12)", + "exam_minutes": 60, + "scenario_ids": [ + "rootnode-runaway-monitor", + "broken-deployment" + ] +} diff --git a/scenarios/data/rootnode-runaway-monitor.json b/scenarios/data/rootnode-runaway-monitor.json new file mode 100644 index 0000000..645daa3 --- /dev/null +++ b/scenarios/data/rootnode-runaway-monitor.json @@ -0,0 +1,58 @@ +{ + "id": "rootnode-runaway-monitor", + "title": "Recover the Monitoring Stack", + "category": "Observability & Ops", + "difficulty": "Medium", + "type": "task", + "weight": 8, + "description": "## Recover the Monitoring Stack\n\nA monitoring agent named `node-metrics` was deployed to the `monitoring` namespace, but its pod never came up. Right now it's stuck **Pending**.\n\nThis is modeled on a real homelab incident: a monitoring component requested far more memory than any node could give it. In production that same class of mistake — an unbounded or oversized monitoring workload — is a classic way to exhaust a node and take the *whole observability stack* down with it.\n\n**Your task:** Diagnose why the pod won't schedule, then **right-size** the resource request so the deployment reaches **1/1 Ready**.\n\n> Important: the goal is to *right-size* the request, not to delete resource governance entirely. A monitoring workload with no requests at all is how you got here in the first place — it should still declare a sane memory request.", + "hints": [ + { + "title": "1. See the failure", + "body": "List pods in the monitoring namespace. You'll see node-metrics stuck Pending. Describe the pod and read the Events at the bottom — the scheduler tells you exactly why it can't place the pod.", + "command": "kubectl get pods -n monitoring\nkubectl describe pod -n monitoring -l app=node-metrics" + }, + { + "title": "2. Find the bad request", + "body": "A FailedScheduling / Insufficient memory event means the pod is asking for more memory than the node has. Inspect the deployment's resource requests to find the absurd value.", + "command": "kubectl get deploy node-metrics -n monitoring -o jsonpath='{.spec.template.spec.containers[0].resources.requests}'" + }, + { + "title": "3. Right-size it", + "body": "Lower the memory request to something a small node can actually satisfy (for an nginx stand-in, 64Mi is plenty). kubectl set resources patches the live deployment and triggers a new rollout. Keep a request set — just make it sane.", + "command": "kubectl set resources deployment node-metrics -n monitoring --requests=memory=64Mi" + } + ], + "setup_commands": [ + { "command": "kubectl create namespace monitoring" }, + { "command": "kubectl create deployment node-metrics --image=nginx:1.25 --replicas=1 -n monitoring" }, + { "command": "kubectl set resources deployment node-metrics -n monitoring --requests=memory=64Gi" } + ], + "validation": { + "description": "Checks that node-metrics is right-sized: scheduled, 1/1 Ready, and still declaring a memory request.", + "commands": [ + { + "description": "Deployment 'node-metrics' has 1 ready replica", + "command": "kubectl get deployment node-metrics -n monitoring -o jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -v '^$' || echo 0", + "expected_output": "1", + "match": "exact" + }, + { + "description": "No pods left in Pending state", + "command": "kubectl get pods -n monitoring --no-headers 2>/dev/null | awk '{print $3}' | grep -c 'Pending' || true", + "expected_output": "0", + "match": "exact" + }, + { + "description": "Container still declares a memory request (governance not removed)", + "command": "kubectl get deployment node-metrics -n monitoring -o jsonpath='{.spec.template.spec.containers[0].resources.requests.memory}' 2>/dev/null | grep -c '.' || echo 0", + "expected_output": "1", + "match": "exact" + } + ] + }, + "default_namespace": "monitoring", + "teardown_commands": [ + { "command": "kubectl delete namespace monitoring --ignore-not-found --wait=false" } + ] +}