The keeper is a process of the tools bundle on its own account (own secret broker-keeper, ADR 0436 §1): it hears the indexer's manifest-accepted, keeps what it heard in its old state asked and the persons' requests in requests (ADR 0595 §10), and says its nine events on its own stream broker-events, 30 days at 256 MiB (design 25 § 12.3, ADR 0620 §2). mesh-broker serves its four reading verbs; its acting verbs are the keeper's own work. The server gains a tcp check, which the keeper's tool check stands beside. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
236 lines
9.9 KiB
Go
236 lines
9.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"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"`
|
|
Provides []struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Credential struct {
|
|
Form string `json:"form"`
|
|
} `json:"credential"`
|
|
} `json:"provides"`
|
|
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)
|
|
}
|
|
// The bus provides "bus-state" beside "mesh-bus" (ADR 0614 §5): a module whose manifest keeps bus state requires it.
|
|
provided := map[string]string{}
|
|
for _, p := range m.Provides {
|
|
provided[p.Name] = p.Scope + "/" + p.Credential.Form
|
|
}
|
|
for _, name := range []string{"mesh-bus", "bus-state"} {
|
|
if provided[name] != "mesh/none" {
|
|
t.Errorf("provision %q is %q: the bus provides it mesh-wide with no credential", name, provided[name])
|
|
}
|
|
}
|
|
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"])
|
|
}
|
|
}
|
|
// 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)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|