The controller asks no build of anybody (ADR 0501 §5, ADR 0525 clause 6, ADR 0503 §3). Every module of a walk is taken from the builder's done key; a key not done yet waits, named in the plan, and never fails it. - C2b/C2c: internal/builder and cmd/mesh-controller's asks go (askABuild, onlyTheBuildersOwn, buildOneAsked, takeIn from an outcome, buildAgentsOwn, the builder's say and its fallback, the daemon hearing an outcome). The server follows the retired seats' built subjects only to let go of them, because the installer's carried user list names them. - C2d: replay, queue, cancel, clear, kill, pause, resume, delivery-check, check-here and merge-gate are refused by name. - C3: the check queues (CHECKS_READY, CHECKS_OTHER) and their workers, the checks-running and plans-building buckets and their grants, the build queue and its hold (plans_building.go, queue.go, link/holds.go, builds_nats.go, checks_nats.go). - C4: merge_gate*.go, the pull request's repo-check path, the Checked event and its layers; a pull request asks the controller nothing but to tell mesh-config what its head declares. - C5: the builds bucket is written only by key_takein. - C10 (help and refusals): the terminal's help lists only what is served. Integration tests of the plan's tiers, sends, stops and retries now run on a stand-in builder with a done key for every module (walk_keys_scaffold_integration_test.go); the tests of the asks, the queue, the merge gate and the replays are deleted.
225 lines
9.9 KiB
Go
225 lines
9.9 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 hand-over asked of a machine",
|
|
"a fresh setuid search asked of a machine",
|
|
"a judgement of a machine's directories asked of it",
|
|
"a machine's applied state and its report",
|
|
"the controller lease",
|
|
"plans and their tiers",
|
|
"conditions",
|
|
"calls and their outcomes",
|
|
"the hand-act log",
|
|
"the healers' acts and their brake",
|
|
"stream definitions and bus permissions",
|
|
"builds and their outcomes",
|
|
// What each module keeps and who may read it, said by the module and kept by the controller (ADR 0495, issue 1790).
|
|
"a module's states announced",
|
|
"the states announced",
|
|
"a merge announced",
|
|
"a pull request's head announced",
|
|
"a pull request's merge check",
|
|
"a provider's standing",
|
|
"the operator-channel's open messages",
|
|
"the facts snapshot",
|
|
"a message a consumer gave up on",
|
|
// The record mirrors, rows of to-be 45 §1's table by design 54 §2 (ADR 0289 §9).
|
|
"the assignments mirror",
|
|
// The chain beside the assignments, the record itself and no mirror of one (ADR 0408, design 47, issue 1089).
|
|
"the builds of the chain",
|
|
"the rollouts of the chain",
|
|
// What each machine has of each module, written by its node-engine under its own keys (ADR 0429 §1, issue 1136).
|
|
"a machine's have",
|
|
// The contents of a declaration's large files, in the bus's object bucket (ADR 0486 §2, issue 1697).
|
|
"a declaration's large files",
|
|
"the declared-config mirror",
|
|
"the predefined-keys mirror",
|
|
"the declared-packages mirror",
|
|
// The provisioner client's word on what it placed (novox/hq ADR 0349 §4, design 57 P6).
|
|
"a module's files placed on a machine",
|
|
}
|
|
|
|
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", "provisioner.retirement"},
|
|
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 asking the controller as another (mesh-cli, ADR 0272)", Principal{Kind: KindNode, Node: "one"},
|
|
[]string{"mesh.control.two.cli"}, "a machine's applied state and its report"},
|
|
{"a module asking the controller as a machine (mesh-cli, ADR 0272)", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"mesh.control.one.cli"}, "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: KindNode, Node: "one"}, []string{"mesh.control.one.cli"}},
|
|
{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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **A hand-over asked of a machine has one publisher, the controller** (novox/hq issue 356): a grant that lets any
|
|
// other principal publish it — a node, the node tools, a module — is refused at composition, naming the state.
|
|
// (Who the engine believes is the signature's; this bounds who is granted the publish.)
|
|
func TestAHandOverAskHasOnePublisher(t *testing.T) {
|
|
for _, p := range []Principal{
|
|
{Kind: KindNode, Node: "laptop"},
|
|
{Kind: KindNodeTools, Node: "laptop", Module: RuntimeModule},
|
|
{Kind: KindModule, Node: "laptop", Module: "notes"},
|
|
} {
|
|
err := CheckWriters(p, []string{"mesh.node.laptop.ask.hand-over"})
|
|
if err == nil || !strings.Contains(err.Error(), "a hand-over asked of a machine") {
|
|
t.Errorf("%s may publish a hand-over: %v", p.Username(), err)
|
|
}
|
|
}
|
|
if err := CheckWriters(Principal{Kind: KindController}, []string{"mesh.node.>"}); err != nil {
|
|
t.Fatalf("the controller may not ask a hand-over: %v", err)
|
|
}
|
|
}
|
|
|
|
// **A fresh setuid search asked of a machine has one publisher, the controller** (novox/hq issue 361), as a
|
|
// hand-over has.
|
|
func TestASetuidSearchAskHasOnePublisher(t *testing.T) {
|
|
for _, p := range []Principal{
|
|
{Kind: KindNode, Node: "laptop"},
|
|
{Kind: KindNodeTools, Node: "laptop", Module: RuntimeModule},
|
|
{Kind: KindModule, Node: "laptop", Module: "notes"},
|
|
} {
|
|
err := CheckWriters(p, []string{AskSetuidSearchSubject("laptop")})
|
|
if err == nil || !strings.Contains(err.Error(), "a fresh setuid search asked of a machine") {
|
|
t.Errorf("%s may ask a setuid search: %v", p.Username(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **A judgement of a machine's directories asked of it has one publisher, the controller** (novox/hq issue 358), as a
|
|
// hand-over has.
|
|
func TestADirectoryJudgementAskHasOnePublisher(t *testing.T) {
|
|
for _, p := range []Principal{
|
|
{Kind: KindNode, Node: "laptop"},
|
|
{Kind: KindNodeTools, Node: "laptop", Module: RuntimeModule},
|
|
{Kind: KindModule, Node: "laptop", Module: "notes"},
|
|
} {
|
|
err := CheckWriters(p, []string{AskJudgeDirectoriesSubject("laptop")})
|
|
if err == nil || !strings.Contains(err.Error(), "a judgement of a machine's directories asked of it") {
|
|
t.Errorf("%s may ask a judgement of directories: %v", p.Username(), err)
|
|
}
|
|
}
|
|
if err := CheckWriters(Principal{Kind: KindController}, []string{AskJudgeDirectoriesSubject("laptop")}); err != nil {
|
|
t.Fatalf("the controller may not ask a judgement: %v", err)
|
|
}
|
|
}
|