The status path was demonstrated with inserted rows, which proves the query and not the path. This sends a real machine something it will genuinely fail at — a package that does not exist — and asks the mesh afterwards. A failure of the ordinary kind: the host tries, the package manager says no, some of the declaration is applied and some is not. That is the situation `status` exists to distinguish from a machine that refused everything, and the test asserts the distinction survives the whole way: the machine is listed as failed rather than refused, the failing resource is named in the host's own words, and the machine that did as it was told is not implicated.
232 lines
11 KiB
TypeScript
232 lines
11 KiB
TypeScript
/**
|
|
* A mesh, raised from nothing, joined by two machines, delivering a credential neither the mesh
|
|
* nor the broker can read.
|
|
*
|
|
* Everything before this proves a part. This proves the parts meet — which is the thing the
|
|
* project keeps saying cannot be checked any other way (novox/hq ADR 0001: every fault of
|
|
* 2026-08-22 was found in production because nothing could be stood up locally).
|
|
*
|
|
* It needs a host binary and the substrate bundle:
|
|
*
|
|
* MESH_LAB_HOST_BINARY=.../mesh-host
|
|
* MESH_LAB_BUNDLE=.../examples/substrate-first-node.lock
|
|
*
|
|
* The bundle's image references are rewritten to the ones this scenario's own registry serves.
|
|
* A digest belongs to whatever registry serves it, so a committed bundle names a registry that is
|
|
* not this one — rewriting is what makes it applicable rather than a placeholder to tidy away.
|
|
*/
|
|
|
|
import { test, before, after } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { loadScenario } from "../../src/declaration/parse.ts";
|
|
import { raise } from "../../src/lifecycle/raise.ts";
|
|
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
|
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
|
import { labIsUsable, destroyAll } from "./harness.ts";
|
|
|
|
const capability = await labIsUsable();
|
|
const binary = hostBinaryPath();
|
|
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
|
|
|
const skip = !capability.usable
|
|
? `lab not usable: ${capability.why}`
|
|
: !binary || !existsSync(binary)
|
|
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
|
: !bundle || !existsSync(bundle)
|
|
? "MESH_LAB_BUNDLE is not set to a substrate bundle (mesh-host examples/)"
|
|
: false;
|
|
|
|
const SCENARIO = "two-nodes";
|
|
let instanceId = "";
|
|
|
|
function quote(s: string): string {
|
|
return `'${s.replaceAll("'", `'\\''`)}'`;
|
|
}
|
|
|
|
async function on(machine: string, command: string): Promise<{ out: string; ok: boolean }> {
|
|
const { stdout } = await exec(instanceId, machine, [
|
|
"sh", "-c", `${command} 2>&1; echo "__exit=$?"`,
|
|
]);
|
|
const marker = stdout.lastIndexOf("__exit=");
|
|
return { out: stdout.slice(0, marker), ok: Number(stdout.slice(marker + 7).trim()) === 0 };
|
|
}
|
|
|
|
async function must(machine: string, command: string): Promise<string> {
|
|
const { out, ok } = await on(machine, command);
|
|
if (!ok) throw new Error(`${machine}: ${command}\n${out}`);
|
|
return out;
|
|
}
|
|
|
|
/** The control plane, which runs in a container on the first node. */
|
|
async function mesh(command: string): Promise<string> {
|
|
return must("anchor", `docker exec mesh-control /mesh-control ${command}`);
|
|
}
|
|
|
|
/**
|
|
* The bundle, with every image reference pointed at this scenario's registry.
|
|
*
|
|
* Matched by repository rather than by the whole reference, because the address and the digest
|
|
* both differ from whatever the committed bundle names — and a bundle that names the wrong
|
|
* registry is not wrong, it is built for a different target.
|
|
*/
|
|
function bundleFor(images: string[]): string {
|
|
let text = readFileSync(bundle, "utf8");
|
|
for (const pinned of images) {
|
|
const repository = pinned.slice(pinned.indexOf("/") + 1, pinned.indexOf("@"));
|
|
const escaped = repository.replaceAll("/", "\\/").replaceAll(".", "\\.");
|
|
text = text.replaceAll(
|
|
new RegExp(`[A-Za-z0-9_.:-]+\\/${escaped}@sha256:[0-9a-f]+`, "g"),
|
|
pinned,
|
|
);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
/** Take a token out of what `token issue` printed. It is the one base64url blob on its own line. */
|
|
function tokenFrom(said: string): string {
|
|
const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" "));
|
|
assert.ok(found, `no token in:\n${said}`);
|
|
return found;
|
|
}
|
|
|
|
before(async () => {
|
|
if (skip) return;
|
|
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {});
|
|
instanceId = raised.instanceId;
|
|
|
|
// The first node raises everything from a file rather than from a bundle built into the binary,
|
|
// because the digests are this registry's and are not known until it is up.
|
|
await must("anchor", `cat > /tmp/substrate.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`);
|
|
await must("anchor", `${HOST_PATH} apply /tmp/substrate.lock`);
|
|
}, { timeout: 1_800_000 });
|
|
|
|
after(async () => {
|
|
if (instanceId) await destroy(instanceId);
|
|
await destroyAll(`${SCENARIO}-`);
|
|
}, { timeout: 600_000 });
|
|
|
|
test("a bare machine becomes a mesh", { skip, timeout: 600_000 }, async () => {
|
|
const running = await must("anchor", `docker ps --format '{{.Names}}'`);
|
|
for (const container of ["mesh-store", "mesh-broker", "mesh-control"]) {
|
|
assert.match(running, new RegExp(container), `${container} is not running`);
|
|
}
|
|
// Answering, not merely up. A container that is running is not a control plane that replies —
|
|
// a distinction this project has already paid for once.
|
|
assert.ok((await mesh("status")).length > 0);
|
|
});
|
|
|
|
test("both machines join it, and the token is all they need", { skip, timeout: 900_000 }, async () => {
|
|
for (const [machine, node] of [["anchor", "anchor"], ["laptop", "laptop"]] as const) {
|
|
await mesh(`node add ${node}`);
|
|
const token = tokenFrom(await mesh(`token issue --node ${node}`));
|
|
// No --name. The token says what the mesh calls the machine, which is the fault this walk
|
|
// found the first time it was run.
|
|
const said = await must(machine, `${HOST_PATH} enrol --token ${quote(token)}`);
|
|
assert.match(said, new RegExp(`enrolled as ${node}`), said);
|
|
assert.match(said, /sealing key/, "no sealing key was generated");
|
|
}
|
|
|
|
const recorded = await must("anchor",
|
|
`docker exec mesh-store psql -U postgres -d inventory -qAt ` +
|
|
`-c "select name from node where sealing_key is not null order by name"`);
|
|
assert.equal(recorded.trim().split("\n").map((l) => l.trim()).sort().join(","), "anchor,laptop",
|
|
"the mesh did not record a sealing key for both machines");
|
|
});
|
|
|
|
test("a credential reaches both ends and the mesh holds neither", { skip, timeout: 900_000 }, async () => {
|
|
// The whole argument, on real machines: the two ends must hold the SAME password, and it must
|
|
// appear nowhere the mesh or the broker could read it.
|
|
await must("anchor", `printf %s '{"module":"postgres","version":"1",` +
|
|
`"provides":[{"name":"database","scope":"mesh"}],"serves":{"database":{"port":5432}},` +
|
|
`"grants":{"database":"/var/lib/mesh-host/grants"},` +
|
|
`"receives":{"database":"/var/lib/mesh-host/grants/mesh.json"},"resources":[]}' > /tmp/pg.json`);
|
|
await must("anchor", `printf %s '{"module":"meshboard","version":"1",` +
|
|
`"requires":["database"],"contributes":{"database":{"name":"meshboard"}},` +
|
|
`"binds":{"database":"/etc/meshboard/database.json"},` +
|
|
`"secrets":{"database":"/etc/meshboard/database.password"},"resources":[]}' > /tmp/app.json`);
|
|
await must("anchor", `docker cp /tmp/pg.json mesh-control:/pg.json`);
|
|
await must("anchor", `docker cp /tmp/app.json mesh-control:/app.json`);
|
|
await mesh("module add /pg.json");
|
|
await mesh("module add /app.json");
|
|
|
|
await mesh("overlay place anchor --hub --endpoint 192.0.2.10:51820 --site lab");
|
|
await mesh("overlay place laptop --site lab");
|
|
for (const node of ["anchor", "laptop"]) await mesh(`assign ${node} networking`);
|
|
await mesh("assign anchor postgres");
|
|
await mesh("assign laptop meshboard");
|
|
|
|
for (const machine of ["anchor", "laptop"]) {
|
|
await must(machine, `nohup ${HOST_PATH} run > /var/log/mesh-host.log 2>&1 & sleep 3`);
|
|
}
|
|
await mesh("push");
|
|
await new Promise((r) => setTimeout(r, 8000));
|
|
|
|
const onConsumer = (await must("laptop", `cat /etc/meshboard/database.password`)).trim();
|
|
const onProvider = (await must("anchor", `cat /var/lib/mesh-host/grants/laptop.secret`)).trim();
|
|
assert.ok(onConsumer.length >= 40, `the consumer's credential is ${onConsumer.length} characters`);
|
|
assert.equal(onConsumer, onProvider,
|
|
"the two ends hold different passwords, so nothing could ever authenticate");
|
|
|
|
// Only the machine it is for may read it.
|
|
assert.match(await must("laptop", `stat -c %a /etc/meshboard/database.password`), /^600/);
|
|
|
|
// And it is nowhere it could have been read on the way. The declaration crossed the broker; the
|
|
// database is the control plane's; the state is what the node reported back.
|
|
for (const [machine, where] of [
|
|
["laptop", "/var/lib/mesh-host/declared.json"],
|
|
["laptop", "/var/lib/mesh-host/state.json"],
|
|
["anchor", "/var/lib/mesh-host/declared.json"],
|
|
] as const) {
|
|
const { out } = await on(machine, `grep -c ${quote(onConsumer)} ${where}`);
|
|
assert.equal(out.trim(), "0", `the password is in ${where} on ${machine}`);
|
|
}
|
|
const inTheMesh = await must("anchor",
|
|
`docker exec mesh-store psql -U postgres -d inventory -qAt ` +
|
|
`-c "select count(*) from secret where for_consumer like '%${onConsumer}%' ` +
|
|
`or for_provider like '%${onConsumer}%'"`);
|
|
assert.equal(inTheMesh.trim(), "0", "the control plane's database holds the password in the clear");
|
|
});
|
|
|
|
test("the consumer is also told where its database is", { skip, timeout: 300_000 }, async () => {
|
|
// A password with no address is not a connection. This is the readable half, which stays
|
|
// readable on purpose — it is the secret half that could not be composed, not this one.
|
|
const told = JSON.parse(await must("laptop", `cat /etc/meshboard/database.json`));
|
|
assert.equal(told.from, "anchor");
|
|
assert.equal(told.at, "anchor.internal");
|
|
assert.equal(told.serves.port, 5432);
|
|
// And the name it resolves to was written by the mesh as well, on this machine.
|
|
assert.match(await must("laptop", `grep anchor.internal /etc/hosts`), /10\.42\.0\.\d+/);
|
|
});
|
|
|
|
test("when a machine cannot do what it was told, the mesh says which and why", { skip, timeout: 900_000 }, async () => {
|
|
// Demonstrated with inserted rows first, which proves the query and not the path. This sends a
|
|
// real machine something it will genuinely fail at, and asks the mesh afterwards.
|
|
//
|
|
// A package that does not exist, because that is a failure of the ordinary kind: the host tries,
|
|
// the package manager says no, and some of the declaration is applied and some is not — which
|
|
// is the situation `status` exists to distinguish from a machine that refused everything.
|
|
await must("anchor", `printf %s '{"module":"impossible","version":"1","resources":[` +
|
|
`{"id":"nothing","type":"package","package":"a-package-that-does-not-exist"}]}' > /tmp/imp.json`);
|
|
await must("anchor", `docker cp /tmp/imp.json mesh-control:/imp.json`);
|
|
await mesh("module add /imp.json");
|
|
await mesh("assign laptop impossible");
|
|
await mesh("push laptop");
|
|
await new Promise((r) => setTimeout(r, 10_000));
|
|
|
|
const said = await mesh("status");
|
|
assert.match(said, /not doing what they were told/, said);
|
|
assert.match(said, /laptop/, said);
|
|
// The machine's own words about the resource that failed, not a summary written at this end.
|
|
assert.match(said, /impossible\.nothing/, `the failing resource is not named:\n${said}`);
|
|
|
|
// And the distinction survives: this machine FAILED, it did not refuse. Refused means it is
|
|
// exactly as it was; failed means it is in a state nobody declared, and they are fixed in
|
|
// different places.
|
|
assert.match(said, /laptop\s+failed/, said);
|
|
|
|
// The other machine is not implicated.
|
|
assert.doesNotMatch(said.split("not heard from")[0] ?? said, /anchor\s+(failed|refused)/,
|
|
"a machine that did as it was told is listed as wrong");
|
|
});
|