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.
244 lines
9.2 KiB
Go
244 lines
9.2 KiB
Go
package broker
|
||
|
||
import (
|
||
"encoding/json"
|
||
"go/ast"
|
||
"go/parser"
|
||
"go/token"
|
||
"io/fs"
|
||
"path/filepath"
|
||
"slices"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/nats-io/nats.go/jetstream"
|
||
)
|
||
|
||
// **The stream a log is, field by field** (novox/hq ADR 0297 §2, the shared contract): a file stream
|
||
// kept by limits, no maximum age, no cap per key, the declared cap, a message of at most 260 KiB (a
|
||
// value of 256 KiB and 4 KiB of headers),
|
||
// refusing what comes next when full, read directly, refusing a delete or a purge, one replica, and
|
||
// a description that says whose it is and why.
|
||
func TestALogsStreamIsAsTheDecisionFixesIt(t *testing.T) {
|
||
got := Log{Module: "mesh-issues", Name: "changes"}.Config()
|
||
want := jetstream.StreamConfig{
|
||
Name: "LOG_mesh-issues_changes",
|
||
Description: got.Description,
|
||
Subjects: []string{"mesh.log.mesh-issues.changes.>"},
|
||
Storage: jetstream.FileStorage,
|
||
Retention: jetstream.LimitsPolicy,
|
||
MaxAge: 0,
|
||
MaxMsgs: -1,
|
||
MaxMsgsPerSubject: -1,
|
||
MaxBytes: 1024 * 1024 * 1024,
|
||
MaxMsgSize: 260 * 1024,
|
||
Discard: jetstream.DiscardNew,
|
||
AllowDirect: true,
|
||
DenyDelete: true,
|
||
DenyPurge: true,
|
||
Replicas: 1,
|
||
}
|
||
a, _ := json.Marshal(got)
|
||
b, _ := json.Marshal(want)
|
||
if string(a) != string(b) {
|
||
t.Fatalf("the log's stream is\n %s\nwant\n %s", a, b)
|
||
}
|
||
if !strings.Contains(got.Description, "mesh-issues") || !strings.Contains(got.Description, "ADR 0297") {
|
||
t.Fatalf("the description does not say whose the log is and why: %q", got.Description)
|
||
}
|
||
if c := (Log{Module: "m", Name: "n", MaxMiB: 8192}).Config(); c.MaxBytes != 8192*1024*1024 {
|
||
t.Fatalf("a cap of 8192 MiB is %d bytes", c.MaxBytes)
|
||
}
|
||
}
|
||
|
||
// **The membership names each of the owner's logs** with exactly the field names the runtime reads:
|
||
// `logs`, and in each `name`, `stream`, `subject`, `writes`. A module with no log is issued none, and
|
||
// the field is absent.
|
||
func TestAMembershipListsItsModulesLogs(t *testing.T) {
|
||
m := MembershipFor("one", Declared{Module: "mesh-issues",
|
||
Logs: []Log{{Module: "mesh-issues", Name: "changes"}}}, Placements{})
|
||
raw, err := json.Marshal(m)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var back map[string]json.RawMessage
|
||
if err := json.Unmarshal(raw, &back); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := string(back["logs"]); got !=
|
||
`[{"name":"changes","stream":"LOG_mesh-issues_changes","subject":"mesh.log.mesh-issues.changes","writes":true}]` {
|
||
t.Fatalf("the membership's logs are %s", got)
|
||
}
|
||
none, _ := json.Marshal(MembershipFor("one", Declared{Module: "audit"}, Placements{}))
|
||
if strings.Contains(string(none), `"logs"`) {
|
||
t.Fatalf("a module with no log was issued logs: %s", none)
|
||
}
|
||
}
|
||
|
||
// logGrantsOf is the grants of a principal that are about logs.
|
||
func logGrantsOf(publish []string) []string {
|
||
var out []string
|
||
for _, s := range publish {
|
||
if strings.HasPrefix(s, "mesh.log.") || strings.Contains(s, ".LOG_") {
|
||
out = append(out, s)
|
||
}
|
||
}
|
||
slices.Sort(out)
|
||
return out
|
||
}
|
||
|
||
// **The runtime is granted exactly the contract's subjects for each log it carries, and nothing else
|
||
// of any log**: appending under the log's subjects, binding to its stream, reading it directly. No
|
||
// consumer, no delete, no purge, and nothing of a log its modules do not keep.
|
||
func TestTheRuntimeIsGrantedItsModulesLogsAndNoMore(t *testing.T) {
|
||
perms, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "one", Module: RuntimeModule,
|
||
Carries: []Declared{
|
||
{Module: "mesh-issues", Logs: []Log{{Module: "mesh-issues", Name: "changes"}}},
|
||
{Module: "audit"},
|
||
}, PasswordHash: "x"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
want := []string{
|
||
"$JS.API.DIRECT.GET.LOG_mesh-issues_changes",
|
||
"$JS.API.DIRECT.GET.LOG_mesh-issues_changes.>",
|
||
"$JS.API.STREAM.INFO.LOG_mesh-issues_changes",
|
||
"mesh.log.mesh-issues.changes.>",
|
||
}
|
||
if got := logGrantsOf(perms.Publish); !slices.Equal(got, want) {
|
||
t.Fatalf("the runtime is granted\n %q\nwant\n %q", got, want)
|
||
}
|
||
for _, s := range perms.Subscribe {
|
||
if strings.HasPrefix(s, "mesh.log.") || strings.Contains(s, "LOG_") {
|
||
t.Fatalf("the runtime subscribes a log's subjects: %q", s)
|
||
}
|
||
}
|
||
// A module running on its own account is granted the same for its own logs.
|
||
own, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "mesh-issues",
|
||
Logs: []string{"changes"}, PasswordHash: "x"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := logGrantsOf(own.Publish); !slices.Equal(got, want) {
|
||
t.Fatalf("the module's own account is granted\n %q\nwant\n %q", got, want)
|
||
}
|
||
// And a module with no log, nothing of any.
|
||
none, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "audit", PasswordHash: "x"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := logGrantsOf(none.Publish); len(got) != 0 {
|
||
t.Fatalf("a module with no log is granted %q", got)
|
||
}
|
||
}
|
||
|
||
// A log name or module that is not one plain token is issued and granted nothing rather than a
|
||
// pattern that happens to parse.
|
||
func TestALogThatNamesNoStreamGrantsNothing(t *testing.T) {
|
||
if got := logGrants("a", []string{"x.y", "x>", "*", ""}); len(got) != 0 {
|
||
t.Fatalf("granted %v for logs that name no stream", got)
|
||
}
|
||
if got := logGrants("a.b", []string{"c"}); len(got) != 0 {
|
||
t.Fatalf("granted %v for a module that is not one token", got)
|
||
}
|
||
}
|
||
|
||
type logs struct {
|
||
ensured []string
|
||
on []string
|
||
}
|
||
|
||
func (l *logs) EnsureLog(x Log) error { l.ensured = append(l.ensured, x.Stream()); return nil }
|
||
func (l *logs) LogStreams() ([]string, error) { return l.on, nil }
|
||
|
||
// Every declared log is asserted; one on the server that nothing declares is said, not removed — and
|
||
// the asserter has no way to remove one.
|
||
func TestRaisingLogsReportsWhatNothingDeclares(t *testing.T) {
|
||
l := &logs{on: []string{"LOG_mesh-issues_changes", "LOG_gone_old", "KV_not_a_log"}}
|
||
undeclared, err := RaiseLogs(l, []Log{{Module: "mesh-issues", Name: "changes"}, {Module: "a", Name: "b"}})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !slices.Equal(l.ensured, []string{"LOG_a_b", "LOG_mesh-issues_changes"}) {
|
||
t.Fatalf("asserted %v", l.ensured)
|
||
}
|
||
if !slices.Equal(undeclared, []string{"LOG_gone_old"}) {
|
||
t.Fatalf("reported %v", undeclared)
|
||
}
|
||
}
|
||
|
||
// **No path of the mesh deletes or purges a log or a bucket, or recreates one** (novox/hq ADR 0297 §2,
|
||
// ADR 0201 §15–16): no code of this repository outside its tests calls the client's stream or bucket
|
||
// delete, or purges a stream, but for the controller lease's own stream, which purges its own history
|
||
// below a sequence (internal/lease), and the controller's record mirrors, which purge the key of a record
|
||
// the store no longer holds (internal/recordmirror; novox/hq ADR 0289, design 54 §2), and the buckets of the plans'
|
||
// state the controller deletes once at its first start (ADR 0539 clause 13). A new call is a decision, not a refactor.
|
||
func TestNoPathDeletesALogOrABucket(t *testing.T) {
|
||
removers := map[string]bool{"DeleteStream": true, "DeleteKeyValue": true, "PurgeStream": true,
|
||
"DeleteObjectStore": true}
|
||
root := filepath.Join("..", "..")
|
||
var found []string
|
||
for _, dir := range []string{"cmd", "internal"} {
|
||
err := filepath.WalkDir(filepath.Join(root, dir), func(path string, e fs.DirEntry, err error) error {
|
||
if err != nil || e.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||
return err
|
||
}
|
||
f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ast.Inspect(f, func(n ast.Node) bool {
|
||
call, ok := n.(*ast.CallExpr)
|
||
if !ok {
|
||
return true
|
||
}
|
||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||
if !ok {
|
||
return true
|
||
}
|
||
name := sel.Sel.Name
|
||
decided := strings.Contains(filepath.ToSlash(path), "internal/lease/") ||
|
||
strings.Contains(filepath.ToSlash(path), "internal/recordmirror/")
|
||
// And the buckets of the plans' state release 6 deleted, at the controller's first start, each said (novox/hq
|
||
// ADR 0539 clause 13): the one decided deletion of a bucket.
|
||
release6 := strings.HasSuffix(filepath.ToSlash(path), "cmd/mesh-controller/composed_start.go") && name == "DeleteKeyValue"
|
||
if (removers[name] && !release6) || (name == "Purge" && !decided) {
|
||
found = append(found, path+": "+name)
|
||
}
|
||
return true
|
||
})
|
||
for _, lit := range stringsIn(f) {
|
||
if strings.Contains(lit, "$JS.API.STREAM.DELETE") || strings.Contains(lit, "$JS.API.STREAM.PURGE") {
|
||
// Only the writers table names it, as a subject no one but the controller may publish.
|
||
if !strings.HasSuffix(filepath.ToSlash(path), "internal/broker/writers.go") {
|
||
found = append(found, path+": "+lit)
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if len(found) > 0 {
|
||
t.Fatalf("a path of the mesh removes a stream, a bucket or what one holds:\n %s", strings.Join(found, "\n "))
|
||
}
|
||
}
|
||
|
||
// stringsIn is every string literal of a file.
|
||
func stringsIn(f *ast.File) []string {
|
||
var out []string
|
||
ast.Inspect(f, func(n ast.Node) bool {
|
||
if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.STRING {
|
||
out = append(out, lit.Value)
|
||
}
|
||
return true
|
||
})
|
||
return out
|
||
}
|
||
|
||
// Against a real server: a log is created as its configuration says, asserting it again keeps what
|
||
// it holds, a changed cap is brought to match in place, an entry cannot be deleted from it, and one
|
||
// nothing declares any more is reported and stays.
|