ADR 0377 §3's allowlist names only tools bundles, files no container mounts and holds no data, and manifest fields that recreate nothing. Holds only checked containers, files, directories and processes/services; any other resource type (archive, package, user, network, ...) fell through unheld. Add a default case that holds and names the resource type and id.
101 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|