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
152 lines
6.0 KiB
Go
152 lines
6.0 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// An installed package is not a capability (novox/hq 04-ISSUES/007). The daemon is asked, and a
|
|
// machine where it does not answer is refused before anything is loaded, written or applied.
|
|
//
|
|
// The refusal has to be plain, because the person reading it is standing in front of a machine
|
|
// that will not work: it says what was asked, what came back, that re-running is safe, and names
|
|
// the record that explains why an installed docker is not enough.
|
|
func TestPreflightRefusesPlainlyWhenTheRuntimeDoesNotAnswer(t *testing.T) {
|
|
silent := func(context.Context, string, ...string) (string, error) {
|
|
return "", errors.New("Cannot connect to the Docker daemon at unix:///var/run/docker.sock")
|
|
}
|
|
|
|
// No wait, so this is one attempt: what is being tested is the refusal, not the patience.
|
|
err := waitForRuntime(context.Background(), silent, time.Second, 0, func(string) {})
|
|
if err == nil {
|
|
t.Fatal("a machine whose container runtime does not answer was accepted")
|
|
}
|
|
for _, wanted := range []string{
|
|
"no container runtime that answers",
|
|
"Cannot connect to the Docker daemon",
|
|
"04-ISSUES/007",
|
|
"idempotent",
|
|
} {
|
|
if !strings.Contains(err.Error(), wanted) {
|
|
t.Errorf("the refusal does not mention %q:\n%v", wanted, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And a runtime that is merely slow to start is waited for rather than refused.
|
|
//
|
|
// A socket-activated daemon queued behind the network is not absent, it is a few seconds away.
|
|
// Refusing on the first attempt would make a correct bootstrap fail for being observed too early —
|
|
// and `docker load` against such a daemon blocks silently rather than failing, which is how one
|
|
// became a 35-minute silence (04-ISSUES/024).
|
|
func TestARuntimeThatIsStillStartingIsWaitedFor(t *testing.T) {
|
|
previous := runtimeAskEvery
|
|
runtimeAskEvery = time.Millisecond
|
|
defer func() { runtimeAskEvery = previous }()
|
|
|
|
attempts := 0
|
|
slow := func(context.Context, string, ...string) (string, error) {
|
|
attempts++
|
|
if attempts < 3 {
|
|
return "", errors.New("Cannot connect to the Docker daemon")
|
|
}
|
|
return "27.0.3\n", nil
|
|
}
|
|
|
|
var said []string
|
|
if err := waitForRuntime(context.Background(), slow, time.Second, time.Second,
|
|
func(line string) { said = append(said, line) }); err != nil {
|
|
t.Fatalf("a runtime that answered on the third ask was refused: %v", err)
|
|
}
|
|
if attempts != 3 {
|
|
t.Errorf("the runtime was asked %d time(s)", attempts)
|
|
}
|
|
if !strings.Contains(strings.Join(said, "\n"), "27.0.3") {
|
|
t.Errorf("the version the daemon reported was not said back: %v", said)
|
|
}
|
|
}
|
|
|
|
// What has to be reachable is what the bundle actually names, not "the internet".
|
|
//
|
|
// The mesh's own image is carried and nothing serves it, so asking a registry about it would be
|
|
// asking a question with no answer — which is the whole point of naming an image by the digest of
|
|
// its own configuration.
|
|
func TestOnlyTheRegistriesTheBundleNamesAreAskedAbout(t *testing.T) {
|
|
parsed, err := declaration.ParseFileTrusted([]byte(`{"declaration":1,"resources":[
|
|
{"id":"store","type":"container","name":"mesh-store","image":"postgres@sha256:` +
|
|
strings.Repeat("7", 64) + `"},
|
|
{"id":"broker","type":"container","name":"mesh-broker","image":"192.0.2.250:5000/lavinmq@sha256:` +
|
|
strings.Repeat("8", 64) + `"},
|
|
{"id":"control-plane","type":"container","name":"mesh-controller","image":"` + held + `"}
|
|
]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got := registriesIn(parsed)
|
|
want := []string{DefaultRegistry, "192.0.2.250:5000"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("asked about %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("asked about %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And the control plane's slot is excluded whatever is in it.
|
|
//
|
|
// Found on the first real run of this installer. A template names the control plane by SOMETHING
|
|
// and step 3 replaces it with the id of the carried image, so whatever was there is never pulled —
|
|
// but preflight was reading that slot like any other and dialling it. The lab's template still
|
|
// carried the address of a registry the lab no longer raises, so a correct install timed out in
|
|
// preflight against a machine with a perfectly good network.
|
|
func TestTheControlPlanesOwnRegistryIsNeverAskedAbout(t *testing.T) {
|
|
parsed, err := declaration.ParseFileTrusted([]byte(`{"declaration":1,"resources":[
|
|
{"id":"store","type":"container","name":"mesh-store","image":"postgres@sha256:` +
|
|
strings.Repeat("7", 64) + `"},
|
|
{"id":"control-plane","type":"container","name":"mesh-controller","image":"192.0.2.250:5000/mesh-controller@sha256:` +
|
|
strings.Repeat("8", 64) + `"}
|
|
]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got := registriesIn(parsed)
|
|
if len(got) != 1 || got[0] != DefaultRegistry {
|
|
t.Fatalf("asked about %v; the control plane's own reference is about to be replaced and "+
|
|
"must not be reached for", got)
|
|
}
|
|
}
|
|
|
|
func TestWhereAnImageWouldBeFetchedFrom(t *testing.T) {
|
|
// The container runtime's own rule: the part before the first slash is a registry host if it
|
|
// has a dot, a port, or is localhost. Getting this wrong means dialling a hostname that is
|
|
// really the first half of a repository name, and refusing a machine that is fine.
|
|
for _, c := range []struct {
|
|
reference string
|
|
host string
|
|
served bool
|
|
}{
|
|
{"postgres@sha256:" + strings.Repeat("a", 64), DefaultRegistry, true},
|
|
{"cloudamqp/lavinmq@sha256:" + strings.Repeat("a", 64), DefaultRegistry, true},
|
|
{"192.0.2.250:5000/postgres@sha256:" + strings.Repeat("a", 64), "192.0.2.250:5000", true},
|
|
{"localhost/mesh-controller@sha256:" + strings.Repeat("a", 64), "localhost:443", true},
|
|
{"registry.example.com/a/b@sha256:" + strings.Repeat("a", 64), "registry.example.com:443", true},
|
|
// Held by this machine. Nothing serves it, and nothing can.
|
|
{"sha256:" + strings.Repeat("a", 64), "", false},
|
|
{"", "", false},
|
|
} {
|
|
host, served := registryOf(c.reference)
|
|
if host != c.host || served != c.served {
|
|
t.Errorf("%q → (%q, %v), want (%q, %v)", c.reference, host, served, c.host, c.served)
|
|
}
|
|
}
|
|
}
|