Files
mesh-host/internal/bootstrap/registry.go
T
jschoubben 121367319d Rename mesh-control -> mesh-controller, substrate -> foundation
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
2026-09-16 18:40:40 +02:00

210 lines
8.0 KiB
Go

package bootstrap
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
// RegistryModule is the module that provides the mesh's artifact store.
//
// **Named for the software, not the job.** The job is `artifact-store`, which is the provision this
// module offers; the software is Distribution, the OCI reference implementation. Calling the module
// `registry` named neither — and promised that any registry could sit there, which is the false
// genericity the naming rule forbids (novox/hq ADR 0040).
const RegistryModule = "distribution"
// registryResource is the id of the resource in that module's manifest that runs the registry.
// What the container is CALLED is read from the manifest rather than assumed, because the name is
// the catalogue's to choose and the installer only has to know which resource to wait for.
const registryResource = "store"
// Registry is what step 7 did.
type Registry struct {
Installed
// Container is the container the manifest declares, confirmed running.
Container string
// Address is host:port the registry answers on, as this machine reaches it.
Address string
// Answered is the status the registry's own `/v2/` gave back.
Answered int
}
// InstallRegistry gives this mesh somewhere to put images.
//
// **Its image is upstream and it is never built.** novox/hq 04-ISSUES/029 is the whole reason this
// step exists in this position: a module that provides the artifact store cannot be delivered
// through the artifact store, so the registry is the one module whose image is pulled from the
// internet like the store and the broker before it. A manifest carrying the catalogue's
// placeholder digest here would mean somebody had made it buildable, which is the cycle again —
// so it is refused rather than pulled.
//
// **No credentials, and that is deliberate.** The registry is reached over the mesh's own private
// network, which is already the encrypted and authenticated thing; a second layer inside it would
// be certificates to issue and rotate for no property the first does not have (mesh-controller's
// `internal/builder`, which pushes to it the same way). So there is nothing here to configure and
// nothing to seal — which is also why step 8 can push without the mesh having issued anything.
//
// **It is verified by asking it, not by looking at it.** A container that is up is not a registry
// that serves: `/v2/` is the registry API's own "yes, I am one and I am ready", and it is the
// question step 8 depends on the answer to.
func InstallRegistry(ctx context.Context, o Options, d Deps, control controlPlane,
say func(string)) (Registry, error) {
out := Registry{Address: o.Registry}
manifest, err := readManifest(o.Catalogue, RegistryModule)
if err != nil {
return out, err
}
container, image, err := containerIn(manifest, registryResource)
if err != nil {
return out, err
}
if strings.Contains(image, placeholderDigest) {
return out, fmt.Errorf(
"the %s module's image is %q, which is the catalogue's placeholder for something the "+
"mesh builds and pushes.\n"+
"This module is the one that cannot work that way: it PROVIDES the place built "+
"images go, so it can never be delivered through it (novox/hq 04-ISSUES/029). Its "+
"image is upstream and pinned in the manifest", RegistryModule, image)
}
out.Container = container
say(" registry image " + image + " — upstream, never built")
installed, err := installModule(ctx, o, control, RegistryModule, manifest, say)
out.Installed = installed
if err != nil {
return out, err
}
// Read back, in two stages, because they fail differently. A container that never appears is
// a declaration that did not reach this node or an image that would not pull; a container
// that is up and does not answer is a registry that started and failed.
if err := waitForContainer(ctx, control.run, o.Timeout, o.Wait, container, say); err != nil {
return out, err
}
status, err := waitForTheRegistry(ctx, d, o, say)
if err != nil {
return out, err
}
out.Answered = status
return out, nil
}
// waitForTheRegistry asks `/v2/` until it answers.
func waitForTheRegistry(ctx context.Context, d Deps, o Options, say func(string)) (int, error) {
where := "http://" + o.Registry + "/v2/"
deadline := time.Now().Add(o.Wait)
var last string
for {
asking, cancel := context.WithTimeout(ctx, o.Timeout)
status, _, err := d.Fetch(asking, where)
cancel()
switch {
case err != nil:
last = err.Error()
case status == http.StatusOK:
say(fmt.Sprintf(" replies %s answered %d", where, status))
return status, nil
default:
// A registry that answers 401 is one that wants credentials, which this one is
// configured not to. Reported as what it said rather than retried into a timeout.
last = fmt.Sprintf("it answered %d", status)
}
if time.Now().After(deadline) {
break
}
select {
case <-ctx.Done():
return 0, ctx.Err()
case <-time.After(answerEvery):
}
}
return 0, fmt.Errorf(
"the registry's container is running and %s does not answer, after waiting %s: %s\n"+
"Running is not serving. `/v2/` is the registry API saying it is ready, and the next "+
"step pushes the control plane's image to it — so this is refused here rather than "+
"discovered inside a `docker push`. `docker logs mesh-registry` says what it did",
where, o.Wait, last)
}
// waitForContainer waits for a container the mesh was asked to create to be running.
//
// Unlike the foundation's own verify, this one waits: the mesh applies through a node's host, over
// the broker, asynchronously. A push that the control plane accepted has not yet happened on the
// machine, and refusing on the first look would refuse every correct install.
func waitForContainer(ctx context.Context, run Runner, probe, wait time.Duration, name string,
say func(string)) error {
deadline := time.Now().Add(wait)
var last string
for {
state, err := containerRunning(ctx, run, probe, name)
switch {
case err != nil:
last = "it is not there at all"
case state.running:
say(" running " + name)
return nil
default:
last = "it is " + state.status
}
if time.Now().After(deadline) {
break
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(answerEvery):
}
}
return fmt.Errorf(
"the mesh accepted the push and %q is not running after %s: %s\n"+
"The control plane sends a declaration over the broker and this node's host applies "+
"it, so the two ends fail differently: `mesh-host` on this machine says what it made "+
"of the declaration, and `status` on the control plane says whether it was collected "+
"at all", name, wait, last)
}
// containerIn finds one container resource in a module manifest and gives back its name and image.
//
// It reads the manifest as data rather than through the catalogue's own parser, because that
// parser lives in the control plane and the host depends on nothing installed first
// (novox/hq ADR 0041) — importing it would put tier 2 inside tier 0. What is read here is two
// fields of a shape the catalogue owns; the manifest is handed to the control plane unchanged, and
// it is the control plane's `module add` that judges whether it is a manifest at all.
func containerIn(manifest []byte, id string) (name, image string, err error) {
var m struct {
Module string `json:"module"`
Resources []struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Image string `json:"image"`
} `json:"resources"`
}
if err := json.Unmarshal(manifest, &m); err != nil {
return "", "", fmt.Errorf("this manifest is not readable as JSON: %w", err)
}
var containers []string
for _, r := range m.Resources {
if r.Type != "container" {
continue
}
containers = append(containers, r.ID)
if r.ID == id {
return r.Name, r.Image, nil
}
}
return "", "", fmt.Errorf(
"the %s module declares no container %q, so the installer does not know what to wait for. "+
"It declares: %s", m.Module, id, strings.Join(containers, ", "))
}