Clean prepaid SoT, identity mailbox, public access planes, leaf policy, fleet spawn
Some checks are pending
offline / test (push) Waiting to run
Some checks are pending
offline / test (push) Waiting to run
Persist account-balance books; edge caches prepaid from books. Add zappier-identity, verae-nats-accounts, verae-jobs-events, verae-access-staff, zapier-decisions. Edge binds loopback; lan-134 stays off; HTTP services prefer local spawn.
This commit is contained in:
parent
345aeeead9
commit
ddf772454b
153 changed files with 2236 additions and 116 deletions
|
|
@ -13,7 +13,7 @@ Operator control plane for Verae Time × Zapier **runtime** services:
|
|||
cd packages/verae-fleet
|
||||
npm test
|
||||
node src/cli.js list
|
||||
node src/cli.js serve # http://127.0.0.1:3850/
|
||||
node src/cli.js serve # http://0.0.0.0:3850/
|
||||
```
|
||||
|
||||
Against a running daemon:
|
||||
|
|
|
|||
|
|
@ -368,9 +368,10 @@
|
|||
<h1 class="title">Operator console</h1>
|
||||
</header>
|
||||
<h1 id="operator-console">Operator console</h1>
|
||||
<p>Loopback UI at <strong>http://127.0.0.1:3850/</strong> — Fleet, Trace
|
||||
(simulator), and Docs in one shell. Not a public site. NATS stays
|
||||
private; SSH identity files stay on disk.</p>
|
||||
<p>Operator UI at <strong>http://0.0.0.0:3850/</strong> (all
|
||||
interfaces). Fleet, Trace (simulator), and Docs in one shell. NATS stays
|
||||
private (<code>127.0.0.1:4222</code>); SSH identity files stay on
|
||||
disk.</p>
|
||||
<div class="sourceCode" id="cb1"><pre
|
||||
class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="bu">cd</span> packages/verae-fleet</span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="ex">node</span> src/cli.js serve</span></code></pre></div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Operator console
|
||||
|
||||
Loopback UI at **http://127.0.0.1:3850/** — Fleet, Trace (simulator), and Docs in one shell. Not a public site. NATS stays private; SSH identity files stay on disk.
|
||||
Operator UI at **http://0.0.0.0:3850/** (all interfaces). Fleet, Trace (simulator), and Docs in one shell. NATS stays private (`127.0.0.1:4222`); SSH identity files stay on disk.
|
||||
|
||||
```bash
|
||||
cd packages/verae-fleet
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"apiVersion": "verae.fleet/v1",
|
||||
"control": {
|
||||
"bind": "127.0.0.1",
|
||||
"bind": "0.0.0.0",
|
||||
"port": 3850,
|
||||
"probeIntervalMs": 750,
|
||||
"unhealthyAfterMs": 2500,
|
||||
|
|
@ -20,6 +20,9 @@
|
|||
"access-api": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
|
||||
"access-leaf": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
|
||||
"access-zapier": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
|
||||
"access-staff": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
|
||||
"identity": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
|
||||
"jobs-events": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
|
||||
"account-balance": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
|
||||
"customer-service": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
|
||||
"sales-pricing": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"PORT": "3023",
|
||||
"AUTHZ_URL": "http://127.0.0.1:3020",
|
||||
"LEAF_TOKEN": "leaf-dev-token",
|
||||
"NATS_POLICY_PATH": "../verae-nats-accounts/policy.json",
|
||||
"NATS_URL": "nats://127.0.0.1:4222"
|
||||
},
|
||||
"nats": {
|
||||
|
|
|
|||
22
packages/verae-fleet/services/access-staff.json
Normal file
22
packages/verae-fleet/services/access-staff.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"id": "access-staff",
|
||||
"title": "Staff access plane",
|
||||
"kind": "http",
|
||||
"package": "verae-access-staff",
|
||||
"role": "access-staff",
|
||||
"managed": true,
|
||||
"runtime": "HTTP :3025 plane=staff",
|
||||
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
|
||||
"ports": { "healthBase": 3025 },
|
||||
"spawn": {
|
||||
"cwd": "../verae-access-staff",
|
||||
"command": "node",
|
||||
"args": ["src/server.js"]
|
||||
},
|
||||
"env": {
|
||||
"PORT": "3025",
|
||||
"AUTHZ_URL": "http://127.0.0.1:3020",
|
||||
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010"
|
||||
},
|
||||
"nats": { "in": [], "out": ["verae.access.authz.check"] }
|
||||
}
|
||||
24
packages/verae-fleet/services/identity.json
Normal file
24
packages/verae-fleet/services/identity.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"id": "identity",
|
||||
"title": "Verae user-id bind/lookup",
|
||||
"kind": "http",
|
||||
"package": "zappier-identity",
|
||||
"role": "identity",
|
||||
"managed": true,
|
||||
"runtime": "HTTP :3026 + NATS verae.identity.*",
|
||||
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
|
||||
"ports": { "healthBase": 3026 },
|
||||
"spawn": {
|
||||
"cwd": "../zappier-identity",
|
||||
"command": "node",
|
||||
"args": ["src/server.js"]
|
||||
},
|
||||
"env": {
|
||||
"PORT": "3026",
|
||||
"NATS_URL": "nats://127.0.0.1:4222"
|
||||
},
|
||||
"nats": {
|
||||
"in": ["verae.identity.bind", "verae.identity.lookup"],
|
||||
"out": []
|
||||
}
|
||||
}
|
||||
24
packages/verae-fleet/services/jobs-events.json
Normal file
24
packages/verae-fleet/services/jobs-events.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"id": "jobs-events",
|
||||
"title": "jobs.events mailbox",
|
||||
"kind": "http",
|
||||
"package": "verae-jobs-events",
|
||||
"role": "jobs-events",
|
||||
"managed": true,
|
||||
"runtime": "HTTP :3030 + NATS verae.zapier.jobs.events",
|
||||
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
|
||||
"ports": { "healthBase": 3030 },
|
||||
"spawn": {
|
||||
"cwd": "../verae-jobs-events",
|
||||
"command": "node",
|
||||
"args": ["src/server.js"]
|
||||
},
|
||||
"env": {
|
||||
"PORT": "3030",
|
||||
"NATS_URL": "nats://127.0.0.1:4222"
|
||||
},
|
||||
"nats": {
|
||||
"in": ["verae.zapier.jobs.events"],
|
||||
"out": []
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
"package": "zappier",
|
||||
"role": "zappier-edge",
|
||||
"managed": true,
|
||||
"runtime": "HTTPS :3000 (portal, admin, meter, proxy to middleware)",
|
||||
"runtime": "loopback :3000 (portal, admin, Stripe; public doors are access-*)",
|
||||
"health": { "type": "http", "path": "/health", "timeoutMs": 4000 },
|
||||
"ports": { "healthBase": 3000 },
|
||||
"spawn": {
|
||||
|
|
@ -15,12 +15,15 @@
|
|||
},
|
||||
"env": {
|
||||
"PORT": "3000",
|
||||
"BIND": "127.0.0.1",
|
||||
"ZAPPIER_UPSTREAM": "http://127.0.0.1:3100",
|
||||
"CS_SERVICE_URL": "http://127.0.0.1:3011",
|
||||
"SALES_PRICING_URL": "http://127.0.0.1:3012",
|
||||
"ACCOUNTING_EXPORT_URL": "http://127.0.0.1:3013",
|
||||
"NATS_URL": "nats://127.0.0.1:4222",
|
||||
"AUTHZ_URL": "http://127.0.0.1:3020"
|
||||
"AUTHZ_URL": "http://127.0.0.1:3020",
|
||||
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
|
||||
"IDENTITY_URL": "http://127.0.0.1:3026"
|
||||
},
|
||||
"nats": {
|
||||
"in": [],
|
||||
|
|
@ -32,5 +35,5 @@
|
|||
"verae.billing.balance.adjust"
|
||||
]
|
||||
},
|
||||
"notes": "Zapier x-api-key lands here. HTTPS to middleware. NATS billing to account-balance."
|
||||
"notes": "Loopback only. Public Zapier traffic hits access-zapier :3024."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,15 +92,15 @@ async function main() {
|
|||
}
|
||||
if (cmd === 'serve' || cmd === 'monitor') {
|
||||
const sup = new Supervisor({ loaded });
|
||||
const mon = new Monitor(sup);
|
||||
startControlServer(sup, mon);
|
||||
print(`fleet ${cmd} pid=${process.pid}\nUI http://${loaded.control.bind || '0.0.0.0'}:${loaded.control.port}/\nSIGINT stops all managed replicas`);
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -41,7 +41,11 @@ export function loadMachines(root, overlayPath) {
|
|||
if (extra && typeof extra === 'object') Object.assign(m, extra);
|
||||
}
|
||||
}
|
||||
return list.map(normalizeMachine);
|
||||
return list.map((m) => {
|
||||
const n = normalizeMachine(m);
|
||||
if (n.id === 'lan-134' && process.env.FLEET_ENABLE_LAN134 !== '1') n.enabled = false;
|
||||
return n;
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeMachine(m) {
|
||||
|
|
@ -86,9 +90,13 @@ export function canHost(machine, serviceId, role) {
|
|||
* @param {string} serviceId
|
||||
* @param {string} [role]
|
||||
*/
|
||||
export function pickMachine(machines, instances, serviceId, role, exclude = []) {
|
||||
export function pickMachine(machines, instances, serviceId, role, exclude = [], opts = {}) {
|
||||
const skip = new Set(exclude);
|
||||
const eligible = machines.filter((m) => canHost(m, serviceId, role) && !skip.has(m.id));
|
||||
let eligible = machines.filter((m) => canHost(m, serviceId, role) && !skip.has(m.id));
|
||||
if (opts.preferLocal) {
|
||||
const local = eligible.filter((m) => m.kind === 'local');
|
||||
if (local.length) eligible = local;
|
||||
}
|
||||
if (!eligible.length) return null;
|
||||
const scored = eligible.map((m) => {
|
||||
const running = instances.filter((i) => i.machine === m.id && i.pid).length;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Operator HTTP: list, status, pause/resume/stop/start/restart.
|
||||
* Binds 127.0.0.1 only.
|
||||
* HTTP bind from fleet.json control.bind (default 0.0.0.0).
|
||||
* @module server
|
||||
*/
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ function json(res, code, obj) {
|
|||
* @param {import('./monitor.js').Monitor} [mon]
|
||||
*/
|
||||
export function startControlServer(sup, mon) {
|
||||
const bind = sup.loaded.control?.bind || '127.0.0.1';
|
||||
const bind = process.env.FLEET_BIND || sup.loaded.control?.bind || '0.0.0.0';
|
||||
const port = Number(sup.loaded.control?.port || 3850);
|
||||
const sim = new Simulator();
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function scpBaseArgs(machine) {
|
|||
return args;
|
||||
}
|
||||
|
||||
export function sshExec(machine, remoteCommand, { timeoutMs = 20000 } = {}) {
|
||||
export function sshExec(machine, remoteCommand, { timeoutMs = 8000 } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [...sshBaseArgs(machine), sshTarget(machine), remoteCommand];
|
||||
const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
|
|
@ -96,7 +96,7 @@ export function scpTo(machine, localFiles, remoteDir) {
|
|||
const t = setTimeout(() => {
|
||||
child.kill('SIGKILL');
|
||||
reject(new Error('scp timeout'));
|
||||
}, 30000);
|
||||
}, 8000);
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(t);
|
||||
if (code !== 0) reject(new Error(Buffer.concat(err).toString('utf8') || `scp exit ${code}`));
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ export class Supervisor {
|
|||
serviceId,
|
||||
spec.role || serviceId,
|
||||
opts.excludeMachines || [],
|
||||
{ preferLocal: Boolean(spec.spawn?.command) },
|
||||
);
|
||||
if (!machine) {
|
||||
this.log('skip', { service: serviceId, reason: 'no-machine-capacity' });
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ describe('verae-fleet', () => {
|
|||
'access-api',
|
||||
'access-leaf',
|
||||
'access-zapier',
|
||||
'access-staff',
|
||||
'identity',
|
||||
'jobs-events',
|
||||
'account-balance',
|
||||
'customer-service',
|
||||
'sales-pricing',
|
||||
|
|
@ -78,6 +81,8 @@ describe('verae-fleet', () => {
|
|||
assert.equal(tree.keepFloor, true);
|
||||
assert.equal(tree.configPath, 'services/tree-node.json');
|
||||
assert.equal(loaded.fleetPath, 'fleet.json');
|
||||
const lan = loaded.machines.find((m) => m.id === 'lan-134');
|
||||
if (lan) assert.equal(lan.enabled, false);
|
||||
});
|
||||
|
||||
it('starts the tree-node floor (3 available copies)', async () => {
|
||||
|
|
|
|||
|
|
@ -53,4 +53,19 @@ describe('SSH remote machine config', () => {
|
|||
const pick = pickMachine(machines, instances, 'tree-node', 'tree-node');
|
||||
assert.equal(pick.id, 'ns1');
|
||||
});
|
||||
|
||||
it('preferLocal keeps HTTP apps on the control host', () => {
|
||||
const machines = [
|
||||
normalizeMachine({ id: 'local', kind: 'local', host: '127.0.0.1', capacity: 32, roles: ['*'] }),
|
||||
normalizeMachine({
|
||||
id: 'ns1',
|
||||
kind: 'ssh',
|
||||
host: '70.88.205.138',
|
||||
capacity: 24,
|
||||
roles: ['*'],
|
||||
}),
|
||||
];
|
||||
const pick = pickMachine(machines, [], 'account-balance', 'account-balance', [], { preferLocal: true });
|
||||
assert.equal(pick.id, 'local');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue