diff --git a/test/integration/assigned-vault.test.ts b/test/integration/assigned-vault.test.ts index 1059194..b3eaf9a 100644 --- a/test/integration/assigned-vault.test.ts +++ b/test/integration/assigned-vault.test.ts @@ -433,3 +433,33 @@ test("the operator recovers a root secret with a key the mesh never held, from t assert.ok(listed.some((k) => k.module === "mesh-vault" && k.name === "broker"), JSON.stringify(served)); assert.ok(!JSON.stringify(served).includes(onDisk), "secret_export returned a plaintext value"); }); + +test("a seeded file is created once, and what a program grows in it survives the next push", { + skip, timeout: 600_000, +}, async () => { + // novox/hq ADR 0087, issue 035. A file that says create-once is written when absent and left + // alone when present — content, mode and owner — so an access list a program persists into is + // not restored to its seed behind the program's back on every reconcile. + const manifest = JSON.stringify({ + module: "seed-test", version: "1", + resources: [ + { id: "dir", type: "directory", path: "/var/lib/seed-test", mode: "0755" }, + { id: "acl", type: "file", path: "/var/lib/seed-test/acl.conf", mode: "0600", "create-once": true, + content: "user default on\n" }, + ], + }); + await must(`printf %s ${quote(manifest)} > /tmp/seed-test.json && docker cp /tmp/seed-test.json mesh-controller:/seed-test.json`); + await mesh("module add /seed-test.json"); + await mesh(`assign ${MACHINE} seed-test`); + await mesh(`push ${MACHINE}`); + await settled(); + assert.equal(await must(`cat /var/lib/seed-test/acl.conf`), "user default on\n"); + + // The program grows it. + await must(`printf 'user app-one on >secret\n' >> /var/lib/seed-test/acl.conf`); + // A second push: the mesh reconciles everything it declared, and leaves the seed alone. + await mesh(`push ${MACHINE}`); + await settled(); + assert.equal(await must(`cat /var/lib/seed-test/acl.conf`), "user default on\nuser app-one on >secret\n", + "the seed was restored and what the program wrote into it was wiped"); +}); diff --git a/test/integration/one-node-mesh.test.ts b/test/integration/one-node-mesh.test.ts index 83c1855..7fcfcaa 100644 --- a/test/integration/one-node-mesh.test.ts +++ b/test/integration/one-node-mesh.test.ts @@ -40,6 +40,7 @@ import { test, before, after } from "node:test"; import assert from "node:assert/strict"; import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs"; import { execFileSync } from "node:child_process"; +import net from "node:net"; import { resolve } from "node:path"; import { loadScenario } from "../../src/declaration/parse.ts"; import { raise } from "../../src/lifecycle/raise.ts"; @@ -247,6 +248,31 @@ async function mesh(command: string, timeoutMs?: number): Promise { } } +/** The machine's address on the lab's uplink bridge — the one the workstation can dial. */ +async function uplinkAddressOf(machine: string): Promise { + const name = await instanceNameOf(instanceId, machine); + const listed = (await incus(["list", name, "--format", "csv", "-c", "4"], 30_000)).stdout; + const addresses = listed.split(/[,\s]+/).map((a: string) => a.trim()).filter((a: string) => /^10\./.test(a)); + assert.ok(addresses.length > 0, `no uplink address for ${machine} in:\n${listed}`); + return addresses[0] as string; +} + +/** Try to open a TCP connection every two seconds to each port, and remember whether any attempt ever succeeded. */ +function probeFromOutside(address: string, ports: number[]): { stop(): void; seen(): Map } { + const seen = new Map(ports.map((p) => [p, false])); + const attempt = () => { + for (const port of ports) { + const socket = net.connect({ host: address, port, timeout: 1000 }); + socket.once("connect", () => { seen.set(port, true); socket.destroy(); }); + socket.once("timeout", () => socket.destroy()); + socket.once("error", () => socket.destroy()); + } + }; + attempt(); + const timer = setInterval(attempt, 2000); + return { stop: () => clearInterval(timer), seen: () => seen }; +} + /** Until the anchor reports the last declaration genesis pushed as applied and current. */ async function settledAfterGenesis(withinMs = 300_000): Promise { const deadline = Date.now() + withinMs; @@ -508,6 +534,11 @@ before(async () => { // nothing held: no image is pre-resolved, because none is here to resolve to. await step("R1", GENESIS, null, async () => { try { + // Probed from the workstation for the whole install (novox/hq ADR 0088, issue 054): the + // store's client port must never answer from outside the machine, while the bus's must + // come to — which is also what proves the probe reaches the machine at all. + const probe = probeFromOutside(await uplinkAddressOf(CONTROL), [5432, 5671]); + try { raised = await genesis({ instanceId, node: CONTROL, @@ -538,6 +569,15 @@ before(async () => { ...(binary ? { hostBinary: binary } : {}), log: (m) => console.log(m), }); + } finally { + probe.stop(); + } + const seen = probe.seen(); + assert.equal(seen.get(5432), false, + "the store's port answered from outside the machine during the install — the base filter did not hold (issue 054)"); + assert.equal(seen.get(5671), true, + "the bus never answered from outside during the install, so the probe proves nothing — is the uplink address right?"); + raised.report.push(` filtered 5432 never answered from outside during the install; 5671 did`); } catch (err) { throw new Error(`the installer never ran: ${(err as Error).message}`); }