Files
mesh-controller/internal/catalogue/holds_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

101 lines
4.9 KiB
Go

package catalogue
import (
"encoding/json"
"strings"
"testing"
)
// novox/hq ADR 0377 §3: a move under a module's own `record` rolls out when it recreates none of its containers
// and moves none of its data, judged from the two builds' manifests.
func TestWhatAMoveUnderRecordHoldsFor(t *testing.T) {
base := func() Manifest {
raw := `{"module":"mailu","tools":["mailu_status"],
"requires":["postgres-database"],
"data":{"own":[{"id":"mail","path":"${dir:data-mail}","class":"valuable"}]},
"bundles":[{"name":"tools","source":"store/mailu-tools@sha256:aaa","digest":"sha256:aaa"}],
"resources":[
{"id":"state","type":"directory"},
{"id":"mesh-state","type":"directory"},
{"id":"data-mail","type":"directory"},
{"id":"config-env","type":"file","path":"${dir:state}/mailu.env","content":"A=1"},
{"id":"runtime-config","type":"file","path":"${dir:mesh-state}/config.json","content":"{}"},
{"id":"inbox-note","type":"file","path":"${dir:data-mail}/README","content":"x"},
{"id":"imap","type":"container","image":"registry/imap@sha256:1","volumes":["${dir:data-mail}:/mail"],
"env-file":["${dir:state}/mailu.env"]},
{"id":"watcher","type":"process","command":"watch"},
{"id":"installation","type":"archive","path":"/opt/mailu-tools","artifact":"archive"},
{"id":"guard","type":"package","package":"nftables"}]}`
var m Manifest
if err := json.Unmarshal([]byte(raw), &m); err != nil {
t.Fatal(err)
}
return m
}
resource := func(m *Manifest, id string) map[string]any {
for _, r := range m.Resources {
if r["id"] == id {
return r
}
}
t.Fatalf("no resource %s", id)
return nil
}
cases := []struct {
name string
change func(m *Manifest)
holds string // empty: it rolls out
}{
{"nothing", func(*Manifest) {}, ""},
{"a tools bundle", func(m *Manifest) {
m.Bundles[0].Digest, m.Bundles[0].Source = "sha256:bbb", "store/mailu-tools@sha256:bbb"
}, ""},
{"the tool list", func(m *Manifest) { m.Tools = append(m.Tools, "mailu_queue") }, ""},
{"a file no container mounts", func(m *Manifest) { resource(m, "runtime-config")["content"] = `{"a":1}` }, ""},
{"a container's image", func(m *Manifest) { resource(m, "imap")["image"] = "registry/imap@sha256:2" }, "recreates 1 of mailu's 1"},
{"a container's declaration", func(m *Manifest) { resource(m, "imap")["health"] = map[string]any{"kind": "runtime"} }, "recreates 1"},
{"a container added", func(m *Manifest) {
m.Resources = append(m.Resources, map[string]any{"id": "smtp", "type": "container", "image": "x"})
}, "recreates"},
{"a mounted file", func(m *Manifest) { resource(m, "config-env")["content"] = "A=2" }, "which its container imap mounts"},
{"a file within declared data", func(m *Manifest) { resource(m, "inbox-note")["content"] = "y" }, "which its container imap mounts"},
{"a directory holding declared data", func(m *Manifest) { resource(m, "data-mail")["mode"] = "0700" }, "data-mail"},
{"the data declaration", func(m *Manifest) { m.Data.Own[0].Class = ClassIrreplaceable }, "data declaration"},
{"a provider", func(m *Manifest) { m.Requires = append(m.Requires, "s3") }, "what it requires"},
{"what it provides", func(m *Manifest) { m.Provides = append(m.Provides, Offer{Name: "smtp"}) }, "what it provides"},
{"a process of a module with data", func(m *Manifest) { resource(m, "watcher")["command"] = "watch2" }, "long-running process watcher"},
{"an archive resource", func(m *Manifest) { resource(m, "installation")["artifact"] = "archive2" }, "the archive installation"},
{"a package resource", func(m *Manifest) { resource(m, "guard")["package"] = "nftables2" }, "the package guard"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
from, to := base(), base()
c.change(&to)
got := Holds(from, to)
switch {
case c.holds == "" && got != "":
t.Fatalf("held: %s", got)
case c.holds != "" && !strings.Contains(got, c.holds):
t.Fatalf("holds %q; want it to say %q", got, c.holds)
}
})
}
// A data directory nothing mounts still holds its data.
from, to := base(), base()
from.Data.Own = append(from.Data.Own, DataItem{ID: "archive", Path: "${dir:mesh-state}/archive", Class: "valuable"})
to.Data = &Data{Own: append([]DataItem(nil), from.Data.Own...)}
resource(&to, "runtime-config")["path"] = "${dir:mesh-state}/archive/config.json"
resource(&from, "runtime-config")["path"] = "${dir:mesh-state}/archive/config.json"
resource(&to, "runtime-config")["content"] = "new"
if got := Holds(from, to); !strings.Contains(got, "lies within its data archive") {
t.Fatalf("a file within data nothing mounts: %q", got)
}
// A process of a module with no data of its own may change.
from, to = base(), base()
from.Data, to.Data = nil, nil
resource(&to, "watcher")["command"] = "watch2"
if got := Holds(from, to); got != "" {
t.Fatalf("a process of a module with no data held: %s", got)
}
}