// Lab-host abstraction (correction D) — resolves a `lab_hosts` row into a connected dockerode // client. Nothing in this module or its callers may assume Docker is local; the endpoint // always comes from a `lab_hosts` row, whether that row points at a Unix socket (Phase 1 dev) // or a remote TCP+TLS endpoint (production Lab Host VM, per docs/LAB_ARCHITECTURE.md §1). const fs = require('fs'); const Docker = require('dockerode'); // Caches one dockerode client per lab_host_id — cheap to keep around, avoids re-parsing TLS // material on every call. const clientCache = new Map(); function buildClient(labHost) { if (labHost.endpoint.startsWith('unix://')) { return new Docker({ socketPath: labHost.endpoint.replace('unix://', '') }); } if (labHost.endpoint.startsWith('tcp://')) { const url = new URL(labHost.endpoint.replace('tcp://', 'http://')); const opts = { host: url.hostname, port: Number(url.port) || 2376, protocol: 'https' }; if (labHost.tls_ca_path && labHost.tls_cert_path && labHost.tls_key_path) { opts.ca = fs.readFileSync(labHost.tls_ca_path); opts.cert = fs.readFileSync(labHost.tls_cert_path); opts.key = fs.readFileSync(labHost.tls_key_path); } else { // ponytail: no TLS material configured for a remote endpoint is a real misconfiguration, // not a silently-degraded mode — fail loudly rather than connect unauthenticated to a // remote Docker API (see docs/PHASE1_PLATFORM_FOUNDATION.md §6 on why that cert matters // as much as the Provisioner API's own shared secret). throw new Error( `lab_host '${labHost.lab_host_id}' has a tcp:// endpoint but no TLS cert/key/ca configured — refusing to connect unauthenticated.` ); } return new Docker(opts); } throw new Error(`lab_host '${labHost.lab_host_id}' has an unrecognized endpoint scheme: ${labHost.endpoint}`); } function clientFor(labHost) { let client = clientCache.get(labHost.lab_host_id); if (!client) { client = buildClient(labHost); clientCache.set(labHost.lab_host_id, client); } return client; } // Phase 1: no scheduler, just "the one enabled row" (docs/PHASE1_PLATFORM_FOUNDATION.md §4). async function selectLabHost(pool) { const { rows } = await pool.query( `SELECT * FROM lab_hosts WHERE enabled = true ORDER BY created_at ASC LIMIT 1` ); if (!rows.length) throw new Error('No enabled lab_hosts row configured — cannot provision.'); return rows[0]; } async function healthCheck(labHost) { try { const client = clientFor(labHost); const info = await client.info(); return { ok: true, containers: info.Containers, serverVersion: info.ServerVersion }; } catch (e) { return { ok: false, error: e.message }; } } module.exports = { clientFor, selectLabHost, healthCheck };