// 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=` in the mesh-host repository, where " + "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 … `", 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 }