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
120 lines
5.1 KiB
Go
120 lines
5.1 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// controlPlane is the running control plane, asked things.
|
|
//
|
|
// **Through `docker exec`, not over a network.** The control plane listens on nothing — `serve` is
|
|
// a broker consumer, and every administrative verb is a subcommand of the same binary that opens
|
|
// the stores directly (mesh-control's own usage). So the way to tell a mesh anything, from the
|
|
// machine the mesh is on, is to run its binary inside its own container. That is also what the lab
|
|
// does, and having the installer and the lab drive the mesh identically is the point: the lab is
|
|
// meant to exercise the installer, not a second procedure that resembles it.
|
|
//
|
|
// It carries which container, because the whole pivot turns on there being two of them: the
|
|
// substrate's `temp-mesh-control` for steps 6 to 9, and the module's `mesh-control` afterwards.
|
|
type controlPlane struct {
|
|
container string
|
|
run Runner
|
|
timeout time.Duration
|
|
}
|
|
|
|
// within is the same control plane, asked with a different patience.
|
|
//
|
|
// A copy rather than a field somebody sets, so a slow command cannot leave every command after it
|
|
// slow: the caller that needs the long wait says so on the call.
|
|
func (c controlPlane) within(timeout time.Duration) controlPlane {
|
|
c.timeout = timeout
|
|
return c
|
|
}
|
|
|
|
// tell runs a mesh-control subcommand and gives back what it said.
|
|
//
|
|
// The failure carries the command AND the output. A mesh-control refusal is a paragraph explaining
|
|
// what is wrong — "nothing provides route, wanted by registry" — and an installer that reported
|
|
// only "exit status 1" would throw away the one thing a person needs.
|
|
func (c controlPlane) tell(ctx context.Context, args ...string) (string, error) {
|
|
asking, cancel := context.WithTimeout(ctx, c.timeout)
|
|
defer cancel()
|
|
|
|
out, err := c.run(asking, "docker", append(
|
|
[]string{"exec", c.container, controlPlaneBinary}, args...)...)
|
|
if err != nil {
|
|
return out, fmt.Errorf("`%s %s` was refused: %w\n%s",
|
|
c.container, strings.Join(args, " "), err, indent(strings.TrimSpace(out)))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// carry puts a file inside the control plane's container.
|
|
//
|
|
// **Because every command that takes a file reads it from its own filesystem.** `module add
|
|
// <file>` and `secret accept --from <file>` open a path, and the process doing the opening is
|
|
// inside the container. The installer is not. So the file is copied in first, exactly as the lab
|
|
// does it.
|
|
//
|
|
// The image is `FROM scratch` and has no shell, so nothing inside can move a file, change its mode
|
|
// or clean up after itself. What is copied in stays until the container is replaced — which, for
|
|
// the temporary control plane, is a container that gets removed at step 10 and takes its contents
|
|
// with it.
|
|
func (c controlPlane) carry(ctx context.Context, local, remote string) error {
|
|
asking, cancel := context.WithTimeout(ctx, c.timeout)
|
|
defer cancel()
|
|
|
|
if _, err := c.run(asking, "docker", "cp", local, c.container+":"+remote); err != nil {
|
|
return fmt.Errorf("cannot put %s into %s at %s: %w", local, c.container, remote, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// carrying writes bytes to a temporary file on the machine and copies them into the container.
|
|
//
|
|
// **0644, and that is not carelessness — 0600 would break it.** `docker cp` keeps the ownership and
|
|
// mode a file had outside, the control plane's image runs as 65534, and the process that reads
|
|
// these files is that one. The lab paid for this exactly once: a 0600 root-owned key copied in
|
|
// landed unreadable, `secret accept` failed with `permission denied`, and what depended on it
|
|
// crash-looped on material it never received. There is no shell in the image to chown it with.
|
|
//
|
|
// What goes through here is a module manifest and a store connection string. The connection is the
|
|
// same value the produced bundle already holds in the clear — a substrate names its own bootstrap
|
|
// credentials, and at genesis there is nowhere else for them to be — so this widens nothing. The
|
|
// file on the machine is removed at once, and the copy inside the container goes when the
|
|
// container does, which for the temporary control plane is step 10.
|
|
func (c controlPlane) carrying(ctx context.Context, name string, content []byte, remote string) error {
|
|
local := filepath.Join(os.TempDir(), name)
|
|
if err := os.WriteFile(local, content, 0o644); err != nil {
|
|
return fmt.Errorf("nowhere to stage %s before copying it into %s: %w", name, c.container, err)
|
|
}
|
|
defer os.Remove(local)
|
|
return c.carry(ctx, local, remote)
|
|
}
|
|
|
|
func indent(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
return " " + strings.ReplaceAll(s, "\n", "\n ")
|
|
}
|
|
|
|
// mentions reports whether one of a listing's lines starts with this exact word.
|
|
//
|
|
// Line-and-word rather than a substring search, because these listings are columns and a
|
|
// substring match would find `registry` inside `registry-mirror` and report a module installed
|
|
// that is not. Every one of mesh-control's `list` verbs prints the name first on the line.
|
|
func mentions(listing, name string) bool {
|
|
for _, line := range strings.Split(listing, "\n") {
|
|
first, _, _ := strings.Cut(strings.TrimSpace(line), " ")
|
|
if first == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|