Beds read the catalogue: a shared loader, eight beds converted, the rest declared
catalogueModule() in the harness reads a module's manifest from the catalogue and rewrites only what the lab must: the build section goes, each artifact becomes the image the machine holds, images are pinned, and a bed may declare a host-port remap or a lab-local address. confluence, gitlab, openai-consumer, audit-logger, ollama, local-model-consumer, model-usage, mosquitto, anthropic-manager and anthropic-consumer now install the catalogue's manifest. A unit test refuses any inline copy naming a catalogue module unless the bed is declared with its reason; the declared list is the debt (novox/hq 04-ISSUES/073).
This commit is contained in:
@@ -36,7 +36,7 @@ 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, foundationBundle, onTheMachine } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, catalogueIsPresent } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
@@ -49,7 +49,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle)
|
||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "catalogue-mqtt";
|
||||
const MACHINE = "anchor";
|
||||
@@ -158,101 +158,11 @@ test("the mesh assigns mosquitto: a run-once step seeds dynsec before the broker
|
||||
skip, timeout: 1_500_000,
|
||||
}, async () => {
|
||||
// mosquitto's dynsec config: the plugin refuses to start unless dynamic-security.json holds an
|
||||
// admin client, so the store MUST be seeded first. The `run-once` bootstrap container is declared
|
||||
// BEFORE `server` (the broker) and reuses the module's runtime image; the host runs it to
|
||||
// completion, then starts the broker. The runtime `server`/`runtime` shape mirrors the committed
|
||||
// manifest, with images pinned to what this scenario serves by digest.
|
||||
const mosquittoConf =
|
||||
"persistence true\n" +
|
||||
"persistence_location /mosquitto/data\n\n" +
|
||||
"log_dest stdout\n" +
|
||||
"log_type warning\n" +
|
||||
"log_type error\n" +
|
||||
"log_type notice\n\n" +
|
||||
"# Every client authenticates; identities and their per-topic ACLs are managed\n" +
|
||||
"# at runtime by the dynamic security plugin, whose store the plugin itself owns.\n" +
|
||||
"allow_anonymous false\n" +
|
||||
"plugin /usr/lib/mosquitto_dynamic_security.so\n" +
|
||||
"plugin_opt_config_file /mosquitto/data/dynamic-security.json\n\n" +
|
||||
"# MQTT listener\n" +
|
||||
"listener 1883\n\n" +
|
||||
"# MQTT-over-WebSockets listener\n" +
|
||||
"listener 8081\n" +
|
||||
"protocol websockets\n";
|
||||
|
||||
const manifest = JSON.stringify({
|
||||
module: "mosquitto",
|
||||
version: "1",
|
||||
provides: [{ name: "mqtt-topic", scope: "mesh" }],
|
||||
serves: { "mqtt-topic": {} },
|
||||
emits: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"],
|
||||
// The events entrypoint subscribes to its own lifecycle events (an audit log), so it consumes
|
||||
// them too — declared, or the foundation never makes the queue the runtime binds (ADR 0046).
|
||||
consumes: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"],
|
||||
receives: { "mqtt-topic": "/var/lib/mosquitto-module/grants/mesh.json" },
|
||||
grants: { "mqtt-topic": "/var/lib/mosquitto-module/grants" },
|
||||
"own-secrets": {
|
||||
admin: "/var/lib/mosquitto-module/admin.secret",
|
||||
broker: "/var/lib/mesh/mosquitto/broker",
|
||||
},
|
||||
resources: [
|
||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/mosquitto", mode: "0700" },
|
||||
{ id: "state", type: "directory", path: "/var/lib/mosquitto-module", mode: "0700" },
|
||||
{ id: "grants-dir", type: "directory", path: "/var/lib/mosquitto-module/grants", mode: "0700" },
|
||||
// The broker runs as uid 1883, so the shared data directory it seeds into and persists to is
|
||||
// its own.
|
||||
{ id: "data", type: "directory", path: "/services/mosquitto/data", mode: "0700", owner: "1883:1883" },
|
||||
{
|
||||
id: "server-conf", type: "file", path: "/var/lib/mosquitto-module/mosquitto.conf",
|
||||
mode: "0600", owner: "1883:1883", content: mosquittoConf,
|
||||
},
|
||||
{ id: "net", type: "network", name: "mosquitto" },
|
||||
// THE run-once step: seed dynsec offline, once, before the broker. It reuses the runtime image
|
||||
// (`mesh-tools run <bootstrap>` imports mosquitto's bootstrap entrypoint, which writes the
|
||||
// admin client into dynamic-security.json and chowns it to the broker's uid, then exits). It is
|
||||
// declared BEFORE `server`; the host runs it to completion and requires exit 0 before starting
|
||||
// the broker.
|
||||
{
|
||||
id: "bootstrap", type: "container", name: "mosquitto-bootstrap",
|
||||
image: pinned("mesh-runtime-mosquitto"), "run-once": true,
|
||||
volumes: [
|
||||
"/services/mosquitto/data:/mosquitto/data",
|
||||
"/var/lib/mosquitto-module/admin.secret:/run/secrets/admin:ro",
|
||||
],
|
||||
env: {
|
||||
MESH_PROVISION_MQTT: "mosquitto:1883",
|
||||
MESH_PROVISION_ADMIN_USER: "mesh-admin",
|
||||
MESH_PROVISION_PASSWORD_FILE: "/run/secrets/admin",
|
||||
MESH_DYNSEC_FILE: "/mosquitto/data/dynamic-security.json",
|
||||
},
|
||||
args: ["run", "/app/modules/mosquitto/dist/bootstrap/index.js"],
|
||||
},
|
||||
{
|
||||
id: "server", type: "container", name: "mosquitto", image: pinned("eclipse-mosquitto"),
|
||||
network: "mosquitto", ports: ["1883", "8081"],
|
||||
volumes: [
|
||||
"/services/mosquitto/data:/mosquitto/data",
|
||||
"/var/lib/mosquitto-module/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "runtime", type: "container", name: "mesh-mosquitto",
|
||||
image: pinned("mesh-runtime-mosquitto"), network: "mosquitto",
|
||||
volumes: [
|
||||
"/var/lib/mesh/mosquitto/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mosquitto-module/grants:/var/lib/mosquitto-module/grants:ro",
|
||||
"/var/lib/mosquitto-module/admin.secret:/run/secrets/admin:ro",
|
||||
],
|
||||
env: {
|
||||
MESH_BROKER_FILE: "/run/secrets/broker",
|
||||
MESH_RECEIVES: "/var/lib/mosquitto-module/grants/mesh.json",
|
||||
MESH_PROVISION_MQTT: "mosquitto:1883",
|
||||
MESH_PROVISION_ADMIN_USER: "mesh-admin",
|
||||
MESH_PROVISION_PASSWORD_FILE: "/run/secrets/admin",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// admin client, so the store MUST be seeded first. The catalogue's manifest declares a `run-once`
|
||||
// bootstrap container BEFORE `server` (the broker), reusing the module's runtime image; the host
|
||||
// runs it to completion, then starts the broker. The manifest is the catalogue's own, its runtime
|
||||
// artifact the image this scenario stocked (novox/hq 04-ISSUES/073).
|
||||
const manifest = catalogueModule("mosquitto", held);
|
||||
|
||||
await must(`printf %s ${quote(manifest)} > /tmp/mosquitto.json && docker cp /tmp/mosquitto.json mesh-controller:/mosquitto.json`);
|
||||
await mesh("module add /mosquitto.json");
|
||||
|
||||
Reference in New Issue
Block a user