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.
198 lines
6.7 KiB
Go
198 lines
6.7 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Step 8 is the pivot's hinge (novox/hq ADR 0067): the carried image gets a manifest digest, which
|
|
// is the first one it has ever had, and that is what lets the control plane be named the way every
|
|
// other module is named. These tests defend how that digest is learned, because a wrong one pins
|
|
// the mesh to an image nothing on this machine serves.
|
|
|
|
func publishing(t *testing.T, fetch func(string) (int, string, error),
|
|
run func(name string, args []string) (string, error)) (Options, Deps, *asked) {
|
|
t.Helper()
|
|
runtime := &asked{answer: run}
|
|
return Options{
|
|
Registry: "127.0.0.1:5000",
|
|
Timeout: time.Second,
|
|
Wait: 0,
|
|
}, Deps{
|
|
Run: runtime.run,
|
|
Fetch: func(_ context.Context, url string) (int, string, error) {
|
|
return fetch(url)
|
|
},
|
|
}, runtime
|
|
}
|
|
|
|
// Nothing has ever been pushed under this name, so the registry says 404 — and that is an answer,
|
|
// not a failure. An installer that treated it as one would refuse on the first run of the step it
|
|
// exists to perform.
|
|
func TestAnImageNoRegistryHasEverHeldIsPushed(t *testing.T) {
|
|
pushed := false
|
|
o, d, runtime := publishing(t,
|
|
func(string) (int, string, error) {
|
|
if !pushed {
|
|
return http.StatusNotFound, "", nil
|
|
}
|
|
return http.StatusOK, `{"name":"mesh-controller","tags":["genesis"]}`, nil
|
|
},
|
|
func(_ string, args []string) (string, error) {
|
|
switch args[0] {
|
|
case "tag":
|
|
return "", nil
|
|
case "push":
|
|
pushed = true
|
|
return "", nil
|
|
case "image":
|
|
return `["127.0.0.1:5000/mesh-controller@sha256:` + strings.Repeat("a", 64) + `"]`, nil
|
|
}
|
|
return "", fmt.Errorf("unexpected: %v", args)
|
|
})
|
|
|
|
out, err := PublishControlPlane(context.Background(), o, d, held, func(string) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Already {
|
|
t.Error("an image no registry held was reported as already published")
|
|
}
|
|
if !strings.HasPrefix(out.Reference, "127.0.0.1:5000/mesh-controller@sha256:") {
|
|
t.Errorf("the control plane is pinned as %q", out.Reference)
|
|
}
|
|
if !runtime.ran("docker push 127.0.0.1:5000/mesh-controller:genesis") {
|
|
t.Errorf("nothing was pushed: %v", runtime.commands)
|
|
}
|
|
}
|
|
|
|
// An image the registry already serves is not pushed again, and says so. Blobs are named by their
|
|
// content, so re-pushing is asking a registry to store what it already has under the name it
|
|
// already has — and the installer is run over and over.
|
|
func TestAnImageTheRegistryAlreadyServesIsNotPushedAgain(t *testing.T) {
|
|
o, d, runtime := publishing(t,
|
|
func(string) (int, string, error) {
|
|
return http.StatusOK, `{"name":"mesh-controller","tags":["genesis"]}`, nil
|
|
},
|
|
func(_ string, args []string) (string, error) {
|
|
if args[0] == "image" {
|
|
return `["127.0.0.1:5000/mesh-controller@sha256:` + strings.Repeat("b", 64) + `"]`, nil
|
|
}
|
|
return "", fmt.Errorf("unexpected: %v", args)
|
|
})
|
|
|
|
out, err := PublishControlPlane(context.Background(), o, d, held, func(string) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !out.Already {
|
|
t.Error("an image the registry already serves was not reported as already published")
|
|
}
|
|
if runtime.ran("docker push") {
|
|
t.Errorf("it was pushed again: %v", runtime.commands)
|
|
}
|
|
}
|
|
|
|
// **The digest is chosen by repository, not taken as element zero.** An image that has been pushed
|
|
// to more than one registry has more than one entry, and element zero is whichever the runtime
|
|
// listed first — which would pin this mesh's control plane to somebody else's registry, silently,
|
|
// which is the dependency the whole pivot exists to remove.
|
|
func TestTheDigestComesFromThisMeshsOwnRegistry(t *testing.T) {
|
|
elsewhere := "some.other.registry/mesh-controller@sha256:" + strings.Repeat("c", 64)
|
|
ours := "127.0.0.1:5000/mesh-controller@sha256:" + strings.Repeat("d", 64)
|
|
|
|
o, d, _ := publishing(t,
|
|
func(string) (int, string, error) {
|
|
return http.StatusOK, `{"tags":["genesis"]}`, nil
|
|
},
|
|
func(_ string, args []string) (string, error) {
|
|
if args[0] == "image" {
|
|
return `["` + elsewhere + `","` + ours + `"]`, nil
|
|
}
|
|
return "", fmt.Errorf("unexpected: %v", args)
|
|
})
|
|
|
|
out, err := PublishControlPlane(context.Background(), o, d, held, func(string) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Reference != ours {
|
|
t.Errorf("the control plane is pinned as %q, and this mesh's registry serves %q",
|
|
out.Reference, ours)
|
|
}
|
|
}
|
|
|
|
// A push that produced no digest this mesh's registry serves is refused, and the refusal says what
|
|
// depends on it. The next step names the control plane's module by that digest, so there would be
|
|
// nothing to name — and finding that out one step later would mean registering a module pinned to
|
|
// an empty string.
|
|
func TestAPushThatProducedNoDigestIsRefused(t *testing.T) {
|
|
pushed := false
|
|
o, d, _ := publishing(t,
|
|
func(string) (int, string, error) {
|
|
if !pushed {
|
|
return http.StatusNotFound, "", nil
|
|
}
|
|
// Pushed, and the registry still does not list it.
|
|
return http.StatusOK, `{"tags":[]}`, nil
|
|
},
|
|
func(_ string, args []string) (string, error) {
|
|
if args[0] == "push" {
|
|
pushed = true
|
|
}
|
|
return "", nil
|
|
})
|
|
|
|
_, err := PublishControlPlane(context.Background(), o, d, held, func(string) {})
|
|
if err == nil {
|
|
t.Fatal("a push that produced no digest was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "does not serve it") {
|
|
t.Errorf("the refusal does not say what is missing: %v", err)
|
|
}
|
|
}
|
|
|
|
// A tag is not a pin. If the runtime answers with something that is not pinned by digest, it is
|
|
// not used — a tag can be made to point at a different image, and this reference is applied on
|
|
// machines with no mesh to ask about anything (novox/hq ADR 0006).
|
|
func TestATagIsNotAPin(t *testing.T) {
|
|
o, d, _ := publishing(t,
|
|
func(string) (int, string, error) {
|
|
return http.StatusOK, `{"tags":["genesis"]}`, nil
|
|
},
|
|
func(_ string, args []string) (string, error) {
|
|
if args[0] == "image" {
|
|
return `["127.0.0.1:5000/mesh-controller:genesis"]`, nil
|
|
}
|
|
return "", nil
|
|
})
|
|
|
|
out, err := PublishControlPlane(context.Background(), o, d, held, func(string) {})
|
|
if err == nil {
|
|
t.Fatalf("a tag was accepted as a pin: %q", out.Reference)
|
|
}
|
|
}
|
|
|
|
// A registry that cannot be reached at all is said so plainly rather than becoming a push that
|
|
// fails for a reason nobody can read.
|
|
func TestARegistryThatCannotBeAskedIsSaidSo(t *testing.T) {
|
|
o, d, _ := publishing(t,
|
|
func(string) (int, string, error) {
|
|
return 0, "", errors.New("connection refused")
|
|
},
|
|
func(string, []string) (string, error) { return "", nil })
|
|
|
|
_, err := PublishControlPlane(context.Background(), o, d, held, func(string) {})
|
|
if err == nil {
|
|
t.Fatal("a registry that refused the connection was treated as empty")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot ask the registry") {
|
|
t.Errorf("the refusal does not say the registry could not be asked: %v", err)
|
|
}
|
|
}
|