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.
212 lines
9.2 KiB
Go
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
|
|
}
|