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]>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
-- lab_hosts — correction D: the abstraction that keeps Docker connectivity out of code.
|
||||
-- See docs/LAB_ARCHITECTURE.md §3/§4 and docs/PHASE1_PLATFORM_FOUNDATION.md §2.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lab_hosts (
|
||||
lab_host_id TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
endpoint TEXT NOT NULL, -- 'unix:///var/run/docker.sock' (dev) or
|
||||
-- 'tcp://10.x.x.x:2376' (prod, TLS-verified)
|
||||
tls_ca_path TEXT, -- null for the local-socket dev case
|
||||
tls_cert_path TEXT,
|
||||
tls_key_path TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
-- Future (not Phase 1): capacity/status columns for real scheduling once there's more
|
||||
-- than one host. Phase 1 selects "the one enabled row" — no scheduler.
|
||||
);
|
||||
@@ -0,0 +1,51 @@
|
||||
-- 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.
|
||||
@@ -0,0 +1,17 @@
|
||||
-- environment_events — audit trail for every lifecycle transition.
|
||||
-- See docs/LAB_ARCHITECTURE.md §3/§8 and docs/PHASE1_PLATFORM_FOUNDATION.md §2/§9.
|
||||
-- Never log secrets or routing tokens in plaintext here (see lifecycle.js emitEvent()).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS environment_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
environment_id UUID NOT NULL REFERENCES environments(environment_id),
|
||||
event_type TEXT NOT NULL, -- provision_requested | host_selected | network_created |
|
||||
-- firewall_configured | container_created | ready |
|
||||
-- reset | destroyed | error | reaped_idle | suspend | resume
|
||||
actor TEXT NOT NULL, -- 'system' | 'instructor:<id>' | 'student:<id>' | 'reaper'
|
||||
detail JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_env_events_environment ON environment_events(environment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_env_events_type ON environment_events(event_type);
|
||||
Reference in New Issue
Block a user