Files
vibn-agent-runner/vibn-frontend/lib/server/infra-coolify-health.ts

117 lines
3.4 KiB
TypeScript

/**
* Coolify connectivity checks for ops / uptime monitors.
*
* - HTTP API (token) — provisioning via REST
* - SSH → Docker on host — required for shell.exec / dev containers
*/
import { listServers } from '@/lib/coolify';
import { runOnCoolifyHost } from '@/lib/coolify-ssh';
export interface CoolifyInfraHealthReport {
checkedAt: string;
ssh: {
configured: boolean;
missingEnvVars: string[];
reachable?: boolean;
latencyMs?: number;
dockerDaemonOk?: boolean;
/** docker Server.Version when daemon responds */
dockerVersion?: string;
error?: string;
};
api: {
configured: boolean;
reachable?: boolean;
latencyMs?: number;
serverCount?: number;
error?: string;
};
}
export function getCoolifySshConfigGap(): {
configured: boolean;
missingEnvVars: string[];
} {
const missing: string[] = [];
if (!process.env.COOLIFY_SSH_HOST?.trim()) missing.push('COOLIFY_SSH_HOST');
if (!process.env.COOLIFY_SSH_PRIVATE_KEY_B64?.trim()) {
missing.push('COOLIFY_SSH_PRIVATE_KEY_B64');
}
return { configured: missing.length === 0, missingEnvVars: missing };
}
/** True when SSH is wired and `docker` responds on the Coolify host. */
export async function runCoolifyInfraHealthProbe(): Promise<CoolifyInfraHealthReport> {
const checkedAt = new Date().toISOString();
const sshGap = getCoolifySshConfigGap();
const report: CoolifyInfraHealthReport = {
checkedAt,
ssh: {
configured: sshGap.configured,
missingEnvVars: sshGap.missingEnvVars,
},
api: {
configured: !!process.env.COOLIFY_API_TOKEN?.trim(),
},
};
if (report.api.configured) {
const t0 = Date.now();
try {
const servers = await listServers();
report.api.reachable = true;
report.api.latencyMs = Date.now() - t0;
report.api.serverCount = Array.isArray(servers) ? servers.length : 0;
} catch (e) {
report.api.reachable = false;
report.api.latencyMs = Date.now() - t0;
report.api.error = e instanceof Error ? e.message : String(e);
}
} else {
report.api.error = 'COOLIFY_API_TOKEN is not set';
}
if (!sshGap.configured) {
report.ssh.error =
`Missing: ${sshGap.missingEnvVars.join(', ')} — dev containers need SSH to the Docker host (see lib/coolify-ssh.ts).`;
return report;
}
const tSsh = Date.now();
try {
const res = await runOnCoolifyHost(`docker info --format '{{.ServerVersion}}'`, {
timeoutMs: 15_000,
maxBytes: 8192,
});
report.ssh.latencyMs = Date.now() - tSsh;
report.ssh.reachable = true;
const ver = res.stdout.trim().split(/\s+/)[0]?.trim() ?? '';
if (res.code === 0 && ver.length > 0) {
report.ssh.dockerDaemonOk = true;
report.ssh.dockerVersion = ver;
} else {
report.ssh.dockerDaemonOk = false;
report.ssh.error = `docker probe exit ${res.code}: ${(res.stderr || res.stdout || '(empty)').slice(0, 600)}`;
}
} catch (e) {
report.ssh.latencyMs = Date.now() - tSsh;
report.ssh.reachable = false;
report.ssh.dockerDaemonOk = false;
report.ssh.error = e instanceof Error ? e.message : String(e);
}
return report;
}
export function isCoolifyInfraOperational(report: CoolifyInfraHealthReport): boolean {
return (
report.ssh.configured &&
report.ssh.reachable === true &&
report.ssh.dockerDaemonOk === true &&
report.api.configured &&
report.api.reachable === true
);
}