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 }