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]>
55 lines
2.1 KiB
YAML
55 lines
2.1 KiB
YAML
# 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
|