internal/chain holds typed Build, Rollout and Assignment and a Backend with no delete or purge, in memory and on the bus. The builds and rollouts buckets are asserted with history 64, no age and values up to just under a mebibyte; the assignments bucket gets the same, keeping its marker age because a stream cannot be made to disallow message ages. Migration 0104 makes the one-row chain_moved switch. The controller keeps no consolidated schema, so 0104 is the whole schema change. Nothing reads the buckets yet.
266 lines
16 KiB
Go
266 lines
16 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
|
|
}
|
|
|
|
// holdsTheBuildSeat is a principal holding the build seat, or the controller, which asserts the bucket the holders write.
|
|
func holdsTheBuildSeat(p Principal, _ string) bool {
|
|
if p.Kind == KindController {
|
|
return true
|
|
}
|
|
return holdsSeatOf(p, "mesh.seat."+TheBuildSeat+".x")
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// ownProvisioner is a machine's provisioner client saying a report about its own machine: the third token of
|
|
// `prov.applied.<node>.…` is its machine (novox/hq ADR 0349 §4, design 57 P3).
|
|
func ownProvisioner(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
return p.Kind == KindProvisioner && len(tokens) > 3 && tokens[2] == p.Node
|
|
}
|
|
|
|
// 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},
|
|
// The operator's hand-over of a directory used as found, asked of the machine's engine at the controller's
|
|
// terminal (novox/hq issue 356). One publisher; and because a responder can still reach the subject through a
|
|
// reply, the ask is signed with the mesh's key and the engine verifies it — the row bounds who is granted the
|
|
// publish, the signature who is believed.
|
|
{State: "a hand-over asked of a machine", Writer: "controller, at its terminal", KeptIn: "the machine, beside its state",
|
|
Others: "the engine verifies the mesh's signature and records it, or refuses",
|
|
Subjects: []string{"mesh.node.*.ask.hand-over"}, Writes: isController},
|
|
// The operator asking a machine's engine for a fresh search for setuid programs, at the controller's
|
|
// terminal (novox/hq issue 361): signed as a hand-over is, under a signing context of its own.
|
|
{State: "a fresh setuid search asked of a machine", Writer: "controller, at its terminal",
|
|
KeptIn: "the machine, which throws its last search away", Others: "the engine verifies the mesh's signature and starts it, or refuses",
|
|
Subjects: []string{"mesh.node.*.ask.setuid-search"}, Writes: isController},
|
|
// A read-only judgement of a machine's declared directories asked of its engine (novox/hq issue 358): signed as a
|
|
// hand-over is, under a signing context of its own. It changes nothing on the machine.
|
|
{State: "a judgement of a machine's directories asked of it", Writer: "controller",
|
|
KeptIn: "nowhere: the engine answers it and keeps nothing", Others: "the engine verifies the mesh's signature and answers, or refuses",
|
|
Subjects: []string{"mesh.node.*.ask.judge-directories"}, 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>.>`). And a line mesh-cli was given
|
|
// on the machine (novox/hq ADR 0272): the node is what the controller judges the terminal by, so that
|
|
// subject is this machine's engine's alone.
|
|
Subjects: []string{"mesh.control.*.report", "mesh.control.*.health", "mesh.control.*.cli"}, 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},
|
|
// And the clock of an open batch's merge window, a plan before it is one (novox/hq ADR 0407 decision 1), and the
|
|
// wait of a rollout for each machine it sent (ADR 0406 §5, issue 974): keys that expire in buckets of their own,
|
|
// written by the lease holder.
|
|
{State: "plans and their tiers", Writer: "controller (lease holder), compare-and-set on the plan's revision",
|
|
KeptIn: "the controller's store; a batch's merge window in key-value " + MergeWindowBucket +
|
|
", a rollout's wait for a machine's report in key-value " + RolloutSilentBucket +
|
|
", and the merges mesh-delivery announced, for a moved module's registration (ADR 0420 §7), in key-value " +
|
|
DeliveryMergesBucket, Others: "read through plans",
|
|
Subjects: append(append(kvOf(MergeWindowBucket), kvOf(RolloutSilentBucket)...), kvOf(DeliveryMergesBucket)...),
|
|
Writes: isController},
|
|
{State: "conditions", Writer: "controller", KeptIn: "key-value " + ConditionsBucket + " (and its history, " +
|
|
ConditionHistoryBucket + "); how often each was observed, when last and its newest evidence in the " +
|
|
"controller's store, so a look that changes nothing writes no bucket", 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 pull request's check asked", Writer: "controller",
|
|
KeptIn: "the work queues " + ChecksReadyStream + " and " + ChecksOtherStream + ", one subject a pull request, the newest kept",
|
|
Others: "the build seat's holders take it",
|
|
Subjects: []string{CheckSubjectPrefix(TheBuildSeat, true) + ">", CheckSubjectPrefix(TheBuildSeat, false) + ">"},
|
|
Writes: isController},
|
|
{State: "a pull request's check running", Writer: "the build seat's holders — several by design, each the key of the check it runs",
|
|
KeptIn: "key-value " + CheckRunningBucket + ", aged out when not renewed", Others: "the build seat's holders read it to drop a duplicate request",
|
|
Subjects: kvOf(CheckRunningBucket), Writes: holdsTheBuildSeat,
|
|
Shared: "every machine holding the build seat puts the key of the check it runs and deletes it when the check ends"},
|
|
{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"},
|
|
// What a consumer gave up on (novox/hq issue 330): copied by the controller from the server's notice,
|
|
// and delivered again or dropped only through its verb, with why.
|
|
{State: "a message a consumer gave up on", Writer: "controller", KeptIn: "the bus, the stream " + DeadLettersStream,
|
|
Others: "read, delivered again or dropped through the controller's dead-letters verb",
|
|
Subjects: []string{deadLetterPrefix + ">", againPrefix + ">"}, Writes: isController},
|
|
// The record mirrors (novox/hq ADR 0289 §9, design 54 §2): rows of this table, so a grant that lets
|
|
// anything else write them is refused at composition. Built so far: the assignments and each registered
|
|
// module's declared config (design 57 F3), and the list of predefined keys kept (design 57 K3).
|
|
{State: "the assignments mirror", Writer: "controller (lease holder), from its store, only on a change",
|
|
KeptIn: "key-value " + AssignmentsBucket, Others: "read through `reads` as mesh-controller." + MirrorAssignments,
|
|
Subjects: kvOf(AssignmentsBucket), Writes: isController},
|
|
// The chain (novox/hq ADR 0408): builds and rollouts beside the assignments above, which keep their bucket. Not
|
|
// mirrors of the store but the record, written by the lease holder by compare-and-set and removed by nobody.
|
|
{State: "the builds of the chain", Writer: "controller (lease holder), compare-and-set on the key's revision",
|
|
KeptIn: "key-value " + BuildsBucket + ", sixty-four revisions of each key, none removed",
|
|
Others: "read through `reads` as mesh-controller." + ChainBuilds, Subjects: kvOf(BuildsBucket), Writes: isController},
|
|
{State: "the rollouts of the chain", Writer: "controller (lease holder), compare-and-set on the key's revision",
|
|
KeptIn: "key-value " + RolloutsBucket + ", sixty-four revisions of each key, none removed",
|
|
Others: "read through `reads` as mesh-controller." + ChainRollouts, Subjects: kvOf(RolloutsBucket), Writes: isController},
|
|
{State: "the declared-config mirror", Writer: "controller (lease holder), from its store, only on a change",
|
|
KeptIn: "key-value " + DeclaredConfigBucket, Others: "read through `reads` as mesh-controller." + MirrorDeclaredConfig,
|
|
Subjects: kvOf(DeclaredConfigBucket), Writes: isController},
|
|
{State: "the predefined-keys mirror", Writer: "controller (lease holder), from its store, only on a change",
|
|
KeptIn: "key-value " + PredefinedBucket, Others: "read through `reads` as mesh-controller." + MirrorPredefined,
|
|
Subjects: kvOf(PredefinedBucket), Writes: isController},
|
|
// What a machine's provisioner client placed of a module's files (novox/hq ADR 0349 §4, design 57 P6): its own
|
|
// machine's word alone, which a walk's end and a gate wait for (ADR 0362, ADR 0349 §15). Kept newest per module
|
|
// in the stream ProvAppliedStream, and by the client in the provisioner server's `applied` bucket.
|
|
{State: "a module's files placed on a machine", Writer: "that machine's provisioner client",
|
|
KeptIn: "the bus, the stream " + ProvAppliedStream + ", newest per module; and key-value " +
|
|
ProvisionerAppliedBucket,
|
|
Others: "the controller reads it at a walk's end and a gate; a failure is that machine's failed apply",
|
|
Subjects: []string{"prov.applied.*.>"}, Writes: ownProvisioner},
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|