@bongos/core 1.20.14 → 1.20.16
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 +26 -26
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +19 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/autobongos-service.ps1 +25 -2
- package/scripts/gds/provision-config.js +10 -14
- package/scripts/gds/provision-disconnect.js +4 -6
- package/scripts/gds/provision-net.js +5 -8
- package/scripts/gds/provision-repo.js +12 -33
- package/scripts/gds/provision-teardown.js +10 -14
- package/scripts/gds/provision-units.js +3 -4
- package/scripts/gds/provision.js +94 -201
- package/src/module-api.js +1 -1
- package/tests/autobongos_service_ps1.mjs +52 -0
- package/tests/provision.mjs +15 -134
- package/tests/provision_account_identity.mjs +2 -5
- package/tests/provision_disconnect.mjs +1 -10
- package/tests/provision_restart.mjs +3 -12
- package/tests/provision_settings_apply.mjs +7 -19
- package/tests/provision_teardown_address.mjs +1 -19
package/tests/provision.mjs
CHANGED
|
@@ -372,24 +372,6 @@ t('healthzCmd hits the instance port /healthz', () => {
|
|
|
372
372
|
assert.match(P.healthzCmd(coTenant), /127\.0\.0\.1:3003\/healthz/);
|
|
373
373
|
});
|
|
374
374
|
|
|
375
|
-
t('dedicatedUserData bakes createdb + migrate + first-version seed into cloud-init (task 2119: dedicated must not diverge)', () => {
|
|
376
|
-
const ud = P.dedicatedUserData(coTenant);
|
|
377
|
-
assert.match(ud, /^#cloud-config/);
|
|
378
|
-
assert.match(ud, /createdb mercury/);
|
|
379
|
-
assert.match(ud, /MEDUSA_GDS_ONLY=1/);
|
|
380
|
-
// the seed rides cloud-init too, so a dedicated droplet also comes up able to take tasks
|
|
381
|
-
assert.match(ud, /INSERT INTO versions/, 'seed baked into cloud-init');
|
|
382
|
-
// it must be its OWN runcmd line AFTER migrate (the schema must exist first)
|
|
383
|
-
const lines = ud.split('\n');
|
|
384
|
-
const iMig = lines.findIndex((l) => /migrate\.sh/.test(l));
|
|
385
|
-
const iSeed = lines.findIndex((l) => /INSERT INTO versions/.test(l));
|
|
386
|
-
assert.ok(iMig >= 0 && iSeed > iMig, 'seed line comes after the migrate line');
|
|
387
|
-
// the seed line is a valid bash -lc "…" item: single-quoted -c arg, NO double quotes to break the YAML scalar
|
|
388
|
-
const seedLine = lines[iSeed];
|
|
389
|
-
assert.match(seedLine, /- \[ bash, -lc, "psql .*" \]/, 'well-formed cloud-init runcmd item');
|
|
390
|
-
assert.doesNotMatch(seedLine.replace(/^.*-lc, "/, '').replace(/" \]$/, ''), /"/, 'the seed command carries no inner double quotes');
|
|
391
|
-
});
|
|
392
|
-
|
|
393
375
|
console.log('\nport allocation:');
|
|
394
376
|
|
|
395
377
|
t('nextFreePort skips reserved + taken, returns the lowest free ≥ base', () => {
|
|
@@ -440,7 +422,6 @@ function recorder({ apply = false } = {}) {
|
|
|
440
422
|
},
|
|
441
423
|
exec: (cmd) => { cmds.push(cmd); return { ok: true, stdout: '' }; },
|
|
442
424
|
writeFile: (p) => { writes.push(p); return { ok: true }; },
|
|
443
|
-
doApi: null,
|
|
444
425
|
// Stub the DNS token-scope preflight (task 1980) so the composition stays hermetic
|
|
445
426
|
// (no real CF call). Default: reachable; a test overrides it to exercise the block.
|
|
446
427
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
@@ -1323,29 +1304,6 @@ await ta('provisionInstance is idempotent: an already-active instance is a no-op
|
|
|
1323
1304
|
assert.equal(cmds.length, 0, 'no commands run for an active instance');
|
|
1324
1305
|
});
|
|
1325
1306
|
|
|
1326
|
-
await ta('provisionInstance(dedicated) does NOT run local createdb/migrate/seed (they ride cloud-init)', async () => {
|
|
1327
|
-
const { deps, cmds } = recorder();
|
|
1328
|
-
await P.provisionInstance({ ...coTenant, hosting_shape: 'dedicated', tier: 's-2vcpu-4gb' }, deps);
|
|
1329
|
-
assert.ok(!cmds.some((c) => /createdb/.test(c)), 'dedicated does not run createdb locally');
|
|
1330
|
-
assert.ok(!cmds.some((c) => /INSERT INTO versions/.test(c)), 'dedicated does not run the version seed locally (it rides cloud-init)');
|
|
1331
|
-
});
|
|
1332
|
-
|
|
1333
|
-
await ta('dedicatedUserData bakes the nightly backup: unit files (b64) + dir + timer enable (task 2213)', async () => {
|
|
1334
|
-
const inst = { ...coTenant, hosting_shape: 'dedicated' };
|
|
1335
|
-
const cloudInit = P.dedicatedUserData(inst);
|
|
1336
|
-
// the two unit files land via write_files (base64, so no YAML block-scalar escaping)
|
|
1337
|
-
assert.match(cloudInit, /write_files:/);
|
|
1338
|
-
assert.match(cloudInit, new RegExp('path: ' + reEsc(P.backupServicePath(inst))));
|
|
1339
|
-
assert.match(cloudInit, new RegExp('path: ' + reEsc(P.backupTimerPath(inst))));
|
|
1340
|
-
assert.match(cloudInit, /encoding: b64/);
|
|
1341
|
-
// the base64 content round-trips to a real unit carrying the right DB
|
|
1342
|
-
const b64 = cloudInit.match(/content: ([A-Za-z0-9+/=]+)/)[1];
|
|
1343
|
-
assert.match(Buffer.from(b64, 'base64').toString('utf8'), /Environment=GDS_BACKUP_DB=mercury/);
|
|
1344
|
-
// and the dir is created + the timer enabled on first boot
|
|
1345
|
-
assert.match(cloudInit, /install -d -o appsvc -g \$\(id -gn appsvc\) -m 0750 \/var\/backups\/gds/);
|
|
1346
|
-
assert.match(cloudInit, /enable --now mercury mercury-db-backup\.timer/);
|
|
1347
|
-
});
|
|
1348
|
-
|
|
1349
1307
|
console.log('\nDNS UPSERT composition (P4, CONFIG override):');
|
|
1350
1308
|
|
|
1351
1309
|
// A recorder that also captures each exec's env (so we can assert the DNS hook env).
|
|
@@ -1359,7 +1317,7 @@ function dnsRecorder({ apply = false } = {}) {
|
|
|
1359
1317
|
clearReleasedDomains: async (_db, id) => { cleared.push(id); },
|
|
1360
1318
|
},
|
|
1361
1319
|
exec: (cmd, opts) => { cmds.push(cmd); envs.push(opts && opts.env); return { ok: true, stdout: '' }; },
|
|
1362
|
-
writeFile: () => ({ ok: true }),
|
|
1320
|
+
writeFile: () => ({ ok: true }),
|
|
1363
1321
|
// Preflight (task 1980) stubbed reachable so --apply DNS tests reach the step-5
|
|
1364
1322
|
// behavior they assert (the preflight itself is covered by its own test below).
|
|
1365
1323
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
@@ -1499,7 +1457,7 @@ await ta('cmdRunIntents (--apply) claims → provisions → resolves the intent
|
|
|
1499
1457
|
claimNextOAuthManifest: async () => null,
|
|
1500
1458
|
resolveOAuthManifest: async () => {},
|
|
1501
1459
|
},
|
|
1502
|
-
exec: () => ({ ok: true }), writeFile: () => ({ ok: true }),
|
|
1460
|
+
exec: () => ({ ok: true }), writeFile: () => ({ ok: true }),
|
|
1503
1461
|
};
|
|
1504
1462
|
const r = await P.cmdRunIntents(deps);
|
|
1505
1463
|
assert.equal(r.ran, 1);
|
|
@@ -1589,10 +1547,8 @@ await ta('placeManifestCreds (--apply): converts, rewrites web.env with the cred
|
|
|
1589
1547
|
assert.ok(cmds.some((c) => /\/healthz/.test(c)), 'probes healthz');
|
|
1590
1548
|
});
|
|
1591
1549
|
|
|
1592
|
-
await ta('placeManifestCreds rejects a
|
|
1550
|
+
await ta('placeManifestCreds rejects a creds-less row', async () => {
|
|
1593
1551
|
const deps = { apply: true, log: () => {}, exec: () => ({ ok: true }), writeFile: () => ({}) };
|
|
1594
|
-
const ded = await P.placeManifestCreds({ ...coTenant, hosting_shape: 'dedicated', domain: 'd.example.com' }, { github_code: 'C' }, deps);
|
|
1595
|
-
assert.equal(ded.ok, false); assert.match(ded.error, /dedicated/);
|
|
1596
1552
|
const noCode = await P.placeManifestCreds({ ...coTenant, domain: 'm.example.com' }, {}, deps);
|
|
1597
1553
|
assert.equal(noCode.ok, false); assert.match(noCode.error, /no github_code/);
|
|
1598
1554
|
});
|
|
@@ -1690,9 +1646,9 @@ await ta('a PRE-conversion failure stays retryable — the narrowing spared the
|
|
|
1690
1646
|
apply: true, privileged: false, log: () => {}, sleep: async () => {},
|
|
1691
1647
|
db: {}, exec: () => ({ ok: true, stdout: '' }), writeFile: () => ({ ok: true }),
|
|
1692
1648
|
provisioning: {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
getInstanceById: async () => ({ ...coTenant,
|
|
1649
|
+
// no github_code → placeManifestCreds refuses BEFORE converting (codeSpent absent)
|
|
1650
|
+
claimNextOAuthManifest: async () => (claims++ === 0 ? { id: 22, instance_id: 1, github_code: null, attempts: 1 } : null),
|
|
1651
|
+
getInstanceById: async () => ({ ...coTenant, domain: 'd.example.com' }),
|
|
1696
1652
|
resolveOAuthManifest: async (_db, _id, status, patch = {}) => { resolves.push({ status, patch }); },
|
|
1697
1653
|
recordEvent: async () => {},
|
|
1698
1654
|
},
|
|
@@ -1772,7 +1728,6 @@ function privRecorder({ apply = false } = {}) {
|
|
|
1772
1728
|
provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
|
|
1773
1729
|
exec: (cmd) => { cmds.push(cmd); return { ok: true, stdout: '' }; },
|
|
1774
1730
|
writeFile: (p, _body, opts) => { writes.push({ p, opts: opts || {} }); return { ok: true }; },
|
|
1775
|
-
doApi: null,
|
|
1776
1731
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
1777
1732
|
checkCaddyWiring: () => null,
|
|
1778
1733
|
};
|
|
@@ -1822,36 +1777,6 @@ await ta('teardownInstance(privileged): systemctl + rm-under-/etc are sudo-prefi
|
|
|
1822
1777
|
assert.ok(idx(/daemon-reload/) < idx(/reset-failed/), 'reset-failed after the reload that forgets the file — reset first and the stopped unit could fail again');
|
|
1823
1778
|
});
|
|
1824
1779
|
|
|
1825
|
-
await ta('teardownInstance(dedicated): no systemd steps at all, so no reset-failed either (its own droplet)', async () => {
|
|
1826
|
-
const { deps, cmds } = recorder();
|
|
1827
|
-
await P.teardownInstance({ ...coTenant, hosting_shape: 'dedicated', droplet_id: 9 }, deps);
|
|
1828
|
-
assert.ok(!cmds.some((c) => /reset-failed|daemon-reload|disable --now/.test(c)), cmds.join(' | '));
|
|
1829
|
-
});
|
|
1830
|
-
|
|
1831
|
-
// F6 (task 1002718): main() builds deps.doApi on first READ, so a caller must only read it
|
|
1832
|
-
// on a path that really calls DigitalOcean — a throwing getter proves the others never do.
|
|
1833
|
-
function doApiTripwire(deps) {
|
|
1834
|
-
Object.defineProperty(deps, 'doApi', { get() { throw new Error('read deps.doApi on a path that makes no DO call'); } });
|
|
1835
|
-
return deps;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
await ta('teardownInstance(dedicated, no droplet_id) under --apply never reads deps.doApi (F6)', async () => {
|
|
1839
|
-
const { deps } = recorder({ apply: true });
|
|
1840
|
-
await P.teardownInstance({ ...coTenant, hosting_shape: 'dedicated', droplet_id: null }, doApiTripwire(deps));
|
|
1841
|
-
});
|
|
1842
|
-
|
|
1843
|
-
await ta('provisionInstance(co-tenant) under --apply never reads deps.doApi (F6)', async () => {
|
|
1844
|
-
const { deps } = recorder({ apply: true });
|
|
1845
|
-
const r = await P.provisionInstance(coTenant, doApiTripwire(deps));
|
|
1846
|
-
assert.ok(!/deps\.doApi/.test(String(r && r.error)), String(r && r.error));
|
|
1847
|
-
});
|
|
1848
|
-
|
|
1849
|
-
t('main() constructs the DO client lazily — no DO_API_TOKEN needed until a DO call (F6)', () => {
|
|
1850
|
-
const src = readFileSync(new URL('../scripts/gds/provision.js', import.meta.url), 'utf8');
|
|
1851
|
-
assert.match(src, /get doApi\(\)/, 'deps.doApi is a getter');
|
|
1852
|
-
assert.ok(!/doApi:\s*DoApi \? new DoApi/.test(src), 'no eager construction at deps build');
|
|
1853
|
-
});
|
|
1854
|
-
|
|
1855
1780
|
await ta('teardownInstance(default): NO sudo (bare, unchanged)', async () => {
|
|
1856
1781
|
const { deps, cmds } = recorder();
|
|
1857
1782
|
await P.teardownInstance(coTenant, deps);
|
|
@@ -1914,7 +1839,6 @@ await ta('provisionInstance(co-tenant, remote): box steps use the box `exec`, bu
|
|
|
1914
1839
|
controlExec: (cmd) => { control.push(cmd); return { ok: true, stdout: '' }; },
|
|
1915
1840
|
writeFile: () => ({ ok: true }),
|
|
1916
1841
|
controlWriteFile: () => ({ ok: true }),
|
|
1917
|
-
doApi: null,
|
|
1918
1842
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
1919
1843
|
checkCaddyWiring: () => null,
|
|
1920
1844
|
};
|
|
@@ -1946,7 +1870,6 @@ await ta('provisionInstance(standalone, remote): EVERY box step runs control-pla
|
|
|
1946
1870
|
controlExec: (cmd, opts) => { control.push(cmd); if (opts && opts.env) dnsEnvs.push({ cmd, env: opts.env }); return { ok: true, stdout: '' }; },
|
|
1947
1871
|
writeFile: (p) => { boxWrites.push(p); return { ok: true }; }, // the REMOTE writer
|
|
1948
1872
|
controlWriteFile: (p) => { controlWrites.push(p); return { ok: true }; }, // the control-plane writer
|
|
1949
|
-
doApi: null,
|
|
1950
1873
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
|
|
1951
1874
|
checkCaddyWiring: () => null,
|
|
1952
1875
|
};
|
|
@@ -1986,7 +1909,7 @@ await ta('provisionInstance(standalone, remote, --apply): FAILS LOUD when the co
|
|
|
1986
1909
|
db: { query: async () => ({ rows: [] }) },
|
|
1987
1910
|
provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
|
|
1988
1911
|
exec: () => ({ ok: true, stdout: '' }), controlExec: () => ({ ok: true, stdout: '' }),
|
|
1989
|
-
writeFile: () => ({ ok: true }), controlWriteFile: () => ({ ok: true }),
|
|
1912
|
+
writeFile: () => ({ ok: true }), controlWriteFile: () => ({ ok: true }),
|
|
1990
1913
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
|
|
1991
1914
|
checkCaddyWiring: () => null,
|
|
1992
1915
|
};
|
|
@@ -2006,7 +1929,7 @@ await ta('teardownInstance(standalone, remote): tears down on the CONTROL PLANE
|
|
|
2006
1929
|
provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
|
|
2007
1930
|
exec: (cmd) => { box.push(cmd); return { ok: true }; },
|
|
2008
1931
|
controlExec: (cmd) => { control.push(cmd); return { ok: true }; },
|
|
2009
|
-
writeFile: () => ({ ok: true }), controlWriteFile: () => ({ ok: true }),
|
|
1932
|
+
writeFile: () => ({ ok: true }), controlWriteFile: () => ({ ok: true }),
|
|
2010
1933
|
};
|
|
2011
1934
|
await P.teardownInstance({ ...coTenant, hosting_shape: 'cloud-host' }, deps);
|
|
2012
1935
|
assert.ok(control.some((c) => /systemctl disable --now mercury/.test(c)), 'disable runs on the control plane');
|
|
@@ -2231,7 +2154,6 @@ function attachRecorder({ apply = false, envBody = WEB_ENV_ON_BOX } = {}) {
|
|
|
2231
2154
|
return { ok: true, stdout: '' };
|
|
2232
2155
|
},
|
|
2233
2156
|
writeFile: (p, body) => { writes.push({ path: p, body }); return { ok: true }; },
|
|
2234
|
-
doApi: null,
|
|
2235
2157
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
|
|
2236
2158
|
checkCaddyWiring: () => null,
|
|
2237
2159
|
};
|
|
@@ -2441,14 +2363,6 @@ await ta('domainAttachInstance: instance no longer active → clean no-op (provi
|
|
|
2441
2363
|
}
|
|
2442
2364
|
});
|
|
2443
2365
|
|
|
2444
|
-
await ta('domainAttachInstance: dedicated is not automated — ok:false with guidance, box untouched', async () => {
|
|
2445
|
-
const { deps, cmds } = attachRecorder();
|
|
2446
|
-
const r = await P.domainAttachInstance({ ...activeStandalone, hosting_shape: 'dedicated' }, deps);
|
|
2447
|
-
assert.equal(r.ok, false);
|
|
2448
|
-
assert.match(r.error, /dedicated/);
|
|
2449
|
-
assert.equal(cmds.length, 0);
|
|
2450
|
-
});
|
|
2451
|
-
|
|
2452
2366
|
await ta('domainAttachInstance(--apply): a DNS preflight failure returns ok:false — never THROWS (a throw would flip the ACTIVE instance to error in the drain loop)', async () => {
|
|
2453
2367
|
const savedHook = P.CONFIG.dnsHook; P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
|
|
2454
2368
|
try {
|
|
@@ -2478,7 +2392,7 @@ await ta('cmdRunIntents (--apply) routes a domain-attach intent to the attach le
|
|
|
2478
2392
|
// the attach leg patches web.env IN PLACE, so a `cat` must return a real file
|
|
2479
2393
|
// — an empty one is a deliberate refusal (task 1003237)
|
|
2480
2394
|
exec: (cmd) => ({ ok: true, stdout: /\bcat\b.*web\.env/.test(String(cmd)) ? 'PORT=3010\n' : '' }),
|
|
2481
|
-
writeFile: () => ({ ok: true }),
|
|
2395
|
+
writeFile: () => ({ ok: true }),
|
|
2482
2396
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
|
|
2483
2397
|
checkCaddyWiring: () => null,
|
|
2484
2398
|
};
|
|
@@ -2503,7 +2417,7 @@ await ta('cmdRunIntents (--apply): an UNKNOWN intent action never falls through
|
|
|
2503
2417
|
resolveOAuthManifest: async () => {},
|
|
2504
2418
|
},
|
|
2505
2419
|
exec: (c) => { cmds.push(c); return { ok: true }; },
|
|
2506
|
-
writeFile: () => ({ ok: true }),
|
|
2420
|
+
writeFile: () => ({ ok: true }),
|
|
2507
2421
|
};
|
|
2508
2422
|
const r = await P.cmdRunIntents(deps);
|
|
2509
2423
|
assert.equal(r.errors, 1, 'surfaced as an error, not silently executed');
|
|
@@ -2541,7 +2455,6 @@ function truthRecorder({ healthzFails = false } = {}) {
|
|
|
2541
2455
|
return { ok: true, stdout: '' };
|
|
2542
2456
|
},
|
|
2543
2457
|
writeFile: () => ({ ok: true }),
|
|
2544
|
-
doApi: null,
|
|
2545
2458
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
|
|
2546
2459
|
checkCaddyWiring: () => null,
|
|
2547
2460
|
getGithubToken: async () => null,
|
|
@@ -2615,7 +2528,7 @@ await ta('F19: a RETIRED intent records its reason on the instance without chang
|
|
|
2615
2528
|
recordEvent: async () => {},
|
|
2616
2529
|
claimNextOAuthManifest: async () => null, resolveOAuthManifest: async () => {},
|
|
2617
2530
|
},
|
|
2618
|
-
exec: () => ({ ok: true }), writeFile: () => ({ ok: true }),
|
|
2531
|
+
exec: () => ({ ok: true }), writeFile: () => ({ ok: true }),
|
|
2619
2532
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'z' }),
|
|
2620
2533
|
checkCaddyWiring: () => null,
|
|
2621
2534
|
};
|
|
@@ -2641,7 +2554,7 @@ await ta('F19: a NON-final failure re-pends the intent and writes nothing on the
|
|
|
2641
2554
|
recordEvent: async () => {},
|
|
2642
2555
|
claimNextOAuthManifest: async () => null, resolveOAuthManifest: async () => {},
|
|
2643
2556
|
},
|
|
2644
|
-
exec: () => ({ ok: true }), writeFile: () => ({ ok: true }),
|
|
2557
|
+
exec: () => ({ ok: true }), writeFile: () => ({ ok: true }),
|
|
2645
2558
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'z' }),
|
|
2646
2559
|
checkCaddyWiring: () => null,
|
|
2647
2560
|
};
|
|
@@ -3114,10 +3027,10 @@ t('dbRoleGrantsCmd: runs on the INSTANCE db and grants forward too — the migra
|
|
|
3114
3027
|
assert.match(c, /GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO %I/);
|
|
3115
3028
|
});
|
|
3116
3029
|
|
|
3117
|
-
t('
|
|
3030
|
+
t('the role + seed commands render with NO double quote — they must survive `bash -s` over SSH unchanged', () => {
|
|
3118
3031
|
const dq = String.fromCharCode(34);
|
|
3119
|
-
for (const c of [P.dbRoleCmd(coTenant, { password: 'pw_ABC-123' }), P.dbRoleGrantsCmd(coTenant)]) {
|
|
3120
|
-
assert.ok(!c.includes(dq), `a double-quoted identifier
|
|
3032
|
+
for (const c of [P.dbRoleCmd(coTenant, { password: 'pw_ABC-123' }), P.dbRoleGrantsCmd(coTenant), P.seedFirstVersionCmd(coTenant)]) {
|
|
3033
|
+
assert.ok(!c.includes(dq), `a double-quoted identifier would need escaping on the remote shell: ${c}`);
|
|
3121
3034
|
}
|
|
3122
3035
|
});
|
|
3123
3036
|
|
|
@@ -3184,38 +3097,6 @@ t('webEnvBody: a CO-LOCATED instance (co-tenant or standalone) carries PGPOOL_MA
|
|
|
3184
3097
|
assert.match(withCred, /^PGUSER=bongos-mercury$/m);
|
|
3185
3098
|
});
|
|
3186
3099
|
|
|
3187
|
-
t('webEnvBody: a DEDICATED droplet has its own Postgres — no PGPOOL_MAX line, pg keeps its default', () => {
|
|
3188
|
-
const b = P.webEnvBody({ ...coTenant, hosting_shape: 'dedicated' });
|
|
3189
|
-
assert.doesNotMatch(b, /^PGPOOL_MAX=/m);
|
|
3190
|
-
assert.match(b, /^PGDATABASE=mercury$/m);
|
|
3191
|
-
});
|
|
3192
|
-
|
|
3193
|
-
t('dedicatedUserData: the droplet runs the SAME account + role steps, in the same order around the migrate', () => {
|
|
3194
|
-
const ud = P.dedicatedUserData(coTenant);
|
|
3195
|
-
const at = (needle) => ud.indexOf(needle);
|
|
3196
|
-
assert.ok(at('useradd --system') > -1, 'the droplet creates the account too');
|
|
3197
|
-
assert.ok(at('usermod -aG $(id -gn appsvc)') > -1);
|
|
3198
|
-
assert.ok(at('useradd --system') < at('createdb mercury'), 'account before the DB');
|
|
3199
|
-
assert.ok(at('migrate.sh') < at('CREATE ROLE %I'), 'the role grants must follow the migrate that created the tables');
|
|
3200
|
-
assert.ok(at('CREATE ROLE %I') < at('GRANT ALL ON ALL TABLES'), 'role before its grants');
|
|
3201
|
-
});
|
|
3202
|
-
|
|
3203
|
-
t('dedicatedUserData: NEVER bakes a password into cloud-init — DO retains user-data and serves it back', () => {
|
|
3204
|
-
const ud = P.dedicatedUserData(coTenant);
|
|
3205
|
-
// The dollar-quoted password literal dbRoleCmd uses. Its absence is the assertion.
|
|
3206
|
-
assert.ok(!ud.includes('$p$'), 'a credential in user-data is a permanent copy we cannot lock down');
|
|
3207
|
-
assert.match(ud, /CREATE ROLE %I WITH LOGIN\$f\$/, 'the role is still created, password-less (single-tenant droplet)');
|
|
3208
|
-
assert.match(ud, /REVOKE CONNECT ON DATABASE %I FROM PUBLIC/, 'and PUBLIC still loses CONNECT');
|
|
3209
|
-
});
|
|
3210
|
-
|
|
3211
|
-
t('dbRoleCmd(peerOnly): omits the PASSWORD clause entirely, and REFUSES a password', () => {
|
|
3212
|
-
const c = P.dbRoleCmd(coTenant, { peerOnly: true });
|
|
3213
|
-
assert.ok(!/PASSWORD/.test(c), 'no PASSWORD clause at all — not an empty one');
|
|
3214
|
-
assert.match(c, /CREATE ROLE %I WITH LOGIN\$f\$/);
|
|
3215
|
-
assert.match(c, /ALTER DATABASE %I OWNER TO %I/, 'ownership and the revoke are unchanged');
|
|
3216
|
-
assert.throws(() => P.dbRoleCmd(coTenant, { peerOnly: true, password: 'pw_ABC' }), /peerOnly takes no password/);
|
|
3217
|
-
});
|
|
3218
|
-
|
|
3219
3100
|
t('dbRoleCmdShown: the label the runner logs names every effect and carries NO password', () => {
|
|
3220
3101
|
const pw = P.generateDbPassword();
|
|
3221
3102
|
const real = P.dbRoleCmd(coTenant, { password: pw });
|
|
@@ -68,13 +68,11 @@ test('accountPlaneGap: a SILENT failure still names a cause — an empty stderr
|
|
|
68
68
|
assert.match(gap, /failed \(Command failed: sh -c\)/, 'falls through the empty Buffer to the message');
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
test('accountPreflight: a
|
|
71
|
+
test('accountPreflight: a dry run asks the box nothing', () => {
|
|
72
72
|
const cmds = [];
|
|
73
73
|
const rec = (c) => { cmds.push(c); return isRoot(); };
|
|
74
|
-
assert.equal(accountPreflight({ exec: rec, apply: true, log: () => {}, shape: 'dedicated' }), null);
|
|
75
|
-
assert.deepEqual(cmds, [], 'cloud-init creates that account as root — nothing to ask');
|
|
76
74
|
const lines = [];
|
|
77
|
-
assert.equal(accountPreflight({ exec: rec, apply: false, log: (m) => lines.push(m)
|
|
75
|
+
assert.equal(accountPreflight({ exec: rec, apply: false, log: (m) => lines.push(m) }), null);
|
|
78
76
|
assert.deepEqual(cmds, [], 'a dry run changes nothing and asks nothing');
|
|
79
77
|
assert.match(lines.join('\n'), /would probe/, 'the plan says the probe is coming, rather than printing step 1c as though it works');
|
|
80
78
|
});
|
|
@@ -100,7 +98,6 @@ function harness({ noRoot = false } = {}) {
|
|
|
100
98
|
},
|
|
101
99
|
exec,
|
|
102
100
|
writeFile: (p) => { writes.push(p); return { ok: true }; },
|
|
103
|
-
doApi: null,
|
|
104
101
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
105
102
|
checkCaddyWiring: () => null,
|
|
106
103
|
getGithubToken: async () => null,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// erase and the catalog forget succeeded, so a failure leaves a row the runner can retry
|
|
15
15
|
// • the dump erase matches the nightly filename EXACTLY, so `test` never erases `test-nk`
|
|
16
16
|
// • each shape erases where its data lives (cloud-host: control plane; co-tenant: the
|
|
17
|
-
// co-hosting box
|
|
17
|
+
// co-hosting box)
|
|
18
18
|
// • a dry run erases nothing and deletes nothing; an invalid slug is refused before any command
|
|
19
19
|
//
|
|
20
20
|
// Hermetic: recorded executors, a fake db, a fake catalog port. No shell, no Postgres.
|
|
@@ -69,7 +69,6 @@ function rig({ apply = true, privileged = false, failOn = null, catalogThrows =
|
|
|
69
69
|
},
|
|
70
70
|
exec: record('box'), controlExec: record('control'),
|
|
71
71
|
writeFile: () => ({ ok: true }), controlWriteFile: () => ({ ok: true }),
|
|
72
|
-
doApi: { deleteDroplet: async (id) => { events.push({ where: 'do', cmd: `delete-droplet ${id}` }); } },
|
|
73
72
|
projectCatalog: catalog ? {
|
|
74
73
|
forgetOwnedProject: async (target, opts) => {
|
|
75
74
|
events.push({ where: 'catalog', cmd: 'forget', target, pool: opts && opts.pool });
|
|
@@ -137,14 +136,6 @@ await ta('co-tenant: the erase runs on the co-hosting box, there is no per-proje
|
|
|
137
136
|
assert.ok(!r.cmds('control').some((x) => /dropdb/.test(x)));
|
|
138
137
|
});
|
|
139
138
|
|
|
140
|
-
await ta('dedicated: its droplet went whole, so nothing is erased by command — the catalog and the row still go', async () => {
|
|
141
|
-
const r = rig();
|
|
142
|
-
const res = await D.disconnectInstance({ ...lantern, hosting_shape: 'dedicated', droplet_id: 9 }, r.deps);
|
|
143
|
-
assert.equal(res.ok, true);
|
|
144
|
-
assert.ok(!r.events.some((e) => /dropdb|rm -rf|userdel|find /.test(e.cmd || '')), r.events.map((e) => e.cmd).join('\n'));
|
|
145
|
-
assert.ok(r.at((e) => e.cmd === 'forget') >= 0 && r.at((e) => e.cmd === 'delete-row') >= 0);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
139
|
console.log('\nthe dump erase is exact:');
|
|
149
140
|
|
|
150
141
|
t('the nightly filename matches, and a project whose name merely STARTS with this one\'s does not', () => {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// address), and the event text says which one was actually observed
|
|
21
21
|
// • a project that stays down is ok:true — a truthful observation for the card,
|
|
22
22
|
// not a failure to retry four more times
|
|
23
|
-
// • the state gate is a clean no-op
|
|
23
|
+
// • the state gate is a clean no-op
|
|
24
24
|
// • cmdRunIntents routes 'restart' to this leg
|
|
25
25
|
//
|
|
26
26
|
// Run: node tests/provision_restart.mjs
|
|
@@ -64,7 +64,6 @@ function recorder({ apply = true, healthzFails = false, privileged = false, rest
|
|
|
64
64
|
},
|
|
65
65
|
exec,
|
|
66
66
|
writeFile: writer('box'), controlWriteFile: writer('control'),
|
|
67
|
-
doApi: null,
|
|
68
67
|
};
|
|
69
68
|
return { deps, cmds, writes, events, probes, cleared, logs };
|
|
70
69
|
}
|
|
@@ -160,14 +159,6 @@ await ta('every non-active state is a clean no-op, not an error — a raced tear
|
|
|
160
159
|
}
|
|
161
160
|
});
|
|
162
161
|
|
|
163
|
-
await ta('a dedicated droplet is refused with the exact command, and nothing is run', async () => {
|
|
164
|
-
const { deps, cmds } = recorder();
|
|
165
|
-
const r = await P.restartInstance({ ...activeStandalone, hosting_shape: 'dedicated' }, deps);
|
|
166
|
-
assert.equal(r.ok, false);
|
|
167
|
-
assert.match(r.error, /systemctl restart spike/);
|
|
168
|
-
assert.equal(cmds.length, 0);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
162
|
await ta('dry-run runs no real work and observes nothing', async () => {
|
|
172
163
|
const { deps, writes } = recorder({ apply: false });
|
|
173
164
|
const r = await P.restartInstance(activeStandalone, deps);
|
|
@@ -212,7 +203,7 @@ await ta("cmdRunIntents (--apply) routes 'restart' to restartInstance and resolv
|
|
|
212
203
|
claimNextOAuthManifest: async () => null, resolveOAuthManifest: async () => {},
|
|
213
204
|
},
|
|
214
205
|
exec: (cmd) => { cmds.push(cmd); return /healthz/.test(cmd) ? { ok: true, stdout: '{}' } : { ok: true, stdout: '' }; },
|
|
215
|
-
writeFile: () => ({ ok: true }),
|
|
206
|
+
writeFile: () => ({ ok: true }),
|
|
216
207
|
};
|
|
217
208
|
const r = await P.cmdRunIntents(deps);
|
|
218
209
|
assert.equal(r.errors, 0, JSON.stringify(r));
|
|
@@ -235,7 +226,7 @@ await ta('cmdRunIntents (--apply): a restart that throws re-pends and never mark
|
|
|
235
226
|
claimNextOAuthManifest: async () => null, resolveOAuthManifest: async () => {},
|
|
236
227
|
},
|
|
237
228
|
exec: (cmd) => { if (/systemctl restart/.test(cmd)) throw new Error('Command failed: sudo systemctl restart spike'); return { ok: true, stdout: '' }; },
|
|
238
|
-
writeFile: () => ({ ok: true }),
|
|
229
|
+
writeFile: () => ({ ok: true }),
|
|
239
230
|
};
|
|
240
231
|
const r = await P.cmdRunIntents(deps);
|
|
241
232
|
assert.equal(r.errors, 1);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// the intent and never flips a healthy ACTIVE instance to 'error' (ADR 0190)
|
|
16
16
|
// • the verify asks the instance's own manifest whether it READ the values, and
|
|
17
17
|
// records only what it earned (true / false / unknown)
|
|
18
|
-
// • the state gate is a clean no-op
|
|
18
|
+
// • the state gate is a clean no-op
|
|
19
19
|
// • cmdRunIntents routes 'settings-apply' to this leg
|
|
20
20
|
//
|
|
21
21
|
// Run: node tests/provision_settings_apply.mjs
|
|
@@ -193,7 +193,6 @@ function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, health
|
|
|
193
193
|
// a standalone must land on the CONTROL-PLANE writer (task 2074 routing) —
|
|
194
194
|
// both writers record, tagged, so the choice is asserted, not assumed
|
|
195
195
|
writeFile: writer('box'), controlWriteFile: writer('control'),
|
|
196
|
-
doApi: null,
|
|
197
196
|
};
|
|
198
197
|
return { deps, cmds, writes, events, probes, cleared, logs };
|
|
199
198
|
}
|
|
@@ -304,17 +303,6 @@ await ta('the state gate: any non-active status is a clean no-op — ok:true noo
|
|
|
304
303
|
}
|
|
305
304
|
});
|
|
306
305
|
|
|
307
|
-
await ta('the shape refusal: dedicated → ok:false with guidance naming the values + file; box untouched', async () => {
|
|
308
|
-
const { deps, cmds, writes } = recorder({ apply: true });
|
|
309
|
-
const r = await P.settingsApplyInstance({ ...activeStandalone, hosting_shape: 'dedicated' }, deps);
|
|
310
|
-
assert.equal(r.ok, false);
|
|
311
|
-
assert.match(r.error, /dedicated/);
|
|
312
|
-
assert.match(r.error, new RegExp(`${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY=gated`), 'the operator is told exactly what to set');
|
|
313
|
-
assert.match(r.error, /\/etc\/spike\/web\.env/);
|
|
314
|
-
assert.equal(cmds.length, 0);
|
|
315
|
-
assert.equal(writes.length, 0);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
306
|
console.log('\nthe control plane\'s own row — the platform pushes a setting to itself (task 1004334):');
|
|
319
307
|
|
|
320
308
|
const U = require('../scripts/gds/provision-units.js');
|
|
@@ -505,7 +493,7 @@ await ta("cmdRunIntents (--apply) routes a 'settings-apply' intent to the leg, w
|
|
|
505
493
|
resolveOAuthManifest: async () => {},
|
|
506
494
|
},
|
|
507
495
|
exec: (cmd) => { cmds.push(cmd); return /\bcat /.test(cmd) ? { ok: true, stdout: LIVE_ENV } : /instance/.test(cmd) ? { ok: true, stdout: manifestWith({ platformVisibility: 'gated', joinability: 'open', visibility: 'public' }) } : { ok: true, stdout: '{"ok":true}' }; },
|
|
508
|
-
writeFile: (p, body) => { writes.push(body); return { ok: true }; },
|
|
496
|
+
writeFile: (p, body) => { writes.push(body); return { ok: true }; },
|
|
509
497
|
};
|
|
510
498
|
const r = await P.cmdRunIntents(deps);
|
|
511
499
|
assert.equal(r.ran, 1);
|
|
@@ -533,7 +521,7 @@ await ta('cmdRunIntents (--apply): a FAILED restart on attempt 1 re-pends the in
|
|
|
533
521
|
if (/systemctl restart/.test(cmd)) throw new Error('Command failed: sudo systemctl restart spike');
|
|
534
522
|
return { ok: true, stdout: '' };
|
|
535
523
|
},
|
|
536
|
-
writeFile: () => ({ ok: true }),
|
|
524
|
+
writeFile: () => ({ ok: true }),
|
|
537
525
|
};
|
|
538
526
|
const r = await P.cmdRunIntents(deps);
|
|
539
527
|
assert.equal(r.errors, 1);
|
|
@@ -544,27 +532,27 @@ await ta('cmdRunIntents (--apply): a FAILED restart on attempt 1 re-pends the in
|
|
|
544
532
|
assert.equal(statusWrites.length, 0, 'a healthy ACTIVE instance is never marked broken over a settings push');
|
|
545
533
|
});
|
|
546
534
|
|
|
547
|
-
await ta('cmdRunIntents (--apply): a
|
|
535
|
+
await ta('cmdRunIntents (--apply): a refused push (no web.env to patch) RETIRES at the cap with the reason on the instance and NO status change', async () => {
|
|
548
536
|
let claims = 0; const resolved = []; const statusWrites = [];
|
|
549
537
|
const deps = {
|
|
550
538
|
apply: true, log: () => {}, sleep: async () => {},
|
|
551
539
|
db: { query: async () => ({ rows: [] }) },
|
|
552
540
|
provisioning: {
|
|
553
541
|
claimNextIntent: async () => (claims++ === 0 ? { id: 13, instance_id: 18, action: 'settings-apply', attempts: P.MAX_INTENT_ATTEMPTS } : null),
|
|
554
|
-
getInstanceById: async () =>
|
|
542
|
+
getInstanceById: async () => activeStandalone, // exec reads back nothing → "cannot read web.env" refusal
|
|
555
543
|
resolveIntent: async (_db, id, state, err) => { resolved.push({ id, state, err }); },
|
|
556
544
|
setInstanceStatus: async (_db, _id, s, patch) => { statusWrites.push({ s, patch }); },
|
|
557
545
|
recordEvent: async () => {},
|
|
558
546
|
claimNextOAuthManifest: async () => null, resolveOAuthManifest: async () => {},
|
|
559
547
|
},
|
|
560
|
-
exec: () => ({ ok: true }), writeFile: () => ({ ok: true }),
|
|
548
|
+
exec: () => ({ ok: true }), writeFile: () => ({ ok: true }),
|
|
561
549
|
};
|
|
562
550
|
const r = await P.cmdRunIntents(deps);
|
|
563
551
|
assert.equal(r.errors, 1);
|
|
564
552
|
assert.equal(resolved[0].state, 'error');
|
|
565
553
|
assert.equal(statusWrites.length, 1);
|
|
566
554
|
assert.equal(statusWrites[0].s, 'active', 'a healthy instance is never marked broken over a refused push');
|
|
567
|
-
assert.match(statusWrites[0].patch.error_note, /
|
|
555
|
+
assert.match(statusWrites[0].patch.error_note, /cannot read/);
|
|
568
556
|
});
|
|
569
557
|
|
|
570
558
|
console.log('\nrenamedEnvCarry — the prelaunch password carried on the box (task 1004395):');
|
|
@@ -51,7 +51,6 @@ function rig({ apply = true, privileged = false, hookThrows = false } = {}) {
|
|
|
51
51
|
return { ok: true, stdout: '' };
|
|
52
52
|
},
|
|
53
53
|
writeFile: () => ({ ok: true }), controlWriteFile: () => ({ ok: true }),
|
|
54
|
-
doApi: { deleteDroplet: async (id) => { events.push({ where: 'do', cmd: `delete ${id}` }); } },
|
|
55
54
|
};
|
|
56
55
|
const on = (where) => events.filter((e) => e.where === where).map((e) => e.cmd);
|
|
57
56
|
const hookCalls = () => events.filter((e) => e.cmd === HOOK);
|
|
@@ -98,14 +97,6 @@ await ta('privileged: the rm under /etc and the reload are sudo-prefixed', async
|
|
|
98
97
|
});
|
|
99
98
|
});
|
|
100
99
|
|
|
101
|
-
await ta('dedicated: no Caddy steps — the droplet (and its Caddy) is deleted whole', async () => {
|
|
102
|
-
await withHook(HOOK, async () => {
|
|
103
|
-
const r = rig();
|
|
104
|
-
await P.teardownInstance({ ...lantern, hosting_shape: 'dedicated', droplet_id: 9 }, r.deps);
|
|
105
|
-
assert.ok(!r.events.some((e) => /caddy/.test(e.cmd || '')), r.events.map((e) => e.cmd).join(' | '));
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
100
|
console.log('\nthe DNS leg:');
|
|
110
101
|
|
|
111
102
|
await ta('the hostname is parked at the blackhole through the release env, from the control plane, before the row reads torn_down', async () => {
|
|
@@ -121,15 +112,6 @@ await ta('the hostname is parked at the blackhole through the release env, from
|
|
|
121
112
|
});
|
|
122
113
|
});
|
|
123
114
|
|
|
124
|
-
await ta('dedicated: its hostname is parked too — the droplet IP it pointed at is gone', async () => {
|
|
125
|
-
await withHook(HOOK, async () => {
|
|
126
|
-
const r = rig();
|
|
127
|
-
await P.teardownInstance({ ...lantern, hosting_shape: 'dedicated', droplet_id: 9 }, r.deps);
|
|
128
|
-
assert.equal(r.hookCalls().length, 1);
|
|
129
|
-
assert.equal(r.hookCalls()[0].where, 'control');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
115
|
await ta('best-effort: a hook that throws is logged and the row still reaches torn_down', async () => {
|
|
134
116
|
await withHook(HOOK, async () => {
|
|
135
117
|
const r = rig({ hookThrows: true });
|
|
@@ -218,7 +200,7 @@ await ta('the row keeps its domain and port, and a revive re-UPSERTs the SAME ho
|
|
|
218
200
|
exec: () => ({ ok: true, stdout: '' }),
|
|
219
201
|
controlExec: (cmd, opts) => { if (cmd === HOOK) dns.push(opts.env); return { ok: true, stdout: '' }; },
|
|
220
202
|
writeFile: () => ({ ok: true }), controlWriteFile: (p) => { writes.push(p); return { ok: true }; },
|
|
221
|
-
|
|
203
|
+
checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
|
|
222
204
|
});
|
|
223
205
|
assert.ok(dns.some((env) => env.BOX_HOSTNAME === 'lantern.cloudbongos.com' && env.BOX_IP === '198.51.100.20'), JSON.stringify(dns));
|
|
224
206
|
assert.ok(writes.some((p) => p.endsWith('/lantern.caddy')), writes.join(' | '));
|