Twenty-eight integration tests each carried their own copy of the same two helpers, which pointed a manifest and the substrate bundle at whatever the lab's registry had assigned. They now share two in the harness, and the difference is the point: ours is rewritten to the ID the machine holds it under, and everything else is left exactly as written so the machine pulls it. **The substrate bundle is where the fiction was most load-bearing.** mesh-host's `examples/substrate-first-node.lock` pins all three of its images at `192.0.2.250:5000/…`, which is the address the lab's registry served from — it was written for a target, and the target was the lab. Two of those are ordinary third-party images and become the digests mesh-catalog's own postgres and lavinmq modules pin, so the substrate's store and broker are literally the images the mesh runs. mesh-control exists in no registry at all and becomes the ID the machine was handed. **The bundle itself should be fixed in mesh-host and this substitution deleted with it.** Beds that wrote a manifest by hand named an image by repository and let the rewrite supply a digest. There is nothing to supply one now, so `onTheMachine` refuses an unpinned reference and hands back the digest the catalogue pins — a bed runs the image the mesh ships, and a bed that drifts from the catalogue is testing a different postgres. Three beds took a third-party image out of the raised list, which no longer contains one: certificates (pebble), objectstore (minio and its client) and provisioner (postgres) now name theirs and pull it. builds and mesh publish into the MESH's own artifact store — the `registry` module's image, on the node, on 5000 — rather than into scenery the lab raised. That is a different claim, and only one of them exists in production. New unit tests cover what a full raise would otherwise be the only way to check: the routes an egress machine gets (that its gateway is still the path to the rest of the scenario, that a range with no path is unreachable rather than leaked to the uplink, that each family gets its own next hop), which machine is handed which of our images, and the `images:` rule that refuses a third-party entry. The "shipped scenarios are valid" test now loads every scenario rather than two of them. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readdirSync } from "node:fs";
|
|
import { parseScenario } from "../src/declaration/parse.ts";
|
|
import { loadScenario } from "../src/declaration/parse.ts";
|
|
import { planRouters } from "../src/lifecycle/router.ts";
|
|
|
|
/** Every rejection below is a fault that would otherwise be silent at runtime. */
|
|
function refuses(yaml: string, pattern: RegExp): void {
|
|
assert.throws(() => parseScenario(yaml), (err: Error) => {
|
|
assert.match(err.message, pattern);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
test("the shipped scenarios are valid", () => {
|
|
// **Every one of them**, not a chosen two. Thirty-odd scenarios were rewritten in one pass when
|
|
// the lab's registry was removed, and a scenario nobody loads is a scenario nobody validates —
|
|
// which is how a bed goes unraisable for weeks and is only found when somebody wants it.
|
|
const files = readdirSync("scenarios").filter((f) => f.endsWith(".yml"));
|
|
assert.ok(files.length > 20, `only ${files.length} scenarios found — is the path right?`);
|
|
for (const file of files) {
|
|
assert.doesNotThrow(() => loadScenario(`scenarios/${file}`), `scenarios/${file}`);
|
|
}
|
|
});
|
|
|
|
test("a public segment on a private range is refused — the mesh would silently never form", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.168.1.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.168.1.1] } } }`,
|
|
/not documentation space/,
|
|
);
|
|
});
|
|
|
|
test("a public segment on a real routable range is refused", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments: { net: { kind: public, cidr: [8.8.8.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [8.8.8.8] } } }`,
|
|
/not documentation space/,
|
|
);
|
|
});
|
|
|
|
test("a private segment may use any range, including someone else's RFC 1918", () => {
|
|
assert.doesNotThrow(() =>
|
|
parseScenario(`scenario: x
|
|
segments:
|
|
pub: { kind: public, cidr: [192.0.2.0/24] }
|
|
cafe: { kind: private, cidr: [10.50.0.0/16], gateway: { to: pub, address: [192.0.2.5], nat: [v4], forwardable: false } }
|
|
machines: { a: { at: { segment: cafe, address: [10.50.0.9] } } }`),
|
|
);
|
|
});
|
|
|
|
test("publishing through an unforwardable gateway is refused — that is the constraint", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments:
|
|
pub: { kind: public, cidr: [192.0.2.0/24] }
|
|
cafe: { kind: private, cidr: [10.50.0.0/16], gateway: { to: pub, address: [192.0.2.5], nat: [v4], forwardable: false } }
|
|
machines:
|
|
a:
|
|
at: { segment: cafe, address: [10.50.0.9] }
|
|
published: [{ port: 443, on: cafe }]`,
|
|
/not forwardable/,
|
|
);
|
|
});
|
|
|
|
test("a gateway address must be on the PARENT segment, not the one behind it", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments:
|
|
pub: { kind: public, cidr: [192.0.2.0/24] }
|
|
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.168.1.1], nat: [v4] } }
|
|
machines: { a: { at: { segment: home, address: [192.168.1.9] } } }`,
|
|
/is not within 'pub'/,
|
|
);
|
|
});
|
|
|
|
test("a machine address outside its segment is refused", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [203.0.113.9] } } }`,
|
|
/is not within segment 'net'/,
|
|
);
|
|
});
|
|
|
|
test("an unknown segment reference is refused", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: nope, address: [192.0.2.1] } } }`,
|
|
/unknown segment 'nope'/,
|
|
);
|
|
});
|
|
|
|
test("a gateway loop is refused rather than raised forever", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments:
|
|
a: { kind: private, cidr: [10.0.0.0/24], gateway: { to: b, address: [10.0.1.1], nat: [] } }
|
|
b: { kind: private, cidr: [10.0.1.0/24], gateway: { to: a, address: [10.0.0.1], nat: [] } }
|
|
machines: { m: { at: { segment: a, address: [10.0.0.9] } } }`,
|
|
/loops through/,
|
|
);
|
|
});
|
|
|
|
test("a detached machine cannot publish", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: detached, published: [{ port: 443, on: net }] } }`,
|
|
/detached but declares published/,
|
|
);
|
|
});
|
|
|
|
test("publishing on a segment the machine is not attached to is refused", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments:
|
|
pub: { kind: public, cidr: [192.0.2.0/24] }
|
|
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
|
|
machines:
|
|
a:
|
|
at: { segment: pub, address: [192.0.2.10] }
|
|
published: [{ port: 443, on: home }]`,
|
|
/not attached to it/,
|
|
);
|
|
});
|
|
|
|
test("two addresses of one family on one attachment is refused", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1, 192.0.2.2] } } }`,
|
|
/two v4 addresses/,
|
|
);
|
|
});
|
|
|
|
test("place naming a machine that does not exist is refused", () => {
|
|
refuses(
|
|
`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1] } } }
|
|
place: { ghost: [host] }`,
|
|
/'ghost' is not a machine/,
|
|
);
|
|
});
|
|
|
|
test("every problem is reported, not just the first", () => {
|
|
try {
|
|
parseScenario(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.168.0.0/24] } }
|
|
machines: { a: { at: { segment: nope, address: [1.2.3.4] } } }
|
|
place: { ghost: [host] }`);
|
|
assert.fail("should have thrown");
|
|
} catch (err) {
|
|
const problems = (err as { problems: string[] }).problems;
|
|
assert.ok(problems.length >= 3, `expected several problems, got ${problems.length}`);
|
|
}
|
|
});
|
|
|
|
test("a detached machine is valid", () => {
|
|
assert.doesNotThrow(() =>
|
|
parseScenario(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines:
|
|
a: { at: { segment: net, address: [192.0.2.1] } }
|
|
roamer: { at: detached }`),
|
|
);
|
|
});
|
|
|
|
test("a multi-homed machine is valid", () => {
|
|
assert.doesNotThrow(() =>
|
|
parseScenario(`scenario: x
|
|
segments:
|
|
pub: { kind: public, cidr: [192.0.2.0/24] }
|
|
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
|
|
machines:
|
|
border:
|
|
at:
|
|
- { segment: pub, address: [192.0.2.60] }
|
|
- { segment: home, address: [192.168.1.2] }`),
|
|
);
|
|
});
|
|
|
|
test("an isolated private segment with no gateway is valid — a site with no internet", () => {
|
|
assert.doesNotThrow(() =>
|
|
parseScenario(`scenario: x
|
|
segments: { island: { kind: private, cidr: [10.9.0.0/24] } }
|
|
machines: { a: { at: { segment: island, address: [10.9.0.1] } } }`),
|
|
);
|
|
});
|
|
|
|
test("two gateways sharing an address are one gateway, not two", () => {
|
|
// Modelled on the real thing: a bridged modem, one gateway holding the public address,
|
|
// everything behind it. Two routers on one address is not a topology, it is a collision —
|
|
// and the lab raised it happily, with the address resolving to whichever container
|
|
// answered ARP last.
|
|
const scenario = parseScenario(`
|
|
scenario: shared-gateway
|
|
segments:
|
|
isp:
|
|
kind: public
|
|
cidr: [198.51.100.0/24, "2001:db8:b::/48"]
|
|
home:
|
|
kind: private
|
|
cidr: [192.168.1.0/24]
|
|
gateway: { to: isp, address: [198.51.100.7, "2001:db8:b::7"], nat: [v4], forwardable: true, mapping_ttl: 120s }
|
|
devices:
|
|
kind: private
|
|
cidr: [192.168.30.0/24]
|
|
gateway: { to: isp, address: [198.51.100.7], nat: [v4], forwardable: true, mapping_ttl: 120s }
|
|
machines:
|
|
thermostat:
|
|
at: { segment: devices, address: [192.168.30.20] }
|
|
`);
|
|
const plans = planRouters(scenario, "test");
|
|
assert.equal(plans.length, 1, `expected one gateway, got ${plans.map((p) => p.inside.join("+")).join(" / ")}`);
|
|
assert.deepEqual([...plans[0]!.inside].sort(), ["devices", "home"]);
|
|
// The union: a v6 address declared on only one of the segments it serves is still carried.
|
|
assert.deepEqual([...plans[0]!.outsideAddresses].sort(), ["198.51.100.7", "2001:db8:b::7"]);
|
|
});
|
|
|
|
test("one box cannot behave two ways", () => {
|
|
// If two gateways share an address they are the same box, so a disagreement about what
|
|
// that box does is a contradiction — refused rather than silently resolved one way.
|
|
assert.throws(
|
|
() =>
|
|
parseScenario(`
|
|
scenario: contradictory-gateway
|
|
segments:
|
|
isp:
|
|
kind: public
|
|
cidr: [198.51.100.0/24]
|
|
home:
|
|
kind: private
|
|
cidr: [192.168.1.0/24]
|
|
gateway: { to: isp, address: [198.51.100.7], nat: [v4], forwardable: true }
|
|
devices:
|
|
kind: private
|
|
cidr: [192.168.30.0/24]
|
|
gateway: { to: isp, address: [198.51.100.7], nat: [v4], forwardable: false }
|
|
machines:
|
|
thermostat:
|
|
at: { segment: devices, address: [192.168.30.20] }
|
|
`),
|
|
/one gateway.*disagree.*forwardable/s,
|
|
);
|
|
});
|
|
|
|
test("a detached machine cannot declare egress", () => {
|
|
refuses(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: detached, egress: true } }`,
|
|
/detached but declares egress/);
|
|
});
|
|
|
|
/**
|
|
* `images:` is the mesh's own images and nothing else.
|
|
*
|
|
* **The rule that replaced the lab's registry.** Anything with somewhere to be fetched from is
|
|
* fetched from there, by the machine, over its uplink. Serving it from inside the scenario instead
|
|
* is what hid the bootstrap faults this lab exists to find — so it is refused rather than quietly
|
|
* done, or the fiction comes back one convenient line at a time.
|
|
*/
|
|
test("a third-party image in images: is refused, because nothing loads it", () => {
|
|
for (const image of ["postgres:17-alpine", "gitea/gitea:1.22", "ghcr.io/mailu/admin:1.9"]) {
|
|
refuses(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
|
|
images: ["${image}"]
|
|
place: { all: [runtime] }`,
|
|
/is not one of the mesh's own images/);
|
|
}
|
|
});
|
|
|
|
test("one of ours in images: is accepted", () => {
|
|
assert.doesNotThrow(() => parseScenario(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
|
|
images: [mesh-control:development, mesh-route-proxy:development]
|
|
place: { all: [runtime] }`));
|
|
});
|
|
|
|
test("images: is named by tag — an image ID is not knowable until the image is built", () => {
|
|
refuses(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: net, address: [192.0.2.1] }, egress: true } }
|
|
images: ["mesh-control@sha256:${"0".repeat(64)}"]
|
|
place: { all: [runtime] }`,
|
|
/is pinned by digest/);
|
|
});
|
|
|
|
test("a machine cannot be handed an image the scenario does not have", () => {
|
|
// Ignoring it silently would be a machine missing a runtime, failing several minutes later
|
|
// inside an apply, as a container that will not start.
|
|
refuses(`scenario: x
|
|
segments: { net: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines:
|
|
a:
|
|
at: { segment: net, address: [192.0.2.1] }
|
|
egress: true
|
|
images: [mesh-runtime-redis:development]
|
|
images: [mesh-control:development]
|
|
place: { all: [runtime] }`,
|
|
/is not in this scenario's images/);
|
|
});
|
|
|
|
test("a segment may not be named 'uplink' — the lab claims that name for egress", () => {
|
|
refuses(`scenario: x
|
|
segments: { uplink: { kind: public, cidr: [192.0.2.0/24] } }
|
|
machines: { a: { at: { segment: uplink, address: [192.0.2.1] }, egress: true } }`,
|
|
/reserved for the lab's own NAT bridge/);
|
|
});
|