The node-engine follows the module-assignment seat: assignments, not placement; the seat's packages move to mesh-contracts and mesh-delivery (issue 1966)

The state, events and types of the seat mesh-module-assigner say assignments; the legacy node key form is gone from the
reads; the dead seat-verb list of the retired name is deleted. The placement guard and the placed-file sense stay.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 22:08:40 +01:00
parent e1b9f3e095
commit 8717ec76d4
58 changed files with 581 additions and 616 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ func enrolThroughSeat(ctx context.Context, opts options, token identity.Token, n
}
fmt.Printf("\nenrolled as %s through the seat mesh-node-enroller, dialled as %s and confirmed\n", answer.Node, answer.Login)
fmt.Printf(" identity %s\n", identityPath)
fmt.Println(" nothing is applied until the placement of this node names a module")
fmt.Println(" nothing is applied until the assignment of this node names a module")
return nil
}
+13 -13
View File
@@ -46,7 +46,7 @@ func (w bucketWatch) Watch(ctx context.Context, pattern string) (<-chan agent.Up
return w.updates(ctx, in), nil
}
// WatchAwait is Watch for a state that its holder makes when it starts (the placement seat's): a bucket not on the bus
// WatchAwait is Watch for a state that its holder makes when it starts (the assignment seat's): a bucket not on the bus
// yet is waited for on the bus's word that it was created (link.Session.KVWatchAwait), and waiting is told why.
func (w bucketWatch) WatchAwait(ctx context.Context, pattern string, waiting func(error)) (<-chan agent.Update, error) {
in, err := w.session.KVWatchAwait(ctx, w.bucket, pattern, waiting)
@@ -454,7 +454,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
return
}
var ag *agent.Agent
// The kept release-5 declaration is no longer read once every module the seat's placement puts here has a have of its
// The kept release-5 declaration is no longer read once every module the seat's assignment puts here has a have of its
// own (ADR 0550 §2: its signature is the controller's, trusted no more after the lease write).
// Nor before this engine keeps the node's own as it runs (applied/node.json): until then the kept declaration is the one
// record of what the node's own composition carries, its folded modules among them (issue 774, review of the
@@ -637,7 +637,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
return certifyThisNode(ctx, opts.state, node, trust, session.AskVault, func() { sayFacts(ctx, ag) })
},
}
// And its answer to every placement candidate, placed or not, applying nothing (novox/hq ADR 0559 §4): written into
// And its answer to every assignment candidate, placed or not, applying nothing (novox/hq ADR 0559 §4): written into
// its own key, `<node>.node`, under candidates.
shadowHooks := hooks
shadowHooks.refused, shadowHooks.kept, shadowHooks.certify, shadowHooks.seatList = nil, nil, nil, nil
@@ -650,10 +650,10 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
go shadow.Keep(run, link.Backoff) // this run's alone: a switch begins a run, and its shadow, again
shadowNow.Store(shadow) // what the verb compose-candidate composes a check's candidate with
// Its own composer, always (issue 1126, A7): a composition made elsewhere (the controller's, in the node's
// composition bucket) is never applied. While the seat's placement holds no key for this node it composes
// nothing and keeps running what it runs; a placement it cannot watch stops the agent, said, and it begins again.
// composition bucket) is never applied. While the seat's assignment holds no key for this node it composes
// nothing and keeps running what it runs; a assignment it cannot watch stops the agent, said, and it begins again.
composer := selfComposer(session, node, hooks, say)
machinesNow.Store(composer) // the machines a failing network part points at, by the placements (issue 774)
machinesNow.Store(composer) // the machines a failing network part points at, by the assignments (issue 774)
composerNow.Store(composer) // what the verb compose asks a pass of, this run's alone
env.Composed, env.SelfComposed, env.Policy, env.ComposedToday = composer, true, composer.Policy, composer.ComposedToday
env.HoldsData = composer.HoldsData // what runs from the kept declaration says Data as its manifest does
@@ -792,7 +792,7 @@ func busyModules() []string {
return out
}
// machinesNow is the composer whose placements name the mesh's machines by address, for the health report's targets
// machinesNow is the composer whose assignments name the mesh's machines by address, for the health report's targets
// (writeHealthReport); nil on a node that does not compose its own, whose parts then name no target.
var machinesNow atomic.Pointer[selfcompose.Composer]
@@ -800,11 +800,11 @@ var machinesNow atomic.Pointer[selfcompose.Composer]
// compositions or no run is going: what the verb compose asks a pass of (issue 774).
var composerNow atomic.Pointer[selfcompose.Composer]
// shadowNow is the shadow of the run now, which answers placement candidates and the verb compose-candidate; nil
// shadowNow is the shadow of the run now, which answers assignment candidates and the verb compose-candidate; nil
// between runs.
var shadowNow atomic.Pointer[selfcompose.Shadow]
// keptRetired says the kept declaration is read no more: every module the seat's placement puts on this node has a have of
// keptRetired says the kept declaration is read no more: every module the seat's assignment puts on this node has a have of
// its own (placedAllHave), so what runs is the agent's alone (ADR 0550 §2, design 47's L3 s9).
var keptRetired atomic.Bool
@@ -1001,8 +1001,8 @@ type composerHooks struct {
seatListFailed bool
}
// selfComposer is the node's own composer: it composes what the seat's placement places on the node, and nothing while
// the placement holds no key for it (issue 1126).
// selfComposer is the node's own composer: it composes what the seat's assignment places on the node, and nothing while
// the assignment holds no key for it (issue 1126).
func selfComposer(session *link.Session, node string, hooks composerHooks, say func(string)) *selfcompose.Composer {
env := composerEnv(session, node, say, hooks)
// And it keeps each placed module's data items in its own state data, which the node's disk-load watches (novox/hq
@@ -1018,11 +1018,11 @@ func selfComposer(session *link.Session, node string, hooks composerHooks, say f
}
// composerEnv is what the node's composers read of the bus, the vault and the agent: its own composer and its answer
// to placement candidates (selfcompose.Shadow) alike.
// to assignment candidates (selfcompose.Shadow) alike.
func composerEnv(session *link.Session, node string, say func(string), hooks composerHooks) selfcompose.Env {
env := selfcompose.Env{Node: node,
Watch: func(ctx context.Context, bucket, pattern string) (<-chan agent.Update, error) {
if bucket == buckets.Placement || bucket == buckets.PlacementCandidates {
if bucket == buckets.Assignments || bucket == buckets.ProposalCandidates {
// The seat's states are made when their holder starts: a node-engine that starts first waits for the
// bus's word that the bucket was created, and the shadow and the composer say they wait.
return bucketWatch{session, bucket}.WatchAwait(ctx, pattern, func(err error) {
+39 -39
View File
@@ -7,13 +7,13 @@ import (
"crypto/ed25519"
"encoding/json"
"errors"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
builderbuilds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"github.com/novox/mesh-host/internal/buckets"
"github.com/novox/mesh-host/internal/buckets/bucketstest"
@@ -230,16 +230,16 @@ func within(t *testing.T, d time.Duration, what string, ok func() bool) {
}
// Issue 1126 (ADR 0547 §2, the milestone's H8): the node agent started whole against a real bus, once the catalogue
// places its node (the placement written after it started), composes a module from its want, the indexer's manifest, its record and its mesh-config value, and applies it,
// places its node (the assignment written after it started), composes a module from its want, the indexer's manifest, its record and its mesh-config value, and applies it,
// with no key in the controller's compositions; a changed config value is composed again on its event and applied as a
// revision of what runs. And a catalogue pull request's placement candidate equal to its placement is answered "same"
// revision of what runs. And a catalogue pull request's assignment candidate equal to its assignment is answered "same"
// in the node's own key, and nothing is applied for it (ADR 0559 §4, subtask 10 of the milestone).
//
// And (issue 1126, ADR 0560) it reads, through its own session under the grant the grants part composes for it
// (nodegrant.For with the served key, as the grants part grants it), the build of a placed module that declares one and the one config value
// a provider on another node serves from (a mail domain); under a grant without that served key the bus refuses the
// read and the node composes nothing.
func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) {
func TestNatsANodeTheAssignmentsNameComposesAndAppliesItsOwn(t *testing.T) {
t.Run("without the served key the bus refuses the read", func(t *testing.T) {
b := newItBed(t)
stop, _ := h8(t, b, false)
@@ -260,7 +260,7 @@ func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) {
b := newItBedAs(t, nodegrant.For(itNode, "it2.mailer.domain"))
stop, st := h8(t, b, true)
defer stop()
placement, resolved, candidates := st.placement, st.resolved, st.candidates
assignment, resolved, candidates := st.assignment, st.resolved, st.candidates
hkey, place, fp := st.hkey, st.place, st.fp
within(t, 5*time.Second, "the node did not compose and apply its own module once placed", func() bool { return b.file("web") == "greeting=bonjour\n" })
// ADR 0565 §6 (1953): the node's engine writes its own key of the state packages, under the grant the grants part
@@ -277,7 +277,7 @@ func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) {
t.Errorf("the node's packages key was written again with no package changed: %v, was %v", e, written)
}
t.Run("one answer per candidate revision", func(t *testing.T) { h8once(t, b, resolved, candidates, place) })
h8back(t, b, placement, resolved, candidates, hkey)
h8back(t, b, assignment, resolved, candidates, hkey)
}
// h8once is issue 1126, 2026-10-28 (shanks answered probe/1126-seed-7 at revision 84 every 13-17 s, its bus flooded):
@@ -290,7 +290,7 @@ func h8once(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, place []
defer b.mu.Unlock()
n := 0
for _, l := range b.said {
if strings.HasPrefix(l, "answering placement candidate feat-x ") {
if strings.HasPrefix(l, "answering assignment candidate feat-x ") {
n++
}
}
@@ -320,8 +320,8 @@ func h8once(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, place []
if got := answering(); got != settled {
t.Errorf("feat-x answered %d more time(s) at the same revision, with nothing it is composed from changed", got-settled)
}
same, _ := meshmoduleassignerclient.DecodePlacement(place)
craw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "feat-x", Placement: &same})
same, _ := meshmoduleassignerclient.DecodeAssignments(place)
craw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "feat-x", Assignments: &same})
if err != nil {
t.Fatal(err)
}
@@ -336,11 +336,11 @@ func h8once(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, place []
}
}
// h8back is A7 of issue 1126: no composition made elsewhere is applied. A candidate deleting the node's placement is
// refused, never answered as a return to the controller's compositions; the placement key deleted leaves the agent on
// h8back is A7 of issue 1126: no composition made elsewhere is applied. A candidate deleting the node's assignment is
// refused, never answered as a return to the controller's compositions; the assignment key deleted leaves the agent on
// its own composer, composing nothing and keeping what runs, said in the journal, the node's have still saying it
// composes its own.
func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValue, hkey string) {
func h8back(t *testing.T, b *itBed, assignment, resolved, candidates nats.KeyValue, hkey string) {
t.Helper()
node := func() nodeengine.HaveByNodeNodeValue {
e, err := b.haves.Get(hkey)
@@ -361,7 +361,7 @@ func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValu
if _, err := candidates.Put(ckey, craw); err != nil {
t.Fatal(err)
}
within(t, 10*time.Second, "the node did not refuse a candidate taking its placement away", func() bool {
within(t, 10*time.Second, "the node did not refuse a candidate taking its assignment away", func() bool {
a := node().Candidates["revert-x"]
return a.Proposal == "revert-x" && a.Returns == "" && a.Verdict == nodeengine.CandidateAnswerVerdictRefused &&
strings.Contains(a.Refused, "composes nothing")
@@ -372,30 +372,30 @@ func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValu
if _, err := resolved.Put(itNode+".web.greeting", []byte(`{"value":"salut","source":"module"}`)); err != nil {
t.Fatal(err)
}
if err := placement.Delete(pkey); err != nil {
if err := assignment.Delete(pkey); err != nil {
t.Fatal(err)
}
within(t, 15*time.Second, "the node did not say it composes nothing once its placement went", func() bool {
within(t, 15*time.Second, "the node did not say it composes nothing once its assignment went", func() bool {
b.mu.Lock()
said := strings.Join(b.said, "\n")
b.mu.Unlock()
return strings.Contains(said, "key in the seat's placement state is gone: it composes nothing of its own")
return strings.Contains(said, "key in the seat's assignment state is gone: it composes nothing of its own")
})
time.Sleep(3 * time.Second) // what a pass in flight would mark lands within it
b.mu.Lock()
said := strings.Join(b.said, "\n")
b.mu.Unlock()
if n := node(); n.Composes != nec.ComposesOwn || strings.Contains(said, "applying the controller's compositions") {
t.Errorf("without its placement the node turned to the controller's compositions: %q", n.Composes)
t.Errorf("without its assignment the node turned to the controller's compositions: %q", n.Composes)
}
if h := b.have("web"); h.Composed != ran || h.Phase == nec.PhaseFailed || b.file("web") != "greeting=salut\n" {
t.Errorf("without its placement what runs moved: %+v, %q", h, b.file("web"))
t.Errorf("without its assignment what runs moved: %+v, %q", h, b.file("web"))
}
}
// h8state is what h8 set up that the rest of H8 writes to.
type h8state struct {
placement, resolved, candidates nats.KeyValue
assignment, resolved, candidates nats.KeyValue
// packages is the engine's state packages (ADR 0565 §6), as the grants part creates it from the engine's manifest.
packages nats.KeyValue
hkey string
@@ -414,10 +414,10 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
}
return kv
}
placement, modules, manifests := kvOf(buckets.Placement), kvOf(buckets.Modules), kvOf(buckets.Manifests)
assignment, modules, manifests := kvOf(buckets.Assignments), kvOf(buckets.Modules), kvOf(buckets.Manifests)
resolved := kvOf(selfcompose.ResolvedBucket)
builds := kvOf(builderbuilds.BuilderBuildsBucket)
candidates := kvOf(buckets.PlacementCandidates)
candidates := kvOf(buckets.ProposalCandidates)
packages := kvOf(link.PackagesBucket)
path := filepath.Join(b.dir, "web.conf")
man := `{"module":"web","version":"1","config":{"greeting":{"type":"string","description":"what the file says","default":"hello"}},` +
@@ -429,11 +429,11 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
mkey, _ := meshcatalogclient.ManifestsKey(digest)
fp := fpIt("a")
modv, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)})
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: itNode, Modules: []meshmoduleassigner.PlacedModule{{Module: "web"}}})
place, _ := meshmoduleassignerclient.EncodeAssignments(meshmoduleassigner.Assignments{Node: itNode, Modules: []meshmoduleassigner.PlacedModule{{Module: "web"}}})
rec, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: itNode, Capabilities: []string{"service-manager", "package-manager"}}})
// it1 in one key per module and node (research 072 row 37), it2 in the form before: a node reads both.
pkey, _ := meshmoduleassignerclient.NodeKey(itNode)
places, _ := meshmoduleassignerclient.PlacementValues(meshmoduleassigner.Placement{Node: itNode, Modules: []meshmoduleassigner.PlacedModule{{Module: "web"}}})
places, _ := meshmoduleassignerclient.AssignmentsValues(meshmoduleassigner.Assignments{Node: itNode, Modules: []meshmoduleassigner.PlacedModule{{Module: "web"}}})
place = places[pkey]
hkey, _ := nec.HaveNodeKey(itNode)
// A provider on it2 serving from its own config value: a mail domain, which it1's composition settles.
@@ -443,9 +443,9 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
mlraw, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "mailer", Manifest: mailer})
mlkey, _ := meshcatalogclient.ManifestsKey(mdigest)
mlmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fpIt("b"), Manifest: mdigest, Repository: "novox/mailer", Commit: strings.Repeat("2", 40)})
place2, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "it2", Modules: []meshmoduleassigner.PlacedModule{{Module: "mailer"}}})
places2, _ := meshmoduleassignerclient.AssignmentsValues(meshmoduleassigner.Assignments{Node: "it2", Modules: []meshmoduleassigner.PlacedModule{{Module: "mailer"}}})
rec2, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "it2", Capabilities: []string{"service-manager"}}})
pkey2, _ := meshmoduleassignerclient.LegacyKey("it2")
pkey2, _ := meshmoduleassignerclient.NodeKey("it2")
hkey2, _ := nec.HaveNodeKey("it2")
buildKey, _ := builderbuilds.BuilderBuildKey("web", fp)
done, _ := builderbuilds.EncodeBuilderBuild(builderbuilds.BuilderBuild{State: builderbuilds.BuildDone, Manifest: digest,
@@ -460,7 +460,7 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
value []byte
}{{manifests, mkey, mraw}, {modules, "web", modv}, {b.haves, hkey, rec},
{resolved, itNode + ".web.greeting", []byte(`{"value":"bonjour","source":"module"}`)},
{manifests, mlkey, mlraw}, {modules, "mailer", mlmod}, {b.haves, hkey2, rec2}, {placement, pkey2, place2},
{manifests, mlkey, mlraw}, {modules, "mailer", mlmod}, {b.haves, hkey2, rec2}, {assignment, pkey2, places2[pkey2]}, {assignment, "it2.mailer", places2["it2.mailer"]},
{resolved, "it2.mailer.domain", []byte(`{"value":"example.org","source":"module"}`)}, {builds, buildKey, firstBuild}} {
if _, err := w.kv.Put(w.key, w.value); err != nil {
t.Fatal(err)
@@ -479,10 +479,10 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
if b.file("web") != "" {
t.Fatal("a node the catalogue does not place composed its own")
}
if _, err := placement.Put(itNode+".web", places[itNode+".web"]); err != nil {
if _, err := assignment.Put(itNode+".web", places[itNode+".web"]); err != nil {
t.Fatal(err)
}
if _, err := placement.Put(pkey, place); err != nil {
if _, err := assignment.Put(pkey, place); err != nil {
t.Fatal(err)
}
if checkBuild {
@@ -503,7 +503,7 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
t.Fatal(err)
}
}
return stop, h8state{placement: placement, resolved: resolved, candidates: candidates, packages: packages, hkey: hkey, place: place, fp: fp}
return stop, h8state{assignment: assignment, resolved: resolved, candidates: candidates, packages: packages, hkey: hkey, place: place, fp: fp}
}
// h8rest is H8 once the node composes its own: the have, a config revision, and the candidate answered "same".
@@ -538,13 +538,13 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str
return h.Fp == fp && h.Composed != first && h.Phase == nec.PhaseRunning
})
// The node says its facts (its record, as the test wrote it), then a candidate equal to its placement is written: the
// The node says its facts (its record, as the test wrote it), then a candidate equal to its assignment is written: the
// node answers "same" at the candidate's head and revision in its own key, and applies nothing.
b.session.SayFacts(context.Background(), link.Report{Profile: map[string]any{"capabilities": []any{
map[string]any{"name": "service-manager", "present": true}}}})
second := b.have("web").Composed
same, _ := meshmoduleassignerclient.DecodePlacement(place)
craw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "feat-x", Placement: &same})
same, _ := meshmoduleassignerclient.DecodeAssignments(place)
craw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "feat-x", Assignments: &same})
if err != nil {
t.Fatal(err)
}
@@ -565,7 +565,7 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str
return err == nil && answer.Proposal == "feat-x" && n.Record != nil
})
if answer.Verdict != nodeengine.CandidateAnswerVerdictSame || answer.Revision != int64(crev) || len(answer.Same) != 1 || answer.Same[0] != "web" {
t.Errorf("the answer to a candidate equal to the placement: %+v", answer)
t.Errorf("the answer to a candidate equal to the assignment: %+v", answer)
}
if h := b.have("web"); h.Composed != second || b.file("web") != "greeting=salut\n" {
t.Errorf("a candidate moved what runs: %+v, %q", h, b.file("web"))
@@ -768,10 +768,10 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test
}
return kv
}
placement, modules, manifests := kvOf(buckets.Placement), kvOf(buckets.Modules), kvOf(buckets.Manifests)
assignment, modules, manifests := kvOf(buckets.Assignments), kvOf(buckets.Modules), kvOf(buckets.Manifests)
kvOf(selfcompose.ResolvedBucket)
builds := kvOf(builderbuilds.BuilderBuildsBucket)
kvOf(buckets.PlacementCandidates)
kvOf(buckets.ProposalCandidates)
kvOf(link.PackagesBucket)
// The bus module at a build, done: its manifest written and the indexer's module naming it at fp.
publish := func(fp, env string) {
@@ -798,7 +798,7 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test
rec, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: itNode,
Capabilities: []string{"service-manager", "package-manager", "container-runtime"}}})
hkey, _ := nec.HaveNodeKey(itNode)
places, _ := meshmoduleassignerclient.PlacementValues(meshmoduleassigner.Placement{Node: itNode, Modules: []meshmoduleassigner.PlacedModule{{Module: "nats"}}})
places, _ := meshmoduleassignerclient.AssignmentsValues(meshmoduleassigner.Assignments{Node: itNode, Modules: []meshmoduleassigner.PlacedModule{{Module: "nats"}}})
// The have the engine wrote of the folded bus: the old-form build, no fingerprint.
natsHave, _ := nec.HaveKey(itNode, "nats")
oldHave, _ := nec.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: "build-1792674595610176744", Phase: nec.PhaseRunning, Health: nec.HealthOK})
@@ -848,7 +848,7 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test
stop := b.start()
defer stop()
for key, value := range places {
if _, err := placement.Put(key, value); err != nil {
if _, err := assignment.Put(key, value); err != nil {
t.Fatal(err)
}
}
+22 -29
View File
@@ -7,11 +7,11 @@ import (
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"github.com/novox/mesh-host/internal/buckets"
"os"
@@ -29,7 +29,7 @@ import (
// **What this engine trusts a mesh signature from** (novox/hq ADR 0550 §2; design 47's L3 item d; issue 1126): the
// vault's key alone, read from the vault's state `signers`, which only the vault may write: the bus grants are its
// anchor, as the wants' and the placement's. The controller's key, which the node was enrolled with (its membership's
// anchor, as the wants' and the assignment's. The controller's key, which the node was enrolled with (its membership's
// signer), is trusted never: the controller ended by its unassignment (the operator, 2026-11-02) and its lease is never
// handed, so no gate waits on it. What it trusts now the node states in its record (NodeRecord.Trusts).
type meshTrust struct {
@@ -180,20 +180,17 @@ func followTrust(ctx context.Context, session *link.Session, trust *meshTrust, s
}
// placedAllHave says whether every module the seat mesh-module-assigner places on node has a have of its own, read from the
// bus: then the kept declaration is read no more (keptRetired). A node the seat does not place, or whose placement or haves
// bus: then the kept declaration is read no more (keptRetired). A node the seat does not place, or whose assignment or haves
// cannot be read, keeps reading it.
func placedAllHave(ctx context.Context, session *link.Session, node string) (bool, string) {
key, err := meshmoduleassignerclient.NodeKey(node)
if err != nil {
return false, err.Error()
}
legacy, _ := meshmoduleassignerclient.LegacyKey(node)
// Its node key names every module placed (research 072 row 37); before the holder converted it, its legacy key does.
// Its node key names every module placed (research 072 row 37).
values := map[string][]byte{}
for _, k := range []string{key, legacy} {
if h, err := session.KVHistory(ctx, buckets.Placement, k); err == nil && len(h) > 0 && !h[len(h)-1].Deleted {
values[k] = h[len(h)-1].Value
}
if h, err := session.KVHistory(ctx, buckets.Assignments, key); err == nil && len(h) > 0 && !h[len(h)-1].Deleted {
values[key] = h[len(h)-1].Value
}
p, err := placedModules(node, values)
if err != nil {
@@ -204,27 +201,23 @@ func placedAllHave(ctx context.Context, session *link.Session, node string) (boo
})
}
// placedModules is the modules node's node key names (a placement of their names alone), or its legacy key's placement
// where it has no node key; an error when neither reads.
func placedModules(node string, values map[string][]byte) (meshmoduleassigner.Placement, error) {
// placedModules is the modules node's node key names (an assignment of their names alone); an error when it does not
// read.
func placedModules(node string, values map[string][]byte) (meshmoduleassigner.Assignments, error) {
key, _ := meshmoduleassignerclient.NodeKey(node)
if raw, ok := values[key]; ok {
n, err := meshmoduleassignerclient.DecodeNode(raw)
if err != nil {
return meshmoduleassigner.Placement{}, err
}
p := meshmoduleassigner.Placement{Node: node}
for _, r := range n.Modules {
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: r.Module})
}
return p, nil
}
legacy, _ := meshmoduleassignerclient.LegacyKey(node)
raw, ok := values[legacy]
raw, ok := values[key]
if !ok {
return meshmoduleassigner.Placement{}, errors.New("its placement is not read")
return meshmoduleassigner.Assignments{}, errors.New("its assignment is not read")
}
return meshmoduleassignerclient.DecodePlacement(raw)
n, err := meshmoduleassignerclient.DecodeNode(raw)
if err != nil {
return meshmoduleassigner.Assignments{}, err
}
p := meshmoduleassigner.Assignments{Node: node}
for _, r := range n.Modules {
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: r.Module})
}
return p, nil
}
// allHave says whether has finds, for every module p places, a have that names a fingerprint of the new form, and why
@@ -232,7 +225,7 @@ func placedModules(node string, values map[string][]byte) (meshmoduleassigner.Pl
// declaration is the one record of what runs of it: retired then, the node's own composed it as nothing and refused to
// remove what runs (issue 774, 2026-10-30 19:43: systemd-resolved on shanks, nats and restic on novox, each node's own
// apply refused from then on).
func allHave(p meshmoduleassigner.Placement, has func(module string) (nodeengine.HaveByNodeModuleValue, bool, error)) (bool, string) {
func allHave(p meshmoduleassigner.Assignments, has func(module string) (nodeengine.HaveByNodeModuleValue, bool, error)) (bool, string) {
if len(p.Modules) == 0 {
return false, "the seat mesh-module-assigner places no module here"
}
+4 -4
View File
@@ -6,8 +6,8 @@ import (
"encoding/json"
"errors"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
engcomp "github.com/novox/mesh-host/internal/compositions"
"path/filepath"
@@ -120,7 +120,7 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
if d, err := keptDeclaration(state, nil); err != nil || len(d.Resources) != 1 {
t.Fatalf("before the read is retired: %+v, %v", d, err)
}
placed := meshmoduleassigner.Placement{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "zsh"}, {Module: "lemurs"}}}
placed := meshmoduleassigner.Assignments{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "zsh"}, {Module: "lemurs"}}}
// Issue 774, 2026-10-30 19:43: a have naming only a build of the old form (systemd-resolved on shanks, folded in the
// node's own) keeps the kept declaration read, since it is the one record of what runs of it.
newForm := nodeengine.HaveByNodeModuleValue{Fp: meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64), Phase: nodeengineclient.PhaseRunning}
@@ -139,7 +139,7 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
}
}
// The module the mesh provides never has a fingerprint, and keeps no read (review of issue 774).
withWireguard := meshmoduleassigner.Placement{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "zsh"}, {Module: overlay.Name}}}
withWireguard := meshmoduleassigner.Assignments{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "zsh"}, {Module: overlay.Name}}}
if got, why := allHave(withWireguard, func(m string) (nodeengine.HaveByNodeModuleValue, bool, error) {
if m == overlay.Name {
return oldForm, true, nil
@@ -148,7 +148,7 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
}); !got {
t.Errorf("%s without a fingerprint beside zsh at the new form keeps the read: %s", overlay.Name, why)
}
if got, _ := allHave(meshmoduleassigner.Placement{Node: "n1"}, nil); got {
if got, _ := allHave(meshmoduleassigner.Assignments{Node: "n1"}, nil); got {
t.Error("a node with nothing placed retires the read")
}
keptRetired.Store(true)