The bundle raised the bus as mesh-broker on docker volumes; the catalogue's nats module keeps it as mesh-broker-nats on directories of the machine, so the broker step refused rather than raise a second bus, and genesis stopped at step 18 of 24. The bundle now raises mesh-broker-nats with its configuration and user list in /var/lib/nats-module/conf, its streams in /var/lib/mesh-broker-nats and its certificate in /var/lib/mesh-broker-tls, published on the node's bus port. The broker step holds that layout to the module's manifest field by field before registering anything, says the one restart onto the module's image, and waits until the bus runs that image and its monitor answers. The installer's bridge writes the composed users at the module's bus-users path and signals mesh-broker-nats. The bundle's bus configuration and user list are seeds, so a re-run after the adoption does not put the bundle's one user back over the module's.
171 lines
5.9 KiB
Go
171 lines
5.9 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
func foundation(t *testing.T) *declaration.Declaration {
|
|
t.Helper()
|
|
out, err := Rewrite(theRealBundle(t), held)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out.Declaration
|
|
}
|
|
|
|
// **A container that is up is not a control plane that replies**, and this project has paid for
|
|
// that distinction more than once. A runtime reports a container running from the moment its
|
|
// process starts — before it has opened a database, and before it has failed to.
|
|
func TestAContainerThatIsUpIsNotAControlPlaneThatReplies(t *testing.T) {
|
|
previous := answerEvery
|
|
answerEvery = time.Millisecond
|
|
defer func() { answerEvery = previous }()
|
|
|
|
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
|
switch args[0] {
|
|
case "container":
|
|
return "true running\n", nil
|
|
case "exec":
|
|
// Up, and saying nothing. The program inside is not answering.
|
|
return "", errors.New("exit status 1")
|
|
}
|
|
return "", fmt.Errorf("unexpected command: %v", args)
|
|
}}
|
|
|
|
_, err := Verify(context.Background(), foundation(t), runtime.run,
|
|
time.Second, 0, func(string) {})
|
|
if err == nil {
|
|
t.Fatal("every container was running, nothing answered, and the foundation was reported up")
|
|
}
|
|
for _, wanted := range []string{"mesh-controller", "Running is not replying", "docker logs"} {
|
|
if !strings.Contains(err.Error(), wanted) {
|
|
t.Errorf("the failure does not mention %q:\n%v", wanted, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Exit zero and silence is not an answer either. A program that returns nothing has not been asked
|
|
// anything, and treating it as success is the same fault one level down.
|
|
func TestAControlPlaneThatSaysNothingHasNotAnswered(t *testing.T) {
|
|
previous := answerEvery
|
|
answerEvery = time.Millisecond
|
|
defer func() { answerEvery = previous }()
|
|
|
|
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
|
if args[0] == "container" {
|
|
return "true running\n", nil
|
|
}
|
|
return " \n", nil
|
|
}}
|
|
|
|
if _, err := Verify(context.Background(), foundation(t), runtime.run,
|
|
time.Second, 0, func(string) {}); err == nil {
|
|
t.Fatal("a control plane that exited zero without saying anything was accepted")
|
|
}
|
|
}
|
|
|
|
// The foundation answering is the whole point, and what it said is reported rather than asserted.
|
|
func TestAFoundationThatIsUpAndAnsweringIsAccepted(t *testing.T) {
|
|
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
|
if args[0] == "container" {
|
|
return "true running\n", nil
|
|
}
|
|
return "1 node, 0 waiting\n", nil
|
|
}}
|
|
|
|
verified, err := Verify(context.Background(), foundation(t), runtime.run,
|
|
time.Second, 0, func(string) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Three long-running containers: the store, the broker and the TEMPORARY control plane. The
|
|
// run-once and scheduled shapes are excluded on purpose — a step that has exited is not a
|
|
// fault. The name is `temp-mesh-controller` because the permanent one is a module and takes the
|
|
// plain name (novox/hq ADR 0067), which is what makes the two of them coexist at all.
|
|
want := []string{"mesh-store", "mesh-broker-nats", "temp-mesh-controller"}
|
|
if len(verified.Running) != len(want) {
|
|
t.Fatalf("confirmed %v running, want %v", verified.Running, want)
|
|
}
|
|
for i := range want {
|
|
if verified.Running[i] != want[i] {
|
|
t.Errorf("confirmed %v running, want %v", verified.Running, want)
|
|
}
|
|
}
|
|
if verified.Answered != "1 node, 0 waiting" {
|
|
t.Errorf("the control plane's reply is reported as %q", verified.Answered)
|
|
}
|
|
}
|
|
|
|
// A control plane that is still opening its stores is waited for, not refused. Refusing on the
|
|
// first attempt would make a correct bootstrap fail for being observed too early.
|
|
func TestAControlPlaneThatIsStillStartingIsWaitedFor(t *testing.T) {
|
|
previous := answerEvery
|
|
answerEvery = time.Millisecond
|
|
defer func() { answerEvery = previous }()
|
|
|
|
attempts := 0
|
|
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
|
if args[0] == "container" {
|
|
return "true running\n", nil
|
|
}
|
|
attempts++
|
|
if attempts < 3 {
|
|
return "", errors.New("exit status 1")
|
|
}
|
|
return "1 node\n", nil
|
|
}}
|
|
|
|
if _, err := Verify(context.Background(), foundation(t), runtime.run,
|
|
time.Second, time.Second, func(string) {}); err != nil {
|
|
t.Fatalf("a control plane that answered on the third ask was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// A container that exited is named with what it IS, so somebody can go and read its logs rather
|
|
// than being told only that something is not what it should be.
|
|
func TestAContainerThatExitedIsNamedWithItsState(t *testing.T) {
|
|
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
|
if args[0] == "container" && args[len(args)-1] == "mesh-broker-nats" {
|
|
return "false exited\n", nil
|
|
}
|
|
if args[0] == "container" {
|
|
return "true running\n", nil
|
|
}
|
|
return "", fmt.Errorf("unexpected command: %v", args)
|
|
}}
|
|
|
|
_, err := Verify(context.Background(), foundation(t), runtime.run,
|
|
time.Second, 0, func(string) {})
|
|
if err == nil {
|
|
t.Fatal("a container that had exited was reported as part of a running foundation")
|
|
}
|
|
if !strings.Contains(err.Error(), "mesh-broker-nats") || !strings.Contains(err.Error(), "exited") {
|
|
t.Errorf("the failure does not say which container is in what state: %v", err)
|
|
}
|
|
}
|
|
|
|
// The control plane is asked by running the binary in its own image directly, because the image is
|
|
// `FROM scratch` and has no shell for a command line to be interpreted by.
|
|
func TestTheControlPlaneIsAskedByRunningItsOwnBinary(t *testing.T) {
|
|
runtime := &asked{answer: func(_ string, args []string) (string, error) {
|
|
if args[0] == "container" {
|
|
return "true running\n", nil
|
|
}
|
|
return "1 node\n", nil
|
|
}}
|
|
if _, err := Verify(context.Background(), foundation(t), runtime.run,
|
|
time.Second, 0, func(string) {}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !runtime.ran("docker exec " + TempPrefix + "mesh-controller " + controlPlaneBinary + " status") {
|
|
t.Errorf("the control plane was never asked anything: %v", runtime.commands)
|
|
}
|
|
}
|