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") } }) }