A bootstrapped seat counts as read by every node-engine, and node users get the seat list (issue 1966, recovery) #174
@@ -186,14 +186,15 @@ func holds(m Module, node string, n Nodes, ordering bool) (bool, int, string) {
|
||||
}
|
||||
|
||||
// readerLacking says why the module's want on node is held for the node's engine (ADR 0614 §4): a reader capability its
|
||||
// manifest needs that the engine does not state, read as the baseline when it states none. It is held under rule 7 (the
|
||||
// manifest needs that the engine does not state, read as the baseline when it states none, a bootstrapped seat counting as
|
||||
// read by every engine (graph.MissingReads, ADR 0636 §2). It is held under rule 7 (the
|
||||
// composition), since no rule of its own is numbered in this release, with words naming the capability; it moves once the
|
||||
// engine states it. The node-engine's own new build is held only by a word of its own manifest no engine states.
|
||||
//
|
||||
// A build's reference forms hold the want only when the engine reads none of them (ADR 0624 §2): every engine reads a
|
||||
// first form, so a build written in one is never held by its forms; the words name the forms it reads none of.
|
||||
func (m Module) readerLacking(node string, n Nodes) (string, bool) {
|
||||
missing := catalogue.MissingManifestReads(m.Needs, n.Reads[node])
|
||||
missing := graph.MissingReads(m.Needs, n.Reads[node])
|
||||
if len(missing) == 0 && forms.Current.Held(m.FormNeeds, n.Reads[node]) {
|
||||
missing = slices.Clone(m.FormNeeds)
|
||||
}
|
||||
|
||||
@@ -138,6 +138,15 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
|
||||
map[string][]string{"ace": catalogue.ManifestReads(), "g14": catalogue.ManifestReads()}, false, nil, ""},
|
||||
{"the baseline's words on engines stating nothing", "mail", []string{"word:claims.serves", "seat:mesh-catalog", "form:config"}, nil, false, nil, ""},
|
||||
{"the node-engine's new build, whose contract has more words than its manifest uses", "mesh-host", hostNeeds, nil, false, nil, ""},
|
||||
// Issue 1966: g14's engine states its words and forms but no seat (its statement made before its seat list was in
|
||||
// use); needs of the bootstrapped seats mesh-catalog and node-backup move all the same (ADR 0636 §2), one of a seat
|
||||
// not bootstrapped is held.
|
||||
{"mesh-catalog on an engine stating no seat", "mesh-host", []string{"seat:mesh-catalog", "word:module"},
|
||||
map[string][]string{"g14": {"word:module"}}, false, nil, ""},
|
||||
{"node-backup on an engine stating no seat", "restic", []string{"seat:node-backup", "word:module"},
|
||||
map[string][]string{"g14": {"word:module"}}, false, nil, ""},
|
||||
{"a seat not bootstrapped on an engine stating no seat: held, naming it", "mail", []string{"seat:mesh-vault", "word:module"},
|
||||
map[string][]string{"g14": {"word:module"}}, true, nil, "seat:mesh-vault"},
|
||||
// ADR 0624 §2, with form:new a form no engine reads unless it states it and artifact-store the first form.
|
||||
{"g14 back stating {old}, a build carrying {old,new}: moves", "mail", nil, map[string][]string{"g14": {"word:module"}}, false,
|
||||
[]string{"form:artifact-store", "form:new"}, ""},
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/trunk"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
@@ -129,6 +130,7 @@ func meshWide(needs []string) []string {
|
||||
const noneCounted = "no node is counted yet (the assignments is not read), so the hold stays until it is"
|
||||
|
||||
// lacking is, of the counted nodes, each whose engine lacks some of needs, with what it lacks; or why no node is counted.
|
||||
// It judges as the assigner does (graph.MissingReads): a bootstrapped seat is read by every engine.
|
||||
func (ix *Indexer) lacking(needs []string) (map[string][]string, string) {
|
||||
counted, known := ix.Counted()
|
||||
if !known || len(counted) == 0 {
|
||||
@@ -136,7 +138,7 @@ func (ix *Indexer) lacking(needs []string) (map[string][]string, string) {
|
||||
}
|
||||
out := map[string][]string{}
|
||||
for node, stated := range counted {
|
||||
if miss := catalogue.MissingManifestReads(needs, stated); len(miss) > 0 {
|
||||
if miss := graph.MissingReads(needs, stated); len(miss) > 0 {
|
||||
out[node] = miss
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,4 +112,10 @@ func storeHoldersNewManifestWaitsForEveryCountedEngine(t *testing.T) {
|
||||
t.Fatalf("once every counted engine states form:seat-reach modules.distribution = manifest %s commit %s, %d holds, finding %s, bounds %v; want the new entry and none",
|
||||
m.Manifest, m.Commit, len(holds.vals), findings.vals[heldKey], bounds)
|
||||
}
|
||||
// Issue 1966: the hold judges as the assigner does (graph.MissingReads): a bootstrapped seat is read by every engine,
|
||||
// so one whose statement names no seat lacks none of it (ADR 0636 §2).
|
||||
stated = map[string][]string{"n1": {"word:module"}}
|
||||
if l, why := ix.lacking([]string{"seat:mesh-catalog", "form:seat-reach"}); why != "" || len(l) != 1 || !slices.Equal(l["n1"], []string{"form:seat-reach"}) {
|
||||
t.Fatalf("an engine stating no seat lacks %v (%s), want form:seat-reach alone", l, why)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2037,7 +2037,8 @@ func TestGrants(t *testing.T) {
|
||||
|
||||
t.Run("assertions: the node-engine's declared state is created though no node is assigned it", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.manifestOf(buscompose.EngineModule, `{"module":"mesh-host","version":"1","state":[{"name":"acts"}]}`)
|
||||
b.manifestOf(buscompose.EngineModule, `{"module":"mesh-host","version":"1","state":[{"name":"acts"}],`+
|
||||
`"invokes":["seat:mesh-catalog.seat-definitions"],"consumes":["mesh-catalog.seat-definition-changed","restic.backed-up"]}`)
|
||||
b.loaded = shadowOf(t, b)
|
||||
p := partOn(b)
|
||||
p.Create = b
|
||||
@@ -2068,6 +2069,39 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Issue 1966: each node's account calls what the engine's manifest invokes in its own name, as a module's account
|
||||
// does (G1), and hears a consumed seat's event on the seat's own subject, where the engine subscribes it, and still
|
||||
// on the emitter's (G2); a consume naming no seat stays on its emitter's alone.
|
||||
users, err := buscompose.Users(recs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nodes := 0
|
||||
for _, u := range users {
|
||||
if u.Kind != buscompose.KindNode {
|
||||
continue
|
||||
}
|
||||
nodes++
|
||||
perms, err := buscompose.PermissionsFor(u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token := buscompose.CallerToken(u.Username())
|
||||
if !buscompose.MayPublish(perms, "mesh.seat.mesh-catalog.call.seat-definitions."+token) ||
|
||||
buscompose.MayPublish(perms, "mesh.seat.mesh-catalog.call.seat-definitions.other~one") ||
|
||||
buscompose.MayPublish(perms, "mesh.seat.mesh-catalog.call.modules."+token) {
|
||||
t.Errorf("%s: the engine's invoke of seat-definitions is not granted in its own name alone", u.Username())
|
||||
}
|
||||
if !buscompose.MaySubscribe(perms, "mesh.seat.mesh-catalog.event.seat-definition-changed") ||
|
||||
!buscompose.MaySubscribe(perms, "mesh.mod.mesh-catalog.event.seat-definition-changed") ||
|
||||
buscompose.MaySubscribe(perms, "mesh.seat.mesh-catalog.event.manifest-accepted") ||
|
||||
!buscompose.MaySubscribe(perms, "mesh.mod.restic.event.backed-up") {
|
||||
t.Errorf("%s: the engine's consumes are not heard on the seat's subject and the emitter's", u.Username())
|
||||
}
|
||||
}
|
||||
if nodes == 0 {
|
||||
t.Fatal("no node account composed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("assertions: a refused create is said, never stops it, and is tried at the next event", func(t *testing.T) {
|
||||
|
||||
@@ -779,6 +779,16 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
}
|
||||
sub = append(sub, subject)
|
||||
}
|
||||
// And the seats' events the node-engine's manifest consumes, on each seat's own subject, and the verbs it invokes,
|
||||
// in the node's own name (callerNamed, below): as a module's are (issue 1966: the seat list's verb and events).
|
||||
sub = append(sub, watchedEvents(p.Watches)...)
|
||||
tp, ts := seatTrafficGrants(SeatTrafficOf(p.Username(), nil, nil, p.Watches))
|
||||
pub, sub = append(pub, tp...), append(sub, ts...)
|
||||
invoked, err := invokedSubjects(p.Invokes)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, invoked...)
|
||||
// And the creation of the seat mesh-module-assigner's two states it composes from (ADR 0567 §9), so a node agent started
|
||||
// before they exist binds each when the bus says it was made, with no timer; no other bucket's.
|
||||
sub = append(sub, NodeComposeAdvisories()...)
|
||||
@@ -918,19 +928,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
|
||||
// 2b. Events of a role it watches, under the seat's own namespace. Subscribe only: watching a
|
||||
// role is hearing what it announced, not taking part in it.
|
||||
for _, w := range p.Watches {
|
||||
if w.Kinded {
|
||||
continue // composed by SeatTrafficOf below
|
||||
}
|
||||
for _, e := range w.Emits {
|
||||
// An event that names its holder's machine is heard from every machine (machineEvent): a flat subject would
|
||||
// match no event the holder says.
|
||||
if pattern, ok := machineEvent(w, e); ok {
|
||||
e = pattern
|
||||
}
|
||||
sub = append(sub, seatSubject(w, "event", e))
|
||||
}
|
||||
}
|
||||
sub = append(sub, watchedEvents(p.Watches)...)
|
||||
|
||||
// 2c. Its own consumer, which it **pulls**: the runtime asks for the next message and is
|
||||
// answered on its own inbox, so what it needs is to ask about the consumer and to ask it
|
||||
@@ -1609,6 +1607,25 @@ func quoted(values []string) string {
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
// watchedEvents are the subjects a principal subscribes to hear the seats it watches (a `consumes` naming a seat's event):
|
||||
// each on the seat's own namespace, an event naming its holder's machine heard from every machine (machineEvent), since a
|
||||
// flat subject would match no event the holder says. A kinded seat's are SeatTrafficOf's.
|
||||
func watchedEvents(watches []Seat) []string {
|
||||
var out []string
|
||||
for _, w := range watches {
|
||||
if w.Kinded {
|
||||
continue
|
||||
}
|
||||
for _, e := range w.Emits {
|
||||
if pattern, ok := machineEvent(w, e); ok {
|
||||
e = pattern
|
||||
}
|
||||
out = append(out, seatSubject(w, "event", e))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// invokedSubjects is the publish side of a grant to call tools: one subject per `<module>.<tool>`,
|
||||
// or the whole tool namespace for `*`. A person's authority and a module's `invokes` are both this
|
||||
// (novox/hq ADR 0152), so a malformed entry is refused in one place, before it could be widened into
|
||||
|
||||
@@ -200,12 +200,16 @@ func Users(r Records) ([]Principal, error) {
|
||||
// each with the node's permissions.
|
||||
// What the node-engine consumes is what its account hears (a `consumes` line of its own manifest; Engine): the
|
||||
// backup holder's word and the status view's `settled`, with no module named in this code.
|
||||
var engineHears []string
|
||||
// And the seats' events it consumes, on each seat's own subject, and the verbs it invokes, in the node's own name,
|
||||
// granted as a module's are (issue 1966: the seat list's verb seat-definitions and its two events).
|
||||
var engineHears, engineInvokes []string
|
||||
var engineWatches []Seat
|
||||
if r.Engine != nil {
|
||||
engineHears = r.Engine.Consumes
|
||||
engineHears, engineWatches, engineInvokes = r.Engine.Consumes, r.Engine.Watches, r.Engine.Invokes
|
||||
}
|
||||
for _, login := range nodeLoginsOf(r, node) {
|
||||
out = append(out, Principal{Kind: KindNode, Node: node, Login: login, Checks: checks, Runner: runner, Consumes: engineHears,
|
||||
Watches: engineWatches, Invokes: engineInvokes,
|
||||
MovesIntoTheVault: servesMoveIn(r), StatesItsKeyToTheVault: servesVerb(r, VaultNodeKey),
|
||||
AsksItsOwnSecrets: servesVerb(r, VaultOwn), AsksTheVaultToSign: servesVerb(r, VaultSign), Uses: asksSeatOf(r),
|
||||
FollowsGrantsUserList: bus, ServedReads: servedOn(served, node), RotatesItsLogin: servesVerb(r, VaultRotateNode)})
|
||||
|
||||
@@ -111,7 +111,12 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
// The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this
|
||||
// part; it adds no principal, since each node's engine speaks as the node's account.
|
||||
if m, ok := declared[buscompose.EngineModule]; ok {
|
||||
out.Engine = &buscompose.Declared{Module: m.Module, Consumes: m.Consumes, State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
|
||||
// What it consumes and invokes are its node account's: a seat's event also on the seat's own subject, as a module's
|
||||
// consumes are split (consumedOf), every consume still on its emitter's as before, and each verb it invokes in the
|
||||
// node's own name (issue 1966: the seat list's verb and events).
|
||||
_, watches := consumedOf(m, seats, declarers)
|
||||
out.Engine = &buscompose.Declared{Module: m.Module, Consumes: m.Consumes, Watches: watches, Invokes: m.Invokes,
|
||||
State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
|
||||
}
|
||||
return out, gaps
|
||||
}
|
||||
@@ -119,29 +124,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
// declaredFor is mesh-controller's declaredFor (internal/inventory/busrecords.go), copied: one module's manifest as the
|
||||
// composer needs it.
|
||||
func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration, declarers map[string]string) buscompose.Declared {
|
||||
var fromModules []string
|
||||
var watches []buscompose.Seat
|
||||
for _, c := range m.Consumes {
|
||||
emitter, event, named := strings.Cut(c, ".")
|
||||
if named {
|
||||
if s, isASeat := seats[emitter]; isASeat && s.Kinded {
|
||||
ev := strings.TrimSuffix(event, ".*")
|
||||
if catalogue.SeatSays(s.Emits, ev) {
|
||||
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{ev},
|
||||
Kinded: true, Proofs: s.Proofs, DeclaredBy: declarers[s.Name]})
|
||||
continue
|
||||
}
|
||||
}
|
||||
if s, isASeat := seats[emitter]; isASeat && catalogue.SeatSays(s.Emits, event) {
|
||||
// Its scope, and whether the node-engine holds it, place an event that names its holder's machine (ADR 0618 §4).
|
||||
compiled, _ := catalogue.SeatNamed(s.Name)
|
||||
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{event},
|
||||
HeldByTheEngine: compiled.HeldByTheEngine})
|
||||
continue
|
||||
}
|
||||
}
|
||||
fromModules = append(fromModules, c)
|
||||
}
|
||||
fromModules, watches := consumedOf(m, seats, declarers)
|
||||
d := buscompose.Declared{
|
||||
Module: m.Module, ToolRunner: catalogue.IsToolRunner(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools,
|
||||
Invokes: m.Invokes, Waits: waitsOf(m, seats), State: bucketsOf(m), Reads: m.Reads, Logs: logsOf(m),
|
||||
@@ -178,6 +161,36 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
return d
|
||||
}
|
||||
|
||||
// consumedOf splits what a manifest consumes as the composer grants it: a seat's event on the seat's own subject (its
|
||||
// watches), every other by its emitter's (novox/hq ADR 0118). A placed module and the node-engine's own manifest are split
|
||||
// alike.
|
||||
func consumedOf(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration, declarers map[string]string) ([]string, []buscompose.Seat) {
|
||||
var fromModules []string
|
||||
var watches []buscompose.Seat
|
||||
for _, c := range m.Consumes {
|
||||
emitter, event, named := strings.Cut(c, ".")
|
||||
if named {
|
||||
if s, isASeat := seats[emitter]; isASeat && s.Kinded {
|
||||
ev := strings.TrimSuffix(event, ".*")
|
||||
if catalogue.SeatSays(s.Emits, ev) {
|
||||
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{ev},
|
||||
Kinded: true, Proofs: s.Proofs, DeclaredBy: declarers[s.Name]})
|
||||
continue
|
||||
}
|
||||
}
|
||||
if s, isASeat := seats[emitter]; isASeat && catalogue.SeatSays(s.Emits, event) {
|
||||
// Its scope, and whether the node-engine holds it, place an event that names its holder's machine (ADR 0618 §4).
|
||||
compiled, _ := catalogue.SeatNamed(s.Name)
|
||||
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{event},
|
||||
HeldByTheEngine: compiled.HeldByTheEngine})
|
||||
continue
|
||||
}
|
||||
}
|
||||
fromModules = append(fromModules, c)
|
||||
}
|
||||
return fromModules, watches
|
||||
}
|
||||
|
||||
// loadsABundle says the node's tool runtime loads a bundle of the module. The controller reads it from the resolved
|
||||
// manifest's Bundles, which a build derives and the indexer's manifest does not carry (the builder's build record names
|
||||
// the repository's manifest digest), so the same answer is derived here from `build.artifacts` by mesh-sdk's rule
|
||||
|
||||
@@ -18,6 +18,25 @@ func TestGraphEdges(t *testing.T) {
|
||||
t.Run("a node's value", nodeValues)
|
||||
t.Run("a reader of the seat list waits for its holder", readersOfTheSeatList)
|
||||
t.Run("a verb is served by a running healthy have of the node-engine contract", servedByHaves)
|
||||
// ADR 0636 §2, issue 1966: a need of a bootstrapped seat is read by every engine whatever its statement names; a seat
|
||||
// not bootstrapped and a word stay missing, and an engine stating none is read as the baseline.
|
||||
t.Run("a bootstrapped seat is read by every engine", func(t *testing.T) {
|
||||
stated := []string{"word:claims", "word:invokes"}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
needs, stated []string
|
||||
want []string
|
||||
}{
|
||||
{"mesh-catalog and node-backup", []string{"seat:mesh-catalog", "seat:node-backup", "word:invokes"}, stated, nil},
|
||||
{"a seat not bootstrapped", []string{"seat:mesh-catalog", "seat:mesh-vault"}, stated, []string{"seat:mesh-vault"}},
|
||||
{"a word", []string{"seat:node-engine", "word:zone"}, stated, []string{"word:zone"}},
|
||||
{"the baseline", []string{"seat:mesh-catalog", "form:seat-reach"}, nil, []string{"form:seat-reach"}},
|
||||
} {
|
||||
if got := MissingReads(c.needs, c.stated); !slices.Equal(got, c.want) {
|
||||
t.Errorf("%s: missing %v, want %v", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
fp := "src2:" + strings.Repeat("b", 64)
|
||||
node := func(repository, path string, edit func(*Node)) Node {
|
||||
n := Node{FP: fp, Repository: repository, Path: path}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
)
|
||||
|
||||
// bootstrappedReads are the reader capabilities `seat:<seat>` of the bootstrapped seats: the embedded files' own, so
|
||||
// computed once.
|
||||
var bootstrappedReads = sync.OnceValue(func() []string {
|
||||
var out []string
|
||||
for _, d := range seatlist.Bootstrapped() {
|
||||
out = append(out, catalogue.ReadSeat+d.Seat)
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
// MissingReads is catalogue.MissingManifestReads with every need of a bootstrapped seat counted as read (novox/hq ADR 0614
|
||||
// §4, ADR 0636 §2): every reader of the seat list holds the bootstrapped seats from its start, before it has read the list,
|
||||
// so a manifest naming one is read by every node-engine whatever its statement names. Without it an engine whose statement
|
||||
// was made before its list was in use states no seat, and the node-engine's own build naming mesh-catalog is held on the
|
||||
// very engine that would state it (issue 1966). The assigner's capability hold and the indexer's hold both judge with it,
|
||||
// so the rule is one.
|
||||
func MissingReads(needs, stated []string) []string {
|
||||
boot := bootstrappedReads()
|
||||
missing := slices.DeleteFunc(catalogue.MissingManifestReads(needs, stated), func(n string) bool {
|
||||
return strings.HasPrefix(n, catalogue.ReadSeat) && slices.Contains(boot, n)
|
||||
})
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
return missing
|
||||
}
|
||||
Reference in New Issue
Block a user