A provisioner removes nothing while its contributions file cannot be used

An unreadable, unparsable or unrecognised contributions file was read as "no consumer asks", and the
reconcile pass then removed every consumer it had applied. On 2026-10-04 one such read made the
postgres provider drop every database on the control node. Such a pass now applies and removes
nothing and says why, once; only a file read with a given list withdraws a consumer, and every
removal is logged before it is made.
This commit is contained in:
2026-10-05 01:28:57 +02:00
parent c253d76321
commit 2865d53991
2 changed files with 94 additions and 16 deletions
+51 -1
View File
@@ -1,6 +1,6 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, writeFile, readFile, readdir } from "node:fs/promises";
import { mkdtemp, writeFile, readFile, readdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -160,6 +160,56 @@ test("provisioner stops calling an unreadable secret a race, and says what to lo
assert.ok(loud[0].includes("issue 225"), "the loud line does not name the record");
});
test("provisioner removes nothing while its contributions file cannot be used (novox/hq issue 241)", async () => {
const dir = await mkdtemp(join(tmpdir(), "prov-"));
const created: string[] = [];
const removed: string[] = [];
await writeFile(join(dir, "a.secret"), "pw-a\n");
await writeFile(join(dir, "b.secret"), "pw-b\n");
const receives = join(dir, "postgres-database.json");
const doc = (given: unknown[]): string =>
JSON.stringify({ contributions: 1, requirement: "postgres-database", given });
const both = doc([
{ from: "forge", node: "anchor", as: "mesh_anchor_forge", secret: join(dir, "a.secret") },
{ from: "mail", node: "anchor", as: "mesh_anchor_mail", secret: join(dir, "b.secret") },
]);
await writeFile(receives, both);
const stop = runProvisioner(
"postgres-database",
{
async create(p) {
created.push(p.as);
},
async remove(p) {
removed.push(p.as);
},
},
{ receives, everyMs: 20 },
);
await waitFor(() => created.length === 2, 2000);
// Each way the file can say nothing usable, held for many passes: nothing is withdrawn.
for (const broken of [
async () => rm(receives),
async () => writeFile(receives, "{ half a fi"),
async () => writeFile(receives, JSON.stringify({ contributions: 1, requirement: "s3-bucket", given: [] })),
async () => writeFile(receives, JSON.stringify({ contributions: 1, requirement: "postgres-database" })),
]) {
await broken();
await new Promise((r) => setTimeout(r, 200));
assert.deepEqual(removed, [], "a consumer was removed while the file could not be used");
await writeFile(receives, both);
await new Promise((r) => setTimeout(r, 100));
}
// The file read and saying so still withdraws: an explicit empty list removes both.
await writeFile(receives, doc([]));
await waitFor(() => removed.length === 2, 2000);
assert.deepEqual([...removed].sort(), ["mesh_anchor_forge", "mesh_anchor_mail"]);
stop();
});
test("provisioner applies again what the backend no longer holds, and trusts memory without holds", async () => {
const dir = await mkdtemp(join(tmpdir(), "prov-holds-"));
await writeFile(join(dir, "webapp.secret"), "minted-pw\n");