The gate judged a module by what the mesh saw from outside, so a container that crash-looped after it applied passed it. Each machine's node-engine now states the health of every long-running resource it runs; the controller keeps the newest statement per machine, raises module.<module>.<machine>.unhealthy on the second statement in a row, clears it on the first that does not say it, and the gate passes a module only when every long-running resource of it is stated healthy since the send. An engine that states nothing is judged as before.
181 lines
8.8 KiB
Go
181 lines
8.8 KiB
Go
package broker
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// The writers table (novox/hq to-be 45 §1, ADR 0227 rule 1), compiled in.
|
|
//
|
|
// **Every kind of state the core keeps has one writer; anyone else asks it.** The table is the design's,
|
|
// row for row, with what each row writes on the bus where it writes there. Two things read it: the
|
|
// composition of every principal's grants (PermissionsFor), which **refuses a grant that lets a
|
|
// principal publish on a subject another writes** — so a second writer cannot be granted by accident,
|
|
// and the composition says which state and whose — and the test beside it, which walks the rows
|
|
// against the design's list and the composition of a whole mesh. Changing a writer is a change to
|
|
// this table, through a decision.
|
|
|
|
// WriterRow is one row of the writers table.
|
|
type WriterRow struct {
|
|
State string
|
|
Writer string
|
|
KeptIn string
|
|
Others string
|
|
// Subjects are the bus subjects a write of this state is a publish to; none for state kept off the
|
|
// bus (the controller's store, a module's own) or not built yet.
|
|
Subjects []string
|
|
// Writes says a principal granted a publish pattern overlapping Subjects is this state's writer —
|
|
// given the pattern, because a machine writes its own report and no other's.
|
|
Writes func(p Principal, pattern string) bool
|
|
// Shared says why more than one principal may publish here; empty for one writer.
|
|
Shared string
|
|
}
|
|
|
|
// isController, and the other writers' tests, are who a row's writer is as a principal.
|
|
func isController(p Principal, _ string) bool { return p.Kind == KindController }
|
|
|
|
// ownMachine is a node writing a subject whose third token is its own name, and no wildcard there.
|
|
func ownMachine(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
return p.Kind == KindNode && len(tokens) > 2 && tokens[2] == p.Node
|
|
}
|
|
|
|
// holdsSeatOf is a principal holding the seat a `mesh.seat.<seat>.…` pattern names: its module's own
|
|
// principal, or the node tools carrying a module that holds it (ADR 0175, the runtime is its modules).
|
|
func holdsSeatOf(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
if len(tokens) < 3 {
|
|
return false
|
|
}
|
|
seat := tokens[2]
|
|
holds := func(seats []Seat) bool {
|
|
return slices.ContainsFunc(seats, func(s Seat) bool { return s.Name == seat })
|
|
}
|
|
switch p.Kind {
|
|
case KindModule:
|
|
return holds(p.Holds)
|
|
case KindNodeTools:
|
|
return slices.ContainsFunc(p.Carries, func(d Declared) bool { return holds(d.Holds) })
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ownModule is a module publishing under its own name, or the node tools carrying it.
|
|
func ownModule(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
if len(tokens) < 3 {
|
|
return false
|
|
}
|
|
module := tokens[2]
|
|
switch p.Kind {
|
|
case KindModule:
|
|
return p.Module == module
|
|
case KindNodeTools:
|
|
return slices.ContainsFunc(p.Carries, func(d Declared) bool { return d.Module == module })
|
|
}
|
|
return false
|
|
}
|
|
|
|
// kvOf is a bucket's write subjects.
|
|
func kvOf(bucket string) []string { return []string{"$KV." + bucket + ".>"} }
|
|
|
|
// WritersTable is to-be 45 §1, in its order.
|
|
var WritersTable = []WriterRow{
|
|
{State: "a machine's declaration", Writer: "controller (lease holder)", KeptIn: "the bus, last per subject",
|
|
Others: "read", Subjects: []string{"mesh.node.*.declare"}, Writes: isController},
|
|
{State: "a machine's applied state and its report", Writer: "the node-engine's apply queue",
|
|
KeptIn: "the machine; the report on the bus", Others: "the reconcile and a delivery enqueue, never apply",
|
|
// And its health statement between reports (novox/hq ADR 0240): the same writer stating the same
|
|
// machine, inside the grant it already had (`mesh.control.<its own>.>`).
|
|
Subjects: []string{"mesh.control.*.report", "mesh.control.*.health"}, Writes: ownMachine},
|
|
{State: "the controller lease", Writer: "the controller instance holding it", KeptIn: "key-value " + LeaseBucket,
|
|
Others: "a candidate waits", Subjects: kvOf(LeaseBucket), Writes: isController},
|
|
{State: "plans and their tiers", Writer: "controller (lease holder), compare-and-set on the plan's revision",
|
|
KeptIn: "the controller's store", Others: "read through plans"},
|
|
{State: "conditions", Writer: "controller", KeptIn: "key-value " + ConditionsBucket + " (and its history, " +
|
|
ConditionHistoryBucket + ")", Others: "raise or clear only through observations the controller reads",
|
|
Subjects: append(kvOf(ConditionsBucket), kvOf(ConditionHistoryBucket)...), Writes: isController},
|
|
{State: "calls and their outcomes", Writer: "controller", KeptIn: "key-value " + CallsBucket,
|
|
Others: "read by id", Subjects: kvOf(CallsBucket), Writes: isController},
|
|
{State: "the hand-act log", Writer: "controller, through the verbs that act", KeptIn: "key-value " + HandActsBucket,
|
|
Others: "—", Subjects: kvOf(HandActsBucket), Writes: isController},
|
|
// What the healers did (novox/hq to-be 45 §7, Phase 3): the controller's alone, in its store — the
|
|
// budgets and the mesh-wide brake are counted from it, so a controller restarting cannot reset them.
|
|
{State: "the healers' acts and their brake", Writer: "controller (lease holder), each act begun before it is made",
|
|
KeptIn: "the controller's store", Others: "read through healers; each act said as healer-acted and in its condition's tried"},
|
|
{State: "stream definitions and bus permissions", Writer: "controller", KeptIn: "the bus", Others: "—",
|
|
// A stream's definition, and a durable consumer's by the API that names it so. Not every
|
|
// consumer create: a module watching its own bucket makes and deletes an ordered consumer on the
|
|
// bucket's stream (ADR 0201), which defines nothing the mesh keeps.
|
|
Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>",
|
|
"$JS.API.CONSUMER.DURABLE.CREATE.>"},
|
|
Writes: isController},
|
|
{State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state",
|
|
Others: "the controller asks",
|
|
Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"},
|
|
Writes: holdsSeatOf,
|
|
Shared: "every machine holding the build seat answers the asks it took; each outcome names its ask"},
|
|
{State: "a merge announced", Writer: "one announcer per forge (the hook, or the poll when the hook is absent — never both)",
|
|
KeptIn: "the bus", Others: "—", Subjects: []string{"mesh.mod.*.event.pull.merged"}, Writes: ownModule},
|
|
// A pull request's head, before it merges (novox/hq to-be 45 §9): the same one announcer.
|
|
{State: "a pull request's head announced", Writer: "the forge's announcer, the one that announces its merges",
|
|
KeptIn: "the bus", Others: "the controller asks the build seat to check it", Subjects: []string{"mesh.mod.*.event.pull.updated"},
|
|
Writes: ownModule},
|
|
{State: "a pull request's merge check", Writer: "the build seat's holder that ran it, said as the controller's `checked`",
|
|
KeptIn: "the bus", Others: "the forge's holder sets it as the pull request's status"},
|
|
{State: "a provider's standing", Writer: "the provider", KeptIn: "the provider's events",
|
|
Others: "the controller keeps the newest word as a condition",
|
|
Subjects: []string{"mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered",
|
|
"mesh.mod.*.event.provisioner.retirement"},
|
|
Writes: ownModule},
|
|
{State: "the operator-channel's open messages", Writer: "the seat's holder", KeptIn: "its own key-value state",
|
|
Others: "—"},
|
|
{State: "the facts snapshot", Writer: "controller", KeptIn: "the artifact store, facts/latest",
|
|
Others: "the build seat reads"},
|
|
}
|
|
|
|
// CheckWriters refuses a grant that lets a principal publish on a subject the writers table gives
|
|
// another writer (to-be 45 §1): which state, whose, and the pattern that would make a second writer.
|
|
func CheckWriters(p Principal, publish []string) error {
|
|
var problems []string
|
|
for _, pattern := range publish {
|
|
for _, row := range WritersTable {
|
|
if row.Writes == nil {
|
|
continue
|
|
}
|
|
for _, subject := range row.Subjects {
|
|
if !SubjectsOverlap(pattern, subject) || row.Writes(p, pattern) {
|
|
continue
|
|
}
|
|
problems = append(problems, fmt.Sprintf("%s may publish %s, which writes %s (%s), whose writer is %s",
|
|
p.Username(), pattern, row.State, subject, row.Writer))
|
|
}
|
|
}
|
|
}
|
|
if len(problems) == 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("a second writer would be granted (novox/hq to-be 45 §1, one writer per piece of state):\n %s",
|
|
strings.Join(problems, "\n "))
|
|
}
|
|
|
|
// SubjectsOverlap says some subject matches both patterns: `*` is one token, `>` one or more to the end.
|
|
func SubjectsOverlap(a, b string) bool {
|
|
x, y := strings.Split(a, "."), strings.Split(b, ".")
|
|
for i := 0; ; i++ {
|
|
switch {
|
|
case i == len(x) && i == len(y):
|
|
return true
|
|
case i == len(x) || i == len(y):
|
|
return false
|
|
case x[i] == ">" || y[i] == ">":
|
|
return true
|
|
case x[i] == "*" || y[i] == "*" || x[i] == y[i]:
|
|
continue
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|