Files
mesh-controller/internal/inventory/placement_words_test.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

263 lines
11 KiB
Go

package inventory
import (
"encoding/json"
"reflect"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// reachedForge is a module every one of the four words reaches: two endpoints, both published, one with a
// directory to place.
func reachedForge() catalogue.Manifest {
return catalogue.Manifest{Module: "forge", Version: "1",
Listens: []catalogue.Listening{{Name: "web", Port: 3000, Protocol: "tcp", From: catalogue.FromMesh},
{Name: "ssh", Port: 22, Protocol: "tcp", From: catalogue.FromMesh}},
Resources: []map[string]any{
{"id": "data", "type": "directory", "mode": "0750"},
{"id": "server", "type": "container", "ports": []any{"3000", "222:22"},
"image": "registry.invalid:5000/forge@sha256:" + strings.Repeat("a", 64),
"volumes": []any{"${dir:data}:/data"}},
{"id": "runtime-config", "type": "file", "path": "${dir:data}/runtime.json", "mode": "0640",
"content": `{"log":"info"}`, "merge": "json"},
}}
}
// **The four words are kept beside the layer, never in it** (novox/hq ADR 0348 clause 11, issue 736): a set of
// `endpoints`, `expose`, `reach` and `ports` keeps them as placement and the rest as the layer; the layer reads
// back whole, composition reads them as placement, and a port given through the placement is held against the
// next module as a port given in a layer was.
func TestTheFourWordsAreKeptBesideTheLayer(t *testing.T) {
inv := ForTest(t)
ctx := t.Context()
for _, m := range []catalogue.Manifest{reachedForge(), {Module: "other", Version: "1",
Listens: []catalogue.Listening{{Name: "ssh", Port: 2200, Protocol: "tcp", From: catalogue.FromMesh}},
Resources: []map[string]any{{"id": "server", "type": "container", "ports": []any{"2200"}, "image": "x"}}}} {
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
t.Fatal(err)
}
}
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
t.Fatal(err)
}
set := map[string]any{"log": "warn",
"expose": map[string]any{"22": "anywhere"},
"ports": map[string]any{"22": float64(2222)},
"reach": map[string]any{"3000": "internal"},
"endpoints": map[string]any{"web": map[string]any{"label": "git"}},
"places": map[string]any{"data": map[string]any{"path": "/var/lib/forge/data"}}}
if err := inv.SetSettings(ctx, "anchor", "forge", set); err != nil {
t.Fatal(err)
}
stored := inv.QueryForTest(t, `select s.values, p.values from settings s join placement p using (module, node)
where s.node is not null`)
if len(stored) != 1 || stored[0][0] != `{"log":"warn"}` {
t.Fatalf("the layer is kept as %v", stored)
}
for _, word := range []string{"expose", "ports", "reach", "endpoints", "places"} {
if !strings.Contains(stored[0][1], `"`+word+`"`) {
t.Errorf("%s is not kept beside the layer: %v", word, stored[0][1])
}
}
if got, _, _ := inv.Layer(ctx, "anchor", "forge"); !reflect.DeepEqual(roundTrip(t, got), roundTrip(t, set)) {
t.Fatalf("the layer reads %v, was set %v", got, set)
}
layers, err := inv.SettingsFor(ctx, "anchor", "forge")
if err != nil || len(layers) != 1 || len(layers[0].Values) != 1 || len(layers[0].Placement) != 5 {
t.Fatalf("composition reads %+v (%v)", layers, err)
}
if given, err := catalogue.GivenPorts(reachedForge(), layers); err != nil || given[22] != 2222 {
t.Fatalf("the given port is %v (%v)", given, err)
}
// Held against the next module: the machine port is the forge's.
err = inv.SetSettings(ctx, "anchor", "other", map[string]any{"ports": map[string]any{"2200": float64(2222)}})
if err == nil || !strings.Contains(err.Error(), "2222") {
t.Fatalf("a machine port the placement gave was given again: %v", err)
}
anchor, err := inv.NodeByName(ctx, "anchor")
if err != nil {
t.Fatal(err)
}
given, err := inv.givenOn(ctx, anchor.ID)
if err != nil || given[2222] != "forge" {
t.Fatalf("the ports given on anchor read %v (%v)", given, err)
}
// And a mesh-wide port is refused as before.
if err := inv.SetSettings(ctx, "", "forge", map[string]any{"ports": map[string]any{"22": float64(2222)}}); err == nil ||
!strings.Contains(err.Error(), "per node") {
t.Fatalf("a mesh-wide port: %v", err)
}
}
// The layers of the issue (736) as the store holds them once migration 0097 ran and before the move: the four words
// still among the settings, a place already beside one of them, a mesh-wide reach, and a layer with none.
var wordedLayers = []struct{ node, settings, placement string }{
{"", `{"reach": {"3000": "internal"}, "log": "warn"}`, ``},
{"anchor", `{"expose": {"22": "anywhere"}, "ports": {"22": 2222}, "endpoints": {"web": {"label": "git"}}, "workers": 4}`,
`{"places": {"data": {"path": "/var/lib/forge/data"}}}`},
{"laptop", `{"ports": {"22": 2022}}`, ``},
{"spare", `{"log": "debug"}`, ``},
}
func seedWordedLayers(t *testing.T) *Inventory {
t.Helper()
inv := ForTest(t)
ctx := t.Context()
if err := inv.RegisterModule(ctx, reachedForge(), Source{}); err != nil {
t.Fatal(err)
}
for _, n := range []string{"anchor", "laptop", "spare"} {
if _, err := inv.AddNode(ctx, n); err != nil {
t.Fatal(err)
}
}
for _, l := range wordedLayers {
if l.node == "" {
inv.ExecForTest(t, `insert into settings (node, module, values, set_by) values (null, 'forge', $1, 'the operator')`,
l.settings)
continue
}
inv.ExecForTest(t, `insert into settings (node, module, values) select id, 'forge', $2 from node where name = $1`,
l.node, l.settings)
if l.placement != "" {
inv.ExecForTest(t, `insert into placement (node, module, values) select id, 'forge', $2 from node where name = $1`,
l.node, l.placement)
}
}
return inv
}
// wholeLayer is a seeded layer as a person said it: its settings and its placement in one map.
func wholeLayer(t *testing.T, settings, placement string) map[string]any {
t.Helper()
out := map[string]any{}
for _, text := range []string{settings, placement} {
if text == "" {
continue
}
var part map[string]any
if err := json.Unmarshal([]byte(text), &part); err != nil {
t.Fatal(err)
}
for k, v := range part {
out[k] = v
}
}
return out
}
// **A layer that still holds the four words reads as placement until the move has run** (issue 736): the move is
// a verb run after the delivery, so until then composition finds the words among a layer's settings and reads them
// as its placement, beside what is kept beside it, and a port given there is held as before.
func TestALayerStillHoldingTheWordsReadsThemAsPlacement(t *testing.T) {
inv := seedWordedLayers(t)
ctx := t.Context()
layers, err := inv.SettingsFor(ctx, "anchor", "forge")
if err != nil || len(layers) != 2 {
t.Fatalf("anchor's layers: %v, %v", layers, err)
}
if layers[0].Values["log"] != "warn" || layers[0].Values["reach"] != nil || layers[0].Placement["reach"] == nil {
t.Fatalf("the mesh's layer split wrong: %+v", layers[0])
}
if layers[1].Values["workers"] == nil || len(layers[1].Values) != 1 || len(layers[1].Placement) != 4 {
t.Fatalf("anchor's layer split wrong: %+v", layers[1])
}
for i, l := range layers {
want := wholeLayer(t, wordedLayers[i].settings, wordedLayers[i].placement)
if !reflect.DeepEqual(roundTrip(t, l.Whole()), want) {
t.Errorf("anchor's layer %s is %v whole, was %v", l.From, l.Whole(), want)
}
}
if given, err := catalogue.GivenPorts(reachedForge(), layers); err != nil || given[22] != 2222 {
t.Fatalf("the given port is %v (%v)", given, err)
}
}
// **The move of placement moves what a layer still holds, and says what it did** (issue 736). Asked without
// applying, it says which layers hold which words and changes nothing. Applied, no settings row holds a placement
// key after, every layer reads whole as it did and composes as it did, each moved layer is kept whole in the
// history with why, when it was set and who set it stay, a layer without placement is not touched, and asked again
// it moves nothing.
func TestTheMoveOfPlacementMovesWhatTheLayersStillHold(t *testing.T) {
inv := seedWordedLayers(t)
ctx := t.Context()
before := map[string][]catalogue.Layer{}
for _, n := range []string{"anchor", "laptop", "spare"} {
layers, err := inv.SettingsFor(ctx, n, "forge")
if err != nil {
t.Fatal(err)
}
before[n] = layers
}
rows := `select coalesce(n.name, ''), s.values, s.set_at, s.set_by from settings s left join node n on n.id = s.node order by 1`
stored := inv.QueryForTest(t, rows)
setAt := inv.QueryForTest(t, `select coalesce(n.name, ''), s.set_at, s.set_by from settings s left join node n on n.id = s.node order by 1`)
want := []MovedLayer{
{Node: "", Module: "forge", Keys: []string{"reach"}},
{Node: "anchor", Module: "forge", Keys: []string{"endpoints", "expose", "ports"}},
{Node: "laptop", Module: "forge", Keys: []string{"ports"}},
}
said, err := inv.MovePlacement(ctx, false)
if err != nil || !reflect.DeepEqual(said, want) {
t.Fatalf("the move, not applied, says %+v (%v), want %+v", said, err, want)
}
if after := inv.QueryForTest(t, rows); !reflect.DeepEqual(after, stored) {
t.Fatalf("the move not applied changed the layers: %v, was %v", after, stored)
}
moved, err := inv.MovePlacement(ctx, true)
if err != nil || !reflect.DeepEqual(moved, want) {
t.Fatalf("the move says %+v (%v), want %+v", moved, err, want)
}
if held := inv.QueryForTest(t, `select module from settings where values ?| $1`, catalogue.PlacementKeys()); len(held) != 0 {
t.Fatalf("a settings layer still holds a placement key: %v", held)
}
if after := inv.QueryForTest(t, `select coalesce(n.name, ''), s.set_at, s.set_by from settings s left join node n on n.id = s.node order by 1`); !reflect.DeepEqual(after, setAt) {
t.Fatalf("the layers or when and by whom they were set changed: %v, was %v", after, setAt)
}
for i, l := range wordedLayers {
got, has, err := inv.Layer(ctx, l.node, "forge")
if want := wholeLayer(t, l.settings, l.placement); err != nil || !has || !reflect.DeepEqual(roundTrip(t, got), want) {
t.Errorf("layer %d on %q reads %v (%v, %v), was %v", i, l.node, got, has, err, want)
}
}
for n, was := range before {
layers, err := inv.SettingsFor(ctx, n, "forge")
if err != nil || !reflect.DeepEqual(roundTripLayers(t, layers), roundTripLayers(t, was)) {
t.Errorf("%s composes from %+v (%v), was %+v", n, layers, err, was)
}
}
history := inv.QueryForTest(t, `select coalesce(n.name, ''), h.replaced_by, h.why, h.set_by from settings_history h
left join node n on n.id = h.node order by 1`)
if !reflect.DeepEqual(history, [][]string{
{`""`, `"moved"`, `"moved to placement, ADR 0348 clause 11"`, `"the operator"`},
{`"anchor"`, `"moved"`, `"moved to placement, ADR 0348 clause 11"`, `null`},
{`"laptop"`, `"moved"`, `"moved to placement, ADR 0348 clause 11"`, `null`},
}) {
t.Fatalf("the history of what moved: %v", history)
}
past, err := inv.SettingsHistory(ctx, "anchor", "forge")
if err != nil || len(past) != 1 || past[0].Values["expose"] == nil || past[0].Values["places"] == nil || past[0].Values["workers"] == nil {
t.Fatalf("anchor's moved layer is not kept whole: %+v, %v", past, err)
}
if again, err := inv.MovePlacement(ctx, true); err != nil || len(again) != 0 {
t.Fatalf("asked again, the move moved %+v (%v)", again, err)
}
if n := inv.QueryForTest(t, `select count(*) from settings_history`); n[0][0] != "3" {
t.Fatalf("asked again, the move kept history: %v", n)
}
}
func roundTripLayers(t *testing.T, layers []catalogue.Layer) []map[string]any {
t.Helper()
out := make([]map[string]any, len(layers))
for i, l := range layers {
out[i] = map[string]any{"from": l.From, "values": roundTrip(t, l.Values), "placement": roundTrip(t, l.Placement)}
}
return out
}