diff --git a/go/catalogue/bus_snapshot_test.go b/go/catalogue/bus_snapshot_test.go index 69bd737..789733f 100644 --- a/go/catalogue/bus_snapshot_test.go +++ b/go/catalogue/bus_snapshot_test.go @@ -43,4 +43,49 @@ func TestTheCataloguesBusIsBackedUpBySnapshot(t *testing.T) { if !found { t.Fatal("the bus declares no item for its streams") } + // 774/S3 (ADR 0594 §7): the one list of what is the bus server names the bus's container and every resource that + // restarts it, read from the catalogue's nats manifest at mesh-catalog 0b2d393b; a module not providing the bus + // has none. + t.Run("the bus server's list", func(t *testing.T) { + raw, err := os.ReadFile("testdata/catalogue-0b2d393b-nats.module.json") + if err != nil { + t.Fatal(err) + } + nats, err := decodeForTest(raw) + if err != nil { + t.Fatal(err) + } + server := BusServerOf(nats) + var containers int + for _, r := range nats.Resources { + if r["type"] != "container" { + continue + } + containers++ + restarts, _ := r["restart-on"].([]any) + if !server[r["id"].(string)] || len(restarts) == 0 { + t.Errorf("the bus's container %v (restarted on %v) is not all in the server's list %v", r["id"], restarts, server) + } + for _, id := range restarts { + if !server[id.(string)] { + t.Errorf("%v restarts the bus server and is not in its list %v", id, server) + } + } + } + if containers != 1 || len(server) != len(BusServer) { + t.Errorf("the bus has %d containers and its server is %v of %v", containers, server, BusServer) + } + xterm, err := os.ReadFile("testdata/catalogue-835bf22b-xterm.module.json") + if err != nil { + t.Fatal(err) + } + other, err := decodeForTest(xterm) + if err != nil { + t.Fatal(err) + } + other.Resources = append(other.Resources, map[string]any{"id": "server", "type": "container"}) + if got := BusServerOf(other); len(got) != 0 { + t.Errorf("a module not providing the bus has a bus server %v", got) + } + }) } diff --git a/go/catalogue/testdata/catalogue-0b2d393b-nats.module.json b/go/catalogue/testdata/catalogue-0b2d393b-nats.module.json new file mode 100644 index 0000000..01f3679 --- /dev/null +++ b/go/catalogue/testdata/catalogue-0b2d393b-nats.module.json @@ -0,0 +1,236 @@ +{ + "module": "nats", + "version": "1", + "upgrade": { + "policy": "record", + "why": "the bus: replaced only as a planned step a person starts (`bus upgrade`), its streams snapshotted first and checked after (hq ADR 0236); the controller holds this whatever is said here" + }, + "provides": [ + { + "name": "mesh-bus", + "scope": "mesh", + "credential": { + "form": "none" + } + } + ], + "claims": [ + { + "name": "mesh-broker", + "scope": "mesh" + } + ], + "bus-users": "/var/lib/nats-module/conf/accounts.conf", + "own-secrets": { + "broker": "${dir:mesh-state}/broker", + "broker-nats-tools": "${dir:tools-secret}/broker-nats-tools" + }, + "capabilities": [ + "container-runtime" + ], + "emits": [ + "consumer-removed", + "bucket-keys-removed", + "stream-removed", + "bucket-expiry-set" + ], + "invokes": [ + "seat:mesh-hand-acts.hand-act" + ], + "consumes": [], + "listens": [ + { + "name": "bus", + "port": 4222, + "protocol": "tcp", + "from": "mesh", + "why": "the mesh bus — every link the mesh has, over TLS, reached across the overlay" + } + ], + "tools": [ + "nats_server", + "nats_connections", + "nats_closed_connections", + "nats_subscriptions", + "nats_streams", + "nats_backlog", + "nats_buckets", + "nats_users", + "nats_user_can", + "nats_snapshot_verify", + "nats_consumer_remove", + "nats_bucket_remove", + "nats_stream_remove", + "nats_bucket_expiry" + ], + "guards": [ + 8222 + ], + "data": { + "own": [ + { + "id": "jetstream", + "path": "${dir:jetstream-data}", + "class": "valuable", + "active": "1d", + "backup": { + "dump": "docker exec -i mesh-broker-nats /opt/mesh/mesh-nats-snapshot snapshot < ${dir:mesh-state}/broker > ${dir:snapshots}/bus.tar.partial && mv ${dir:snapshots}/bus.tar.partial ${dir:snapshots}/bus.tar || { rm -f ${dir:snapshots}/bus.tar.partial; exit 1; }", + "into": "snapshots" + }, + "why": "the bus's streams and key-value buckets: the hand-act log, conditions, every module's state; written all the time, so copied by the server's own snapshot of each stream (novox/hq ADR 0235), never as live files" + }, + { + "id": "snapshots", + "path": "${dir:snapshots}", + "class": "rebuildable", + "why": "last night's snapshot of every stream with its manifest, made again every night" + } + ] + }, + "resources": [ + { + "id": "jetstream-data", + "type": "directory", + "path": "/var/lib/mesh-broker-nats", + "mode": "0700", + "unknown": "program" + }, + { + "id": "conf-dir", + "type": "directory", + "path": "/var/lib/nats-module/conf", + "mode": "0700", + "unknown": "program" + }, + { + "id": "tools-secret", + "type": "directory", + "path": "/var/lib/mesh/nats-tools", + "mode": "0711", + "unknown": "program" + }, + { + "id": "snapshots", + "type": "directory", + "mode": "0700", + "unknown": "program" + }, + { + "id": "mesh-state", + "type": "directory", + "mode": "0700", + "place": "mesh", + "unknown": "program" + }, + { + "id": "server-conf", + "type": "file", + "path": "/var/lib/nats-module/conf/nats.conf", + "template": "files/nats.conf", + "mode": "0644" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": ".", + "unknown": "program" + }, + { + "id": "program-snapshot", + "type": "archive", + "path": "${dir:state}/snapshot", + "artifact": "snapshot", + "unknown": "program" + }, + { + "id": "program-entrypoint", + "type": "archive", + "path": "${dir:state}/entrypoint", + "artifact": "entrypoint", + "unknown": "program" + }, + { + "id": "server", + "type": "container", + "name": "mesh-broker-nats", + "ports": [ + "4222:4222", + "127.0.0.1:8222:8222" + ], + "volumes": [ + "/var/lib/mesh-broker-nats:/data", + "/var/lib/nats-module/conf:/etc/nats:ro", + "${access:tls}:/tls:ro", + "${dir:state}/snapshot:/opt/mesh:ro", + "${dir:state}/entrypoint:/opt/mesh-entrypoint:ro" + ], + "args": [ + "sh", + "/opt/mesh-entrypoint/entrypoint.sh" + ], + "artifact": "server", + "restart-on": [ + "program-snapshot", + "program-entrypoint" + ] + } + ], + "accesses": [ + { + "id": "tls", + "path": "/var/lib/mesh-broker-tls", + "mode": "read" + } + ], + "build": { + "artifacts": [ + { + "name": "server", + "kind": "upstream", + "from": "nats:2.11.17-alpine@sha256:e4bf19f15fd3218814a4e3c9e0064e1334bd8aa20d5984b9f1a0afd084f8cc00" + }, + { + "name": "snapshot", + "kind": "bundle", + "language": "go", + "system": "alpine", + "from": "cmd/mesh-nats-snapshot", + "binary": "mesh-nats-snapshot" + }, + { + "name": "entrypoint", + "kind": "archive", + "from": "program" + }, + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/nats-tools", + "binary": "nats-tools", + "loads": [ + "nats-tools" + ], + "bus": { + "removes": [ + "controller-leftovers" + ] + }, + "env": { + "MESH_NATS_MONITOR": "http://127.0.0.1:8222", + "MESH_NATS_CONTAINER": "mesh-broker-nats", + "MESH_NATS_USERS_FILE": "/etc/nats/accounts.conf", + "MESH_NATS_SNAPSHOTS_DIR": "${dir:snapshots}", + "NATS_TOOLS_CREDENTIAL": "${dir:tools-secret}/broker-nats-tools" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/catalogue/upgrade.go b/go/catalogue/upgrade.go index a89e994..c22c5e1 100644 --- a/go/catalogue/upgrade.go +++ b/go/catalogue/upgrade.go @@ -2,6 +2,7 @@ package catalogue import ( "fmt" + "slices" "strings" "time" ) @@ -69,6 +70,26 @@ func ProvidesBus(m Manifest) bool { return false } +// BusServer names the resources of a module that provides the mesh's bus which are its server (novox/hq ADR 0594 §7): +// the server's container and what restarts it. A node agent applying the bus module's want without the warrant's mark +// leaves these as they run (§2), and the assigner says which resources wait for the warrant from this list; neither +// keeps a copy of it. The ids are the catalogue's nats module's (modules/nats/module.json). +var BusServer = []string{"server", "program-snapshot", "program-entrypoint"} + +// BusServerOf is the ids of BusServer that m declares as resources, empty unless m provides the mesh's bus. +func BusServerOf(m Manifest) map[string]bool { + if !ProvidesBus(m) { + return nil + } + of := map[string]bool{} + for _, r := range m.Resources { + if id, _ := r["id"].(string); slices.Contains(BusServer, id) { + of[id] = true + } + } + return of +} + // irreplaceable names the first irreplaceable data the module keeps, its own or its consumers', or // nothing. func (m Manifest) irreplaceable() string {