86 lines
2.8 KiB
JavaScript
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();
|
|
}
|