Beds for the seed file and the foundation's filter; beds deliver secrets as files; a bundle-raised bed derives the anchor's filter #40
@@ -168,14 +168,13 @@ test("the mesh assigns mongodb, unifi, marrytts and postgres to one node in one
|
|||||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/postgres", mode: "0700" },
|
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/postgres", mode: "0700" },
|
||||||
{ id: "state", type: "directory", path: "/var/lib/postgres", mode: "0700" },
|
{ id: "state", type: "directory", path: "/var/lib/postgres", mode: "0700" },
|
||||||
{ id: "grants", type: "directory", path: "/var/lib/postgres/grants", mode: "0700" },
|
{ id: "grants", type: "directory", path: "/var/lib/postgres/grants", mode: "0700" },
|
||||||
{ id: "superuser-env", type: "file", path: "/var/lib/postgres/superuser.env", mode: "0600", content: "POSTGRES_PASSWORD=${secret:superuser}\n" },
|
|
||||||
{ id: "data", type: "directory", path: "/services/postgres/db-data", mode: "0700" },
|
{ id: "data", type: "directory", path: "/services/postgres/db-data", mode: "0700" },
|
||||||
{ id: "net", type: "network", name: "postgres" },
|
{ id: "net", type: "network", name: "postgres" },
|
||||||
{
|
{
|
||||||
id: "server", type: "container", name: "postgres", image: pinned("postgres"), network: "postgres",
|
id: "server", type: "container", name: "postgres", image: pinned("postgres"), network: "postgres",
|
||||||
env: { POSTGRES_USER: "postgres", POSTGRES_DB: "postgres" },
|
// The superuser reaches postgres as a file (novox/hq ADR 0086), the shape the catalogue's manifest has.
|
||||||
"env-file": ["/var/lib/postgres/superuser.env"],
|
env: { POSTGRES_USER: "postgres", POSTGRES_DB: "postgres", POSTGRES_PASSWORD_FILE: "/run/secrets/superuser" },
|
||||||
volumes: ["/services/postgres/db-data:/var/lib/postgresql/data"],
|
volumes: ["/services/postgres/db-data:/var/lib/postgresql/data", "/var/lib/postgres/superuser.secret:/run/secrets/superuser:ro"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "runtime", type: "container", name: "mesh-postgres", image: pinned("mesh-runtime-postgres"),
|
id: "runtime", type: "container", name: "mesh-postgres", image: pinned("mesh-runtime-postgres"),
|
||||||
@@ -216,18 +215,19 @@ test("the mesh assigns mongodb, unifi, marrytts and postgres to one node in one
|
|||||||
receives: { "mongodb-database": "/var/lib/mongodb/grants/mesh.json" },
|
receives: { "mongodb-database": "/var/lib/mongodb/grants/mesh.json" },
|
||||||
grants: { "mongodb-database": "/var/lib/mongodb/grants" },
|
grants: { "mongodb-database": "/var/lib/mongodb/grants" },
|
||||||
"own-secrets": { root: "/var/lib/mongodb/root.secret", broker: "/var/lib/mesh/mongodb/broker" },
|
"own-secrets": { root: "/var/lib/mongodb/root.secret", broker: "/var/lib/mesh/mongodb/broker" },
|
||||||
|
// The image drops to its own user before it reads the password file (ADR 0086; as the catalogue's).
|
||||||
|
"secrets-owner": "999:999",
|
||||||
resources: [
|
resources: [
|
||||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/mongodb", mode: "0700" },
|
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/mongodb", mode: "0700" },
|
||||||
{ id: "state", type: "directory", path: "/var/lib/mongodb", mode: "0700" },
|
{ id: "state", type: "directory", path: "/var/lib/mongodb", mode: "0700" },
|
||||||
{ id: "grants", type: "directory", path: "/var/lib/mongodb/grants", mode: "0700" },
|
{ id: "grants", type: "directory", path: "/var/lib/mongodb/grants", mode: "0700" },
|
||||||
{ id: "root-env", type: "file", path: "/var/lib/mongodb/root.env", mode: "0600", content: "MONGO_INITDB_ROOT_PASSWORD=${secret:root}\n" },
|
|
||||||
{ id: "data", type: "directory", path: "/services/mongodb/db-data", mode: "0700" },
|
{ id: "data", type: "directory", path: "/services/mongodb/db-data", mode: "0700" },
|
||||||
{ id: "net", type: "network", name: "mongodb" },
|
{ id: "net", type: "network", name: "mongodb" },
|
||||||
{
|
{
|
||||||
id: "server", type: "container", name: "mongo", image: pinned("mongo"), network: "mongodb",
|
id: "server", type: "container", name: "mongo", image: pinned("mongo"), network: "mongodb",
|
||||||
env: { MONGO_INITDB_ROOT_USERNAME: "root" },
|
// The root password reaches mongo as a file (novox/hq ADR 0086), the shape the catalogue's manifest has.
|
||||||
"env-file": ["/var/lib/mongodb/root.env"],
|
env: { MONGO_INITDB_ROOT_USERNAME: "root", MONGO_INITDB_ROOT_PASSWORD_FILE: "/run/secrets/root" },
|
||||||
volumes: ["/services/mongodb/db-data:/data/db"],
|
volumes: ["/services/mongodb/db-data:/data/db", "/var/lib/mongodb/root.secret:/run/secrets/root:ro"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "runtime", type: "container", name: "mesh-mongodb", image: pinned("mesh-runtime-mongodb"),
|
id: "runtime", type: "container", name: "mesh-mongodb", image: pinned("mesh-runtime-mongodb"),
|
||||||
@@ -415,7 +415,9 @@ test("the mesh assigns mongodb, unifi, marrytts and postgres to one node in one
|
|||||||
assert.doesNotMatch(mongoRes.out, /authentication failed/i,
|
assert.doesNotMatch(mongoRes.out, /authentication failed/i,
|
||||||
`mongodb delivered a credential that does not authenticate:\n${mongoRes.out}\n---runtime log---\n${(await on(`docker logs mesh-mongodb 2>&1 | tail -30`)).out}`);
|
`mongodb delivered a credential that does not authenticate:\n${mongoRes.out}\n---runtime log---\n${(await on(`docker logs mesh-mongodb 2>&1 | tail -30`)).out}`);
|
||||||
assert.match(mongoRes.out, /MONGO_OK/,
|
assert.match(mongoRes.out, /MONGO_OK/,
|
||||||
`the consumer could not use its granted database as ${mongoAs}:\n${mongoRes.out}`);
|
`the consumer could not use its granted database as ${mongoAs}:\n${mongoRes.out}\n---containers---\n` +
|
||||||
|
`${(await on(`docker ps -a --format '{{.Names}} {{.Status}}'`)).out}\n---mongo log---\n` +
|
||||||
|
`${(await on(`docker logs mongo 2>&1 | tail -15`)).out}`);
|
||||||
|
|
||||||
// --- mongodb and unifi serve their tools over their scoped accounts -------------------------------
|
// --- mongodb and unifi serve their tools over their scoped accounts -------------------------------
|
||||||
let served = "";
|
let served = "";
|
||||||
|
|||||||
@@ -178,14 +178,13 @@ test("the mesh assigns postgres, redis, minio and plex to one node in one push,
|
|||||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/postgres", mode: "0700" },
|
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/postgres", mode: "0700" },
|
||||||
{ id: "state", type: "directory", path: "/var/lib/postgres", mode: "0700" },
|
{ id: "state", type: "directory", path: "/var/lib/postgres", mode: "0700" },
|
||||||
{ id: "grants", type: "directory", path: "/var/lib/postgres/grants", mode: "0700" },
|
{ id: "grants", type: "directory", path: "/var/lib/postgres/grants", mode: "0700" },
|
||||||
{ id: "superuser-env", type: "file", path: "/var/lib/postgres/superuser.env", mode: "0600", content: "POSTGRES_PASSWORD=${secret:superuser}\n" },
|
|
||||||
{ id: "data", type: "directory", path: "/services/postgres/db-data", mode: "0700" },
|
{ id: "data", type: "directory", path: "/services/postgres/db-data", mode: "0700" },
|
||||||
{ id: "net", type: "network", name: "postgres" },
|
{ id: "net", type: "network", name: "postgres" },
|
||||||
{
|
{
|
||||||
id: "server", type: "container", name: "postgres", image: pinned("postgres"), network: "postgres",
|
id: "server", type: "container", name: "postgres", image: pinned("postgres"), network: "postgres",
|
||||||
env: { POSTGRES_USER: "postgres", POSTGRES_DB: "postgres" },
|
// The superuser reaches postgres as a file (novox/hq ADR 0086), the shape the catalogue's manifest has.
|
||||||
"env-file": ["/var/lib/postgres/superuser.env"],
|
env: { POSTGRES_USER: "postgres", POSTGRES_DB: "postgres", POSTGRES_PASSWORD_FILE: "/run/secrets/superuser" },
|
||||||
volumes: ["/services/postgres/db-data:/var/lib/postgresql/data"],
|
volumes: ["/services/postgres/db-data:/var/lib/postgresql/data", "/var/lib/postgres/superuser.secret:/run/secrets/superuser:ro"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "runtime", type: "container", name: "mesh-postgres", image: pinned("mesh-runtime-postgres"),
|
id: "runtime", type: "container", name: "mesh-postgres", image: pinned("mesh-runtime-postgres"),
|
||||||
@@ -229,14 +228,16 @@ test("the mesh assigns postgres, redis, minio and plex to one node in one push,
|
|||||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/minio", mode: "0700" },
|
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/minio", mode: "0700" },
|
||||||
{ id: "state", type: "directory", path: "/var/lib/minio", mode: "0700" },
|
{ id: "state", type: "directory", path: "/var/lib/minio", mode: "0700" },
|
||||||
{ id: "grants", type: "directory", path: "/var/lib/minio/grants", mode: "0700" },
|
{ id: "grants", type: "directory", path: "/var/lib/minio/grants", mode: "0700" },
|
||||||
{ id: "root-env", type: "file", path: "/var/lib/minio/root.env", mode: "0600", content: "MINIO_ROOT_USER=meshroot\nMINIO_ROOT_PASSWORD=${secret:root}\n" },
|
// The root password reaches minio as a file (novox/hq ADR 0086), the shape the catalogue's manifest has.
|
||||||
|
{ id: "root-env", type: "file", path: "/var/lib/minio/root.env", mode: "0600", content: "MINIO_ROOT_USER=meshroot\n" },
|
||||||
{ id: "data", type: "directory", path: "/services/minio/data/data1-1", mode: "0700" },
|
{ id: "data", type: "directory", path: "/services/minio/data/data1-1", mode: "0700" },
|
||||||
{ id: "net", type: "network", name: "minio" },
|
{ id: "net", type: "network", name: "minio" },
|
||||||
{
|
{
|
||||||
id: "server", type: "container", name: "minio", image: pinned("minio/minio"), network: "minio",
|
id: "server", type: "container", name: "minio", image: pinned("minio/minio"), network: "minio",
|
||||||
args: ["server", "/data", "--console-address", ":9001"],
|
args: ["server", "/data", "--console-address", ":9001"],
|
||||||
"env-file": ["/var/lib/minio/root.env"],
|
"env-file": ["/var/lib/minio/root.env"],
|
||||||
volumes: ["/services/minio/data/data1-1:/data"],
|
env: { MINIO_ROOT_PASSWORD_FILE: "/run/secrets/root" },
|
||||||
|
volumes: ["/services/minio/data/data1-1:/data", "/var/lib/minio/root.secret:/run/secrets/root:ro"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "runtime", type: "container", name: "mesh-minio", image: pinned("mesh-runtime-minio"),
|
id: "runtime", type: "container", name: "mesh-minio", image: pinned("mesh-runtime-minio"),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
|||||||
import { raise } from "../../src/lifecycle/raise.ts";
|
import { raise } from "../../src/lifecycle/raise.ts";
|
||||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, deriveTheFilterOn } from "./harness.ts";
|
||||||
import type { HeldImage } from "../../src/pinning.ts";
|
import type { HeldImage } from "../../src/pinning.ts";
|
||||||
|
|
||||||
const capability = await labIsUsable();
|
const capability = await labIsUsable();
|
||||||
@@ -177,6 +177,10 @@ before(async () => {
|
|||||||
}, { timeout: 1_800_000 });
|
}, { timeout: 1_800_000 });
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
|
if (process.env["MESH_LAB_KEEP"]) {
|
||||||
|
console.log(`\nLEFT STANDING: ${instanceId} — not destroyed (MESH_LAB_KEEP).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (instanceId) await destroy(instanceId);
|
if (instanceId) await destroy(instanceId);
|
||||||
await destroyAll(`${SCENARIO}-`);
|
await destroyAll(`${SCENARIO}-`);
|
||||||
}, { timeout: 600_000 });
|
}, { timeout: 600_000 });
|
||||||
@@ -202,15 +206,14 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
|
|||||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/postgres", mode: "0700" },
|
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/postgres", mode: "0700" },
|
||||||
{ id: "state", type: "directory", path: "/var/lib/postgres", mode: "0700" },
|
{ id: "state", type: "directory", path: "/var/lib/postgres", mode: "0700" },
|
||||||
{ id: "grants", type: "directory", path: "/var/lib/postgres/grants", mode: "0700" },
|
{ id: "grants", type: "directory", path: "/var/lib/postgres/grants", mode: "0700" },
|
||||||
{ id: "superuser-env", type: "file", path: "/var/lib/postgres/superuser.env", mode: "0600", content: "POSTGRES_PASSWORD=${secret:superuser}\n" },
|
|
||||||
{ id: "data", type: "directory", path: "/services/postgres/db-data", mode: "0700" },
|
{ id: "data", type: "directory", path: "/services/postgres/db-data", mode: "0700" },
|
||||||
{ id: "net", type: "network", name: "postgres" },
|
{ id: "net", type: "network", name: "postgres" },
|
||||||
{
|
{
|
||||||
id: "server", type: "container", name: "postgres", image: pinned("postgres"), network: "postgres",
|
id: "server", type: "container", name: "postgres", image: pinned("postgres"), network: "postgres",
|
||||||
env: { POSTGRES_USER: "postgres", POSTGRES_DB: "postgres" },
|
// The superuser reaches postgres as a file (novox/hq ADR 0086), the shape the catalogue's manifest has.
|
||||||
"env-file": ["/var/lib/postgres/superuser.env"],
|
env: { POSTGRES_USER: "postgres", POSTGRES_DB: "postgres", POSTGRES_PASSWORD_FILE: "/run/secrets/superuser" },
|
||||||
ports: ["5432"],
|
ports: ["5432"],
|
||||||
volumes: ["/services/postgres/db-data:/var/lib/postgresql/data"],
|
volumes: ["/services/postgres/db-data:/var/lib/postgresql/data", "/var/lib/postgres/superuser.secret:/run/secrets/superuser:ro"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "runtime", type: "container", name: "mesh-postgres", image: pinned("mesh-runtime-postgres"),
|
id: "runtime", type: "container", name: "mesh-postgres", image: pinned("mesh-runtime-postgres"),
|
||||||
@@ -249,10 +252,12 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
|
|||||||
resources: [
|
resources: [
|
||||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/model-usage", mode: "0700" },
|
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/model-usage", mode: "0700" },
|
||||||
{ id: "state", type: "directory", path: "/var/lib/model-usage", mode: "0700" },
|
{ id: "state", type: "directory", path: "/var/lib/model-usage", mode: "0700" },
|
||||||
|
// The connection string carries the password, so it reaches the runtime as a file the mesh
|
||||||
|
// templates (novox/hq ADR 0086), the shape the catalogue's manifest has.
|
||||||
{
|
{
|
||||||
id: "db-env", type: "file", path: "/var/lib/model-usage/db.env", mode: "0600",
|
id: "database-url", type: "file", path: "/var/lib/model-usage/database.url", mode: "0600",
|
||||||
content:
|
content:
|
||||||
"DATABASE_URL=postgresql://${bound:postgres-database:as}:${secret:postgres-database}@" +
|
"postgresql://${bound:postgres-database:as}:${secret:postgres-database}@" +
|
||||||
"${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n",
|
"${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -261,9 +266,9 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
|
|||||||
volumes: [
|
volumes: [
|
||||||
"/var/lib/mesh/model-usage/broker:/run/secrets/broker:ro",
|
"/var/lib/mesh/model-usage/broker:/run/secrets/broker:ro",
|
||||||
"/var/lib/model-usage:/run/state",
|
"/var/lib/model-usage:/run/state",
|
||||||
|
"/var/lib/model-usage/database.url:/run/secrets/database-url:ro",
|
||||||
],
|
],
|
||||||
env: { MESH_BROKER_FILE: "/run/secrets/broker" },
|
env: { MESH_BROKER_FILE: "/run/secrets/broker", DATABASE_URL_FILE: "/run/secrets/database-url" },
|
||||||
"env-file": ["/var/lib/model-usage/db.env"],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -281,6 +286,9 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
|
|||||||
await mesh(`overlay place ${NODE} --site lab`);
|
await mesh(`overlay place ${NODE} --site lab`);
|
||||||
await mesh("assign anchor networking");
|
await mesh("assign anchor networking");
|
||||||
await mesh(`assign ${NODE} networking`);
|
await mesh(`assign ${NODE} networking`);
|
||||||
|
// The anchor's derived filter, admitting the hub's port — what genesis does on the control-node,
|
||||||
|
// and what a bed raised from the bundle must do itself (ADR 0088; see the harness).
|
||||||
|
await deriveTheFilterOn({ machine: "anchor", node: "anchor", hubPort: 51820, must, mesh, on });
|
||||||
|
|
||||||
await addIssueAssign("postgres", postgresManifest);
|
await addIssueAssign("postgres", postgresManifest);
|
||||||
await addIssueAssign("model-usage", modelUsageManifest);
|
await addIssueAssign("model-usage", modelUsageManifest);
|
||||||
@@ -304,6 +312,19 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
|
|||||||
assert.equal(bound.provision, "postgres-database", `model-usage was bound the wrong provision: ${bound.provision}`);
|
assert.equal(bound.provision, "postgres-database", `model-usage was bound the wrong provision: ${bound.provision}`);
|
||||||
const pw = (await must(NODE, `cat /var/lib/model-usage/database.secret`)).trim();
|
const pw = (await must(NODE, `cat /var/lib/model-usage/database.secret`)).trim();
|
||||||
assert.ok(bound.as && pw, `model-usage's login or password was empty (as=${bound.as})`);
|
assert.ok(bound.as && pw, `model-usage's login or password was empty (as=${bound.as})`);
|
||||||
|
// What the machine can say when the login below fails — asked now, so the failure carries it.
|
||||||
|
const account = async () => [
|
||||||
|
"--- provisioner (mesh-postgres):", (await on(NODE, `docker logs --tail 25 mesh-postgres 2>&1`)).out,
|
||||||
|
"--- runtime (mesh-model-usage):", (await on(NODE, `docker logs --tail 25 mesh-model-usage 2>&1`)).out,
|
||||||
|
"--- grants:", (await on(NODE, `ls -la /var/lib/postgres/grants/; cat /var/lib/postgres/grants/mesh.json 2>&1 | head -30`)).out,
|
||||||
|
"--- roles:", (await on(NODE, `docker exec postgres psql -U postgres -tAc "select rolname from pg_roles where rolname like 'mesh%'" 2>&1`)).out,
|
||||||
|
"--- host log:", (await on(NODE, `tail -40 /var/log/mesh-host.log 2>&1`)).out,
|
||||||
|
"--- containers:", (await on(NODE, `docker ps -a --format '{{.Names}} {{.Status}}'`)).out,
|
||||||
|
"--- postgres server:", (await on(NODE, `docker logs --tail 15 postgres 2>&1; ls -la /var/lib/postgres/`)).out,
|
||||||
|
"--- laptop filter:", (await on(NODE, `nft list ruleset 2>&1 | head -60`)).out,
|
||||||
|
"--- laptop → broker from a container:", (await on(NODE, `docker run --rm --network postgres alpine sh -c 'nc -zvw3 192.0.2.10 5671' 2>&1`)).out,
|
||||||
|
"--- anchor filter counters:", (await on("anchor", `nft -a list table inet mesh 2>&1 | head -60`)).out,
|
||||||
|
].join("\n");
|
||||||
const conn = `postgresql://${bound.as}:${encodeURIComponent(pw)}@postgres:5432/${bound.as}?sslmode=disable`;
|
const conn = `postgresql://${bound.as}:${encodeURIComponent(pw)}@postgres:5432/${bound.as}?sslmode=disable`;
|
||||||
|
|
||||||
async function usageQuery(sql: string): Promise<{ out: string; ok: boolean }> {
|
async function usageQuery(sql: string): Promise<{ out: string; ok: boolean }> {
|
||||||
@@ -328,7 +349,7 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
|
|||||||
// for it before emitting, so the consumer's upsert has a table to write (a reading that arrives
|
// for it before emitting, so the consumer's upsert has a table to write (a reading that arrives
|
||||||
// before the table would be logged and lost).
|
// before the table would be logged and lost).
|
||||||
const tableReady = await waitFor("select to_regclass('usage') is not null", /^t$/m);
|
const tableReady = await waitFor("select to_regclass('usage') is not null", /^t$/m);
|
||||||
assert.match(tableReady, /^t$/m, `the usage table was never created (the consumer did not migrate):\n${tableReady}`);
|
assert.match(tableReady, /^t$/m, `the usage table was never created (the consumer did not migrate):\n${tableReady}\n${await account()}`);
|
||||||
|
|
||||||
// Inject a usage event into the mesh. model-usage is a PURE CONSUMER, so its own broker account has
|
// Inject a usage event into the mesh. model-usage is a PURE CONSUMER, so its own broker account has
|
||||||
// no publish right (mesh-controller grants write to mesh.events only to a module that declares `emits`).
|
// no publish right (mesh-controller grants write to mesh.events only to a module that declares `emits`).
|
||||||
|
|||||||
@@ -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(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");
|
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");
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
import { isReachable, pools, supportedDrivers } from "../../src/incus/client.ts";
|
import { isReachable, pools, supportedDrivers } from "../../src/incus/client.ts";
|
||||||
import { destroy, list } from "../../src/lifecycle/operate.ts";
|
import { destroy, list } from "../../src/lifecycle/operate.ts";
|
||||||
import { diagramFromLive } from "../../src/diagram/from-live.ts";
|
import { diagramFromLive } from "../../src/diagram/from-live.ts";
|
||||||
@@ -77,7 +78,7 @@ const UPSTREAM = new Map<string, string>([
|
|||||||
["lscr.io/linuxserver/radarr", "lscr.io/linuxserver/radarr@sha256:119aaa4a4f7349bcd2a136c5373a0d7925b5479915c7dfe0c0ad352db2a6d438"],
|
["lscr.io/linuxserver/radarr", "lscr.io/linuxserver/radarr@sha256:119aaa4a4f7349bcd2a136c5373a0d7925b5479915c7dfe0c0ad352db2a6d438"],
|
||||||
["lscr.io/linuxserver/sonarr", "lscr.io/linuxserver/sonarr@sha256:c19aa4ecdf03d73e1d5c901da33744cb7eb4d921f89bafed1ca264601d7fa224"],
|
["lscr.io/linuxserver/sonarr", "lscr.io/linuxserver/sonarr@sha256:c19aa4ecdf03d73e1d5c901da33744cb7eb4d921f89bafed1ca264601d7fa224"],
|
||||||
["lscr.io/linuxserver/unifi-controller", "lscr.io/linuxserver/unifi-controller@sha256:fcd5d8b13a77a588c79c1b49e5fc9ad08115aa3bb1a3576c589c64908a68845f"],
|
["lscr.io/linuxserver/unifi-controller", "lscr.io/linuxserver/unifi-controller@sha256:fcd5d8b13a77a588c79c1b49e5fc9ad08115aa3bb1a3576c589c64908a68845f"],
|
||||||
["minio/minio", "minio/minio@sha256:aefec8a86702aff0b0dcfdd9284bd7ab7c5631cbf9be63275799e6edcb30dfa2"],
|
["minio/minio", "quay.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e"], // docker.io denies anonymous pulls; the catalogue pins quay.io (mesh-catalog #29)
|
||||||
["mongo", "mongo@sha256:e3fa459b4f4b72f3257c67a23c145e250b8b5700f033860392c68539b998bbe3"],
|
["mongo", "mongo@sha256:e3fa459b4f4b72f3257c67a23c145e250b8b5700f033860392c68539b998bbe3"],
|
||||||
["ollama/ollama", "ollama/ollama@sha256:32931b46719f673c05fdbaa81ccb26da18ea4a1c57590a754874ab28ba269eb2"],
|
["ollama/ollama", "ollama/ollama@sha256:32931b46719f673c05fdbaa81ccb26da18ea4a1c57590a754874ab28ba269eb2"],
|
||||||
["postgres", "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee"],
|
["postgres", "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee"],
|
||||||
@@ -230,3 +231,57 @@ export async function assertUniversalInvariants(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the packet filter, where a bed raises the foundation without genesis ------------------------
|
||||||
|
|
||||||
|
/** The packet filter genesis installs on the control-node, by the name the catalogue gives it. */
|
||||||
|
export const FILTER_MODULE = "nftables";
|
||||||
|
|
||||||
|
/** The catalogue's manifest for a module, under either spelling of MESH_LAB_CATALOG: the modules
|
||||||
|
* directory, or the checkout that holds it. */
|
||||||
|
export function catalogueManifest(module: string): string {
|
||||||
|
const dir = process.env["MESH_LAB_CATALOG"] ?? "";
|
||||||
|
for (const candidate of [resolve(dir, module, "module.json"), resolve(dir, "modules", module, "module.json")]) {
|
||||||
|
if (existsSync(candidate)) return candidate;
|
||||||
|
}
|
||||||
|
throw new Error(`no manifest for ${module} under MESH_LAB_CATALOG=${dir || "(unset)"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(s: string): string {
|
||||||
|
return `'${s.replaceAll("'", `'\\''`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The foundation is raised behind a base ruleset that admits ssh, the bus and the registry and
|
||||||
|
* nothing else, "until the mesh derives one" (novox/hq ADR 0088) — and the mesh derives one only
|
||||||
|
* where the packet-filter module is assigned, which genesis does on the control-node. A bed that
|
||||||
|
* raises the foundation from the bundle skips genesis, so it must do the same before it relies on
|
||||||
|
* an overlay hub there: the hub's port is derived from its endpoint, and until the derived ruleset
|
||||||
|
* lands no joined node's tunnel forms, and every module that dials the anchor by its overlay name
|
||||||
|
* times out fetching the broker's certificate — the failure this helper was written after.
|
||||||
|
*
|
||||||
|
* Registered, assigned, pushed, and then WAITED FOR: what the machine loaded must admit the hub's
|
||||||
|
* port, which the base ruleset cannot. Call it after the hub is placed, so there is a port to derive.
|
||||||
|
*/
|
||||||
|
export async function deriveTheFilterOn(o: {
|
||||||
|
machine: string; node: string; hubPort: number;
|
||||||
|
must: (machine: string, command: string, timeoutMs?: number) => Promise<string>;
|
||||||
|
mesh: (command: string, timeoutMs?: number) => Promise<string>;
|
||||||
|
on: (machine: string, command: string, timeoutMs?: number) => Promise<{ out: string; ok: boolean }>;
|
||||||
|
}): Promise<string> {
|
||||||
|
const manifest = readFileSync(catalogueManifest(FILTER_MODULE), "utf8");
|
||||||
|
await o.must(o.machine,
|
||||||
|
`printf %s ${shellQuote(manifest)} > /tmp/${FILTER_MODULE}.json && docker cp /tmp/${FILTER_MODULE}.json mesh-controller:/${FILTER_MODULE}.json`);
|
||||||
|
await o.mesh(`module add /${FILTER_MODULE}.json`);
|
||||||
|
await o.mesh(`assign ${o.node} ${FILTER_MODULE}`);
|
||||||
|
await o.mesh(`push ${o.node}`, 600_000);
|
||||||
|
const admits = new RegExp(`udp dport ${o.hubPort} accept`);
|
||||||
|
const deadline = Date.now() + 180_000;
|
||||||
|
let ruleset = "";
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
ruleset = (await o.on(o.machine, `nft list table inet mesh 2>&1`)).out;
|
||||||
|
if (admits.test(ruleset)) return ruleset;
|
||||||
|
await new Promise((r) => setTimeout(r, 5_000));
|
||||||
|
}
|
||||||
|
assert.fail(`${FILTER_MODULE} is assigned to ${o.node} and the ruleset it loaded does not admit the hub's udp/${o.hubPort}:\n${ruleset}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
|||||||
import { raise } from "../../src/lifecycle/raise.ts";
|
import { raise } from "../../src/lifecycle/raise.ts";
|
||||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, deriveTheFilterOn } from "./harness.ts";
|
||||||
import type { HeldImage } from "../../src/pinning.ts";
|
import type { HeldImage } from "../../src/pinning.ts";
|
||||||
|
|
||||||
const capability = await labIsUsable();
|
const capability = await labIsUsable();
|
||||||
@@ -301,6 +301,9 @@ test("the lavinmq provider and its consumer ride laptop while the foundation bro
|
|||||||
await mesh(`overlay place ${NODE} --site lab`);
|
await mesh(`overlay place ${NODE} --site lab`);
|
||||||
await mesh("assign anchor networking");
|
await mesh("assign anchor networking");
|
||||||
await mesh(`assign ${NODE} networking`);
|
await mesh(`assign ${NODE} networking`);
|
||||||
|
// The anchor's derived filter, admitting the hub's port — what genesis does on the control-node,
|
||||||
|
// and what a bed raised from the bundle must do itself (ADR 0088; see the harness).
|
||||||
|
await deriveTheFilterOn({ machine: "anchor", node: "anchor", hubPort: 51820, must, mesh, on });
|
||||||
|
|
||||||
await addIssueAssign("lavinmq", lavinmqManifest);
|
await addIssueAssign("lavinmq", lavinmqManifest);
|
||||||
await addIssueAssign("amqp-ping", amqpPingManifest);
|
await addIssueAssign("amqp-ping", amqpPingManifest);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { test, before, after } from "node:test";
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
|
import net from "node:net";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { loadScenario } from "../../src/declaration/parse.ts";
|
import { loadScenario } from "../../src/declaration/parse.ts";
|
||||||
import { raise } from "../../src/lifecycle/raise.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. */
|
/** Until the anchor reports the last declaration genesis pushed as applied and current. */
|
||||||
async function settledAfterGenesis(withinMs = 300_000): Promise<void> {
|
async function settledAfterGenesis(withinMs = 300_000): Promise<void> {
|
||||||
const deadline = Date.now() + withinMs;
|
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.
|
// nothing held: no image is pre-resolved, because none is here to resolve to.
|
||||||
await step("R1", GENESIS, null, async () => {
|
await step("R1", GENESIS, null, async () => {
|
||||||
try {
|
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({
|
raised = await genesis({
|
||||||
instanceId,
|
instanceId,
|
||||||
node: CONTROL,
|
node: CONTROL,
|
||||||
@@ -538,6 +569,15 @@ before(async () => {
|
|||||||
...(binary ? { hostBinary: binary } : {}),
|
...(binary ? { hostBinary: binary } : {}),
|
||||||
log: (m) => console.log(m),
|
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) {
|
} catch (err) {
|
||||||
throw new Error(`the installer never ran: ${(err as Error).message}`);
|
throw new Error(`the installer never ran: ${(err as Error).message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
|||||||
import { raise } from "../../src/lifecycle/raise.ts";
|
import { raise } from "../../src/lifecycle/raise.ts";
|
||||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, deriveTheFilterOn } from "./harness.ts";
|
||||||
import type { HeldImage } from "../../src/pinning.ts";
|
import type { HeldImage } from "../../src/pinning.ts";
|
||||||
|
|
||||||
const capability = await labIsUsable();
|
const capability = await labIsUsable();
|
||||||
@@ -284,6 +284,9 @@ test("the whole ace service set resolves, installs and converges on one node in
|
|||||||
await mesh(`overlay place ${NODE} --site lab`);
|
await mesh(`overlay place ${NODE} --site lab`);
|
||||||
await mesh("assign anchor networking");
|
await mesh("assign anchor networking");
|
||||||
await mesh(`assign ${NODE} networking`);
|
await mesh(`assign ${NODE} networking`);
|
||||||
|
// The anchor's derived filter, admitting the hub's port — what genesis does on the control-node,
|
||||||
|
// and what a bed raised from the bundle must do itself (ADR 0088; see the harness).
|
||||||
|
await deriveTheFilterOn({ machine: "anchor", node: "anchor", hubPort: 51820, must, mesh, on });
|
||||||
|
|
||||||
// Add / issue / assign, resiliently: a module the node cannot host is recorded and skipped so one
|
// Add / issue / assign, resiliently: a module the node cannot host is recorded and skipped so one
|
||||||
// bad assignment cannot poison the whole-node push.
|
// bad assignment cannot poison the whole-node push.
|
||||||
|
|||||||
Reference in New Issue
Block a user