Beds read the catalogue: one loader, eight beds converted, the rest declared (hq issue 073) #41
@@ -21,29 +21,25 @@
|
|||||||
*/
|
*/
|
||||||
import { test, before, after } from "node:test";
|
import { test, before, after } from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { dirname, 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";
|
||||||
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, catalogueIsPresent, catalogueModule, needsBrokerAccount } 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();
|
||||||
const binary = hostBinaryPath();
|
const binary = hostBinaryPath();
|
||||||
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
||||||
const modulesEnv = process.env["MESH_LAB_MODULES"] ?? "";
|
|
||||||
const skip = !capability.usable ? `lab not usable: ${capability.why}`
|
const skip = !capability.usable ? `lab not usable: ${capability.why}`
|
||||||
: !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
: !binary || !existsSync(binary) ? "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"
|
: !bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a foundation bundle"
|
||||||
: false;
|
: catalogueIsPresent();
|
||||||
|
|
||||||
const SCENARIO = "adopted-store-cross-node";
|
const SCENARIO = "adopted-store-cross-node";
|
||||||
const NODE = "node2";
|
const NODE = "node2";
|
||||||
const catalogDir = process.env["MESH_LAB_CATALOG"]
|
|
||||||
?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "")
|
|
||||||
?? resolve(process.cwd(), "..", "mesh-catalog", "modules");
|
|
||||||
|
|
||||||
let instanceId = "";
|
let instanceId = "";
|
||||||
let held: HeldImage[] = [];
|
let held: HeldImage[] = [];
|
||||||
@@ -65,29 +61,12 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi
|
|||||||
async function mesh(command: string, timeoutMs?: number): Promise<string> {
|
async function mesh(command: string, timeoutMs?: number): Promise<string> {
|
||||||
return must("anchor", `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs);
|
return must("anchor", `docker exec mesh-controller /mesh-controller ${command}`, timeoutMs);
|
||||||
}
|
}
|
||||||
function pinned(reference: string): string { return onTheMachine(reference, held); }
|
|
||||||
function bundleFor(images: HeldImage[]): string { return foundationBundle(bundle, images); }
|
function bundleFor(images: HeldImage[]): string { return foundationBundle(bundle, images); }
|
||||||
|
|
||||||
/** Load a committed module.json with its container images rewritten to the scenario's pinned digests. */
|
/** The catalogue's manifest as the lab runs it (harness), and whether it needs a broker account. */
|
||||||
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
||||||
const path = resolve(catalogDir, name, "module.json");
|
const manifest = catalogueModule(name, held);
|
||||||
const m = JSON.parse(readFileSync(path, "utf8")) as {
|
return { manifest, broker: needsBrokerAccount(manifest) };
|
||||||
resources?: { type: string; image?: string; artifact?: string }[];
|
|
||||||
};
|
|
||||||
for (const r of m.resources ?? []) {
|
|
||||||
if (r.type !== "container") continue;
|
|
||||||
if (typeof r.image === "string") {
|
|
||||||
// A placeholder image (mesh-runtime-<m>@0…0) resolves to the stocked digest, as redis does.
|
|
||||||
r.image = pinned(r.image);
|
|
||||||
} else if (typeof r.artifact === "string") {
|
|
||||||
// The bundle-model bed does not build, so resolve a module's runtime ARTIFACT to its stocked
|
|
||||||
// image directly — postgres/lavinmq name their provisioner by artifact, not a placeholder.
|
|
||||||
r.image = pinned(`mesh-runtime-${name}@sha256:${"0".repeat(64)}`);
|
|
||||||
delete r.artifact;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const manifest = JSON.stringify(m);
|
|
||||||
return { manifest, broker: manifest.includes("MESH_BROKER_FILE") };
|
|
||||||
}
|
}
|
||||||
function tokenFrom(said: string): string {
|
function tokenFrom(said: string): string {
|
||||||
const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" "));
|
const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" "));
|
||||||
@@ -105,7 +84,6 @@ async function install(name: string, node: string): Promise<void> {
|
|||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
if (skip) return;
|
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}`) });
|
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), { onProgress: (m) => console.log(`raise: ${m}`) });
|
||||||
instanceId = raised.instanceId;
|
instanceId = raised.instanceId;
|
||||||
held = raised.images;
|
held = raised.images;
|
||||||
|
|||||||
@@ -37,13 +37,12 @@
|
|||||||
|
|
||||||
import { test, before, after } from "node:test";
|
import { test, before, after } from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { dirname, 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";
|
||||||
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, catalogueIsPresent, catalogueModule, needsBrokerAccount } 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();
|
||||||
@@ -56,16 +55,13 @@ const skip = !capability.usable
|
|||||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||||
: !bundle || !existsSync(bundle)
|
: !bundle || !existsSync(bundle)
|
||||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||||
: false;
|
: catalogueIsPresent();
|
||||||
|
|
||||||
const SCENARIO = "two-node-db";
|
const SCENARIO = "two-node-db";
|
||||||
/** The node that carries the whole DB-consumer chain. anchor carries only the foundation. */
|
/** The node that carries the whole DB-consumer chain. anchor carries only the foundation. */
|
||||||
const NODE = "laptop";
|
const NODE = "laptop";
|
||||||
|
|
||||||
const modulesEnv = process.env["MESH_LAB_MODULES"] ?? "";
|
|
||||||
const catalogDir = process.env["MESH_LAB_CATALOG"]
|
|
||||||
?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "")
|
|
||||||
?? resolve(process.cwd(), "..", "mesh-catalog", "modules");
|
|
||||||
|
|
||||||
let instanceId = "";
|
let instanceId = "";
|
||||||
/** The mesh's own images, as the machines hold them. */
|
/** The mesh's own images, as the machines hold them. */
|
||||||
@@ -359,22 +355,11 @@ test("consumers on a joined node get their databases from the one foundation sto
|
|||||||
await mesh(`assign ${NODE} ${name}`);
|
await mesh(`assign ${NODE} ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load a committed catalog module.json with its container images rewritten to this scenario's
|
// The catalogue's manifest as the lab runs it (harness), so the foundation store can be ADOPTED
|
||||||
// pinned digests, so the foundation store can be ADOPTED in place as the one postgres.
|
// in place as the one postgres.
|
||||||
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
||||||
const m = JSON.parse(readFileSync(resolve(catalogDir, name, "module.json"), "utf8")) as {
|
const manifest = catalogueModule(name, held);
|
||||||
resources?: { type: string; image?: string; artifact?: string }[];
|
return { manifest, broker: needsBrokerAccount(manifest) };
|
||||||
};
|
|
||||||
for (const r of m.resources ?? []) {
|
|
||||||
if (r.type !== "container") continue;
|
|
||||||
if (typeof r.image === "string") r.image = pinned(r.image);
|
|
||||||
else if (typeof r.artifact === "string") {
|
|
||||||
r.image = pinned(`mesh-runtime-${name}@sha256:${"0".repeat(64)}`);
|
|
||||||
delete r.artifact;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const manifest = JSON.stringify(m);
|
|
||||||
return { manifest, broker: manifest.includes("MESH_BROKER_FILE") };
|
|
||||||
}
|
}
|
||||||
async function installCatalog(name: string, node: string): Promise<void> {
|
async function installCatalog(name: string, node: string): Promise<void> {
|
||||||
const { manifest, broker } = loadManifest(name);
|
const { manifest, broker } = loadManifest(name);
|
||||||
|
|||||||
@@ -283,10 +283,11 @@ export interface ForTheLab {
|
|||||||
*/
|
*/
|
||||||
artifacts?: Record<string, string>;
|
artifacts?: Record<string, string>;
|
||||||
/**
|
/**
|
||||||
* Host-port remaps by container id, where one machine carries modules whose published ports
|
* Host-port remaps, where one machine carries modules whose published ports collide —
|
||||||
* collide — `{ server: { "8080": "8090:8080" } }`. The container side never changes.
|
* `{ "8080": "8090:8080" }`, applied to every container of the module. The container side never
|
||||||
|
* changes.
|
||||||
*/
|
*/
|
||||||
ports?: Record<string, Record<string, string>>;
|
ports?: Record<string, string> | undefined;
|
||||||
/**
|
/**
|
||||||
* Environment a container gets in the lab that it does not get in the mesh — an address the bed
|
* 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.
|
* stands up in place of a real upstream, and nothing else. Merged over the manifest's own.
|
||||||
@@ -323,8 +324,7 @@ export function catalogueModule(module: string, held: HeldImage[], lab: ForTheLa
|
|||||||
} else if (typeof r.image === "string") {
|
} else if (typeof r.image === "string") {
|
||||||
r.image = onTheMachine(r.image, held);
|
r.image = onTheMachine(r.image, held);
|
||||||
}
|
}
|
||||||
const remap = lab.ports?.[r.id];
|
if (lab.ports && Array.isArray(r.ports)) r.ports = r.ports.map((p) => lab.ports![p] ?? p);
|
||||||
if (remap && Array.isArray(r.ports)) r.ports = r.ports.map((p) => remap[p] ?? p);
|
|
||||||
const env = lab.env?.[r.id];
|
const env = lab.env?.[r.id];
|
||||||
if (env) r.env = { ...(r.env ?? {}), ...env };
|
if (env) r.env = { ...(r.env ?? {}), ...env };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,19 +25,17 @@
|
|||||||
|
|
||||||
import { test, before, after } from "node:test";
|
import { test, before, after } from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { dirname, 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";
|
||||||
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, deriveTheFilterOn } from "./harness.ts";
|
import { labIsUsable, destroyAll, foundationBundle, deriveTheFilterOn, catalogueIsPresent, catalogueModule, needsBrokerAccount } 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();
|
||||||
const binary = hostBinaryPath();
|
const binary = hostBinaryPath();
|
||||||
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
||||||
const modulesEnv = process.env["MESH_LAB_MODULES"] ?? "";
|
|
||||||
|
|
||||||
const skip = !capability.usable
|
const skip = !capability.usable
|
||||||
? `lab not usable: ${capability.why}`
|
? `lab not usable: ${capability.why}`
|
||||||
@@ -45,14 +43,12 @@ const skip = !capability.usable
|
|||||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||||
: !bundle || !existsSync(bundle)
|
: !bundle || !existsSync(bundle)
|
||||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||||
: false;
|
: catalogueIsPresent();
|
||||||
|
|
||||||
const SCENARIO = "whole-mesh-ace";
|
const SCENARIO = "whole-mesh-ace";
|
||||||
const NODE = "ace";
|
const NODE = "ace";
|
||||||
|
|
||||||
const catalogDir = process.env["MESH_LAB_CATALOG"]
|
|
||||||
?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "")
|
|
||||||
?? resolve(process.cwd(), "..", "mesh-catalog", "modules");
|
|
||||||
|
|
||||||
/** The operator-owned media library the ADR-0051 `accesses` point at — pre-created before the push. */
|
/** The operator-owned media library the ADR-0051 `accesses` point at — pre-created before the push. */
|
||||||
const MEDIA_DIRS = [
|
const MEDIA_DIRS = [
|
||||||
@@ -175,27 +171,17 @@ 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. */
|
/** 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 {
|
function bundleFor(images: HeldImage[]): string {
|
||||||
return foundationBundle(bundle, images);
|
return foundationBundle(bundle, images);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The catalogue's manifest as the lab runs it (harness). This bed's own loader never resolved a
|
||||||
|
* runtime ARTIFACT to a stocked image, so every module whose runtime the mesh builds travelled to
|
||||||
|
* the machine unresolved — the shared loader does (novox/hq 04-ISSUES/073). */
|
||||||
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
||||||
const path = resolve(catalogDir, name, "module.json");
|
const manifest = catalogueModule(name, held, { ports: REMAP[name] });
|
||||||
const m = JSON.parse(readFileSync(path, "utf8")) as {
|
return { manifest, broker: needsBrokerAccount(manifest) };
|
||||||
resources?: { type: string; image?: string; ports?: string[] }[];
|
|
||||||
};
|
|
||||||
const remap = REMAP[name] ?? {};
|
|
||||||
for (const r of m.resources ?? []) {
|
|
||||||
if (r.type !== "container") continue;
|
|
||||||
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") };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tokenFrom(said: string): string {
|
function tokenFrom(said: string): string {
|
||||||
@@ -240,7 +226,6 @@ async function nodeState(node: string): Promise<NodeState> {
|
|||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
if (skip) return;
|
if (skip) return;
|
||||||
assert.ok(existsSync(catalogDir), `mesh-catalog modules not found at ${catalogDir}`);
|
|
||||||
|
|
||||||
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {
|
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {
|
||||||
onProgress: (m) => console.log(`raise: ${m}`),
|
onProgress: (m) => console.log(`raise: ${m}`),
|
||||||
|
|||||||
@@ -58,21 +58,20 @@ import { test, before, after } from "node:test";
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
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 { loadScenario } from "../../src/declaration/parse.ts";
|
||||||
import { raise } from "../../src/lifecycle/raise.ts";
|
import { raise } from "../../src/lifecycle/raise.ts";
|
||||||
import { destroy, exec, instanceNameOf, push } from "../../src/lifecycle/operate.ts";
|
import { destroy, exec, instanceNameOf, push } from "../../src/lifecycle/operate.ts";
|
||||||
import {
|
import {
|
||||||
bootstrapBinaryPath, hostBinaryPath, placeBootstrap, BOOTSTRAP_PATH, HOST_PATH,
|
bootstrapBinaryPath, hostBinaryPath, placeBootstrap, BOOTSTRAP_PATH, HOST_PATH,
|
||||||
} from "../../src/lifecycle/place.ts";
|
} 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";
|
import { referenceFor, type HeldImage } from "../../src/pinning.ts";
|
||||||
|
|
||||||
const capability = await labIsUsable();
|
const capability = await labIsUsable();
|
||||||
const binary = hostBinaryPath();
|
const binary = hostBinaryPath();
|
||||||
const installer = bootstrapBinaryPath();
|
const installer = bootstrapBinaryPath();
|
||||||
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
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
|
// 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
|
// (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.
|
// 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"
|
? "MESH_LAB_SOURCE is not set to the repository the control plane is built from"
|
||||||
: !sourceRef
|
: !sourceRef
|
||||||
? "MESH_LAB_SOURCE_REF is not set to the commit to build"
|
? "MESH_LAB_SOURCE_REF is not set to the commit to build"
|
||||||
: false;
|
: catalogueIsPresent();
|
||||||
|
|
||||||
const SCENARIO = "whole-mesh-full";
|
const SCENARIO = "whole-mesh-full";
|
||||||
/** novox hosts the foundation and the control plane; it is where `mesh` commands run. */
|
/** 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
|
* `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.
|
* 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.
|
* 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 KEEP = !!process.env["MESH_LAB_KEEP"];
|
||||||
const FIXED_ID = process.env["MESH_LAB_INSTANCE_ID"] ?? (KEEP ? "whole-mesh-full-live" : undefined);
|
const FIXED_ID = process.env["MESH_LAB_INSTANCE_ID"] ?? (KEEP ? "whole-mesh-full-live" : undefined);
|
||||||
|
|
||||||
const catalogDir = process.env["MESH_LAB_CATALOG"]
|
/** The catalogue's modules directory (harness). Absent, the bed skips — see `skip`. */
|
||||||
?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "")
|
const catalogDir = catalogueIsPresent() ? "" : catalogueDir();
|
||||||
?? resolve(process.cwd(), "..", "mesh-catalog", "modules");
|
|
||||||
|
|
||||||
const MEDIA_DIRS = [
|
const MEDIA_DIRS = [
|
||||||
"/services/media/series", "/services/media/anime", "/services/media/movies",
|
"/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
|
// 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
|
// 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.
|
// 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",
|
name: "mailu",
|
||||||
containers: [
|
containers: [
|
||||||
@@ -210,16 +208,16 @@ const NOVOX: Mod[] = [
|
|||||||
"mailu-front", "mesh-mailu",
|
"mailu-front", "mesh-mailu",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ name: "firewall", containers: [], node: true },
|
{ name: "nftables", containers: [], node: true },
|
||||||
{ name: "fail2ban", containers: [], node: true },
|
{ name: "fail2ban", containers: [], node: true },
|
||||||
];
|
];
|
||||||
const CORE_NOVOX = new Set([
|
const CORE_NOVOX = new Set([
|
||||||
"postgres", "redis", "minio", "mongodb", "mssql", "lavinmq",
|
"postgres", "redis", "minio", "mongodb", "mssql", "lavinmq",
|
||||||
"route-proxy", "keycloak", "gitea", "nextcloud", "invoicing", "photos", "novox.be",
|
"route-proxy", "keycloak", "gitea", "nextcloud", "invoicing", "photos", "novox.be",
|
||||||
"portainer", "verdaccio", "registry",
|
"portainer", "verdaccio", "distribution",
|
||||||
]);
|
]);
|
||||||
const GAPS_NOVOX = new Set([
|
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
|
// 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
|
// 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.
|
// 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. */
|
/** 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 {
|
function bundleFor(images: HeldImage[]): string {
|
||||||
return foundationBundle(bundle, images);
|
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 } {
|
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
||||||
const path = resolve(catalogDir, name, "module.json");
|
const manifest = catalogueModule(name, held, { ports: REMAP[name] });
|
||||||
const m = JSON.parse(readFileSync(path, "utf8")) as {
|
return { manifest, broker: needsBrokerAccount(manifest) };
|
||||||
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") };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tokenFrom(said: string): string {
|
function tokenFrom(said: string): string {
|
||||||
@@ -745,7 +714,6 @@ async function joinTheMesh(): Promise<void> {
|
|||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
if (skip) return;
|
if (skip) return;
|
||||||
assert.ok(existsSync(catalogDir), `mesh-catalog modules not found at ${catalogDir}`);
|
|
||||||
|
|
||||||
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {
|
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {
|
||||||
onProgress: (m) => console.log(`raise: ${m}`),
|
onProgress: (m) => console.log(`raise: ${m}`),
|
||||||
|
|||||||
@@ -36,19 +36,17 @@
|
|||||||
|
|
||||||
import { test, before, after } from "node:test";
|
import { test, before, after } from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { dirname, 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";
|
||||||
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, catalogueIsPresent, catalogueModule, needsBrokerAccount } 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();
|
||||||
const binary = hostBinaryPath();
|
const binary = hostBinaryPath();
|
||||||
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
||||||
const modulesEnv = process.env["MESH_LAB_MODULES"] ?? "";
|
|
||||||
|
|
||||||
const skip = !capability.usable
|
const skip = !capability.usable
|
||||||
? `lab not usable: ${capability.why}`
|
? `lab not usable: ${capability.why}`
|
||||||
@@ -56,15 +54,12 @@ const skip = !capability.usable
|
|||||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||||
: !bundle || !existsSync(bundle)
|
: !bundle || !existsSync(bundle)
|
||||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||||
: false;
|
: catalogueIsPresent();
|
||||||
|
|
||||||
const SCENARIO = "whole-mesh-novox";
|
const SCENARIO = "whole-mesh-novox";
|
||||||
const NODE = "novox";
|
const NODE = "novox";
|
||||||
|
|
||||||
/** Where the committed module.json files live: the mesh-catalog beside mesh-controller. */
|
|
||||||
const catalogDir = process.env["MESH_LAB_CATALOG"]
|
|
||||||
?? (modulesEnv ? resolve(dirname(dirname(dirname(modulesEnv))), "mesh-catalog", "modules") : "")
|
|
||||||
?? resolve(process.cwd(), "..", "mesh-catalog", "modules");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The set, in dependency-reading order (the resolver accepts any order). Each row: the module, and
|
* The set, in dependency-reading order (the resolver accepts any order). Each row: the module, and
|
||||||
@@ -229,38 +224,14 @@ function bundleFor(images: HeldImage[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a committed module.json, rewrite every container image to the scenario's pinned digest, and
|
* The catalogue's manifest as the lab runs it (harness): images pinned, artifacts resolved to the
|
||||||
* apply the host-port remaps. Returns the manifest as a string and whether it needs a broker account
|
* stocked images, the host-port remaps applied. Returns the manifest and whether it needs a broker
|
||||||
* (a runtime that reads MESH_BROKER_FILE — providers and tooled apps do; plain/node modules do not).
|
* account (a runtime that reads MESH_BROKER_FILE — providers and tooled apps do; plain/node modules
|
||||||
|
* do not).
|
||||||
*/
|
*/
|
||||||
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
function loadManifest(name: string): { manifest: string; broker: boolean } {
|
||||||
const path = resolve(catalogDir, name, "module.json");
|
const manifest = catalogueModule(name, held, { ports: REMAP[name] });
|
||||||
const m = JSON.parse(readFileSync(path, "utf8")) as {
|
return { manifest, broker: needsBrokerAccount(manifest) };
|
||||||
resources?: { type: string; image?: string; artifact?: string; ports?: string[] }[];
|
|
||||||
build?: unknown;
|
|
||||||
};
|
|
||||||
const remap = REMAP[name] ?? {};
|
|
||||||
for (const r of m.resources ?? []) {
|
|
||||||
if (r.type !== "container") continue;
|
|
||||||
if (typeof r.image === "string") {
|
|
||||||
r.image = pinned(r.image);
|
|
||||||
} else if (typeof r.artifact === "string") {
|
|
||||||
// Since issue 060 a module's own runtime container names an artifact the mesh's builder
|
|
||||||
// would fill, not a placeholder image. This bed stocks the image instead of building, so
|
|
||||||
// map the artifact to the stocked `mesh-runtime-<module>` the machine holds — keyed on the
|
|
||||||
// MODULE name, not the container's (mailu's runtime container is `mesh-mailu`, its image is
|
|
||||||
// `mesh-runtime-mailu`). The placeholder digest is what `pinned` already resolves for a
|
|
||||||
// mesh-built repo, exactly as it did for the old `image` field.
|
|
||||||
r.image = pinned(`mesh-runtime-${name}@sha256:${"0".repeat(64)}`);
|
|
||||||
delete r.artifact;
|
|
||||||
}
|
|
||||||
if (Array.isArray(r.ports)) r.ports = r.ports.map((p) => remap[p] ?? p);
|
|
||||||
}
|
|
||||||
// The build section the mesh's builder would consume: dropped, because this bed stocks the image
|
|
||||||
// rather than building it. Harmless to leave (the push path never reads it), removed for clarity.
|
|
||||||
delete m.build;
|
|
||||||
const manifest = JSON.stringify(m);
|
|
||||||
return { manifest, broker: manifest.includes("MESH_BROKER_FILE") };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tokenFrom(said: string): string {
|
function tokenFrom(said: string): string {
|
||||||
@@ -306,7 +277,6 @@ async function nodeState(node: string): Promise<NodeState> {
|
|||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
if (skip) return;
|
if (skip) return;
|
||||||
assert.ok(existsSync(catalogDir), `mesh-catalog modules not found at ${catalogDir}`);
|
|
||||||
|
|
||||||
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {
|
const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), {
|
||||||
onProgress: (m) => console.log(`raise: ${m}`),
|
onProgress: (m) => console.log(`raise: ${m}`),
|
||||||
|
|||||||
Reference in New Issue
Block a user