The five beds that read the catalogue read it through the harness
adopted-store-cross-node, two-node-db and the three whole-mesh beds each carried a private loader; they drifted. The ace loader never resolved a runtime artifact, so a module the mesh builds travelled unresolved; whole-mesh-full still asked for 'registry' and 'firewall', which the catalogue names distribution and nftables, and swallowed the miss as NOT ASSIGNED. One loader now (novox/hq 04-ISSUES/073).
This commit is contained in:
@@ -58,21 +58,20 @@ import { test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { join, resolve } from "node:path";
|
||||
import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec, instanceNameOf, push } from "../../src/lifecycle/operate.ts";
|
||||
import {
|
||||
bootstrapBinaryPath, hostBinaryPath, placeBootstrap, BOOTSTRAP_PATH, HOST_PATH,
|
||||
} from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, catalogueDir, catalogueIsPresent, catalogueModule, needsBrokerAccount } from "./harness.ts";
|
||||
import { referenceFor, type HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
const binary = hostBinaryPath();
|
||||
const installer = bootstrapBinaryPath();
|
||||
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
||||
const modulesEnv = process.env["MESH_LAB_MODULES"] ?? "";
|
||||
// What the installer is told to build. It carries a builder rather than a finished control plane
|
||||
// (novox/hq ADR 0073), so genesis is given a repository and a commit — and a commit rather than a
|
||||
// branch, because what is cloned is the trust anchor for everything this mesh will ever run.
|
||||
@@ -93,7 +92,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_SOURCE is not set to the repository the control plane is built from"
|
||||
: !sourceRef
|
||||
? "MESH_LAB_SOURCE_REF is not set to the commit to build"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "whole-mesh-full";
|
||||
/** novox hosts the foundation and the control plane; it is where `mesh` commands run. */
|
||||
@@ -118,7 +117,7 @@ const CATALOGUE_ON_MACHINE = "/opt/mesh-catalog";
|
||||
* `internal/bootstrap` RegistryModule and ControlPlaneModule. If it ever opens a third, this list
|
||||
* is where the bed finds out, by the installer saying which manifest it could not read.
|
||||
*/
|
||||
const CATALOGUE_MODULES = ["registry", "mesh-controller", "builder"];
|
||||
const CATALOGUE_MODULES = ["distribution", "mesh-controller", "builder"];
|
||||
|
||||
/**
|
||||
* Where this mesh keeps its own images, as the anchor reaches it.
|
||||
@@ -152,9 +151,8 @@ const PUBLIC_DOMAIN: Record<string, string> = { novox: "novox.incus", ace: "zura
|
||||
const KEEP = !!process.env["MESH_LAB_KEEP"];
|
||||
const FIXED_ID = process.env["MESH_LAB_INSTANCE_ID"] ?? (KEEP ? "whole-mesh-full-live" : undefined);
|
||||
|
||||
const catalogDir = process.env["MESH_LAB_CATALOG"]
|
||||
?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "")
|
||||
?? resolve(process.cwd(), "..", "mesh-catalog", "modules");
|
||||
/** The catalogue's modules directory (harness). Absent, the bed skips — see `skip`. */
|
||||
const catalogDir = catalogueIsPresent() ? "" : catalogueDir();
|
||||
|
||||
const MEDIA_DIRS = [
|
||||
"/services/media/series", "/services/media/anime", "/services/media/movies",
|
||||
@@ -201,7 +199,7 @@ const NOVOX: Mod[] = [
|
||||
// what an operator's `module add` + `assign` would do on a mesh that already has it, and a
|
||||
// module the installer put there had better survive being asked for a second time. It also keeps
|
||||
// mesh-registry in the convergence report, where a reader expects to see it.
|
||||
{ name: "registry", containers: ["mesh-registry"] },
|
||||
{ name: "distribution", containers: ["mesh-registry"] },
|
||||
{
|
||||
name: "mailu",
|
||||
containers: [
|
||||
@@ -210,16 +208,16 @@ const NOVOX: Mod[] = [
|
||||
"mailu-front", "mesh-mailu",
|
||||
],
|
||||
},
|
||||
{ name: "firewall", containers: [], node: true },
|
||||
{ name: "nftables", containers: [], node: true },
|
||||
{ name: "fail2ban", containers: [], node: true },
|
||||
];
|
||||
const CORE_NOVOX = new Set([
|
||||
"postgres", "redis", "minio", "mongodb", "mssql", "lavinmq",
|
||||
"route-proxy", "keycloak", "gitea", "nextcloud", "invoicing", "photos", "novox.be",
|
||||
"portainer", "verdaccio", "registry",
|
||||
"portainer", "verdaccio", "distribution",
|
||||
]);
|
||||
const GAPS_NOVOX = new Set([
|
||||
"umami", "mailu", "firewall", "fail2ban", "only-office", "de-spiegel", "amqp-email-forwarder",
|
||||
"umami", "mailu", "nftables", "fail2ban", "only-office", "de-spiegel", "amqp-email-forwarder",
|
||||
// step-ca is reported, not gated: the internal-CA ISSUANCE path is still being fixed in
|
||||
// mesh-controller, and this bed is not the place to discover that a fix has not landed yet. What is
|
||||
// gated is the half that is decided and cheap — see the ADR 0066 section at the end.
|
||||
@@ -348,45 +346,16 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
|
||||
}
|
||||
|
||||
/** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */
|
||||
function pinned(reference: string): string {
|
||||
return onTheMachine(reference, held);
|
||||
}
|
||||
|
||||
function bundleFor(images: HeldImage[]): string {
|
||||
return foundationBundle(bundle, images);
|
||||
}
|
||||
|
||||
/** The catalogue's manifest as the lab runs it (harness): artifacts resolved to the images the
|
||||
* scenario stocked — the lab standing in for the builder — images pinned, host ports remapped. */
|
||||
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
||||
const path = resolve(catalogDir, name, "module.json");
|
||||
const m = JSON.parse(readFileSync(path, "utf8")) as {
|
||||
resources?: { type: string; image?: string; artifact?: string; ports?: string[] }[];
|
||||
};
|
||||
const remap = REMAP[name] ?? {};
|
||||
for (const r of m.resources ?? []) {
|
||||
if (r.type !== "container") continue;
|
||||
// **A container naming an artifact is a module the mesh builds, and this bed does not build.**
|
||||
// It pre-builds the same images on the workstation and stocks them, which is the lab standing
|
||||
// in for the builder — so it does here what the builder does: replace the artifact with the
|
||||
// reference the machine actually holds. Without this the unresolved field travels to the
|
||||
// machine, whose declaration language has no such field, and the whole declaration is refused.
|
||||
//
|
||||
// The repository is `mesh-runtime-<module>`, which is not a guess: it is what this repository's
|
||||
// own `scripts/build-module-runtime.sh <module>` produces and what the scenarios stock by name.
|
||||
if (typeof r.artifact === "string" && typeof r.image !== "string") {
|
||||
const reference = referenceFor(held, `mesh-runtime-${name}`);
|
||||
assert.ok(reference,
|
||||
`${name} declares the "${r.artifact}" artifact and this scenario stocked no ` +
|
||||
`mesh-runtime-${name}. The mesh would have to build it, and this bed does not build — ` +
|
||||
`add it to the machine's images: in the scenario, or build it with ` +
|
||||
`scripts/build-module-runtime.sh ${name}`);
|
||||
r.image = reference;
|
||||
delete r.artifact;
|
||||
}
|
||||
if (typeof r.image === "string") r.image = pinned(r.image);
|
||||
if (Array.isArray(r.ports)) r.ports = r.ports.map((p) => remap[p] ?? p);
|
||||
}
|
||||
const manifest = JSON.stringify(m);
|
||||
return { manifest, broker: manifest.includes("MESH_BROKER_FILE") };
|
||||
const manifest = catalogueModule(name, held, { ports: REMAP[name] });
|
||||
return { manifest, broker: needsBrokerAccount(manifest) };
|
||||
}
|
||||
|
||||
function tokenFrom(said: string): string {
|
||||
@@ -745,7 +714,6 @@ async function joinTheMesh(): Promise<void> {
|
||||
|
||||
before(async () => {
|
||||
if (skip) return;
|
||||
assert.ok(existsSync(catalogDir), `mesh-catalog modules not found at ${catalogDir}`);
|
||||
|
||||
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {
|
||||
onProgress: (m) => console.log(`raise: ${m}`),
|
||||
|
||||
Reference in New Issue
Block a user