268 lines
6.8 KiB
JavaScript
268 lines
6.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* run-vega-source-server.mjs — start the source-mounted SF server container on
|
|
* vega without requiring Docker Compose.
|
|
*
|
|
* Purpose: keep the local production server containerized while using the live
|
|
* checkout at /home/mhugo/code/singularity-forge as the source of truth.
|
|
*
|
|
* Consumer: `npm run docker:vega:up` on vega.
|
|
*/
|
|
import { spawnSync } from "node:child_process";
|
|
import { statSync } from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
const image = process.env.SF_VEGA_IMAGE || "sf-source-server:vega";
|
|
const bind = process.env.SF_VEGA_BIND || "127.0.0.1";
|
|
const workspace = resolve(process.env.SF_WORKSPACE_DIR || root);
|
|
const workspacesRoot = resolve(process.env.SF_WORKSPACES_DIR || dirname(root));
|
|
const sourceHostRoot = resolve(process.env.SF_SOURCE_HOST_ROOT || root);
|
|
const workspaceHost = resolve(process.env.SF_WORKSPACE_HOST_DIR || workspace);
|
|
const workspacesHost = resolve(
|
|
process.env.SF_WORKSPACES_HOST_DIR || workspacesRoot,
|
|
);
|
|
const sfHomeHost = resolve(process.env.SF_HOME_HOST_DIR || `${homedir()}/.sf`);
|
|
const name = process.env.SF_VEGA_CONTAINER || "sf-server-vega";
|
|
const port = process.env.SF_VEGA_PORT || "4000";
|
|
const uid = process.env.PUID || String(process.getuid?.() ?? 1000);
|
|
const gid = process.env.PGID || String(process.getgid?.() ?? 1000);
|
|
const dockerSocketGid = socketGroupId("/var/run/docker.sock");
|
|
const command = process.argv[2] ?? "up";
|
|
const skipImageBuild = process.env.SF_VEGA_SKIP_IMAGE_BUILD === "1";
|
|
|
|
if (command === "--help" || command === "-h" || command === "help") {
|
|
process.stdout.write(`Usage:
|
|
npm run docker:vega:up
|
|
SF_WORKSPACE_DIR=/path/to/repo npm run docker:vega:up
|
|
node scripts/run-vega-source-server.mjs print
|
|
node scripts/run-vega-source-server.mjs logs
|
|
node scripts/run-vega-source-server.mjs down
|
|
|
|
Environment:
|
|
SF_WORKSPACE_DIR initial repo mounted at /workspace, defaults to this checkout
|
|
SF_WORKSPACES_DIR repo parent mounted at /workspaces, defaults to ../
|
|
SF_VEGA_BIND host bind address, defaults to 127.0.0.1
|
|
SF_VEGA_PORT shared webserver port, defaults to 4000
|
|
SF_VEGA_CONTAINER explicit container name
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (command === "print") {
|
|
process.stdout.write(
|
|
JSON.stringify(
|
|
{
|
|
name,
|
|
image,
|
|
bind,
|
|
port,
|
|
workspace,
|
|
workspacesRoot,
|
|
sourceHostRoot,
|
|
workspaceHost,
|
|
workspacesHost,
|
|
sfHomeHost,
|
|
sfSource: root,
|
|
},
|
|
null,
|
|
2,
|
|
) + "\n",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (command === "logs") {
|
|
run("docker", ["logs", "-f", "--tail=200", name]);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (command === "down") {
|
|
await requestDrain(port);
|
|
drainContainer(name);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (command !== "up") {
|
|
process.stderr.write(`Unknown command: ${command}\n`);
|
|
process.exit(2);
|
|
}
|
|
|
|
const allowedOrigins =
|
|
process.env.SF_WEB_ALLOWED_ORIGINS ||
|
|
`http://127.0.0.1:${port},http://localhost:${port}`;
|
|
|
|
if (!skipImageBuild) {
|
|
run(
|
|
"docker",
|
|
["build", "-f", "docker/Dockerfile.source-server", "-t", image, "."],
|
|
{ env: dockerBuildEnv() },
|
|
);
|
|
}
|
|
|
|
await requestDrain(port);
|
|
drainContainer(name);
|
|
|
|
run("docker", [
|
|
"run",
|
|
"-d",
|
|
"--name",
|
|
name,
|
|
"--restart",
|
|
"unless-stopped",
|
|
"--user",
|
|
`${uid}:${gid}`,
|
|
...(dockerSocketGid ? ["--group-add", dockerSocketGid] : []),
|
|
"-p",
|
|
`${bind}:${port}:4000`,
|
|
"-e",
|
|
"HOME=/home/node",
|
|
"-e",
|
|
"NODE_ENV=development",
|
|
"-e",
|
|
"SF_SOURCE_ROOT=/opt/sf",
|
|
"-e",
|
|
"SF_RUNTIME_SOURCE_ROOT=/opt/sf",
|
|
"-e",
|
|
"SF_RELEASE_MANIFEST=/opt/sf/dist/sf-release-manifest.json",
|
|
"-e",
|
|
`SF_WEB_PROJECT_CWD=${workspace}`,
|
|
"-e",
|
|
`SF_WORKSPACES_DIR=${workspacesRoot}`,
|
|
"-e",
|
|
`SF_SOURCE_HOST_ROOT=${sourceHostRoot}`,
|
|
"-e",
|
|
`SF_WORKSPACE_HOST_DIR=${workspaceHost}`,
|
|
"-e",
|
|
`SF_WORKSPACES_HOST_DIR=${workspacesHost}`,
|
|
"-e",
|
|
`SF_HOME_HOST_DIR=${sfHomeHost}`,
|
|
"-e",
|
|
"HOSTNAME=0.0.0.0",
|
|
"-e",
|
|
"PORT=4000",
|
|
"-e",
|
|
"SF_WEB_HOST=0.0.0.0",
|
|
"-e",
|
|
"SF_WEB_PORT=4000",
|
|
"-e",
|
|
`SF_WEB_ALLOWED_ORIGINS=${allowedOrigins}`,
|
|
"-e",
|
|
"SF_DEV_SERVER_WATCH=1",
|
|
"-e",
|
|
"SF_RPC_SHUTDOWN_GRACE_MS=600000",
|
|
"-v",
|
|
`${sourceHostRoot}:/opt/sf`,
|
|
"-v",
|
|
`${workspaceHost}:/workspace`,
|
|
"-v",
|
|
`${workspacesHost}:/workspaces`,
|
|
"-v",
|
|
`${workspacesHost}:${workspacesRoot}`,
|
|
"-v",
|
|
`${sfHomeHost}:/home/node/.sf`,
|
|
"-v",
|
|
`${homedir()}/.gitconfig:/home/node/.gitconfig:ro`,
|
|
"-v",
|
|
"/var/run/docker.sock:/var/run/docker.sock",
|
|
image,
|
|
"node",
|
|
"/opt/sf/dist/web/standalone/server.js",
|
|
]);
|
|
|
|
process.stdout.write(`${name} listening on ${bind}:${port}\n`);
|
|
process.stdout.write(`SF source: ${root}\n`);
|
|
process.stdout.write(`Initial workspace: ${workspace}\n`);
|
|
process.stdout.write(`Workspace parent: ${workspacesRoot}\n`);
|
|
|
|
function run(command, args, options = {}) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: root,
|
|
stdio: "inherit",
|
|
env: options.env ?? process.env,
|
|
});
|
|
if (result.status !== 0 && !options.allowFailure) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
function dockerBuildEnv() {
|
|
return {
|
|
...process.env,
|
|
DOCKER_BUILDKIT: "1",
|
|
BUILDKIT_PROGRESS: process.env.BUILDKIT_PROGRESS || "plain",
|
|
DEBIAN_FRONTEND: "noninteractive",
|
|
};
|
|
}
|
|
|
|
function socketGroupId(path) {
|
|
try {
|
|
return String(statSync(path).gid);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function drainContainer(containerName) {
|
|
if (!containerExists(containerName)) return;
|
|
const stopTime = process.env.SF_VEGA_DRAIN_STOP_TIME || "610";
|
|
run("docker", ["stop", "-t", stopTime, containerName], {
|
|
allowFailure: true,
|
|
});
|
|
run("docker", ["rm", "-f", containerName], { allowFailure: true });
|
|
}
|
|
|
|
async function requestDrain(targetPort) {
|
|
if (!containerExists(name)) return;
|
|
const baseUrl = `http://${bind}:${targetPort}`;
|
|
try {
|
|
const response = await fetch(`${baseUrl}/api/drain`, {
|
|
method: "POST",
|
|
headers: authHeaders(),
|
|
});
|
|
if (!response.ok && response.status !== 404) {
|
|
throw new Error(`drain returned ${response.status}`);
|
|
}
|
|
if (response.ok) {
|
|
await waitForDrainHealthz(baseUrl);
|
|
}
|
|
} catch (error) {
|
|
process.stdout.write(
|
|
`drain preflight skipped: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}\n`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function waitForDrainHealthz(baseUrl) {
|
|
const deadline = Date.now() + 10_000;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const response = await fetch(`${baseUrl}/api/healthz`, {
|
|
cache: "no-store",
|
|
headers: authHeaders(),
|
|
});
|
|
if (response.status === 503) return;
|
|
} catch {
|
|
return;
|
|
}
|
|
await new Promise((resolveDelay) => setTimeout(resolveDelay, 250));
|
|
}
|
|
}
|
|
|
|
function authHeaders() {
|
|
const token = process.env.SF_WEB_AUTH_TOKEN;
|
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
}
|
|
|
|
function containerExists(containerName) {
|
|
const result = spawnSync("docker", ["container", "inspect", containerName], {
|
|
cwd: root,
|
|
stdio: "ignore",
|
|
env: process.env,
|
|
});
|
|
return result.status === 0;
|
|
}
|