A raise does not finish until the machines can pull from the registry

The first whole-mesh raise of the ADR 0056 code died on the anchor's substrate
apply: the image pulls failed, the anchor never came up, no node could enrol,
and the instance was left a bare shell — VMs and a registry, no substrate. The
identical apply, run by hand once the registry was warm, succeeded immediately.

`raiseRegistry` proves the wrong thing. It curls `localhost:5000` from inside
the registry's OWN machine, which says the registry process is up and holds the
blobs, and says nothing about the path anybody else uses: across a segment, and
for the home nodes through a NAT gateway whose default route and firewall are
applied two steps LATER. So "serving" was reported on evidence that excluded the
network, and the caller — which pins every image in the substrate bundle to that
registry — was handed a fact it could not rely on.

So the check moves to where it means something. After the routes and the
firewalls, before the minutes spent placing, each machine is asked for `/v2/` and
for one stocked manifest BY DIGEST, at the address it will pin, over the network
it will use. That is the pair of requests a pull begins with, from the same
place. Layers are not fetched: every digest was already read back inside the
registry machine, so what is in question here is the path, not the content.

Verified by typecheck and the unit suite (136 pass), and by confirming against a
standing four-node instance that `curl` exists in the machines and that both
segments — including a home node through the gateway — answer 200 for the
registry's `/v2/`. The ordering itself is unverified in a live raise from cold,
which takes hours.
This commit is contained in:
2026-09-10 21:05:52 +02:00
parent d9bf178546
commit 2f4cb871d9
2 changed files with 82 additions and 1 deletions
+16 -1
View File
@@ -24,7 +24,7 @@ import { planRouters, raiseRouters, raiseTransit } from "./router.ts";
import { applyHostFirewalls } from "./firewall.ts";
import { IMAGE_PREFIX, BASE_IMAGE_ALIAS, BASE_IMAGE_HOWTO, planPlacements, applyPlacements } from "./place.ts";
import { baseImageExists, UPSTREAM_IMAGE } from "./base.ts";
import { discardStock, raiseRegistry, stockRegistry } from "./registry.ts";
import { confirmRegistryServes, discardStock, raiseRegistry, stockRegistry } from "./registry.ts";
import { log as record } from "../log.ts";
/** Drivers whose snapshots are copy-on-write. On `dir` a snapshot is a full copy. */
@@ -340,6 +340,21 @@ export async function raise(
enter("applying host firewalls");
await applyHostFirewalls(scenario, byMachine, log);
// **Only now can "the registry is serving" be said truthfully.** Raising it proved the
// registry answers on its own machine; a machine pulls across a segment, and the home nodes
// pull through a gateway whose route and firewall were applied in the two steps above. So the
// path is checked here, where it is finally the one a pull will take — and before `placing`,
// which is minutes of work that a machine unable to fetch an image cannot use.
//
// The alternative is what happened: `raise` returned, the caller applied a substrate whose
// every image is pinned to this registry, the first pull failed, no node enrolled, and the
// instance was left a bare shell. A raise that reports success owes the next step the fact it
// depends on.
if (registry) {
enter("confirming the registry serves the machines");
await confirmRegistryServes(registry, [...byMachine.values()], stock, log);
}
// Last, and only once the underlay is real. Placing before the machines can reach each
// other would test the host against a network the scenario does not describe.
enter("placing");