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
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 "inspect":
|
|
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] == "inspect" {
|
|
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] == "inspect" {
|
|
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] == "inspect" {
|
|
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)
|
|
}
|
|
}
|