Initial import of verae-fleet from zapier monorepo
This commit is contained in:
commit
9b70f1c03b
42 changed files with 3084 additions and 0 deletions
145
src/cli.js
Normal file
145
src/cli.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
#!/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;
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue