355 lines
12 KiB
Go
355 lines
12 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/overlay"
|
|
)
|
|
|
|
func TestAConvergedDeclarationIsByteForByteWhatItWas(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, helloWeb())
|
|
if _, err := unassign(ctx, open, "laptop", overlay.Name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := assign(ctx, open, "laptop", "hello-web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
declared := composed(t, open, "laptop")
|
|
if declared.Adoption != nil {
|
|
t.Fatal("a converged node was given an adoption envelope")
|
|
}
|
|
body, err := declared.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(body) != convergedBefore {
|
|
t.Fatalf("a converged declaration changed; an older host parses this strictly:\n%s\n%s",
|
|
body, convergedBefore)
|
|
}
|
|
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 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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
func TestTheMachineSideOfAMappingIsMovedEverywhereTheNumberIsUsed(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "forge", Version: "1",
|
|
Provides: []catalogue.Offer{{Name: "git-over-ssh", Scope: catalogue.ScopeMesh}},
|
|
Serves: map[string]map[string]any{"git-over-ssh": {"port": 2222}},
|
|
Listens: []catalogue.Listening{{Port: 2222, From: catalogue.FromMesh}},
|
|
Resources: []map[string]any{{"id": "server", "type": "container", "name": "forge",
|
|
"ports": []any{"2222:22"},
|
|
"image": "registry.example/forge@sha256:" + strings.Repeat("c", 64)}}})
|
|
register(t, open, catalogue.Manifest{Module: "app", Version: "1",
|
|
Requires: []string{"git-over-ssh"}})
|
|
if err := open.inventory.SetAdopted(ctx, "anchor", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := assign(ctx, open, "anchor", "forge"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := assign(ctx, open, "laptop", "app"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.Take(ctx, "anchor", "forge"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := open.inventory.SetSettings(ctx, "anchor", "forge",
|
|
map[string]any{catalogue.PortsSetting: map[string]any{"2222": 222}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resources := composed(t, open, "anchor").Resources
|
|
var container, opening map[string]any
|
|
for _, r := range resources {
|
|
switch r["id"] {
|
|
case "forge.server":
|
|
container = r
|
|
case catalogue.OpeningID("tcp", 222, catalogue.PathForwarded):
|
|
opening = r
|
|
}
|
|
if id, _ := r["id"].(string); strings.HasPrefix(id, "adoption.opening-tcp-2222-") {
|
|
t.Errorf("the adopted node is told to open the port the forge was moved off: %s", id)
|
|
}
|
|
}
|
|
if container == nil || !reflect.DeepEqual(container["ports"], []any{"222:22"}) {
|
|
t.Fatalf("the forge's container publishes %v", container["ports"])
|
|
}
|
|
if opening == nil || opening["to"] != 22 || opening["from"] != catalogue.OpeningFromMesh {
|
|
t.Fatalf("no opening for the port this node gave the forge: %v", opening)
|
|
}
|
|
var guard map[string]any
|
|
for _, r := range resources {
|
|
if r["id"] == catalogue.GuardID() {
|
|
guard = r
|
|
}
|
|
}
|
|
if guard == nil || guard["content"] != catalogue.AsGuard([]int{222}) {
|
|
t.Fatalf("the guard does not refuse the port the forge is on:\n%v", guard["content"])
|
|
}
|
|
|
|
// And the consumer on the other machine dials the same number.
|
|
plan, _, err := planFor(ctx, open, "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var told any
|
|
for _, n := range plan.Needs {
|
|
if n.Name == "git-over-ssh" {
|
|
told = n.Serves["port"]
|
|
}
|
|
}
|
|
if told != 222 {
|
|
t.Fatalf("the consumer is told the forge answers on %v", told)
|
|
}
|
|
}
|
|
|
|
func TestADefinitionMovingUnderAStoredSettingLeavesThatModuleOutNotTheMachine(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
web := helloWeb()
|
|
web.Resources[1]["ports"] = []any{"8080"}
|
|
register(t, open, web)
|
|
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
|
|
Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/notes.conf", "content": "x"}}})
|
|
for _, m := range []string{"hello-web", "notes"} {
|
|
if _, err := assign(ctx, open, "laptop", m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// Judged where it is stored: a port the module does not publish is refused by name.
|
|
err := open.inventory.SetSettings(ctx, "laptop", "hello-web",
|
|
map[string]any{catalogue.PortsSetting: map[string]any{"9999": 10000}})
|
|
if err == nil || !strings.Contains(err.Error(), "hello-web on laptop") || !strings.Contains(err.Error(), "9999") {
|
|
t.Fatalf("an impossible setting was stored: %v", err)
|
|
}
|
|
if err := open.inventory.SetSettings(ctx, "laptop", "hello-web",
|
|
map[string]any{catalogue.PortsSetting: map[string]any{"8080": 10000}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if declared := composed(t, open, "laptop"); len(declared.LeftOut) != 0 {
|
|
t.Fatalf("a setting that composes left a module out: %v", declared.LeftOut)
|
|
}
|
|
|
|
// The definition moves: the container publishes another port now.
|
|
web.Version = "2"
|
|
web.Resources[1]["ports"] = []any{"9090"}
|
|
register(t, open, web)
|
|
declared := composed(t, open, "laptop")
|
|
if len(declared.LeftOut) != 1 || declared.LeftOut[0] != "hello-web" {
|
|
t.Fatalf("hello-web is not left out: %v", declared.LeftOut)
|
|
}
|
|
if !strings.Contains(declared.leftOutWhy["hello-web"], "no container of its publishes 8080") {
|
|
t.Fatalf("why it was left out is not said: %v", declared.leftOutWhy)
|
|
}
|
|
if hasID(declared.Resources, "hello-web.server") || !hasID(declared.Resources, "notes.conf") {
|
|
t.Fatalf("the machine was not told everything else: %v", declared.Resources)
|
|
}
|
|
body, err := declared.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var env map[string]any
|
|
if err := json.Unmarshal(body, &env); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
left, _ := env["left_out"].([]any)
|
|
if len(left) != 1 || left[0] != "hello-web" {
|
|
t.Fatalf("the envelope does not say what was left out: %v", env)
|
|
}
|
|
}
|