verae-fleet/src/agent.js

86 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Remote host agent: spawn/kill fleet workers so the control plane can spread replicas.
*
* FLEET_AGENT_PORT=3851 node src/agent.js
*
* Bind loopback or a private interface. Do not publish NATS.
*/
import http from 'node:http';
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const WORKER = path.join(path.dirname(fileURLToPath(import.meta.url)), 'worker.js');
const PORT = Number(process.env.FLEET_AGENT_PORT || 3851);
const BIND = process.env.FLEET_AGENT_BIND || '127.0.0.1';
const children = new Map();
function json(res, code, obj) {
res.writeHead(code, { 'content-type': 'application/json' });
res.end(JSON.stringify(obj));
}
async function readBody(req) {
const chunks = [];
for await (const c of req) chunks.push(c);
const raw = Buffer.concat(chunks).toString('utf8');
return raw ? JSON.parse(raw) : {};
}
export function startAgent({ port = PORT, bind = BIND } = {}) {
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${bind}:${port}`);
try {
if (req.method === 'GET' && url.pathname === '/health') {
return json(res, 200, {
ok: true,
role: 'fleet-agent',
workers: [...children.keys()],
});
}
if (req.method === 'POST' && url.pathname === '/spawn') {
const body = await readBody(req);
const instance = body.instance;
if (!instance) return json(res, 400, { error: 'instance required' });
if (children.has(instance)) {
return json(res, 200, { pid: children.get(instance).pid, instance, existing: true });
}
const child = spawn(process.execPath, [WORKER], {
env: { ...process.env, ...(body.env || {}) },
stdio: ['ignore', 'pipe', 'pipe'],
});
children.set(instance, child);
child.on('exit', () => children.delete(instance));
return json(res, 200, { pid: child.pid, instance });
}
if (req.method === 'POST' && url.pathname === '/kill') {
const body = await readBody(req);
const child = children.get(body.instance);
if (child) {
child.kill('SIGTERM');
children.delete(body.instance);
}
return json(res, 200, { killed: Boolean(child), instance: body.instance });
}
json(res, 404, { error: 'not found' });
} catch (err) {
json(res, 400, { error: err.message });
}
});
server.listen(port, bind, () => {
process.stdout.write(`fleet-agent http://${bind}:${port}/\n`);
});
return {
server,
async close() {
for (const c of children.values()) c.kill('SIGTERM');
children.clear();
await new Promise((r) => server.close(r));
},
};
}
if (process.argv[1] && path.basename(process.argv[1]) === 'agent.js') {
startAgent();
}