verae-fleet/src/cli.js

145 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
/**
* verae-fleet — catalog, monitor, restart, pause.
*
* Local (no daemon): list
* Daemon: serve | monitor
* Against daemon: status | start | stop | pause | resume | restart
*/
import http from 'node:http';
import { loadFleet, listServices } from './load.js';
import { Supervisor } from './supervisor.js';
import { Monitor } from './monitor.js';
import { startControlServer } from './server.js';
const loaded = loadFleet();
const [cmd = 'help', target] = process.argv.slice(2);
const BASE = process.env.FLEET_URL || `http://127.0.0.1:${loaded.control.port}`;
function print(obj) {
process.stdout.write(`${typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2)}\n`);
}
function help() {
print(`verae-fleet
list services, config files, replica min/max
serve start floors + monitor + UI ${BASE}/
monitor same without printing extra
status GET daemon status
start <service> turn on, spawn up to min
stop <service|instance> turn off (service disable) or stop one replica
pause <service|instance> pause (does not count toward tree-node floor)
resume <service|instance>
restart <instance> kill + respawn
reconcile force floor check
Central spec: fleet.json
Machines: machines.json (add hosts to spread replicas)
Per service: services/<id>.json
Remote SSH: machines.json user + host + identityFile (path only)
ssh-check [id] test SSH login (default ns1)
`);
}
function api(method, pathname) {
return new Promise((resolve, reject) => {
const u = new URL(pathname, BASE);
const req = http.request(
{ hostname: u.hostname, port: u.port, path: u.pathname, method, timeout: 8000 },
(res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
try {
resolve({ status: res.statusCode, body: JSON.parse(raw) });
} catch {
resolve({ status: res.statusCode, body: raw });
}
});
},
);
req.on('error', reject);
req.end();
});
}
async function main() {
if (cmd === 'help' || cmd === '-h' || cmd === '--help') return help();
if (cmd === 'list') {
for (const r of listServices(loaded)) {
print(
`${r.id.padEnd(22)} min=${String(r.min).padStart(2)} max=${String(r.max).padStart(2)} floor=${r.keepFloor ? 'yes' : 'no '} managed=${r.managed ? 'yes' : 'no '} ${r.configPath}`,
);
}
print(`\ncentral replica spec: ${loaded.fleetPath}`);
print(`tree-node floor: min=${loaded.services['tree-node'].min} keepFloor=${loaded.services['tree-node'].keepFloor}`);
print('\nmachines:');
for (const m of loaded.machines) {
const ssh = m.kind === 'ssh' ? `${m.user}@${m.host} key=${m.identityFile}` : m.host;
print(` ${m.id.padEnd(12)} ${m.kind.padEnd(6)} ${m.enabled ? 'on ' : 'off'} cap=${m.capacity} ${ssh}`);
}
return;
}
if (cmd === 'ssh-check') {
const { sshCheck } = await import('./ssh.js');
const id = target || 'ns1';
const m = loaded.machines.find((x) => x.id === id);
if (!m) throw new Error(`unknown machine ${id}`);
print(await sshCheck(m));
return;
}
if (cmd === 'serve' || cmd === 'monitor') {
const sup = new Supervisor({ loaded });
for (const id of Object.keys(loaded.services)) {
const s = loaded.services[id];
if (s.managed && s.enabled && s.min > 0) await sup.startService(id);
}
const mon = new Monitor(sup);
await mon.tick();
mon.start();
startControlServer(sup, mon);
print(`fleet ${cmd} pid=${process.pid}\nUI ${BASE}/\nSIGINT stops all managed replicas`);
process.on('SIGINT', async () => {
mon.stop();
await sup.stopAll();
process.exit(0);
});
process.on('SIGTERM', async () => {
mon.stop();
await sup.stopAll();
process.exit(0);
});
return;
}
const instance = target && /-\d+$/.test(target);
const map = {
status: ['GET', '/api/status'],
reconcile: ['POST', '/api/reconcile'],
};
if (map[cmd] && !target) {
const [m, p] = map[cmd];
const r = await api(m, p);
print(r.body);
if (r.status >= 400) process.exitCode = 1;
return;
}
if (!target) {
help();
process.exitCode = 1;
return;
}
const pathName = instance
? `/api/instances/${target}/${cmd}`
: `/api/services/${target}/${cmd}`;
const r = await api('POST', pathName);
print(r.body);
if (r.status >= 400) process.exitCode = 1;
}
main().catch((err) => {
print({ error: err.message, hint: 'Is `verae-fleet serve` running?' });
process.exitCode = 1;
});