265 lines
12 KiB
Go
265 lines
12 KiB
Go
package meshmoduleassignerclient
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
func TestPlacementClient(t *testing.T) {
|
|
t.Run("proposal id", func(t *testing.T) {
|
|
for id, ok := range map[string]bool{"p-20261028t183000-1a2b3c": true, "abc": true, "ab": false, "A-bc": false, "-abc": false, "a.bc": false, "": false} {
|
|
if (CheckProposalID(id) == nil) != ok {
|
|
t.Errorf("CheckProposalID(%q) = %v", id, CheckProposalID(id))
|
|
}
|
|
}
|
|
})
|
|
t.Run("key and file", func(t *testing.T) {
|
|
// Research 072 row 37: one node key and one key per module, a legacy key read beside them, any other refused.
|
|
for key, want := range map[string]struct {
|
|
node, module string
|
|
kind KeyKind
|
|
}{"g14.node": {"g14", "", NodeKind}, "g14.app": {"g14", "app", ModuleKind},
|
|
"placement.g14": {"g14", "", LegacyKind}} {
|
|
if n, m, k, err := ParseKey(key); err != nil || n != want.node || m != want.module || k != want.kind {
|
|
t.Errorf("ParseKey(%q) = %q, %q, %d, %v", key, n, m, k, err)
|
|
}
|
|
}
|
|
for _, bad := range []string{"want.g14.x", "placement.", "placement.a.b", "g14", "g14.a.b", ".node"} {
|
|
if _, _, _, err := ParseKey(bad); err == nil {
|
|
t.Errorf("ParseKey(%q) took it", bad)
|
|
}
|
|
}
|
|
if k, err := NodeKey("g14"); err != nil || k != "g14.node" {
|
|
t.Errorf("NodeKey = %q, %v", k, err)
|
|
}
|
|
if k, err := ModuleKey("g14", "app"); err != nil || k != "g14.app" {
|
|
t.Errorf("ModuleKey = %q, %v", k, err)
|
|
}
|
|
if k, err := NodeKeys("g14"); err != nil || k != "g14.>" {
|
|
t.Errorf("NodeKeys = %q, %v", k, err)
|
|
}
|
|
if k, err := LegacyKey("g14"); err != nil || k != "placement.g14" {
|
|
t.Errorf("LegacyKey = %q, %v", k, err)
|
|
}
|
|
for name, err := range map[string]error{"a dotted node": func() error { _, err := NodeKey("a.b"); return err }(),
|
|
"a node named placement": func() error { _, err := NodeKey("placement"); return err }(),
|
|
"a module named node": func() error { _, err := ModuleKey("g14", "node"); return err }()} {
|
|
if err == nil {
|
|
t.Errorf("%s was taken", name)
|
|
}
|
|
}
|
|
})
|
|
|
|
g14 := seat.Placement{Node: "g14", Revision: "abc",
|
|
Decided: seat.NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}},
|
|
Modules: []seat.PlacedModule{
|
|
{Module: "zsh"},
|
|
{Module: "app", Ports: map[string]int64{"8080": 18080}, Bindings: map[string]seat.Provider{"postgres": {Node: "novox", Module: "postgres"}}},
|
|
},
|
|
Pins: map[string]seat.Provider{"shell": {Node: "g14", Module: "zsh"}},
|
|
Holds: []seat.Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}},
|
|
}
|
|
novox := seat.Placement{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}},
|
|
Holds: []seat.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 := Assigned(g14); !reflect.DeepEqual(got, []string{"zsh", "app"}) {
|
|
t.Errorf("Assigned = %v, want the file's order", got)
|
|
}
|
|
if got := ChosenPins(g14); !reflect.DeepEqual(got, map[string]catalogue.Chosen{"shell": {Node: "g14", Module: "zsh"}}) {
|
|
t.Errorf("ChosenPins = %v", got)
|
|
}
|
|
if got := Bound(g14); !reflect.DeepEqual(got, map[string]map[string]catalogue.Chosen{"app": {"postgres": {Node: "novox", Module: "postgres"}}}) {
|
|
t.Errorf("Bound = %v", got)
|
|
}
|
|
if got := HostPorts(g14); !reflect.DeepEqual(got, map[string]map[int]int{"app": {8080: 18080}}) {
|
|
t.Errorf("HostPorts = %v", got)
|
|
}
|
|
if got := Held(g14); !reflect.DeepEqual(got, []catalogue.Held{{Claim: "node-shell", Scope: "node", Node: "g14", Module: "zsh"}}) {
|
|
t.Errorf("Held = %v", got)
|
|
}
|
|
layered := seat.PlacedModule{Module: "app", HasMeshWide: 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 := Layers(layered, "g14", map[string]any{"mode": "x"}); !reflect.DeepEqual(got, want) {
|
|
t.Errorf("Layers = %#v, want %#v", got, want)
|
|
}
|
|
if got := Layers(seat.PlacedModule{Module: "a"}, "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 := Layers(seat.PlacedModule{Module: "a", HasLayer: true}, "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 := Layers(seat.PlacedModule{Module: "a"}, "g14", nil); got != nil {
|
|
t.Errorf("no layer and no value is no layer: %#v", got)
|
|
}
|
|
empty := seat.Placement{Node: "x"}
|
|
if ChosenPins(empty) != nil || Bound(empty) != nil || HostPorts(empty) != nil || len(Held(empty)) != 0 {
|
|
t.Error("an empty placement reads as something")
|
|
}
|
|
})
|
|
|
|
// Research 072 row 37: a node's placement as its node key and module keys, written so a reader never takes a part of
|
|
// a write, read beside the form before, and converted from it once.
|
|
t.Run("one key per module and node", func(t *testing.T) {
|
|
values, err := PlacementValues(g14)
|
|
if err != nil || len(values) != 4 || values["g14.node"] == nil || values["g14.zsh"] == nil || values["g14.app"] == nil ||
|
|
values["placement.g14"] == nil {
|
|
t.Fatalf("PlacementValues = %v, %v", values, err)
|
|
}
|
|
got, err := PlacementsOf(values)
|
|
if err != nil || !SamePlacement(got["g14"], g14) || len(got) != 1 {
|
|
t.Fatalf("PlacementsOf = %+v, %v", got, err)
|
|
}
|
|
if got := InsideAddresses(map[string]seat.Placement{"g14": g14, "novox": novox, "x": {Node: "x"}, "y": {Node: "y", Decided: seat.NodeDecided{Address: " 10.77.0.1"}}}); strings.Join(got, ",") != "10.77.0.1,10.77.0.2" {
|
|
t.Errorf("InsideAddresses = %v", got)
|
|
}
|
|
legacy, _ := EncodePlacement(novox)
|
|
stale, _ := EncodePlacement(seat.Placement{Node: "g14"})
|
|
both := map[string][]byte{"placement.novox": legacy, "placement.g14": stale}
|
|
for k, v := range values {
|
|
both[k] = v
|
|
}
|
|
if got, err := PlacementsOf(both); err != nil || !SamePlacement(got["g14"], g14) || !SamePlacement(got["novox"], novox) {
|
|
t.Errorf("a node key wins over a legacy key, and a legacy key alone places its node: %+v, %v", got, err)
|
|
}
|
|
|
|
keys := NewKeys()
|
|
rev := uint64(0)
|
|
apply := func(writes []KeyWrite, upTo int) {
|
|
t.Helper()
|
|
for i, w := range writes {
|
|
if i == upTo {
|
|
return
|
|
}
|
|
rev++
|
|
if _, err := keys.Put(w.Key, w.Value, rev, w.Value == nil); err != nil {
|
|
t.Fatalf("write %d (%s): %v", i, w.Key, err)
|
|
}
|
|
}
|
|
}
|
|
// The form before converts once: its node and module keys, the legacy key left for the readers not yet delivered.
|
|
rev++
|
|
legacyG14, _ := EncodePlacement(g14)
|
|
if _, err := keys.Put("placement.g14", legacyG14, rev, false); err != nil || !keys.Legacy("g14") {
|
|
t.Fatalf("the legacy key: %v", err)
|
|
}
|
|
migrations, err := keys.Migrations()
|
|
if err != nil || len(migrations["g14"]) != 3 || !migrations["g14"][2].Commit {
|
|
// The legacy key holds the placement already: no mirror write.
|
|
t.Fatalf("Migrations = %+v, %v", migrations, err)
|
|
}
|
|
apply(migrations["g14"], -1)
|
|
if m, _ := keys.Migrations(); len(m) != 0 || keys.Legacy("g14") {
|
|
t.Fatalf("a converted node converts again: %+v", m)
|
|
}
|
|
if p, r, ok := keys.Of("g14"); !ok || !SamePlacement(p, g14) || r != rev {
|
|
t.Fatalf("Of after the conversion = %+v, %d, %v", p, r, ok)
|
|
}
|
|
|
|
// A write under way is never read half: app's new port is not read before the node key names it.
|
|
after := clonePlacement(g14)
|
|
after.Modules = []seat.PlacedModule{{Module: "app", Ports: map[string]int64{"8080": 18081}}, {Module: "new"}}
|
|
after.Holds, after.Pins = nil, nil
|
|
writes, err := keys.Writes("g14", &after)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var kinds []string
|
|
for _, w := range writes {
|
|
kinds = append(kinds, fmt.Sprintf("%s:%v:%d", w.Key, w.Value != nil, w.Base))
|
|
}
|
|
if strings.Join(kinds, " ") != "g14.app:true:3 g14.new:true:0 g14.node:true:4 placement.g14:true:1 g14.zsh:false:2" ||
|
|
!writes[3].Mirror {
|
|
t.Fatalf("Writes = %v", kinds)
|
|
}
|
|
apply(writes, 2)
|
|
if p, _, _ := keys.Of("g14"); !SamePlacement(p, g14) {
|
|
t.Errorf("the module keys written before the node key are read: %+v", p)
|
|
}
|
|
apply(writes[2:], -1)
|
|
if p, _, _ := keys.Of("g14"); !SamePlacement(p, after) {
|
|
t.Errorf("the write done is not read whole: %+v", p)
|
|
}
|
|
if legacy, _ := PlacementsOf(map[string][]byte{"placement.g14": writes[3].Value}); !SamePlacement(legacy["g14"], after) {
|
|
t.Errorf("the mirror is not the placement written: %+v", legacy)
|
|
}
|
|
// A module key that stays is not written again, nor a mirror that holds it.
|
|
if again, _ := keys.Writes("g14", &after); len(again) != 1 || !again[0].Commit {
|
|
t.Errorf("an unchanged placement writes %+v", again)
|
|
}
|
|
// Keys that do not agree keep what the node had, and say so.
|
|
stray, _ := EncodeModule(seat.PlacementByNodeModuleValue{Node: "g14", Placed: seat.PlacedModule{Module: "app"}})
|
|
if _, err := keys.Put("g14.app", stray, 99, false); err == nil {
|
|
t.Error("a module key the node key does not name the value of was taken silently")
|
|
}
|
|
if p, _, _ := keys.Of("g14"); !SamePlacement(p, after) {
|
|
t.Errorf("a stray module key moved the placement: %+v", p)
|
|
}
|
|
// The holder's next write puts the module key right again.
|
|
repair, _ := keys.Writes("g14", &after)
|
|
if len(repair) != 2 || repair[0].Key != "g14.app" || repair[0].Base != 99 || !repair[1].Commit {
|
|
t.Fatalf("the repair writes %+v", repair)
|
|
}
|
|
apply(repair, -1)
|
|
// No placement: the legacy key first, then the node key, then the module keys; none is read after.
|
|
gone, _ := keys.Writes("g14", nil)
|
|
kinds = nil
|
|
for _, w := range gone {
|
|
kinds = append(kinds, w.Key)
|
|
}
|
|
if strings.Join(kinds, " ") != "placement.g14 g14.node g14.app g14.new" {
|
|
t.Errorf("Writes(nil) = %v", kinds)
|
|
}
|
|
apply(gone, 2)
|
|
if _, _, ok := keys.Of("g14"); ok {
|
|
t.Error("a node key deleted still places its node")
|
|
}
|
|
if _, err := DecodeNode([]byte(`{"v":1,"node":"g14","modules":[{"module":"a","digest":"x"},{"module":"a","digest":"x"}]}`)); err == nil {
|
|
t.Error("a node key naming a module twice reads")
|
|
}
|
|
})
|
|
|
|
}
|