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
197 lines
8.7 KiB
Go
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
|
|
}
|