Files
mesh-lab/src/diagram/from-declaration.ts
T
jschoubben 2243618f01 Draw a scenario, from the declaration and from the hypervisor
`mesh-lab diagram` renders a scenario as draw.io, from either source, through
one layout — so a difference between what was asked for and what exists is a
difference you can see.

The shape says what a resource is and is fixed per kind. The badges say what is
true about that particular one and come entirely from metadata: translation,
forwardability, mapping expiry, refuses-inbound, container-or-VM, running. The
interesting properties of a network are exactly the ones with no visual
consequence — a translated address looks identical to an untranslated one.

For the live picture to be a record rather than a restatement, raise now writes
down what it applied: a segment's kind, ranges and MTU on the link; a gateway's
translation, forwardability and expiry on the gateway; inbound: deny on the
machine. Every behavioural tag is written AFTER the thing works, never at
creation — a failed raise leaves wreckage standing on purpose, and a picture of
that wreckage must not badge translation the router never got.

The pairing earned itself immediately: drawn side by side, every virtual machine
held no addresses. A container's interface carries the device's name and a VM
names its own, so joining them by name silently dropped one whole class of
machine. Fixed by joining on MAC.

Also brings tests under the typecheck gate, which caught integration timeouts
being passed as a 4th argument and therefore ignored entirely.
2026-08-24 22:53:00 +02:00

69 lines
2.5 KiB
TypeScript

/** The picture of what a scenario asks for. Needs nothing running. */
import type { Scenario } from "../declaration/types.ts";
import { planRouters, routerMachineName, ttlSeconds } from "../lifecycle/router.ts";
import { depthOf, type Diagram, type DiagramMachine, type DiagramSegment } from "./model.ts";
export function diagramFromDeclaration(scenario: Scenario): Diagram {
const parentOf = (name: string) => scenario.segments[name]?.gateway?.to;
const segments: DiagramSegment[] = Object.entries(scenario.segments).map(([name, segment]) => ({
name,
kind: segment.kind,
cidr: segment.cidr,
mtu: segment.mtu,
behind: segment.gateway?.to,
depth: depthOf(name, parentOf),
}));
const machines: DiagramMachine[] = Object.entries(scenario.machines).map(([name, spec]) => {
const notes: string[] = [];
if (spec.inbound === "deny") notes.push("refuses inbound");
for (const publication of spec.published ?? []) {
notes.push(`published :${publication.port} via ${publication.on}`);
}
return {
name,
kind: "machine" as const,
notes,
attachments:
spec.at === "detached"
? []
: spec.at.map((a) => ({ segment: a.segment, addresses: a.address })),
};
});
// Routers are implicit in a declaration — a scenario says a segment sits behind one and
// never names it. The diagram has to show them anyway, or the picture omits the machines
// that carry every interesting property.
for (const plan of planRouters(scenario, "diagram")) {
const notes: string[] = [];
notes.push(plan.nat.length ? `NAT ${plan.nat.join("+")}` : "routed, no NAT");
notes.push(plan.forwardable ? "forwardable" : "NOT forwardable");
const ttl = ttlSeconds(plan.mappingTtl);
if (ttl !== undefined) notes.push(`mappings expire ${ttl}s`);
machines.push({
name: routerMachineName(plan),
kind: "router",
notes,
attachments: [
{ segment: plan.outside, addresses: plan.outsideAddresses },
...plan.inside.map((segment) => ({ segment, addresses: [] })),
],
});
}
const publicSegments = segments.filter((s) => s.kind === "public");
if (publicSegments.length > 1) {
machines.push({
name: "transit",
kind: "transit",
notes: ["routed, never bridged"],
attachments: publicSegments.map((s) => ({ segment: s.name, addresses: [] })),
});
}
return { title: scenario.scenario, source: "declared", segments, machines };
}