From 2dfffd6dac90eeda6c2dae1ae9e419b102df009a Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 12:34:59 +0200 Subject: [PATCH] Review of 081: the bed asserts baserow chose its own cache and it challenges for its password; the check reads only what a module hands its software; stale comments --- scenarios/two-node-db.yml | 5 ++-- scenarios/whole-mesh-ace.yml | 2 +- ...ache-consumers-present-their-login.test.ts | 21 +++++++++++++++- test/integration/assigned-two-node-db.test.ts | 24 ++++++++++++------- test/integration/whole-mesh-ace.test.ts | 4 ++-- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/scenarios/two-node-db.yml b/scenarios/two-node-db.yml index dd9c57c..801f35c 100644 --- a/scenarios/two-node-db.yml +++ b/scenarios/two-node-db.yml @@ -6,9 +6,8 @@ # the baserow and letta CONSUMERS of the one store (baserow keeps its cache inside its own container, # novox/hq 081). Both # machines sit on one shared segment and enrol into the one mesh; only enrolment crosses to anchor, -# over the underlay both machines already share. Provider and consumers are co-located on laptop, so -# no cross-node module comms and no overlay are needed — and the 5432-vs-foundation conflict is gone -# because the foundation store is on the OTHER node. +# over the underlay both machines already share. The consumers' databases are minted on the one +# foundation store on anchor and reached over the overlay; laptop runs no provider of its own. scenario: two-node-db segments: diff --git a/scenarios/whole-mesh-ace.yml b/scenarios/whole-mesh-ace.yml index 3ecb8f1..8d8147c 100644 --- a/scenarios/whole-mesh-ace.yml +++ b/scenarios/whole-mesh-ace.yml @@ -3,7 +3,7 @@ # scenarios/whole-mesh-novox.yml; same topology, a different (larger, media-heavy) module set. # # Foundation (store, broker, control) rides `anchor` and NOTHING else; ALL of ace's services ride the -# `ace` node. An overlay is placed so the two DB consumers (baserow, letta) reach the postgres/redis +# `ace` node. An overlay is placed so the two DB consumers (baserow, letta) reach the postgres # providers co-located with them. The media stack (sonarr/radarr/lidarr/plex/bazarr/nzbget/ # qbittorrent/bookshelf) shares the operator-owned library directories under /services/media (ADR # 0051 `accesses`); the test pre-creates them on the node, as the operator would, before the push — diff --git a/test/cache-consumers-present-their-login.test.ts b/test/cache-consumers-present-their-login.test.ts index 227c5dc..8468c2c 100644 --- a/test/cache-consumers-present-their-login.test.ts +++ b/test/cache-consumers-present-their-login.test.ts @@ -21,9 +21,23 @@ import { catalogueIsPresent, catalogueDir } from "./integration/harness.ts"; const CACHE = "redis-cache"; +/** What a module hands its software: every file it writes, and every container's environment and + * arguments. A comment, a `why`, or a declared exception is not handed to anything. */ +function handedToSoftware(manifest: string): string[] { + const m = JSON.parse(manifest) as { resources?: Record[] }; + const out: string[] = []; + for (const r of m.resources ?? []) { + if (typeof r["content"] === "string") out.push(r["content"] as string); + if (r["env"] && typeof r["env"] === "object") out.push(...Object.values(r["env"] as Record).map(String)); + if (Array.isArray(r["args"])) out.push(...(r["args"] as unknown[]).map(String)); + } + return out; +} + /** Whether a manifest hands its software the login it is granted for the cache. */ function presentsTheLogin(manifest: string): boolean { - return manifest.includes(`\${bound:${CACHE}:as}`); + const login = `\${bound:${CACHE}:as}`; + return handedToSoftware(manifest).some((given) => given.includes(login)); } test("the check sees a module that hands its software the password and not the login", () => { @@ -33,6 +47,11 @@ test("the check sees a module that hands its software the password and not the l { id: "env", type: "file", path: "/x", content: `USER=\${bound:${CACHE}:as}\nPASSWORD=\${secret:${CACHE}}\n` }] }); assert.equal(presentsTheLogin(passwordOnly), false); assert.equal(presentsTheLogin(withLogin), true); + // Named only where no software reads it — a declared reason — is not presenting it. + const onlyInAReason = JSON.stringify({ module: "m", requires: [CACHE], resources: [ + { id: "srv", type: "container", name: "s", image: "x", env: { PASSWORD: `\${secret:${CACHE}}` }, + "secrets-in-environment": `the login is \${bound:${CACHE}:as}` }] }); + assert.equal(presentsTheLogin(onlyInAReason), false); }); test("every catalogue module that takes the shared cache presents the login it was granted", (t) => { diff --git a/test/integration/assigned-two-node-db.test.ts b/test/integration/assigned-two-node-db.test.ts index c87c837..81391a1 100644 --- a/test/integration/assigned-two-node-db.test.ts +++ b/test/integration/assigned-two-node-db.test.ts @@ -28,7 +28,7 @@ * scripts/build-module-runtime.sh postgres /tmp/postgres.tar * scripts/build-module-runtime.sh baserow /tmp/baserow.tar * scripts/build-module-runtime.sh letta /tmp/letta.tar - * The service images (postgres:17-alpine, baserow/baserow:latest, letta/letta:latest) + * The service images (postgres, baserow, letta, each pinned by digest) * must be in the local daemon too; scenarios/two-node-db.yml stocks all of them, and each node pulls * what it runs from the scenario's own registry by digest. */ @@ -486,19 +486,25 @@ test("consumers on a joined node get their databases from the one foundation sto } // baserow's cache is its own: with no REDIS_HOST the image runs one inside the container, under a - // password it makes itself, and the mesh grants nothing (novox/hq 081). Up means it answers on - // loopback — PONG, or the challenge for the password it holds — and baserow logged no refusal. + // password it makes itself, and the mesh grants nothing (novox/hq 081). Three things say so: + // baserow said it chose its own; the cache answers on loopback with the challenge for that password + // (PONG would be a cache anyone can use, and fails); and nothing was refused a login. + const baserowLog = async () => (await on(NODE, `docker logs baserow 2>&1`)).out; + let chose = ""; let cache = { out: "", ok: false }; - const untilCache = Date.now() + 180_000; + const untilCache = Date.now() + 240_000; while (Date.now() < untilCache) { + chose = await baserowLog(); cache = await on(NODE, `docker exec baserow redis-cli -h 127.0.0.1 ping 2>&1`); - if (/PONG|NOAUTH/.test(cache.out)) break; + if (/Using embedded baserow redis/.test(chose) && /NOAUTH/.test(cache.out)) break; await new Promise((r) => setTimeout(r, 5000)); } - assert.match(cache.out, /PONG|NOAUTH/, `baserow's own cache is not running inside its container:\n${cache.out}`); - const baserowLog = (await on(NODE, `docker logs baserow 2>&1 | tail -400`)).out; - assert.doesNotMatch(baserowLog, /WRONGPASS|NOPERM|NOAUTH|Error .*connecting to .*6379/i, - `baserow could not use its cache:\n${baserowLog.split("\n").filter((l) => /redis|6379|NOAUTH|WRONGPASS|NOPERM/i.test(l)).slice(-15).join("\n")}`); + assert.match(chose, /Using embedded baserow redis/, + `baserow did not start its own cache:\n${chose.split("\n").filter((l) => /redis/i.test(l)).slice(-15).join("\n")}`); + assert.match(cache.out, /NOAUTH/, + `baserow's own cache does not answer with its password challenge inside the container:\n${cache.out}`); + assert.doesNotMatch(chose, /WRONGPASS|NOPERM/, + `baserow was refused by its cache:\n${chose.split("\n").filter((l) => /WRONGPASS|NOPERM/.test(l)).slice(-15).join("\n")}`); // Helper: wait for the mesh to write a consumer's bound file with an `as`, and parse it. async function waitForBinding(path: string): Promise<{ as: string; provision: string }> { diff --git a/test/integration/whole-mesh-ace.test.ts b/test/integration/whole-mesh-ace.test.ts index 818c40d..5b1d0c5 100644 --- a/test/integration/whole-mesh-ace.test.ts +++ b/test/integration/whole-mesh-ace.test.ts @@ -4,7 +4,7 @@ * whole-mesh-novox.test.ts; same harness and topology, a larger, media-heavy module set. * * Foundation (store, broker, control) rides `anchor` and nothing else; ALL of ace's services ride the - * `ace` node. An overlay is placed so the two DB consumers (baserow, letta) reach the postgres/redis + * `ace` node. An overlay is placed so the two DB consumers (baserow, letta) reach the postgres * providers co-located with them. The media stack shares the operator-owned library directories * under /services/media (ADR 0051 `accesses`): the mesh writes an `access` resource that CONFIRMS * each path exists and mounts it, but creates and chowns none of it — so before() pre-creates those @@ -264,7 +264,7 @@ test("the whole ace service set resolves, installs and converges on one node in }, async () => { for (const d of DROPPED) console.log(`DROPPED ${d.name}: ${d.why}`); - // The overlay, so a consumer's binding `at` is non-empty (baserow/letta reach postgres/redis). + // The overlay, so a consumer's binding `at` is non-empty (baserow/letta reach postgres). await mesh("overlay place anchor --hub --endpoint 192.0.2.10:51820 --site lab"); await mesh(`overlay place ${NODE} --site lab`); await mesh("assign anchor networking");