remove: vega docker / source-server self-upgrade path
Now superseded by k3s self-deploy: build → push → kubectl set image performs rolling rollout, so the in-band docker-compose-on-vega upgrade path (docker:vega:* scripts, /api/server-upgrade route, Dockerfile.source-server, docker-compose.vega.yaml, projects-view "Upgrade Server" button) is dead code. The k3s deploy workflow (.forgejo/workflows/self-deploy.yml) and sf-server kustomization under /srv/infra/clusters/default/tenants/hugo/apps/sf-server/ are the only deploy path going forward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
06b1fefd35
commit
743af0e28b
5 changed files with 0 additions and 684 deletions
|
|
@ -1,28 +0,0 @@
|
|||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Local source-mounted SF server image.
|
||||
#
|
||||
# Purpose: run the vega development/production server inside a container while
|
||||
# keeping /home/mhugo/code/singularity-forge as the source of truth.
|
||||
#
|
||||
# Consumer: docker/docker-compose.vega.yaml.
|
||||
|
||||
FROM node:26.1-slim
|
||||
|
||||
ENV NODE_ENV=development
|
||||
ENV HOME=/home/node
|
||||
ENV SF_WEB_PREFER_SOURCE=0
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
docker-buildx \
|
||||
docker-cli \
|
||||
git \
|
||||
libsecret-1-0 \
|
||||
tini \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt/sf
|
||||
EXPOSE 4000
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["npm", "run", "sf:server", "--", "--host", "0.0.0.0", "--port", "4000"]
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
services:
|
||||
sf-server:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile.source-server
|
||||
container_name: ${SF_VEGA_CONTAINER:-sf-server-vega}
|
||||
working_dir: /opt/sf
|
||||
user: "${PUID:-1000}:${PGID:-1000}"
|
||||
group_add:
|
||||
- "${DOCKER_GID:-999}"
|
||||
ports:
|
||||
- "${SF_VEGA_BIND:-127.0.0.1}:4000:4000"
|
||||
volumes:
|
||||
- ../:/opt/sf
|
||||
- ${SF_WORKSPACE_DIR:-..}:/workspace
|
||||
- ${SF_WORKSPACES_DIR:-..}:/workspaces
|
||||
- ${SF_WORKSPACES_DIR:-/home/mhugo/code}:${SF_WORKSPACES_DIR:-/home/mhugo/code}
|
||||
- ${HOME}/.sf:/home/node/.sf
|
||||
- ${HOME}/.gitconfig:/home/node/.gitconfig:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
HOME: /home/node
|
||||
NODE_ENV: development
|
||||
SF_SOURCE_ROOT: /opt/sf
|
||||
SF_RUNTIME_SOURCE_ROOT: /opt/sf
|
||||
SF_RELEASE_MANIFEST: /opt/sf/dist/sf-release-manifest.json
|
||||
SF_WEB_PROJECT_CWD: ${SF_WORKSPACE_DIR:-/home/mhugo/code/singularity-forge}
|
||||
SF_WORKSPACES_DIR: ${SF_WORKSPACES_DIR:-/home/mhugo/code}
|
||||
SF_SOURCE_HOST_ROOT: ${SF_SOURCE_HOST_ROOT:-/home/mhugo/code/singularity-forge}
|
||||
SF_WORKSPACE_HOST_DIR: ${SF_WORKSPACE_HOST_DIR:-/home/mhugo/code/singularity-forge}
|
||||
SF_WORKSPACES_HOST_DIR: ${SF_WORKSPACES_HOST_DIR:-/home/mhugo/code}
|
||||
SF_HOME_HOST_DIR: ${SF_HOME_HOST_DIR:-/home/mhugo/.sf}
|
||||
SF_WEB_HOST: 0.0.0.0
|
||||
SF_WEB_PORT: "4000"
|
||||
HOSTNAME: 0.0.0.0
|
||||
PORT: "4000"
|
||||
SF_WEB_ALLOWED_ORIGINS: ${SF_WEB_ALLOWED_ORIGINS:-http://127.0.0.1:4000,http://localhost:4000}
|
||||
SF_DEV_SERVER_WATCH: "1"
|
||||
SF_RPC_SHUTDOWN_GRACE_MS: "600000"
|
||||
command:
|
||||
- node
|
||||
- /opt/sf/dist/web/standalone/server.js
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- node -e "fetch('http://127.0.0.1:4000/api/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 60s
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
#!/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;
|
||||
}
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* upgrade-vega-source-server.mjs — blue/green upgrade the shared vega SF
|
||||
* webserver.
|
||||
*
|
||||
* Purpose: prove a candidate source-mounted server on a side port before
|
||||
* replacing the shared production container on port 4000.
|
||||
*
|
||||
* Consumer: `npm run docker:vega:upgrade` locally and Forgejo/host-side deploy
|
||||
* automation when vega is the target.
|
||||
*/
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const bind = process.env.SF_VEGA_BIND || "127.0.0.1";
|
||||
const prodName = process.env.SF_VEGA_CONTAINER || "sf-server-vega";
|
||||
const candidateName =
|
||||
process.env.SF_VEGA_CANDIDATE_CONTAINER || "sf-server-vega-candidate";
|
||||
const prodPort = process.env.SF_VEGA_PORT || "4000";
|
||||
const candidatePort = process.env.SF_VEGA_CANDIDATE_PORT || "4001";
|
||||
const workspacesRoot = process.env.SF_WORKSPACES_DIR || dirname(root);
|
||||
const skipBuild = process.env.SF_VEGA_UPGRADE_SKIP_BUILD === "1";
|
||||
const probeBind = process.env.SF_VEGA_PROBE_HOST || bind;
|
||||
|
||||
if (!skipBuild) {
|
||||
run("npm", ["run", "build:web-host"], { env: buildEnv() });
|
||||
run(process.execPath, [
|
||||
"scripts/generate-release-manifest.mjs",
|
||||
"--out",
|
||||
"dist/sf-release-manifest.json",
|
||||
]);
|
||||
}
|
||||
run(
|
||||
"docker",
|
||||
[
|
||||
"build",
|
||||
"-f",
|
||||
"docker/Dockerfile.source-server",
|
||||
"-t",
|
||||
process.env.SF_VEGA_IMAGE || "sf-source-server:vega",
|
||||
".",
|
||||
],
|
||||
{ env: dockerBuildEnv() },
|
||||
);
|
||||
|
||||
startServer(candidateName, candidatePort);
|
||||
await probeServer(candidatePort, "candidate");
|
||||
|
||||
await requestDrain(prodPort, "prod");
|
||||
drainContainer(prodName);
|
||||
startServer(prodName, prodPort);
|
||||
await probeServer(prodPort, "prod");
|
||||
|
||||
await requestDrain(candidatePort, "candidate");
|
||||
drainContainer(candidateName);
|
||||
process.stdout.write(
|
||||
`sf server upgraded: ${prodName} is healthy on ${bind}:${prodPort}\n`,
|
||||
);
|
||||
|
||||
function startServer(name, port) {
|
||||
run("node", ["scripts/run-vega-source-server.mjs", "up"], {
|
||||
env: {
|
||||
...process.env,
|
||||
SF_VEGA_CONTAINER: name,
|
||||
SF_VEGA_PORT: port,
|
||||
SF_VEGA_SKIP_IMAGE_BUILD: "1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function probeServer(port, label) {
|
||||
const baseUrl = `http://${probeBind}:${port}`;
|
||||
const checks = [
|
||||
["healthz", `${baseUrl}/api/healthz`],
|
||||
["ready", `${baseUrl}/api/ready`],
|
||||
["version", `${baseUrl}/api/version`],
|
||||
[
|
||||
"projects",
|
||||
`${baseUrl}/api/projects?root=${encodeURIComponent(workspacesRoot)}&detail=true`,
|
||||
],
|
||||
];
|
||||
const deadline = Date.now() + 60_000;
|
||||
let lastError = "";
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
for (const [name, url] of checks) {
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`${name} returned ${response.status}`);
|
||||
}
|
||||
if (name === "projects") {
|
||||
const projects = await response.json();
|
||||
if (!Array.isArray(projects) || projects.length === 0) {
|
||||
throw new Error("projects returned no configured repos");
|
||||
}
|
||||
} else {
|
||||
await response.arrayBuffer();
|
||||
}
|
||||
}
|
||||
process.stdout.write(`${label} probes passed on ${baseUrl}\n`);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
showLogs(label === "candidate" ? candidateName : prodName);
|
||||
throw new Error(`${label} probes failed: ${lastError}`);
|
||||
}
|
||||
|
||||
async function requestDrain(port, label) {
|
||||
const baseUrl = `http://${probeBind}:${port}`;
|
||||
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(port, label);
|
||||
}
|
||||
} catch (error) {
|
||||
process.stdout.write(
|
||||
`${label} drain preflight skipped: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForDrainHealthz(port, label) {
|
||||
const baseUrl = `http://${probeBind}:${port}`;
|
||||
const deadline = Date.now() + 10_000;
|
||||
let lastStatus = "unknown";
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/healthz`, {
|
||||
cache: "no-store",
|
||||
headers: authHeaders(),
|
||||
});
|
||||
lastStatus = String(response.status);
|
||||
if (response.status === 503) {
|
||||
process.stdout.write(`${label} drain acknowledged on ${baseUrl}\n`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
lastStatus = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
await delay(250);
|
||||
}
|
||||
process.stdout.write(
|
||||
`${label} drain did not surface on healthz before stop (last=${lastStatus})\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function authHeaders() {
|
||||
const token = process.env.SF_WEB_AUTH_TOKEN;
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
function showLogs(name) {
|
||||
spawnSync("docker", ["logs", "--tail=120", name], {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
function buildEnv() {
|
||||
const nodeOptions = [process.env.NODE_OPTIONS, "--disable-warning=DEP0205"]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return {
|
||||
...process.env,
|
||||
NODE_ENV: "production",
|
||||
NEXT_TELEMETRY_DISABLED: "1",
|
||||
NODE_OPTIONS: nodeOptions,
|
||||
NPM_CONFIG_UPDATE_NOTIFIER: "false",
|
||||
npm_config_update_notifier: "false",
|
||||
};
|
||||
}
|
||||
|
||||
function dockerBuildEnv() {
|
||||
return {
|
||||
...process.env,
|
||||
DOCKER_BUILDKIT: "1",
|
||||
BUILDKIT_PROGRESS: process.env.BUILDKIT_PROGRESS || "plain",
|
||||
DEBIAN_FRONTEND: "noninteractive",
|
||||
};
|
||||
}
|
||||
|
||||
function drainContainer(name) {
|
||||
if (!containerExists(name)) return;
|
||||
// 610s: matches SF_RPC_SHUTDOWN_GRACE_MS=600000 in rpc-mode's
|
||||
// graceful-shutdown handler with a 10s safety margin for Node exit.
|
||||
// Normal drains finish in <1s; the long ceiling is for pathological
|
||||
// lock contention so queued self-feedback writes are never lost
|
||||
// across an upgrade. Override per-deployment via env if needed.
|
||||
const stopTime = process.env.SF_VEGA_DRAIN_STOP_TIME || "610";
|
||||
run("docker", ["stop", "-t", stopTime, name], { allowFailure: true });
|
||||
run("docker", ["rm", "-f", name], { allowFailure: true });
|
||||
}
|
||||
|
||||
function containerExists(name) {
|
||||
const result = spawnSync("docker", ["container", "inspect", name], {
|
||||
cwd: root,
|
||||
stdio: "ignore",
|
||||
env: process.env,
|
||||
});
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { statSync } from "node:fs";
|
||||
import { getgid, getuid } from "node:process";
|
||||
|
||||
import { verifyAuthToken } from "../../../lib/auth-guard";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
const authError = verifyAuthToken(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const sourceHostRoot =
|
||||
process.env.SF_SOURCE_HOST_ROOT ?? "/home/mhugo/code/singularity-forge";
|
||||
const workspaceHost =
|
||||
process.env.SF_WORKSPACE_HOST_DIR ??
|
||||
process.env.SF_WEB_PROJECT_CWD ??
|
||||
sourceHostRoot;
|
||||
const workspacesHost =
|
||||
process.env.SF_WORKSPACES_HOST_DIR ?? "/home/mhugo/code";
|
||||
const sfHomeHost = process.env.SF_HOME_HOST_DIR ?? "/home/mhugo/.sf";
|
||||
const image = process.env.SF_VEGA_IMAGE ?? "sf-source-server:vega";
|
||||
const name = `sf-server-vega-upgrader-${randomUUID().slice(0, 8)}`;
|
||||
const uid = process.env.PUID ?? String(getuid?.() ?? 1000);
|
||||
const gid = process.env.PGID ?? String(getgid?.() ?? 1000);
|
||||
const dockerSocketGid = socketGroupId("/var/run/docker.sock");
|
||||
|
||||
const args = [
|
||||
"run",
|
||||
"-d",
|
||||
"--rm",
|
||||
"--name",
|
||||
name,
|
||||
"--network",
|
||||
"host",
|
||||
"--user",
|
||||
`${uid}:${gid}`,
|
||||
...(dockerSocketGid ? ["--group-add", dockerSocketGid] : []),
|
||||
"-v",
|
||||
`${sourceHostRoot}:/opt/sf`,
|
||||
"-v",
|
||||
`${workspaceHost}:/workspace`,
|
||||
"-v",
|
||||
`${workspacesHost}:/workspaces`,
|
||||
"-v",
|
||||
`${workspacesHost}:${workspacesHost}`,
|
||||
"-v",
|
||||
`${sfHomeHost}:/home/node/.sf`,
|
||||
"-v",
|
||||
"/var/run/docker.sock:/var/run/docker.sock",
|
||||
"-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",
|
||||
`SF_WORKSPACE_DIR=${workspaceHost}`,
|
||||
"-e",
|
||||
`SF_WORKSPACES_DIR=${workspacesHost}`,
|
||||
"-e",
|
||||
"SF_VEGA_PORT=4000",
|
||||
"-e",
|
||||
"SF_VEGA_CANDIDATE_PORT=4001",
|
||||
"-e",
|
||||
"SF_VEGA_PROBE_HOST=127.0.0.1",
|
||||
"-e",
|
||||
"DOCKER_BUILDKIT=1",
|
||||
"-e",
|
||||
"BUILDKIT_PROGRESS=plain",
|
||||
image,
|
||||
"node",
|
||||
"/opt/sf/scripts/upgrade-vega-source-server.mjs",
|
||||
];
|
||||
|
||||
try {
|
||||
const result = spawnSync("docker", args, {
|
||||
cwd: "/opt/sf",
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || result.stdout || "docker run failed");
|
||||
}
|
||||
return Response.json(
|
||||
{ triggered: true, upgrader: name, containerId: result.stdout.trim() },
|
||||
{ status: 202, headers: { "Cache-Control": "no-store" } },
|
||||
);
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
{ error: error instanceof Error ? error.message : String(error) },
|
||||
{ status: 500, headers: { "Cache-Control": "no-store" } },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function socketGroupId(path: string): string | null {
|
||||
try {
|
||||
return String(statSync(path).gid);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue