mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/no-change-takes-effect-unseen delivered: every member is delivered
Two incidents had one shape: a change took effect that nobody saw first (hq issues 241, 304). Three guards, each silent when nothing is at stake: - settings show [--history], and a set that answers each key it adds, changes and removes, and refuses a removal unless --replace; the replaced or cleared layer is kept in settings_history, in the same transaction as the write (migration 0078). - every send keeps a summary of what it sent (no file content), plan <node> --diff compares with it, and push with no machine is refused unless --all. - a push that would give a running container's mount another host directory holds that machine, naming the module, mount and both directories, until push <node> --move <module>; a named push's cascade is held the same way. Rebased onto main and fitted to it: the hold runs before the push says what it recreates, a whole push still says so first and leaves machines waiting for a gate, the verb's push with no machine is still --behind and a push still needs why. The verbs take plan diff, settings replace and history, and push move beside a machine, each refused where it cannot take effect.
105 lines
3.9 KiB
Go
105 lines
3.9 KiB
Go
package inventory
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// novox/hq ADR 0217: a settings layer can be read, and the one a set or a clear replaced is kept, so
|
|
// a previous value is one command away rather than in a database backup (novox/hq issue 304).
|
|
func TestTheLayerASetReplacesIsKeptAndReadable(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
web := catalogue.Manifest{Module: "web", Version: "1",
|
|
Resources: []map[string]any{
|
|
{"id": "server", "type": "container", "name": "web", "image": "x"},
|
|
{"id": "conf", "type": "file", "path": "/etc/web.json", "content": "{}", "merge": "json"},
|
|
}}
|
|
if err := inv.RegisterModule(ctx, web, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, has, err := inv.Layer(ctx, "anchor", "web"); err != nil || has {
|
|
t.Fatalf("a layer nobody set: %v %v", has, err)
|
|
}
|
|
first := map[string]any{"colour": "blue", "size": "large"}
|
|
if err := inv.SetSettings(ctx, "anchor", "web", first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, has, err := inv.Layer(ctx, "anchor", "web")
|
|
if err != nil || !has || got["colour"] != "blue" || got["size"] != "large" {
|
|
t.Fatalf("the layer read back: %v %v %v", got, has, err)
|
|
}
|
|
if past, err := inv.SettingsHistory(ctx, "anchor", "web"); err != nil || len(past) != 0 {
|
|
t.Fatalf("a first set replaced something: %v %v", past, err)
|
|
}
|
|
|
|
if err := inv.SetSettings(ctx, "anchor", "web", map[string]any{"colour": "red"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.ClearSettings(ctx, "anchor", "web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
past, err := inv.SettingsHistory(ctx, "anchor", "web")
|
|
if err != nil || len(past) != 2 {
|
|
t.Fatalf("history %v %v", past, err)
|
|
}
|
|
// The latest first: the clear took the red layer, the set took the first.
|
|
if past[0].ReplacedBy != "clear" || past[0].Values["colour"] != "red" {
|
|
t.Errorf("the cleared layer: %+v", past[0])
|
|
}
|
|
if past[1].ReplacedBy != "set" || past[1].Values["size"] != "large" {
|
|
t.Errorf("the replaced layer still has what the set dropped: %+v", past[1])
|
|
}
|
|
// The mesh-wide layer keeps its own history, apart from the node's.
|
|
if err := inv.SetSettings(ctx, "", "web", map[string]any{"colour": "green"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SetSettings(ctx, "", "web", map[string]any{"colour": "grey"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mesh, err := inv.SettingsHistory(ctx, "", "web")
|
|
if err != nil || len(mesh) != 1 || mesh[0].Values["colour"] != "green" {
|
|
t.Fatalf("the mesh-wide history %v %v", mesh, err)
|
|
}
|
|
// And a mesh-wide clear keeps the layer it removes, beside the node's untouched.
|
|
if err := inv.ClearSettings(ctx, "", "web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, has, err := inv.Layer(ctx, "", "web"); err != nil || has {
|
|
t.Fatalf("a cleared mesh-wide layer is still there: %v %v", has, err)
|
|
}
|
|
mesh, err = inv.SettingsHistory(ctx, "", "web")
|
|
if err != nil || len(mesh) != 2 || mesh[0].ReplacedBy != "clear" || mesh[0].Values["colour"] != "grey" {
|
|
t.Fatalf("the mesh-wide clear was not kept: %+v %v", mesh, err)
|
|
}
|
|
if past, err := inv.SettingsHistory(ctx, "anchor", "web"); err != nil || len(past) != 2 {
|
|
t.Fatalf("the node's history changed with the mesh's: %v %v", past, err)
|
|
}
|
|
}
|
|
|
|
// What a machine was last sent is kept, summarised, and read back by its name; a machine sent
|
|
// nothing since has nothing to compare with.
|
|
func TestWhatAMachineWasSentIsKeptForComparison(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
n, err := inv.AddNode(ctx, "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := inv.SentSummary(ctx, "anchor"); err != nil || len(got) != 0 {
|
|
t.Fatalf("a machine never sent anything: %s %v", got, err)
|
|
}
|
|
if err := inv.RecordSentSummary(ctx, n.ID, []byte(`[{"id":"web.server","type":"container"}]`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := inv.SentSummary(ctx, "anchor")
|
|
if err != nil || len(got) == 0 {
|
|
t.Fatalf("read back: %s %v", got, err)
|
|
}
|
|
}
|