feat(provisioner): add disposable lab lifecycle
Internal-only service (platform/provisioner/) implementing the environment lifecycle from docs/PHASE1_PLATFORM_FOUNDATION.md: provision -> running -> (reset -> running | destroy -> destroyed), plus experimental suspend/resume (decision #3 — never on the guaranteed path, never relied on for correctness). - src/docker.js — the lab-host abstraction (correction D). Resolves a lab_hosts row into a connected dockerode client, unix:// socket or tcp:// with TLS client-cert auth. Refuses to connect to a tcp:// endpoint with no TLS material configured rather than connecting unauthenticated. Validated against both a local socket and a real remote Docker-in-Docker TLS endpoint during testing — same code path either way, only the lab_hosts config differs, which is the entire point. - src/lifecycle.js — provision/destroy/reset/suspend/resume orchestration, wired to docker.js + egress.js + the Postgres registry. Every Lab container is privileged (required for k3s, not avoidable), gets its own Docker network, no published host ports, deterministic naming (rootnode-lab-<short-id> / rootnode-lab-net-<short-id>), and a CSPRNG routing token. Includes reconcileOnStartup(), implementing the "don't trust stale state after a crash/restart" requirement from docs/LAB_ARCHITECTURE.md §8 — verified by killing a container out-of-band and confirming a Provisioner restart flags the mismatch instead of trusting the stale 'running' row. Found and fixed during testing: a provisioning failure (e.g. readiness timeout) left an orphaned container/network/firewall chain behind with only the DB row marked 'error'. provisionEnvironment's catch block now does best-effort teardown of whatever was already created before marking the row error — confirmed by deliberately failing a provision and checking no container/network/chain survived it. - src/server.js — the internal HTTP API (X-Provisioner-Key shared-secret auth on every route, matching the instructor-passcode pattern already used for the roster endpoint). Input validation on student_id/environment_id, structured JSON errors with a request id, routing tokens redacted from every response. - docker-compose.dev.yml — Postgres + egress-proxy + provisioner on one machine (the documented Phase 1 dev exception). provisioner and egress-proxy run with network_mode: host + NET_ADMIN/NET_RAW, which is what lets src/egress.js manipulate the host's DOCKER-USER chain without needing interactive host sudo. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# Phase 1 local development/testing stack — Postgres + egress-proxy + provisioner, all on one
|
||||
# machine (docs/LAB_ARCHITECTURE.md §1's documented dev exception). Nothing here represents the
|
||||
# production two-VM topology; it's what makes the provisioner testable on a laptop.
|
||||
#
|
||||
# provisioner and egress-proxy run with network_mode: host + NET_ADMIN/NET_RAW because:
|
||||
# - egress.js manipulates the host's DOCKER-USER iptables chain directly (§5) — that's only
|
||||
# possible if the process can see the host network namespace.
|
||||
# - egress-proxy needs to be reachable from every per-student bridge network's gateway IP,
|
||||
# which only works if it's bound across all host interfaces (host networking), not sandboxed
|
||||
# inside its own compose-managed bridge network.
|
||||
# Because provisioner uses host networking, it reaches Postgres via localhost:5433 (Postgres's
|
||||
# published host port), not the compose service name.
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: rootnode_platform
|
||||
ports:
|
||||
- "5433:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
egress-proxy:
|
||||
build: ../egress-proxy
|
||||
network_mode: host
|
||||
# Squid listens on :3128 across every host interface, including each per-student bridge's
|
||||
# gateway — that's the whole reason this needs host networking rather than a normal
|
||||
# compose network.
|
||||
|
||||
provisioner:
|
||||
build: .
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
PLATFORM_DB_URL: postgres://postgres:postgres@localhost:5433/rootnode_platform
|
||||
PROVISIONER_SHARED_SECRET: ${PROVISIONER_SHARED_SECRET:?set a real secret, do not use a default}
|
||||
PROVISIONER_PORT: "4100"
|
||||
LAB_IMAGE_REF: ${LAB_IMAGE_REF:-rootnode-academy:test}
|
||||
EGRESS_PROXY_PORT: "3128"
|
||||
DNS_RESOLVER: "127.0.0.11"
|
||||
NO_PROXY_CIDRS: "10.42.0.0/16,10.43.0.0/16"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
Reference in New Issue
Block a user