Compare commits
2
Commits
d42aa779b1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e31b051f4e | ||
|
|
f7cdb48833 |
@@ -1,28 +1,41 @@
|
||||
<div align="center">
|
||||
<img src="frontend/public/logo.svg" alt="KubeKosh Logo" width="100" />
|
||||
|
||||
<h1>KubeKosh</h1>
|
||||
<img src="frontend/public/logo.svg" alt="KubeKosh" height="96" />
|
||||
|
||||
<p><strong>Self-hosted Kubernetes Lab for Hands-on Learning</strong></p>
|
||||
# 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://k3s.io)
|
||||
[](https://docs.docker.com/get-docker/)
|
||||
[](https://github.com/ced4568)
|
||||
|
||||
<p>
|
||||
<a href="https://hub.docker.com/r/zeborg/kubekosh"><img src="https://img.shields.io/docker/pulls/zeborg/kubekosh?style=flat-square&logo=docker&label=Docker%20Hub" alt="Docker Hub" /></a>
|
||||
<img src="https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square" alt="License" />
|
||||
<img src="https://img.shields.io/badge/platforms-amd64%20%7C%20arm64-lightgrey?style=flat-square" alt="Platforms" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
## 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 <your_custom_directory>:/data zeborg/kubekosh:latest
|
||||
-v <host_dir>:/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 <ns>` | `kubectl config set-context --current --namespace=<ns>` |
|
||||
| `kctx <ctx>` | `kubectl config use-context <ctx>` |
|
||||
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
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Container["Single Docker Image (--privileged)"]
|
||||
NGINX["nginx :80 — reverse proxy"]
|
||||
FE["Frontend<br/>React + Vite + xterm.js"]
|
||||
BE["Backend<br/>Node / Express + node-pty (WebSocket PTY)"]
|
||||
K3S["K3s<br/>single-node in-container cluster"]
|
||||
DB[("SQLite<br/>/data/progress.db")]
|
||||
|
||||
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 container port `80`, mapped to host port `7554` |
|
||||
| Proxy | nginx on port `80`, mapped to host `7554` |
|
||||
| Storage | SQLite (`better-sqlite3`) at `/data/progress.db` |
|
||||
|
||||
Everything runs inside a **single Docker image** managed by `scripts/entrypoint.sh`.
|
||||
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 -> <scenario-id>.json
|
||||
├── bundles/ # One JSON file per bundle -> <bundle-id>.json
|
||||
└── SCHEMA.md # Full schema reference
|
||||
├── data/ # one JSON file per scenario -> <scenario-id>.json
|
||||
├── bundles/ # one JSON file per bundle -> <bundle-id>.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
|
||||
Hot-reload while authoring (no rebuild):
|
||||
|
||||
**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 <path_to_scenarios_directory>:/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
|
||||
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
|
||||
```
|
||||
|
||||
> **NOTE:**
|
||||
> The content in `<path_to_scenarios_directory>` 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
|
||||
|
||||
```bash
|
||||
# 1. Fork the repo on GitHub, then clone your fork
|
||||
git clone https://github.com/<your-username>/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
|
||||
```
|
||||
|
||||
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).
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>KubeKosh — Interactive Kubernetes Playground</title>
|
||||
<title>RootNode Academy — Build From the Root</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Epilogue:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "kubekosh-ui",
|
||||
"name": "rootnode-academy-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
+17
-55
@@ -1,59 +1,21 @@
|
||||
<svg height="470" width="480" viewBox="100 95 480 470" role="img" xmlns="http://www.w3.org/2000/svg" style="">
|
||||
<title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">KubeKosh icon</title>
|
||||
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">KubeKosh heptagon logo mark in blue with a white innermost layer and dark terminal prompt icon.</desc>
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" role="img" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>RootNode Academy</title>
|
||||
<desc>A filled root node with branching connections to three open network nodes — a root-and-network mark.</desc>
|
||||
|
||||
<!--
|
||||
Heptagon, cx=340, cy=340, angle offset=-90°, step=51.4286°
|
||||
r=220 (outer):
|
||||
k0: (340, 120)
|
||||
k1: (340+220cos(-38.57°), 340+220sin(-38.57°)) = (340+171.9, 340-137.1) = (511.9, 202.9)
|
||||
k2: (340+220cos(12.86°), 340+220sin(12.86°)) = (340+214.4, 340+48.9) = (554.4, 388.9)
|
||||
k3: (340+220cos(64.29°), 340+220sin(64.29°)) = (340+95.0, 340+198.3) = (435.0, 538.3)
|
||||
k4: (340-95.0, 538.3) = (245.0, 538.3)
|
||||
k5: (340-214.4, 388.9) = (125.6, 388.9)
|
||||
k6: (340-171.9, 202.9) = (168.1, 202.9)
|
||||
<!-- Connections (drawn first, sit under the nodes) -->
|
||||
<g stroke="#39FF14" stroke-width="3.4" stroke-linecap="round" fill="none">
|
||||
<line x1="32" y1="38" x2="32" y2="15" />
|
||||
<line x1="32" y1="38" x2="14" y2="47" />
|
||||
<line x1="32" y1="38" x2="50" y2="47" />
|
||||
</g>
|
||||
|
||||
r=178 (mid ring), factor=178/220=0.8091:
|
||||
delta from center scaled by 0.8091:
|
||||
k0: (340, 340+(120-340)*0.8091)=(340, 161.9)
|
||||
k1: (340+171.9*0.8091, 340-137.1*0.8091)=(479.1, 228.9)
|
||||
k2: (340+214.4*0.8091, 340+48.9*0.8091)=(513.5, 379.6)
|
||||
k3: (340+95.0*0.8091, 340+198.3*0.8091)=(416.9, 500.5)
|
||||
k4: (263.1, 500.5)
|
||||
k5: (166.5, 379.6)
|
||||
k6: (200.9, 228.9)
|
||||
<!-- Outer nodes — open circles, electric blue -->
|
||||
<g stroke="#00D4FF" stroke-width="3.2" fill="none">
|
||||
<circle cx="32" cy="13" r="6" />
|
||||
<circle cx="13" cy="48" r="5.5" />
|
||||
<circle cx="51" cy="48" r="5.5" />
|
||||
</g>
|
||||
|
||||
r=132 (inner screen), factor=132/220=0.6:
|
||||
k0: (340, 340+(120-340)*0.6)=(340, 208)
|
||||
k1: (340+171.9*0.6, 340-137.1*0.6)=(443.1, 257.7)
|
||||
k2: (340+214.4*0.6, 340+48.9*0.6)=(468.6, 369.3)
|
||||
k3: (340+95.0*0.6, 340+198.3*0.6)=(397.0, 458.9)
|
||||
k4: (283.0, 458.9)
|
||||
k5: (340-214.4*0.6, 369.3)=(211.4, 369.3)
|
||||
k6: (340-171.9*0.6, 257.7)=(236.9, 257.7)
|
||||
-->
|
||||
|
||||
<!-- Outer heptagon — mid blue -->
|
||||
<polygon points="340,120 511.9,202.9 554.4,388.9 435.0,538.3 245.0,538.3 125.6,388.9 168.1,202.9" fill="#378ADD" style="fill:rgb(55, 138, 221);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Mid ring — darker blue -->
|
||||
<polygon points="340,161.9 479.1,228.9 513.5,379.6 416.9,500.5 263.1,500.5 166.5,379.6 200.9,228.9" fill="#185FA5" style="fill:rgb(24, 95, 165);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Inner heptagon — white -->
|
||||
<polygon points="340,208 443.1,257.7 468.6,369.3 397.0,458.9 283.0,458.9 211.4,369.3 236.9,257.7" fill="#FFFFFF" style="fill:rgb(255, 255, 255);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Terminal prompt, centered at (340, 333) -->
|
||||
<!-- Caret > -->
|
||||
<polyline points="296,308 340,333 296,358" fill="none" stroke="#0C447C" stroke-width="480" stroke-linecap="round" stroke-linejoin="round" style="fill:none;stroke:rgb(12, 68, 124);color:rgb(251, 251, 254);stroke-width:18px;stroke-linecap:round;stroke-linejoin:round;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<!-- Cursor underscore -->
|
||||
<line x1="352" y1="384" x2="404" y2="384" stroke="#185FA5" stroke-width="480" stroke-linecap="round" style="fill:rgb(0, 0, 0);stroke:rgb(24, 95, 165);color:rgb(251, 251, 254);stroke-width:13px;stroke-linecap:round;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
|
||||
<!-- Vertex dots -->
|
||||
<circle cx="340" cy="120" r="10" fill="#B5D4F4" style="fill:rgb(181, 212, 244);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<circle cx="511.9" cy="202.9" r="10" fill="#B5D4F4" style="fill:rgb(181, 212, 244);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<circle cx="554.4" cy="388.9" r="10" fill="#B5D4F4" style="fill:rgb(181, 212, 244);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<circle cx="435.0" cy="538.3" r="10" fill="#B5D4F4" style="fill:rgb(181, 212, 244);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<circle cx="245.0" cy="538.3" r="10" fill="#B5D4F4" style="fill:rgb(181, 212, 244);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<circle cx="125.6" cy="388.9" r="10" fill="#B5D4F4" style="fill:rgb(181, 212, 244);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<circle cx="168.1" cy="202.9" r="10" fill="#B5D4F4" style="fill:rgb(181, 212, 244);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||
<!-- Root node — filled, terminal green -->
|
||||
<circle cx="32" cy="38" r="8.5" fill="#39FF14" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 865 B |
@@ -410,9 +410,9 @@ export default function App() {
|
||||
)}
|
||||
|
||||
<footer className={styles.footer}>
|
||||
<div>© {new Date().getFullYear()} The KubeKosh Project • All rights reserved</div>
|
||||
<div>© {new Date().getFullYear()} RootNode Academy • All rights reserved</div>
|
||||
<div>
|
||||
Made with <span className={styles.heart}>❤️</span> by <a href="https://github.com/zeborg" target="_blank" rel="noopener noreferrer" className={styles.footerLink}>zeborg</a>
|
||||
Built on the open-source <a href="https://github.com/zeborg/kubekosh" target="_blank" rel="noopener noreferrer" className={styles.footerLink}>KubeKosh</a> project by zeborg
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -98,18 +98,18 @@ export default function Header({ clusterReady, onShowHistory }) {
|
||||
<header className={styles.header}>
|
||||
<div className={styles.brand}>
|
||||
<div className={styles.logo}>
|
||||
<img src="/logo.svg" alt="KubeKosh Logo" className={styles.logoImage} />
|
||||
<span className={styles.logoText}>KubeKosh</span>
|
||||
<img src="/logo.svg" alt="RootNode Academy Logo" className={styles.logoImage} />
|
||||
<span className={styles.logoText}>RootNode Academy</span>
|
||||
<span className={styles.version}>{import.meta.env.VITE_APP_VERSION}</span>
|
||||
</div>
|
||||
<span className={styles.tagline}>Interactive Kubernetes Playground</span>
|
||||
<span className={styles.tagline}>Build From the Root</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.right}>
|
||||
{/* GitHub link */}
|
||||
<div className={styles.githubBtnContainer}>
|
||||
<a
|
||||
href="https://github.com/zeborg/kubekosh"
|
||||
href="https://github.com/ced4568/kubekosh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.githubBtn}
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function TerminalComponent({ collapsed, onToggleCollapse }) {
|
||||
</div>
|
||||
<span className={styles.barTitle}>
|
||||
<span className={styles.barIcon}>$_</span>
|
||||
bash — kubekosh
|
||||
bash — rootnode
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.barRight}>
|
||||
|
||||
+13
-13
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user