Two controllers could both act (issue 204), a reconcile's report could overtake the apply after it and the digest decided (issue 267), and a grant could make a second writer of a machine's report. - The lease (internal/lease, ADR 0229): mesh-controller_lease key `holder`, 15 s age, renewed every 5 s by compare-and-set; the epoch is the revision it was taken at. The gate is the clock (stops 3 s before expiry); a refused renewal is a loss and the process exits; a holder that stops gives it back. serve takes it before asserting the bus. Epochs kept in the store (migration 0068 controller_epoch) as a floor: a bucket raised from nothing is compacted past it. Unleased (no epoch, S12 urgent) only when nobody holds it and the bus will not let it be written. A shell command acts under the holder's epoch, or its own lease when none. - Declarations carry `epoch` inside the signed envelope, only to a machine whose latest account carried a report_sequence (mesh-host #35); would-send is composed with the epoch last sent. Allot and the send both pass the gate. - Reports: contract in internal/link/order.go (epoch, sequence, report_sequence, older_than, refused_older). Accounts kept by epoch, then sequence, then report sequence; older refused, counted; unordered reports keep the digest rule. Plans by compare-and-set on a revision, with epoch. Conditions and calls carry the epoch and are not written off the lease. - S12 and S13 (naming the writer by epoch) watched, D5 run; reset of the bucket said. Writers table compiled in and enforced in PermissionsFor; the controller no longer publishes mesh.control.>. A contract per consumed kind, and the empty-on-error lint over the repository. - mesh-host pinned to its main with the epoch in the validator (D1 validates the envelope as sent). Needs mesh-host's genesis lock with the lease grant (mesh-host PR) for TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose.
144 lines
5.8 KiB
Go
144 lines
5.8 KiB
Go
package broker
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The writers table, checked (novox/hq to-be 45 §1, ADR 0227 rule 1, "how it is checked"): it is the
|
|
// design's table row for row; every row that writes on the bus names its writer; a whole mesh composes
|
|
// with one writer per piece of state; and a grant that would make a second writer is refused, naming
|
|
// the state and whose it is.
|
|
|
|
// designRows are the states of to-be 45 §1, in its order. A row added to the design is added here and
|
|
// to the table; one dropped from either fails.
|
|
var designRows = []string{
|
|
"a machine's declaration",
|
|
"a machine's applied state and its report",
|
|
"the controller lease",
|
|
"plans and their tiers",
|
|
"conditions",
|
|
"calls and their outcomes",
|
|
"the hand-act log",
|
|
"stream definitions and bus permissions",
|
|
"builds and their outcomes",
|
|
"a merge announced",
|
|
"a provider's standing",
|
|
"the operator-channel's open messages",
|
|
"the facts snapshot",
|
|
}
|
|
|
|
func TestTheWritersTableIsTheDesigns(t *testing.T) {
|
|
var states []string
|
|
for _, row := range WritersTable {
|
|
states = append(states, row.State)
|
|
if row.Writer == "" || row.KeptIn == "" || row.Others == "" {
|
|
t.Errorf("%q does not say who writes it, where it is kept and what others do", row.State)
|
|
}
|
|
if len(row.Subjects) > 0 && row.Writes == nil {
|
|
t.Errorf("%q is written on the bus and names no writer among the principals", row.State)
|
|
}
|
|
}
|
|
if !slices.Equal(states, designRows) {
|
|
t.Fatalf("the writers table is not to-be 45 §1's:\n have %q\n want %q", states, designRows)
|
|
}
|
|
}
|
|
|
|
// A mesh of every kind of principal composes: nobody is granted a second writer's subject.
|
|
func TestAWholeMeshComposesWithOneWriterPerState(t *testing.T) {
|
|
builder := Seat{Name: "node-build-agent", Scope: "node", Accepts: []string{"build"}, Emits: []string{"built", "started"}}
|
|
principals := []Principal{
|
|
{Kind: KindController, PasswordHash: "x"},
|
|
{Kind: KindNode, Node: "one", PasswordHash: "x"},
|
|
{Kind: KindEnrolment, Node: "two", PasswordHash: "x"},
|
|
{Kind: KindPerson, Module: "jochen", Invokes: []string{"*"}, PasswordHash: "x"},
|
|
{Kind: KindModule, Node: "one", Module: "gitea", Emits: []string{"pull.merged"}, PasswordHash: "x"},
|
|
{Kind: KindModule, Node: "one", Module: "postgres", Emits: []string{"provisioner.failing", "provisioner.recovered"},
|
|
PasswordHash: "x"},
|
|
{Kind: KindModule, Node: "one", Module: "build-agent", Holds: []Seat{builder}, PasswordHash: "x"},
|
|
{Kind: KindNodeTools, Node: "one", Module: RuntimeModule, PasswordHash: "x", Carries: []Declared{
|
|
{Module: "gitea", Emits: []string{"pull.merged"}},
|
|
{Module: "build-agent", Holds: []Seat{builder}}}},
|
|
}
|
|
for _, p := range principals {
|
|
if _, err := PermissionsFor(p); err != nil {
|
|
t.Errorf("%s does not compose: %v", p.Username(), err)
|
|
}
|
|
}
|
|
if _, err := ComposeAccounts(principals); err != nil {
|
|
t.Fatalf("the mesh does not compose: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestASecondWriterIsRefusedAtComposition(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name string
|
|
p Principal
|
|
publish []string
|
|
state string
|
|
}{
|
|
{"the controller publishing what machines say", Principal{Kind: KindController},
|
|
[]string{"mesh.control.>"}, "a machine's applied state and its report"},
|
|
{"a machine publishing another's report", Principal{Kind: KindNode, Node: "one"},
|
|
[]string{"mesh.control.two.report"}, "a machine's applied state and its report"},
|
|
{"a machine publishing every machine's", Principal{Kind: KindNode, Node: "one"},
|
|
[]string{"mesh.control.*.>"}, "a machine's applied state and its report"},
|
|
{"a module writing the lease", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"$KV.mesh-controller_lease.>"}, "the controller lease"},
|
|
{"a module sending a declaration", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"mesh.node.one.declare"}, "a machine's declaration"},
|
|
{"a module defining a stream", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"$JS.API.>"}, "stream definitions and bus permissions"},
|
|
{"a module announcing another forge's merge", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"mesh.mod.gitea.event.pull.merged"}, "a merge announced"},
|
|
{"a module saying a build it did not do", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"mesh.seat.node-build-agent.event.built"}, "builds and their outcomes"},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
err := CheckWriters(c.p, c.publish)
|
|
if err == nil {
|
|
t.Fatalf("%v granted to %s was not refused", c.publish, c.p.Username())
|
|
}
|
|
if !strings.Contains(err.Error(), c.state) {
|
|
t.Fatalf("the refusal does not name %q: %v", c.state, err)
|
|
}
|
|
})
|
|
}
|
|
// And the writers themselves are not refused.
|
|
for _, ok := range []struct {
|
|
p Principal
|
|
publish []string
|
|
}{
|
|
{Principal{Kind: KindNode, Node: "one"}, []string{"mesh.control.one.>"}},
|
|
{Principal{Kind: KindController}, []string{"mesh.node.>", "$JS.API.>", "$KV.mesh-controller_lease.>"}},
|
|
{Principal{Kind: KindModule, Module: "gitea"}, []string{"mesh.mod.gitea.event.pull.merged"}},
|
|
{Principal{Kind: KindModule, Module: "shop"}, []string{"$JS.API.CONSUMER.CREATE.KV_shop_carts.>"}},
|
|
} {
|
|
if err := CheckWriters(ok.p, ok.publish); err != nil {
|
|
t.Errorf("a writer was refused its own: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSubjectsOverlap(t *testing.T) {
|
|
for _, c := range []struct {
|
|
a, b string
|
|
want bool
|
|
}{
|
|
{"mesh.control.>", "mesh.control.*.report", true},
|
|
{"mesh.control.one.>", "mesh.control.*.report", true},
|
|
{"mesh.control.one.alive", "mesh.control.*.report", false},
|
|
{"mesh.control.*", "mesh.control.*.report", false},
|
|
{"$JS.API.>", "$JS.API.STREAM.CREATE.>", true},
|
|
{"$JS.API.CONSUMER.CREATE.KV_x.>", "$JS.API.STREAM.CREATE.>", false},
|
|
{"a.b", "a.b", true},
|
|
{"a.b", "a.b.c", false},
|
|
{"a.>", "a", false},
|
|
} {
|
|
if got := SubjectsOverlap(c.a, c.b); got != c.want || SubjectsOverlap(c.b, c.a) != c.want {
|
|
t.Errorf("%s ~ %s: %v, want %v", c.a, c.b, got, c.want)
|
|
}
|
|
}
|
|
}
|