263 lines
11 KiB
Go
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
|
|
}
|