9d145865dd
Gives each lane its own slot-derived runtime (ports, detached process lifecycle, profile-driven hooks) and its own database/Redis logical index/.env file, so two lanes running the same repo's stack at once no longer share state. Machine-level DB/Redis credentials live at ~/.ccam/secrets.env (mode 0600, never returned by any route); a hook's output is redacted of that password (raw and URL-encoded forms) before it reaches a log file or the lane_hook_output websocket broadcast. Wired into provision/up/reset/remove; reset accepts --keep-db to skip the drop/recreate/migrate/reseed block entirely.
99 lines
3.8 KiB
JavaScript
99 lines
3.8 KiB
JavaScript
/**
|
|
* @file A lane's own database: create it once at provisioning and boot, drop
|
|
* it on remove. CCAM stays stack-agnostic here — `createdb` vs `mysqladmin
|
|
* create` vs `touch foo.db` genuinely differ, so the actual command lives in
|
|
* the profile's `db-create.sh`/`db-drop.sh` hooks (already in the A1
|
|
* allowlist); this module only decides WHEN to call them and guards WHICH
|
|
* name a drop may ever target.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const { assertManaged } = require("./worktree");
|
|
const { dbName, slotDirs } = require("./lane-slots");
|
|
const { runHook } = require("./lane-profile");
|
|
|
|
/**
|
|
* Where CCAM records that a slot's database has already been created.
|
|
*
|
|
* A hook can't be trusted to know this on its own without being stack-aware
|
|
* (a plain `createdb` errors on a database that already exists; `touch`
|
|
* wouldn't), so CCAM tracks it itself with one flag file per database name,
|
|
* beside the rest of a slot's runtime bookkeeping. This is also how `upLane`
|
|
* tells "freshly created" from "already existed" to decide whether to seed.
|
|
*/
|
|
function markerPath(slot, name) {
|
|
return path.join(slotDirs(slot).stateDir, `db-created-${name}`);
|
|
}
|
|
|
|
/**
|
|
* Ensure a lane's database exists, creating it via the profile's `db-create`
|
|
* hook the first time only.
|
|
*
|
|
* A no-op when the profile declares no `DB_PREFIX` — no name is derived, so
|
|
* no hook is ever called and nothing is created.
|
|
*
|
|
* @param {object} lane - Lane row; `slot` must already be allocated.
|
|
* @param {object} profile - Resolved profile.
|
|
* @param {{onLine?: Function, timeoutMs?: number}} [options]
|
|
* @returns {Promise<{name: string|null, created: boolean}>}
|
|
*/
|
|
async function ensureDatabase(lane, profile, options = {}) {
|
|
const name = dbName(profile, lane.slot);
|
|
if (!name) return { name: null, created: false };
|
|
|
|
const marker = markerPath(lane.slot, name);
|
|
if (fs.existsSync(marker)) return { name, created: false };
|
|
|
|
const result = await runHook(lane, profile, "db-create", [name], options);
|
|
if (result.code !== 0) {
|
|
throw Object.assign(new Error(`db-create hook exited ${result.code}`), {
|
|
code: "EDBCREATE",
|
|
logPath: result.logPath,
|
|
});
|
|
}
|
|
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
fs.writeFileSync(marker, new Date().toISOString());
|
|
return { name, created: true };
|
|
}
|
|
|
|
/**
|
|
* Drop a database via the profile's `db-drop` hook.
|
|
*
|
|
* Two checks run before anything is spawned, and neither trusts the caller:
|
|
* `assertManaged` (an adopted lane's data is the user's own, never CCAM's to
|
|
* destroy) and a name check against what THIS lane's slot actually derives —
|
|
* the lane's own database or its `_test` sibling, and nothing else. Only a
|
|
* name CCAM itself computed can ever reach the hook.
|
|
*
|
|
* @param {object} lane - Lane row; `slot` must already be allocated.
|
|
* @param {object} profile - Resolved profile.
|
|
* @param {string} name - The database to drop; must be `dbName` or `${dbName}_test`.
|
|
* @param {{onLine?: Function, timeoutMs?: number}} [options]
|
|
* @returns {Promise<{name: string}>}
|
|
*/
|
|
async function dropDatabase(lane, profile, name, options = {}) {
|
|
assertManaged(lane);
|
|
const derived = dbName(profile, lane.slot);
|
|
const allowed = derived && (name === derived || name === `${derived}_test`);
|
|
if (!allowed) {
|
|
throw Object.assign(new Error(`refusing to drop undeclared database: ${name}`), {
|
|
code: "EBADDBNAME",
|
|
});
|
|
}
|
|
|
|
const result = await runHook(lane, profile, "db-drop", [name], options);
|
|
if (result.code !== 0) {
|
|
throw Object.assign(new Error(`db-drop hook exited ${result.code}`), {
|
|
code: "EDBDROP",
|
|
logPath: result.logPath,
|
|
});
|
|
}
|
|
fs.rmSync(markerPath(lane.slot, name), { force: true });
|
|
return { name };
|
|
}
|
|
|
|
module.exports = { ensureDatabase, dropDatabase, markerPath };
|