Files
mesh-host/internal/bootstrap/load.go
jschoubben 121367319d Rename mesh-control -> mesh-controller, substrate -> foundation
One name per thing, per the HQ glossary: the module/container/image/binary/repo
becomes mesh-controller, the seat the-controller, and the store+broker pair the
foundation (embedded base bundles, default template and example lock renamed with
their go:embed directives). No behaviour change — a pure vocabulary rename.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 18:40:40 +02:00

200 lines
9.2 KiB
Go

package bootstrap
import (
"context"
"fmt"
"os"
"strings"
"github.com/novox/mesh-host/internal/image"
)
// Loaded is the control plane's image on this machine.
type Loaded struct {
// ID is what THIS RUNTIME holds the image as, read back from it after the load. It is what the
// bundle names, and outside a dry run it is never a prediction — see Load.
ID string
// Archive is what the carried tar calls the same image. Kept because the two differ in
// practice, and a report showing only one of them cannot say that they did. Never what the
// bundle names.
Archive string
// Tag is the name the runtime is asked by. Load-bearing rather than decoration: it is the one
// name that survives `docker save` and `docker load` unchanged.
Tag string
// Tags is everything the archive was called when it was saved.
Tags []string
// Held is true when the machine already held it and nothing moved.
Held bool
// Predicted is true only on a dry run, where nothing was loaded and ID is therefore the
// archive's id — which is not necessarily the one this machine would end up with.
Predicted bool
}
// Load puts the carried builder image into this machine's container runtime, and reports
// what the runtime decided to call it.
//
// **The digest of a configuration is not portable across runtimes, and that is why the id is read
// back rather than predicted.** An image id is the sha256 of the image's configuration document,
// and a runtime REWRITES that document as it loads: a newer Docker saves in one format, an older
// one stores it in another, and the same layers come out under a different name. Measured on a
// live raise, an image saved as `sha256:b86bb81c…` on a workstation was loaded as
// `sha256:2dc21904…` on the machine it was carried to.
//
// This code used to read the id out of the tar before the runtime was asked anything and use it
// for both idempotence and the bundle. That is right on the machine the image was built on and
// wrong on every machine it is carried to — which is every machine this program exists for. The
// bundle would have named an image the machine does not hold; nothing serves an image named by
// the digest of its own configuration, which is the whole point of naming one that way; and the
// apply would have stopped inside a pull that cannot succeed. The lab hit exactly this.
//
// **So the image is identified by its TAG.** A tag is ordinary metadata the tar carries through
// unchanged, and asking the runtime what a tag resolves to is asking the only party entitled to
// answer. The tag never reaches the bundle — a pinned bundle may not rely on one
// (novox/hq ADR 0006) — it is how the id is obtained, not what is written down.
//
// **Idempotence is decided from what the runtime holds.** The tag is asked before the load and
// again after: the same id either side means nothing moved, which is a fact about this machine
// rather than a guess about the file. A tag that already resolves means the image is already
// held, and nothing is loaded at all.
//
// It reads back (novox/hq ADR 0018). A load that reported success and left nothing there is a
// failure, not a convergence.
func Load(ctx context.Context, run Runner, dryRun bool, say func(string)) (Loaded, error) {
saved, err := image.Saved()
if err != nil {
return Loaded{}, err
}
return loadImage(ctx, run, saved, dryRun, say)
}
// loadImage is Load with the carried bytes handed in, so the whole path can be tested against a
// saved image a test builds rather than against whatever a particular build embedded.
func loadImage(ctx context.Context, run Runner, saved []byte, dryRun bool, say func(string)) (Loaded, error) {
archiveID, err := image.ArchiveID(saved)
if err != nil {
return Loaded{}, err
}
loaded := Loaded{Archive: archiveID, Tags: image.Tags(saved)}
// **An untagged archive is a build-time fault, refused here rather than worked around.**
// Without a tag there is no portable name to ask the runtime about, and the only thing left is
// scraping the sentence `docker load` prints for a person — which differs between runtime
// versions and is exactly the kind of guess this whole step exists to stop making. The release
// target tags the image; an installer built without one was built wrong.
loaded.Tag = firstOr(loaded.Tags, "")
if loaded.Tag == "" {
return loaded, fmt.Errorf(
"the carried control-plane image has no tag, so there is no portable name to ask this "+
"machine's runtime what id it gave it.\n"+
"An image id is the digest of the image's configuration and a runtime rewrites "+
"that as it loads, so the id in the archive (%s) is not necessarily the id this "+
"machine will hold — and a bundle naming the wrong one names an image nothing "+
"serves. Rebuild the installer with a tagged image: `make bootstrap "+
"IMAGE=<name>:<tag>`", archiveID)
}
// Already held? Asked of the runtime, by the tag, before anything is written anywhere.
before, err := idOfImage(ctx, run, loaded.Tag)
if err != nil {
return loaded, err
}
if before != "" {
loaded.ID, loaded.Held = before, true
say(" already held " + before + " as " + loaded.Tag + " — nothing loaded")
sayIfDifferent(say, archiveID, before)
return loaded, nil
}
if dryRun {
// Nothing is loaded, so the runtime has not been asked to decide anything — and what it
// would decide cannot be worked out from here. Said, rather than quietly guessed at.
loaded.ID, loaded.Predicted = archiveID, true
say(fmt.Sprintf(" would load %s (%d bytes) as %s", archiveID, len(saved), loaded.Tag))
say(" NOT THE FINAL ID a runtime rewrites an image's configuration as it loads, and an")
say(" id is that configuration's digest. The bundle names what the")
say(" runtime answers for " + loaded.Tag + " afterwards, which a dry run")
say(" cannot ask for without loading.")
return loaded, nil
}
// Through a file rather than through stdin: the runner this repository shares runs a command
// and captures its output, and giving it a second mouth for one caller would change every
// applier's contract for the sake of one step (internal/apply's Runner).
tarball, err := os.CreateTemp("", "mesh-controller-*.tar")
if err != nil {
return loaded, fmt.Errorf("nowhere to put the carried image while loading it: %w", err)
}
defer os.Remove(tarball.Name())
if _, err := tarball.Write(saved); err != nil {
tarball.Close()
return loaded, fmt.Errorf("cannot write the carried image to %s: %w", tarball.Name(), err)
}
if err := tarball.Close(); err != nil {
return loaded, fmt.Errorf("cannot finish writing %s: %w", tarball.Name(), err)
}
if _, err := run(ctx, "docker", "load", "--input", tarball.Name()); err != nil {
return loaded, fmt.Errorf(
"the container runtime would not load the carried control-plane image: %w", err)
}
// Read back, and THIS is the answer the bundle is rewritten to.
after, err := idOfImage(ctx, run, loaded.Tag)
if err != nil {
return loaded, err
}
if after == "" {
return loaded, fmt.Errorf(
"the load reported success and this machine holds nothing called %s.\n"+
"The bundle names the control plane by the id this runtime assigned, so there is "+
"nothing to name. Check what `docker load` actually took", loaded.Tag)
}
loaded.ID = after
say(" loaded " + after + " as " + loaded.Tag)
sayIfDifferent(say, archiveID, after)
return loaded, nil
}
// sayIfDifferent reports the archive's own id when the runtime chose another.
//
// Said every time it happens, because it is surprising, it is ordinary, and somebody comparing
// this report against `docker images` on the machine the image was built on would otherwise
// conclude that the wrong image had been carried.
func sayIfDifferent(say func(string), archiveID, held string) {
if archiveID == held {
return
}
say(" the archive says " + archiveID)
say(" this runtime stored the same image under a different configuration, " +
"which is ordinary — the bundle names what the machine holds")
}
// idOfImage asks the runtime what it holds under a name, or empty if it holds nothing.
//
// It asks for the id back rather than reading the exit code, because the id is what is wanted and
// an exit code is not it. Absent is an answer and not a failure: every other reason the runtime
// might refuse looks the same from here, which is why preflight proves the runtime answers before
// this runs rather than this trying to tell the two apart from an exit status.
//
// What it will not do is accept an answer that is not an image id. That answer becomes the name
// the bundle applies on a machine with no mesh to check anything against, so it is checked here
// where the refusal can say whose mistake it is.
func idOfImage(ctx context.Context, run Runner, name string) (string, error) {
out, err := run(ctx, "docker", "image", "inspect", "--format", "{{.Id}}", name)
if err != nil {
return "", nil
}
got := strings.TrimSpace(firstLineOf(out))
if got == "" {
return "", nil
}
if !isImageID(got) {
return "", fmt.Errorf(
"asked what this machine holds as %q, the runtime answered %q, which is not an image "+
"id. The bundle would name the control plane by that answer, and it is refused "+
"rather than written down", name, got)
}
return got, nil
}