Files
mesh-lab/test/integration/provisioner.test.ts
T
jschoubben cb0edcee8d The end-to-end test reads the grant where the mesh now writes it
Two assertions in the full-mesh test encoded the old naming: the grant
file read back from the provider, and the PostgreSQL role the real
application logs in as. Both are named after the consumer now, and a
consumer is a module on a machine.

These are the two that matter most in this file — it is the only place
where a real application authenticates against a real database with a
password the mesh delivered and cannot read, so they are what would have
caught the naming going wrong end to end.
2026-09-01 02:48:24 +02:00

259 lines
11 KiB
TypeScript

/**
* The last step of a credential, against a real database.
*
* The mesh generates a password, seals it to the machine that must accept it, and discards the
* plaintext — so it cannot tell PostgreSQL to start accepting it. Something on that machine reads
* what the host wrote and makes it true. Everything up to that point is proven elsewhere; this is
* the step where a password either becomes a login or does not.
*
* Against a real PostgreSQL because there is no version of this worth asserting against a fake:
* what is under test is whether `create role ... password` and a connection agree, which is
* exactly what a fake would be told to agree about (novox/hq ADR 0017).
*/
import { test, after, before } from "node:test";
import assert from "node:assert/strict";
import { loadScenario } from "../../src/declaration/parse.ts";
import { raise } from "../../src/lifecycle/raise.ts";
import { destroy, exec } from "../../src/lifecycle/operate.ts";
import { labIsUsable, destroyAll } from "./harness.ts";
import { incus } from "../../src/incus/client.ts";
import { machineName } from "../../src/lifecycle/names.ts";
const capability = await labIsUsable();
const provisioner = process.env["MESH_LAB_PROVISIONER"] ?? "";
const skip = !capability.usable
? `lab not usable: ${capability.why}`
: !provisioner
? "set MESH_LAB_PROVISIONER to a built provisioner (mesh-control: go build ./examples/postgres-provisioner)"
: false;
const SCENARIO = "a-provider";
const MACHINE = "anchor";
const GRANTS = "/var/lib/postgres/grants";
const SUPER = "postgres://postgres:super@127.0.0.1:5432/postgres?sslmode=disable";
let instanceId = "";
/** The postgres image, by digest, from the registry the scenario raised. */
let image = "";
function shellQuote(s: string): string {
return `'${s.replaceAll("'", `'\\''`)}'`;
}
/** Run something on the machine and return what it said, with its exit status. */
async function on(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=");
const status = Number(stdout.slice(marker + 7).trim());
return { out: stdout.slice(0, marker), ok: status === 0 };
}
/** The same, refusing to continue past a failure nobody would otherwise see. */
async function must(command: string): Promise<string> {
const { out, ok } = await on(command);
if (!ok) throw new Error(`${command}\n${out}`);
return out;
}
/** psql as the superuser, inside the database container. */
async function sql(query: string): Promise<string> {
return (await must(`docker exec mesh-db psql -U postgres -qAt -c ${shellQuote(query)}`)).trim();
}
/**
* Write what the host would have written from a declaration: the manifest of who asked, and one
* file per consumer holding its password alone.
*
* Written here rather than by running the host, because what is under test is the step *after*
* the host — and that the host writes these exact shapes is asserted in its own suite.
*/
async function meshWrote(
consumers: { node: string; module: string; name: string; password: string }[],
): Promise<void> {
const manifest = {
contributions: 1,
requirement: "postgres-database",
generated: "by the mesh",
given: consumers.map((c) => ({
from: c.module,
node: c.node,
secret: `${GRANTS}/${c.node}.${c.module}.secret`,
values: { name: c.name },
})),
};
await must(`mkdir -p ${GRANTS}`);
await must(`printf %s ${shellQuote(JSON.stringify(manifest))} > ${GRANTS}/mesh.json`);
// Every credential file rewritten from nothing, so a removed consumer's does not linger and
// make the revocation test pass for a reason that is not the one being tested.
await must(`find ${GRANTS} -name '*.secret' -delete`);
for (const c of consumers) {
await must(`printf %s ${shellQuote(c.password)} > ${GRANTS}/${c.node}.${c.module}.secret`);
await must(`chmod 600 ${GRANTS}/${c.node}.${c.module}.secret`);
}
}
/** The provisioner, as the module shipping PostgreSQL would run it. */
async function provision(): Promise<{ out: string; ok: boolean }> {
return on(
`GRANTS=${GRANTS} MESH_PROVISION_POSTGRES=${shellQuote(SUPER)} /usr/local/bin/mesh-provision-postgres`,
);
}
/**
* Can this role log in with this password?
*
* Over the bridge, from a container of its own. `--network container:mesh-db` would share the
* database's namespace and put us back on its loopback, which is the very thing being avoided.
*
* The address comes from `.NetworkSettings.Networks.bridge.IPAddress` rather than the top-level
* `.NetworkSettings.IPAddress`, which docker 29 no longer populates — it templates to empty, psql
* silently falls back to a unix socket that is not there, and every login looks impossible.
*
* From a separate container, reaching the database over the bridge — **not** from inside it over
* loopback. PostgreSQL's default `pg_hba.conf` trusts `127.0.0.1`, so a check made from inside
* the container authenticates nothing and returns true for any password at all. Which is what the
* first version of this did: two tests passed without ever verifying a password, and only the
* rotation test noticed, by asserting that an old password had *stopped* working.
*/
async function canLogIn(role: string, password: string, database: string): Promise<boolean> {
return (await tryLogIn(role, password, database)).ok;
}
/** The same, keeping what the database said — so a failure says why rather than only that. */
async function tryLogIn(
role: string,
password: string,
database: string,
): Promise<{ ok: boolean; out: string }> {
const { out } = await on(
`docker run --rm -e PGPASSWORD=${shellQuote(password)} ${image} ` +
`psql -h "$(docker inspect -f '{{.NetworkSettings.Networks.bridge.IPAddress}}' mesh-db)" ` +
`-U ${role} -d ${database} -qAt -c 'select 1'`,
);
return { ok: out.trim() === "1", out };
}
before(async () => {
if (skip) return;
const scenario = loadScenario(`scenarios/${SCENARIO}.yml`);
const instance = await raise(scenario, {});
instanceId = instance.instanceId;
// From the registry the scenario raised, by digest. There is no route to a public registry from
// a documentation range, which is the point of the lab having its own.
const stocked = instance.images.find((r) => r.includes("postgres"));
assert.ok(stocked, `the scenario stocked no postgres image: ${instance.images.join(", ")}`);
image = stocked;
await must(
`docker run -d --name mesh-db -e POSTGRES_PASSWORD=super ` +
`-p 127.0.0.1:5432:5432 ${image}`,
);
let ready = false;
for (let i = 0; i < 90 && !ready; i++) {
({ ok: ready } = await on(`docker exec mesh-db pg_isready -U postgres`));
if (!ready) await new Promise((r) => setTimeout(r, 1000));
}
assert.ok(ready, "the database never became ready");
await incus([
"file", "push", provisioner,
`${machineName(instanceId, MACHINE)}/usr/local/bin/mesh-provision-postgres`,
"--mode", "0755",
], 180_000);
}, { timeout: 1_200_000 });
after(async () => {
if (instanceId) await destroy(instanceId);
await destroyAll(`${SCENARIO}-`);
}, { timeout: 600_000 });
test("a password the mesh generated becomes a login that works", { skip, timeout: 300_000 }, async () => {
await meshWrote([
{ node: "workstation", module: "meshboard", name: "meshboard", password: "first-password-aaa" },
]);
const { out, ok } = await provision();
assert.ok(ok, out);
assert.equal(await sql(`select rolcanlogin from pg_roles where rolname = 'mesh_workstation_meshboard'`), "t");
assert.equal(await sql(`select 1 from pg_database where datname = 'meshboard'`), "1");
const attempt = await tryLogIn("mesh_workstation_meshboard", "first-password-aaa", "meshboard");
assert.ok(attempt.ok, `the consumer cannot log in with the password the mesh gave it:\n${attempt.out}`);
});
test("running it again reaches the same state and says nothing", { skip, timeout: 300_000 }, async () => {
// It runs after every declaration and is never told what changed, so arriving at an already
// correct state is the ordinary case rather than an edge one.
const { out, ok } = await provision();
assert.ok(ok, out);
assert.equal(out.trim(), "", `it did work on a second run: ${out}`);
assert.ok(await canLogIn("mesh_workstation_meshboard", "first-password-aaa", "meshboard"));
});
test("rotating the password makes the new one work and the old one stop", { skip, timeout: 300_000 }, async () => {
// The failure this guards is a provisioner that only ever creates: the mesh replaces the file,
// the role exists, nothing happens, and a rotation reports success while changing nothing.
await meshWrote([
{ node: "workstation", module: "meshboard", name: "meshboard", password: "second-password-bbb" },
]);
const { out, ok } = await provision();
assert.ok(ok, out);
assert.ok(
await canLogIn("mesh_workstation_meshboard", "second-password-bbb", "meshboard"),
"the rotated password does not work",
);
assert.equal(
await canLogIn("mesh_workstation_meshboard", "first-password-aaa", "meshboard"),
false,
"the old password still works, so the rotation changed nothing",
);
});
test("a consumer that goes away loses its login", { skip, timeout: 300_000 }, async () => {
// The half usually missing. A consumer removed from the mesh otherwise keeps a working login
// for ever and nothing says so — the same rule the host follows about removing what it declared
// and no longer declares.
await meshWrote([]);
const { out, ok } = await provision();
assert.ok(ok, out);
assert.match(out, /revoked mesh_workstation_meshboard/);
assert.equal(await sql(`select rolcanlogin from pg_roles where rolname = 'mesh_workstation_meshboard'`), "f");
assert.equal(
await canLogIn("mesh_workstation_meshboard", "second-password-bbb", "meshboard"),
false,
"a consumer nobody asks for any more can still log in",
);
});
test("a role nobody here made is left alone", { skip, timeout: 300_000 }, async () => {
// A provisioner that removed every role it did not recognise could not safely be run on a
// database that predates it — which is every database anybody would want to adopt.
await sql(`create role someone_elses with login password 'theirs'`);
await sql(`create database theirs owner someone_elses`);
await meshWrote([]);
const { ok } = await provision();
assert.ok(ok);
assert.equal(await sql(`select rolcanlogin from pg_roles where rolname = 'someone_elses'`), "t");
assert.ok(await canLogIn("someone_elses", "theirs", "theirs"));
});
test("a manifest naming a credential that was never written is refused", { skip, timeout: 300_000 }, async () => {
// Rather than creating a role with no password — a login nothing can use, which nothing would
// report until something tried to connect.
await meshWrote([]);
await must(
`printf %s '{"contributions":1,"requirement":"postgres-database","given":[` +
`{"from":"meshboard","node":"ghost","secret":"${GRANTS}/ghost.meshboard.secret","values":{"name":"ghost"}}` +
`]}' > ${GRANTS}/mesh.json`,
);
const { out, ok } = await provision();
assert.equal(ok, false, "it carried on past a missing credential");
assert.match(out, /should be at .*ghost\.meshboard\.secret/);
assert.equal(await sql(`select count(*) from pg_roles where rolname = 'mesh_ghost_meshboard'`), "0");
});