Files
mesh-catalog/modules/nats/cmd/nats-tools/manifest_test.go
T
jschoubben 40c4fabff6 Let the bus tools remove the controller's leftovers by one word
The controller ended by its unassignment, so its consumers, streams and
buckets may be removed as recorded hand acts (ADR 0590 §6). The nats tools'
account declares controller-leftovers in place of the lease write's two
words, and the removal test proves a consumer of EVENTS and a bucket's
stream go while EVENTS and its other consumers stay (issue 1126 B5).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00

146 lines
5.8 KiB
Go

package main
import (
"encoding/json"
"os"
"slices"
"strings"
"testing"
)
// The shape ADR 0480 clauses 1, 2, 3 and 5 give the bus's manifest: the upstream image by digest, the mesh's two
// programs mounted read-only under /opt, unpacked by archive resources the container names under restart-on
// (the node-engine does not look inside a mounted directory), and the container the same one it was.
func TestTheBussManifestMountsItsProgramsIntoTheUpstreamImage(t *testing.T) {
type artifact struct {
Name, Kind, From, Language, System, Binary string
Env map[string]string
Bus struct {
Removes []string `json:"removes"`
} `json:"bus"`
}
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Data struct {
Own []struct {
ID string `json:"id"`
Backup struct {
Dump string `json:"dump"`
} `json:"backup"`
} `json:"own"`
} `json:"data"`
Tools []string `json:"tools"`
Emits []string `json:"emits"`
Resources []map[string]any `json:"resources"`
Build struct {
On any `json:"on"`
Artifacts []artifact `json:"artifacts"`
} `json:"build"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if m.Build.On != nil {
t.Error("build.on is declared: nothing is built on a base image any more")
}
artifacts := map[string]artifact{}
for _, a := range m.Build.Artifacts {
artifacts[a.Name] = a
if a.Kind == "image" {
t.Errorf("artifact %s is an image built from a Dockerfile (ADR 0452)", a.Name)
}
}
if a := artifacts["server"]; a.Kind != "upstream" || !strings.Contains(a.From, "@sha256:") {
t.Errorf("server is %+v: an upstream image pinned by digest", a)
}
if a := artifacts["snapshot"]; a.Kind != "bundle" || a.Language != "go" || a.System != "alpine" ||
a.From != "cmd/mesh-nats-snapshot" || a.Binary != "mesh-nats-snapshot" {
t.Errorf("snapshot is %+v: a Go bundle of the module's own package, stamped for alpine", a)
}
if a := artifacts["entrypoint"]; a.Kind != "archive" || a.From != "program" {
t.Errorf("entrypoint is %+v: an archive of program/", a)
}
if _, err := os.Stat("../../program/entrypoint.sh"); err != nil {
t.Error(err)
}
if _, err := os.Stat("../../Dockerfile"); err == nil {
t.Error("the Dockerfile is still there")
}
byID := map[string]map[string]any{}
for _, r := range m.Resources {
byID[r["id"].(string)] = r
}
server := byID["server"]
if server["name"] != "mesh-broker-nats" || server["artifact"] != "server" {
t.Errorf("the container is %v on artifact %v: it keeps its name, which the dump and the jails use", server["name"], server["artifact"])
}
volumes, _ := server["volumes"].([]any)
restart, _ := server["restart-on"].([]any)
for _, p := range []struct{ resource, artifact, mount string }{
{"program-snapshot", "snapshot", ":/opt/mesh:ro"},
{"program-entrypoint", "entrypoint", ":/opt/mesh-entrypoint:ro"},
} {
r := byID[p.resource]
if r == nil || r["type"] != "archive" || r["artifact"] != p.artifact {
t.Errorf("%s is %v: an archive resource unpacking %s", p.resource, r, p.artifact)
continue
}
// Under a directory the module declares (ADR 0030: nothing is mounted from a path no resource covers).
if dir, _, _ := strings.Cut(strings.TrimPrefix(r["path"].(string), "${dir:"), "}"); !strings.HasPrefix(r["path"].(string), "${dir:") ||
byID[dir] == nil || byID[dir]["type"] != "directory" {
t.Errorf("%s is at %v: not under a directory the module declares", p.resource, r["path"])
}
mounted := false
for _, v := range volumes {
if v == r["path"].(string)+p.mount {
mounted = true
}
}
if !mounted {
t.Errorf("%s is not mounted read-only as %s", p.resource, p.mount)
}
named := false
for _, v := range restart {
named = named || v == p.resource
}
if !named {
t.Errorf("%s is not in the container's restart-on: a new program would leave the old one running", p.resource)
}
}
// The tools that change the bus (issue 1952 N1-N3): listed, their audit events declared, and the credential
// they dial with the module's own secret `broker`.
for _, want := range []string{"nats_consumer_remove", "nats_bucket_remove", "nats_stream_remove", "nats_bucket_expiry"} {
if !slices.Contains(m.Tools, want) {
t.Errorf("tools do not list %s", want)
}
}
for _, want := range []string{"consumer-removed", "bucket-keys-removed", "stream-removed", "bucket-expiry-set"} {
if !slices.Contains(m.Emits, want) {
t.Errorf("emits do not list %s: the removal tools say it as the audit line", want)
}
}
for _, a := range m.Build.Artifacts {
// Issue 1126 B5 (ADR 0590 §6): the tools may remove the controller's leftovers, by the one word for them, and
// nothing else of the controller's by a word of its own.
if a.Name == "tools" && !slices.Equal(a.Bus.Removes, []string{"controller-leftovers"}) {
t.Errorf("the tools' bus removes %v: the controller's leftovers by their one word, controller-leftovers", a.Bus.Removes)
}
if a.Name == "tools" && a.Env["NATS_TOOLS_CREDENTIAL"] != "${dir:tools-secret}/broker-nats-tools" {
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"])
}
}
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)
}
for _, d := range m.Data.Own {
if d.ID == "jetstream" && !strings.Contains(d.Backup.Dump, "docker exec -i mesh-broker-nats /opt/mesh/mesh-nats-snapshot snapshot") {
t.Errorf("the dump is %q: it calls the mounted program", d.Backup.Dump)
}
}
}