Automate the lab registry: a sealed machine pulls by digest
Closes 04-ISSUES/009. A scenario declares `images:` by tag; the lab stocks a
registry on this workstation where there is a network, raises it inside the
scenario as scenery, and reports the references a declaration pins -- which are
the digests THIS registry assigned, and are not knowable until it is raised.
Verified in a sealed machine, confirmed by ping to have no route out: package,
service including boot state, a container pinned by digest, and an action
inside that container. Applied, idempotent on re-apply, and read back from the
machine rather than from the apply's own report. That is the first time the
container shape has worked in the lab at all, and it was the shape blocking the
substrate bootstrap.
Four faults found by running it, three of them mine and one worth keeping:
The read-back checked that the catalog endpoint answered, by looking for the
substring "repositories" -- which `{"repositories":[]}` also contains. So it
passed on a registry holding nothing, and the failure surfaced much later as a
container that could not be pulled. It now asks for each image's manifest BY
DIGEST, which is what a machine does.
A recursive push needs its destination to exist, or incus copies the source's
contents rather than the source. The data landed one directory too shallow and
the registry found nothing where it looks.
The registry writes its blobs as root through a bind mount, so the workstation
could not remove its own scratch directory afterwards. Whoever made the files
removes them -- the cleanup now runs in a container too. And a cleanup failure
no longer fails a raise that succeeded: the scenario is standing and usable,
and saying otherwise would be a false report.
The base image build did not verify that the runtime trusts the documentation
ranges as plain-HTTP registries. Writing the file is not the daemon honouring
it, and a base image that looks right fails much later, in a sealed scenario,
a long way from its cause. It is now read back from `docker info`.
This commit is contained in:
+32
-4
@@ -24,6 +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";
|
||||
|
||||
/** Drivers whose snapshots are copy-on-write. On `dir` a snapshot is a full copy. */
|
||||
const COW_DRIVERS = ["btrfs", "zfs"];
|
||||
@@ -44,6 +45,13 @@ export interface RaisedScenario {
|
||||
machines: string[];
|
||||
networks: string[];
|
||||
pool: string;
|
||||
/**
|
||||
* Images the scenario's registry serves, as references a declaration can pin.
|
||||
*
|
||||
* Reported rather than declared, because the digest is the one this registry assigned and
|
||||
* is not knowable before it was raised.
|
||||
*/
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export class RaiseError extends Error {
|
||||
@@ -171,9 +179,10 @@ export async function raise(
|
||||
// A scenario that places a runtime or an image needs machines built from the base image,
|
||||
// because a sealed machine cannot install one (novox/hq ADR 0006). Chosen here rather than
|
||||
// declared, so a scenario says WHAT it needs and not which image provides it.
|
||||
const needsRuntime = planPlacements(scenario).some(({ artifacts }) =>
|
||||
artifacts.some((a) => a === "runtime" || a.startsWith(IMAGE_PREFIX))
|
||||
);
|
||||
const needsRuntime = (scenario.images ?? []).length > 0 ||
|
||||
planPlacements(scenario).some(({ artifacts }) =>
|
||||
artifacts.some((a) => a === "runtime" || a.startsWith(IMAGE_PREFIX))
|
||||
);
|
||||
if (needsRuntime && !options.image && !(await baseImageExists())) {
|
||||
throw new Error(
|
||||
`this scenario needs a container runtime inside its machines, and '${BASE_IMAGE_ALIAS}' ` +
|
||||
@@ -231,6 +240,24 @@ export async function raise(
|
||||
const routers = await raiseRouters(scenario, instanceId, planRouters(scenario, instanceId), log);
|
||||
if (transit) routers.push(transit);
|
||||
|
||||
// Stocked on this workstation, where there is a network, and served from inside the
|
||||
// scenario, where there is not (novox/hq 04-ISSUES/009).
|
||||
step = "stocking the registry";
|
||||
const stock = await stockRegistry(scenario.images ?? [], log);
|
||||
let registry: Awaited<ReturnType<typeof raiseRegistry>> = null;
|
||||
try {
|
||||
step = "raising the registry";
|
||||
registry = await raiseRegistry(scenario, instanceId, stock, log);
|
||||
} finally {
|
||||
// Cleaning up scratch must not fail a raise that succeeded. The scenario is standing
|
||||
// and usable; a directory left behind is untidy, and saying so is the honest report.
|
||||
try {
|
||||
await discardStock(stock);
|
||||
} catch (err) {
|
||||
log(` (could not remove the registry's scratch directory: ${(err as Error).message})`);
|
||||
}
|
||||
}
|
||||
|
||||
step = "routing machines through their gateways";
|
||||
await applyDefaultRoutes(scenario, byMachine, log);
|
||||
|
||||
@@ -247,7 +274,8 @@ export async function raise(
|
||||
return {
|
||||
instanceId,
|
||||
scenario: scenario.scenario,
|
||||
machines: [...created, ...routers],
|
||||
images: registry?.pinned ?? [],
|
||||
machines: [...created, ...routers, ...(registry ? [registry.machine] : [])],
|
||||
networks,
|
||||
pool,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user