Files
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

197 lines
8.7 KiB
Go

// Package image is the control plane's image, carried inside the installer.
//
// **Carried rather than built, and that is not an optimisation.** The mesh's forge runs on the
// mesh. A bootstrap that needed the control plane's source in order to build it would need a
// forge to fetch that source from, and the forge is one of the things the mesh raises — so the
// mesh would be required in order to raise the mesh. Embedding the image breaks that cycle the
// same way `internal/bundle` breaks it for the declaration: the installer arrives holding
// everything a bare machine has to be given, and a bare machine is given one file
// (novox/hq ADR 0005).
//
// It is also the rule the rest of tier 0 already follows. Nobody compiles `mesh-host` on the
// machine it will run on; the binary is put there. The image it raises arrives the same way.
//
// What is embedded is the output of `docker save` — a tar holding the image's config, its layers
// and a `manifest.json` naming them. The control plane's image is `FROM scratch` with one static
// binary in it (novox/hq ADR 0006), so this is tens of megabytes rather than hundreds.
package image
import (
"archive/tar"
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"path"
"strings"
)
// The saved image, replaced at release time by `make bootstrap`.
//
// **It is the builder, not the control plane** (novox/hq ADR 0073). The installer used to carry
// the thing it was going to run; it now carries the thing that makes it. One artifact either way —
// but a mesh raised by the second one holds a control plane it built from source, out of the same
// repository and path every later rebuild of it will use, and can therefore rebuild it. A mesh
// raised by the first held an artifact it could not reproduce and knew nothing about.
//
// What is committed here is a placeholder, for the same reason `internal/bundle` commits locks
// that are only comments: `go:embed` refuses to compile against a file that is not there, so a
// checkout with nothing embedded would not build at all — and someone reading this repository or
// running `go test ./...` would meet a compile error instead of a program. The placeholder keeps
// the tree buildable and makes the absence a thing the installer *says*, at the earliest moment
// it can, rather than a thing a compiler says to the wrong person.
//
// It is small and it is committed. A saved image is not, and `make bootstrap` puts one here for
// the length of one build and then puts the placeholder back — exactly what `make host` does with
// the bundle it embeds.
//
//go:embed builder.tar
var saved []byte
// ErrEmpty means this installer carries no builder image.
//
// A separate error rather than a message, so the caller can refuse in preflight — before a
// machine has been touched — instead of discovering it at the load, after the runtime has been
// probed and a bundle has been written.
var ErrEmpty = errors.New(
"this mesh-bootstrap carries no builder image, so it cannot raise a mesh. A release build " +
"embeds one: `make bootstrap IMAGE=<image>` in the mesh-host repository, where <image> " +
"is a mesh-builder image already built from the mesh-controller source")
// IsEmpty reports whether anything was built in.
//
// It asks whether the bytes are a tar rather than comparing them against the placeholder's text,
// because the question that matters is "can this be loaded", and a truncated or corrupted embed
// answers no to that while matching no placeholder. A tar's first header carries the string
// `ustar` at offset 257 and nothing else does by accident.
func IsEmpty() bool {
const magicAt, magic = 257, "ustar"
return len(saved) < magicAt+len(magic) || string(saved[magicAt:magicAt+len(magic)]) != magic
}
// Saved returns the embedded tar, for loading into a container runtime.
func Saved() ([]byte, error) {
if IsEmpty() {
return nil, ErrEmpty
}
return saved, nil
}
// manifestEntry is the part of a saved image's `manifest.json` this needs.
//
// One field. The layers are the runtime's business and the repository tags are decoration — what
// is wanted is the config, because the digest of the config IS the image id.
type manifestEntry struct {
Config string `json:"Config"`
RepoTags []string `json:"RepoTags"`
}
// ArchiveID is what this archive calls the image it holds. **It is not necessarily the id the
// runtime will assign when the archive is loaded, and it must never be what a bundle names.**
//
// This was called ID, and its comment said it was the id the runtime would give the image once
// loaded. That was wrong, and wrong in the worst available way: right on the machine the image
// was saved on, and wrong on the machine it was carried to. 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 and an older one stores it in another. Same layers, same program,
// different name. Measured on a live raise:
//
// saved on the workstation sha256:b86bb81ca2f9691f24f4725f50962d1e49c98c5ffe211113241243d42d18ceea
// loaded on the machine sha256:2dc219046c73702fc640317f0342a28ec962ef1e9ef547b2f02861c508ca78fb
//
// A bundle naming this id would then name an image the machine does not hold — and nothing serves
// an image named by the digest of its own configuration, which is the whole point of naming one
// that way, so the apply would stop at a pull that cannot succeed. The id a bundle names is read
// back from the runtime after the load (`internal/bootstrap`.Load), which is the only place it is
// a fact rather than a prediction.
//
// What it is still good for is a statement about the FILE — which build somebody embedded — and
// as a hint printed beside the runtime's answer when the two differ, so a person can see that
// they did.
//
// `manifest.json` names the configuration document by its digest — as `<64hex>.json` in the older
// layout and `blobs/sha256/<64hex>` in the OCI one — so both forms reduce to the same sixty-four
// characters.
func ArchiveID(saved []byte) (string, error) {
manifest, err := fileFromTar(saved, "manifest.json")
if err != nil {
return "", err
}
var entries []manifestEntry
if err := json.Unmarshal(manifest, &entries); err != nil {
return "", fmt.Errorf(
"the carried image has a manifest.json this cannot read, so the image it holds "+
"cannot be named: %w", err)
}
// One image, deliberately. A tar holding several would leave the installer choosing which
// one is the control plane, and a bootstrap must not be the thing that guesses.
if len(entries) != 1 {
return "", fmt.Errorf(
"the carried image holds %d images, and the installer raises exactly one control "+
"plane. Save a single image: `docker save --output … <image>`", len(entries))
}
digest := strings.TrimSuffix(path.Base(entries[0].Config), ".json")
if !isSHA256(digest) {
return "", fmt.Errorf(
"the carried image names its configuration %q, which is not a sha256 digest. An "+
"image id is the digest of that configuration, so there is nothing to call this "+
"image", entries[0].Config)
}
return "sha256:" + digest, nil
}
// Tags is what the saved image was called when it was saved.
//
// **Not decoration any more, and this comment used to say it was.** A tag is exactly what a pinned
// bundle may not rely on (novox/hq ADR 0006) and none of this ever reaches a bundle — but the tag
// is how the installer ASKS the runtime what id it assigned, because the id itself is not
// knowable beforehand (see ArchiveID). It is the one name that survives `docker save` and
// `docker load` unchanged, which is precisely what the id does not.
//
// It also still says which build somebody embedded, which is otherwise sixty-four hex characters.
func Tags(saved []byte) []string {
manifest, err := fileFromTar(saved, "manifest.json")
if err != nil {
return nil
}
var entries []manifestEntry
if err := json.Unmarshal(manifest, &entries); err != nil || len(entries) == 0 {
return nil
}
return entries[0].RepoTags
}
func fileFromTar(archive []byte, want string) ([]byte, error) {
reader := tar.NewReader(bytes.NewReader(archive))
for {
header, err := reader.Next()
if errors.Is(err, io.EOF) {
return nil, fmt.Errorf(
"the carried image has no %s, so it is not something `docker save` produced. "+
"Embed the output of `docker save`, not a layer or a build context", want)
}
if err != nil {
return nil, fmt.Errorf("the carried image cannot be read as a tar: %w", err)
}
if path.Clean(header.Name) == want {
return io.ReadAll(reader)
}
}
}
func isSHA256(s string) bool {
if len(s) != 64 {
return false
}
for _, c := range s {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
return false
}
}
return true
}