One-node bed: SDK-by-version, rename, and the store/broker upgrade proofs #27
+30
-30
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"scenario": "one-node-mesh",
|
||||
"established": 12,
|
||||
"established": 18,
|
||||
"of": 21,
|
||||
"steps": [
|
||||
{
|
||||
"code": "R1",
|
||||
"title": "a bare machine becomes a mesh of one, raised by the installer",
|
||||
"status": "pass",
|
||||
"seconds": 135,
|
||||
"seconds": 133,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
@@ -56,28 +56,28 @@
|
||||
"code": "P1",
|
||||
"title": "the mesh builds the shared base from source",
|
||||
"status": "pass",
|
||||
"seconds": 87,
|
||||
"seconds": 83,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "P2",
|
||||
"title": "the mesh builds and runs a store of its own",
|
||||
"status": "pass",
|
||||
"seconds": 41,
|
||||
"seconds": 47,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "P3",
|
||||
"title": "the mesh builds and runs its own catalogue",
|
||||
"status": "pass",
|
||||
"seconds": 37,
|
||||
"seconds": 26,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "P4",
|
||||
"title": "the mesh rebuilds its own control plane from source",
|
||||
"status": "pass",
|
||||
"seconds": 35,
|
||||
"seconds": 30,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
@@ -90,58 +90,58 @@
|
||||
{
|
||||
"code": "N2",
|
||||
"title": "the machine has a packet filter, loaded from what modules declared",
|
||||
"status": "fail",
|
||||
"seconds": 0,
|
||||
"why": "anchor: docker exec mesh-control /mesh-control assign anchor firewall\n\nmesh-control: no module of that name: firewall\n"
|
||||
"status": "pass",
|
||||
"seconds": 7,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "U1",
|
||||
"title": "the mesh builds a module standing on that base",
|
||||
"status": "skip",
|
||||
"seconds": 0,
|
||||
"why": "not attempted — N2 (the machine has a packet filter, loaded from what modules declared) did not succeed"
|
||||
"status": "pass",
|
||||
"seconds": 23,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "U2",
|
||||
"title": "the mesh runs a broker for that module to talk to",
|
||||
"status": "skip",
|
||||
"seconds": 0,
|
||||
"why": "not attempted — U1 (the mesh builds a module standing on that base) did not succeed"
|
||||
"status": "pass",
|
||||
"seconds": 20,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "U3",
|
||||
"title": "the anchor runs the module the mesh built",
|
||||
"status": "skip",
|
||||
"seconds": 0,
|
||||
"why": "not attempted — U2 (the mesh runs a broker for that module to talk to) did not succeed"
|
||||
"status": "pass",
|
||||
"seconds": 6,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "V1",
|
||||
"title": "the control plane can describe the mesh, and what it says is true",
|
||||
"status": "skip",
|
||||
"seconds": 0,
|
||||
"why": "not attempted — U3 (the anchor runs the module the mesh built) did not succeed"
|
||||
"status": "pass",
|
||||
"seconds": 1,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "V2",
|
||||
"title": "the catalogue holds every module this mesh built",
|
||||
"status": "skip",
|
||||
"seconds": 0,
|
||||
"why": "not attempted — V1 (the control plane can describe the mesh, and what it says is true) did not succeed"
|
||||
"status": "fail",
|
||||
"seconds": 1,
|
||||
"why": "the catalogue does not hold mesh-tools, postgres — the mesh built them and its own record has no trace of it (novox/hq issue 050):\n{\"modules\":[{\"module\":\"amqp-ping\",\"commit\":\"e0c92195d4240841bfcf4b4a9ef869d5afeca331\",\"repository\":\"https://git.novox.be/novox/mesh-catalog.git\",\"path\":\"modules/amqp-ping\"},{\"module\":\"lavinmq\",\"commit\":\"e0c92195d4240841bfcf4b4a9ef869d5afeca331\",\"repository\":\"https://git.novox.be/novox/mesh-catalog.git\",\"path\":\"modules/lavinmq\"},{\"module\":\"mesh-control\",\"commit\":\"5062c36fc9efe159aa9706c0ca2c873351ef1ce0\",\"repository\":\"https://git.novox.be/novox/mesh-control.git\",\"path\":\"\"}]}\n\n+ actual - expected\n\n+ [\n+ 'mesh-tools',\n+ 'postgres'\n+ ]\n- []\n"
|
||||
},
|
||||
{
|
||||
"code": "V3",
|
||||
"title": "the machine's networking is what the modules asked for",
|
||||
"status": "skip",
|
||||
"seconds": 0,
|
||||
"why": "not attempted — V1 (the control plane can describe the mesh, and what it says is true) did not succeed"
|
||||
"status": "pass",
|
||||
"seconds": 1,
|
||||
"why": ""
|
||||
},
|
||||
{
|
||||
"code": "E1",
|
||||
"title": "a change to a module's source reaches the machine on its own",
|
||||
"status": "skip",
|
||||
"seconds": 0,
|
||||
"why": "not attempted — V3 (the machine's networking is what the modules asked for) did not succeed"
|
||||
"status": "fail",
|
||||
"seconds": 1,
|
||||
"why": "the mesh does not report a module behind its source:\n1 machine(s), all doing what they were told, all heard from, running what the mesh would send them, and every module current with its source\n"
|
||||
},
|
||||
{
|
||||
"code": "E2",
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
*/
|
||||
import { test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, writeFileSync, appendFileSync } from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
@@ -133,7 +134,7 @@ const CATALOGUED = "the catalogue holds every module this mesh built";
|
||||
const NETWORK = "the machine's networking is what the modules asked for";
|
||||
const FOLLOWS = "a change to a module's source reaches the machine on its own";
|
||||
const SURVIVES = "the mesh comes back after the machine reboots";
|
||||
const MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping" };
|
||||
const MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping", container: "amqp-ping" };
|
||||
|
||||
const capability = await labIsUsable();
|
||||
const binary = hostBinaryPath();
|
||||
@@ -833,26 +834,51 @@ before(async () => {
|
||||
// machinery; this is what the machinery is for.
|
||||
await step("E1", FOLLOWS, NETWORK, async () => {
|
||||
const before = await mesh(`builds ${MODULE.module}`);
|
||||
const wasPinned = before.match(/sha256:[0-9a-f]{64}/)?.[0] ?? "";
|
||||
assert.ok(wasPinned, `nothing is pinned to rebuild from:\n${before}`);
|
||||
const was = before.match(/sha256:[0-9a-f]{64}/)?.[0] ?? "";
|
||||
assert.ok(was, `nothing is pinned to rebuild from:\n${before}`);
|
||||
|
||||
// The mesh is told its copy is older than the source. In life a push does this; here it is
|
||||
// stated, because what is under test is what the mesh does next, not how it hears.
|
||||
const head = (await must(CONTROL, `git ls-remote ${forgeUrl(MODULE.repo)} ` +
|
||||
`${refFor(MODULE.repo)} | cut -f1`, 120_000)).trim();
|
||||
assert.match(head, /^[0-9a-f]{40}$/, `could not read the source's head: ${head}`);
|
||||
// **The source has to actually move, and it cannot be faked.**
|
||||
//
|
||||
// The first version of this read the branch head and told the mesh the source had moved there
|
||||
// — the same commit it had just built. The mesh answered, correctly, that everything was
|
||||
// current. Naming some other commit would not work either: staleness compares ARTIFACTS, not
|
||||
// commits, which is a deliberate choice so that editing a comment in a shared base does not
|
||||
// rebuild everything standing on it to arrive back where it started.
|
||||
//
|
||||
// So this makes a real change to the module's source and pushes it. It is a test that writes
|
||||
// to a branch, which is worth knowing about; the alternative is a test that proves the loop by
|
||||
// telling the mesh something untrue.
|
||||
const checkout = resolve(catalogDir, "..");
|
||||
const marker = `// changed by the one-node test at build ${was.slice(7, 19)}\n`;
|
||||
const file = resolve(catalogDir, MODULE.module, "index.ts");
|
||||
await must(CONTROL, `true`); // keep the shape uniform; the change is made on this workstation
|
||||
appendFileSync(file, marker);
|
||||
// Path-scoped: `commit -am` would sweep whatever else is in the working tree into a commit
|
||||
// this test is about to push.
|
||||
execFileSync("git", ["-C", checkout, "add", file], { stdio: "pipe" });
|
||||
execFileSync("git", ["-C", checkout, "commit", "-q", "-m",
|
||||
`Move ${MODULE.module}'s source, so the mesh has something to notice`], { stdio: "pipe" });
|
||||
execFileSync("git", ["-C", checkout, "push", "-q", "origin", refFor(MODULE.repo)],
|
||||
{ stdio: "pipe" });
|
||||
const head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"],
|
||||
{ encoding: "utf8" }).trim();
|
||||
|
||||
// Now the mesh is told. In life a push notices itself; what is under test here is what the
|
||||
// mesh does NEXT, not how it hears.
|
||||
await mesh(`module moved ${MODULE.module} ${head}`);
|
||||
|
||||
const behind = await mesh(`status`);
|
||||
assert.match(behind, /behind|build --behind/,
|
||||
`the mesh does not report a module behind its source:\n${behind}`);
|
||||
`the source moved and the mesh does not report the module behind it:\n${behind}`);
|
||||
|
||||
await mesh(`build --behind --wait 1200s`, 1_500_000);
|
||||
const rolled = await mesh(`upgrade ${MODULE.module} roll-out`, 900_000);
|
||||
await waitForContainer(CONTROL, MODULE.module);
|
||||
await waitForContainer(CONTROL, MODULE.container);
|
||||
|
||||
const after = await mesh(`builds ${MODULE.module}`);
|
||||
assert.match(after, /sha256:[0-9a-f]{64}/, `nothing was pinned after the rebuild:\n${after}`);
|
||||
const now = after.match(/sha256:[0-9a-f]{64}/)?.[0] ?? "";
|
||||
assert.notEqual(now, was,
|
||||
`the module was rebuilt and came back on the same artifact, so nothing reached the machine:` +
|
||||
`\n${after}`);
|
||||
return `${behind}\n${rolled}\n${after}`;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user