@bongos/core 1.21.126 → 1.21.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +48 -23
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +6 -0
- package/docs/adr/0354-a-builder-hosts-one-project-free-and-more-takes-the-fleet-permission.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/gds/exec-path-guard.js +1 -1
- package/scripts/gds/fitness.js +1 -0
- package/scripts/gds/module-error-report.js +148 -0
- package/scripts/gds/module.js +1 -0
- package/scripts/gds/provision-caps.js +63 -0
- package/scripts/gds/provision-process.js +3 -0
- package/scripts/gds/provision-units.js +3 -0
- package/scripts/gds/provision.js +3 -1
- package/src/module-api.js +1 -1
- package/src/module-loader/error-tally.js +188 -0
- package/src/module-loader/loader.js +52 -4
- package/src/platform-server.js +7 -0
- package/tests/module_error_counts.mjs +265 -0
- package/tests/provision.mjs +2 -0
- package/tests/provision_resource_caps.mjs +117 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// tests/provision_resource_caps.mjs
|
|
2
|
+
//
|
|
3
|
+
// Every hosted project's service is capped (task 1004414). A cloud-host project runs its
|
|
4
|
+
// owner's code on the box that also serves the hub, and ADR 0354 lets any signed-in
|
|
5
|
+
// builder host one, so one runaway project must not starve the others or the hub.
|
|
6
|
+
//
|
|
7
|
+
// Covers:
|
|
8
|
+
// • the numbers are the owner's (2026-10-15): MemoryHigh 160M, MemoryMax 256M,
|
|
9
|
+
// CPUQuota 50%, TasksMax 256 — pinned here so a change is a decision, not a drift
|
|
10
|
+
// • a NEW unit carries all four, rendered from the same table
|
|
11
|
+
// • an EXISTING unit takes them live with `systemctl set-property` (no restart, and no
|
|
12
|
+
// rewrite of a unit whose User= predates the per-instance accounts)
|
|
13
|
+
// • `provision.js apply-caps` reaches every ACTIVE cloud-host project, on the control
|
|
14
|
+
// plane, carries on past one that fails, and says which failed
|
|
15
|
+
// • teardown removes the drop-in set-property wrote
|
|
16
|
+
//
|
|
17
|
+
// Run: node tests/provision_resource_caps.mjs
|
|
18
|
+
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
// ESM named imports, so knip credits these exports to their test (a createRequire read is not)
|
|
24
|
+
import { RESOURCE_CAPS, capDirectives, setCapsCmd, cmdApplyCaps } from '../scripts/gds/provision-caps.js';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const CAPS = { RESOURCE_CAPS, capDirectives, setCapsCmd, cmdApplyCaps };
|
|
28
|
+
const { serviceUnit } = require('../scripts/gds/provision-units.js');
|
|
29
|
+
const PM = require('../scripts/gds/provision-process.js');
|
|
30
|
+
// the REAL shape makeExec answers an allowFail'd failure with: ok:true, softFailed:true
|
|
31
|
+
const { softFailResult } = require('../scripts/gds/provision-config.js');
|
|
32
|
+
|
|
33
|
+
const CLOUD = { id: 18, slug: 'mercury', hosting_shape: 'cloud-host', port: 4100, db_name: 'mercury', target_ref: 'owner/mercury' };
|
|
34
|
+
const OWNER_NUMBERS = ['MemoryHigh=160M', 'MemoryMax=256M', 'CPUQuota=50%', 'TasksMax=256'];
|
|
35
|
+
|
|
36
|
+
test('the caps are the owner’s numbers (2026-10-15)', () => {
|
|
37
|
+
assert.deepEqual(CAPS.capDirectives(), OWNER_NUMBERS);
|
|
38
|
+
assert.ok(Object.isFrozen(CAPS.RESOURCE_CAPS), 'the table cannot be edited at runtime');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('a new hosted project’s unit carries all four caps in [Service]', () => {
|
|
42
|
+
const unit = serviceUnit(CLOUD, { etcBase: '/etc' });
|
|
43
|
+
const service = unit.slice(unit.indexOf('[Service]'), unit.indexOf('[Install]'));
|
|
44
|
+
for (const d of OWNER_NUMBERS) assert.match(service, new RegExp(`^${d.replace(/[.*+?^${}()|[\]\\%]/g, '\\$&')}$`, 'm'), d);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('an existing unit takes the caps live through set-property', () => {
|
|
48
|
+
assert.equal(CAPS.setCapsCmd(CLOUD, { sudoP: 'sudo ' }),
|
|
49
|
+
'sudo systemctl set-property mercury.service MemoryHigh=160M MemoryMax=256M CPUQuota=50% TasksMax=256');
|
|
50
|
+
assert.equal(CAPS.setCapsCmd(CLOUD), 'systemctl set-property mercury.service MemoryHigh=160M MemoryMax=256M CPUQuota=50% TasksMax=256');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function fleet(rows, failSlug = null) {
|
|
54
|
+
const queries = [];
|
|
55
|
+
const ran = [];
|
|
56
|
+
const logs = [];
|
|
57
|
+
const exec = (cmd, opts = {}) => {
|
|
58
|
+
ran.push({ cmd, allowFail: !!opts.allowFail, on: 'control' });
|
|
59
|
+
return cmd.includes(` ${failSlug}.service `) ? softFailResult(new Error(`Unit ${failSlug}.service not loaded.`), false) : { ok: true, stdout: '' };
|
|
60
|
+
};
|
|
61
|
+
const deps = {
|
|
62
|
+
db: { query: async (sql, params) => { queries.push({ sql, params }); return { rows }; } },
|
|
63
|
+
controlExec: exec,
|
|
64
|
+
exec: () => { throw new Error('the box executor is for co-tenant; a cloud-host runs on the control plane'); },
|
|
65
|
+
privileged: true,
|
|
66
|
+
log: (s) => logs.push(s),
|
|
67
|
+
};
|
|
68
|
+
return { deps, queries, ran, logs };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test('apply-caps reaches every active cloud-host project on the control plane', async () => {
|
|
72
|
+
const f = fleet([{ slug: 'mercury' }, { slug: 'venus' }]);
|
|
73
|
+
const r = await CAPS.cmdApplyCaps(f.deps);
|
|
74
|
+
assert.match(f.queries[0].sql, /hosting_shape = 'cloud-host'/);
|
|
75
|
+
assert.match(f.queries[0].sql, /status = 'active'/);
|
|
76
|
+
assert.deepEqual(f.ran.map((x) => x.cmd), [
|
|
77
|
+
'sudo systemctl set-property mercury.service MemoryHigh=160M MemoryMax=256M CPUQuota=50% TasksMax=256',
|
|
78
|
+
'sudo systemctl set-property venus.service MemoryHigh=160M MemoryMax=256M CPUQuota=50% TasksMax=256',
|
|
79
|
+
]);
|
|
80
|
+
assert.ok(f.ran.every((x) => x.allowFail), 'one project that refuses must not stop the rest');
|
|
81
|
+
assert.deepEqual(r, { ok: true, capped: ['mercury', 'venus'], failed: [] });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('apply-caps carries on past a failure and names it', async () => {
|
|
85
|
+
const f = fleet([{ slug: 'mercury' }, { slug: 'venus' }, { slug: 'pluto' }], 'venus');
|
|
86
|
+
const r = await CAPS.cmdApplyCaps(f.deps);
|
|
87
|
+
assert.equal(f.ran.length, 3);
|
|
88
|
+
assert.deepEqual(r, { ok: false, capped: ['mercury', 'pluto'], failed: ['venus'] });
|
|
89
|
+
assert.ok(f.logs.some((l) => /venus/.test(l)), 'the failure is said out loud');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a slug outside the create route’s charset is refused, never run as root', () => {
|
|
93
|
+
for (const slug of ['a;rm -rf /', '../etc', 'Mercury', '-x']) assert.throws(() => CAPS.setCapsCmd({ slug }), /unsafe slug/, slug);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('apply-caps on an empty fleet does nothing and succeeds', async () => {
|
|
97
|
+
const f = fleet([]);
|
|
98
|
+
assert.deepEqual(await CAPS.cmdApplyCaps(f.deps), { ok: true, capped: [], failed: [] });
|
|
99
|
+
assert.equal(f.ran.length, 0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('provision.js lists apply-caps as a command', () => {
|
|
103
|
+
const r = spawnSync(process.execPath, ['scripts/gds/provision.js', 'help'], { encoding: 'utf8' });
|
|
104
|
+
assert.equal(r.status, 0, r.stderr);
|
|
105
|
+
assert.match(r.stdout, /apply-caps/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('teardown removes the drop-in set-property wrote, before the reload', () => {
|
|
109
|
+
const cmds = [];
|
|
110
|
+
const exec = (cmd, opts = {}) => { cmds.push(cmd); return { ok: true }; };
|
|
111
|
+
PM.systemd.removeService(CLOUD, { exec, sudoP: 'sudo ' });
|
|
112
|
+
const rm = cmds.indexOf('sudo rm -f /etc/systemd/system.control/mercury.service.d/50-*.conf');
|
|
113
|
+
assert.ok(rm >= 0, cmds.join('\n'));
|
|
114
|
+
assert.ok(cmds.includes('sudo rmdir /etc/systemd/system.control/mercury.service.d'), 'the emptied dir goes too');
|
|
115
|
+
assert.ok(!cmds.some((c) => /rm -rf/.test(c)), 'never a recursive delete');
|
|
116
|
+
assert.ok(rm < cmds.indexOf('sudo systemctl daemon-reload'), 'removed before the reload that forgets the unit');
|
|
117
|
+
});
|