Files
rootnode-academy/platform/provisioner/migrations/002_environments.sql
T
ced4568andClaude Sonnet 5 62241d26d2 feat(platform-db): add lab host and environment registry
New Phase 1 Platform database — PostgreSQL, separate from the Lab image's own
SQLite (which keeps its current students/progress data untouched, decision #2 /
correction C in docs/LAB_ARCHITECTURE.md).

Schema (platform/provisioner/migrations/):
- lab_hosts: the abstraction that keeps Docker connectivity out of code —
  endpoint (unix:// or tcp://+TLS), enabled flag. Phase 1 ships one row.
- environments: environment_id (UUID), student_id (opaque external identifier,
  no FK — students table isn't in this database yet), lab_host_id (real FK),
  status, routing_token (CSPRNG via crypto.randomBytes, opaque), container/
  network identity, resource limits, lifecycle timestamps.
- environment_events: append-only audit trail, one row per lifecycle
  transition, never logs the routing token in plaintext (see
  lifecycle.js's redact()).

File-based migration runner (db.js) matching the check-before-alter idempotency
pattern already used in the Lab image's backend/server.js, applied on every
Provisioner boot.

Resource limits and the lab_hosts seed are config files
(config/resource-policy.yaml, config/lab-hosts.seed.yaml), not database tables —
there's no admin UI to manage them via a database yet, so a DB-backed policy
table would just be a write path nothing uses.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-18 01:21:31 -05:00

52 lines
2.6 KiB
SQL

-- environments — the disposable per-student lab registry.
-- See docs/LAB_ARCHITECTURE.md §3 and docs/PHASE1_PLATFORM_FOUNDATION.md §2.
DO $$ BEGIN
CREATE TYPE environment_status AS ENUM (
'requested', 'provisioning', 'running', 'resetting',
'suspended', -- experimental path only, see decision #3 / correction A
'destroying', 'destroyed', 'error'
);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
CREATE TABLE IF NOT EXISTS environments (
environment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
student_id TEXT NOT NULL, -- opaque external identifier (correction C) —
-- no FK: students table isn't in this database
-- until a later, separate Platform DB migration.
-- The Provisioner trusts its caller on this value.
lab_host_id TEXT NOT NULL REFERENCES lab_hosts(lab_host_id), -- real FK: both
-- tables are Provisioner-owned Postgres.
status environment_status NOT NULL DEFAULT 'requested',
routing_token TEXT NOT NULL, -- CSPRNG, opaque, unique
container_id TEXT,
container_name TEXT,
network_id TEXT,
network_name TEXT,
network_subnet CIDR, -- needed to install/remove the per-env firewall chain
image_ref TEXT NOT NULL,
cpu_limit NUMERIC NOT NULL,
mem_limit_mb INTEGER NOT NULL,
pids_limit INTEGER NOT NULL,
storage_limit_mb INTEGER NOT NULL,
internal_ip TEXT,
internal_port INTEGER NOT NULL DEFAULT 80,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
last_active_at TIMESTAMPTZ,
destroy_at TIMESTAMPTZ, -- idle-reaper's scheduled destroy time
error_reason TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_environments_routing_token ON environments(routing_token);
CREATE INDEX IF NOT EXISTS idx_environments_student ON environments(student_id);
CREATE INDEX IF NOT EXISTS idx_environments_status ON environments(status);
CREATE INDEX IF NOT EXISTS idx_environments_lab_host ON environments(lab_host_id);
-- Phase 1 invariant "at most one non-terminal environment per student" is enforced in
-- application logic (lifecycle.js), not a DB constraint yet — a partial unique index is the
-- natural upgrade once this needs to be bulletproof under concurrent requests, not needed for
-- a single-instance provisioner in this phase.