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:
@@ -237,14 +237,104 @@ export async function assertUniversalInvariants(
|
||||
/** 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;
|
||||
// --- the catalogue: a bed installs a module by reading its manifest, never by carrying a copy ----
|
||||
|
||||
/**
|
||||
* The catalogue's `modules/` directory: MESH_LAB_CATALOG under either spelling (the checkout, or
|
||||
* its modules directory), else the checkout beside this one, the way the main layout has it.
|
||||
*
|
||||
* Beds used to build the manifests they install inline, as literals copied from the catalogue when
|
||||
* each bed was written. The copies did not move when the catalogue did, so a catalogue change was
|
||||
* proven nowhere — and a bed that installs a copy proves the copy (novox/hq 04-ISSUES/073). A bed
|
||||
* reads the catalogue, or it does not install a catalogue module; `beds-read-the-catalogue.test.ts`
|
||||
* refuses an inline copy that names one.
|
||||
*/
|
||||
export function catalogueDir(): string {
|
||||
const named = process.env["MESH_LAB_CATALOG"];
|
||||
const candidates = named
|
||||
? [resolve(named, "modules"), resolve(named)]
|
||||
: [resolve(process.cwd(), "..", "mesh-catalog", "modules")];
|
||||
for (const dir of candidates) {
|
||||
if (existsSync(resolve(dir, "mesh-controller", "module.json"))) return dir;
|
||||
}
|
||||
throw new Error(`no manifest for ${module} under MESH_LAB_CATALOG=${dir || "(unset)"}`);
|
||||
throw new Error(
|
||||
`no catalogue: MESH_LAB_CATALOG=${named ?? "(unset)"} and nothing at ${candidates.join(", ")}`);
|
||||
}
|
||||
|
||||
/** Whether a catalogue is where a bed will look — for a skip guard, which says so instead of failing. */
|
||||
export function catalogueIsPresent(): string | false {
|
||||
try { catalogueDir(); return false; } catch (err) { return (err as Error).message; }
|
||||
}
|
||||
|
||||
/** The catalogue's manifest for a module, as a path. */
|
||||
export function catalogueManifest(module: string): string {
|
||||
const path = resolve(catalogueDir(), module, "module.json");
|
||||
if (!existsSync(path)) throw new Error(`no manifest for ${module} at ${path}`);
|
||||
return path;
|
||||
}
|
||||
|
||||
/** What the lab may rewrite in a catalogue manifest, and nothing else. */
|
||||
export interface ForTheLab {
|
||||
/**
|
||||
* The image repository each build artifact was built as on this workstation, by artifact name.
|
||||
* A module's own runtime is `mesh-runtime-<module>` by default — what `scripts/build-module-runtime.sh`
|
||||
* tags and what the scenarios stock; a bed names it only where the scenario stocks another name.
|
||||
* An artifact this does not name is refused: the bed must say what stands in for the builder.
|
||||
*/
|
||||
artifacts?: Record<string, string>;
|
||||
/**
|
||||
* Host-port remaps by container id, where one machine carries modules whose published ports
|
||||
* collide — `{ server: { "8080": "8090:8080" } }`. The container side never changes.
|
||||
*/
|
||||
ports?: Record<string, Record<string, string>>;
|
||||
/**
|
||||
* Environment a container gets in the lab that it does not get in the mesh — an address the bed
|
||||
* stands up in place of a real upstream, and nothing else. Merged over the manifest's own.
|
||||
*/
|
||||
env?: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A catalogue manifest as a machine in the lab can run it: the mesh's build section gone (the lab
|
||||
* stocks images rather than building), each artifact replaced by the image the machine holds for it,
|
||||
* every image pinned to what the machine holds or the upstream digest the catalogue pins, and the
|
||||
* declared lab rewrites applied. Everything else is the catalogue's, verbatim — which is the point.
|
||||
*/
|
||||
export function catalogueModule(module: string, held: HeldImage[], lab: ForTheLab = {}): string {
|
||||
const m = JSON.parse(readFileSync(catalogueManifest(module), "utf8")) as {
|
||||
resources?: { id: string; type: string; image?: string; artifact?: string; ports?: string[]; env?: Record<string, string> }[];
|
||||
build?: unknown;
|
||||
};
|
||||
const artifacts: Record<string, string> = { runtime: `mesh-runtime-${module}`, ...(lab.artifacts ?? {}) };
|
||||
for (const r of m.resources ?? []) {
|
||||
if (r.type !== "container") continue;
|
||||
if (typeof r.artifact === "string") {
|
||||
const repository = artifacts[r.artifact];
|
||||
assert.ok(repository,
|
||||
`${module}'s container '${r.id}' names the "${r.artifact}" artifact, which the mesh would ` +
|
||||
`build. The lab does not build: the bed must say which stocked image stands in for it ` +
|
||||
`(artifacts: { ${r.artifact}: "<repository>" }).`);
|
||||
const reference = referenceFor(held, repository);
|
||||
assert.ok(reference,
|
||||
`${module}'s "${r.artifact}" artifact is ${repository} and this scenario stocked no such ` +
|
||||
`image. Add it to the scenario's images: and build it (scripts/build-module-runtime.sh ${module}).`);
|
||||
r.image = reference;
|
||||
delete r.artifact;
|
||||
} else if (typeof r.image === "string") {
|
||||
r.image = onTheMachine(r.image, held);
|
||||
}
|
||||
const remap = lab.ports?.[r.id];
|
||||
if (remap && Array.isArray(r.ports)) r.ports = r.ports.map((p) => remap[p] ?? p);
|
||||
const env = lab.env?.[r.id];
|
||||
if (env) r.env = { ...(r.env ?? {}), ...env };
|
||||
}
|
||||
delete m.build;
|
||||
return JSON.stringify(m);
|
||||
}
|
||||
|
||||
/** Whether a manifest's runtime dials the broker — the module then needs a scoped broker account. */
|
||||
export function needsBrokerAccount(manifest: string): boolean {
|
||||
return manifest.includes("MESH_BROKER_FILE");
|
||||
}
|
||||
|
||||
function shellQuote(s: string): string {
|
||||
|
||||
Reference in New Issue
Block a user