With no plan left to take a done key in, a write of the builder's keys registers the new fingerprint, composes the module's nodes and the bus's user list, and writes its grant, so the assigner's rules can name it (ADR 0539 clauses 5 and 7). The new controller composes every node at its start, and the controller it replaced at its own fingerprint, the operator's way back (clause 10); the merge window's, silent sends' and building plans' buckets are no longer asserted and are deleted once, each said as the event bucket-deleted (clause 13). The assigner may read the compositions' digests, and a node-engine hears its backup holder's backed-up.
310 lines
14 KiB
Go
310 lines
14 KiB
Go
package broker
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
// **The controller may write every bucket it writes** (novox/hq to-be 45 §1, issue 269). Writing a
|
|
// key is a publish to the bucket's own subject, which the management interface's grant does not
|
|
// cover: the cancelled sets' writes timed out for want of this, and the controller's own buckets
|
|
// would have.
|
|
func TestTheControllerMayWriteEveryBucketItWrites(t *testing.T) {
|
|
p, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []string{"$KV." + CallsBucket + ".>", "$KV." + HandActsBucket + ".>",
|
|
"$KV." + AssignmentsBucket + ".>", "$KV." + DeclaredConfigBucket + ".>",
|
|
"$KV." + PredefinedBucket + ".>"}
|
|
for _, subject := range want {
|
|
if !slices.Contains(p.Publish, subject) {
|
|
t.Errorf("the controller may not publish %s, so it cannot write that bucket", subject)
|
|
}
|
|
}
|
|
if slices.Contains(p.Publish, "$KV.>") {
|
|
t.Error("the controller may write any bucket, a module's state included")
|
|
}
|
|
}
|
|
|
|
// **A module reads a record mirror through `reads`, and never writes it** (ADR 0201 §1, design 54 §5,
|
|
// design 57 F3): granted the direct reads and watches of the bucket, and refused at composition a grant to
|
|
// write it, whose writer is the controller.
|
|
func TestAModuleReadsTheControllersMirrorsAndWritesNone(t *testing.T) {
|
|
reader := Principal{Kind: KindModule, Node: "one", Module: "mesh-config", PasswordHash: "x",
|
|
Reads: []string{"mesh-controller." + MirrorAssignments, "mesh-controller." + MirrorDeclaredConfig,
|
|
"mesh-controller." + MirrorPredefined}}
|
|
p, err := PermissionsFor(reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, bucket := range []string{AssignmentsBucket, DeclaredConfigBucket, PredefinedBucket} {
|
|
for _, subject := range []string{"$JS.API.STREAM.INFO.KV_" + bucket, "$JS.API.DIRECT.GET.KV_" + bucket + ".>",
|
|
"$JS.API.CONSUMER.CREATE.KV_" + bucket + ".>"} {
|
|
if !slices.Contains(p.Publish, subject) {
|
|
t.Errorf("a module reading %s may not publish %s", bucket, subject)
|
|
}
|
|
}
|
|
for _, subject := range p.Publish {
|
|
if SubjectsOverlap(subject, "$KV."+bucket+".>") {
|
|
t.Errorf("a module reading %s may write it through %s", bucket, subject)
|
|
}
|
|
}
|
|
if err := CheckWriters(reader, []string{"$KV." + bucket + ".>"}); err == nil {
|
|
t.Errorf("a module granted a write of %s would compose", bucket)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **A machine's node tools may say they are there, as that machine and no other** (novox/hq to-be 45
|
|
// S11), and the controller may hear the bus's advisories and ask who answers — read-only, named.
|
|
func TestTheWatchedSignalsMayBeSaidAndHeard(t *testing.T) {
|
|
tools, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule, PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Contains(tools.Publish, "mesh.control.anchor.tools-alive") {
|
|
t.Error("the node tools may not say they are there")
|
|
}
|
|
for _, s := range tools.Publish {
|
|
if strings.Contains(s, "tools-alive") && s != "mesh.control.anchor.tools-alive" {
|
|
t.Errorf("the node tools may say %s", s)
|
|
}
|
|
}
|
|
controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range BusAdvisories {
|
|
if !slices.Contains(controller.Subscribe, s) {
|
|
t.Errorf("the controller may not hear %s", s)
|
|
}
|
|
}
|
|
if !slices.Contains(controller.Publish, "$SRV.INFO") || slices.Contains(controller.Subscribe, "$JS.EVENT.>") {
|
|
t.Error("the controller may not ask who answers, or hears every API call")
|
|
}
|
|
}
|
|
|
|
// The controller's record of what it asked the operator is bounded (correctness review of 2026-10-08): one
|
|
// value a key, a month's age, and a size it cannot outgrow.
|
|
|
|
// **A listed mirror is no undeclared bucket** (ADR 0356 §3, ADR 0201 §16): the controller's list of its mirrors
|
|
// stands for their declaration, so the report of state nothing declares never names one.
|
|
func TestAListedMirrorIsNotReportedAsUndeclared(t *testing.T) {
|
|
names := []string{"gone_state"}
|
|
for _, m := range controllerReadableStates() {
|
|
names = append(names, BucketName(ControllerSeat, m))
|
|
}
|
|
undeclared, err := RaiseBuckets(fakeBuckets{names: names}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Equal(undeclared, []string{"gone_state"}) {
|
|
t.Fatalf("the report of undeclared state names %v", undeclared)
|
|
}
|
|
}
|
|
|
|
// **The mirrors' list and the writers table agree** (ADR 0356 §3, to-be 45 §1): every listed mirror is a row of
|
|
// the table whose one writer is the controller, on its bucket alone; and every row that writes a bucket of the
|
|
// controller's named as a mirror is on the list.
|
|
func TestTheMirrorsListAndTheWritersTableAgree(t *testing.T) {
|
|
controller := Principal{Kind: KindController}
|
|
module := Principal{Kind: KindModule, Module: ControllerSeat, Node: "one"}
|
|
rowed := map[string]bool{}
|
|
for _, row := range WritersTable {
|
|
if !strings.HasSuffix(row.State, " mirror") && !strings.HasSuffix(row.State, " of the chain") && row.State != "the grants issued" &&
|
|
row.State != "the compositions and their digests" {
|
|
continue
|
|
}
|
|
for _, subject := range row.Subjects {
|
|
bucket := strings.TrimSuffix(strings.TrimPrefix(subject, "$KV."), ".>")
|
|
rowed[bucket] = true
|
|
if row.Writes == nil || !row.Writes(controller, subject) || row.Writes(module, subject) {
|
|
t.Errorf("%q is not written by the controller alone", row.State)
|
|
}
|
|
}
|
|
}
|
|
listed := map[string]bool{}
|
|
for _, m := range controllerReadableStates() {
|
|
bucket := BucketName(ControllerSeat, m)
|
|
listed[bucket] = true
|
|
if why, other := MirrorsWrittenByAnother[m]; other { // 1155/AC3: the exception, with its reason
|
|
if why == "" {
|
|
t.Errorf("the mirror %s is written by another and says no reason", bucket)
|
|
}
|
|
continue
|
|
}
|
|
if !rowed[bucket] {
|
|
t.Errorf("the mirror %s is listed and no row of the writers table writes it", bucket)
|
|
}
|
|
if !IsControllerBucket(bucket) || !slices.Contains(ControllerBuckets(), bucket) {
|
|
t.Errorf("the mirror %s is listed and not asserted as the controller's own", bucket)
|
|
}
|
|
}
|
|
for bucket := range rowed {
|
|
if !listed[bucket] {
|
|
t.Errorf("the writers table writes the mirror %s, which the list does not name", bucket)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The controller may ask the vault's intake verbs in its own name** (design 57's V2, issue 675): the vault
|
|
// takes a value only from the controller, which it knows from the subject that names its caller — so the
|
|
// controller is granted that subject for the verbs the vault's holder serves, and no more of the vault's seat.
|
|
// `get` among them since issue 755 (ADR 0349 §16): the vault answers the controller a core credential sealed to its
|
|
// machine alone, which the controller carries and cannot open.
|
|
// Composed from the records, as the issued tools are: the installer's first user list carries none.
|
|
func TestTheControllerMayAskTheVaultInItsOwnName(t *testing.T) {
|
|
vault := Declared{Module: "mesh-vault", Holds: []Seat{{Name: "mesh-vault",
|
|
Serves: []string{"get", "accept", "secret-ask", "issued", "rotate"}}}}
|
|
users, err := Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {vault}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
controller := users[0]
|
|
if strings.Join(controller.AsksTheVault, ",") != "accept,get,issued,rotate,secret-ask" {
|
|
t.Fatalf("composed %v", controller.AsksTheVault)
|
|
}
|
|
p, err := PermissionsFor(controller)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, verb := range []string{"accept", "secret-ask", "issued", "rotate", "get"} {
|
|
if !MayPublish(p, CalledSubject("mesh.seat.mesh-vault.tool."+verb, ControllerName)) {
|
|
t.Errorf("the controller may not ask the vault's %s in its own name", verb)
|
|
}
|
|
if MayPublish(p, CalledSubject("mesh.seat.mesh-vault.tool."+verb, "anchor.node-tools")) {
|
|
t.Errorf("the controller may ask the vault's %s in another's name", verb)
|
|
}
|
|
}
|
|
if MayPublish(p, CalledSubject("mesh.seat.mesh-vault.tool.move-in", ControllerName)) {
|
|
t.Error("the controller may move a value in: that is a node-engine's alone")
|
|
}
|
|
// A vault that serves none of them yet composes no grant, nor does genesis.
|
|
older := Declared{Module: "mesh-vault", Holds: []Seat{{Name: "mesh-vault", Serves: []string{"move-in"}}}}
|
|
users, _ = Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {older}}})
|
|
if len(users[0].AsksTheVault) != 0 {
|
|
t.Errorf("a vault serving none composed %v", users[0].AsksTheVault)
|
|
}
|
|
genesis, _ := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
|
for _, s := range genesis.Publish {
|
|
if strings.Contains(s, "mesh-vault") {
|
|
t.Errorf("the installer's controller may publish %s", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The controller may ask mesh-config's `config` in its own name** (issue 925): move-config and the settings verb
|
|
// set a trusted value through mesh-config, which takes it as the controller carries it only from the subject that
|
|
// names the controller as caller. Granted when the records hold a holder serving the verb, and not otherwise;
|
|
// the installer's first user list carries none.
|
|
func TestTheControllerMayAskMeshConfigInItsOwnName(t *testing.T) {
|
|
config := Declared{Module: "mesh-config", Holds: []Seat{{Name: "mesh-config", Serves: []string{"config"}}}}
|
|
users, err := Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {config}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(users[0].AsksTheConfig, ",") != "config" {
|
|
t.Fatalf("composed %v", users[0].AsksTheConfig)
|
|
}
|
|
p, err := PermissionsFor(users[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Contains(p.Publish, "mesh.seat.mesh-config.call.config.controller") {
|
|
t.Errorf("the controller may not publish mesh.seat.mesh-config.call.config.controller: %v", p.Publish)
|
|
}
|
|
if !MayPublish(p, CalledSubject("mesh.seat.mesh-config.tool.config", ControllerName)) {
|
|
t.Error("the controller may not ask mesh-config's config in its own name")
|
|
}
|
|
if MayPublish(p, CalledSubject("mesh.seat.mesh-config.tool.config", "anchor.node-tools")) {
|
|
t.Error("the controller may ask mesh-config's config in another's name")
|
|
}
|
|
// No holder serving it composes no grant, nor does genesis.
|
|
other := Declared{Module: "mesh-config", Holds: []Seat{{Name: "mesh-config", Serves: []string{"list"}}}}
|
|
for _, r := range []Records{{Nodes: []string{"novox"}}, {Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {other}}}} {
|
|
users, _ = Users(r)
|
|
if len(users[0].AsksTheConfig) != 0 {
|
|
t.Errorf("composed %v without a holder serving config", users[0].AsksTheConfig)
|
|
}
|
|
}
|
|
genesis, _ := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
|
for _, s := range genesis.Publish {
|
|
if strings.Contains(s, "mesh-config") {
|
|
t.Errorf("the installer's controller may publish %s", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The merge window is a bucket whose keys expire** (novox/hq ADR 0407 decision 1, issue 973): a key made with a
|
|
// time to live goes when it is up, and a watcher on the bucket hears it go. The controller may write it, and it is
|
|
// no undeclared state.
|
|
|
|
// **A rollout's wait for a machine is a bucket whose keys expire** (novox/hq ADR 0406 §5, issue 974): a key made with a
|
|
// time to live goes when it is up, and a watcher hears it go as a purge; a key a report deleted is a delete. The
|
|
// controller may write it, and it is no undeclared state.
|
|
|
|
// **The silent keys' bucket ages**, or its delete markers would fill it (issue 974, review 1): every report deletes a key,
|
|
// the delete leaves a marker, and with one value per key and no age the markers stay for ever. A bucket made before the
|
|
// age was set is brought to it by the next start, which the server accepts as an update of an existing bucket.
|
|
|
|
// **The merges have a consumer of their own, which a reset of the events consumer leaves alone** (issue 973, novox/hq
|
|
// ADR 0407 decision 3): healer H4 re-makes the events consumer from now, dropping what it holds pending; a merge
|
|
// waiting in the merges consumer is not that consumer's, and nothing would catch it up.
|
|
|
|
// **Delivering this change loses no merge** (review of issue 973): a merge published under the old filter of the
|
|
// `controller` consumer and never acknowledged by it is delivered by the merges consumer once the assertion has
|
|
// made it, because it starts where `controller` has acknowledged up to, not from now.
|
|
|
|
// **The chain's buckets keep sixty-four revisions and no age** (ADR 0408 §4 to §6, issue 1089): the bus's largest
|
|
// history, no age that could forget a key, a value up to just under the server's largest message, on disk.
|
|
func TestTheChainBucketsKeepSixtyFourRevisionsAndNoAge(t *testing.T) {
|
|
if want := []string{"mesh-controller_builds", "mesh-controller_rollouts", "mesh-controller_assignments"}; !slices.Equal(ChainBuckets(), want) {
|
|
t.Fatalf("the chain's buckets are %v, not %v", ChainBuckets(), want)
|
|
}
|
|
for _, bucket := range ChainBuckets() {
|
|
cfg := chainBucket(bucket, "why", ChainBuildsBytes, 0)
|
|
if cfg.History != 64 || cfg.TTL != 0 || cfg.Storage != jetstream.FileStorage {
|
|
t.Errorf("%s keeps %d revisions, age %v, on %v", bucket, cfg.History, cfg.TTL, cfg.Storage)
|
|
}
|
|
if cfg.MaxValueSize <= MirrorValueBytes || cfg.MaxValueSize >= 1<<20 {
|
|
t.Errorf("%s takes a value of %d bytes, not just under a mebibyte", bucket, cfg.MaxValueSize)
|
|
}
|
|
if cfg.Replicas > 1 {
|
|
t.Errorf("%s is replicated %d times", bucket, cfg.Replicas)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **Every bucket of the chain is the controller's, listed for a reader, and has a writer row** (ADR 0408 §3).
|
|
func TestEveryChainBucketIsOnTheControllersListAndHasAWriterRow(t *testing.T) {
|
|
controller := Principal{Kind: KindController}
|
|
module := Principal{Kind: KindModule, Module: ControllerSeat, Node: "one"}
|
|
for _, bucket := range ChainBuckets() {
|
|
if !IsControllerBucket(bucket) || !slices.Contains(ControllerBuckets(), bucket) {
|
|
t.Errorf("%s is not on the controller's list of buckets", bucket)
|
|
}
|
|
local := strings.TrimPrefix(bucket, ControllerSeat+"_")
|
|
if !slices.Contains(controllerReadableStates(), local) {
|
|
t.Errorf("a module cannot name %s in `reads`", bucket)
|
|
}
|
|
rows := 0
|
|
for _, row := range WritersTable {
|
|
for _, subject := range row.Subjects {
|
|
if subject == "$KV."+bucket+".>" {
|
|
rows++
|
|
if row.Writes == nil || !row.Writes(controller, subject) || row.Writes(module, subject) {
|
|
t.Errorf("%q is not written by the controller alone", row.State)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if rows != 1 {
|
|
t.Errorf("%s has %d writer rows", bucket, rows)
|
|
}
|
|
}
|
|
}
|