Archived
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)
|
|
}
|
|
}
|