diff --git a/modules/nats/cmd/nats-tools/main.go b/modules/nats/cmd/nats-tools/main.go index ef376d51..11e00c00 100644 --- a/modules/nats/cmd/nats-tools/main.go +++ b/modules/nats/cmd/nats-tools/main.go @@ -157,3 +157,12 @@ func main() { os.Exit(1) } } + +// The seat mesh-broker's reading verbs, which this bundle serves (design 25 § 12.3): every entry, one entry, what the +// bus holds against what was asked, and what no manifest asks for. Its acting verbs (create, apply, recreate, drop) are +// the keeper's own work, on its own account alone (keeper.go), so no other account, the tool runner's included, holds +// them. +var ReadingVerbs = []string{"list", "show", "check", "orphans"} + +// Events are the bus module's own events (ADR 0595 §6, ADR 0620 §2), on its own stream broker-events (BROKER_EVENTS). +var Events = []string{"created", "refused", "drifted", "drift-cleared", "full", "full-cleared", "orphaned", "recreated", "dropped"} diff --git a/modules/nats/cmd/nats-tools/manifest_test.go b/modules/nats/cmd/nats-tools/manifest_test.go index 93e0a8b3..e7c4c503 100644 --- a/modules/nats/cmd/nats-tools/manifest_test.go +++ b/modules/nats/cmd/nats-tools/manifest_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "os" "slices" "strings" @@ -150,6 +151,78 @@ func TestTheBussManifestMountsItsProgramsIntoTheUpstreamImage(t *testing.T) { t.Errorf("the tools' credential is %q: the process nats-tools' own secret broker-nats-tools, under a word of the bundle's own (the runtime strips MESH_BROKER_FILE)", a.Env["NATS_TOOLS_CREDENTIAL"]) } } + // The bus module provides bus state (design 25 § 12.2-12.3, ADR 0595 §4-§6; CAT-N1): its keeper is a process of + // its own on its own account (own secret broker-keeper, ADR 0436 §1), creating only bus state; it hears the indexer's + // manifest-accepted, keeps what it heard in its own old state `asked` (ADR 0595 §10) and the persons' requests in + // `requests`; its nine events are its own (ADR 0620 §2), on its own stream kept 30 days at 256 MiB; the seat's four + // reading verbs are served, its four acting verbs are the keeper's own work and no other account's; its health is + // the keeper's own tool, beside the server's tcp check. + var nm struct { + Claims []struct { + Name string `json:"name"` + Serves []string `json:"serves"` + } `json:"claims"` + Consumes []string `json:"consumes"` + Invokes []string `json:"invokes"` + Uses []string `json:"uses"` + OwnSecrets map[string]json.RawMessage `json:"own-secrets"` + Streams []struct { + Name string `json:"name"` + Events []string `json:"events"` + MaxAgeDays int `json:"max-age-days"` + MaxMiB int `json:"max-mib"` + } `json:"streams"` + State []struct { + Name string `json:"name"` + } `json:"state"` + } + if err := json.Unmarshal(raw, &nm); err != nil { + t.Fatal(err) + } + if len(nm.Claims) != 1 || nm.Claims[0].Name != "mesh-broker" || !slices.Equal(nm.Claims[0].Serves, ReadingVerbs) { + t.Errorf("claims are %+v: mesh-broker, serving its reading verbs %v alone", nm.Claims, ReadingVerbs) + } + if !slices.Equal(nm.Consumes, []string{"mesh-delivery-indexer.manifest-accepted"}) { + t.Errorf("consumes %v: the indexer's manifest-accepted alone (ADR 0595 §4)", nm.Consumes) + } + for _, want := range []string{"mesh-delivery-indexer.indexer_bus_state", "seat:mesh-hand-acts.hand-act"} { + if !slices.Contains(nm.Invokes, want) { + t.Errorf("invokes %v lack %s", nm.Invokes, want) + } + } + if !slices.Contains(nm.Uses, "operator-channel") { + t.Errorf("uses %v: the operator channel, for the warrant of recreate and drop", nm.Uses) + } + if len(nm.Streams) != 1 || nm.Streams[0].Name != "broker-events" || !slices.Equal(nm.Streams[0].Events, Events) || + nm.Streams[0].MaxAgeDays != 30 || nm.Streams[0].MaxMiB != 256 { + t.Errorf("streams are %+v: broker-events carrying %v, 30 days, 256 MiB", nm.Streams, Events) + } + for _, e := range Events { + if slices.Contains(m.Emits, e) { + t.Errorf("emits lists %s, which its stream carries: an event is named in one place (ADR 0363)", e) + } + } + var states []string + for _, s := range nm.State { + states = append(states, s.Name) + } + if !slices.Equal(states, []string{"asked", "requests"}) { + t.Errorf("state is %v: asked and requests, old sections (ADR 0595 §10)", states) + } + if _, ok := nm.OwnSecrets["broker-keeper"]; !ok { + t.Error("own-secrets lack broker-keeper, the keeper's own account (ADR 0436 §1)") + } + keeper := byID["keeper"] + bus, _ := keeper["bus"].(map[string]any) + health, _ := keeper["health"].(map[string]any) + if keeper["type"] != "process" || keeper["artifact"] != "tools" || fmt.Sprint(keeper["run"]) != "[./nats-tools keep]" || + fmt.Sprint(bus["state"]) != "[asked requests]" || + health["kind"] != "tool" || health["tool"] != "nats_keeper_health" { + t.Errorf("the keeper is %v: a process of the tools bundle, run as keep, keeping asked and requests, judged by nats_keeper_health", keeper) + } + if h, _ := server["health"].(map[string]any); h["kind"] != "tcp" || h["endpoint"] != "bus" { + t.Errorf("the server's health is %v: tcp on bus, the check the keeper's tool check stands beside", server["health"]) + } args, _ := server["args"].([]any) if len(args) != 2 || args[0] != "sh" || args[1] != "/opt/mesh-entrypoint/entrypoint.sh" { t.Errorf("args are %v: the entrypoint run through sh, so no execute bit is relied on", args) diff --git a/modules/nats/module.json b/modules/nats/module.json index 4f028cc2..413fe33c 100644 --- a/modules/nats/module.json +++ b/modules/nats/module.json @@ -24,14 +24,24 @@ "claims": [ { "name": "mesh-broker", - "scope": "mesh" + "scope": "mesh", + "serves": [ + "list", + "show", + "check", + "orphans" + ] } ], "bus-users": "/var/lib/nats-module/conf/accounts.conf", "core-credentials": true, "own-secrets": { "broker": "${dir:mesh-state}/broker", - "broker-nats-tools": "${dir:tools-secret}/broker-nats-tools" + "broker-nats-tools": "${dir:tools-secret}/broker-nats-tools", + "broker-keeper": { + "path": "${dir:keeper-state}/broker-keeper", + "owner": "nats-keeper" + } }, "capabilities": [ "container-runtime" @@ -43,16 +53,51 @@ "bucket-expiry-set" ], "invokes": [ - "seat:mesh-hand-acts.hand-act" + "seat:mesh-hand-acts.hand-act", + "mesh-delivery-indexer.indexer_bus_state" + ], + "consumes": [ + "mesh-delivery-indexer.manifest-accepted" + ], + "uses": [ + "operator-channel" + ], + "streams": [ + { + "name": "broker-events", + "events": [ + "created", + "refused", + "drifted", + "drift-cleared", + "full", + "full-cleared", + "orphaned", + "recreated", + "dropped" + ], + "max-age-days": 30, + "max-mib": 256 + } + ], + "state": [ + { + "name": "asked", + "history": 5 + }, + { + "name": "requests", + "history": 5, + "ttl-seconds": 86400 + } ], - "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" + "why": "the mesh bus \u2014 every link the mesh has, over TLS, reached across the overlay" } ], "tools": [ @@ -96,6 +141,13 @@ ] }, "resources": [ + { + "id": "keeper-account", + "type": "user", + "name": "nats-keeper", + "shell": "/usr/bin/nologin", + "home": "/var/lib/nats-keeper" + }, { "id": "jetstream-data", "type": "directory", @@ -117,6 +169,14 @@ "mode": "0711", "unknown": "program" }, + { + "id": "keeper-state", + "type": "directory", + "path": "/var/lib/nats-keeper", + "mode": "0700", + "owner": "nats-keeper", + "unknown": "program" + }, { "id": "snapshots", "type": "directory", @@ -158,6 +218,41 @@ "artifact": "entrypoint", "unknown": "program" }, + { + "id": "keeper", + "type": "process", + "name": "nats-keeper", + "artifact": "tools", + "run": [ + "./nats-tools", + "keep" + ], + "user": "nats-keeper", + "env": { + "MESH_BROKER_FILE": "${dir:keeper-state}/broker-keeper", + "MESH_NATS_FULL_RAISE": "75", + "MESH_NATS_FULL_CLEAR": "70" + }, + "bus": { + "state": [ + "asked", + "requests" + ], + "asks": true, + "invokes": [ + "mesh-delivery-indexer.indexer_bus_state", + "seat:mesh-hand-acts.hand-act" + ] + }, + "health": { + "kind": "tool", + "tool": "nats_keeper_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "180s" + } + }, { "id": "server", "type": "container", @@ -181,7 +276,11 @@ "restart-on": [ "program-snapshot", "program-entrypoint" - ] + ], + "health": { + "kind": "tcp", + "endpoint": "bus" + } } ], "accesses": [