173 lines
7.7 KiB
Go
173 lines
7.7 KiB
Go
package delivery
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// ADR 0549 §1, §2: the placement key and file, the value's refusals, its reading as the composition reads a node, what
|
|
// a change says, and the catalogue's check of every file.
|
|
func TestPlacement(t *testing.T) {
|
|
t.Run("key and file", func(t *testing.T) {
|
|
key, err := PlacementKey("g14")
|
|
if err != nil || key != "placement.g14" {
|
|
t.Fatalf("PlacementKey = %q, %v", key, err)
|
|
}
|
|
if n, err := ParsePlacementKey(key); err != nil || n != "g14" {
|
|
t.Fatalf("ParsePlacementKey = %q, %v", n, err)
|
|
}
|
|
for _, bad := range []string{"want.g14.x", "placement.", "placement.a.b"} {
|
|
if _, err := ParsePlacementKey(bad); err == nil {
|
|
t.Errorf("ParsePlacementKey(%q) took it", bad)
|
|
}
|
|
}
|
|
if _, err := PlacementKey("a.b"); err == nil {
|
|
t.Error("PlacementKey took a dotted node")
|
|
}
|
|
if PlacementBucket != "mesh-delivery-indexer_placement" || PlacementFile("ace") != "placement/ace.json" {
|
|
t.Fatalf("bucket %q, file %q", PlacementBucket, PlacementFile("ace"))
|
|
}
|
|
for p, want := range map[string]string{"placement/ace.json": "ace", "./placement/ace.json": "ace",
|
|
"placement/x/ace.json": "", "modules/ace.json": "", "placement/ace.yaml": ""} {
|
|
if n, ok := PlacementFileNode(p); n != want || ok != (want != "") {
|
|
t.Errorf("PlacementFileNode(%q) = %q, %v", p, n, ok)
|
|
}
|
|
}
|
|
})
|
|
|
|
g14 := Placement{Node: "g14", Revision: "abc",
|
|
Decided: NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}},
|
|
Modules: []PlacedModule{
|
|
{Module: "zsh"},
|
|
{Module: "app", Ports: map[string]int{"8080": 18080}, Bindings: map[string]Provider{"postgres": {Node: "novox", Module: "postgres"}}},
|
|
},
|
|
Pins: map[string]Provider{"shell": {Node: "g14", Module: "zsh"}},
|
|
Holds: []Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}},
|
|
}
|
|
novox := Placement{Node: "novox", Decided: NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []PlacedModule{{Module: "postgres", Ports: map[string]int{"5432": 5432}}},
|
|
Holds: []Holding{{Claim: "mesh-store", Scope: "mesh", Module: "postgres"}}}
|
|
|
|
t.Run("value", func(t *testing.T) {
|
|
raw, err := EncodePlacement(g14)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := DecodePlacement(raw)
|
|
if err != nil || back.V != Version || !SamePlacement(back, g14) {
|
|
t.Fatalf("round trip: %+v, %v", back, err)
|
|
}
|
|
moved := g14
|
|
moved.Revision = "def"
|
|
if !SamePlacement(g14, moved) {
|
|
t.Error("a new revision alone counts as a change")
|
|
}
|
|
if _, err := DecodePlacement([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("version 9: %v", err)
|
|
}
|
|
for name, bad := range map[string]string{
|
|
"module twice": `{"v":1,"node":"g14","modules":[{"module":"a"},{"module":"a"}]}`,
|
|
"port not number": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"http":80}}]}`,
|
|
"host port zero": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"80":0}}]}`,
|
|
"binding no node": `{"v":1,"node":"g14","modules":[{"module":"a","bindings":{"pg":{"module":"pg"}}}]}`,
|
|
"pin no module": `{"v":1,"node":"g14","modules":[],"pins":{"pg":{"node":"x"}}}`,
|
|
"hold not placed": `{"v":1,"node":"g14","modules":[],"holds":[{"claim":"s","module":"a"}]}`,
|
|
"node not a name": `{"v":1,"node":"g.14","modules":[]}`,
|
|
"value not shaped": `{"v":1,"node":"g14","modules":{}}`,
|
|
} {
|
|
if _, err := DecodePlacement([]byte(bad)); err == nil {
|
|
t.Errorf("%s: taken", name)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("as the composition reads it", func(t *testing.T) {
|
|
if got := g14.Assigned(); !reflect.DeepEqual(got, []string{"zsh", "app"}) {
|
|
t.Errorf("Assigned = %v, want the file's order", got)
|
|
}
|
|
if got := g14.ChosenPins(); !reflect.DeepEqual(got, map[string]catalogue.Chosen{"shell": {Node: "g14", Module: "zsh"}}) {
|
|
t.Errorf("ChosenPins = %v", got)
|
|
}
|
|
if got := g14.Bound(); !reflect.DeepEqual(got, map[string]map[string]catalogue.Chosen{"app": {"postgres": {Node: "novox", Module: "postgres"}}}) {
|
|
t.Errorf("Bound = %v", got)
|
|
}
|
|
if got := g14.HostPorts(); !reflect.DeepEqual(got, map[string]map[int]int{"app": {8080: 18080}}) {
|
|
t.Errorf("HostPorts = %v", got)
|
|
}
|
|
if got := g14.Held(); !reflect.DeepEqual(got, []catalogue.Held{{Claim: "node-shell", Scope: "node", Node: "g14", Module: "zsh"}}) {
|
|
t.Errorf("Held = %v", got)
|
|
}
|
|
layered := PlacedModule{Module: "app", HasMesh: true, Layer: map[string]any{"expose": map[string]any{"80": "mesh"}}}
|
|
want := []catalogue.Layer{
|
|
{From: catalogue.MeshWideLayer, Values: map[string]any{}},
|
|
{From: "g14", Values: map[string]any{"mode": "x"}, Placement: map[string]any{"expose": map[string]any{"80": "mesh"}}},
|
|
}
|
|
if got := layered.Layers("g14", map[string]any{"mode": "x"}); !reflect.DeepEqual(got, want) {
|
|
t.Errorf("Layers = %#v, want %#v", got, want)
|
|
}
|
|
if got := (PlacedModule{Module: "a"}).Layers("g14", map[string]any{"k": 1}); len(got) != 1 || got[0].From != "g14" {
|
|
t.Errorf("values alone make the node's layer: %#v", got)
|
|
}
|
|
if got := (PlacedModule{Module: "a", HasLayer: true}).Layers("g14", nil); len(got) != 1 || got[0].Values == nil {
|
|
t.Errorf("an empty layer of the node's own is kept, with empty values: %#v", got)
|
|
}
|
|
if got := (PlacedModule{Module: "a"}).Layers("g14", nil); got != nil {
|
|
t.Errorf("no layer and no value is no layer: %#v", got)
|
|
}
|
|
empty := Placement{Node: "x"}
|
|
if empty.ChosenPins() != nil || empty.Bound() != nil || empty.HostPorts() != nil || len(empty.Held()) != 0 {
|
|
t.Error("an empty placement reads as something")
|
|
}
|
|
})
|
|
|
|
t.Run("what a change says", func(t *testing.T) {
|
|
after := g14
|
|
after.Revision = "def"
|
|
after.Modules = []PlacedModule{{Module: "app", Ports: map[string]int{"8080": 18081}}, {Module: "new"}}
|
|
got := ChangeOf(g14, after)
|
|
want := PlacementChange{Node: "g14", Revision: "def", Added: []string{"new"}, Removed: []string{"zsh"}, Changed: []string{"app"}}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("ChangeOf = %+v, want %+v", got, want)
|
|
}
|
|
pinned := g14
|
|
pinned.Pins = nil
|
|
if got := ChangeOf(g14, pinned); !reflect.DeepEqual(got.Changed, []string{"app", "zsh"}) {
|
|
t.Errorf("a pin change changes every module on the node: %+v", got)
|
|
}
|
|
if got := ChangeOf(Placement{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" {
|
|
t.Errorf("a new node: %+v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("the catalogue's check", func(t *testing.T) {
|
|
held := func(m string) bool { return m != "ghost" }
|
|
enrolled := func(n string) bool { return n != "stranger" }
|
|
if r := CheckPlacements(map[string]Placement{"g14": g14, "novox": novox}, held, enrolled); len(r) != 0 {
|
|
t.Fatalf("a sound placement is refused: %v", r)
|
|
}
|
|
clash := g14
|
|
clash.Modules = append([]PlacedModule{{Module: "ghost", Ports: map[string]int{"80": 18080}}}, g14.Modules...)
|
|
clash.Modules[2] = PlacedModule{Module: "app", Ports: map[string]int{"8080": 18080}, Bindings: map[string]Provider{"postgres": {Node: "ace", Module: "postgres"}}}
|
|
clash.Pins = map[string]Provider{"shell": {Node: "g14", Module: "bash"}}
|
|
twice := novox
|
|
twice.Node = "stranger"
|
|
twice.Decided.Address = "10.77.0.2"
|
|
r := CheckPlacements(map[string]Placement{"g14": clash, "novox": novox, "stranger": twice}, held, enrolled)
|
|
for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080",
|
|
"bound to ace/postgres", "pin of shell names g14/bash", "stranger is not an enrolled node",
|
|
"mesh-store, held once for the mesh, is held on novox and stranger",
|
|
"g14 and stranger both have the address 10.77.0.2", "novox, stranger are each placed as it"} {
|
|
found := false
|
|
for _, line := range r {
|
|
found = found || strings.Contains(line, want)
|
|
}
|
|
if !found {
|
|
t.Errorf("no refusal says %q; got %v", want, r)
|
|
}
|
|
}
|
|
})
|
|
}
|