feat(platform-api): split lightweight platform service from Lab image

New platform/api/ service owns students, cohorts, curriculum, progress,
scenario attempts, and student/admin sessions. No k3s, no --privileged,
no terminal runtime — starts instantly, unlike the old identity service
which booted an unused k3s cluster just to serve roster/progress APIs.

Provisioner calls (student and admin) are consolidated here too, in
provisioner-client.js, so it's the only service that talks to the
Provisioner directly.
This commit is contained in:
2026-08-18 12:22:58 -05:00
parent 42dfb1c31b
commit 6b52c9e1d2
18 changed files with 1176 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
// Thin client for the Provisioner's internal API — same role as
// platform/gateway/src/provisioner-client.js. Phase 2B consolidates ALL Provisioner access
// (both student launch/reset and admin prepare/destroy/reset) into this one service, per
// "environment orchestration calls" being a Platform API responsibility
// (docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md §1). The Gateway no longer calls the Provisioner
// directly — it proxies /api/environment/* to here instead.
const PROVISIONER_URL = process.env.PROVISIONER_URL || 'http://localhost:4100';
const PROVISIONER_SHARED_SECRET = process.env.PROVISIONER_SHARED_SECRET;
if (!PROVISIONER_SHARED_SECRET) {
console.error('FATAL: PROVISIONER_SHARED_SECRET is not set. Refusing to start.');
process.exit(1);
}
async function call(method, path, body) {
const res = await fetch(`${PROVISIONER_URL}${path}`, {
method,
headers: { 'Content-Type': 'application/json', 'X-Provisioner-Key': PROVISIONER_SHARED_SECRET },
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let json; try { json = JSON.parse(text); } catch { json = null; }
return { status: res.status, body: json };
}
const provisioner = {
getEnvironmentByToken: (token) => call('GET', `/environments/by-token/${encodeURIComponent(token)}`),
getEnvironment: (id) => call('GET', `/environments/${encodeURIComponent(id)}`),
getToken: (id) => call('GET', `/environments/${encodeURIComponent(id)}/token`),
listForStudent: (studentId) => call('GET', `/environments?student_id=${encodeURIComponent(studentId)}`),
provision: (studentId) => call('POST', '/environments', { student_id: studentId }),
reset: (id) => call('POST', `/environments/${encodeURIComponent(id)}/reset`),
destroy: (id) => call('DELETE', `/environments/${encodeURIComponent(id)}`),
events: (id) => call('GET', `/environments/${encodeURIComponent(id)}/events`),
};
module.exports = { provisioner };