docker inspect <name> resolves across every object kind, not just
containers. A module regularly names a network the same as the
container that joins it (keycloak does this today, ordinarily) — so
when the container does not exist yet but the same-named network
already does, the bare form answers with the network's JSON instead
of reporting the container absent, and the template these callers use
(.State.Running) fails to execute against it entirely.
Live on novox tonight: minio's LB container, named the same as its
network ("minio"), could never be created — every apply crashed on
"the container runtime could not say whether minio is here", stuck
since first push, because the check itself never got a clean answer.
Fixed at every call site asking a container's state by name
(containerState, inspectFound, NamesFree, raiseGiteaServer,
containerRunning) by scoping to `docker container inspect`, matching
the type-scoped form this codebase already uses correctly for
networks, volumes and images elsewhere. Also scoped the one image
inspect that was still bare (publish.go), for the same reason.
mesh-host runs as a host-level service (nox-mesh-host.service), not a
Docker module — merging this does not redeploy it. The live novox
failure persists until the service itself is rebuilt and updated.
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", "image", "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
|
|
}
|