lavinmq becomes a provider of a user-facing amqp interface: a consumer
that requires a message queue is given its OWN broker — a scoped vhost
and user on a lavinmq provider — not an account on the mesh's own
control-plane broker (ADR 0048). Vhost-per-login is the isolation model,
the exact analog of postgres's database-per-login: the provider names a
vhost after the consumer's login and a user with full rights on that
vhost and none elsewhere, so a login is a broker the consumer alone can
reach.
The provider drives lavinmq through its HTTP management API (client.ts,
the module's one impure seam), with a run-once bootstrap that computes
the RabbitMQ-compatible password hash lavinmq's config wants from the
plain admin secret the mesh mints — the value no ${secret:...}
placeholder can produce and the reason the bootstrap exists (ADR 0052).
serves.amqp carries the port so consumers reference ${bound:amqp:port}.
amqp-ping is a demo consumer: it contributes nothing (the vhost is the
login), reads its grant from an env-file the mesh fills, and uses
${bound:amqp:as} for BOTH its username and its vhost — the db-name
lesson applied to AMQP. It speaks AMQP 0-9-1 over a raw socket with no
npm dependency (the way redis speaks RESP) and round-trips one message.
It carries a slug so its identity fits the 20-char backend bound
(ADR 0049).
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
46 lines
2.7 KiB
TypeScript
46 lines
2.7 KiB
TypeScript
// lavinmq's run-once bootstrap — lavinmq's own code (novox/hq ADR 0039), run once before the broker
|
|
// first starts (ADR 0052). lavinmq's default admin is set at first boot from a config file's
|
|
// `default_password_hash`, and that value is a HASH of the mesh-minted admin password, not the
|
|
// password itself — a form the mesh's plain-secret delivery cannot produce and no `${secret:...}`
|
|
// placeholder can compute. So this step computes it: it reads the admin password the mesh minted and
|
|
// the host unsealed, hashes it the way lavinmq expects (see client.rabbitHash), and writes the config
|
|
// file the broker container reads with `--config`. The host runs it to completion and only then
|
|
// starts the broker the manifest places after it — so the broker's first boot finds a config with an
|
|
// admin it can authenticate, and the provisioner (which reaches the management API as that admin)
|
|
// can do its work.
|
|
//
|
|
// It runs in the module's own runtime image, under the module's own account, as `mesh-tools run`
|
|
// imports it — no broker connection, because writing a config file is an offline operation and there
|
|
// is no broker to reach yet.
|
|
//
|
|
// lavinmq consults `default_user`/`default_password_hash` only on a first boot with an empty data
|
|
// dir; a later boot uses the persisted user database and ignores them. So this seeds the admin once,
|
|
// and a rotation of the admin secret does not re-key an already-initialised broker — the same
|
|
// first-boot-only shape the RabbitMQ-compatible default user has always had.
|
|
|
|
import { writeFileSync } from "node:fs";
|
|
import { readFileSync } from "node:fs";
|
|
import { rabbitHash } from "../client.js";
|
|
|
|
const adminUser = process.env.MESH_PROVISION_ADMIN_USER ?? process.env.MESH_LAVINMQ_ADMIN_USER ?? "mesh-admin";
|
|
const passwordFile = process.env.MESH_PROVISION_PASSWORD_FILE ?? process.env.MESH_LAVINMQ_ADMIN_PASSWORD_FILE ?? "/run/secrets/default";
|
|
const configOut = process.env.MESH_LAVINMQ_CONFIG_OUT ?? "/var/lib/lavinmq-module/lavinmq.ini";
|
|
const dataDir = process.env.MESH_LAVINMQ_DATA_DIR ?? "/var/lib/lavinmq";
|
|
|
|
const password = readFileSync(passwordFile, "utf8").replace(/\n$/, "");
|
|
if (!password) {
|
|
throw new Error(`[lavinmq:bootstrap] admin password file ${passwordFile} is empty — cannot seed the admin`);
|
|
}
|
|
|
|
// The broker reads only what it needs to authenticate its admin on first boot; bind/ports come from
|
|
// the container's entrypoint (`-b 0.0.0.0`), so this file names the admin and nothing else about the
|
|
// network.
|
|
const ini =
|
|
"[main]\n" +
|
|
`data_dir = ${dataDir}\n` +
|
|
`default_user = ${adminUser}\n` +
|
|
`default_password_hash = ${rabbitHash(password)}\n`;
|
|
|
|
writeFileSync(configOut, ini, { mode: 0o600 });
|
|
console.log(`[lavinmq:bootstrap] wrote ${configOut} with admin '${adminUser}' (password hashed for lavinmq)`);
|