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
186 lines
8.2 KiB
Go
186 lines
8.2 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// ControlPlaneRepository is what the control plane's image is called in the mesh's own registry.
|
|
const ControlPlaneRepository = "mesh-controller"
|
|
|
|
// genesisTag is the tag the first push uses.
|
|
//
|
|
// A tag is not a pin and is never what anything is deployed from — the digest the registry assigns
|
|
// is (novox/hq ADR 0006). This exists so a person reading `/v2/mesh-controller/tags/list` can see
|
|
// which image this mesh started from, and so the push has something to name. Everything downstream
|
|
// uses the digest that comes back.
|
|
const genesisTag = "genesis"
|
|
|
|
// Published is what step 8 did.
|
|
type Published struct {
|
|
// Reference is `<registry>/mesh-controller@sha256:…` — the first manifest digest this image has
|
|
// ever had, and the thing that makes the control plane an ordinary module.
|
|
Reference string
|
|
// Tagged is where it was pushed, tag and all.
|
|
Tagged string
|
|
// Already is true when the registry was already serving it and nothing was pushed.
|
|
Already bool
|
|
}
|
|
|
|
// PublishControlPlane puts the carried image into the mesh's own registry and reads back its digest.
|
|
//
|
|
// **This is the pivot's hinge.** Every image must be pinned by digest, and a digest a pin can mean
|
|
// is one a REGISTRY assigned when something was pushed to it. The control plane's image is built
|
|
// from source and pushed nowhere, so it has none — which is why the foundation names it by the
|
|
// digest of its own configuration, and why that is legal exactly where nothing could have served
|
|
// one. The moment this push completes, that stops being true: the image has a manifest digest, so
|
|
// the control plane can be named the way every other module is named, so the mesh can build and
|
|
// roll out its own upgrades. If this step is skipped the machine still works and the mesh cannot
|
|
// upgrade itself, which is the check novox/hq ADR 0067 states: after installing, the running
|
|
// control plane must be pinned by a digest the mesh's own registry assigned, not by an image id.
|
|
//
|
|
// **It mirrors mesh-controller's `internal/builder`.PublishImage rather than importing it.** Tag,
|
|
// push, read back `RepoDigests`, refuse anything without `@sha256:` — the same four steps, because
|
|
// there is exactly one right way to learn what a registry will serve something as, and it is to
|
|
// ask the registry. It is not imported because that code is tier 2: the host and its installer
|
|
// depend on nothing that must be installed first (novox/hq ADR 0041), and taking a dependency on
|
|
// the control plane's repository to raise the control plane would be the cycle this whole ADR is
|
|
// about, one layer up. The duplication is four commands, and it is deliberate.
|
|
//
|
|
// One difference, and it is a correction rather than a divergence: the digest is chosen from
|
|
// `RepoDigests` by repository instead of taken as element zero. An image that has been pushed to
|
|
// more than one registry has more than one entry, and element zero is then whichever the runtime
|
|
// happened to list first — which would pin this mesh to somebody else's registry, silently.
|
|
func PublishControlPlane(ctx context.Context, o Options, d Deps, imageID string,
|
|
say func(string)) (Published, error) {
|
|
|
|
return publishAs(ctx, o, d, imageID, ControlPlaneRepository, say)
|
|
}
|
|
|
|
// publishAs puts one locally held image into this mesh's registry, under a repository name.
|
|
//
|
|
// **The same act for every image genesis has to place**, which is now two: the control plane it
|
|
// built, and the builder it carried. They arrive differently and are published identically — the
|
|
// registry does not care where an image came from, and a second copy of this that drifted would be
|
|
// the kind of difference nobody finds until one of them stops working.
|
|
func publishAs(ctx context.Context, o Options, d Deps, imageID, repository string,
|
|
say func(string)) (Published, error) {
|
|
|
|
remote := o.Registry + "/" + repository
|
|
out := Published{Tagged: remote + ":" + genesisTag}
|
|
|
|
// Asked first. A digest already served is a fact about the registry, and re-pushing an image
|
|
// the registry already holds is asking it to store what it already has under the name it
|
|
// already has.
|
|
if held, err := digestOf(ctx, o, d, remote); err != nil {
|
|
return out, err
|
|
} else if held != "" {
|
|
out.Reference, out.Already = held, true
|
|
say(" already published " + held)
|
|
return out, nil
|
|
}
|
|
|
|
if _, err := d.Run(ctx, "docker", "tag", imageID, out.Tagged); err != nil {
|
|
return out, fmt.Errorf("cannot tag %s as %s: %w", imageID, out.Tagged, err)
|
|
}
|
|
if _, err := d.Run(ctx, "docker", "push", out.Tagged); err != nil {
|
|
return out, fmt.Errorf(
|
|
"the container runtime would not push %s: %w\n"+
|
|
"The registry is plain HTTP and wants no credentials, deliberately — it is reached "+
|
|
"over the mesh's own network, which is already the encrypted and authenticated "+
|
|
"thing. A runtime refusing it for being insecure is refusing a registry on %s, "+
|
|
"which it does not do for a loopback address",
|
|
out.Tagged, err, o.Registry)
|
|
}
|
|
|
|
// Read back, from the registry's own answer rather than computed here. What matters is what
|
|
// the registry will serve for that reference, and only it can say (novox/hq ADR 0018).
|
|
pinned, err := digestOf(ctx, o, d, remote)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
if pinned == "" {
|
|
return out, fmt.Errorf(
|
|
"%s was pushed and the registry does not serve it.\n"+
|
|
"The next step names this module by the digest this was supposed to produce, so "+
|
|
"there is nothing to name. Check `docker push` and "+
|
|
"http://%s/v2/%s/tags/list", out.Tagged, o.Registry, repository)
|
|
}
|
|
out.Reference = pinned
|
|
say(" published " + pinned)
|
|
return out, nil
|
|
}
|
|
|
|
// digestOf is what the registry serves this repository as, or empty if it serves it at all.
|
|
//
|
|
// Both halves are asked, because either alone lies. The registry's tag list says something was
|
|
// pushed and not what its digest is; the runtime's `RepoDigests` says what a digest was and not
|
|
// whether the registry still has it — a registry whose volume was recreated would leave the
|
|
// runtime remembering a digest nothing serves, and the module registered against it would pin the
|
|
// mesh to an image that cannot be pulled.
|
|
func digestOf(ctx context.Context, o Options, d Deps, remote string) (string, error) {
|
|
asking, cancel := context.WithTimeout(ctx, o.Timeout)
|
|
status, body, err := d.Fetch(asking,
|
|
"http://"+o.Registry+"/v2/"+ControlPlaneRepository+"/tags/list")
|
|
cancel()
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot ask the registry at %s what it holds: %w", o.Registry, err)
|
|
}
|
|
if status == http.StatusNotFound {
|
|
// Nothing has ever been pushed under this name. An answer, not a failure.
|
|
return "", nil
|
|
}
|
|
if status != http.StatusOK {
|
|
return "", fmt.Errorf("the registry answered %d when asked what it holds for %s",
|
|
status, ControlPlaneRepository)
|
|
}
|
|
var listed struct {
|
|
Tags []string `json:"tags"`
|
|
}
|
|
if err := json.Unmarshal([]byte(body), &listed); err != nil {
|
|
return "", fmt.Errorf("the registry's answer about %s is not readable: %w",
|
|
ControlPlaneRepository, err)
|
|
}
|
|
if !contains(listed.Tags, genesisTag) {
|
|
return "", nil
|
|
}
|
|
|
|
// The registry has it. What digest, according to the runtime that pushed it.
|
|
reading, cancel := context.WithTimeout(ctx, o.Timeout)
|
|
out, err := d.Run(reading, "docker", "inspect", "--format", "{{json .RepoDigests}}",
|
|
remote+":"+genesisTag)
|
|
cancel()
|
|
if err != nil {
|
|
// The registry holds the tag and this machine's runtime does not hold the image. That
|
|
// happens on a re-run after the image was pruned, and it is not something to work around
|
|
// by trusting the tag: a tag can be made to point elsewhere.
|
|
return "", nil
|
|
}
|
|
var digests []string
|
|
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &digests); err != nil {
|
|
return "", fmt.Errorf("the runtime's answer about %s is not readable: %w", remote, err)
|
|
}
|
|
for _, digest := range digests {
|
|
if !strings.HasPrefix(digest, remote+"@sha256:") {
|
|
// Somebody else's registry serving the same image. Skipped rather than used: pinning
|
|
// this mesh's control plane to a registry it does not run is exactly the dependency
|
|
// the pivot exists to remove.
|
|
continue
|
|
}
|
|
return digest, nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func contains(values []string, want string) bool {
|
|
for _, v := range values {
|
|
if v == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|