lab: name the base image's age, refuse a stale one, and rebuild it (issue 698)
mesh/merge-gate head ff9d3ee6a06c merged into trunk ebf7f3773dc7 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head ff9d3ee6a06c merged into trunk ebf7f3773dc7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head ff9d3ee6a06c merged into trunk ebf7f3773dc7 — pass: builds lab → shanks; no bus step; every machine composes with the change as it did…
mesh/repo-check head ff9d3ee6a06c merged into trunk ebf7f3773dc7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
A bed machine carries the package database its base image was built with, and the mirrors stop serving it within weeks: genesis-keeps-its-bus failed on a 404 from an image 44 days old, with nothing saying its age and no mesh tool to rebuild it. lab_check and lab_run now say the age, lab_run refuses an image older than 14 days and names the remedy, and lab_base reads the image and rebuilds it with mesh-lab's base build when asked.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
// The lab's base image: how old it is, and rebuilding it (novox/hq issue 698).
|
||||
//
|
||||
// Every bed machine that needs a container runtime is a clone of one base image, `mesh-lab/base`, which
|
||||
// mesh-lab's `base build` makes from the distribution's current image. A machine raised from it carries the
|
||||
// package database the image was built with. On a rolling distribution the mirrors stop serving what an old
|
||||
// database names within weeks, and a bed that installs a package then fails on a 404 that reads as a mirror
|
||||
// outage (novox/hq issue 205; seen 2026-10-14 when genesis-keeps-its-bus installed postgresql-libs from a
|
||||
// database six weeks old). So the lab names the image's age wherever it is asked, refuses a run on an image
|
||||
// older than the bound, and rebuilds the image when asked to.
|
||||
//
|
||||
// **The bound is 14 days** (BASE_MAX_AGE_DAYS). Arch's mirrors keep a package version for as long as it is
|
||||
// current and drop it soon after it is replaced; an image a fortnight old has reliably installed what beds
|
||||
// install, and the one that failed was 44 days old. A rebuild takes minutes, so a short bound costs little;
|
||||
// a run refused is cheaper than a bed that fails an hour in for a reason that is not the change under test.
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
/** The image every bed machine with a runtime is raised from, as mesh-lab names it (src/lifecycle/place.ts). */
|
||||
export const BASE_IMAGE_ALIAS = "mesh-lab/base";
|
||||
|
||||
/** How old the base image may be, in days, before a run is refused (see the head of this file). */
|
||||
export const BASE_MAX_AGE_DAYS = 14;
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
export interface BaseImage {
|
||||
alias: string;
|
||||
/** Whether the image is there at all. */
|
||||
exists: boolean;
|
||||
/** When this machine made it, ISO. */
|
||||
built?: string;
|
||||
/** Its age in whole days. */
|
||||
ageDays?: number;
|
||||
/** Why its age could not be read, when it could not. */
|
||||
problem?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* baseImageFrom reads `incus image list <alias> --format json`: the image whose aliases hold the base's
|
||||
* name, and when it was made here (its upload, which a publish sets).
|
||||
*/
|
||||
export function baseImageFrom(listed: string | undefined, now: Date): BaseImage {
|
||||
const alias = BASE_IMAGE_ALIAS;
|
||||
if (listed === undefined) return { alias, exists: false, problem: "incus could not list the lab's images" };
|
||||
let images: Array<{ aliases?: Array<{ name?: string }>; uploaded_at?: string; created_at?: string }>;
|
||||
try {
|
||||
images = JSON.parse(listed);
|
||||
} catch {
|
||||
return { alias, exists: false, problem: "incus listed the lab's images as something that is not JSON" };
|
||||
}
|
||||
const image = (Array.isArray(images) ? images : []).find((i) => (i.aliases ?? []).some((a) => a.name === alias));
|
||||
if (!image) return { alias, exists: false };
|
||||
const made = [image.uploaded_at, image.created_at].map((t) => (t ? Date.parse(t) : NaN))
|
||||
.find((t) => Number.isFinite(t) && t > 0);
|
||||
if (made === undefined) return { alias, exists: true, problem: "incus gives no time the image was made" };
|
||||
return { alias, exists: true, built: new Date(made).toISOString(), ageDays: Math.floor((now.getTime() - made) / DAY) };
|
||||
}
|
||||
|
||||
/** readBaseImage asks incus, on this machine, for the base image. */
|
||||
export function readBaseImage(now = new Date()): BaseImage {
|
||||
const listed = spawnSync("incus", ["image", "list", BASE_IMAGE_ALIAS, "--format", "json"], { encoding: "utf8" });
|
||||
return baseImageFrom(listed.status === 0 ? listed.stdout : undefined, now);
|
||||
}
|
||||
|
||||
/** The base image's age in one sentence, as lab_check and lab_run say it. */
|
||||
export function describeBase(b: BaseImage): string {
|
||||
if (b.problem) return `the base image ${b.alias}: its age is not known: ${b.problem}`;
|
||||
if (!b.exists) return `the base image ${b.alias} is not on this machine; rebuild it with lab_base {"rebuild": true}`;
|
||||
return `the base image ${b.alias} was built ${b.built} (${b.ageDays} day(s) ago; the bound is ${BASE_MAX_AGE_DAYS})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* staleBase is why a run is refused for its base image, or undefined when it is not: an image older than the
|
||||
* bound. An image that is missing or whose age cannot be read is not refused here — a bed that needs it says
|
||||
* so itself, and a bed that does not need it runs.
|
||||
*/
|
||||
export function staleBase(b: BaseImage): string | undefined {
|
||||
if (!b.exists || b.ageDays === undefined || b.ageDays <= BASE_MAX_AGE_DAYS) return undefined;
|
||||
return `the base image ${b.alias} was built ${b.built}, ${b.ageDays} days ago, older than the lab's bound of ` +
|
||||
`${BASE_MAX_AGE_DAYS} days: a bed machine raised from it carries a package database the mirrors no longer serve ` +
|
||||
`(novox/hq issues 205, 698). Rebuild it with lab_base {"rebuild": true}, then run again`;
|
||||
}
|
||||
|
||||
/**
|
||||
* runRefusal is why lab_run starts no run for the base image, or undefined: a rebuild still going, which
|
||||
* replaces the image a bed raises its machines from, or an image older than the bound.
|
||||
*/
|
||||
export function runRefusal(base: BaseImage, rebuilding?: { id: string; started: string }): string | undefined {
|
||||
if (rebuilding) return `the base image is being rebuilt (${rebuilding.id}, since ${rebuilding.started}); run once lab_base says it is built`;
|
||||
return staleBase(base);
|
||||
}
|
||||
Reference in New Issue
Block a user