Beds for the seed file and for the foundation's filter
The vault bed grows into a create-once file and pushes again; the genesis bed probes the machine from the workstation for the whole install and asserts the store's port never answers while the bus's does.
This commit is contained in:
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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<string> {
|
||||
}
|
||||
}
|
||||
|
||||
/** The machine's address on the lab's uplink bridge — the one the workstation can dial. */
|
||||
async function uplinkAddressOf(machine: string): Promise<string> {
|
||||
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<number, boolean> } {
|
||||
const seen = new Map<number, boolean>(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<void> {
|
||||
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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user