Files
mesh-controller/cmd/mesh-controller/sendable_test.go
T

267 lines
8.6 KiB
Go

package main
import (
"bytes"
"encoding/json"
"io"
"os"
"reflect"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
)
// novox/hq ADR 0100: every declaration an adopted node is sent says it is adopted and which modules
// were taken on it; a converged node's declaration is byte for byte what it was.
// helloWeb is a module the predecessor also runs: a page and a server under names it uses.
func helloWeb() catalogue.Manifest {
return catalogue.Manifest{Module: "hello-web", Version: "1",
Resources: []map[string]any{
{"id": "page", "type": "file", "path": "/var/lib/hello-web/index.html", "content": "hello"},
{"id": "server", "type": "container", "name": "hello-web",
"image": "registry.example/hello@sha256:" + strings.Repeat("a", 64)},
{"id": "served", "type": "directory", "path": "/var/lib/hello-web"},
}}
}
// composed is what node would be sent now, as push composes it.
func composed(t *testing.T, open *stores, node string) sendable {
t.Helper()
plan, settings, err := planFor(t.Context(), open, node)
if err != nil {
t.Fatal(err)
}
declared, err := declarationFor(t.Context(), open, node, plan, settings)
if err != nil {
t.Fatal(err)
}
return declared
}
func TestAConvergedDeclarationIsByteForByteWhatItWas(t *testing.T) {
open := aMesh(t)
register(t, open, helloWeb())
if _, err := assign(t.Context(), open, "anchor", "hello-web"); err != nil {
t.Fatal(err)
}
declared := composed(t, open, "anchor")
if declared.Adoption != nil {
t.Fatal("a converged node was given an adoption envelope")
}
body, err := declared.Body()
if err != nil {
t.Fatal(err)
}
// The envelope exactly as every send site marshalled it before adoption existed.
before, err := json.Marshal(map[string]any{"declaration": 1, "resources": declared.Resources})
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(body, before) {
t.Fatalf("a converged declaration changed:\n%s\n%s", body, before)
}
if bytes.Contains(body, []byte(`"adoption"`)) {
t.Fatal("a converged declaration names adoption; an older host would refuse it")
}
}
func TestAnAdoptedDeclarationCarriesItsModeAndWhatWasTaken(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
register(t, open, helloWeb())
if err := open.inventory.SetAdopted(ctx, "anchor", true); err != nil {
t.Fatal(err)
}
if _, err := assign(ctx, open, "anchor", "hello-web"); err != nil {
t.Fatal(err)
}
declared := composed(t, open, "anchor")
if declared.Adoption == nil {
t.Fatal("an adopted node's declaration does not say it is adopted")
}
untaken := declared.Adoption.Untaken["hello-web"]
// Every kind — its directory too (novox/hq ADR 0103).
if !reflect.DeepEqual(untaken, []string{"hello-web.page", "hello-web.server",
"hello-web.served"}) {
t.Fatalf("hello-web's resources are not all named untaken: %v", declared.Adoption)
}
if len(declared.Adoption.Taken) != 0 {
t.Fatalf("nothing was taken, and the declaration says %v", declared.Adoption.Taken)
}
body, err := declared.Body()
if err != nil {
t.Fatal(err)
}
var envelope map[string]any
if err := json.Unmarshal(body, &envelope); err != nil {
t.Fatal(err)
}
adoption, _ := envelope["adoption"].(map[string]any)
if _, ok := adoption["taken"].([]any); !ok {
t.Fatalf("taken is not a list on the wire, even empty: %s", body)
}
// The digest the mesh compares is the digest of what is sent: status and push agree.
would, err := wouldSend(ctx, open, mustNodes(t, open))
if err != nil {
t.Fatal(err)
}
if would["anchor"] != digestOf(body) {
t.Fatal("the digest the mesh compares is not of the declaration push sends")
}
// plan --json prints that same envelope.
printed := stdoutOf(t, func() error { return planCommand(ctx, []string{"anchor", "--json"}) })
var compact bytes.Buffer
if err := json.Compact(&compact, []byte(printed)); err != nil {
t.Fatalf("plan --json is not JSON: %v\n%s", err, printed)
}
if digestOf(compact.Bytes()) != digestOf(body) {
t.Fatalf("plan --json shows something other than what push sends:\n%s", printed)
}
// Taking the module moves its resources out of untaken.
if err := open.inventory.Take(ctx, "anchor", "hello-web"); err != nil {
t.Fatal(err)
}
declared = composed(t, open, "anchor")
if _, still := declared.Adoption.Untaken["hello-web"]; still {
t.Fatalf("a taken module is still untaken: %v", declared.Adoption)
}
if !reflect.DeepEqual(declared.Adoption.Taken, []string{"hello-web"}) {
t.Fatalf("taken is %v", declared.Adoption.Taken)
}
}
func mustNodes(t *testing.T, open *stores) []inventory.Node {
t.Helper()
nodes, err := open.inventory.Nodes(t.Context())
if err != nil {
t.Fatal(err)
}
return nodes
}
// stdoutOf is what run printed.
func stdoutOf(t *testing.T, run func() error) string {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
saved := os.Stdout
os.Stdout = w
done := make(chan string)
go func() {
all, _ := io.ReadAll(r)
done <- string(all)
}()
runErr := run()
os.Stdout = saved
w.Close()
out := <-done
if runErr != nil {
t.Fatalf("%v\n%s", runErr, out)
}
return out
}
// novox/hq ADR 0100: a port a node was given for the store is where its consumers on other
// machines are told to reach it, and a port given for the whole mesh is refused.
func TestConsumersAreToldTheGivenPort(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "store", Version: "1",
Provides: []catalogue.Offer{{Name: "database", Scope: catalogue.ScopeMesh}},
Listens: []catalogue.Listening{{Port: 5432, From: catalogue.FromMesh}},
Guards: []int{5432},
Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-store",
"ports": []any{"5432:5432"},
"image": "registry.example/pg@sha256:" + strings.Repeat("b", 64)}}})
register(t, open, catalogue.Manifest{Module: "app", Version: "1", Requires: []string{"database"}})
if _, err := assign(ctx, open, "anchor", "store"); err != nil {
t.Fatal(err)
}
if _, err := assign(ctx, open, "laptop", "app"); err != nil {
t.Fatal(err)
}
if err := open.inventory.SetSettings(ctx, "anchor", "store",
map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5433}}); err != nil {
t.Fatal(err)
}
plan, _, err := planFor(ctx, open, "laptop")
if err != nil {
t.Fatal(err)
}
var told any
for _, n := range plan.Needs {
if n.Name == "database" {
told = n.Serves["port"]
}
}
if told != 5433 {
t.Fatalf("the consumer is told the store is on %v", told)
}
for _, r := range composed(t, open, "anchor").Resources {
if r["id"] == "store.server" && !reflect.DeepEqual(r["ports"], []any{"5433:5432"}) {
t.Fatalf("the store publishes %v", r["ports"])
}
}
// A port for the whole mesh is refused where it is set, not stored to refuse every node's
// declaration afterwards.
if err := open.inventory.SetSettings(ctx, "", "store",
map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5434}}); err == nil ||
!strings.Contains(err.Error(), "per node") {
t.Fatalf("a port given for the whole mesh was not refused: %v", err)
}
plan, settings, err := planFor(ctx, open, "anchor")
if err != nil {
t.Fatal(err)
}
if _, err := declarationFor(ctx, open, "anchor", plan, settings); err != nil {
t.Fatalf("the refused mesh-wide layer was stored anyway: %v", err)
}
}
// novox/hq ADR 0103: the guard an adopted node is sent follows what was taken there. A store
// assigned but not taken is not guarded — its port may still be the predecessor's — and taking it
// guards it from the next declaration.
func TestTheGuardIsSentForTakenModulesOnly(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "store", Version: "1",
Listens: []catalogue.Listening{{Port: 5432, From: catalogue.FromMesh}},
Guards: []int{5432},
Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-store",
"ports": []any{"5432:5432"},
"image": "registry.example/pg@sha256:" + strings.Repeat("b", 64)}}})
if err := open.inventory.SetAdopted(ctx, "anchor", true); err != nil {
t.Fatal(err)
}
if _, err := assign(ctx, open, "anchor", "store"); err != nil {
t.Fatal(err)
}
if hasID(composed(t, open, "anchor").Resources, catalogue.GuardID()) {
t.Fatal("an untaken store is guarded")
}
if err := open.inventory.Take(ctx, "anchor", "store"); err != nil {
t.Fatal(err)
}
for _, r := range composed(t, open, "anchor").Resources {
if r["id"] == catalogue.GuardID() {
if r["content"] != catalogue.AsGuard([]int{5432}) {
t.Fatalf("the taken store's guard is:\n%s", r["content"])
}
return
}
}
t.Fatal("a taken store is not guarded")
}