ROOT_FOUNDATIONS_ARCHITECTURE.md designs all 26 modules across ROOT 01 (Computing & Linux Foundations), 02 (Linux Administration & Networking), 03 (Git & Collaborative Development), and 04 (Containers & Docker), using the existing course/module/activity model — outcomes, prerequisites, activities, and Real Ops connections for each, per docs/COURSE_AUTHORING_STANDARD.md. ROOT_FOUNDATIONS_LAB_STRATEGY.md documents the central finding this phase is built on: the existing scenario engine (backend/server.js's runCommand()) executes an arbitrary shell command string and checks its output — nothing about it is Kubernetes-specific. The existing `lab` activity type and scenario JSON format already fully support Linux and Git exercises with real, deterministic validation. No shell_lab/git_lab/container_lab activity type is built or needed. Also documents the container-runtime decision for ROOT 04, evaluated and tested empirically (not assumed) against the real Lab image and production resource caps — see the following commit for the implementation. Co-Authored-By: Claude Sonnet 5 <[email protected]>
118 lines
7.9 KiB
Markdown
118 lines
7.9 KiB
Markdown
# ROOT Foundations Lab Strategy
|
|
|
|
Phase 6D §7/§10 — what the existing disposable Lab can safely teach for ROOT 01-04, evaluated
|
|
by actually reading `backend/server.js` and testing inside a real container, not assumed.
|
|
|
|
## 1. The scenario engine is already generic — no new activity type needed
|
|
|
|
`POST /api/scenarios/:id/setup` and `POST /api/scenarios/:id/validate` (`backend/server.js`)
|
|
run `setup_commands[].command` / `validation.commands[].command` through `runCommand()`, which
|
|
is a bare `child_process.exec(cmd)` — an arbitrary shell command string, checked against
|
|
`expected_output` by `checkMatch()` (exact/contains/not_contains/regex). **Nothing about this
|
|
is Kubernetes-specific.** Every existing scenario just happens to write `kubectl ...` as its
|
|
command string. `isolation_type` (`shared_namespace` etc.) is pure descriptive metadata — grepped
|
|
across the entire backend and frontend, it gates no actual behavior.
|
|
|
|
This means the existing `lab` activity type (`module_activities.activity_type = 'lab'`,
|
|
`completion_behavior = 'auto_scenario'`) and the existing `scenarios/data/*.json` format already
|
|
fully support Linux, networking-inspection, and Git exercises with real, deterministic
|
|
validation — a scenario file with `"command": "test -f /root/lab/notes.txt && echo exists"` is
|
|
exactly as valid as one with `"command": "kubectl get pod ..."`. **No `shell_lab`/`git_lab`/
|
|
`container_lab` activity type is built or needed.** The speculative multi-type model the
|
|
authorization raised as a possibility is unnecessary — it would duplicate a generic mechanism
|
|
that already exists.
|
|
|
|
The only wrinkle: `runCommand` does not pass a `cwd`, so every command runs wherever the backend
|
|
process's cwd is; multi-step Linux/Git scenarios must `cd /root/lab-<id> && ...` inside each
|
|
command string (exactly how a real student would chain commands), not rely on shell state
|
|
persisting between `setup` and `validate` calls beyond what the filesystem/git/process state
|
|
itself carries.
|
|
|
|
## 2. What the Lab image already contains (verified from the real `Dockerfile`)
|
|
|
|
Ubuntu 22.04 base with `curl wget git vim nano jq bash-completion iproute2 iptables
|
|
iputils-ping procps htop mount kmod python3 nginx` already installed, plus k3s (bundling
|
|
`kubectl`/`crictl`/containerd) started by `scripts/entrypoint.sh`. The student terminal execs
|
|
into this same container via node-pty — there is no separate "Linux sandbox," it's the same
|
|
shell k3s runs in.
|
|
|
|
**Already usable with zero image changes:** shell navigation, file manipulation (`cp`/`mv`/`rm`/
|
|
`cat`/`grep`/pipes/redirection), process inspection (`ps`/`top`/`kill`), permissions
|
|
(`chmod`/`chown`/`sudo` — the container runs as root, so a permission-teaching scenario should
|
|
`useradd` a second user in its `setup_commands` to demonstrate a real permission denial), and
|
|
network inspection (`ip addr`/`ip route`/`ss`/`ping`/`curl`).
|
|
|
|
**Missing, needed for ROOT 02 Module 5 (DNS & HTTP):** `dig`/`nslookup` are NOT installed
|
|
(`dnsutils`/`bind9-dnsutils` is absent from the current package list). This is a genuinely small,
|
|
justified Dockerfile addition (one more `apt-get install` package, same risk class as everything
|
|
else already there) — not a new mechanism. `curl` alone already covers the HTTP half.
|
|
|
|
## 3. Container runtime decision — tested, not assumed
|
|
|
|
**Decision: install real Docker Engine (`docker.io`) inside the same already-privileged Lab
|
|
container, alongside k3s, as a second independent container runtime with its own nested
|
|
`/var/run/docker.sock`.** Podman was not chosen — Docker Engine matches the actual ROOT 04
|
|
curriculum content (Docker CLI, Dockerfile, Compose terminology) and required no new privilege.
|
|
|
|
**Why this doesn't weaken the security boundary:** the student Lab container is already
|
|
launched with `Privileged: true` (`platform/provisioner/src/lifecycle.js` — required for k3s,
|
|
already the accepted threat model per `docs/LAB_ARCHITECTURE.md` §7: the Lab Host VM, not the
|
|
container boundary, is what actually contains a student). A second container runtime running
|
|
*inside* an already-fully-privileged guest adds no new host capability. **The Lab Host's own
|
|
Docker socket is never touched or exposed** — the nested `dockerd` started inside the guest
|
|
listens on its own `/var/run/docker.sock` *inside that guest's filesystem namespace*, entirely
|
|
separate from the Lab Host's real Docker daemon that the Provisioner itself talks to.
|
|
|
|
**Tested empirically** (not assumed) against the exact production resource caps
|
|
(`resource-policy.yaml`: 2 vCPU, 4096MB memory, 512 PIDs, `Privileged: true`): built
|
|
`rootnode-academy:test` fresh from the real `Dockerfile`, layered `docker.io` on top, launched
|
|
it under those exact `docker run` constraints, and ran real workloads inside:
|
|
|
|
| Step | Result | Memory | PIDs |
|
|
|---|---|---|---|
|
|
| k3s alone, ready | ✓ | 357MB | 36 |
|
|
| + `dockerd --storage-driver=vfs` started | ✓ | — | — |
|
|
| `docker run --rm hello-world` | ✓ passed | — | — |
|
|
| `docker run -d -p 8080:80 nginx:alpine`, `curl localhost:8080` | ✓ 200, real HTML returned | 866MB | 159 |
|
|
| `docker build` a real 2-line Dockerfile, `docker run` the result | ✓ passed | 884MB | 160 |
|
|
|
|
Comfortable headroom remained in all three dimensions (884MB/4096MB, 160/512 PIDs, CPU settled
|
|
after the image pull). `--storage-driver=vfs` was used deliberately — overlay2-on-overlay2
|
|
(the Lab Host's own overlay filesystem, nested inside another overlay driver) is a known source
|
|
of instability in Docker-in-Docker setups; vfs is slower per-layer but has no such nesting
|
|
hazard, and ROOT 04's exercises are small single-purpose images, not build-heavy CI workloads.
|
|
|
|
**`docker compose` is NOT included by `docker.io`** — Ubuntu's packaged Docker CLI does not ship
|
|
the Compose v2 plugin. Fixed the same way the Dockerfile already fetches k3s: download the
|
|
static `docker-compose` v2 binary directly (matching the existing "curl a static binary rather
|
|
than add a whole package repository" pattern already used for k3s) into
|
|
`/usr/local/lib/docker/cli-plugins/docker-compose` so `docker compose` works as a CLI subcommand.
|
|
|
|
**What was NOT done:** no change to the Provisioner, Lab Router, or Gateway. No change to
|
|
`HostConfig` (still `Privileged: true`, same CPU/memory/PIDs caps as every other Lab). No mount
|
|
of the Lab Host's `/var/run/docker.sock` into any container — verified this was never written
|
|
anywhere in this phase's changes.
|
|
|
|
## 4. Git strategy
|
|
|
|
`git` is already installed. All ROOT 03 exercises use **local repositories created inside the
|
|
student's own Lab** (`git init` in a scenario's `setup_commands`) — no Gitea account, no network
|
|
credentials, no dependency on RootNode's own canonical repository ever touching a student Lab.
|
|
A later module ("Remote Repositories") teaches `clone`/`fetch`/`pull`/`push`/`origin` conceptually
|
|
and against a **second local bare repository** created in the same Lab (`git init --bare`,
|
|
`git remote add origin /root/lab-remote.git`) to give students a genuine push/pull/clone
|
|
round-trip without needing any real network service, credential, or RootNode-adjacent
|
|
infrastructure — Step 9's explicit constraint.
|
|
|
|
## 5. Classification summary
|
|
|
|
| Capability | Classification |
|
|
|---|---|
|
|
| Shell/filesystem/process/permissions exercises | **SUPPORTED NOW** — zero image or code change |
|
|
| Network inspection (`ip`, `ss`, `ping`, `curl`) | **SUPPORTED NOW** |
|
|
| DNS tooling (`dig`/`nslookup`) | **REQUIRES SMALL PLATFORM EXTENSION** — one apt package added to the Lab Dockerfile |
|
|
| Git exercises (local + local-bare-remote) | **SUPPORTED NOW** — zero image or code change |
|
|
| Docker CLI / image build / single containers | **REQUIRES SMALL PLATFORM EXTENSION** — `docker.io` + entrypoint change to start `dockerd`, tested and proven safe/performant above |
|
|
| Docker Compose | **REQUIRES SMALL PLATFORM EXTENSION** — static `docker-compose` v2 binary, same pattern as k3s's own binary fetch |
|
|
| A distinct `shell_lab`/`git_lab`/`container_lab` activity type | **NOT NEEDED** — the existing generic `lab` + scenario-JSON model already covers all of the above |
|