mesh/merge-gate head 37947ca4eed1 merged into trunk d8e785bb4b4b — pass: the change touches no module of the mesh's graph
mesh/repo-check head 37947ca4eed1 merged into trunk d8e785bb4b4b — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The installer could not raise a first node: the old bundle's control plane had no MESH_BUS_NATS and crash-looped, and the NATS bundle was refused because rootsecrets only knew the previous broker's guest:guest@ template. One bundle now, the NATS one. rootsecrets makes the controller's bus account and puts it in both placeholders (the address and the bcrypt hash in the carried user list), kept with its hash so a re-run writes the same bundle. Preflight refuses a template naming no bus connection it knows, naming the placeholder, instead of waiting out the verify step. The broker step adopts the bus as the nats module and refuses, by name, a catalogue without it or a server that is not the foundation's container.
170 lines
7.0 KiB
Go
170 lines
7.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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A template whose temporary control plane is given no bus is refused at preflight, before anything
|
|
// is changed, rather than raised and waited on at the verify step until its timeout (novox/hq issue
|
|
// 146, ADR 0331 §4): that control plane says it has no MESH_BUS_NATS every few seconds, for ever.
|
|
func TestATemplateWhoseControlPlaneHasNoBusIsRefusedBeforeAnythingIsRaised(t *testing.T) {
|
|
template := string(theRealBundle(t))
|
|
if _, err := CheckTemplate([]byte(template)); err != nil {
|
|
t.Fatalf("the shipped bundle is refused at preflight: %v", err)
|
|
}
|
|
const bus = `"MESH_BUS_NATS": "nats://controller:bootstrap@127.0.0.1:5671",`
|
|
if !strings.Contains(template, bus) {
|
|
t.Fatalf("the shipped bundle does not give its control plane %s", bus)
|
|
}
|
|
_, err := CheckTemplate([]byte(strings.Replace(template, bus, "", 1)))
|
|
if err == nil || !strings.Contains(err.Error(), BusVar) {
|
|
t.Fatalf("a control plane with no bus was let through preflight: %v", err)
|
|
}
|
|
}
|