docs(phase2b): document admin and platform-data architecture
New docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md covers the Platform API split, Postgres schema and migration guarantees/rollback, the session cookie architecture (including the whoami-routing-bug fix), the admin portal, the API/security boundary, known limitations, and Phase 3+ future work. Updates ENGINEERING_SOP.md §8, LAB_ARCHITECTURE.md, README.md, and platform/README.md to reflect Platform API as the sole Provisioner caller and Postgres as authoritative for identity/progress as of Phase 2B.
This commit is contained in:
@@ -134,11 +134,18 @@ layers rather than changing the image itself:
|
|||||||
|
|
||||||
- `platform/provisioner/` — Postgres-backed environment registry, per-student Docker
|
- `platform/provisioner/` — Postgres-backed environment registry, per-student Docker
|
||||||
networking, firewall-enforced egress, idle reaper (`docs/PHASE1_PLATFORM_FOUNDATION.md`).
|
networking, firewall-enforced egress, idle reaper (`docs/PHASE1_PLATFORM_FOUNDATION.md`).
|
||||||
- `platform/gateway/` — the only student-facing path to a Lab: resolves identity → environment
|
- `platform/api/` — lightweight Platform API: students, cohorts, curriculum, progress,
|
||||||
→ routing token, proxies HTTP/WebSocket into that one student's container, never a raw
|
scenario attempts, student/admin sessions, instructor/admin control plane. No k3s, no
|
||||||
container IP/port (`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`).
|
`--privileged` (`docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md`).
|
||||||
|
- `platform/gateway/` — the only student-facing path to a Lab: resolves identity from a
|
||||||
|
server-controlled session cookie (not a client-suppliable header/param), proxies
|
||||||
|
HTTP/WebSocket into that one student's container, never a raw container IP/port
|
||||||
|
(`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`, session model in
|
||||||
|
`docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` §3).
|
||||||
- The student flow (roster login → curriculum dashboard → Launch Lab → Lab session →
|
- The student flow (roster login → curriculum dashboard → Launch Lab → Lab session →
|
||||||
Reset/Recreate) lives in `frontend/src/platform/` — a small addition alongside the existing,
|
Reset/Recreate) lives in `frontend/src/platform/`; the instructor/admin portal
|
||||||
|
(cohorts, students, classroom "Prepare Labs," environment controls) lives in
|
||||||
|
`frontend/src/admin/` at the `/admin` route — both small additions alongside the existing,
|
||||||
unmodified Lab UI, not a rewrite of it.
|
unmodified Lab UI, not a rewrite of it.
|
||||||
|
|
||||||
**Never publish the raw privileged container** — front it with isolation (that's what the
|
**Never publish the raw privileged container** — front it with isolation (that's what the
|
||||||
@@ -151,9 +158,14 @@ Key properties of the platform (not this image):
|
|||||||
- One container **per student**, its own Docker network, its own firewall chain — never a
|
- One container **per student**, its own Docker network, its own firewall chain — never a
|
||||||
shared cluster between learners. Verified with two simultaneous students: separate clusters,
|
shared cluster between learners. Verified with two simultaneous students: separate clusters,
|
||||||
separate progress, no cross-access.
|
separate progress, no cross-access.
|
||||||
- Progress/identity live outside the disposable container (a persistent identity instance
|
- Progress/identity live outside the disposable container, in the Platform API's own
|
||||||
running this same image, currently SQLite-backed, moving to the Platform's PostgreSQL in a
|
PostgreSQL (authoritative as of Phase 2B) — destroying or recreating a student's Lab never
|
||||||
later phase) — destroying or recreating a student's Lab never loses their academic record.
|
loses their academic record. Verified with a full class-sized (3-student) cohort workflow:
|
||||||
|
provision, complete different scenarios, reset one student's Lab, destroy all Labs after
|
||||||
|
class — progress survives every step.
|
||||||
|
- Instructors manage cohorts, students, curriculum assignment, and classroom Lab prep
|
||||||
|
(provision an entire cohort before class, verify readiness, destroy Labs after) from
|
||||||
|
`/admin` — no manual database, Docker, or Kubernetes access needed.
|
||||||
- CPU/memory/PID/storage limits are enforced per container, config-driven.
|
- CPU/memory/PID/storage limits are enforced per container, config-driven.
|
||||||
- Egress is restricted to an allowlisted proxy at the firewall layer, not left open.
|
- Egress is restricted to an allowlisted proxy at the firewall layer, not left open.
|
||||||
- Idle/abandoned environments are automatically cleaned up, and a student can launch a fresh
|
- Idle/abandoned environments are automatically cleaned up, and a student can launch a fresh
|
||||||
|
|||||||
@@ -171,13 +171,15 @@ section is just the map.
|
|||||||
environment per student — its own Docker network, its own container running the existing,
|
environment per student — its own Docker network, its own container running the existing,
|
||||||
unmodified Lab image, its own firewall-enforced egress. Postgres-backed registry
|
unmodified Lab image, its own firewall-enforced egress. Postgres-backed registry
|
||||||
(`environments`, `environment_events`, `lab_hosts`), idle reaper, startup reconciliation.
|
(`environments`, `environment_events`, `lab_hosts`), idle reaper, startup reconciliation.
|
||||||
- **What it is not:** directly reachable by a student, even now — the Gateway (§9) sits in
|
- **What it is not:** directly reachable by a student or the Gateway anymore — as of Phase 2B,
|
||||||
front of it and is the only caller. Still exercised directly via its internal API
|
the Platform API (§9) is the *only* caller (student launch/reset AND admin prepare/destroy/
|
||||||
(shared-secret auth) or `platform/provisioner/test/run.js` for isolated Provisioner testing.
|
reset both go through it). Still exercised directly via its internal API (shared-secret
|
||||||
- **Where identity lives:** unchanged from §5 — `students`/`progress` are still on the Lab
|
auth) or `platform/provisioner/test/run.js` for isolated Provisioner testing.
|
||||||
image's own SQLite. `environments.student_id` is deliberately opaque (no FK) until a later,
|
- **Where identity lives:** as of Phase 2B, Postgres is authoritative —
|
||||||
separate migration puts roster/progress on the same Postgres instance as the environment
|
`students`/`cohorts`/`progress`/`scenario_attempts` live in the same `rootnode_platform`
|
||||||
registry.
|
database as `environments`, owned by the Platform API's own migrations. See §9.
|
||||||
|
`environments.student_id` is still a plain opaque reference, not a real FK — that's a
|
||||||
|
deliberate, documented choice (docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md §2), not an oversight.
|
||||||
- **Local dev:** `platform/provisioner/docker-compose.dev.yml` runs Postgres + the egress
|
- **Local dev:** `platform/provisioner/docker-compose.dev.yml` runs Postgres + the egress
|
||||||
proxy + the Provisioner on one machine — the documented Phase 1 dev exception. Production
|
proxy + the Provisioner on one machine — the documented Phase 1 dev exception. Production
|
||||||
is a two-VM Proxmox split (Platform VM / Lab Host VM); the dev compose file is explicitly
|
is a two-VM Proxmox split (Platform VM / Lab Host VM); the dev compose file is explicitly
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
# RootNode Academy — Ephemeral Lab Architecture
|
# RootNode Academy — Ephemeral Lab Architecture
|
||||||
|
|
||||||
Status: **Phase 1 (platform foundation) + Phase 2A (Gateway + student flow) IMPLEMENTED,
|
Status: **Phase 1 (platform foundation) + Phase 2A (Gateway + student flow) + Phase 2B
|
||||||
2026-08-18** — see `docs/PHASE1_PLATFORM_FOUNDATION.md` and
|
(Platform API split, Postgres academic data, server sessions, admin portal) IMPLEMENTED,
|
||||||
`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` for the concrete schema/API/test results, and
|
2026-08-18** — see `docs/PHASE1_PLATFORM_FOUNDATION.md`, `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`,
|
||||||
`platform/provisioner/README.md` for how to run the stack. This document remains the
|
and `docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` for concrete schema/API/test results, and
|
||||||
architecture record for the full ephemeral-lab direction; instructor dashboard, production
|
`platform/README.md` for how to run the stack. This document remains the architecture record
|
||||||
auth/SSO, and production Proxmox deployment are still future work (Phase 2B — see the Phase 2A
|
for the full ephemeral-lab direction; production OIDC/SSO, per-instructor accounts, and
|
||||||
doc's §9). Supersedes the namespace-isolation
|
production Proxmox deployment are still future work (Phase 3+ — see the Phase 2B doc's §13).
|
||||||
|
Supersedes the namespace-isolation
|
||||||
(`shared_namespace`/`ephemeral_cluster`) approach that was in flight before this redirect — see
|
(`shared_namespace`/`ephemeral_cluster`) approach that was in flight before this redirect — see
|
||||||
`scenarios/SCHEMA.md`'s `isolation_type` note for the pointer back here.
|
`scenarios/SCHEMA.md`'s `isolation_type` note for the pointer back here.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# RootNode Academy — Phase 2B: Admin Portal & Platform Data
|
||||||
|
|
||||||
|
Status: **IMPLEMENTED and end-to-end tested, 2026-08-18.** Builds on
|
||||||
|
`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` (Phase 2A milestone
|
||||||
|
`42dfb1c31b599d5e86b64915beb4b871b2e91055`). This doc covers: the platform/lab split, the
|
||||||
|
Postgres academic schema and SQLite migration, server-controlled sessions, and the
|
||||||
|
instructor/admin control plane.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Split: Platform API vs. Lab image
|
||||||
|
|
||||||
|
Phase 2A's "identity" instance was a full, `--privileged` Lab image booting an unused k3s
|
||||||
|
cluster just to serve roster/curriculum/progress APIs. Phase 2B replaces it with
|
||||||
|
`platform/api/` — a plain Node/Express service: no k3s, no `--privileged`, no terminal
|
||||||
|
runtime, no nginx (serves the built frontend directly via `express.static`). Startup is
|
||||||
|
instant instead of a 1-2 minute k3s cold start — directly observable in testing (see §8).
|
||||||
|
|
||||||
|
The Lab image (`Dockerfile`, `backend/server.js`, `scripts/entrypoint.sh`) is **completely
|
||||||
|
unchanged in this phase** — it's still the disposable-per-student, nested-k3s, terminal +
|
||||||
|
scenario-execution engine, exactly as Phase 1/2A built it. Platform API never runs
|
||||||
|
setup/validate/teardown commands itself; it only relays outcomes the Lab already produced
|
||||||
|
(see §7, "Progress relay," carried over from Phase 2A and extended).
|
||||||
|
|
||||||
|
Platform API owns: students, cohorts, enrollments, curriculum assignments, progress, scenario
|
||||||
|
attempts, student/admin session identity, and — new in Phase 2B — **all** Provisioner calls,
|
||||||
|
both student-initiated (launch/reset) and admin-initiated (prepare-labs, destroy-labs, reset,
|
||||||
|
destroy). The Gateway no longer calls the Provisioner directly at all; it proxies
|
||||||
|
`/api/environment/*` and `/api/admin/*` to Platform API, which is now the single place that
|
||||||
|
holds the Provisioner's shared secret.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. PostgreSQL academic schema
|
||||||
|
|
||||||
|
New tables in the existing `rootnode_platform` database (same instance the Provisioner's
|
||||||
|
`lab_hosts`/`environments`/`environment_events` already live in — different migration
|
||||||
|
directory, same Postgres):
|
||||||
|
|
||||||
|
- `students` — `student_id` stays the same opaque TEXT identifier used everywhere else
|
||||||
|
(no breaking change to `environments.student_id`, which remains a plain reference with no
|
||||||
|
FK, exactly as Phase 1 designed it).
|
||||||
|
- `cohorts` / `cohort_enrollments` — replaces the old single `students.cohort` TEXT column
|
||||||
|
with a real many-to-many model (a student can move between cohorts over time; the old
|
||||||
|
model couldn't represent that at all).
|
||||||
|
- `curriculum_assignments` — records which curriculum path a cohort or student is on. Only
|
||||||
|
one path exists today (`rootnode-path`) — this table exists so assigning it is a real,
|
||||||
|
auditable instructor action, not because multiple paths exist yet.
|
||||||
|
- `progress` — same shape as the SQLite table it replaces.
|
||||||
|
- `scenario_attempts` — **new capability**, not a migration of anything that existed before.
|
||||||
|
One row per validate/answer outcome (result, validation count, timestamp, which
|
||||||
|
environment), giving real per-attempt history instead of just an aggregate status +
|
||||||
|
counter. `hint_count` is present but always NULL — the existing Lab engine's hints are
|
||||||
|
static UI content only (`frontend/src/components/ScenarioPanel.jsx`), nothing server-side
|
||||||
|
ever records a hint view. Documented here rather than inventing a number; the column is
|
||||||
|
ready if hint-view tracking is ever added to the Lab engine.
|
||||||
|
- `exam_sessions` — historical import of the old SQLite `sessions` table. **Scope note:**
|
||||||
|
exam mode's *live* session bookkeeping (start/shuffle/submit/abandon) stays Lab-image-local
|
||||||
|
for Phase 2B — productionizing it would need the same relay pattern progress got in Phase
|
||||||
|
2A, and it isn't exercised by the Phase 2B acceptance test. This table makes historical exam
|
||||||
|
records survive the migration and visible to instructors; it isn't written to by new exam
|
||||||
|
sessions yet. Flagged as a real gap, not silently ignored — see §13.
|
||||||
|
|
||||||
|
## 3. SQLite → Postgres migration
|
||||||
|
|
||||||
|
`platform/api/migrate-sqlite.js`. Run once as the cutover:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SQLITE_PATH=/path/to/progress.db PLATFORM_DB_URL=postgres://... node migrate-sqlite.js
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Backs up first, unconditionally** — copies the SQLite file to
|
||||||
|
`<path>.backup-<timestamp>` before reading anything. The backup is never deleted or
|
||||||
|
modified by the script.
|
||||||
|
- **Idempotent** — every write uses `ON CONFLICT`; re-running re-syncs from the SQLite source
|
||||||
|
rather than duplicating or erroring.
|
||||||
|
- **Never silently drops data** — prints and writes a JSON report
|
||||||
|
(`migration-report-<timestamp>.json`) with exact counts (students, progress rows,
|
||||||
|
exam_sessions, cohorts created, enrollments created) and anything that couldn't be read
|
||||||
|
(e.g. a missing table), with the reason.
|
||||||
|
- **Preserves the old `students.cohort` column** as a real cohort + enrollment row instead of
|
||||||
|
discarding it.
|
||||||
|
- **Rollback:** Postgres only becomes authoritative once this has run and the Platform API is
|
||||||
|
pointed at it — nothing here modifies or deletes the SQLite source. To roll back: stop
|
||||||
|
Platform API, point student/instructor traffic back at a SQLite-backed service (the old
|
||||||
|
Phase 2A "identity" image is unmodified and still buildable/runnable — see
|
||||||
|
`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`), and restore from the backup file if the live
|
||||||
|
SQLite file was somehow touched (it shouldn't have been — the migration is read-only against
|
||||||
|
it).
|
||||||
|
|
||||||
|
See the final report for actual migrated record counts from this implementation's test run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Student session — productionized ownership
|
||||||
|
|
||||||
|
Phase 2A's `?sid=` was explicitly pilot-only: a client-supplied query parameter, trivially
|
||||||
|
changeable. Phase 2B replaces it with a server-controlled session:
|
||||||
|
|
||||||
|
- `POST /api/login {student_id}` (unchanged trigger — pick-your-name, no password, still
|
||||||
|
pilot-grade identity, stated as such) creates a session server-side
|
||||||
|
(`platform/api/src/sessions.js`, in-memory, 8h TTL) and returns an opaque, CSPRNG token as
|
||||||
|
an `HttpOnly`, `SameSite=Lax` cookie (`rn_session`). The browser cannot read or fabricate
|
||||||
|
this value.
|
||||||
|
- Platform API derives `student_id` **only** from this cookie
|
||||||
|
(`studentSession` middleware) — never from a header or query parameter a client controls.
|
||||||
|
- The Gateway resolves "who is this browser" by calling Platform API's
|
||||||
|
`GET /api/session/whoami` (forwarding the cookie), then compares that student_id against
|
||||||
|
the environment's actual owning `student_id` (from the Provisioner, via Platform API) —
|
||||||
|
**on every proxied Lab request, HTTP and WebSocket, not just the initial navigation.** A
|
||||||
|
short-TTL cache (`SESSION_CACHE_TTL_MS`, 5s) keeps the extra round trip cheap.
|
||||||
|
- Changing the token, URL, query parameter, or environment ID in the browser has **no effect**
|
||||||
|
on who the Gateway thinks is asking — that's determined entirely by the unforgeable cookie.
|
||||||
|
Verified by testing: a valid session for Student A requesting Student B's `/lab/<token>/`
|
||||||
|
gets **403**, not just an unset-parameter fallback.
|
||||||
|
|
||||||
|
**Explicitly not production SSO/OIDC.** Sessions are in-memory (a Platform API restart logs
|
||||||
|
everyone out — acceptable for a pilot/classroom tool, not silently ignored, see §13), and
|
||||||
|
login is still pick-your-name with no password. Production OIDC/Google Workspace SSO remains
|
||||||
|
Phase 3+ (§13/§14).
|
||||||
|
|
||||||
|
### Routing-hint cookie — a real bug found while building this
|
||||||
|
|
||||||
|
The Gateway's unprefixed fallback route (needed so the *unmodified* Lab UI's root-relative
|
||||||
|
`/api/scenarios`, `/shell-ws`, etc. resolve correctly once a browser is inside `/lab/<token>/`
|
||||||
|
— see Phase 2A's cookie design) initially used "does this session currently own a running
|
||||||
|
environment" as the signal for "should I try routing this to the Lab." That's wrong: it's
|
||||||
|
true on the *dashboard* too, not just inside the lab page, and it hijacked
|
||||||
|
`GET /api/session/whoami` (and would have hijacked `/api/environment/*`, `/api/admin/*`,
|
||||||
|
`/api/roster*`) to the student's Lab container — which doesn't have those routes at all —
|
||||||
|
whenever a student had ever launched a lab. Confirmed by testing (whoami returned the SPA's
|
||||||
|
`index.html` instead of JSON).
|
||||||
|
|
||||||
|
Fixed with an explicit allowlist (`LAB_OWNED_PATHS` in `platform/gateway/src/server.js`) of
|
||||||
|
the specific unprefixed paths the Lab UI actually calls
|
||||||
|
(`/api/scenarios*`, `/api/bundles`, `/api/sessions*`, `/api/cache*`, `/api/health`,
|
||||||
|
`/shell-ws`, `/assets/*`) — everything else always goes to Platform API regardless of whether
|
||||||
|
the student has a running lab. `LAB_ACTIVE_COOKIE` (set only on an actual `/lab/<token>/`
|
||||||
|
visit) is a secondary gate on top of that, not the sole signal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4-9. Admin/Instructor portal
|
||||||
|
|
||||||
|
New `/admin` route in the same frontend bundle (`frontend/src/admin/`) — a separate React tree
|
||||||
|
from the student Platform Shell, gated by its own login (`rn_admin` session cookie, completely
|
||||||
|
separate cookie/token namespace from `rn_session` — a student session can never call an admin
|
||||||
|
route and vice versa).
|
||||||
|
|
||||||
|
**Dashboard** (`AdminDashboard.jsx`) — active cohorts, total students, Labs by state
|
||||||
|
(running/provisioning/error/idle), recent completions, recent environment failures. No
|
||||||
|
Prometheus/Grafana — this is "is class ready / is anything on fire," not an observability
|
||||||
|
platform (explicit non-goal).
|
||||||
|
|
||||||
|
**Cohorts** (`CohortsView.jsx`) — create/edit, roster (add/view students), curriculum
|
||||||
|
assignment, per-stage progress bars aggregated across the cohort, and the classroom
|
||||||
|
preparation workflow:
|
||||||
|
|
||||||
|
- **Prepare Labs** — finds enrolled active students, provisions anyone without a usable
|
||||||
|
environment. Runs concurrently (`Promise.allSettled`) — sequential would make a real cohort
|
||||||
|
prep take the better part of an hour, since each provision is a genuine k3s cold start.
|
||||||
|
Returns a per-student readiness report.
|
||||||
|
- **Destroy Cohort Labs** — destroys every enrolled student's environment. Academic progress
|
||||||
|
lives entirely in Postgres, untouched by this — by construction, not by a separate
|
||||||
|
safeguard, since the destroy path never touches `students`/`progress`/`scenario_attempts`.
|
||||||
|
|
||||||
|
**Students** (`StudentsView.jsx`) — per-student curriculum progress, recent attempts
|
||||||
|
(scenario, result, validation count, timestamp), current environment status, and controls:
|
||||||
|
provision / reset-recreate / destroy / view event history.
|
||||||
|
|
||||||
|
**Admin auth** (`platform/api/src/admin-auth.js`) — a single instructor credential
|
||||||
|
(`ADMIN_PASSWORD` env var, the same secret-injection channel every other component here
|
||||||
|
uses), verified via `scrypt` + `timingSafeEqual` rather than a plain string comparison — no
|
||||||
|
plaintext password ever sits in a database or gets logged. Explicitly not OIDC; documented as
|
||||||
|
Phase 3+ future work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10-12. API routes, security boundary
|
||||||
|
|
||||||
|
See the route table in the final report. Boundary preserved throughout:
|
||||||
|
|
||||||
|
```
|
||||||
|
Student browser → Gateway → Platform API → Provisioner → Lab Host
|
||||||
|
Admin browser → Gateway → Platform API → Provisioner → Lab Host
|
||||||
|
```
|
||||||
|
|
||||||
|
Browser JavaScript never reaches the Provisioner directly in either path — Platform API is
|
||||||
|
now the *only* thing holding `PROVISIONER_SHARED_SECRET`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Known limitations
|
||||||
|
|
||||||
|
- **Sessions are in-memory, single-process.** A Platform API restart invalidates every student
|
||||||
|
and admin session. Acceptable at pilot/classroom scale; would need a shared store (Postgres
|
||||||
|
or Redis) before this runs across multiple Platform API instances.
|
||||||
|
- **Exam mode is not fully productionized** — live exam sessions stay Lab-local; only
|
||||||
|
historical records migrate to Postgres. See §2.
|
||||||
|
- **`identity`'s Phase 2A `network_mode: bridge` requirement no longer applies** — Platform
|
||||||
|
API replaces it entirely and doesn't run k3s, so that whole class of problem is gone, not
|
||||||
|
just documented around.
|
||||||
|
- **Admin credential is a single shared password**, not per-instructor accounts — fine for a
|
||||||
|
single-instructor pilot, not a multi-instructor deployment model.
|
||||||
|
- **`?sid=` is gone entirely** (Phase 2A's mechanism) — nothing reads it anymore; a stale
|
||||||
|
bookmarked URL containing it is harmless, the parameter is simply ignored.
|
||||||
|
|
||||||
|
## Future (Phase 3+)
|
||||||
|
|
||||||
|
Production OIDC/SSO, per-instructor accounts, full students/progress historical fidelity for
|
||||||
|
exam mode, shared session store for horizontal scaling, everything already listed as
|
||||||
|
out-of-scope in Phase 2A/2B (parent portal, billing, capstone engine, production Proxmox,
|
||||||
|
full observability, multi-host scheduling, public enrollment).
|
||||||
+29
-19
@@ -6,42 +6,52 @@ Start here, then go to the component you need:
|
|||||||
- `provisioner/` — Postgres-backed environment registry and lifecycle. Design:
|
- `provisioner/` — Postgres-backed environment registry and lifecycle. Design:
|
||||||
`docs/LAB_ARCHITECTURE.md`, `docs/PHASE1_PLATFORM_FOUNDATION.md`. Operational reference:
|
`docs/LAB_ARCHITECTURE.md`, `docs/PHASE1_PLATFORM_FOUNDATION.md`. Operational reference:
|
||||||
`provisioner/README.md`.
|
`provisioner/README.md`.
|
||||||
|
- `api/` — the lightweight Platform API: students, cohorts, enrollments, curriculum, progress,
|
||||||
|
scenario attempts, student/admin sessions, and the instructor/admin control plane. No k3s, no
|
||||||
|
`--privileged` — this replaced Phase 2A's "identity" instance, which was a full Lab image
|
||||||
|
booting an unused k3s cluster just to serve these APIs. Design:
|
||||||
|
`docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md`. Operational reference: `api/README.md`.
|
||||||
- `gateway/` — the only student-facing path to a Lab (HTTP + WebSocket proxy, environment-aware
|
- `gateway/` — the only student-facing path to a Lab (HTTP + WebSocket proxy, environment-aware
|
||||||
routing, student identity glue). Design: `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`.
|
routing derived from a server-controlled session — not a client-suppliable header/param).
|
||||||
|
Design: `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` (routing), `docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` §3 (session ownership).
|
||||||
- `egress-proxy/` — shared allowlisting proxy every Lab's outbound traffic passes through.
|
- `egress-proxy/` — shared allowlisting proxy every Lab's outbound traffic passes through.
|
||||||
- `docker-compose.dev.yml` — the full stack (Postgres, egress proxy, Provisioner, a persistent
|
- `docker-compose.dev.yml` — the full stack (Postgres, egress proxy, Provisioner, Platform API,
|
||||||
"identity" instance, Gateway) on one machine. This is the Phase 1/2A **local dev exception**
|
Gateway) on one machine. This is the Phase 1/2A/2B **local dev exception** — production is
|
||||||
— production is the two-VM Proxmox split described in `docs/LAB_ARCHITECTURE.md` §1.
|
the two-VM Proxmox split described in `docs/LAB_ARCHITECTURE.md` §1.
|
||||||
|
|
||||||
## Running the full stack locally
|
## Running the full stack locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd platform
|
cd platform
|
||||||
echo "PROVISIONER_SHARED_SECRET=$(openssl rand -hex 32)" > .env
|
echo "PROVISIONER_SHARED_SECRET=$(openssl rand -hex 32)" > .env
|
||||||
|
echo "ADMIN_PASSWORD=$(openssl rand -hex 12)" >> .env
|
||||||
docker compose -f docker-compose.dev.yml build
|
docker compose -f docker-compose.dev.yml build
|
||||||
docker compose -f docker-compose.dev.yml up -d
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Wait for `identity` to boot (it runs a real k3s cluster, same as any Lab — check with
|
No k3s boot to wait for at this layer anymore (Platform API starts instantly) — the whole
|
||||||
`curl http://localhost:7554/api/health`), then the whole platform is reachable at
|
platform is reachable at `http://localhost:4200` (the Gateway) right away: students at `/`,
|
||||||
`http://localhost:4200` (the Gateway) — that's the one origin a student's browser would use.
|
instructors at `/admin`.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
- `provisioner/test/run.js` — fast, isolated Provisioner tests (network/container/firewall
|
- `provisioner/test/run.js` — fast, isolated Provisioner tests (network/container/firewall
|
||||||
mechanics, no Gateway/identity involved). Run against `provisioner/docker-compose.dev.yml`.
|
mechanics, no Gateway/Platform API involved). Run against `provisioner/docker-compose.dev.yml`.
|
||||||
- `test/e2e-phase2a.js` — slow, full-stack tests against the compose file above: roster →
|
- `test/e2e-phase2a.js` — student-flow regression through real session cookies: roster → login
|
||||||
login → curriculum → launch → Gateway routing → cross-student rejection → progress relay →
|
→ curriculum → launch → Gateway routing → cross-student rejection → progress relay → reset.
|
||||||
reset. Real k3s boots, budget several minutes. Run whenever the Gateway, Provisioner API
|
- `test/e2e-phase2b.js` — the classroom acceptance workflow: instructor creates a cohort, adds
|
||||||
surface, or the identity backend's progress/roster/curriculum routes change.
|
students, assigns curriculum, Prepare Labs (concurrent), students complete different
|
||||||
|
scenarios, instructor resets one, instructor destroys all cohort Labs after class — progress
|
||||||
|
survives every step. Needs `ADMIN_PASSWORD` set (see `platform/.env`).
|
||||||
|
|
||||||
Neither substitutes for the manual real-Lab-image checks (node-pty, terminal WebSocket,
|
Both e2e suites are slow (real k3s boots) and need the full stack up. Neither substitutes for
|
||||||
`kubectl`, scenario validation) in `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` §8 — those need a
|
the manual real-Lab-image checks (node-pty, terminal WebSocket, `kubectl`, scenario validation)
|
||||||
browser-equivalent client and aren't automated here.
|
in `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` §8 — those need a browser-equivalent client and
|
||||||
|
aren't automated here.
|
||||||
|
|
||||||
## Known limitations
|
## Known limitations
|
||||||
|
|
||||||
See `provisioner/README.md` (Phase 1) and `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` §9
|
See `provisioner/README.md` (Phase 1), `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md` §9 (Phase
|
||||||
(Phase 2A) for the full, honest lists — including the `identity` service's `network_mode:
|
2A), and `docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` §13 (Phase 2B) for the full, honest lists —
|
||||||
bridge` requirement and its unused-k3s resource cost, both flagged as real, not swept under
|
including in-memory sessions, the single shared admin credential, and exam mode's live
|
||||||
the rug.
|
bookkeeping staying Lab-local.
|
||||||
|
|||||||
Reference in New Issue
Block a user