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
146 lines
6.1 KiB
Go
146 lines
6.1 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"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
|
|
}
|