import { Router } from 'express'; import { access } from 'node:fs/promises'; import { constants } from 'node:fs'; import { config, missingEnvVars } from '../config.js'; import { spawnCommand, runRestic } from '../executor.js'; export const healthRouter = Router(); healthRouter.get('/', async (_req, res) => { const missing = missingEnvVars(); const checks = { config: checkValue(!missing.length, missing.length ? `missing: ${missing.join(', ')}` : 'ok'), commands: await checkCommands({ incus: ['version'], zfs: ['version'], zpool: ['version'], restic: ['version'], udevadm: ['--version'], dd: ['--version'], }), zfsPool: await checkZfsPool(), zvol: await checkZvolAccess(), resticRepository: await checkResticRepository(), }; const ok = Object.values(checks).every((value) => value.ok); res.status(ok ? 200 : 503).json({ ok, checks }); }); async function checkCommands(commands) { const results = {}; await Promise.all(Object.entries(commands).map(async ([command, args]) => { results[command] = await checkCommand(command, args); })); const failed = Object.entries(results).filter(([, result]) => !result.ok); return { ok: failed.length === 0, message: failed.length ? `failed: ${failed.map(([command]) => command).join(', ')}` : 'ok', details: results, }; } async function checkCommand(command, args) { try { const result = await spawnCommand(command, args, { ignoreExitCode: true }); return checkValue(result.exitCode === 0, result.exitCode === 0 ? 'ok' : result.stderr.trim() || result.stdout.trim() || 'failed'); } catch (error) { return checkValue(false, error.message); } } async function checkZfsPool() { if (!config.zfsPoolName) return checkValue(false, 'ZFS_POOL_NAME is not configured'); try { const result = await spawnCommand('zpool', ['list', '-Hp', '-o', 'name,size,alloc,free,cap', config.zfsPoolName], { ignoreExitCode: true }); if (result.exitCode !== 0) return checkValue(false, result.stderr.trim() || 'zpool list failed'); const [name, size, allocated, free, capacity] = result.stdout.trim().split('\t'); return { ok: true, message: 'ok', details: { name, size: Number(size), allocated: Number(allocated), free: Number(free), capacity }, }; } catch (error) { return checkValue(false, error.message); } } async function checkZvolAccess() { try { await access('/dev/zvol', constants.R_OK); return checkValue(true, 'ok'); } catch (error) { return checkValue(false, error.message); } } async function checkResticRepository() { try { const result = await runRestic(['--no-lock', 'snapshots', '--json'], { ignoreExitCode: true }); return checkValue(result.exitCode === 0, result.exitCode === 0 ? 'ok' : result.stderr.trim() || 'restic repository check failed'); } catch (error) { return checkValue(false, error.message); } } function checkValue(ok, message, details = null) { return { ok, message, ...(details ? { details } : {}) }; }