Give the module the broker it asks for, rather than a module that asks for nothing
The mesh refused to place amqp-ping: nothing provides amqp. That refusal is right. The substrate raises a broker, but as a bundle resource — plumbing, not a module the mesh has a record of — so it offers nothing to anything, and a module wanting a broker wants one in the graph. lavinmq is that module and needs no building, its image being upstream, so this is a register and an assign. The alternative was to pick a module with no requires, which would have passed by testing less. Also: the control plane's image has no /tmp to copy a manifest into. Root does. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -67,6 +67,16 @@ const REGISTRY = `${ANCHOR}:5000`;
|
|||||||
* under test (novox/hq SELF-UPGRADE-PLAN, rule 1).
|
* under test (novox/hq SELF-UPGRADE-PLAN, rule 1).
|
||||||
*/
|
*/
|
||||||
const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" };
|
const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" };
|
||||||
|
/**
|
||||||
|
* What `amqp-ping` requires, and what the substrate does not supply.
|
||||||
|
*
|
||||||
|
* The installer raises a broker, but as a bundle resource — plumbing, not a module the mesh has a
|
||||||
|
* record of, so it provides nothing to anything. A module asking for `amqp` is asking for a
|
||||||
|
* provider in the graph, and this is it. No build: its image is upstream.
|
||||||
|
*/
|
||||||
|
const PROVIDER = { module: "lavinmq" };
|
||||||
|
/** Named once, because the step title is also how later steps say what they waited on. */
|
||||||
|
const NEEDS = "the mesh runs a broker for that module to talk to";
|
||||||
const MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping" };
|
const MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping" };
|
||||||
|
|
||||||
const capability = await labIsUsable();
|
const capability = await labIsUsable();
|
||||||
@@ -146,13 +156,17 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
|
|||||||
* Pushing the manifest to the machine and naming that path got `no such file or directory` from
|
* Pushing the manifest to the machine and naming that path got `no such file or directory` from
|
||||||
* inside mesh-control, which is correct and was briefly mistaken for a missing manifest. It is
|
* inside mesh-control, which is correct and was briefly mistaken for a missing manifest. It is
|
||||||
* copied the last step of the way with `docker cp`.
|
* copied the last step of the way with `docker cp`.
|
||||||
|
*
|
||||||
|
* **Into the root, not into /tmp.** The control plane's image is a minimal one and has no `/tmp`
|
||||||
|
* to copy into — `docker cp` says so in those words. `/` is the one directory every image has.
|
||||||
*/
|
*/
|
||||||
async function registerModule(module: string, manifest: string): Promise<string> {
|
async function registerModule(module: string, manifest: string): Promise<string> {
|
||||||
assert.ok(existsSync(manifest), `no manifest for ${module} at ${manifest}`);
|
assert.ok(existsSync(manifest), `no manifest for ${module} at ${manifest}`);
|
||||||
const onMachine = `/tmp/${module}.json`;
|
const onMachine = `/tmp/${module}.json`;
|
||||||
|
const inContainer = `/${module}.json`;
|
||||||
await push(instanceId, CONTROL, manifest, onMachine);
|
await push(instanceId, CONTROL, manifest, onMachine);
|
||||||
await must(CONTROL, `docker cp ${onMachine} mesh-control:${onMachine}`);
|
await must(CONTROL, `docker cp ${onMachine} mesh-control:${inContainer}`);
|
||||||
return mesh(`module add ${onMachine}`);
|
return mesh(`module add ${inContainer}`);
|
||||||
}
|
}
|
||||||
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(" "));
|
||||||
@@ -297,12 +311,36 @@ before(async () => {
|
|||||||
return `${built}\n${builds}`;
|
return `${built}\n${builds}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- 4b. WHAT THE MODULE NEEDS --------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// `amqp-ping` requires the `amqp` provision, and the mesh refused to place it: "nothing provides
|
||||||
|
// amqp, wanted by amqp-ping". That refusal is correct and is the reason this step exists rather
|
||||||
|
// than the reason to pick an easier module. **The substrate's broker is not a provider.** It is
|
||||||
|
// raised by the installer as part of the bundle, so it is a running container and not a module
|
||||||
|
// with something to offer — the mesh's own plumbing, not an entry in its graph. A module that
|
||||||
|
// wants a broker wants one the mesh knows about.
|
||||||
|
//
|
||||||
|
// `lavinmq` is that module, and it needs no building: its image is upstream, so this is a
|
||||||
|
// register and an assign, and it is a fair test of provisioning rather than of building.
|
||||||
|
await step(NEEDS, "the mesh builds a module standing on that base", async () => {
|
||||||
|
await registerModule(PROVIDER.module, resolve(catalogDir, PROVIDER.module, "module.json"));
|
||||||
|
await mesh(`assign ${CONTROL} ${PROVIDER.module}`);
|
||||||
|
await mesh(`push ${CONTROL}`, 600_000);
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
const ps = (await on(CONTROL, `docker ps --format '{{.Names}}\t{{.Status}}'`)).out;
|
||||||
|
const line = ps.split("\n").find((l) => l.includes(PROVIDER.module));
|
||||||
|
if (line && /Up /.test(line)) return ps;
|
||||||
|
await new Promise((r) => setTimeout(r, 5_000));
|
||||||
|
}
|
||||||
|
throw new Error(`${PROVIDER.module} never came up on ${CONTROL}:\n` +
|
||||||
|
(await on(CONTROL, `docker ps -a --format '{{.Names}}\t{{.Status}}'`)).out);
|
||||||
|
});
|
||||||
|
|
||||||
// ---- 5. THE ANCHOR RUNS IT ------------------------------------------------------------------
|
// ---- 5. THE ANCHOR RUNS IT ------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
// The machine that built it. This is the case every earlier proof covered, and it is here as the
|
// The machine that built it. This is the case every earlier proof covered, and it is here as the
|
||||||
// control for step 6: if this fails, step 6's failure says nothing about fetching.
|
// control for step 6: if this fails, step 6's failure says nothing about fetching.
|
||||||
await step("the anchor runs the module the mesh built",
|
await step("the anchor runs the module the mesh built", NEEDS, async () => {
|
||||||
"the mesh builds a module standing on that base", async () => {
|
|
||||||
await mesh(`module issue ${MODULE.module} --node ${CONTROL}`).catch(() => {});
|
await mesh(`module issue ${MODULE.module} --node ${CONTROL}`).catch(() => {});
|
||||||
await mesh(`assign ${CONTROL} ${MODULE.module}`);
|
await mesh(`assign ${CONTROL} ${MODULE.module}`);
|
||||||
await mesh(`push ${CONTROL}`, 600_000);
|
await mesh(`push ${CONTROL}`, 600_000);
|
||||||
@@ -362,6 +400,7 @@ for (const name of [
|
|||||||
"three machines join it across the household gateway",
|
"three machines join it across the household gateway",
|
||||||
"the mesh builds the shared base from source",
|
"the mesh builds the shared base from source",
|
||||||
"the mesh builds a module standing on that base",
|
"the mesh builds a module standing on that base",
|
||||||
|
NEEDS,
|
||||||
"the anchor runs the module the mesh built",
|
"the anchor runs the module the mesh built",
|
||||||
"a joined machine runs the module the mesh built",
|
"a joined machine runs the module the mesh built",
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user