diff --git a/go/catalogue/controller_dependence.go b/go/catalogue/controller_dependence.go new file mode 100644 index 0000000..6bd22fa --- /dev/null +++ b/go/catalogue/controller_dependence.go @@ -0,0 +1,261 @@ +package catalogue + +import ( + "fmt" + "slices" + "sort" + "strings" +) + +// **What still depends on the controller, and the lease write's check** (novox/hq ADR 0571 §13, ADR 0582 clause 2, +// issue 774): every declaration of a manifest other than the controller's on the controller (a read of one of its +// states, an invoke of one of its verbs or tools, a consume or a wait on one of its events, a use of its seat), judged +// against the named, temporary list of what another accepted record keeps until the lease write. The grants part's +// grants_lease_check runs it over every manifest the indexer holds; ManifestRefusals runs it over a repository's +// manifests, in the test ADR 0571 §13 names in mesh-catalog, mesh-delivery and mesh-tools. A read of the lease key is no +// dependency: it names the lease's holder, whoever that is, and parts read it to learn that the write happened. + +// ControllerLease is the controller's lease as a reader names it (`.`): no dependency. +const ControllerLease = ControllerSeatName + ".lease" + +// The kinds of a dependency on the controller, as a manifest declares them. +const ( + DependsRead = "read" + DependsInvoke = "invoke" + DependsConsume = "consume" + DependsUses = "uses" + DependsWaits = "waits" +) + +// The phases of the lease write's check (ADR 0571 §13): before the lease write's own delivery the whole named list is +// allowed; that delivery drops every entry but the three kept to the write itself, and its check runs with those alone. +const ( + PhaseBefore = "before-delivery" + PhaseWrite = "write" +) + +// Dependency is one declaration of a manifest other than the controller's on the controller: a read of one of its +// states, an invoke of one of its verbs or tools, a consume or a wait on one of its events, or a send to its seat. +type Dependency struct { + Module string `json:"module"` + // By is the account declaring it when it is not the module's own: `process ` or `tools `. + By string `json:"by,omitempty"` + Kind string `json:"kind"` + What string `json:"what"` + // Kept is the record that keeps it until the lease write, from the named list; empty when none does. + Kept string `json:"kept,omitempty"` + // ToTheWrite says it is one of the three kept to the write itself, which the lease write's own delivery keeps. + ToTheWrite bool `json:"to_the_write,omitempty"` +} + +// KeptDependency is one entry of the named, temporary list ADR 0571 §13 allows: a read, invoke or consume another +// accepted record keeps until the lease write, with that record. ToTheWrite marks the three kept to the write itself. +type KeptDependency struct { + Module, Kind, What, Record string + ToTheWrite bool +} + +// KeepList is the named list. Each entry names the clause that keeps it; a dependency not on it fails the check. The +// third entry kept to the write, the bus node agent's following of the controller's user list (ADR 0571 §8, ADR 0547 +// §8), is no manifest's declaration but the node-engine's own, so no manifest matches it; it stays named here so the +// list reads as the record has it. +var KeepList = []KeptDependency{ + // Kept to the write itself (ADR 0571 §4, §8, §13). + {Module: "mesh-delivery-builder", Kind: DependsConsume, What: "mesh-controller.artifacts-gone", Record: "ADR 0571 §4, §13", ToTheWrite: true}, + {Module: "artifact-store-tools", Kind: DependsInvoke, What: "seat:mesh-controller.collect", Record: "ADR 0571 §4, §13", ToTheWrite: true}, + {Module: "node-engine", Kind: DependsRead, What: "the controller's user list of the bus module's composition", Record: "ADR 0571 §8, §13; ADR 0547 §8", ToTheWrite: true}, + // Kept until the lease write's own delivery. + {Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.placement-export", Record: "ADR 0567 §7"}, + {Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.assign", Record: "ADR 0567 §8"}, + {Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.unassign", Record: "ADR 0567 §8"}, + {Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.pin", Record: "ADR 0567 §8"}, + {Module: "mesh-delivery-assigner", Kind: DependsInvoke, What: "seat:mesh-controller.unpin", Record: "ADR 0567 §8"}, + {Module: "mesh-delivery-assigner", Kind: DependsRead, What: "mesh-controller.assignments", Record: "ADR 0567 §8"}, + {Module: "mesh-delivery-assigner", Kind: DependsRead, What: "mesh-controller.composed", Record: "ADR 0571 §8"}, + {Module: "mesh-grants", Kind: DependsRead, What: "mesh-controller.assignments", Record: "ADR 0549 §5"}, + {Module: "mesh-grants", Kind: DependsConsume, What: "mesh-controller.condition-raised", Record: "ADR 0564 §9"}, + {Module: "mesh-grants", Kind: DependsConsume, What: "mesh-controller.condition-cleared", Record: "ADR 0564 §9"}, + {Module: "messenger", Kind: DependsConsume, What: "mesh-controller.condition-raised", Record: "ADR 0564 §9"}, + {Module: "messenger", Kind: DependsConsume, What: "mesh-controller.condition-cleared", Record: "ADR 0564 §9"}, + {Module: "messenger", Kind: DependsConsume, What: "mesh-controller.condition-changed", Record: "ADR 0564 §9"}, + {Module: "messenger", Kind: DependsInvoke, What: "seat:mesh-controller.conditions", Record: "ADR 0564 §9"}, + {Module: "mesh-watcher", Kind: DependsConsume, What: "mesh-controller.doctor-heartbeat", Record: "ADR 0564 §9"}, + {Module: "mesh-conditions", Kind: DependsRead, What: "mesh-controller.conditions", Record: "ADR 0564 §9"}, + {Module: "mesh-config", Kind: DependsRead, What: "mesh-controller.declared-config", Record: "ADR 0565 §4"}, + {Module: "mesh-config", Kind: DependsRead, What: "mesh-controller.predefined", Record: "ADR 0565 §4"}, + {Module: "mesh-healers", Kind: DependsRead, What: "mesh-controller.hand-acts", Record: "ADR 0565 §11"}, + {Module: "mesh-hand-acts", Kind: DependsRead, What: "mesh-controller.hand-acts", Record: "ADR 0565 §11"}, + // pacman's fallback for a node whose engine has not written its own packages yet. + {Module: "pacman", Kind: DependsRead, What: "mesh-controller.declared-packages", Record: "ADR 0565 §6"}, +} + +// EndedBy names, for a declaration no entry keeps, the record that already ended it, so a refusal says where it moved. +// The controller's `grants` is one: ADR 0571 §9 has no manifest read it from L7's delivery, which changes ADR 0567 §10 +// and ADR 0547 §5's shadow compare, so neither the assigner's nor the grants part's read of it is kept. +var EndedBy = map[string]string{ + DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read", + DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts", + DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts", + DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: each machine's engine answers root-free (seat:node-engine.root-free@machine)", + DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's", +} + +// keptFor is the list's entry for one dependency, or nil. +func keptFor(d Dependency) *KeptDependency { + for i, k := range KeepList { + if k.Module == d.Module && k.Kind == d.Kind && k.What == d.What { + return &KeepList[i] + } + } + return nil +} + +// onController says whether a name `.` is the controller's. +func onController(name string) bool { return strings.HasPrefix(name, ControllerSeatName+".") } + +// invokesController says whether an invoke names a verb of the controller's seat or a tool of its module. `*` names none: +// it is the console's grant to every tool, judged by no record as a dependency. +func invokesController(invoke string) bool { + if seat, _, ok := SeatInvoke(invoke); ok { + return seat == ControllerSeatName + } + return onController(invoke) +} + +// Dependencies are every declaration on the controller in the manifests given, by module, each with the list's entry +// that keeps it. The controller's own manifest is not judged, nor a read of its lease key: it names the lease's holder, +// whoever that is, and parts read it to learn that the write happened (ADR 0571 §13). +func Dependencies(manifests map[string]Manifest) []Dependency { + var out []Dependency + add := func(module, by, kind, what string) { + d := Dependency{Module: module, By: by, Kind: kind, What: what} + if k := keptFor(d); k != nil { + d.Kept, d.ToTheWrite = k.Record, k.ToTheWrite + } + if !slices.Contains(out, d) { + out = append(out, d) + } + } + for name, m := range manifests { + if name == ControllerSeatName || m.Module == ControllerSeatName { + continue + } + for _, r := range m.Reads { + if onController(r) && r != ControllerLease { + add(name, "", DependsRead, r) + } + } + for _, i := range m.Invokes { + if invokesController(i) { + add(name, "", DependsInvoke, i) + } + } + for _, c := range m.Consumes { + if onController(c) { + add(name, "", DependsConsume, c) + } + } + for _, w := range m.Waits { + if onController(w) { + add(name, "", DependsWaits, w) + } + } + for _, u := range m.Uses { + if u == ControllerSeatName { + add(name, "", DependsUses, u) + } + } + for _, a := range m.BusAccounts() { + by := "process " + a.ID + if a.Tools { + by = "tools " + a.ID + } + for _, i := range a.Bus.Invokes { + if invokesController(i) { + add(name, by, DependsInvoke, i) + } + } + } + } + sort.Slice(out, func(i, j int) bool { + a, b := out[i], out[j] + if a.Module != b.Module { + return a.Module < b.Module + } + if a.Kind != b.Kind { + return a.Kind < b.Kind + } + if a.What != b.What { + return a.What < b.What + } + return a.By < b.By + }) + return out +} + +// Verdict is the lease write's check in one phase. +type Verdict struct { + Phase string `json:"phase"` + Passes bool `json:"passes"` + Reasons []string `json:"reasons"` +} + +// Coverage is how much of the mesh's manifests the dependencies were derived from: the manifests read, the modules the +// indexer holds (-1 when its keys could not be listed, which Unread then names), and each manifest that could not be +// read. +type Coverage struct { + Read, Indexed int + Unread []Unread +} + +// Unread is one manifest, or one listing of them, that could not be read, and why. +type Unread struct { + Input string `json:"input"` + Why string `json:"why"` +} + +// Judge runs the lease write's check over the dependencies (ADR 0571 §13) in one phase: before the lease write's own +// delivery, every dependency must be on the named list; at the write, only the three kept to the write itself may stay. +// It passes only over every module the indexer holds: no manifest read, fewer read than the indexer holds, or one that +// could not be read fails it, since what was not read could hide a dependency. A lease already handed away leaves +// nothing to write. +func Judge(deps []Dependency, phase string, cov Coverage, handedAway bool, leaseSays string) Verdict { + v := Verdict{Phase: phase, Reasons: []string{}} + if handedAway { + v.Reasons = append(v.Reasons, "there is nothing to write: "+leaseSays) + } + switch { + case cov.Read == 0: + v.Reasons = append(v.Reasons, fmt.Sprintf("no manifest was read from the indexer (it holds %d modules): with nothing read, nothing on the controller can be judged absent", max(cov.Indexed, 0))) + case cov.Indexed >= 0 && cov.Read < cov.Indexed: + v.Reasons = append(v.Reasons, fmt.Sprintf("only %d of the %d modules the indexer holds had their manifest read, so what the others declare on the controller is not known", cov.Read, cov.Indexed)) + } + for _, d := range deps { + who := d.Module + if d.By != "" { + who += " (" + d.By + ")" + } + switch { + case d.Kept == "": + why := fmt.Sprintf("%s declares %s %s, which no record keeps until the lease write (ADR 0571 §13)", who, d.Kind, d.What) + if ended := EndedBy[d.Kind+" "+d.What]; ended != "" { + why += "; ended by " + ended + } + v.Reasons = append(v.Reasons, why) + case phase == PhaseWrite && !d.ToTheWrite: + v.Reasons = append(v.Reasons, fmt.Sprintf("%s declares %s %s, kept by %s only until the lease write's own delivery, which drops it", who, d.Kind, d.What, d.Kept)) + } + } + for _, g := range cov.Unread { + v.Reasons = append(v.Reasons, fmt.Sprintf("%s could not be read (%s), so what it declares on the controller is not known", g.Input, g.Why)) + } + v.Passes = len(v.Reasons) == 0 + return v +} + +// ManifestRefusals is the lease write's check over a repository's manifests, by module name, in one phase (PhaseBefore +// or PhaseWrite): each reason the check gives, none when it passes. Every manifest given counts as read: the caller reads +// them from its own tree, and none given fails it. +func ManifestRefusals(manifests map[string]Manifest, phase string) []string { + return Judge(Dependencies(manifests), phase, Coverage{Read: len(manifests), Indexed: -1}, false, "").Reasons +} diff --git a/go/catalogue/controller_dependence_test.go b/go/catalogue/controller_dependence_test.go new file mode 100644 index 0000000..09f7604 --- /dev/null +++ b/go/catalogue/controller_dependence_test.go @@ -0,0 +1,97 @@ +package catalogue + +import ( + "slices" + "strings" + "testing" +) + +// Issue 774 (ADR 0571 §13, ADR 0582 clause 2): the lease write's check judges every manifest's dependency on the +// controller against the named list, at the write (the three kept to it alone) and before its delivery. Moved with the +// judge from mesh-grants' leasecheck_test.go (TestTheLeaseCheckJudgesEveryDependencyOnTheController's manifest rows and +// TestTheKeepListNamesItsRecordsAndThreeToTheWrite), whose manifests were mesh-grants' fixtures; the conditions row is +// mesh-conditions' manifest at mesh-catalog ce3bedd5 (module.json:31-32). +func TestTheLeaseWritesCheckJudgesEveryDependencyOnTheController(t *testing.T) { + toTheWrite := 0 + for _, k := range KeepList { + if !strings.HasPrefix(k.Record, "ADR ") || k.Module == "" || k.What == "" { + t.Errorf("entry %+v names no record, module or declaration", k) + } + if k.ToTheWrite { + toTheWrite++ + } + } + if toTheWrite != 3 { + t.Errorf("%d entries kept to the write itself, ADR 0571 §13 names three", toTheWrite) + } + + type want struct { + deps []string // " ", kept by the record or "unkept" + write string // a reason the check at the write names, or "" when it passes + before string // a reason the check before the delivery names, or "" when it passes + } + for _, row := range []struct { + name string + manifests map[string]string + want want + }{ + // 774/S4/AC2. + {"a read of the lease alone is no dependency, and the controller's own manifest is not judged", + map[string]string{"mod-l": `{"module":"mod-l","version":"1","reads":["mesh-controller.lease"]}`, + "mesh-controller": `{"module":"mesh-controller","version":"1","invokes":["seat:mesh-controller.status"]}`}, + want{}}, + // 774/S4/AC1. + {"the keeper's read of the controller's conditions is kept before the write's delivery and refused at the write", + map[string]string{"mesh-conditions": `{"module":"mesh-conditions","version":"1","reads":["mesh-controller.conditions","mesh-controller.lease"]}`}, + want{deps: []string{"mesh-conditions read mesh-controller.conditions kept by ADR 0564 §9"}, + write: "mesh-conditions declares read mesh-controller.conditions, kept by ADR 0564 §9 only until the lease write's own delivery"}}, + {"the builder's consume of artifacts-gone is kept to the write itself", + map[string]string{"mesh-delivery-builder": `{"module":"mesh-delivery-builder","version":"1","consumes":["mesh-controller.artifacts-gone"]}`}, + want{deps: []string{"mesh-delivery-builder consume mesh-controller.artifacts-gone kept by ADR 0571 §4, §13"}}}, + {"a read no record keeps fails both phases, named", + map[string]string{"mod-u": `{"module":"mod-u","version":"1","reads":["mesh-controller.declared-packages"]}`}, + want{deps: []string{"mod-u read mesh-controller.declared-packages unkept"}, + write: "mod-u declares read mesh-controller.declared-packages, which no record keeps until the lease write", + before: "mod-u declares read mesh-controller.declared-packages, which no record keeps until the lease write"}}, + {"a seat verb and a module tool of the controller's, a consume and a wait are each one; every tool (*) is none", + map[string]string{"mod-i": `{"module":"mod-i","version":"1","invokes":["seat:mesh-controller.root-free","mesh-controller.status","*","nats.nats_users"],` + + `"consumes":["mesh-controller.checked","mod-x.thing"],"uses":["mesh-controller"]}`, + "node-tools": `{"module":"node-tools","version":"1","waits":["mesh-controller.checked"]}`}, + want{deps: []string{"mod-i consume mesh-controller.checked unkept", "mod-i invoke mesh-controller.status unkept", + "mod-i invoke seat:mesh-controller.root-free unkept", "mod-i uses mesh-controller unkept", "node-tools waits mesh-controller.checked unkept"}, + write: "mod-i declares invoke seat:mesh-controller.root-free, which no record keeps until the lease write (ADR 0571 §13); ended by ADR 0571 §11", + before: "node-tools declares waits mesh-controller.checked"}}, + {"a process's own invoke of the controller is its module's dependency, named by the process", + map[string]string{"mod-p": `{"module":"mod-p","version":"1","resources":[{"id":"account","type":"user","name":"mod-p"},{"id":"worker","type":"process","run":["./w"],"user":"mod-p",` + + `"bus":{"invokes":["seat:mesh-controller.held"]}}]}`}, + want{deps: []string{"mod-p invoke seat:mesh-controller.held unkept"}, + write: "mod-p (process worker) declares invoke seat:mesh-controller.held", before: "mod-p (process worker)"}}, + {"no manifest fails both phases", map[string]string{}, want{write: "no manifest was read", before: "no manifest was read"}}, + } { + manifests := map[string]Manifest{} + for name, raw := range row.manifests { + m, err := Decode([]byte(raw)) + if err != nil { + t.Fatalf("%s: %s: %v", row.name, name, err) + } + manifests[name] = m + } + var deps []string + for _, d := range Dependencies(manifests) { + kept := "unkept" + if d.Kept != "" { + kept = "kept by " + d.Kept + } + deps = append(deps, d.Module+" "+d.Kind+" "+d.What+" "+kept) + } + if !slices.Equal(deps, row.want.deps) { + t.Errorf("%s: dependencies %q, want %q", row.name, deps, row.want.deps) + } + for phase, reason := range map[string]string{PhaseWrite: row.want.write, PhaseBefore: row.want.before} { + got := ManifestRefusals(manifests, phase) + if (reason == "") != (len(got) == 0) || !slices.ContainsFunc(got, func(r string) bool { return strings.Contains(r, reason) }) && reason != "" { + t.Errorf("%s: at %s refused %q, want %q", row.name, phase, got, reason) + } + } + } +}