Attempt 2 of a management ui.
This commit is contained in:
54 files changed
+8804
-267
No files matched your search
@@ -0,0 +1,90 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Every call here runs `docker compose` with `cwd` set to a stack's own
|
||||
* directory (stacks/<slug>/) rather than passing explicit -f/--env-file
|
||||
* flags — compose already auto-loads `compose.yaml` and `.env` from the
|
||||
* current directory, and this is the same convention Dockge itself uses,
|
||||
* so a stack behaves identically whether it's started by this app, by
|
||||
* Dockge, or by hand from the command line.
|
||||
*
|
||||
* This app's own container needs the Docker socket mounted and the
|
||||
* `docker compose` CLI installed (see manager/Dockerfile) — these calls
|
||||
* are executed by the HOST's dockerd via that socket, not by a nested
|
||||
* Docker daemon inside this container.
|
||||
*/
|
||||
|
||||
export interface DockerCommandResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
async function runDockerCompose(stackDir: string, args: string[]): Promise<DockerCommandResult> {
|
||||
const result = await execFileAsync("docker", ["compose", ...args], {
|
||||
cwd: stackDir,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function composeUp(stackDir: string, options: { forceRecreate?: boolean } = {}): Promise<void> {
|
||||
const args = ["up", "-d"];
|
||||
if (options.forceRecreate) args.push("--force-recreate");
|
||||
await runDockerCompose(stackDir, args);
|
||||
}
|
||||
|
||||
export async function composeDown(stackDir: string): Promise<void> {
|
||||
await runDockerCompose(stackDir, ["down"]);
|
||||
}
|
||||
|
||||
export async function composeRestart(stackDir: string): Promise<void> {
|
||||
await runDockerCompose(stackDir, ["restart"]);
|
||||
}
|
||||
|
||||
export async function composeLogs(stackDir: string, tailLines = 200): Promise<string> {
|
||||
const result = await runDockerCompose(stackDir, ["logs", "--no-color", "--tail", String(tailLines)]);
|
||||
// Compose writes container logs to stderr, not stdout — this is normal
|
||||
// (docker's own `logs` command does the same), not a failure signal.
|
||||
return result.stdout + result.stderr;
|
||||
}
|
||||
|
||||
export type StackRunState = "running" | "stopped" | "unknown";
|
||||
|
||||
/** Best-effort — a stack that's never been started at all reports "stopped", not an error. */
|
||||
export async function composeStatus(stackDir: string): Promise<StackRunState> {
|
||||
try {
|
||||
const result = await runDockerCompose(stackDir, ["ps", "--status", "running", "--format", "json"]);
|
||||
return result.stdout.trim().length > 0 ? "running" : "stopped";
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export async function composeExec(stackDir: string, serviceName: string, command: string[]): Promise<void> {
|
||||
await runDockerCompose(stackDir, ["exec", "-T", serviceName, ...command]);
|
||||
}
|
||||
|
||||
/** Applies pending Prisma migrations inside a freshly-started business container — retries briefly since the container's Node process may take a moment to come up after `composeUp`. */
|
||||
export async function runMigrations(stackDir: string, serviceName: string): Promise<void> {
|
||||
const maxAttempts = 6;
|
||||
let lastError: unknown;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
try {
|
||||
await composeExec(stackDir, serviceName, ["npx", "prisma", "migrate", "deploy"]);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
throw lastError instanceof Error ? lastError : new Error("Migrations failed after retries.");
|
||||
}
|
||||
|
||||
/** Caddy doesn't watch its sites/ directory — adding or removing a business's .caddy file needs this to take effect. */
|
||||
export async function reloadCaddy(): Promise<void> {
|
||||
const containerName = process.env.CADDY_CONTAINER_NAME ?? "next-chair-caddy";
|
||||
await execFileAsync("docker", ["exec", containerName, "caddy", "reload", "--config", "/etc/caddy/Caddyfile"]);
|
||||
}
|
||||
Reference in new issue
Block a user