145 lines
4.7 KiB
JavaScript
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;
|
|
});
|