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 `/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 }