Files
mesh-host/internal/bootstrap/module.go
T
jschoubben 5223169226 Genesis registers the control plane with the manifest its build produced
The control plane's manifest existed twice: at the root of its repository, read
whenever the mesh rebuilds it from source, and as a copy in the catalogue, read by
genesis. Nothing kept them equal, and the first rebuild replaced the mesh's record
with the repository's shape while every later push was refused (novox/hq
04-ISSUES/072). The builder's one-shot result already carries the manifest it built,
artifact resolved to the image; step 3 keeps it and step 9 registers it, re-pinning
the built image's bare id to the reference the registry assigned. The catalogue is
still read for the registry's and the builder's manifests and for phase two.
2026-09-21 15:17:47 +02:00

212 lines
9.2 KiB
Go

package bootstrap
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
// placeholderDigest is what the catalogue writes where a built image's digest will go.
//
// Sixty-four zeros. A manifest in the catalogue names an image the mesh builds and pushes, and
// until that has happened there is no digest to name — so the convention is a digest that is
// obviously not one, replaced by the pipeline when it publishes. The installer meets it twice: it
// must NOT be there in the registry's manifest, whose image is upstream and never built
// (novox/hq 04-ISSUES/029), and it MUST be there in the control plane's, which is the image
// step 8 has just pushed.
const placeholderDigest = "sha256:" + "0000000000000000000000000000000000000000000000000000000000000000"
// catalogueDir is where a mesh-catalog checkout keeps its manifests.
const catalogueDir = "modules"
// manifestFile is the path a module's manifest is read from, given a catalogue checkout.
func manifestFile(catalogue, module string) string {
return filepath.Join(catalogue, catalogueDir, module, "module.json")
}
// readManifest reads one module's manifest out of a mesh-catalog checkout.
//
// **From a checkout rather than from anything the mesh serves**, and that is the ordering the whole
// pivot exists to respect: at this point the mesh has no build machine, no forge and — until step 7
// finishes — no registry. What a manifest is, is a file; the installer is handed the directory it
// is in and reads it.
func readManifest(catalogue, module string) ([]byte, error) {
path := manifestFile(catalogue, module)
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf(
"the %s module's manifest could not be read: %w\n"+
"--catalog is a checkout of the mesh's catalogue repository, and this is read from "+
"%s inside it", module, err, filepath.Join(catalogueDir, module, "module.json"))
}
return raw, nil
}
// Installed is what installing one module did.
type Installed struct {
// Module is its name.
Module string
// Known is true when the mesh already had this module in its catalogue. The manifest is
// registered either way — a re-run with a changed manifest must land — so this reports what
// was found rather than what was skipped.
Known bool
// Assigned is true when this node was given the module during this run.
Assigned bool
// Pushed is what the mesh said when it sent this node its declaration.
Pushed string
}
// installModule registers a manifest, gives it to this node, and sends it.
//
// The three verbs a person types, in the order they type them, through the same commands. There is
// no installer-only path into the mesh: everything here is `module add`, `assign` and `push`, so
// what the installer does on a bare machine and what an operator does on a running mesh are the
// same act (novox/hq ADR 0035, one refusal per act however it is asked for).
func installModule(ctx context.Context, o Options, control controlPlane, module string,
manifest []byte, say func(string)) (Installed, error) {
out, err := registerAndAssign(ctx, o, control, module, manifest, say)
if err != nil {
return out, err
}
out.Pushed, err = pushNode(ctx, o, control, say)
return out, err
}
// registerAndAssign is the half of installing that happens before anything is sent.
//
// Split out because one module needs something in between: the control plane's own store
// connections have to be accepted before its declaration is composed, or the mesh would seal
// thirty-two random bytes into the file it expects a connection string in and the container would
// come up unable to open anything (mesh-controller's `secret accept`, and what it exists for).
//
// **`module add` is run every time and is not skipped when the module is already known.** It is an
// upsert on the manifest, and the manifest is exactly what changes between runs — step 9 registers
// the control plane with a digest that did not exist the first time. Skipping it because the name
// was already in the catalogue would silently pin the mesh to the previous image.
func registerAndAssign(ctx context.Context, o Options, control controlPlane, module string,
manifest []byte, say func(string)) (Installed, error) {
out := Installed{Module: module}
known, err := control.tell(ctx, "module", "list")
if err != nil {
return out, err
}
out.Known = mentions(known, module)
remote := "/" + module + "-module.json"
if err := control.carrying(ctx, module+"-module.json", manifest, remote); err != nil {
return out, err
}
if _, err := control.tell(ctx, "module", "add", remote); err != nil {
return out, err
}
if out.Known {
say(" registered " + module + " — the mesh already knew it; the manifest is now this one")
} else {
say(" registered " + module)
}
assigned, err := control.tell(ctx, "assign", o.Node, module)
if err != nil {
return out, err
}
out.Assigned = true
say(" assigned " + module + " to " + o.Node)
if refusal := strings.TrimSpace(assigned); strings.Contains(refusal, "but ") {
// `assign` records what a person meant and says at once when the machine cannot host it.
// Repeated rather than swallowed: the push below will apply everything else and this is
// the only place the reason appears.
say(indent(refusal))
}
return out, nil
}
// pushNode sends this node everything it should be.
//
// Given the long wait rather than the probe timeout: a push composes every declaration this node
// should hold, seals every secret in them and publishes them, and on a first node that is the
// slowest thing the mesh does.
func pushNode(ctx context.Context, o Options, control controlPlane, say func(string)) (string, error) {
said, err := control.within(o.Wait).tell(ctx, "push", o.Node)
if err != nil {
return "", fmt.Errorf(
"%w\n\nWhat was registered and assigned is registered and assigned, and this node has "+
"not been sent it. Nothing is half-applied — a push the mesh refused sent nothing "+
"at all. Fix what it named and run this installer again: it will find the module "+
"already registered and try the push again", err)
}
say(" pushed " + o.Node)
return strings.TrimSpace(said), nil
}
// pinPlaceholder replaces the catalogue's placeholder digest with what the registry assigned — the
// builder's manifest, which the catalogue still holds (the control plane's comes out of its own
// build, see pinImage).
//
// **Textual, and every place it appears.** A manifest may name its image in more than one resource
// — the catalogue's converted modules routinely carry a runtime container beside the application's
// — and the same reasoning as the bundle rewrite applies: replacing one and not the others leaves
// something pointing at an image nothing serves, and it fails half way through an apply rather
// than here.
//
// It refuses a manifest with no placeholder in it. That is not pedantry: a manifest already
// carrying a real digest is one somebody pinned by hand, and quietly registering it would install a
// control plane that is not the image this machine just published — which is the one thing this
// step exists to guarantee.
func pinPlaceholder(manifest []byte, reference, module string) ([]byte, int, error) {
places := bytes.Count(manifest, []byte(placeholderDigest))
if places == 0 {
return nil, 0, fmt.Errorf(
"the %s module's manifest carries no placeholder digest (%s), so there is nothing to "+
"pin to the image this machine just published.\n"+
"A manifest already naming a digest was pinned by somebody else, to some other "+
"build. Registering it would install a module that is not the one this "+
"installer carried and pushed", module, placeholderDigest)
}
// The reference the registry gave back is `<registry>/<repository>@sha256:…`, and what the
// manifest holds is `<something>@sha256:0…0`. Replacing only the digest would leave the
// manifest's own repository name in front of it — which may be `mesh-controller` with no
// registry, and a runtime would then pull it from the internet. The whole reference moves.
var out bytes.Buffer
rest := manifest
for {
at := bytes.Index(rest, []byte(placeholderDigest))
if at < 0 {
out.Write(rest)
break
}
// Back up over the repository this digest belongs to, which runs to the opening quote.
start := bytes.LastIndexByte(rest[:at], '"')
if start < 0 {
return nil, 0, fmt.Errorf(
"the %s module's manifest has a placeholder digest that is not inside a JSON "+
"string, so the installer cannot tell what image it belongs to", module)
}
out.Write(rest[:start+1])
out.WriteString(reference)
rest = rest[at+len(placeholderDigest):]
}
pinned := out.Bytes()
// Read back. A substitution on text can catch more than it was aimed at, and the manifest is
// about to be handed to the mesh as the description of what it runs.
var checked map[string]any
if err := json.Unmarshal(pinned, &checked); err != nil {
return nil, 0, fmt.Errorf(
"pinning the %s module's image broke its manifest: %w", module, err)
}
if bytes.Contains(pinned, []byte(placeholderDigest)) {
return nil, 0, fmt.Errorf(
"the %s module's manifest still carries a placeholder digest after pinning",
module)
}
return pinned, places, nil
}