Steps 6 to 10, which turn a substrate into a mesh that can maintain itself
(novox/hq ADR 0067).
6 enrol a node record, a token, `mesh-host enrol`, and the host agent
running. Proved by the mesh having HEARD from the node, not by a
process existing: a host that cannot reach the broker looks exactly
like a successful install until the first push applies nothing.
7 registry the module that gives this mesh an image store, registered from a
--catalog checkout, assigned and pushed. Its image is upstream and
never built (04-ISSUES/029) — a placeholder digest there is refused.
Verified by asking `/v2/`, because a container that is up is not a
registry that serves.
8 publish the carried image pushed into that registry, which assigns it the
first manifest digest it has ever had. This is the hinge: without
it the mesh works and can never upgrade itself.
9 control the control plane registered as an ordinary module pinned to that
digest, with the substrate's own store connections delivered
through `secret accept` — read out of the bundle that made them,
because the mesh cannot invent a credential that predates it.
10 retire the temporary control plane dropped from the bundle and removed by
the host's ordinary removal pass.
Every step asks before it acts and reports "already done". No step leaves the
machine without a control plane: steps 9 and 10 overlap deliberately, and two
stateless control planes are untidy rather than broken.
mesh-control's `internal/builder`.PublishImage is mirrored rather than imported —
tier 0 depends on nothing that must be installed first — with one correction: the
digest is chosen from RepoDigests by repository instead of taken as element zero,
so an image pushed to two registries cannot silently pin this mesh to the wrong
one.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
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-control'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
|
|
}
|