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:
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -13,9 +13,9 @@ require (
|
||||
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"github.com/novox/mesh-host/internal/compositions"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"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/seats/meshwants"
|
||||
"github.com/novox/mesh-host/internal/buckets/bucketstest"
|
||||
"github.com/novox/mesh-host/internal/compositions"
|
||||
"github.com/novox/mesh-host/internal/judge"
|
||||
|
||||
@@ -183,7 +183,7 @@ type Options struct {
|
||||
// SDKSource is where the mesh's shared library is built from. It is published to the package
|
||||
// registry before the base is built, because the base resolves it by version (novox/hq ADR 0076).
|
||||
SDKSource Source
|
||||
// Site is where this machine sits, for the private network's placement.
|
||||
// Site is where this machine sits, for the private network's assignment.
|
||||
Site string
|
||||
// Answers are the choices, answered by flag: name → answer. What a terminal would be asked.
|
||||
Answers map[string]string
|
||||
|
||||
@@ -146,7 +146,7 @@ func InstallFromCatalogue(ctx context.Context, o Options, control controlPlane,
|
||||
//
|
||||
// **Assigning is not being on the network** — a lesson paid for: the module installed, the names
|
||||
// file was written with no names in it, and everything reported success, because nobody had said
|
||||
// where this machine IS. So placement is part of the step, not a separate act.
|
||||
// where this machine IS. So assignment is part of the step, not a separate act.
|
||||
func PlaceOnTheNetwork(ctx context.Context, o Options, control controlPlane,
|
||||
brokerAddress string, say func(string)) error {
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
|
||||
vault "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
wants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
wants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
@@ -27,13 +27,13 @@ const (
|
||||
// HaveHistory is the number of revisions the have bucket keeps per key.
|
||||
const HaveHistory = 20
|
||||
|
||||
// The states of the placement seat's holder and of the wants seat's holder that the engine reads, named by the holder's
|
||||
// The states of the assigner seat's holder and of the wants seat's holder that the engine reads, named by the holder's
|
||||
// module as every state is (ADR 0634 §1, finding F4: the holder's name is kept here, in the engine's code).
|
||||
const (
|
||||
// Placement holds a node's placement, `<node>.node`, and each of its modules, `<node>.<module>`.
|
||||
Placement = AssignerHolder + "_" + seat.StatePlacement
|
||||
// PlacementCandidates holds a node's candidate of a proposal, `<proposal>.<node>`.
|
||||
PlacementCandidates = AssignerHolder + "_" + seat.StateCandidates
|
||||
// Assignments holds a node's assignments, `<node>.node`, and each of its modules, `<node>.<module>`.
|
||||
Assignments = AssignerHolder + "_" + seat.StateAssignments
|
||||
// ProposalCandidates holds a node's candidate of a proposal, `<proposal>.<node>`.
|
||||
ProposalCandidates = AssignerHolder + "_" + seat.StateCandidates
|
||||
// Wants holds what each node should run, `want.<node>.<module>` and `want.<node>.node`.
|
||||
Wants = AssignerHolder + "_" + wants.StateWants
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestACandidateKeyEscapesTheGroup(t *testing.T) {
|
||||
// holder (ADR 0625 §4), so these rows are the one place the node-engine's code says them.
|
||||
func TestTheHoldersBucketsAreTheirStatesBuckets(t *testing.T) {
|
||||
for name, c := range map[string][2]string{
|
||||
"Placement": {Placement, "mesh-delivery-assigner_placement"}, "PlacementCandidates": {PlacementCandidates, "mesh-delivery-assigner_candidates"},
|
||||
"Assignments": {Assignments, "mesh-delivery-assigner_assignments"}, "ProposalCandidates": {ProposalCandidates, "mesh-delivery-assigner_candidates"},
|
||||
"Wants": {Wants, "mesh-delivery-assigner_wants"}, "Modules": {Modules, "mesh-delivery-indexer_modules"},
|
||||
"Manifests": {Manifests, "mesh-delivery-indexer_manifests"}, "Verdicts": {Verdicts, "mesh-delivery-indexer_verdicts"},
|
||||
"Candidates": {Candidates, "mesh-delivery-indexer_candidates"}, "Provisions": {Provisions, "mesh-provisioner_provisions"},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package bucketstest writes the values of the states the engine only reads (a want, a placement candidate) as their
|
||||
// Package bucketstest writes the values of the states the engine only reads (a want, a assignment candidate) as their
|
||||
// holders do, for the tests that stand in for the holders.
|
||||
package bucketstest
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"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-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
)
|
||||
|
||||
// Version is the value version the holders write.
|
||||
@@ -26,13 +26,13 @@ func NodeWant(w meshwants.WantsByWantNodeNodeValue) ([]byte, error) {
|
||||
return json.Marshal(w)
|
||||
}
|
||||
|
||||
// Candidate is a node's candidate of a proposal as the placement seat's holder writes it.
|
||||
// Candidate is a node's candidate of a proposal as the assignment seat's holder writes it.
|
||||
func Candidate(c meshmoduleassigner.CandidatesByProposalNodeValue) ([]byte, error) {
|
||||
c.V = Version
|
||||
if c.Placement != nil {
|
||||
p := *c.Placement
|
||||
if c.Assignments != nil {
|
||||
p := *c.Assignments
|
||||
p.V = Version
|
||||
c.Placement = &p
|
||||
c.Assignments = &p
|
||||
}
|
||||
return json.Marshal(c)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func Pull(head meshdeliveryclient.Head, modules []string, fps map[string]meshcat
|
||||
return meshcatalog.CandidatesByGroupValuePullsItem{Repository: head.Repository, Number: int64(head.Number), Head: head.Head, Modules: modules, Fps: fps}
|
||||
}
|
||||
|
||||
// Raw is each value as JSON, as a composer case carries verdicts and placements.
|
||||
// Raw is each value as JSON, as a composer case carries verdicts and assignments.
|
||||
func Raw[V any](in map[string]V) map[string]json.RawMessage {
|
||||
out := map[string]json.RawMessage{}
|
||||
for k, v := range in {
|
||||
|
||||
@@ -50,7 +50,7 @@ func CompositionsApplied(in Input, node string, fps map[string]string) (map[stri
|
||||
}
|
||||
|
||||
// CompositionsShadow is CompositionsApplied for a composition that is compared and never applied (a node's answer to a
|
||||
// placement candidate, novox/hq ADR 0559 §4): composed in the Foreseeing mode, so an own secret never issued (a module
|
||||
// assignment candidate, novox/hq ADR 0559 §4): composed in the Foreseeing mode, so an own secret never issued (a module
|
||||
// the candidate adds) is a named stand-in rather than an error, and what it stands in for is answered (stood, sorted,
|
||||
// the bus's user list left out) rather than refused, so the caller judges each stand-in: a value not yet issued for a
|
||||
// module the candidate adds, or one missing for a module the node runs. A module whose every value is held composes
|
||||
|
||||
@@ -351,7 +351,7 @@ func (c *composer) ownSecretsFor(node Node, plan catalogue.Resolution, handing c
|
||||
if !held && mode == Foreseeing {
|
||||
// Asked ahead of the send: what the send would do about it, by the send's own rules. Made by it -- a stand-in,
|
||||
// named; refused by it -- refused here, the same words (novox/hq issue 275). A bus account not yet issued is
|
||||
// foreseen, never refused: the vault mints it from the grants part's wanted list once the placement is written
|
||||
// foreseen, never refused: the vault mints it from the grants part's wanted list once the assignment is written
|
||||
// on the operator's Approve (ADR 0561 §1, ADR 0567 §5), so a composition asked ahead of that cannot hold it
|
||||
// (issue 774, 2026-10-30: every proposal placing mesh-conditions, mesh-self-check or mesh-healers refused).
|
||||
if err := c.wouldMakeSecretForModule(node, m.Module, name); err != nil {
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestAnOutsideSecretIsPlacedOnlyOnceAPersonGaveIt(t *testing.T) {
|
||||
|
||||
// 1364/AC3 (the send's refusals, novox/hq issue 275): in the Foreseeing mode a given secret under an old key is refused in the
|
||||
// controller's words. No secret value is in an error. A bus account not yet issued is foreseen, issued or not (issue 774: the vault
|
||||
// mints it once the placement is written on the Approve, ADR 0561 §1, ADR 0567 §5; mesh-self-check's proposal on ace was refused
|
||||
// mints it once the assignment is written on the Approve, ADR 0561 §1, ADR 0567 §5; mesh-self-check's proposal on ace was refused
|
||||
// "has no bus credential: nothing was issued for ace.mesh-self-check").
|
||||
func TestTheSendsRefusalsAreRefusedInTheSendsWords(t *testing.T) {
|
||||
broker := catalogue.Manifest{Module: "mesh-self-check", Version: "1", RunsAs: "mesh-self-check",
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"github.com/novox/mesh-host/internal/compose/resolution"
|
||||
)
|
||||
|
||||
// Answer is a node's answer to its proposal's placement candidates (novox/hq ADR 0567 §3, ADR 0559 §4): s.Node composed in shadow with
|
||||
// s.Placements laid over the trunk's placement (compose.CompositionsShadow, its own sealed values and boxes through r as
|
||||
// Answer is a node's answer to its proposal's assignment candidates (novox/hq ADR 0567 §3, ADR 0559 §4): s.Node composed in shadow with
|
||||
// s.Assignments laid over the trunk's assignment (compose.CompositionsShadow, its own sealed values and boxes through r as
|
||||
// for its own composition), compared with what it runs (compositions.CompareComposed). It applies nothing and writes
|
||||
// nothing; the proposal and the revision are the candidate's, said back in the answer.
|
||||
//
|
||||
@@ -35,9 +35,9 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
|
||||
a.Verdict = nodeengine.CandidateAnswerVerdictRefused
|
||||
return a
|
||||
}
|
||||
trunk, err := r.Placements(ctx)
|
||||
trunk, err := r.Assignments(ctx)
|
||||
if err != nil {
|
||||
return refuse(fmt.Errorf("frombus: placement: %w", err))
|
||||
return refuse(fmt.Errorf("frombus: assignment: %w", err))
|
||||
}
|
||||
if s.Running == nil {
|
||||
s.Running = running
|
||||
@@ -144,7 +144,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
|
||||
|
||||
// What the proposal adds, by node and module: placed by a candidate and not by the trunk on that node.
|
||||
added := map[string]bool{}
|
||||
for node, p := range s.Placements {
|
||||
for node, p := range s.Assignments {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
@@ -430,7 +430,7 @@ const (
|
||||
leaveOwn = "own-secrets"
|
||||
)
|
||||
|
||||
// assignedOn is the modules placed on node, in placement order.
|
||||
// assignedOn is the modules placed on node, in assignment order.
|
||||
func assignedOn(in compose.Input, node string) []string {
|
||||
for _, n := range in.Nodes {
|
||||
if n.Name == node {
|
||||
@@ -454,7 +454,7 @@ func manifestOn(in compose.Input, node, module string) (catalogue.Manifest, bool
|
||||
}
|
||||
|
||||
// culprit is the module placed on node whose composition the error is of, and what of its offer to leave out: the
|
||||
// first, in placement order, whose stub (compose.Stub) makes the composition in mode succeed or fail otherwise (leave
|
||||
// first, in assignment order, whose stub (compose.Stub) makes the composition in mode succeed or fail otherwise (leave
|
||||
// ""); else the first whose stub without what it contributes to one provision does (leave that provision: de-spiegel's
|
||||
// routes, its names unset, and not its smtp account, issue 1960); else the first whose bare stub does (leaveAll: its
|
||||
// stub contributing nothing, its own secret never issued, issue 1126 seed-8). Empty when none does (the error is not
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"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/seats/meshmoduleassigner"
|
||||
"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/modules/mesh-delivery-builder/builds"
|
||||
@@ -41,11 +41,11 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
lemurs := []byte(`{"module":"lemurs","version":"1","resources":[{"id":"service","type":"service","unit":"lemurs.service","state":"running","boot":"enabled"}]}`)
|
||||
fp := func(c string) string { return "src2:" + strings.Repeat(c, 64) }
|
||||
mk := func(placed ...string) *bus {
|
||||
p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
||||
p := meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
||||
for _, m := range placed {
|
||||
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
|
||||
}
|
||||
b := &bus{placements: map[string]meshmoduleassigner.Placement{"n1": p},
|
||||
b := &bus{assignments: map[string]meshmoduleassigner.Assignments{"n1": p},
|
||||
records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd", "container-runtime"}}},
|
||||
modules: map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Manifest: meshcatalogclient.ManifestDigest(lemurs)}},
|
||||
manifests: map[string][]byte{meshcatalogclient.ManifestDigest(lemurs): lemurs},
|
||||
@@ -69,14 +69,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
}
|
||||
return b
|
||||
}
|
||||
given := func(b *bus) map[string]*meshmoduleassigner.Placement {
|
||||
p := b.placements["n1"]
|
||||
return map[string]*meshmoduleassigner.Placement{"n1": &p}
|
||||
given := func(b *bus) map[string]*meshmoduleassigner.Assignments {
|
||||
p := b.assignments["n1"]
|
||||
return map[string]*meshmoduleassigner.Assignments{"n1": &p}
|
||||
}
|
||||
|
||||
// Every refusal: two builds taken in, and a module the composition refuses (its image never built), each named.
|
||||
b := mk("lemurs", "web", "api", "spiegel")
|
||||
a := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b)},
|
||||
a := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Assignments: given(b)},
|
||||
map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3)
|
||||
var refused []string
|
||||
for _, r := range a.Refusals {
|
||||
@@ -100,13 +100,13 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
}
|
||||
// Issue 1126: a module the node holds today that the candidate holds too is held as today, no refusal: with every
|
||||
// refused module held today the verdict is no refusal, and each is named under Held.
|
||||
today := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: map[string]string{"api": "held", "spiegel": "held", "web": "held"}},
|
||||
today := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Assignments: given(b), HeldToday: map[string]string{"api": "held", "spiegel": "held", "web": "held"}},
|
||||
map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3)
|
||||
if today.Verdict == nodeengine.CandidateAnswerVerdictRefused || len(today.Refusals) != 0 || len(today.Held) != 3 || today.Held[0].Why == "" {
|
||||
t.Errorf("held as today: %+v", today)
|
||||
}
|
||||
// A refused module the node does not hold today stays a refusal: holding only api today leaves spiegel and web refused.
|
||||
part := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: map[string]string{"api": "held"}},
|
||||
part := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Assignments: given(b), HeldToday: map[string]string{"api": "held"}},
|
||||
map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3)
|
||||
if part.Verdict != nodeengine.CandidateAnswerVerdictRefused || len(part.Refusals) != 2 || len(part.Held) != 1 || part.Held[0].Module != "api" {
|
||||
t.Errorf("held as today in part: %+v", part)
|
||||
@@ -123,9 +123,9 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
// The fold: spiegel runs inside the node's own composition at a build of the old form. Its resources are carried as
|
||||
// they run, each after the one it runs after: the node's own is the one it runs, and the node composes itself.
|
||||
b = mk("lemurs", overlay.Name, "spiegel")
|
||||
p := b.placements["n1"]
|
||||
p := b.assignments["n1"]
|
||||
p.Decided.Endpoint = "n1.example:51820"
|
||||
b.placements["n1"] = p
|
||||
b.assignments["n1"] = p
|
||||
fps := map[string]string{"lemurs": fp("e")}
|
||||
stub := Scope{Node: "n1"}
|
||||
in, err := Input(ctx, b, Mesh{}, stub)
|
||||
@@ -169,14 +169,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
if string(gotOwn) != string(wantOwn) {
|
||||
t.Errorf("the node's own composition is\n%s\nwant what runs\n%s", gotOwn, wantOwn)
|
||||
}
|
||||
a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, running, "p-h1", 3)
|
||||
a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Assignments: given(b), InOwn: []string{"spiegel"}}, fps, running, "p-h1", 3)
|
||||
if a.Verdict != nodeengine.CandidateAnswerVerdictSame {
|
||||
t.Errorf("the fold's answer: %+v, want the same", a)
|
||||
}
|
||||
// (A) The agent also says spiegel runs by module, from the declaration kept before the switch (agent.Everything): a
|
||||
// module the composition folds is judged inside the node's own, not as removed (issue 1126, seed #531's answer).
|
||||
keptToo := append(append([]engcomp.Composed{}, running...), engcomp.Composed{Node: "n1", Module: "spiegel", Resources: carriedRes})
|
||||
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, keptToo, "p-h1", 3); a.Verdict != nodeengine.CandidateAnswerVerdictSame || len(a.Removed) != 0 {
|
||||
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Assignments: given(b), InOwn: []string{"spiegel"}}, fps, keptToo, "p-h1", 3); a.Verdict != nodeengine.CandidateAnswerVerdictSame || len(a.Removed) != 0 {
|
||||
t.Errorf("a folded module running from the kept declaration is answered %+v, want the same", a)
|
||||
}
|
||||
// (B) The node's own composition carries its bus membership as it runs it (the controller's, from its store): composed
|
||||
@@ -189,7 +189,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
}
|
||||
withMembership = append(withMembership, c)
|
||||
}
|
||||
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, withMembership, "p-h1", 3); a.Verdict != nodeengine.CandidateAnswerVerdictSame {
|
||||
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Assignments: given(b), InOwn: []string{"spiegel"}}, fps, withMembership, "p-h1", 3); a.Verdict != nodeengine.CandidateAnswerVerdictSame {
|
||||
t.Errorf("the node's own with its bus membership is answered %+v, want the same", a)
|
||||
}
|
||||
// Without what runs, the fold composes spiegel from its manifest, as before, and the composition refuses it.
|
||||
@@ -202,9 +202,9 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
// "not built for this mesh".
|
||||
{
|
||||
bb := mk("lemurs", overlay.Name, "spiegel")
|
||||
bp := bb.placements["n1"]
|
||||
bp := bb.assignments["n1"]
|
||||
bp.Decided.Endpoint = "n1.example:51820"
|
||||
bb.placements["n1"] = bp
|
||||
bb.assignments["n1"] = bp
|
||||
sp := bb.modules["spiegel"]
|
||||
sp.Fp2 = fp("3")
|
||||
bb.modules["spiegel"] = sp
|
||||
@@ -237,14 +237,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw
|
||||
b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)}
|
||||
}
|
||||
p = b.placements["n1"]
|
||||
p = b.assignments["n1"]
|
||||
p.Decided.Endpoint = "n1.example:51820"
|
||||
for i := range p.Modules {
|
||||
if p.Modules[i].Module == "spiegel" {
|
||||
p.Modules[i].Ports = map[string]int64{"8080": 20005}
|
||||
}
|
||||
}
|
||||
b.placements["n1"] = p
|
||||
b.assignments["n1"] = p
|
||||
rec := b.records["n1"]
|
||||
rec.OutwardLinks = []string{"eth0"}
|
||||
b.records["n1"] = rec
|
||||
@@ -375,11 +375,11 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw
|
||||
b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)}
|
||||
}
|
||||
p = b.placements["n1"]
|
||||
p = b.assignments["n1"]
|
||||
p.Decided.Endpoint = "n1.example:51820"
|
||||
b.placements["n1"] = p
|
||||
b.assignments["n1"] = p
|
||||
fps = map[string]string{"lemurs": fp("e"), "proxy": fp("f"), "mailu": fp("d")}
|
||||
a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"de-spiegel"}}, fps, nil, "p-h1", 3)
|
||||
a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Assignments: given(b), InOwn: []string{"de-spiegel"}}, fps, nil, "p-h1", 3)
|
||||
refused = nil
|
||||
for _, r := range a.Refusals {
|
||||
refused = append(refused, r.Module)
|
||||
@@ -475,14 +475,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw
|
||||
b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)}
|
||||
}
|
||||
p = b.placements["n1"]
|
||||
p = b.assignments["n1"]
|
||||
p.Decided.Endpoint = "n1.example:51820"
|
||||
for i := range p.Modules {
|
||||
if p.Modules[i].Module == "site" {
|
||||
p.Modules[i].Ports = map[string]int64{"8080": 20005}
|
||||
}
|
||||
}
|
||||
b.placements["n1"] = p
|
||||
b.assignments["n1"] = p
|
||||
rec = b.records["n1"]
|
||||
rec.OutwardLinks = []string{"eth0"}
|
||||
b.records["n1"] = rec
|
||||
@@ -516,14 +516,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
b2.manifests[meshcatalogclient.ManifestDigest(raw)] = raw
|
||||
b2.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)}
|
||||
}
|
||||
p2 := b2.placements["n1"]
|
||||
p2 := b2.assignments["n1"]
|
||||
p2.Decided.Endpoint = "n1.example:51820"
|
||||
for i := range p2.Modules {
|
||||
if p2.Modules[i].Module == "site" {
|
||||
p2.Modules[i].Ports = map[string]int64{"8080": 20005}
|
||||
}
|
||||
}
|
||||
b2.placements["n1"] = p2
|
||||
b2.assignments["n1"] = p2
|
||||
b2.records["n1"] = rec
|
||||
b2.held = true
|
||||
fps["app"] = fp("9")
|
||||
@@ -561,7 +561,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
for i := range p.Modules {
|
||||
p.Modules[i].Ports = nil
|
||||
}
|
||||
b.placements["n1"] = p
|
||||
b.assignments["n1"] = p
|
||||
siteRuns.Resources[1] = json.RawMessage(`{"id":"site.server","module":"site","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["8080"]}`)
|
||||
siteRuns.FP = fp("7")
|
||||
b.manifests[meshcatalogclient.ManifestDigest(site)] = site
|
||||
@@ -591,18 +591,18 @@ func otherNodeNotResolvedIsNamed(t *testing.T) {
|
||||
prov := []byte(`{"module":"prov","version":"1","capabilities":["container-runtime"],"provides":[{"name":"thing","scope":"mesh"}]}`)
|
||||
cons := []byte(`{"module":"cons","version":"1","requires":["thing"]}`)
|
||||
// n2 runs prov, which needs a container runtime n2's record does not say it has: n2's set does not resolve here.
|
||||
b := &bus{placements: map[string]meshmoduleassigner.Placement{
|
||||
b := &bus{assignments: map[string]meshmoduleassigner.Assignments{
|
||||
"n1": {Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1", Endpoint: "n1.example:51820"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "cons"}, {Module: overlay.Name}}},
|
||||
"n2": {Node: "n2", Decided: meshmoduleassigner.NodeDecided{Address: "10.77.0.2"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "prov"}, {Module: overlay.Name}}}},
|
||||
records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t)}, "n2": {Node: "n2", TunnelKey: publicKey(t)}},
|
||||
modules: map[string]meshcatalog.ModulesByModuleValue{"prov": {Manifest: meshcatalogclient.ManifestDigest(prov)}, "cons": {Manifest: meshcatalogclient.ManifestDigest(cons)}},
|
||||
manifests: map[string][]byte{meshcatalogclient.ManifestDigest(prov): prov, meshcatalogclient.ManifestDigest(cons): cons}}
|
||||
given := map[string]*meshmoduleassigner.Placement{}
|
||||
for n, p := range b.placements {
|
||||
given := map[string]*meshmoduleassigner.Assignments{}
|
||||
for n, p := range b.assignments {
|
||||
p := p
|
||||
given[n] = &p
|
||||
}
|
||||
a := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given}, map[string]string{"cons": "src2:" + strings.Repeat("1", 64)}, nil, "p-h1", 1)
|
||||
a := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Assignments: given}, map[string]string{"cons": "src2:" + strings.Repeat("1", 64)}, nil, "p-h1", 1)
|
||||
if a.Verdict != nodeengine.CandidateAnswerVerdictRefused || !strings.Contains(a.Refused, "prov on n2 lacks container-runtime") {
|
||||
t.Errorf("a refusal for want of n2's provider = %+v", a)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package frombus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
"github.com/novox/mesh-host/internal/compose"
|
||||
)
|
||||
|
||||
// Export is one node's placement as the composition input the controller fills says it (novox/hq ADR 0555 §6, ADR 0567
|
||||
// §7): the inverse of Input for what a person decides, so the controller's one read-only verb `placement-export`
|
||||
// Export is one node's assignment as the composition input the controller fills says it (novox/hq ADR 0555 §6, ADR 0567
|
||||
// §7): the inverse of Input for what a person decides, so the controller's one read-only export verb
|
||||
// answers, from its own store by its own code, exactly what the seat's adopt proposes as the seed. Its answer must stay
|
||||
// stable for the same store, since adopt checks its digest again at the warrant.
|
||||
// What the machine observes (its record) is not in it.
|
||||
func Export(in compose.Input, node string) (meshmoduleassigner.Placement, error) {
|
||||
func Export(in compose.Input, node string) (meshmoduleassigner.Assignments, error) {
|
||||
var n *compose.Node
|
||||
for i := range in.Nodes {
|
||||
if in.Nodes[i].Name == node {
|
||||
@@ -24,11 +24,11 @@ func Export(in compose.Input, node string) (meshmoduleassigner.Placement, error)
|
||||
}
|
||||
}
|
||||
if n == nil {
|
||||
return meshmoduleassigner.Placement{}, fmt.Errorf("frombus: the input has no node %q", node)
|
||||
return meshmoduleassigner.Assignments{}, fmt.Errorf("frombus: the input has no node %q", node)
|
||||
}
|
||||
// At the version a placement key is read at, with a list of modules even when none is placed: the export is the
|
||||
// At the version a assignment key is read at, with a list of modules even when none is placed: the export is the
|
||||
// seed's value, and the reader refuses a value of version 0 or without its modules.
|
||||
p := meshmoduleassigner.Placement{V: sdkkeys.Version, Node: node, Modules: []meshmoduleassigner.PlacedModule{}, Decided: meshmoduleassigner.NodeDecided{
|
||||
p := meshmoduleassigner.Assignments{V: sdkkeys.Version, Node: node, Modules: []meshmoduleassigner.PlacedModule{}, Decided: meshmoduleassigner.NodeDecided{
|
||||
Endpoint: n.Place.Endpoint, Site: n.Place.Site, Hub: n.Place.Hub, Address: n.Place.Address,
|
||||
PublicDomain: n.PublicDomain, Account: n.Account, AccountHome: n.AccountHome, AgentAccount: n.AgentAccount,
|
||||
AgentAccountHome: n.AgentAccountHome, Adopted: n.Adopted, Taken: n.Taken}}
|
||||
@@ -66,7 +66,7 @@ func Export(in compose.Input, node string) (meshmoduleassigner.Placement, error)
|
||||
m.Bindings[provision] = meshmoduleassigner.Provider{Node: to.Node, Module: to.Module}
|
||||
}
|
||||
// A module in the settings form keeps its config values in its layers, not in mesh-config: they are exported with
|
||||
// the placement. A module in the config form has mesh-config's values laid into its layer, which are not.
|
||||
// the assignment. A module in the config form has mesh-config's values laid into its layer, which are not.
|
||||
settingsForm := !in.Catalogue[module].DeclaresConfig()
|
||||
for _, layer := range n.Settings[module] {
|
||||
if layer.From == catalogue.MeshWideLayer {
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
// through a Reader, so this package holds no bus client, no store and no timer: what it is given is all it reads.
|
||||
//
|
||||
// **What it reads, and from whom.**
|
||||
// - which modules run on each node, with their pins, holdings, host ports, bindings and settings placement: the
|
||||
// seat mesh-module-assigner's state `placement`, one key per node (ADR 0567 §1);
|
||||
// - which modules run on each node, with their pins, holdings, host ports, bindings and settings assignment: the
|
||||
// seat mesh-module-assigner's state `assignment`, one key per node (ADR 0567 §1);
|
||||
// - what a person decided of each node (its endpoint, site, hub, address on the private network, public domain and
|
||||
// accounts): the same placement key, Placement.Decided;
|
||||
// accounts): the same assignment key, Assignments.Decided;
|
||||
// - what each node observes of itself (its tunnel key, capabilities, keys, certificate, links and networks): the
|
||||
// node's own have key, `<node>.node` (HaveNode.Record); the tunnel found at a node's adoption and the names of the hub's peers are decided,
|
||||
// in its placement, and whether the hub took the tunnel over and who enrolled under a peer's key are read from both;
|
||||
// in its assignment, and whether the hub took the tunnel over and who enrolled under a peer's key are read from both;
|
||||
// - every module's manifest: the indexer's `modules.<module>` and its manifests by digest; for each module placed on
|
||||
// the composing node, its built manifest instead: the builder's done key at the trunk's FP2, or at the fingerprint
|
||||
// the node's want names where it runs another build (Scope.KeptAt), resolved by the controller's own take-in rule
|
||||
@@ -41,10 +41,10 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
"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"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
@@ -62,8 +62,8 @@ import (
|
||||
// Reader is the bus as a caller reads it with its own credential. Each method reads one part's state; an error is a
|
||||
// read that failed, never an absent key (an absent key is an empty answer).
|
||||
type Reader interface {
|
||||
// Placements is every node's placement, by node (buckets.Placement).
|
||||
Placements(ctx context.Context) (map[string]meshmoduleassigner.Placement, error)
|
||||
// Assignments is every node's assignment, by node (buckets.Assignments).
|
||||
Assignments(ctx context.Context) (map[string]meshmoduleassigner.Assignments, error)
|
||||
// Records is every node's record, by node: HaveNode.Record of each node's own have key, `<node>.node` of
|
||||
// buckets.Have (ADR 0555 §3).
|
||||
Records(ctx context.Context) (map[string]nodeengine.NodeRecord, error)
|
||||
@@ -137,11 +137,11 @@ type Mesh struct {
|
||||
// controller composes from the manifest its take-in resolved: so a moved module is handed to the provisioner, a resource
|
||||
// naming an artifact names what was built, and a templated file has its text. A placed module the builder holds no done
|
||||
// build of at the trunk's FP2 composes from the indexer's manifest, as before. Every leaves the catalogue as the indexer
|
||||
// read it: the check of every node judges placement, and reads no archive.
|
||||
// read it: the check of every node judges assignment, and reads no archive.
|
||||
//
|
||||
// Placements, when set, stands in for the placement of the nodes it names: a proposal's candidates
|
||||
// Assignments, when set, stands in for the assignment of the nodes it names: a proposal's candidates
|
||||
// (meshmoduleassigner.CandidatesByProposalNodeValue), so a node composes in shadow with a proposal laid over and compares the result with
|
||||
// what it runs (ADR 0567 §3); a nil value removes the node's placement. The rest is read as always.
|
||||
// what it runs (ADR 0567 §3); a nil value removes the node's assignment. The rest is read as always.
|
||||
//
|
||||
// InOwn is the modules the composing node runs inside its own composition (InOwn of what it runs), composed there again
|
||||
// (compose.Node.InOwn) rather than cut out as modules no build is given for (issue 1126).
|
||||
@@ -150,12 +150,12 @@ type Mesh struct {
|
||||
// (compose.Node.RunsOwn), so a module folded there that names a build of the old form is left to the node, carried
|
||||
// as it runs (issue 1126). Nil composes every folded module from its manifest.
|
||||
type Scope struct {
|
||||
Node string
|
||||
Every bool
|
||||
KeptAt map[string]string
|
||||
Placements map[string]*meshmoduleassigner.Placement
|
||||
InOwn []string
|
||||
Running []engcomp.Composed
|
||||
Node string
|
||||
Every bool
|
||||
KeptAt map[string]string
|
||||
Assignments map[string]*meshmoduleassigner.Assignments
|
||||
InOwn []string
|
||||
Running []engcomp.Composed
|
||||
// HeldToday is, in an answer, the modules the node keeps today as they run ("node" for its own composition), each with
|
||||
// how ("held", "failed", "runs a build of the old form"): a candidate that cannot compose one, or holds one only
|
||||
// behind one of them, keeps it as today, which is said under CandidateAnswer.Held and is no refusal (issue 1126).
|
||||
@@ -174,7 +174,7 @@ type Scope struct {
|
||||
// controller's composed writer folded there because no fingerprint of the new form names them (mesh-wireguard, a module
|
||||
// run at a build of the old form; issue 1126). A node composing itself passes it as Scope.InOwn, so its own
|
||||
// composition keeps them as it runs them until a want names a build of the new form for one.
|
||||
func InOwn(running []engcomp.Composed, p meshmoduleassigner.Placement) []string {
|
||||
func InOwn(running []engcomp.Composed, p meshmoduleassigner.Assignments) []string {
|
||||
placed := map[string]bool{}
|
||||
for _, m := range p.Modules {
|
||||
placed[m.Module] = true
|
||||
@@ -258,10 +258,10 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
|
||||
// base is the part of a node's input that the fingerprints it composes its modules at leave alone (issue 1954): read once
|
||||
// for its plans at the wanted builds and at the newest, whose builds alone differ (PlansHeldTwice).
|
||||
type base struct {
|
||||
in compose.Input
|
||||
placements map[string]meshmoduleassigner.Placement
|
||||
records map[string]nodeengine.NodeRecord
|
||||
modules map[string]meshcatalog.ModulesByModuleValue
|
||||
in compose.Input
|
||||
assignments map[string]meshmoduleassigner.Assignments
|
||||
records map[string]nodeengine.NodeRecord
|
||||
modules map[string]meshcatalog.ModulesByModuleValue
|
||||
// ownRefused is each module placed on the composing node whose manifest the indexer refused, with why, in the order
|
||||
// read: every scope holds it.
|
||||
ownRefused [][2]string
|
||||
@@ -291,29 +291,29 @@ func (b base) cloned() compose.Input {
|
||||
return in
|
||||
}
|
||||
|
||||
// readBase is input up to the builds: placement, records, the indexer's modules and manifests, each node's values.
|
||||
// readBase is input up to the builds: assignment, records, the indexer's modules and manifests, each node's values.
|
||||
func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
|
||||
var b base
|
||||
placements, err := r.Placements(ctx)
|
||||
assignments, err := r.Assignments(ctx)
|
||||
if err != nil {
|
||||
return b, fmt.Errorf("frombus: placement: %w", err)
|
||||
return b, fmt.Errorf("frombus: assignment: %w", err)
|
||||
}
|
||||
if len(s.Placements) > 0 {
|
||||
given := make(map[string]meshmoduleassigner.Placement, len(placements)+len(s.Placements))
|
||||
for node, p := range placements {
|
||||
if len(s.Assignments) > 0 {
|
||||
given := make(map[string]meshmoduleassigner.Assignments, len(assignments)+len(s.Assignments))
|
||||
for node, p := range assignments {
|
||||
given[node] = p
|
||||
}
|
||||
for node, p := range s.Placements {
|
||||
for node, p := range s.Assignments {
|
||||
if p == nil {
|
||||
delete(given, node)
|
||||
continue
|
||||
}
|
||||
if p.Node != node {
|
||||
return b, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node)
|
||||
return b, fmt.Errorf("frombus: the assignment given for %s names the node %q", node, p.Node)
|
||||
}
|
||||
given[node] = *p
|
||||
}
|
||||
placements = given
|
||||
assignments = given
|
||||
}
|
||||
records, err := r.Records(ctx)
|
||||
if err != nil {
|
||||
@@ -323,8 +323,8 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
|
||||
if err != nil {
|
||||
return b, fmt.Errorf("frombus: the indexer's modules: %w", err)
|
||||
}
|
||||
if _, ok := placements[s.Node]; !ok && !s.Every {
|
||||
return b, fmt.Errorf("frombus: %s has no placement in the catalogue", s.Node)
|
||||
if _, ok := assignments[s.Node]; !ok && !s.Every {
|
||||
return b, fmt.Errorf("frombus: %s has no assignment in the catalogue", s.Node)
|
||||
}
|
||||
|
||||
in := compose.Input{OverlayCIDR: mesh.OverlayCIDR, InternalSuffix: mesh.InternalSuffix, Authority: mesh.Authority,
|
||||
@@ -357,7 +357,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
|
||||
(*field)[module] = why
|
||||
}
|
||||
ownPlaced := map[string]bool{}
|
||||
for _, pm := range placements[s.Node].Modules {
|
||||
for _, pm := range assignments[s.Node].Modules {
|
||||
ownPlaced[pm.Module] = true
|
||||
}
|
||||
for _, name := range sortedKeys(modules) {
|
||||
@@ -401,9 +401,9 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for _, node := range nodesOf(placements, records) {
|
||||
for _, node := range nodesOf(assignments, records) {
|
||||
rec, hasRecord := records[node]
|
||||
p := placements[node]
|
||||
p := assignments[node]
|
||||
if !hasRecord {
|
||||
if len(p.Modules) > 0 {
|
||||
missing = append(missing, node)
|
||||
@@ -492,13 +492,13 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
|
||||
}
|
||||
}
|
||||
if in.Adopted != nil && in.Adopted.Adopted {
|
||||
in.CarriedPeers = carriedPeers(in.Adopted.Tunnel, placements[in.Adopted.Hub].Decided.PeerNames, records)
|
||||
in.CarriedPeers = carriedPeers(in.Adopted.Tunnel, assignments[in.Adopted.Hub].Decided.PeerNames, records)
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return b, fmt.Errorf("frombus: modules are placed on %s, whose agent has written no record yet",
|
||||
strings.Join(missing, ", "))
|
||||
}
|
||||
b.in, b.placements, b.records, b.modules = in, placements, records, modules
|
||||
b.in, b.assignments, b.records, b.modules = in, assignments, records, modules
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -523,7 +523,7 @@ func (b base) finish(ctx context.Context, r Reader, build func(ctx context.Conte
|
||||
if _, ok := b.records[s.Node]; !ok {
|
||||
return compose.Input{}, fmt.Errorf("frombus: %s has written no record of itself yet", s.Node)
|
||||
}
|
||||
if err := fromBuilds(ctx, r, build, &in, s, b.placements[s.Node], b.modules, read, parse, refuse, wanted); err != nil {
|
||||
if err := fromBuilds(ctx, r, build, &in, s, b.assignments[s.Node], b.modules, read, parse, refuse, wanted); err != nil {
|
||||
return compose.Input{}, err
|
||||
}
|
||||
// Its own modules' secrets, sealed to its own key by the vault: a composition carries them sealed (ADR 0550 §5).
|
||||
@@ -799,7 +799,7 @@ func ranOf(ctx context.Context, r Reader, mesh Mesh, running []engcomp.Composed,
|
||||
// by the one take-in rule (takein.Manifest), a templates archive read in the artifact store as the node reaches it
|
||||
// (issue 1126).
|
||||
func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*builds.BuilderBuild, error),
|
||||
in *compose.Input, s Scope, p meshmoduleassigner.Placement, modules map[string]meshcatalog.ModulesByModuleValue,
|
||||
in *compose.Input, s Scope, p meshmoduleassigner.Assignments, modules map[string]meshcatalog.ModulesByModuleValue,
|
||||
read func(module, digest string) (catalogue.Manifest, error), parse func([]byte) (catalogue.Manifest, error),
|
||||
refuse func(module string, err error), wanted map[string]string) error {
|
||||
// stub keeps a refused module in the composition as a stub for this node alone (compose.Stub): what it provides
|
||||
@@ -1004,9 +1004,9 @@ func attachedOf(in []nodeengine.AttachedNetwork) []catalogue.AttachedNetwork {
|
||||
return out
|
||||
}
|
||||
|
||||
// nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person
|
||||
// nodeOf is the composition's node from its record (what the machine observes) and its assignment (what a person
|
||||
// decided of it, and where its modules run).
|
||||
func nodeOf(rec nodeengine.NodeRecord, p meshmoduleassigner.Placement) compose.Node {
|
||||
func nodeOf(rec nodeengine.NodeRecord, p meshmoduleassigner.Assignments) compose.Node {
|
||||
n := compose.Node{
|
||||
Name: rec.Node,
|
||||
Place: compose.Place{Key: rec.TunnelKey},
|
||||
@@ -1024,9 +1024,9 @@ func nodeOf(rec nodeengine.NodeRecord, p meshmoduleassigner.Placement) compose.N
|
||||
return n
|
||||
}
|
||||
|
||||
// placeNode sets what a person decided of n and where its modules run, from its placement; what the machine observes
|
||||
// placeNode sets what a person decided of n and where its modules run, from its assignment; what the machine observes
|
||||
// (its keys, links, networks, certificate) and its settings layers are left as they are.
|
||||
func placeNode(n *compose.Node, p meshmoduleassigner.Placement) {
|
||||
func placeNode(n *compose.Node, p meshmoduleassigner.Assignments) {
|
||||
d := p.Decided
|
||||
n.Place = compose.Place{Key: n.Place.Key, Endpoint: d.Endpoint, Site: d.Site, Hub: d.Hub, Address: d.Address}
|
||||
n.Assigned, n.PublicDomain = meshmoduleassignerclient.Assigned(p), d.PublicDomain
|
||||
@@ -1035,15 +1035,15 @@ func placeNode(n *compose.Node, p meshmoduleassigner.Placement) {
|
||||
n.Pins, n.Bound, n.Ports = meshmoduleassignerclient.ChosenPins(p), meshmoduleassignerclient.Bound(p), meshmoduleassignerclient.HostPorts(p)
|
||||
}
|
||||
|
||||
// Placed is in with the placements given standing in for those of the nodes they name (issue 1126): each such node's
|
||||
// decided parts, modules, pins, bindings, host ports, settings placement and holdings are the given placement's, its
|
||||
// Placed is in with the assignments given standing in for those of the nodes they name (issue 1126): each such node's
|
||||
// decided parts, modules, pins, bindings, host ports, settings assignment and holdings are the given assignment's, its
|
||||
// config values those in already carries for it (the layer of the node's own name), and what the machine observes is
|
||||
// kept. A nil placement leaves the node in the mesh with nothing placed on it. A node in does not hold is refused: it has
|
||||
// kept. A nil assignment leaves the node in the mesh with nothing placed on it. A node in does not hold is refused: it has
|
||||
// no record to compose it from. The found tunnel and the hub's peers are read from in as they are, not from the given
|
||||
// placement. It is how a check composes a change's own placement over an input already filled (composer.Case's
|
||||
// Placements).
|
||||
func Placed(in compose.Input, placements map[string]*meshmoduleassigner.Placement) (compose.Input, error) {
|
||||
if len(placements) == 0 {
|
||||
// assignment. It is how a check composes a change's own assignment over an input already filled (composer.Case's
|
||||
// Assignments).
|
||||
func Placed(in compose.Input, assignments map[string]*meshmoduleassigner.Assignments) (compose.Input, error) {
|
||||
if len(assignments) == 0 {
|
||||
return in, nil
|
||||
}
|
||||
out := in
|
||||
@@ -1052,17 +1052,17 @@ func Placed(in compose.Input, placements map[string]*meshmoduleassigner.Placemen
|
||||
for i, n := range out.Nodes {
|
||||
index[n.Name] = i
|
||||
}
|
||||
for _, node := range sortedKeys(placements) {
|
||||
p := placements[node]
|
||||
for _, node := range sortedKeys(assignments) {
|
||||
p := assignments[node]
|
||||
i, ok := index[node]
|
||||
if !ok {
|
||||
return compose.Input{}, fmt.Errorf("frombus: a placement is given for %s, which the input holds no node for", node)
|
||||
return compose.Input{}, fmt.Errorf("frombus: a assignment is given for %s, which the input holds no node for", node)
|
||||
}
|
||||
if p == nil {
|
||||
p = &meshmoduleassigner.Placement{Node: node}
|
||||
p = &meshmoduleassigner.Assignments{Node: node}
|
||||
}
|
||||
if p.Node != node {
|
||||
return compose.Input{}, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node)
|
||||
return compose.Input{}, fmt.Errorf("frombus: the assignment given for %s names the node %q", node, p.Node)
|
||||
}
|
||||
n := out.Nodes[i]
|
||||
placeNode(&n, *p)
|
||||
@@ -1086,12 +1086,12 @@ func Placed(in compose.Input, placements map[string]*meshmoduleassigner.Placemen
|
||||
}
|
||||
out.Holdings = nil
|
||||
for _, h := range in.Holdings {
|
||||
if _, given := placements[h.Node]; !given {
|
||||
if _, given := assignments[h.Node]; !given {
|
||||
out.Holdings = append(out.Holdings, h)
|
||||
}
|
||||
}
|
||||
for _, node := range sortedKeys(placements) {
|
||||
if p := placements[node]; p != nil {
|
||||
for _, node := range sortedKeys(assignments) {
|
||||
if p := assignments[node]; p != nil {
|
||||
out.Holdings = append(out.Holdings, meshmoduleassignerclient.Held(*p)...)
|
||||
}
|
||||
}
|
||||
@@ -1132,10 +1132,10 @@ func carriedPeers(t compose.Tunnel, names map[string]string, records map[string]
|
||||
return out
|
||||
}
|
||||
|
||||
// nodesOf is every node named by a placement or a record, by name: the order the controller reads them in.
|
||||
func nodesOf(placements map[string]meshmoduleassigner.Placement, records map[string]nodeengine.NodeRecord) []string {
|
||||
// nodesOf is every node named by a assignment or a record, by name: the order the controller reads them in.
|
||||
func nodesOf(assignments map[string]meshmoduleassigner.Assignments, records map[string]nodeengine.NodeRecord) []string {
|
||||
seen := map[string]bool{}
|
||||
for n := range placements {
|
||||
for n := range assignments {
|
||||
seen[n] = true
|
||||
}
|
||||
for n := range records {
|
||||
|
||||
@@ -11,10 +11,10 @@ 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"
|
||||
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-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
type bus struct {
|
||||
vaultAuthority string
|
||||
handed map[string]bool
|
||||
placements map[string]meshmoduleassigner.Placement
|
||||
assignments map[string]meshmoduleassigner.Assignments
|
||||
records map[string]nodeengine.NodeRecord
|
||||
modules map[string]meshcatalog.ModulesByModuleValue
|
||||
manifests map[string][]byte
|
||||
@@ -44,7 +44,7 @@ type bus struct {
|
||||
own []compose.OwnSecretRow
|
||||
ownErr error
|
||||
// asked is every consumer credential asked; the fake refuses the whole ask, as the vault's own does, for a consuming
|
||||
// module its placement does not put on the node.
|
||||
// module its assignment does not put on the node.
|
||||
asked []compose.CredentialKey
|
||||
// builds are the builder's keys, by "<module> <fp>"; archives the artifact store's blobs, by digest, and fetched
|
||||
// every URL asked; served another node's values by "<node>.<module>", and servedRead every ask with its keys.
|
||||
@@ -83,8 +83,8 @@ func (b *bus) Served(_ context.Context, node, module string, keys []string) (map
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (b *bus) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
|
||||
return b.placements, nil
|
||||
func (b *bus) Assignments(context.Context) (map[string]meshmoduleassigner.Assignments, error) {
|
||||
return b.assignments, nil
|
||||
}
|
||||
func (b *bus) VaultAuthority(context.Context) string { return b.vaultAuthority }
|
||||
func (b *bus) Handed(context.Context, string) map[string]bool { return b.handed }
|
||||
@@ -109,7 +109,7 @@ func (b *bus) Credentials(_ context.Context, node string, asConsumer, asProvider
|
||||
b.asked = append(b.asked, asConsumer...)
|
||||
for _, k := range asConsumer {
|
||||
placed := false
|
||||
for _, m := range b.placements[node].Modules {
|
||||
for _, m := range b.assignments[node].Modules {
|
||||
placed = placed || m.Module == k.ConsumerModule
|
||||
}
|
||||
if !placed {
|
||||
@@ -143,7 +143,7 @@ func publicKey(t *testing.T) string {
|
||||
return base64.StdEncoding.EncodeToString(k.PublicKey().Bytes())
|
||||
}
|
||||
|
||||
// Issue 1126 (S3, S4, S5): the bus's placement, node records, manifests and config values fill the same compose.Input the
|
||||
// Issue 1126 (S3, S4, S5): the bus's assignment, node records, manifests and config values fill the same compose.Input the
|
||||
// controller's store fills for compose's own two-machine mesh (compose's aMesh: anchor the hub, laptop), and that Input
|
||||
// composes; a node reads only its own config values; what cannot be composed is refused, naming it.
|
||||
func TestInput(t *testing.T) {
|
||||
@@ -167,7 +167,7 @@ func TestInput(t *testing.T) {
|
||||
}
|
||||
digest := meshcatalogclient.ManifestDigest(xterm)
|
||||
// A module in the settings form (plex and the media modules on one node, 2026-10-26): its config values are in its
|
||||
// layers, not mesh-config, so the placement carries them (MeshValues, Values) and mesh-config's are not read for it.
|
||||
// layers, not mesh-config, so the assignment carries them (MeshValues, Values) and mesh-config's are not read for it.
|
||||
media := []byte(`{"module":"media","version":"1","settings":{"lines":{"kind":"preference","default":0,"why":"kept"},` +
|
||||
`"name":{"kind":"preference","default":"x","why":"shown"}},"own-secrets":{"token":"/srv/media/token"},"resources":[{"id":"conf","type":"file",` +
|
||||
`"path":"/srv/media/conf","mode":"0644","content":"uid=${setting:lines} name=${setting:name}\n"}]}`)
|
||||
@@ -178,7 +178,7 @@ func TestInput(t *testing.T) {
|
||||
Peers: []meshmoduleassigner.FoundTunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}, {PublicKey: keys["laptop"], Address: "10.77.0.2/32"}}}
|
||||
mk := func() *bus {
|
||||
return &bus{
|
||||
placements: map[string]meshmoduleassigner.Placement{
|
||||
assignments: map[string]meshmoduleassigner.Assignments{
|
||||
"anchor": {Node: "anchor", Decided: meshmoduleassigner.NodeDecided{Endpoint: "anchor.example:51820", Site: "here", Hub: true, Address: "10.77.0.1",
|
||||
Tunnel: tunnel, PeerNames: map[string]string{"peer": "phone"}},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: overlay.Name}},
|
||||
@@ -289,15 +289,15 @@ func TestInput(t *testing.T) {
|
||||
t.Fatalf("compositions differ or are empty: %d from the bus, %d from the store", len(fromBus), len(fromStore))
|
||||
}
|
||||
|
||||
// Issue 1126: a candidate placement equal to the node's composes what it runs, and CompareComposed says so; one
|
||||
// Issue 1126: a candidate assignment equal to the node's composes what it runs, and CompareComposed says so; one
|
||||
// thing changed in the group's candidate of another node (the hub's endpoint) names exactly the module it moves
|
||||
// here. Placed puts the same placement over the store's input unchanged.
|
||||
// here. Placed puts the same assignment over the store's input unchanged.
|
||||
running := make([]compositions.Composed, 0, len(fromBus))
|
||||
for _, c := range fromBus {
|
||||
running = append(running, c)
|
||||
}
|
||||
laptop := mk().placements["laptop"]
|
||||
asIs, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*meshmoduleassigner.Placement{"laptop": &laptop}}, fps)
|
||||
laptop := mk().assignments["laptop"]
|
||||
asIs, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Assignments: map[string]*meshmoduleassigner.Assignments{"laptop": &laptop}}, fps)
|
||||
if err == nil && len(held) > 0 {
|
||||
err = fmt.Errorf("modules held: %v", held)
|
||||
}
|
||||
@@ -305,11 +305,11 @@ func TestInput(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c := compositions.CompareComposed(asIs, running); len(c.Differs)+len(c.Added)+len(c.Removed) != 0 || len(c.Same) != len(running) {
|
||||
t.Errorf("the candidate equal to the placement: %+v", c)
|
||||
t.Errorf("the candidate equal to the assignment: %+v", c)
|
||||
}
|
||||
moved := mk().placements["anchor"]
|
||||
moved := mk().assignments["anchor"]
|
||||
moved.Decided.Endpoint = "anchor.example:51821"
|
||||
changed, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*meshmoduleassigner.Placement{"laptop": &laptop, "anchor": &moved}}, fps)
|
||||
changed, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Assignments: map[string]*meshmoduleassigner.Assignments{"laptop": &laptop, "anchor": &moved}}, fps)
|
||||
if err == nil && len(held) > 0 {
|
||||
err = fmt.Errorf("modules held: %v", held)
|
||||
}
|
||||
@@ -335,11 +335,11 @@ func TestInput(t *testing.T) {
|
||||
files, _ := json.Marshal(catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"},
|
||||
Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/files/config.json", "mode": "0600", "content": "{}", "merge": "json"}}})
|
||||
one := func(placed ...string) *bus {
|
||||
p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
||||
p := meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
||||
for _, m := range placed {
|
||||
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
|
||||
}
|
||||
return &bus{placements: map[string]meshmoduleassigner.Placement{"n1": p},
|
||||
return &bus{assignments: map[string]meshmoduleassigner.Assignments{"n1": p},
|
||||
records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: keys["anchor"], SealingKey: "sk", Capabilities: []string{"systemd"}}},
|
||||
modules: map[string]meshcatalog.ModulesByModuleValue{"keeper": {Manifest: meshcatalogclient.ManifestDigest(keeper)}, "lemurs": {Manifest: meshcatalogclient.ManifestDigest(lemurs)},
|
||||
"objects": {Manifest: meshcatalogclient.ManifestDigest(objects)}, "files": {Manifest: meshcatalogclient.ManifestDigest(files)}},
|
||||
@@ -385,14 +385,14 @@ func TestInput(t *testing.T) {
|
||||
// node's own, each in its place, its module taken off; the rest split by module.
|
||||
tunnelled := func() *bus {
|
||||
b := one("keeper", "lemurs", overlay.Name)
|
||||
p := b.placements["n1"]
|
||||
p := b.assignments["n1"]
|
||||
p.Decided.Endpoint = "n1.example:51820"
|
||||
b.placements["n1"] = p
|
||||
b.assignments["n1"] = p
|
||||
return b
|
||||
}
|
||||
tunnelledAs := func() map[string]*meshmoduleassigner.Placement {
|
||||
p := tunnelled().placements["n1"]
|
||||
return map[string]*meshmoduleassigner.Placement{"n1": &p}
|
||||
tunnelledAs := func() map[string]*meshmoduleassigner.Assignments {
|
||||
p := tunnelled().assignments["n1"]
|
||||
return map[string]*meshmoduleassigner.Assignments{"n1": &p}
|
||||
}
|
||||
folded := func() []compositions.Composed {
|
||||
b := tunnelled()
|
||||
@@ -437,16 +437,16 @@ func TestInput(t *testing.T) {
|
||||
}
|
||||
return out
|
||||
}()
|
||||
if got := InOwn(folded, tunnelled().placements["n1"]); !reflect.DeepEqual(got, []string{"lemurs", overlay.Name}) {
|
||||
if got := InOwn(folded, tunnelled().assignments["n1"]); !reflect.DeepEqual(got, []string{"lemurs", overlay.Name}) {
|
||||
t.Errorf("InOwn of the controller's composition = %v, want lemurs and %s", got, overlay.Name)
|
||||
}
|
||||
placedAs := func(modules ...string) map[string]*meshmoduleassigner.Placement {
|
||||
p := one(modules...).placements["n1"]
|
||||
return map[string]*meshmoduleassigner.Placement{"n1": &p}
|
||||
placedAs := func(modules ...string) map[string]*meshmoduleassigner.Assignments {
|
||||
p := one(modules...).assignments["n1"]
|
||||
return map[string]*meshmoduleassigner.Assignments{"n1": &p}
|
||||
}
|
||||
for name, c := range map[string]struct {
|
||||
b *bus
|
||||
given map[string]*meshmoduleassigner.Placement
|
||||
given map[string]*meshmoduleassigner.Assignments
|
||||
running []compositions.Composed
|
||||
verdict string
|
||||
check func(nodeengine.CandidateAnswer) bool
|
||||
@@ -483,7 +483,7 @@ func TestInput(t *testing.T) {
|
||||
runsNow, nodeengine.CandidateAnswerVerdictRefused, func(a nodeengine.CandidateAnswer) bool {
|
||||
return len(a.Refusals) == 1 && a.Refusals[0].Module == "keeper" && len(a.Added) == 0
|
||||
}},
|
||||
"cannot compose": {one("keeper"), map[string]*meshmoduleassigner.Placement{"n1": {Node: "n2"}}, runsNow, nodeengine.CandidateAnswerVerdictRefused,
|
||||
"cannot compose": {one("keeper"), map[string]*meshmoduleassigner.Assignments{"n1": {Node: "n2"}}, runsNow, nodeengine.CandidateAnswerVerdictRefused,
|
||||
func(a nodeengine.CandidateAnswer) bool { return a.Refused != "" }},
|
||||
// Issue 1126 (the seed's review): the private network's module and one at a build of the old form, which no want
|
||||
// names and the controller folds into the node's own, are folded there again: the node's own is the same, and
|
||||
@@ -495,7 +495,7 @@ func TestInput(t *testing.T) {
|
||||
if name == "mesh-wireguard and an old-form module, folded" {
|
||||
fps = map[string]string{"keeper": oneFps["keeper"]} // the node's wants: no build of lemurs or the private network
|
||||
}
|
||||
a := Answer(ctx, c.b, Mesh{}, Scope{Node: "n1", Placements: c.given, InOwn: InOwn(c.running, *c.given["n1"])}, fps, c.running, "p-h1", 7)
|
||||
a := Answer(ctx, c.b, Mesh{}, Scope{Node: "n1", Assignments: c.given, InOwn: InOwn(c.running, *c.given["n1"])}, fps, c.running, "p-h1", 7)
|
||||
if a.Verdict != c.verdict || !c.check(a) {
|
||||
t.Errorf("%s: %+v", name, a)
|
||||
}
|
||||
@@ -503,28 +503,28 @@ func TestInput(t *testing.T) {
|
||||
t.Errorf("%s: the answer is not one a have holds: %v (%s)", name, err, raw)
|
||||
}
|
||||
}
|
||||
if got, err := Placed(want, map[string]*meshmoduleassigner.Placement{"laptop": &laptop}); err != nil || !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Placed with the node's own placement changes the input: %v", err)
|
||||
if got, err := Placed(want, map[string]*meshmoduleassigner.Assignments{"laptop": &laptop}); err != nil || !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Placed with the node's own assignment changes the input: %v", err)
|
||||
}
|
||||
if got, err := Placed(want, map[string]*meshmoduleassigner.Placement{"laptop": nil}); err != nil || len(got.Nodes[1].Assigned) != 0 || got.Nodes[1].Place.Key != keys["laptop"] {
|
||||
t.Errorf("Placed with no placement: %+v, %v", got.Nodes[1], err)
|
||||
if got, err := Placed(want, map[string]*meshmoduleassigner.Assignments{"laptop": nil}); err != nil || len(got.Nodes[1].Assigned) != 0 || got.Nodes[1].Place.Key != keys["laptop"] {
|
||||
t.Errorf("Placed with no assignment: %+v, %v", got.Nodes[1], err)
|
||||
}
|
||||
if _, err := Placed(want, map[string]*meshmoduleassigner.Placement{"stranger": {Node: "stranger"}}); err == nil {
|
||||
if _, err := Placed(want, map[string]*meshmoduleassigner.Assignments{"stranger": {Node: "stranger"}}); err == nil {
|
||||
t.Error("Placed a node the input holds no node for")
|
||||
}
|
||||
if _, err := Input(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*meshmoduleassigner.Placement{"laptop": {Node: "anchor"}}}); err == nil {
|
||||
t.Error("a given placement naming another node was taken")
|
||||
if _, err := Input(ctx, mk(), Mesh{}, Scope{Node: "laptop", Assignments: map[string]*meshmoduleassigner.Assignments{"laptop": {Node: "anchor"}}}); err == nil {
|
||||
t.Error("a given assignment naming another node was taken")
|
||||
}
|
||||
|
||||
// ADR 0555 §6: Export is Input's inverse for what a person decides: the store's input exported, node by node, is the
|
||||
// placement the bus holds, and read back it is the store's input again.
|
||||
for node, placed := range mk().placements {
|
||||
// assignment the bus holds, and read back it is the store's input again.
|
||||
for node, placed := range mk().assignments {
|
||||
got, err := Export(want, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gj, _ := meshmoduleassignerclient.EncodePlacement(got); !meshmoduleassignerclient.SamePlacement(got, placed) {
|
||||
pj, _ := meshmoduleassignerclient.EncodePlacement(placed)
|
||||
if gj, _ := meshmoduleassignerclient.EncodeAssignments(got); !meshmoduleassignerclient.SameAssignments(got, placed) {
|
||||
pj, _ := meshmoduleassignerclient.EncodeAssignments(placed)
|
||||
t.Errorf("%s exported as %s, want %s", node, gj, pj)
|
||||
}
|
||||
}
|
||||
@@ -622,10 +622,10 @@ func TestInput(t *testing.T) {
|
||||
clientArtifacts := []builds.BuiltArtifact{{Name: "client", Kind: "bundle", Reference: "artifact-store://mesh-provisioner-client/client/blobs/" + digestOf("7"), Digest: digestOf("7")}}
|
||||
movedMesh := func(built bool) *bus {
|
||||
b := one()
|
||||
p := b.placements["n1"]
|
||||
p := b.assignments["n1"]
|
||||
p.Modules = []meshmoduleassigner.PlacedModule{{Module: "store"}, {Module: "mesh-provisioner-client"}, {Module: "restic"}}
|
||||
p.Decided.Account, p.Decided.AccountHome = "operator", "/home/operator"
|
||||
b.placements["n1"] = p
|
||||
b.assignments["n1"] = p
|
||||
rec := b.records["n1"]
|
||||
rec.Contract, rec.Capabilities = compose.HandedContract, []string{"systemd", "package-manager"}
|
||||
b.records["n1"] = rec
|
||||
@@ -771,9 +771,9 @@ func TestInput(t *testing.T) {
|
||||
`"provides":[{"name":"smtp","scope":"mesh"}],"serves":{"smtp":{"port":587,"domain":"${config:domain}"}}}`)
|
||||
withMailer := func(set bool) *bus {
|
||||
b := mk()
|
||||
p := b.placements["anchor"]
|
||||
p := b.assignments["anchor"]
|
||||
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: "mailer"})
|
||||
b.placements["anchor"] = p
|
||||
b.assignments["anchor"] = p
|
||||
b.modules["mailer"] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(mailer)}
|
||||
b.manifests[meshcatalogclient.ManifestDigest(mailer)] = mailer
|
||||
if set {
|
||||
@@ -798,7 +798,7 @@ func TestInput(t *testing.T) {
|
||||
says string
|
||||
}{
|
||||
"no record": {func(b *bus) { delete(b.records, "laptop") }, Scope{Node: "anchor"}, "laptop, whose agent has written no record"},
|
||||
"no placement": {func(b *bus) {}, Scope{Node: "stranger"}, "stranger has no placement"},
|
||||
"no assignment": {func(b *bus) {}, Scope{Node: "stranger"}, "stranger has no assignment"},
|
||||
"a manifest unread": {func(b *bus) { delete(b.manifests, digest) }, Scope{Node: "laptop"}, "the manifest of xterm"},
|
||||
"a kept one unbuilt": {func(b *bus) {}, Scope{Node: "laptop", KeptAt: map[string]string{"xterm": "src2:gone"}}, "holds no done build"},
|
||||
} {
|
||||
@@ -840,11 +840,11 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates {
|
||||
}
|
||||
out[state][key] = raw
|
||||
}
|
||||
placements, _ := b.Placements(ctx)
|
||||
for _, p := range placements {
|
||||
values, err := meshmoduleassignerclient.PlacementValues(p)
|
||||
assignments, _ := b.Assignments(ctx)
|
||||
for _, p := range assignments {
|
||||
values, err := meshmoduleassignerclient.AssignmentsValues(p)
|
||||
for k, raw := range values {
|
||||
put(PlacementRead, k, raw, err)
|
||||
put(AssignmentsRead, k, raw, err)
|
||||
}
|
||||
}
|
||||
records, _ := b.Records(ctx)
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"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"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
@@ -27,7 +27,7 @@ var testdata embed.FS
|
||||
|
||||
// Bus is a frombus.Reader over values held in memory. A credential asked is answered held when Held is set.
|
||||
type Bus struct {
|
||||
Placement map[string]meshmoduleassigner.Placement
|
||||
ByNode map[string]meshmoduleassigner.Assignments
|
||||
Record map[string]nodeengine.NodeRecord
|
||||
Module map[string]meshcatalog.ModulesByModuleValue
|
||||
Manifests map[string][]byte
|
||||
@@ -41,8 +41,8 @@ type Bus struct {
|
||||
|
||||
var _ frombus.Reader = (*Bus)(nil)
|
||||
|
||||
func (b *Bus) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
|
||||
return b.Placement, nil
|
||||
func (b *Bus) Assignments(context.Context) (map[string]meshmoduleassigner.Assignments, error) {
|
||||
return b.ByNode, nil
|
||||
}
|
||||
func (b *Bus) Records(context.Context) (map[string]nodeengine.NodeRecord, error) {
|
||||
return b.Record, nil
|
||||
@@ -156,7 +156,7 @@ func Provisioned() (*Bus, error) {
|
||||
digestOf := func(c string) string { return "sha256:" + strings.Repeat(c, 64) }
|
||||
fps := FPs()
|
||||
b := &Bus{
|
||||
Placement: map[string]meshmoduleassigner.Placement{Node: {Node: Node,
|
||||
ByNode: map[string]meshmoduleassigner.Assignments{Node: {Node: Node,
|
||||
Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1", Account: "operator", AccountHome: "/home/operator"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "store"}, {Module: "postgres"}, {Module: "mesh-provisioner-client"}, {Module: "restic"}}}},
|
||||
Record: map[string]nodeengine.NodeRecord{Node: {Node: Node, TunnelKey: "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=",
|
||||
|
||||
@@ -3,10 +3,10 @@ package frombustest
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"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/modules/mesh-delivery-builder/builds"
|
||||
@@ -70,9 +70,9 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
}
|
||||
if row.bad != "" {
|
||||
if !row.elsewhere {
|
||||
p := b.Placement[Node]
|
||||
p := b.ByNode[Node]
|
||||
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: "backup"})
|
||||
b.Placement[Node] = p
|
||||
b.ByNode[Node] = p
|
||||
}
|
||||
digest := meshcatalogclient.ManifestDigest([]byte(row.bad))
|
||||
b.Manifests[digest] = []byte(row.bad)
|
||||
@@ -166,19 +166,19 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
byKey := map[string]int{}
|
||||
ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error {
|
||||
byKey[state+" watched"]++
|
||||
return handle(stdio.StateChange{Key: "placement." + Node, Op: stdio.OpPut, Value: []byte(`{}`), Current: true})
|
||||
return handle(stdio.StateChange{Key: Node + ".node", Op: stdio.OpPut, Value: []byte(`{}`), Current: true})
|
||||
}, func(state, key string) ([]byte, bool, error) { byKey[state]++; return []byte(key), true, nil })
|
||||
w := fromruntime.Watched{WatchedStates: ws}
|
||||
for range 2 {
|
||||
_, _, _ = w.Get(frombus.BuildsRead, "restic."+fp)
|
||||
_, _, _ = w.Get(frombus.ManifestsRead, "sha256-d")
|
||||
if _, found, _ := w.Get(frombus.PlacementRead, "placement."+Node); !found {
|
||||
t.Error("the watched reader does not answer the placement from its copy")
|
||||
if _, found, _ := w.Get(frombus.AssignmentsRead, Node+".node"); !found {
|
||||
t.Error("the watched reader does not answer the assignment from its copy")
|
||||
}
|
||||
}
|
||||
if byKey[frombus.BuildsRead] != 2 || byKey[frombus.ManifestsRead] != 1 || byKey[frombus.PlacementRead] != 0 ||
|
||||
byKey[frombus.PlacementRead+" watched"] != 1 {
|
||||
t.Errorf("the watched reader read %v, want a build by key each time, a manifest once, the placement watched once", byKey)
|
||||
if byKey[frombus.BuildsRead] != 2 || byKey[frombus.ManifestsRead] != 1 || byKey[frombus.AssignmentsRead] != 0 ||
|
||||
byKey[frombus.AssignmentsRead+" watched"] != 1 {
|
||||
t.Errorf("the watched reader read %v, want a build by key each time, a manifest once, the assignment watched once", byKey)
|
||||
}
|
||||
fetched := 0
|
||||
keep := fromruntime.KeptFetch(func(context.Context, string) ([]byte, error) { fetched++; return []byte("tar"), nil })
|
||||
@@ -279,10 +279,10 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
|
||||
// held: its build not done while it runs
|
||||
bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &builds.BuilderBuild{State: builds.BuildBuilding}
|
||||
} else {
|
||||
// taken out of the node's placement while what runs still names it: the account goes with it, never kept stale
|
||||
p := bb.Placement[Node]
|
||||
// taken out of the node's assignment while what runs still names it: the account goes with it, never kept stale
|
||||
p := bb.ByNode[Node]
|
||||
p.Modules = slices.DeleteFunc(slices.Clone(p.Modules), func(m meshmoduleassigner.PlacedModule) bool { return m.Module == "mesh-provisioner-client" })
|
||||
bb.Placement[Node] = p
|
||||
bb.ByNode[Node] = p
|
||||
want = ""
|
||||
}
|
||||
after, _, held, err := frombus.Compose(context.Background(), bb, frombus.Mesh{}, frombus.Scope{Node: Node, Running: runs}, fps)
|
||||
@@ -314,9 +314,14 @@ func statesOf(t *testing.T, b *Bus) frombus.States {
|
||||
}
|
||||
s[state][key] = raw
|
||||
}
|
||||
for node, p := range b.Placement {
|
||||
raw, err := meshmoduleassignerclient.EncodePlacement(p)
|
||||
put(frombus.PlacementRead, "placement."+node, raw, err)
|
||||
for _, p := range b.ByNode {
|
||||
vals, err := meshmoduleassignerclient.AssignmentsValues(p)
|
||||
if err != nil {
|
||||
put(frombus.AssignmentsRead, p.Node+".node", nil, err)
|
||||
}
|
||||
for key, raw := range vals {
|
||||
put(frombus.AssignmentsRead, key, raw, nil)
|
||||
}
|
||||
}
|
||||
for node, rec := range b.Record {
|
||||
raw, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &rec})
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// aNode is a bus with n1 the hub, each of manifests placed on it and held at the indexer, in the order given.
|
||||
func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) {
|
||||
t.Helper()
|
||||
p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
||||
p := meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
||||
b := &bus{records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd"}}},
|
||||
modules: map[string]meshcatalog.ModulesByModuleValue{}, manifests: map[string][]byte{}, builds: map[string]*builds.BuilderBuild{}}
|
||||
fps := map[string]string{}
|
||||
@@ -31,7 +31,7 @@ func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) {
|
||||
b.manifests[meshcatalogclient.ManifestDigest([]byte(raw))] = []byte(raw)
|
||||
fps[module] = "src2:" + strings.Repeat(string(rune('1'+i)), 64)
|
||||
}
|
||||
b.placements = map[string]meshmoduleassigner.Placement{"n1": p}
|
||||
b.assignments = map[string]meshmoduleassigner.Assignments{"n1": p}
|
||||
return b, fps
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package frombus
|
||||
|
||||
// **The bus read through a module's declared `reads`** (the lead, 2026-10-29: one reader in place of the copies in
|
||||
// mesh-provisioner and disk-load): a Reader over the states a module reads, by a runtime's Get and Keys (a bundle's
|
||||
// stdio.State, a process's own), for a holder that composes plans or data items and reads no value: placement, each
|
||||
// stdio.State, a process's own), for a holder that composes plans or data items and reads no value: assignment, each
|
||||
// node's record in its own have key, the indexer's modules and manifests, the builder's builds and mesh-config's resolved
|
||||
// values. No own secret and no credential is read (compose.Planning). Pure as the rest of this package: a bundle's
|
||||
// runtime states and an HTTP fetch are compose/fromruntime's.
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"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/modules/mesh-delivery-builder/builds"
|
||||
@@ -31,16 +31,16 @@ import (
|
||||
const (
|
||||
// The holders' names are kept here, in the engine's own code, as the names of the states a manifest's reads use
|
||||
// (ADR 0634 §1, finding F4); a seat's contract names a state locally, not the module that holds it.
|
||||
PlacementRead = buckets.AssignerHolder + ".placement"
|
||||
ModulesRead = buckets.CatalogHolder + ".modules"
|
||||
ManifestsRead = buckets.CatalogHolder + ".manifests"
|
||||
BuildsRead = builds.BuilderModule + ".builds"
|
||||
HaveRead = "node-engine.have"
|
||||
ResolvedRead = "mesh-config.resolved"
|
||||
AssignmentsRead = buckets.AssignerHolder + ".assignment"
|
||||
ModulesRead = buckets.CatalogHolder + ".modules"
|
||||
ManifestsRead = buckets.CatalogHolder + ".manifests"
|
||||
BuildsRead = builds.BuilderModule + ".builds"
|
||||
HaveRead = "node-engine.have"
|
||||
ResolvedRead = "mesh-config.resolved"
|
||||
)
|
||||
|
||||
// StatesReads are those reads, in a manifest's order.
|
||||
var StatesReads = []string{PlacementRead, ModulesRead, ManifestsRead, BuildsRead, HaveRead, ResolvedRead}
|
||||
var StatesReads = []string{AssignmentsRead, ModulesRead, ManifestsRead, BuildsRead, HaveRead, ResolvedRead}
|
||||
|
||||
// States is a module's read states as its runtime gives them: a key's value, false when it has none.
|
||||
type States interface {
|
||||
@@ -85,9 +85,9 @@ func (r statesReader) each(read string, visit func(key string, value []byte)) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r statesReader) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
|
||||
func (r statesReader) Assignments(context.Context) (map[string]meshmoduleassigner.Assignments, error) {
|
||||
keys := meshmoduleassignerclient.NewKeys()
|
||||
err := r.each(PlacementRead, func(k string, v []byte) {
|
||||
err := r.each(AssignmentsRead, func(k string, v []byte) {
|
||||
_, _ = keys.Put(k, v, 0, false) // a key that does not read places nothing, as before
|
||||
})
|
||||
return keys.All(), err
|
||||
|
||||
@@ -2,7 +2,7 @@ package frombus
|
||||
|
||||
// **A node's plans at two sets of builds, over one read** (issue 1954; novox/hq ADR 0570 clause 2, ADR 0580 clause 2):
|
||||
// the seat mesh-provisioner's holder composes each node at the builds its wants name and at the newest build of each
|
||||
// placed module. Both are composed from one read of the bus (placement, records, the indexer's modules and manifests,
|
||||
// placed module. Both are composed from one read of the bus (assignment, records, the indexer's modules and manifests,
|
||||
// values: base); only the builds the node's modules are composed from differ, each build read once for both, and a
|
||||
// build taken in once per content by a reader that keeps it (StatesReader). Where the time went is said by phase.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ func (States) Keys(state string) ([]string, error) { return stdio.State(state).K
|
||||
func Reader() frombus.Reader { return frombus.StatesReader(States{}, HTTPFetch) }
|
||||
|
||||
// Watched is frombus's States over a watched copy (novox/hq ADR 0580 clause 1): the states a composition reads whole
|
||||
// (placement, haves, modules, resolved values) from the copy, a build by key when asked, a manifest once per digest.
|
||||
// (assignment, haves, modules, resolved values) from the copy, a build by key when asked, a manifest once per digest.
|
||||
type Watched struct{ *stdio.WatchedStates }
|
||||
|
||||
// Get is a key's value: a build read now, a manifest kept once read, any other state's from the copy.
|
||||
|
||||
@@ -260,7 +260,7 @@ type ComposedHow struct {
|
||||
HeldBefore map[string]string
|
||||
}
|
||||
|
||||
// Place is where a node is, as its placement says (inv.Overlay).
|
||||
// Place is where a node is, as its assignment says (inv.Overlay).
|
||||
type Place struct {
|
||||
Key string
|
||||
Endpoint string
|
||||
|
||||
@@ -123,10 +123,10 @@ func (c *composer) network(on map[string]bool, refused map[string]string) (*over
|
||||
Site: p.Site, Hub: p.Hub, Address: p.Address,
|
||||
}
|
||||
if t, takes := tunnels[node.Name]; takes && t.NodeAdopted {
|
||||
// Only an adopted node is told to take the found unit over. A hub whose placement disagrees with the tunnel is
|
||||
// Only an adopted node is told to take the found unit over. A hub whose assignment disagrees with the tunnel is
|
||||
// refused, not composed.
|
||||
if wrong := disagrees(p, t.Tunnel); wrong != "" {
|
||||
return nil, fmt.Errorf("%s takes over the tunnel on %s and its placement disagrees with it: %s. "+
|
||||
return nil, fmt.Errorf("%s takes over the tunnel on %s and its assignment disagrees with it: %s. "+
|
||||
"Re-place it — `overlay place %s --hub --endpoint <host>:%d …` — and push again; "+
|
||||
"nothing was composed", node.Name, t.Interface, wrong, node.Name, t.Port)
|
||||
}
|
||||
@@ -217,7 +217,7 @@ func (c *composer) generators() (map[string]catalogue.Generator, error) {
|
||||
return map[string]catalogue.Generator{overlay.Name: net}, nil
|
||||
}
|
||||
|
||||
// disagrees says how a node's placement differs from the tunnel it takes over — its address not the tunnel's, its
|
||||
// disagrees says how a node's assignment differs from the tunnel it takes over — its address not the tunnel's, its
|
||||
// endpoint not on the tunnel's port — or nothing when both agree.
|
||||
func disagrees(p Place, t Tunnel) string {
|
||||
var wrong []string
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestTheOverlaysRangeIsTheAdoptedTunnels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A takeover is composed only for a hub whose placement agrees with the tunnel (mesh-controller network_test.go
|
||||
// A takeover is composed only for a hub whose assignment agrees with the tunnel (mesh-controller network_test.go
|
||||
// TestATakeoverIsNotComposedForAHubPlacedOffItsTunnel): an address or an endpoint port that differs would have the host stop
|
||||
// the found interface and raise the mesh's where no peer is listening.
|
||||
func TestATakeoverIsNotComposedForAHubPlacedOffItsTunnel(t *testing.T) {
|
||||
|
||||
@@ -78,7 +78,7 @@ func (c *composer) render(nodeName string, mode Mode) (Rendered, error) {
|
||||
}
|
||||
r := placed.Rendering
|
||||
out.ForeseenPorts = placed.ForeseenPorts
|
||||
// A plan carries its contributions' machine ports, refused where the placement assigned none (issue 774).
|
||||
// A plan carries its contributions' machine ports, refused where the assignment assigned none (issue 774).
|
||||
r.Planned = mode == Planning
|
||||
r.Grants, r.Withheld, r.Unbound = grants, withheld, unbound
|
||||
out.Withheld, out.Unbound = withheld, unbound
|
||||
|
||||
@@ -328,7 +328,7 @@ func (c *composer) namesInTheMesh() map[string]string {
|
||||
}
|
||||
|
||||
// StoreSeat is the seat whose holder is the artifact store (novox/hq ADR 0632 §1): the store's address is that holder's
|
||||
// listen open to the mesh, on the machine placement puts it on. The provision artifact-store it was found by before is
|
||||
// listen open to the mesh, on the machine assignment puts it on. The provision artifact-store it was found by before is
|
||||
// retired; no module serves it.
|
||||
const StoreSeat = "container-image-registry"
|
||||
|
||||
|
||||
@@ -1520,7 +1520,7 @@ func resolutionContributedOf(r mcat.Resolution, m mcat.Manifest, to string, with
|
||||
}
|
||||
|
||||
// atAssignedPort is a contribution of m with its port where this machine publishes it for m: the number
|
||||
// `${port:<software port>}` gives m itself and its declaration publishes (machinePort, port_into.go), the placement's
|
||||
// `${port:<software port>}` gives m itself and its declaration publishes (machinePort, port_into.go), the assigner's
|
||||
// assignment where it holds one and the software's own number where it holds none (a bare port not yet moved, a fixed
|
||||
// mapping, the host's network). What its provider dials, on this machine over loopback and from another at m's address.
|
||||
//
|
||||
|
||||
@@ -15,22 +15,22 @@ func enrolmentVerbs() []mcat.Verb {
|
||||
}
|
||||
return []mcat.Verb{
|
||||
{Name: mcat.EnrolmentVerbIssue, Description: "Issue a one-time token for a new machine, on the operator's Approve: " +
|
||||
"refused at once, naming every finding, for a name that is not a node name or that a node record, a placement " +
|
||||
"refused at once, naming every finding, for a name that is not a node name or that a node record, an assignments " +
|
||||
"key or an open enrolment holds, and for every call before the controller's lease is handed away. Otherwise " +
|
||||
"reserves the lowest free private-network address, asks the operator under the seat's own name and, on the " +
|
||||
"warrant, adds the node to placement with its tunnel key, asks the vault for the token and says enrolment-issued. " +
|
||||
"warrant, adds the node to the assignments with its tunnel key, asks the vault for the token and says enrolment-issued. " +
|
||||
"The token itself is read only through tokens.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the new machine's name in the mesh",
|
||||
"why": "why it joins, shown to the operator with the ask",
|
||||
"expires": "how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours",
|
||||
// The operator's decision of 2026-11-02 01:05: the tunnel key is given at issue, so on the Approve the node
|
||||
// enters placement with it and the hub peers it before redeem.
|
||||
// enters the assignments with it and the hub peers it before redeem.
|
||||
"overlay_key": "the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it",
|
||||
}, []string{"node", "why", "overlay_key"})},
|
||||
{Name: mcat.EnrolmentVerbRedeem, Description: "Spend a token: asked by a joining node-engine dialled as enrol.<node> " +
|
||||
"alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the " +
|
||||
"vault for the node's account and certificate, writes the node into placement, and answers the account sealed " +
|
||||
"vault for the node's account and certificate, writes the node into the assignments, and answers the account sealed " +
|
||||
"to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys " +
|
||||
"it finishes what is not done and answers the same; with other keys it is refused.",
|
||||
Input: map[string]any{"type": "object", "additionalProperties": false,
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) {
|
||||
t.Fatalf("the plan gives %+v (%v)", given, err)
|
||||
}
|
||||
// Its port is where the machine publishes it, the number ${port:9000} gives the module and its declaration publishes,
|
||||
// never the software's own where the placement moved it (2026-10-31: route-proxy dialled gitea's 3000, published at
|
||||
// never the software's own where the assigner moved it (2026-10-31: route-proxy dialled gitea's 3000, published at
|
||||
// 20000): its provider dials what the plan says. Where a plan carries it, a port the module does not listen on is
|
||||
// refused by module, node and port.
|
||||
for _, p := range []struct {
|
||||
@@ -65,10 +65,10 @@ func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) {
|
||||
port float64
|
||||
refused string
|
||||
}{
|
||||
{"assigned by the placement (gitea 3000 at 20000)", map[string]map[int]int{"notify": {9000: 20009}}, nil, true, 20009, ""},
|
||||
{"given by the node over the placement", map[string]map[int]int{"notify": {9000: 20009}},
|
||||
{"assigned by the assigner (gitea 3000 at 20000)", map[string]map[int]int{"notify": {9000: 20009}}, nil, true, 20009, ""},
|
||||
{"given by the node over the assigner", map[string]map[int]int{"notify": {9000: 20009}},
|
||||
map[string]map[int]int{"notify": {9000: 9900}}, true, 9900, ""},
|
||||
{"a bare published port the placement holds none for keeps its number (influxdb 8086)",
|
||||
{"a bare published port the assigner holds none for keeps its number (influxdb 8086)",
|
||||
map[string]map[int]int{"notify": {9000: 9000}}, nil, true, 9000, ""},
|
||||
{"a mapping the module fixed (as mailu 7080:80)", map[string]map[int]int{"notify": {9000: 7080}}, nil, true, 7080, ""},
|
||||
{"a port the module does not listen on", nil, nil, true, 0,
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// placementVerbs are the seat's verbs: what reads all it keeps and does by hand all it does by itself (ADR 0567 §10).
|
||||
// Optional while the holder that serves them is not yet on every node (design 33 §7): the seat is new in the same
|
||||
// delivery as its holder.
|
||||
func placementVerbs() []mcat.Verb {
|
||||
changes := map[string]any{"type": "array", "minItems": 1, "description": "the changes, each an object with `op`: " +
|
||||
"place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), " +
|
||||
"unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide " +
|
||||
"(node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, " +
|
||||
"layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-node-enroller's alone, the " +
|
||||
"proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), " +
|
||||
"remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-node-enroller's alone, " +
|
||||
"on the same warrant)",
|
||||
"items": map[string]any{"type": "object", "required": []string{"op", "node"}}}
|
||||
return []mcat.Verb{
|
||||
{Name: mcat.PlacementVerbPlacement, Optional: true, Description: "Read placement: one node's key, the nodes one " +
|
||||
"module is placed on, or every key, each with its revision, and how full the state is against its cap. With " +
|
||||
"history, one node's or one module's changes in order: a revision the state keeps whole, an older one as the " +
|
||||
"proposal's changes from its placement-changed event.",
|
||||
Input: schema(map[string]string{"node": "one node", "module": "one module: the nodes it is placed on",
|
||||
"history": "\"true\": the changes in order, from the stream and the state's revisions",
|
||||
// ADR 0571 clause 4: who holds which seat on which node, from the state seat-holders.
|
||||
"seats": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's"},
|
||||
nil, "history", "seats")},
|
||||
{Name: mcat.PlacementVerbProposals, Optional: true, Description: "Every open proposal of placement: its changes, " +
|
||||
"the revision of each key it was laid over, its candidates, every node's answer, the differences computed and " +
|
||||
"answered, its verdict and its ask. Narrowed to one by id.",
|
||||
Input: schema(map[string]string{"id": "one proposal's id"}, nil)},
|
||||
{Name: mcat.PlacementVerbPropose, Optional: true, Description: "Propose a change of placement, with why: laid over " +
|
||||
"the current keys and refused at once, naming every finding, when the result cannot be placed (two modules " +
|
||||
"on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two " +
|
||||
"hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose " +
|
||||
"it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the " +
|
||||
"caller, as the runtime proves it. An add-node from mesh-node-enroller, whose warrant names its node and address, " +
|
||||
"is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).",
|
||||
Input: map[string]any{"type": "object", "additionalProperties": false, "required": []string{"why", "changes"},
|
||||
"properties": map[string]any{
|
||||
"why": map[string]any{"type": "string", "description": "why, shown to the operator with the proposal"},
|
||||
"changes": changes,
|
||||
// ADR 0587 §7: an add-node carries the warrant of the enrolment that issued the node's token, whose
|
||||
// Approve is the proposal's; the holder writes it with no ask of its own.
|
||||
"warrant": map[string]any{"type": "object", "description": "for an add-node alone: the enrolment's " +
|
||||
"warrant {node, address, ask, warrant}, naming the change's node and address",
|
||||
"required": []string{"node", "address", "ask", "warrant"}},
|
||||
}}},
|
||||
{Name: mcat.PlacementVerbWithdraw, Optional: true, Description: "Withdraw an open proposal: its key and its " +
|
||||
"candidates are deleted and placement-withdrawn carries it whole. With why.",
|
||||
Input: schema(map[string]string{"id": "the proposal's id", "why": "why, kept in the event"}, []string{"id", "why"})},
|
||||
{Name: mcat.PlacementVerbAgain, Optional: true, Description: "Judge an open proposal now with the answers that " +
|
||||
"stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. " +
|
||||
"Nothing more: the recovery of a proposal whose judgement or ask was lost.",
|
||||
Input: schema(map[string]string{"id": "the proposal's id"}, []string{"id"})},
|
||||
}
|
||||
}
|
||||
@@ -408,11 +408,11 @@ func TestRouteProxysProofsOfAComposition(t *testing.T) {
|
||||
placed map[int]int
|
||||
at int
|
||||
}{
|
||||
{"beside the proxy, moved by the placement (gitea)", "anchor", "gitea", 3000, []any{"3000"}, map[int]int{3000: 20000}, 20000},
|
||||
{"on another machine, moved by the placement (grafana)", "home", "grafana", 3000, []any{"3000"}, map[int]int{3000: 20012}, 20012},
|
||||
{"a bare port the placement holds none for (influxdb)", "home", "influxdb", 8086, []any{"8086"}, nil, 8086},
|
||||
{"beside the proxy, moved by the assigner (gitea)", "anchor", "gitea", 3000, []any{"3000"}, map[int]int{3000: 20000}, 20000},
|
||||
{"on another machine, moved by the assigner (grafana)", "home", "grafana", 3000, []any{"3000"}, map[int]int{3000: 20012}, 20012},
|
||||
{"a bare port the assigner holds none for (influxdb)", "home", "influxdb", 8086, []any{"8086"}, nil, 8086},
|
||||
{"a fixed mapping (mailu 7080:80)", "anchor", "mailu", 7080, []any{"25", "7080:80"}, nil, 7080},
|
||||
{"a fixed mapping the placement names another port of (photos 9102:9000)", "anchor", "photos", 9102,
|
||||
{"a fixed mapping the assigner names another port of (photos 9102:9000)", "anchor", "photos", 9102,
|
||||
[]any{"9102:9000"}, map[int]int{9000: 20011}, 9102},
|
||||
{"the host's network (home-assistant)", "anchor", "home-assistant", 8123, nil, nil, 8123},
|
||||
} {
|
||||
|
||||
@@ -10,8 +10,8 @@ package composer
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
|
||||
"slices"
|
||||
@@ -138,19 +138,19 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) {
|
||||
if err != nil {
|
||||
return CaseVerdict{}, err
|
||||
}
|
||||
placements := map[string]*meshmoduleassigner.Placement{}
|
||||
for node, raw := range c.Placements {
|
||||
assignments := map[string]*meshmoduleassigner.Assignments{}
|
||||
for node, raw := range c.Assignments {
|
||||
if string(raw) == "null" {
|
||||
placements[node] = nil
|
||||
assignments[node] = nil
|
||||
continue
|
||||
}
|
||||
p, perr := meshmoduleassignerclient.DecodePlacement(raw)
|
||||
p, perr := meshmoduleassignerclient.DecodeAssignments(raw)
|
||||
if perr != nil {
|
||||
return CaseVerdict{}, fmt.Errorf("composer: the case %q: the placement of %s is not one: %w", c.Name, node, perr)
|
||||
return CaseVerdict{}, fmt.Errorf("composer: the case %q: the assignment of %s is not one: %w", c.Name, node, perr)
|
||||
}
|
||||
placements[node] = &p
|
||||
assignments[node] = &p
|
||||
}
|
||||
in, err := frombus.Placed(base, placements)
|
||||
in, err := frombus.Placed(base, assignments)
|
||||
if err != nil {
|
||||
return CaseVerdict{}, fmt.Errorf("composer: the case %q: %w", c.Name, err)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package composer
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"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-networking/modules/mesh-wireguard/go/overlay"
|
||||
@@ -55,17 +55,17 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
|
||||
if _, err := Run(Request{V: Version, Depth: "deep"}); err == nil {
|
||||
t.Error("an unknown depth runs")
|
||||
}
|
||||
// A case's own placement (issue 1126) composes over a node the input holds, and is refused for one it does not.
|
||||
// A case's own assignment (issue 1126) composes over a node the input holds, and is refused for one it does not.
|
||||
placed := withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthResolve,
|
||||
Cases: []Case{{Name: "trunk", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}},
|
||||
{Name: "change", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}},
|
||||
Placements: bucketstest.Raw(map[string]meshmoduleassigner.Placement{"a": {V: 1, Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "ghost"}}}})}}},
|
||||
Assignments: bucketstest.Raw(map[string]meshmoduleassigner.Assignments{"a": {V: 1, Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "ghost"}}}})}}},
|
||||
compose.Input{Nodes: []compose.Node{{Name: "a"}}})
|
||||
if v, err := Run(placed); err != nil || !v.Cases[0].Machines["a"].Composes || v.Cases[1].Machines["a"].Composes ||
|
||||
!strings.Contains(v.Cases[1].Machines["a"].Reason, "ghost") {
|
||||
t.Errorf("a case composes its own placement: %+v, %v", v, err)
|
||||
t.Errorf("a case composes its own assignment: %+v, %v", v, err)
|
||||
}
|
||||
placed.Cases[1].Placements = bucketstest.Raw(map[string]meshmoduleassigner.Placement{"z": {V: 1, Node: "z"}})
|
||||
placed.Cases[1].Assignments = bucketstest.Raw(map[string]meshmoduleassigner.Assignments{"z": {V: 1, Node: "z"}})
|
||||
if _, err := Run(placed); err == nil || !strings.Contains(err.Error(), "holds no node for") {
|
||||
t.Errorf("a case placing a node the input lacks: %v", err)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ const (
|
||||
MaxAnswerBytes = 160 << 10
|
||||
)
|
||||
|
||||
// ReturnsToNothing is what a node answers a proposal that takes its placement key away.
|
||||
const ReturnsToNothing = "the proposal takes this node's placement away: it composes nothing then"
|
||||
// ReturnsToNothing is what a node answers a proposal that takes its assignment key away.
|
||||
const ReturnsToNothing = "the proposal takes this node's assignment away: it composes nothing then"
|
||||
|
||||
// NodeWord is the last part of a node's own key, and the name of its own composition in an answer.
|
||||
const NodeWord = "node"
|
||||
|
||||
@@ -24,7 +24,7 @@ type Comparison struct {
|
||||
}
|
||||
|
||||
// CompareComposed compares the compositions a node composed (by key, as compose.Compositions answers them) with the ones
|
||||
// it runs, as its agent would (ADR 0559 §4: a node's answer to a placement candidate). A module is the same when its
|
||||
// it runs, as its agent would (ADR 0559 §4: a node's answer to a assignment candidate). A module is the same when its
|
||||
// composition's digest is the one it runs (ComposedDigestOf, the signature left out), so the agent would apply nothing
|
||||
// of it; it differs when the digests differ (both named); it is added when composed and not run, removed when run and
|
||||
// not composed. The node's own composition is named HaveNodeWord. The bus's user list (catalogue.BusUsersID) is left out
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestMemoryIsReadOrReportedMissing(t *testing.T) {
|
||||
t.Errorf("memoryFrom returned %d, %v", kb, ok)
|
||||
}
|
||||
// A meminfo without MemTotal must not read as zero memory — a machine reporting no memory
|
||||
// would be excluded from placement for a parsing failure.
|
||||
// would be excluded from assignment for a parsing failure.
|
||||
if _, ok := memoryFrom("MemFree: 1 kB\n"); ok {
|
||||
t.Error("a meminfo with no MemTotal was read as a successful measurement")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package judge is the node-engine's judgement of a move (novox/hq ADR 0377 §3, ADR 0547 §3): what moving a module from
|
||||
// the composition it runs to a new one does, and how a placement candidate's answer is fitted to the node's key. It
|
||||
// the composition it runs to a new one does, and how a assignment candidate's answer is fitted to the node's key. It
|
||||
// moved here from mesh-sdk's delivery package (novox/hq ADR 0593 §1, §7), since only the node-engine judges a move;
|
||||
// the shapes it judges (a composition, a judgement's words, a candidate's answer) stay in mesh-sdk.
|
||||
package judge
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// An answer too large for the node's key is fitted: the texts go first, the verdict and the paths stay, and the answer
|
||||
// given is not changed (moved from mesh-sdk's placement test with FitAnswer, novox/hq ADR 0593 §7).
|
||||
// given is not changed (moved from mesh-sdk's assignment test with FitAnswer, novox/hq ADR 0593 §7).
|
||||
func TestFitAnswer(t *testing.T) {
|
||||
long := strings.Repeat("x", compositions.MaxBody)
|
||||
big := CandidateAnswer{Proposal: "p-x1", Revision: 1, Verdict: nodeengine.CandidateAnswerVerdictDiffers, Differs: []ModuleDiffers{{
|
||||
|
||||
@@ -159,7 +159,7 @@ type Joining struct {
|
||||
|
||||
// Run joins: the answer is verified before anything is kept (an answer not signed by the key the token carries keeps nothing),
|
||||
// kept, and then the machine dials as the login its key of node-logins names and confirms. Nothing is applied here: the engine
|
||||
// applies once its placement names a module.
|
||||
// applies once its assignment names a module.
|
||||
func (j Joining) Run(ctx context.Context) (meshnodeenrollerclient.RedeemAnswer, error) {
|
||||
answer, err := j.Redeem(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -52,7 +52,7 @@ type haveState struct {
|
||||
// nodeSealed is, on a node composing its own, each value its own composition carries sealed to its key (ADR 0547 §3,
|
||||
// issue 1126): kept in its key so a restart reuses them and applies nothing again.
|
||||
nodeSealed compositions.Seals
|
||||
// nodeCandidates is the node's answer to each placement candidate proposal (novox/hq ADR 0559 §4), written into its key.
|
||||
// nodeCandidates is the node's answer to each assignment candidate proposal (novox/hq ADR 0559 §4), written into its key.
|
||||
nodeCandidates map[string]nodeengine.CandidateAnswer
|
||||
// nodeComposes and nodeComposesWhy are whose compositions the node applies and why (HaveNode.Composes), written into
|
||||
// its key.
|
||||
@@ -748,7 +748,7 @@ func (q *Session) NoteNode(ctx context.Context, composed string, sealed composit
|
||||
q.writeHaves(ctx, bus)
|
||||
}
|
||||
|
||||
// NoteCandidates takes the node's answers to the placement candidates, by proposal (its shadow composer's, novox/hq ADR
|
||||
// NoteCandidates takes the node's answers to the assignment candidates, by proposal (its shadow composer's, novox/hq ADR
|
||||
// 0559 §4), and writes the node's key where it differs; a proposal left out is no longer answered.
|
||||
func (q *Session) NoteCandidates(ctx context.Context, answers map[string]nodeengine.CandidateAnswer) {
|
||||
q.init()
|
||||
|
||||
@@ -617,7 +617,7 @@ func TestAnAppliedModulesHaveNamesTheFingerprintAndTheDigestAndFollowsItsHealth(
|
||||
if n, ok := q.ReadNodeHave(context.Background()); !ok || n.Composed != dig || len(n.SealedAt) != 1 {
|
||||
t.Fatalf("the node key read back: %+v %v", n, ok)
|
||||
}
|
||||
// The node's answers to placement candidates ride in the same key (ADR 0559 §4), and go when no group is answered.
|
||||
// The node's answers to assignment candidates ride in the same key (ADR 0559 §4), and go when no group is answered.
|
||||
answers := map[string]nodeengine.CandidateAnswer{"p-1": {Proposal: "p-1", Revision: 1, Verdict: nodeengine.CandidateAnswerVerdictSame, Same: []string{"web"}}}
|
||||
q.NoteCandidates(context.Background(), answers)
|
||||
if n, err := nodeengineclient.DecodeHaveNode(l.kv["n1.node"].value); err != nil || n.Candidates["p-1"].Proposal != "p-1" || n.Composed != dig {
|
||||
|
||||
@@ -71,7 +71,7 @@ func TestNatsAKeysRevisionsAreReadBack(t *testing.T) {
|
||||
}
|
||||
|
||||
// A bucket that is not there, and one the account may not read, are errors at once, never a wait: the node agent
|
||||
// follows its placement key by one watch at its start (endWhenPlaced) and, refused, stays on the controller's compositions and says why.
|
||||
// follows its assignment key by one watch at its start (endWhenPlaced) and, refused, stays on the controller's compositions and says why.
|
||||
if _, err := bus.KVWatch(ctx, "never-it", ">"); err == nil {
|
||||
t.Error("a watch of a bucket that is not there gave no error")
|
||||
}
|
||||
@@ -112,13 +112,13 @@ func TestNatsAKeysRevisionsAreReadBack(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := later.PutString("placement.n1", "v"); err != nil {
|
||||
if _, err := later.PutString("n1.node", "v"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for seen := false; !seen; {
|
||||
select {
|
||||
case u := <-ch:
|
||||
seen = u.Key == "placement.n1" && string(u.Value) == "v"
|
||||
seen = u.Key == "n1.node" && string(u.Value) == "v"
|
||||
case <-actx.Done():
|
||||
t.Fatal("the bucket made after the watch was never watched")
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type KVUpdate struct {
|
||||
type KVBus interface {
|
||||
// KVWatch yields the keys of the bucket matching the pattern now, a Caught update, then every change, until ctx ends
|
||||
// or the link does (the channel is then closed). A bucket the node's account may not read is an error. The pattern
|
||||
// may be several, separated by spaces (WatchPatterns), watched as one: a node's placement in either form of its
|
||||
// may be several, separated by spaces (WatchPatterns), watched as one: a node's assignment in either form of its
|
||||
// keys (research 072 row 37).
|
||||
KVWatch(ctx context.Context, bucket, pattern string) (<-chan KVUpdate, error)
|
||||
}
|
||||
@@ -269,7 +269,7 @@ func CreatedAdvisory(bucket string) string {
|
||||
return "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_" + bucket
|
||||
}
|
||||
|
||||
// KVWatchAwait is KVWatch for a bucket that may not be on the bus yet (the placement seat's states, made when their
|
||||
// KVWatchAwait is KVWatch for a bucket that may not be on the bus yet (the assignment seat's states, made when their
|
||||
// holder starts): the channel is returned at once, and while the bucket is missing it yields nothing and waiting is told
|
||||
// why; when the bus says the bucket's stream was created the watch is made and its updates follow. No timer: the
|
||||
// advisory is heard before the first try, so a creation between the try and the wait is not missed. Any other error of
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@
|
||||
"$JS.API.CONSUMER.CREATE.KV_composed-NODE.>",
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.NODE.>",
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_candidates.>",
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_placement.>",
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_assignments.>",
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_wants.*.$KV.mesh-delivery-assigner_wants.want.NODE.>",
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-builder_builds.>",
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-indexer_candidates.>",
|
||||
@@ -22,7 +22,7 @@
|
||||
"$JS.API.CONSUMER.DELETE.KV_composed-NODE.>",
|
||||
"$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>",
|
||||
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_candidates.>",
|
||||
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_placement.>",
|
||||
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_assignments.>",
|
||||
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-assigner_wants.>",
|
||||
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-builder_builds.>",
|
||||
"$JS.API.CONSUMER.DELETE.KV_mesh-delivery-indexer_candidates.>",
|
||||
@@ -40,7 +40,7 @@
|
||||
"$JS.API.DIRECT.GET.KV_composed-NODE.>",
|
||||
"$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.NODE.>",
|
||||
"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_candidates.>",
|
||||
"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_placement.>",
|
||||
"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_assignments.>",
|
||||
"$JS.API.DIRECT.GET.KV_mesh-delivery-assigner_wants.$KV.mesh-delivery-assigner_wants.want.NODE.>",
|
||||
"$JS.API.DIRECT.GET.KV_mesh-delivery-builder_builds.>",
|
||||
"$JS.API.DIRECT.GET.KV_mesh-delivery-indexer_candidates.>",
|
||||
@@ -58,7 +58,7 @@
|
||||
"$JS.API.STREAM.INFO.KV_composed-NODE",
|
||||
"$JS.API.STREAM.INFO.KV_mesh-config_resolved",
|
||||
"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_candidates",
|
||||
"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_placement",
|
||||
"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_assignments",
|
||||
"$JS.API.STREAM.INFO.KV_mesh-delivery-assigner_wants",
|
||||
"$JS.API.STREAM.INFO.KV_mesh-delivery-builder_builds",
|
||||
"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_candidates",
|
||||
@@ -75,7 +75,7 @@
|
||||
"$JS.FC.KV_composed-NODE.>",
|
||||
"$JS.FC.KV_mesh-config_resolved.>",
|
||||
"$JS.FC.KV_mesh-delivery-assigner_candidates.>",
|
||||
"$JS.FC.KV_mesh-delivery-assigner_placement.>",
|
||||
"$JS.FC.KV_mesh-delivery-assigner_assignments.>",
|
||||
"$JS.FC.KV_mesh-delivery-assigner_wants.>",
|
||||
"$JS.FC.KV_mesh-delivery-builder_builds.>",
|
||||
"$JS.FC.KV_mesh-delivery-indexer_candidates.>",
|
||||
@@ -112,7 +112,7 @@
|
||||
"PublishDeny": null,
|
||||
"Subscribe": [
|
||||
"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_candidates",
|
||||
"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_placement",
|
||||
"$JS.EVENT.ADVISORY.STREAM.CREATED.KV_mesh-delivery-assigner_assignments",
|
||||
"$SRV.INFO",
|
||||
"$SRV.INFO.node-engine",
|
||||
"$SRV.INFO.node-engine.>",
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"github.com/novox/mesh-host/internal/buckets"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"maps"
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
// **A check's candidate, composed in shadow on a call** (novox/hq ADR 0593 §7): the node seat's verb compose-candidate.
|
||||
// Given the check's composition key (`<group>.<heads hash>`), it reads the group's candidate from the indexer
|
||||
// (buckets.Candidates, keyed by the group alone, buckets.CandidateKey) at those heads, and composes itself
|
||||
// twice from what its shadow holds (placement, records, the indexer's modules, its config values), with its own sealed
|
||||
// twice from what its shadow holds (assignment, records, the indexer's modules, its config values), with its own sealed
|
||||
// values: once as it is, and once with the candidate's modules laid over the indexer's (each at the candidate's
|
||||
// fingerprint, its manifest by digest and the indexer's verdict on it, ADR 0578 §3; a module the candidate removes left
|
||||
// out). It answers per module placed here composed (its digest, and whether the candidate changes it), refused by the
|
||||
@@ -315,7 +315,7 @@ func (c *Composer) scratch() *Composer {
|
||||
out := New(c.env)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
out.placements = maps.Clone(c.placements)
|
||||
out.assignments = maps.Clone(c.assignments)
|
||||
out.records = maps.Clone(c.records)
|
||||
out.inOwn = maps.Clone(c.inOwn)
|
||||
out.modules = maps.Clone(c.modules)
|
||||
@@ -326,7 +326,7 @@ func (c *Composer) scratch() *Composer {
|
||||
out.sealedOwn = maps.Clone(c.sealedOwn)
|
||||
out.ownHanded = maps.Clone(c.ownHanded)
|
||||
out.certified = c.certified
|
||||
out.placementKeys = c.placementKeys
|
||||
out.assignmentKeys = c.assignmentKeys
|
||||
out.origin = c
|
||||
return out
|
||||
}
|
||||
@@ -433,14 +433,14 @@ func (c *Composer) layCandidate(ctx context.Context, cand meshcatalog.Candidates
|
||||
func (c *Composer) composeShadow(ctx context.Context, running []engcomp.Composed, laid map[string]bool, input func(frombus.Scope, map[string]string)) (map[string]engcomp.Composed, map[string]string, error) {
|
||||
node := c.env.Node
|
||||
c.mu.Lock()
|
||||
placement, placed := c.placements[node]
|
||||
assignment, placed := c.assignments[node]
|
||||
modules := maps.Clone(c.modules)
|
||||
inOwn := c.inOwnOf(placement)
|
||||
inOwn := c.inOwnOf(assignment)
|
||||
c.mu.Unlock()
|
||||
if !placed {
|
||||
return nil, nil, errors.New("this node's key in the seat's placement state is gone: it composes nothing of its own")
|
||||
return nil, nil, errors.New("this node's key in the seat's assignment state is gone: it composes nothing of its own")
|
||||
}
|
||||
fps := shadowBuilds(running, placement, modules, inOwn)
|
||||
fps := shadowBuilds(running, assignment, modules, inOwn)
|
||||
// A module the laid candidate changes is composed at the candidate's build, though the node runs another.
|
||||
for module := range fps {
|
||||
if mod, ok := modules[module]; ok && laid[module] {
|
||||
|
||||
@@ -3,8 +3,7 @@ package selfcompose
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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/go/seats/meshmoduleassigner"
|
||||
"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"
|
||||
@@ -25,7 +24,7 @@ import (
|
||||
// novox/hq ADR 0593 §7: the verb compose-candidate composes a check's candidate on this node in shadow. A candidate that
|
||||
// changes a placed module composes it at the candidate's build, changed; one whose manifest the indexer refused is
|
||||
// answered refused, naming the module and the judge's words, and the verdict is refused; a candidate the indexer does not
|
||||
// hold is refused in words. Nothing is applied, nothing is written, no placement answer is handed, and the answer comes
|
||||
// hold is refused in words. Nothing is applied, nothing is written, no assignment answer is handed, and the answer comes
|
||||
// within the bound; a shadow not ready and an ended bound answer unready at once, which the checker takes as no answer.
|
||||
func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
|
||||
b := newFakeBus()
|
||||
@@ -87,7 +86,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
|
||||
if a := s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: accepted}); a.Verdict != nec.Unready || a.Refused == "" {
|
||||
t.Errorf("a shadow not ready answered %+v", a)
|
||||
}
|
||||
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}})
|
||||
have, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||
@@ -96,10 +95,10 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
|
||||
for watching() < 6 && time.Now().Before(deadline) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
b.put(t, buckets.Placement, "placement.n1", place)
|
||||
b.putAssigned(t, place)
|
||||
b.put(t, buckets.Have, "n1.node", have)
|
||||
b.put(t, buckets.Modules, "lemurs", mod)
|
||||
for _, bucket := range []string{buckets.PlacementCandidates, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket} {
|
||||
for _, bucket := range []string{buckets.ProposalCandidates, buckets.Assignments, buckets.Have, buckets.Modules, ResolvedBucket} {
|
||||
b.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
for !s.ready.Load() && time.Now().Before(deadline) {
|
||||
@@ -185,7 +184,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
|
||||
var tick atomic.Int64
|
||||
env.Now = func() time.Time { return time.Unix(0, 0).Add(time.Duration(tick.Add(1)) * time.Second) }
|
||||
s := NewShadow(env, nil)
|
||||
s.c.placements["n1"] = meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
s.c.assignments["n1"] = meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}}
|
||||
s.c.records["n1"] = nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}
|
||||
s.c.modules["lemurs"] = meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}
|
||||
|
||||
@@ -54,7 +54,7 @@ type published struct {
|
||||
fps map[string]string // by module, the wanted fingerprint
|
||||
comps map[string]engcomp.Composed
|
||||
held map[string]string
|
||||
// placed is the modules the node's placement puts here, ran what runs of each module (by its composition's FP).
|
||||
// placed is the modules the node's assignment puts here, ran what runs of each module (by its composition's FP).
|
||||
placed []string
|
||||
ran map[string]string
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@ package selfcompose
|
||||
import (
|
||||
"context"
|
||||
"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/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/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"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"
|
||||
@@ -91,20 +90,20 @@ func TestTheNodePublishesItsCompositions(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}})
|
||||
have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||
omod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: ofp, Manifest: odigest, Repository: "novox/o", Commit: "c"})
|
||||
want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true})
|
||||
nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true})
|
||||
b.put(t, buckets.Placement, "placement.n1", place)
|
||||
b.putAssigned(t, place)
|
||||
b.put(t, buckets.Have, "n1.node", have)
|
||||
b.put(t, buckets.Modules, "lemurs", mod)
|
||||
b.put(t, buckets.Modules, "oldie", omod)
|
||||
b.put(t, buckets.Wants, "want.n1.lemurs", want)
|
||||
b.put(t, buckets.Wants, "want.n1.node", nodeWant)
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
b.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
for u := next(t, out); !u.Caught; u = next(t, out) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// modules and its own composition with mesh-sdk's composer, from what is on the bus, and hands the compositions to the
|
||||
// agent as the agent once read them from the controller's bucket `composed-<node>`.
|
||||
//
|
||||
// **It watches, it never polls** (ADR 0547 §2): its inputs are watches of the node's wants, the indexer's placement and
|
||||
// **It watches, it never polls** (ADR 0547 §2): its inputs are watches of the node's wants, the indexer's assignment and
|
||||
// modules, every node's own have key (where a node's record rides, ADR 0555 §3) and mesh-config's resolved values of this
|
||||
// node. A change of any of them marks the node to compose again; one loop drains the mark, so a burst of changes composes
|
||||
// the node once (composing one node takes 0.8 to 4.4 s, measured 2026-10-26). It hands the agent only the compositions that
|
||||
@@ -19,14 +19,14 @@ 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-contracts/placement/go/seats/meshwants"
|
||||
"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/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"github.com/novox/mesh-host/internal/buckets"
|
||||
@@ -135,13 +135,13 @@ const VaultSecretChanged = "mesh.mod.mesh-vault.event.secret-changed"
|
||||
type Composer struct {
|
||||
env Env
|
||||
|
||||
mu sync.Mutex
|
||||
wants map[string]meshwants.WantsByWantNodeModuleValue // by module, the node's own under agent.NodeModule
|
||||
placements map[string]meshmoduleassigner.Placement
|
||||
// placementKeys is the seat's state placement key by key in either form, each node's placement assembled from it
|
||||
mu sync.Mutex
|
||||
wants map[string]meshwants.WantsByWantNodeModuleValue // by module, the node's own under agent.NodeModule
|
||||
assignments map[string]meshmoduleassigner.Assignments
|
||||
// assignmentKeys is the seat's state assignment key by key in either form, each node's assignment assembled from it
|
||||
// (meshmoduleassignerclient.Keys; research 072 row 37): a write of the holder's under way is never composed half.
|
||||
placementKeys *meshmoduleassignerclient.Keys
|
||||
records map[string]nodeengine.NodeRecord
|
||||
assignmentKeys *meshmoduleassignerclient.Keys
|
||||
records map[string]nodeengine.NodeRecord
|
||||
// inOwn is, by module, whether this node's have of it names no fingerprint of the new form (mesh-wireguard, a module
|
||||
// at a build of the old form): the node runs it inside its own composition, and composes it there again (issue 1126).
|
||||
inOwn map[string]bool
|
||||
@@ -307,7 +307,7 @@ func New(env Env) *Composer {
|
||||
if env.Say == nil {
|
||||
env.Say = func(string) {}
|
||||
}
|
||||
return &Composer{env: env, wants: map[string]meshwants.WantsByWantNodeModuleValue{}, placements: map[string]meshmoduleassigner.Placement{}, placementKeys: meshmoduleassignerclient.NewKeys(),
|
||||
return &Composer{env: env, wants: map[string]meshwants.WantsByWantNodeModuleValue{}, assignments: map[string]meshmoduleassigner.Assignments{}, assignmentKeys: meshmoduleassignerclient.NewKeys(),
|
||||
records: map[string]nodeengine.NodeRecord{}, inOwn: map[string]bool{}, modules: map[string]meshcatalog.ModulesByModuleValue{}, values: map[string]map[string]any{},
|
||||
manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]builds.BuilderBuild{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{},
|
||||
pkgs: map[string]compositions.Composed{}, asked: make(chan chan Pass, 8)}
|
||||
@@ -363,7 +363,7 @@ func (c *Composer) inputs() []input {
|
||||
node := c.env.Node
|
||||
return []input{
|
||||
{buckets.Wants, "want." + node + ".>", (*Composer).takeWant, nil},
|
||||
{buckets.Placement, ">", (*Composer).takePlacement, nil},
|
||||
{buckets.Assignments, ">", (*Composer).takeAssignments, nil},
|
||||
// Every node's record, and this node's own haves (what it runs inside its own composition): two watches, each
|
||||
// within the node's grant (buscompose: readGrants of the have bucket at `*.node`, NodeKeysGrants at `<node>.>`).
|
||||
// A watch of every key is refused by the bus (issue 1126, 2026-10-27 09:21: the fold's `>` stopped every agent).
|
||||
@@ -396,7 +396,7 @@ func (c *Composer) takeBuildDone(u agent.Update) bool {
|
||||
w, wanted := c.wants[module]
|
||||
m := c.modules[module]
|
||||
placedHere := false
|
||||
for _, pm := range c.placements[c.env.Node].Modules {
|
||||
for _, pm := range c.assignments[c.env.Node].Modules {
|
||||
placedHere = placedHere || pm.Module == module
|
||||
}
|
||||
c.mu.Unlock()
|
||||
@@ -593,7 +593,7 @@ func (c *Composer) caughtSent() bool {
|
||||
return sent
|
||||
}
|
||||
|
||||
// takeChanged takes u through in.take and says whether the entry it names changed: a want, a placement, a module or a
|
||||
// takeChanged takes u through in.take and says whether the entry it names changed: a want, a assignment, a module or a
|
||||
// config value written again with the value it held changes nothing composed, so it makes no pass (issue 774,
|
||||
// 2026-10-31: every put under want.<node>.> made a pass, and each pass asked the vault for the node's own secrets).
|
||||
func (c *Composer) takeChanged(in input, u agent.Update) bool {
|
||||
@@ -623,14 +623,14 @@ func (c *Composer) entry(bucket, key string) (v any, has, known bool) {
|
||||
}
|
||||
w, ok := c.wants[module]
|
||||
return w.Fp, ok, true
|
||||
case buckets.Placement:
|
||||
// Any key of a node, its node key, a module key or its legacy key, stands for the node's whole placement: a
|
||||
case buckets.Assignments:
|
||||
// Any key of a node, its node key, a module key or its legacy key, stands for the node's whole assignment: a
|
||||
// module key written before its node key names it changes nothing composed.
|
||||
node, _, _, err := meshmoduleassignerclient.ParseKey(key)
|
||||
if err != nil {
|
||||
return nil, false, true
|
||||
}
|
||||
p, ok := c.placements[node]
|
||||
p, ok := c.assignments[node]
|
||||
return p, ok, true
|
||||
case buckets.Modules:
|
||||
name, err := meshcatalogclient.ParseModulesKey(key)
|
||||
@@ -677,25 +677,25 @@ func (c *Composer) takeWant(u agent.Update) {
|
||||
c.wants[module] = w
|
||||
}
|
||||
|
||||
func (c *Composer) takePlacement(u agent.Update) {
|
||||
func (c *Composer) takeAssignments(u agent.Update) {
|
||||
if u.Caught {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.placementKeys == nil {
|
||||
c.placementKeys = meshmoduleassignerclient.NewKeys()
|
||||
if c.assignmentKeys == nil {
|
||||
c.assignmentKeys = meshmoduleassignerclient.NewKeys()
|
||||
}
|
||||
node, err := c.placementKeys.Put(u.Key, u.Value, u.Revision, u.Deleted || len(u.Value) == 0)
|
||||
node, err := c.assignmentKeys.Put(u.Key, u.Value, u.Revision, u.Deleted || len(u.Value) == 0)
|
||||
if node != "" {
|
||||
if p, _, ok := c.placementKeys.Of(node); ok {
|
||||
c.placements[node] = p
|
||||
if p, _, ok := c.assignmentKeys.Of(node); ok {
|
||||
c.assignments[node] = p
|
||||
} else {
|
||||
delete(c.placements, node)
|
||||
delete(c.assignments, node)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if node != "" && err != nil && c.env.Say != nil {
|
||||
c.env.Say("the seat's placement of " + node + ": " + err.Error())
|
||||
c.env.Say("the seat's assignment of " + node + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,14 +758,14 @@ func (c *Composer) takeModuleHave(u agent.Update) bool {
|
||||
}
|
||||
|
||||
// inOwnOf is, of the modules p places, those the node runs inside its own composition (c.inOwn), sorted; c.mu held.
|
||||
// Machines is, from the placements this composer holds, each node by its address on the private network, and the hub's
|
||||
// Machines is, from the assignments this composer holds, each node by its address on the private network, and the hub's
|
||||
// node: what names the machine a failing network part points at (nodeengine.NetworkPart.Targets, issue 774). Empty
|
||||
// before any placement is read.
|
||||
// before any assignment is read.
|
||||
func (c *Composer) Machines() (byAddress map[string]string, hub string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
byAddress = map[string]string{}
|
||||
for node, p := range c.placements {
|
||||
for node, p := range c.assignments {
|
||||
if a, _, _ := strings.Cut(p.Decided.Address, "/"); a != "" {
|
||||
byAddress[a] = node
|
||||
}
|
||||
@@ -776,7 +776,7 @@ func (c *Composer) Machines() (byAddress map[string]string, hub string) {
|
||||
return byAddress, hub
|
||||
}
|
||||
|
||||
func (c *Composer) inOwnOf(p meshmoduleassigner.Placement) []string {
|
||||
func (c *Composer) inOwnOf(p meshmoduleassigner.Assignments) []string {
|
||||
var out []string
|
||||
for _, m := range p.Modules {
|
||||
if c.inOwn[m.Module] {
|
||||
@@ -874,14 +874,14 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
|
||||
_, nodeWanted := c.wants[agent.NodeModule]
|
||||
// What it runs inside its own composition and no want names at a build of the new form stays there, as the
|
||||
// controller's composed writer folds it (issue 1126); compose folds none a want names.
|
||||
own, placed := c.placements[c.env.Node]
|
||||
own, placed := c.assignments[c.env.Node]
|
||||
inOwn := c.inOwnOf(own)
|
||||
r := reader{c: c}
|
||||
c.mu.Unlock()
|
||||
if !placed {
|
||||
// The catalogue does not place it: it composes nothing, applies no composition made elsewhere, and this is no
|
||||
// refusal of what it runs (issue 1126).
|
||||
c.sayOnce("this node's key in the seat's placement state is gone: it composes nothing of its own, and keeps running what it runs")
|
||||
c.sayOnce("this node's key in the seat's assignment state is gone: it composes nothing of its own, and keeps running what it runs")
|
||||
c.paid() // nothing of its own to compose, so no pass is owed and no timer armed again
|
||||
return nil
|
||||
}
|
||||
@@ -1317,10 +1317,10 @@ func (r reader) VaultAuthority(context.Context) string {
|
||||
|
||||
var _ frombus.SignersReader = reader{}
|
||||
|
||||
// certificateWanted says whether a module the placement p puts on this node asks for a certificate for the machine's
|
||||
// certificateWanted says whether a module the assignment p puts on this node asks for a certificate for the machine's
|
||||
// internal name (its manifest's `certificate`), read from the indexer's manifests; a manifest that cannot be read is
|
||||
// taken as asking none, and the composition says what it misses.
|
||||
func (c *Composer) certificateWanted(ctx context.Context, r reader, p meshmoduleassigner.Placement) bool {
|
||||
func (c *Composer) certificateWanted(ctx context.Context, r reader, p meshmoduleassigner.Assignments) bool {
|
||||
for _, m := range p.Modules {
|
||||
c.mu.Lock()
|
||||
digest := c.modules[m.Module].Manifest
|
||||
@@ -1348,7 +1348,7 @@ func (c *Composer) certificateWanted(ctx context.Context, r reader, p meshmodule
|
||||
|
||||
// certifyIfWanted asks Certify once per serving key when a module placed here asks for a certificate and this node's
|
||||
// record holds none for its internal name and serving key from an authority it trusts (the one it holds, or the vault's).
|
||||
func (c *Composer) certifyIfWanted(ctx context.Context, r reader, p meshmoduleassigner.Placement) {
|
||||
func (c *Composer) certifyIfWanted(ctx context.Context, r reader, p meshmoduleassigner.Assignments) {
|
||||
if c.env.Certify == nil || !c.certificateWanted(ctx, r, p) {
|
||||
return
|
||||
}
|
||||
@@ -1377,11 +1377,11 @@ func (c *Composer) certifyIfWanted(ctx context.Context, r reader, p meshmoduleas
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r reader) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
|
||||
func (r reader) Assignments(context.Context) (map[string]meshmoduleassigner.Assignments, error) {
|
||||
r.c.mu.Lock()
|
||||
defer r.c.mu.Unlock()
|
||||
out := make(map[string]meshmoduleassigner.Placement, len(r.c.placements))
|
||||
for k, v := range r.c.placements {
|
||||
out := make(map[string]meshmoduleassigner.Assignments, len(r.c.assignments))
|
||||
for k, v := range r.c.assignments {
|
||||
out[k] = v
|
||||
}
|
||||
return out, nil
|
||||
@@ -1721,7 +1721,7 @@ func Transient(err error) bool {
|
||||
// ComposedToday is the digest of module composed at the build fp with today's settings and secrets, as this node would
|
||||
// compose it now beside its other modules at their wanted builds, the boxes it runs kept as compose keeps them, and whether
|
||||
// it composed (novox/hq ADR 0601 §1): the node agent compares it with what runs, to say whether a move to another build
|
||||
// changes the build alone. A module held, a node refused, or no placement: false, and the move is asked of a person.
|
||||
// changes the build alone. A module held, a node refused, or no assignment: false, and the move is asked of a person.
|
||||
func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string, bool) {
|
||||
c.mu.Lock()
|
||||
fps, kept := map[string]string{}, map[string]string{}
|
||||
@@ -1739,7 +1739,7 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string
|
||||
if mm, ok := c.modules[module]; !ok || mm.Fp2 != fp {
|
||||
kept[module] = fp
|
||||
}
|
||||
own, placed := c.placements[c.env.Node]
|
||||
own, placed := c.assignments[c.env.Node]
|
||||
inOwn := c.inOwnOf(own)
|
||||
c.mu.Unlock()
|
||||
if !placed {
|
||||
|
||||
@@ -8,13 +8,13 @@ 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"
|
||||
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/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"github.com/novox/mesh-host/internal/buckets"
|
||||
@@ -197,13 +197,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
okey, _ := meshcatalogclient.ManifestsKey(odigest)
|
||||
b.kv[buckets.Manifests+"/"+okey] = oman
|
||||
omod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64), Manifest: odigest, Repository: "novox/o", Commit: "c"})
|
||||
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}})
|
||||
have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||
want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true})
|
||||
nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true})
|
||||
b.put(t, buckets.Placement, "placement.n1", place)
|
||||
b.putAssigned(t, place)
|
||||
b.put(t, buckets.Have, "n1.node", have)
|
||||
oldHave, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: "build-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})
|
||||
b.put(t, buckets.Have, "n1.oldie", oldHave)
|
||||
@@ -211,7 +211,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
b.put(t, buckets.Modules, "oldie", omod)
|
||||
b.put(t, buckets.Wants, "want.n1.lemurs", want)
|
||||
b.put(t, buckets.Wants, "want.n1.node", nodeWant)
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
b.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
got := map[string]bool{}
|
||||
@@ -324,8 +324,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
for k, v := range c.wants {
|
||||
c2.wants[k] = v
|
||||
}
|
||||
for k, v := range c.placements {
|
||||
c2.placements[k] = v
|
||||
for k, v := range c.assignments {
|
||||
c2.assignments[k] = v
|
||||
}
|
||||
for k, v := range c.records {
|
||||
c2.records[k] = v
|
||||
@@ -441,9 +441,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
c3.modules[m] = meshcatalog.ModulesByModuleValue{Fp2: mfp, Manifest: dig, Repository: "novox/" + m, Commit: "c"}
|
||||
c3.wants[m] = meshwants.WantsByWantNodeModuleValue{Fp: mfp, Self: true}
|
||||
}
|
||||
p3 := c3.placements["n1"]
|
||||
p3 := c3.assignments["n1"]
|
||||
p3.Modules = append(append([]meshmoduleassigner.PlacedModule{}, p3.Modules...), meshmoduleassigner.PlacedModule{Module: "proxy"}, meshmoduleassigner.PlacedModule{Module: "de-spiegel"})
|
||||
c3.placements["n1"] = p3
|
||||
c3.assignments["n1"] = p3
|
||||
r3 := c3.records["n1"]
|
||||
r3.Capabilities = []string{"systemd", "container-runtime"}
|
||||
c3.records["n1"] = r3
|
||||
@@ -495,9 +495,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
// The indexer's verdict on the captured manifest (ADR 0578): the engine judges nothing, it takes this.
|
||||
Verdict: meshcatalogclient.VerdictRefused, Problems: []string{`bar item "shows" is none of them`}}
|
||||
c4.wants["power"] = meshwants.WantsByWantNodeModuleValue{Fp: powerFP, Self: true}
|
||||
p4 := c4.placements["n1"]
|
||||
p4 := c4.assignments["n1"]
|
||||
p4.Modules = append(append([]meshmoduleassigner.PlacedModule{}, p4.Modules...), meshmoduleassigner.PlacedModule{Module: "power"})
|
||||
c4.placements["n1"] = p4
|
||||
c4.assignments["n1"] = p4
|
||||
handed4 := map[string]agent.Update{}
|
||||
for _, u := range c4.compose(ctx) {
|
||||
handed4[u.Key] = u
|
||||
@@ -513,10 +513,10 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
// what runs nor the kept declaration: with nothing running (the kept read retired, Running empty) the node's own
|
||||
// still carries its resources, so retiring the read removes nothing of it.
|
||||
c5, told5 := fresh(env.Get)
|
||||
p5 := c5.placements["n1"]
|
||||
p5 := c5.assignments["n1"]
|
||||
p5.Modules = append(append([]meshmoduleassigner.PlacedModule{}, p5.Modules...), meshmoduleassigner.PlacedModule{Module: overlay.Name})
|
||||
p5.Decided.Endpoint = "n1.example:51820"
|
||||
c5.placements["n1"] = p5
|
||||
c5.assignments["n1"] = p5
|
||||
c5.inOwn[overlay.Name] = true
|
||||
nodeKey5, _ := compositions.NodeKey("n1")
|
||||
var own5 string
|
||||
@@ -597,12 +597,12 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
// secrets. A key written again with what it held, or a want whose words moved and not its build, makes none.
|
||||
worded, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true, Words: "lemurs is held at aaaaaaaaaaaa"})
|
||||
elsewhere, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: meshcatalogclient.SourcePrefix2 + strings.Repeat("e", 64), Self: true})
|
||||
// Research 072 row 37: the placement converted to one key per module and node moves nothing composed; a write in
|
||||
// keys makes its pass at its node key, the commit, and not at a module key written before it nor at the mirror.
|
||||
placeBase := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
// Research 072 row 37: the assignments kept as one key per module and node move nothing composed when written again; a
|
||||
// write in keys makes its pass at its node key, the commit, and not at a module key written before it.
|
||||
placeBase := meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}}
|
||||
placeKeys := meshmoduleassignerclient.NewKeys()
|
||||
keyRows := func(after meshmoduleassigner.Placement, what string) []struct {
|
||||
keyRows := func(after meshmoduleassigner.Assignments, what string) []struct {
|
||||
name, bucket, key string
|
||||
value []byte
|
||||
pass bool
|
||||
@@ -622,23 +622,25 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
name, bucket, key string
|
||||
value []byte
|
||||
pass bool
|
||||
}{what + ": " + w.Key, buckets.Placement, w.Key, w.Value, w.Commit && what != "converted"})
|
||||
}{what + ": " + w.Key, buckets.Assignments, w.Key, w.Value, w.Commit})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
_, _ = placeKeys.Put("placement.n1", place, 1, false)
|
||||
for k, v := range place {
|
||||
_, _ = placeKeys.Put(k, v, 1, false)
|
||||
}
|
||||
lemursMoved := placeBase
|
||||
lemursMoved.Modules = []meshmoduleassigner.PlacedModule{{Module: "lemurs", Values: map[string]any{"greeting": "hoi"}}, {Module: "oldie"}}
|
||||
var placementRows []struct {
|
||||
var assignmentRows []struct {
|
||||
name, bucket, key string
|
||||
value []byte
|
||||
pass bool
|
||||
}
|
||||
for _, step := range []struct {
|
||||
what string
|
||||
p meshmoduleassigner.Placement
|
||||
}{{"converted", placeBase}, {"lemurs moved", lemursMoved}, {"lemurs back", placeBase}} {
|
||||
placementRows = append(placementRows, keyRows(step.p, step.what)...)
|
||||
p meshmoduleassigner.Assignments
|
||||
}{{"lemurs moved", lemursMoved}, {"lemurs back", placeBase}} {
|
||||
assignmentRows = append(assignmentRows, keyRows(step.p, step.what)...)
|
||||
}
|
||||
for _, row := range append([]struct {
|
||||
name, bucket, key string
|
||||
@@ -648,11 +650,11 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
{"lemurs's want again", buckets.Wants, "want.n1.lemurs", want, false},
|
||||
{"the node's own want again", buckets.Wants, "want.n1.node", nodeWant, false},
|
||||
{"lemurs's want with other words", buckets.Wants, "want.n1.lemurs", worded, false},
|
||||
{"the placement again", buckets.Placement, "placement.n1", place, false},
|
||||
{"the assignment again", buckets.Assignments, "n1.node", place["n1.node"], false},
|
||||
{"lemurs's module again", buckets.Modules, "lemurs", mod, false},
|
||||
{"lemurs's want at another build", buckets.Wants, "want.n1.lemurs", elsewhere, true},
|
||||
{"lemurs's want back at its build", buckets.Wants, "want.n1.lemurs", want, true},
|
||||
}, placementRows...) {
|
||||
}, assignmentRows...) {
|
||||
before := passes()
|
||||
b.put(t, row.bucket, row.key, row.value)
|
||||
if !row.pass {
|
||||
@@ -698,18 +700,18 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tplace, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
tplace := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "tool"}}})
|
||||
tmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: next2, Manifest: tdigest, Repository: "novox/t", Commit: "c"})
|
||||
twant, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: next2, Self: true})
|
||||
building, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: "building"})
|
||||
bkey, _ := builds.BuilderBuildKey("tool", next2)
|
||||
tb.kv[builds.BuilderBuildsBucket+"/"+bkey] = building
|
||||
tb.put(t, buckets.Placement, "placement.n1", tplace)
|
||||
tb.putAssigned(t, tplace)
|
||||
tb.put(t, buckets.Have, "n1.node", have)
|
||||
tb.put(t, buckets.Modules, "tool", tmod)
|
||||
tb.put(t, buckets.Wants, "want.n1.tool", twant)
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules,
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, buckets.Modules,
|
||||
ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
tb.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
@@ -777,18 +779,19 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
b.kv[buckets.Manifests+"/"+kkey] = kman
|
||||
b.mu.Unlock()
|
||||
kmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: kdigest, Repository: "novox/k", Commit: "c"})
|
||||
both, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
both := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}, {Module: "keeper"}}})
|
||||
b.put(t, buckets.Modules, "keeper", kmod)
|
||||
// n1 is held in keys per module and node since the rows above, which a legacy key no longer moves.
|
||||
bothPlaced, _ := meshmoduleassignerclient.DecodePlacement(both)
|
||||
bothWrites, err := placeKeys.Writes("n1", &bothPlaced)
|
||||
// n1 is held in keys per module and node, which the writes below move.
|
||||
bothPlaced, _ := meshmoduleassignerclient.AssignmentsOf(both)
|
||||
bothNode := bothPlaced["n1"]
|
||||
bothWrites, err := placeKeys.Writes("n1", &bothNode)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, w := range bothWrites {
|
||||
if w.Value != nil {
|
||||
b.put(t, buckets.Placement, w.Key, w.Value)
|
||||
b.put(t, buckets.Assignments, w.Key, w.Value)
|
||||
}
|
||||
}
|
||||
b.put(t, buckets.Wants, "want.n1.keeper", want)
|
||||
@@ -889,8 +892,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
for k, v := range c.wants {
|
||||
again.wants[k] = v
|
||||
}
|
||||
for k, v := range c.placements {
|
||||
again.placements[k] = v
|
||||
for k, v := range c.assignments {
|
||||
again.assignments[k] = v
|
||||
}
|
||||
for k, v := range c.records {
|
||||
again.records[k] = v
|
||||
@@ -912,7 +915,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
// Then the catalogue no longer places the node: it composes nothing of its own, and owes no pass either, so
|
||||
// no timer is armed again (rev-seed-same-3, round 1).
|
||||
again.mu.Lock()
|
||||
clear(again.placements)
|
||||
clear(again.assignments)
|
||||
again.mu.Unlock()
|
||||
if again.compose(ctx); again.owed.Load() {
|
||||
t.Errorf("%s: an unplaced node still owes a pass", row.name)
|
||||
@@ -1034,17 +1037,17 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
if asked, _ := json.Marshal(sent["credentials"]); !strings.Contains(string(asked), `"role":"consumer"`) || !strings.Contains(string(asked), `"role":"provider"`) {
|
||||
t.Errorf("the vault was asked %s", asked)
|
||||
}
|
||||
// ADR 0559 §4: the node answers placement candidates in shadow, placed or not, applying nothing and handing no
|
||||
// composition: the same; a module the group adds, its secret not yet issued; a module removed; a placement it cannot
|
||||
// ADR 0559 §4: the node answers assignment candidates in shadow, placed or not, applying nothing and handing no
|
||||
// composition: the same; a module the group adds, its secret not yet issued; a module removed; a assignment it cannot
|
||||
// compose; a group whose keys went is no longer answered. What runs is never composed from a candidate: the
|
||||
// Composer does not read the state.
|
||||
for _, in := range New(env).inputs() {
|
||||
if in.bucket == buckets.PlacementCandidates {
|
||||
t.Fatal("the node's own composer reads placement candidates")
|
||||
if in.bucket == buckets.ProposalCandidates {
|
||||
t.Fatal("the node's own composer reads assignment candidates")
|
||||
}
|
||||
}
|
||||
sb := newPatternBus()
|
||||
trunk := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}}
|
||||
trunk := meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}}
|
||||
mods := map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"},
|
||||
"keeper": {Fp2: fp, Manifest: kdigest, Repository: "novox/k", Commit: "c"}}
|
||||
readOnly := &fakeBus{kv: map[string][]byte{buckets.Manifests + "/" + key: man, buckets.Manifests + "/" + kkey: kman}}
|
||||
@@ -1058,7 +1061,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
return json.RawMessage(`{"node":"n1","key":"sk","own":[]}`), nil // nothing issued for keeper: it is not placed here
|
||||
}
|
||||
// What n1 runs: lemurs and its own composition, as its composer composes them from the trunk.
|
||||
running, _, _, err := frombus.Compose(ctx, fixedReader{placements: map[string]meshmoduleassigner.Placement{"n1": trunk},
|
||||
running, _, _, err := frombus.Compose(ctx, fixedReader{assignments: map[string]meshmoduleassigner.Assignments{"n1": trunk},
|
||||
records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}},
|
||||
modules: mods, manifests: map[string][]byte{digest: []byte(lemurs), kdigest: []byte(keeper)}}, frombus.Mesh{}, frombus.Scope{Node: "n1"}, map[string]string{"lemurs": fp})
|
||||
if err != nil {
|
||||
@@ -1071,7 +1074,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
}
|
||||
return out
|
||||
}
|
||||
// The controller's compositions at n1's wants, in n1's composition bucket: what a head deleting its placement
|
||||
// The controller's compositions at n1's wants, in n1's composition bucket: what a head deleting its assignment
|
||||
// returns it to (the seed's revert); here the ones it runs.
|
||||
composedBucket, _ := compositions.BucketFor("n1")
|
||||
for key, c := range running {
|
||||
@@ -1089,15 +1092,15 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
defer scancel()
|
||||
// Review of L2b, round 1, finding 2: the candidates' state is not there when the node starts (a delivery before the
|
||||
// indexer made it): the shadow tries again, and answers once it appears.
|
||||
sb.missing(buckets.PlacementCandidates, true)
|
||||
sb.missing(buckets.ProposalCandidates, true)
|
||||
kept := make(chan struct{})
|
||||
go func() { sh.Keep(sctx, func(int) time.Duration { return 5 * time.Millisecond }); close(kept) }()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
sb.missing(buckets.PlacementCandidates, false)
|
||||
sb.missing(buckets.ProposalCandidates, false)
|
||||
sb.made(6)
|
||||
rec, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
tp, _ := meshmoduleassignerclient.EncodePlacement(trunk)
|
||||
sb.put(buckets.Placement, "placement.n1", tp, 1)
|
||||
tp := assignedValues(t, trunk)
|
||||
sb.putAssigned(tp, 1)
|
||||
sb.put(buckets.Have, "n1.node", rec, 1)
|
||||
for name, m := range mods {
|
||||
raw, _ := meshcatalogclient.EncodeModule(m)
|
||||
@@ -1124,33 +1127,33 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
for _, m := range modules {
|
||||
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
|
||||
}
|
||||
raw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: group, Placement: &p})
|
||||
raw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: group, Assignments: &p})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k := meshmoduleassigner.CandidatesByProposalNodeKey(group, "n1")
|
||||
sb.put(buckets.PlacementCandidates, k, raw, rev)
|
||||
sb.put(buckets.ProposalCandidates, k, raw, rev)
|
||||
}
|
||||
candidate("same", 3, "lemurs")
|
||||
candidate("adds", 4, "lemurs", "keeper")
|
||||
candidate("drops", 5)
|
||||
candidate("ghost", 6, "lemurs", "ghost")
|
||||
// A head deleting n1's placement file (the seed's revert, mesh-catalog #526): n1 returns to the controller's
|
||||
// A head deleting n1's assignment file (the seed's revert, mesh-catalog #526): n1 returns to the controller's
|
||||
// compositions, answered as such, not as every module removed.
|
||||
if raw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "deletes"}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
k := meshmoduleassigner.CandidatesByProposalNodeKey("deletes", "n1")
|
||||
sb.put(buckets.PlacementCandidates, k, raw, 7)
|
||||
sb.put(buckets.ProposalCandidates, k, raw, 7)
|
||||
}
|
||||
// A proposal that changes another node's key and reaches n1's composition (ADR 0567 §3): n1's candidate holds no
|
||||
// placement of its own change (Reached, nil), and n1 answers against its current placement, never as one whose key
|
||||
// assignment of its own change (Reached, nil), and n1 answers against its current assignment, never as one whose key
|
||||
// the proposal takes away.
|
||||
if raw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "reach", Reached: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
k := meshmoduleassigner.CandidatesByProposalNodeKey("reach", "n1")
|
||||
sb.put(buckets.PlacementCandidates, k, raw, 8)
|
||||
sb.put(buckets.ProposalCandidates, k, raw, 8)
|
||||
}
|
||||
sb.caught()
|
||||
var answers map[string]nodeengine.CandidateAnswer
|
||||
@@ -1199,7 +1202,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
return a.Verdict == nodeengine.CandidateAnswerVerdictSame && a.Returns == "" && a.Revision == 8 && len(a.Removed) == 0 &&
|
||||
fmt.Sprint(a.Same) == "[lemurs node]"
|
||||
},
|
||||
// Issue 1126, A7: a candidate deleting the node's placement is refused, never answered as a return to the
|
||||
// Issue 1126, A7: a candidate deleting the node's assignment is refused, never answered as a return to the
|
||||
// controller's compositions: no composition made elsewhere is applied, so the node would compose nothing.
|
||||
"deletes": func(a nodeengine.CandidateAnswer) bool {
|
||||
return a.Verdict == nodeengine.CandidateAnswerVerdictRefused && a.Returns == "" && a.Revision == 7 &&
|
||||
@@ -1218,7 +1221,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
// Issue 1126, 2026-10-28 (shanks answered probe/1126-seed-7 at revision 84 every 13-17 s): a candidate revision is
|
||||
// answered once. A module's have whose health or words alone move (each look rewrites them), and a placement, a
|
||||
// answered once. A module's have whose health or words alone move (each look rewrites them), and a assignment, a
|
||||
// module or a config value written again with the value it held, answer nothing again; a new revision answers its
|
||||
// group alone, and what runs moving answers every group once.
|
||||
answering := func() int {
|
||||
@@ -1226,7 +1229,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
defer readOnly.mu.Unlock()
|
||||
n := 0
|
||||
for _, l := range readOnly.said {
|
||||
if strings.HasPrefix(l, "answering placement candidate ") {
|
||||
if strings.HasPrefix(l, "answering assignment candidate ") {
|
||||
n++
|
||||
}
|
||||
}
|
||||
@@ -1255,7 +1258,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
settled := answering()
|
||||
lemursHave("its tool lemurs_health: no answer within 10s", 11)
|
||||
lemursHave("its tool lemurs_health: no answer within 20s", 12)
|
||||
sb.put(buckets.Placement, "placement.n1", tp, 2)
|
||||
sb.putAssigned(tp, 2)
|
||||
lraw, _ := meshcatalogclient.EncodeModule(mods["lemurs"])
|
||||
sb.put(buckets.Modules, "lemurs", lraw, 2)
|
||||
drain()
|
||||
@@ -1279,7 +1282,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if got := lastSaid("answering placement candidate same "); got != "answering placement candidate same (revision 8; woken by the candidate)" {
|
||||
if got := lastSaid("answering assignment candidate same "); got != "answering assignment candidate same (revision 8; woken by the candidate)" {
|
||||
t.Errorf("a new revision's answer said %q", got)
|
||||
}
|
||||
settled = answering()
|
||||
@@ -1290,7 +1293,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
if got := answering(); got != settled+len(answers) {
|
||||
t.Errorf("what runs moved, and %d answer(s) were composed again for %d group(s)", got-settled, len(answers))
|
||||
}
|
||||
if got := lastSaid("answering placement candidate drops "); got != "answering placement candidate drops (revision 5; woken by haves)" {
|
||||
if got := lastSaid("answering assignment candidate drops "); got != "answering assignment candidate drops (revision 5; woken by haves)" {
|
||||
t.Errorf("an answer woken by what runs moving said %q", got)
|
||||
}
|
||||
// A read the bus did not answer is no answer to keep: the group is answered again after a bound, though nothing moved.
|
||||
@@ -1305,41 +1308,41 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
return a["adds"].Revision == 9 && a["adds"].Verdict == nodeengine.CandidateAnswerVerdictDiffers
|
||||
})
|
||||
drain()
|
||||
if got := lastSaid("answering placement candidate adds "); got != "answering placement candidate adds (revision 9; woken by a retry after a read the bus did not answer)" {
|
||||
if got := lastSaid("answering assignment candidate adds "); got != "answering assignment candidate adds (revision 9; woken by a retry after a read the bus did not answer)" {
|
||||
t.Errorf("an answer composed again after the bus did not answer said %q", got)
|
||||
}
|
||||
k := meshmoduleassigner.CandidatesByProposalNodeKey("same", "n1")
|
||||
sb.del(buckets.PlacementCandidates, k)
|
||||
sb.del(buckets.ProposalCandidates, k)
|
||||
waitFor("the deleted group's answer stays", func(a map[string]nodeengine.CandidateAnswer) bool { _, ok := a["same"]; return !ok && len(a) == 5 })
|
||||
|
||||
// Review of L2b, round 2: a watch ends while the link stays; a group's key is deleted before the shadow watches
|
||||
// again. The new run reads afresh, so that group is no longer answered.
|
||||
sb.missing(buckets.PlacementCandidates, true)
|
||||
sb.missing(buckets.ProposalCandidates, true)
|
||||
sb.end(buckets.Modules)
|
||||
dropsKey := meshmoduleassigner.CandidatesByProposalNodeKey("drops", "n1")
|
||||
sb.del(buckets.PlacementCandidates, dropsKey)
|
||||
sb.del(buckets.ProposalCandidates, dropsKey)
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
sb.missing(buckets.PlacementCandidates, false)
|
||||
sb.missing(buckets.ProposalCandidates, false)
|
||||
waitFor("a group deleted while the shadow was down is still answered", func(a map[string]nodeengine.CandidateAnswer) bool {
|
||||
_, ok := a["drops"]
|
||||
return !ok && len(a) == 4
|
||||
})
|
||||
|
||||
// Issue 1126, A7 AC1: a placement that cannot be read composes nothing and says why; the node never applies a
|
||||
// composition made elsewhere instead. (A placement read without the node's key composes nothing either: above.)
|
||||
// Issue 1126, A7 AC1: a assignment that cannot be read composes nothing and says why; the node never applies a
|
||||
// composition made elsewhere instead. (A assignment read without the node's key composes nothing either: above.)
|
||||
{
|
||||
refused := newFakeBus()
|
||||
env := refused.env()
|
||||
watch := env.Watch
|
||||
env.Watch = func(ctx context.Context, bucket, pattern string) (<-chan agent.Update, error) {
|
||||
if bucket == buckets.Placement {
|
||||
if bucket == buckets.Assignments {
|
||||
return nil, fmt.Errorf("nats: Permissions Violation for Subscription to %s", bucket)
|
||||
}
|
||||
return watch(ctx, bucket, pattern)
|
||||
}
|
||||
out, err := New(env).Watch(ctx, "")
|
||||
if out != nil || err == nil || !strings.Contains(err.Error(), buckets.Placement) || !strings.Contains(err.Error(), "cannot be watched") {
|
||||
t.Errorf("an unreadable placement: composed %v, said %v", out != nil, err)
|
||||
if out != nil || err == nil || !strings.Contains(err.Error(), buckets.Assignments) || !strings.Contains(err.Error(), "cannot be watched") {
|
||||
t.Errorf("an unreadable assignment: composed %v, said %v", out != nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1359,20 +1362,20 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
defer ncancel()
|
||||
go func() { _ = nsh.Run(nctx) }()
|
||||
nb.made(6)
|
||||
nb.put(buckets.Placement, "placement.n1", tp, 1)
|
||||
nb.putAssigned(tp, 1)
|
||||
nb.put(buckets.Have, "n1.node", rec, 1)
|
||||
for name, m := range mods {
|
||||
raw, _ := meshcatalogclient.EncodeModule(m)
|
||||
nb.put(buckets.Modules, name, raw, 1)
|
||||
}
|
||||
sameRaw, _ := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "same", Placement: &trunk})
|
||||
sameRaw, _ := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "same", Assignments: &trunk})
|
||||
sameKey := meshmoduleassigner.CandidatesByProposalNodeKey("same", "n1")
|
||||
nb.put(buckets.PlacementCandidates, sameKey, sameRaw, 3)
|
||||
nb.put(buckets.ProposalCandidates, sameKey, sameRaw, 3)
|
||||
// Issue 1126, T3: an older group (a closed pull request's candidate, rev 2) is answered after the newest.
|
||||
oldRaw, _ := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "older", Placement: &trunk})
|
||||
oldRaw, _ := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "older", Assignments: &trunk})
|
||||
oldKey := meshmoduleassigner.CandidatesByProposalNodeKey("older", "n1")
|
||||
nb.put(buckets.PlacementCandidates, oldKey, oldRaw, 2)
|
||||
for _, bucket := range []string{buckets.PlacementCandidates, buckets.Placement, buckets.Have, buckets.Modules, builds.BuilderBuildsBucket} {
|
||||
nb.put(buckets.ProposalCandidates, oldKey, oldRaw, 2)
|
||||
for _, bucket := range []string{buckets.ProposalCandidates, buckets.Assignments, buckets.Have, buckets.Modules, builds.BuilderBuildsBucket} {
|
||||
nb.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
select {
|
||||
@@ -1384,7 +1387,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
t.Fatal("a shadow not ready after its wait answered nothing")
|
||||
}
|
||||
nmu.Lock()
|
||||
if !strings.Contains(strings.Join(nsaid, "\n"), "this node answers no placement candidate yet: the node has not read "+ResolvedBucket) {
|
||||
if !strings.Contains(strings.Join(nsaid, "\n"), "this node answers no assignment candidate yet: the node has not read "+ResolvedBucket) {
|
||||
t.Errorf("a shadow not ready said %q", nsaid)
|
||||
}
|
||||
nmu.Unlock()
|
||||
@@ -1408,13 +1411,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
// Issue 1126, T2: an answer says when it starts and when it ends (and, every minute between, what it waits on).
|
||||
nmu.Lock()
|
||||
said := strings.Join(nsaid, "\n")
|
||||
for _, line := range []string{"the answer to same took", "answering placement candidate same (revision 3; woken by its first answer since the shadow began)",
|
||||
"answered placement candidate same in", "answering placement candidate older (revision 2;"} {
|
||||
for _, line := range []string{"the answer to same took", "answering assignment candidate same (revision 3; woken by its first answer since the shadow began)",
|
||||
"answered assignment candidate same in", "answering assignment candidate older (revision 2;"} {
|
||||
if !strings.Contains(said, line) {
|
||||
t.Errorf("%q was not said: %q", line, nsaid)
|
||||
}
|
||||
}
|
||||
if strings.Index(said, "answering placement candidate same") > strings.Index(said, "answering placement candidate older") {
|
||||
if strings.Index(said, "answering assignment candidate same") > strings.Index(said, "answering assignment candidate older") {
|
||||
t.Errorf("the older group was answered first: %q", nsaid)
|
||||
}
|
||||
nmu.Unlock()
|
||||
@@ -1454,7 +1457,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
// trunk's.
|
||||
inOwnRunning := []compositions.Composed{{Node: "n1", Module: "lemurs", FP: fp}}
|
||||
other := "src2:" + strings.Repeat("9", 64)
|
||||
gotFps := shadowBuilds(inOwnRunning, meshmoduleassigner.Placement{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "nats"}, {Module: "keeper"}}},
|
||||
gotFps := shadowBuilds(inOwnRunning, meshmoduleassigner.Assignments{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "nats"}, {Module: "keeper"}}},
|
||||
map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Fp2: other}, "nats": {Fp2: other}, "keeper": {Fp2: other}}, []string{"nats"})
|
||||
if fmt.Sprint(gotFps) != fmt.Sprint(map[string]string{"lemurs": fp, "keeper": other}) {
|
||||
t.Errorf("shadow builds %v; want lemurs at what runs, keeper at the trunk's, nats at none (folded)", gotFps)
|
||||
@@ -1519,12 +1522,12 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
cc.manifests[cdigest] = []byte(withCert)
|
||||
cc.modules = map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Manifest: digest}, "web": {Manifest: cdigest}}
|
||||
cc.records["n1"] = nodeengine.NodeRecord{Node: "n1", ServingKey: base64.StdEncoding.EncodeToString(make([]byte, 32))}
|
||||
onlyLemurs := meshmoduleassigner.Placement{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}}
|
||||
onlyLemurs := meshmoduleassigner.Assignments{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}}
|
||||
cc.certifyIfWanted(context.Background(), reader{c: cc}, onlyLemurs)
|
||||
if certAsks != 0 {
|
||||
t.Errorf("no module asks for a certificate, and the vault was asked %d times", certAsks)
|
||||
}
|
||||
withWeb := meshmoduleassigner.Placement{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "web"}}}
|
||||
withWeb := meshmoduleassigner.Assignments{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "web"}}}
|
||||
cc.certifyIfWanted(context.Background(), reader{c: cc}, withWeb)
|
||||
cc.certifyIfWanted(context.Background(), reader{c: cc}, withWeb)
|
||||
if certAsks != 1 {
|
||||
@@ -1665,14 +1668,14 @@ func (b *patternBus) caught() {
|
||||
|
||||
// fixedReader is frombus's Reader over values held, for composing what a node runs.
|
||||
type fixedReader struct {
|
||||
placements map[string]meshmoduleassigner.Placement
|
||||
records map[string]nodeengine.NodeRecord
|
||||
modules map[string]meshcatalog.ModulesByModuleValue
|
||||
manifests map[string][]byte
|
||||
assignments map[string]meshmoduleassigner.Assignments
|
||||
records map[string]nodeengine.NodeRecord
|
||||
modules map[string]meshcatalog.ModulesByModuleValue
|
||||
manifests map[string][]byte
|
||||
}
|
||||
|
||||
func (r fixedReader) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
|
||||
return r.placements, nil
|
||||
func (r fixedReader) Assignments(context.Context) (map[string]meshmoduleassigner.Assignments, error) {
|
||||
return r.assignments, nil
|
||||
}
|
||||
func (r fixedReader) Records(context.Context) (map[string]nodeengine.NodeRecord, error) {
|
||||
return r.records, nil
|
||||
@@ -1791,18 +1794,18 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}})
|
||||
have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||
want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true})
|
||||
nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true})
|
||||
b.put(t, buckets.Placement, "placement.n1", place)
|
||||
b.putAssigned(t, place)
|
||||
b.put(t, buckets.Have, "n1.node", have)
|
||||
b.put(t, buckets.Modules, "lemurs", mod)
|
||||
b.put(t, buckets.Wants, "want.n1.lemurs", want)
|
||||
b.put(t, buckets.Wants, "want.n1.node", nodeWant)
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
b.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
handed := map[string]bool{}
|
||||
@@ -1872,18 +1875,18 @@ func TestAHeldOwnCompositionThatWasHandedIsNotDropped(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}})
|
||||
have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||
want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true})
|
||||
nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true})
|
||||
b.put(t, buckets.Placement, "placement.n1", place)
|
||||
b.putAssigned(t, place)
|
||||
b.put(t, buckets.Have, "n1.node", have)
|
||||
b.put(t, buckets.Modules, "lemurs", mod)
|
||||
b.put(t, buckets.Wants, "want.n1.lemurs", want)
|
||||
b.put(t, buckets.Wants, "want.n1.node", nodeWant)
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
b.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
// The list is read: the node's own composition is handed. Then the reader cannot act again (it restarted and has
|
||||
@@ -1999,13 +2002,13 @@ func TestTheHolderOfTheCatalogIsStartedFromTheOwnCompositionBeforeTheListIsReadA
|
||||
b.put(t, buckets.Have, hk, hv)
|
||||
placed = append(placed, meshmoduleassigner.PlacedModule{Module: m})
|
||||
}
|
||||
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: placed})
|
||||
place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: placed})
|
||||
have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true})
|
||||
b.put(t, buckets.Placement, "placement.n1", place)
|
||||
b.putAssigned(t, place)
|
||||
b.put(t, buckets.Have, "n1.node", have)
|
||||
b.put(t, buckets.Wants, "want.n1.node", nodeWant)
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
|
||||
b.send(bucket, agent.Update{Caught: true})
|
||||
}
|
||||
servicesIn := func(u agent.Update) []string {
|
||||
@@ -2056,3 +2059,28 @@ func TestACompositionHoldsWhenTheSeatListReaderCouldNotStart(t *testing.T) {
|
||||
t.Errorf("held %v, comps %v: a reader that did not start must hold the node's own composition", held, comps)
|
||||
}
|
||||
}
|
||||
|
||||
// assignedValues is every key the assigner writes for p: the node's, `<node>.node`, and each module's, `<node>.<module>`.
|
||||
func assignedValues(t *testing.T, p meshmoduleassigner.Assignments) map[string][]byte {
|
||||
t.Helper()
|
||||
vals, err := meshmoduleassignerclient.AssignmentsValues(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
// putAssigned writes the keys of one node's assignments into the assignments state.
|
||||
func (b *fakeBus) putAssigned(t *testing.T, vals map[string][]byte) {
|
||||
t.Helper()
|
||||
for _, k := range slices.Sorted(maps.Keys(vals)) {
|
||||
b.put(t, buckets.Assignments, k, vals[k])
|
||||
}
|
||||
}
|
||||
|
||||
// putAssigned sends the keys of one node's assignments, each at the revision.
|
||||
func (b *patternBus) putAssigned(vals map[string][]byte, rev uint64) {
|
||||
for _, k := range slices.Sorted(maps.Keys(vals)) {
|
||||
b.put(buckets.Assignments, k, vals[k], rev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"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/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"github.com/novox/mesh-host/internal/buckets"
|
||||
"github.com/novox/mesh-host/internal/compose/frombus"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
@@ -23,13 +23,13 @@ import (
|
||||
"github.com/novox/mesh-host/internal/agent"
|
||||
)
|
||||
|
||||
// Shadow is the node's answer to placement candidates (novox/hq ADR 0559 §4, issue 1126): for every proposal with a key in
|
||||
// the placement seat's state `candidates`, it composes this node in shadow from the trunk's placement with the proposal's
|
||||
// Shadow is the node's answer to assignment candidates (novox/hq ADR 0559 §4, issue 1126): for every proposal with a key in
|
||||
// the assignment seat's state `candidates`, it composes this node in shadow from the trunk's assignment with the proposal's
|
||||
// keys laid over it and its own inputs (the sealed values and boxes it holds among them), compares the result with what
|
||||
// the node runs, and hands the answers to be written into the node's own have key, `<node>.node`, under `candidates`.
|
||||
// It applies nothing and hands no composition: what runs is still composed from `placement` alone, by the Composer.
|
||||
// It applies nothing and hands no composition: what runs is still composed from `assignment` alone, by the Composer.
|
||||
//
|
||||
// **Event-driven, like the Composer.** It watches every candidate key, the trunk's placement, every node's record, the
|
||||
// **Event-driven, like the Composer.** It watches every candidate key, the trunk's assignment, every node's record, the
|
||||
// indexer's modules, this node's config values, and this node's module haves (what it runs moved); a burst of changes
|
||||
// answers once. It runs whether or not the node is placed yet, against what it runs. An answer is handed only when it changes, and a proposal's answer is dropped when its
|
||||
// keys go.
|
||||
@@ -72,7 +72,7 @@ type answeredAt struct {
|
||||
}
|
||||
|
||||
// inputWords name the shadow's inputs, in Run's order, as an answer says which of them woke it.
|
||||
var inputWords = [...]string{"the candidate", "placement", "records", "modules", "values", "haves"}
|
||||
var inputWords = [...]string{"the candidate", "assignment", "records", "modules", "values", "haves"}
|
||||
|
||||
// wokenBy says what moved between the answer a proposal stands at (had, when done) and the one about to be composed (at):
|
||||
// its first answer, a retry, its candidate's revisions, or the inputs that changed, by their words.
|
||||
@@ -130,7 +130,7 @@ func (s *Shadow) Keep(ctx context.Context, backoff func(try int) time.Duration)
|
||||
why = err.Error()
|
||||
}
|
||||
if why != said {
|
||||
s.c.env.Say("this node answers no placement candidate for now, and tries again: " + why)
|
||||
s.c.env.Say("this node answers no assignment candidate for now, and tries again: " + why)
|
||||
said = why
|
||||
}
|
||||
select {
|
||||
@@ -161,8 +161,8 @@ func (s *Shadow) Run(ctx context.Context) error {
|
||||
node := c.env.Node
|
||||
// In inputWords' order.
|
||||
ins := []input{
|
||||
{buckets.PlacementCandidates, ">", nil, nil},
|
||||
{buckets.Placement, ">", (*Composer).takePlacement, nil},
|
||||
{buckets.ProposalCandidates, ">", nil, nil},
|
||||
{buckets.Assignments, ">", (*Composer).takeAssignments, nil},
|
||||
{buckets.Have, "*." + nodeengineclient.NodeWord, nil, nil},
|
||||
{buckets.Modules, ">", (*Composer).takeModule, nil},
|
||||
{ResolvedBucket, node + ".>", (*Composer).takeValue, nil},
|
||||
@@ -178,7 +178,7 @@ func (s *Shadow) Run(ctx context.Context) error {
|
||||
for i, in := range ins {
|
||||
ch, err := c.env.Watch(ctx, in.bucket, in.pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the node answers placement candidates from %s, which cannot be watched: %w", in.bucket, err)
|
||||
return fmt.Errorf("the node answers assignment candidates from %s, which cannot be watched: %w", in.bucket, err)
|
||||
}
|
||||
go func(i int, ch <-chan agent.Update) {
|
||||
for u := range ch {
|
||||
@@ -216,7 +216,7 @@ func (s *Shadow) Run(ctx context.Context) error {
|
||||
continue
|
||||
case t := <-merged:
|
||||
if !t.ok {
|
||||
return fmt.Errorf("the watch of %s ended; the node answers no placement candidate until its agent starts again", ins[t.in].bucket)
|
||||
return fmt.Errorf("the watch of %s ended; the node answers no assignment candidate until its agent starts again", ins[t.in].bucket)
|
||||
}
|
||||
// Only a change of what an answer is composed from answers again: a key written again with the value it held,
|
||||
// this node's own key (its answers), or a module's have whose health or words moved but not what it runs, is
|
||||
@@ -258,7 +258,7 @@ func (s *Shadow) Run(ctx context.Context) error {
|
||||
if s.owed {
|
||||
wait := LaterWait(tries)
|
||||
tries++
|
||||
c.env.Say(fmt.Sprintf("this node answers again in %s the placement candidates whose answer met a bus that did not answer", wait))
|
||||
c.env.Say(fmt.Sprintf("this node answers again in %s the assignment candidates whose answer met a bus that did not answer", wait))
|
||||
again = time.After(wait)
|
||||
} else {
|
||||
tries, again = 0, nil
|
||||
@@ -300,7 +300,7 @@ func (s *Shadow) takeCandidate(u agent.Update) bool {
|
||||
return true
|
||||
}
|
||||
c, err := meshmoduleassigner.DecodeCandidatesByProposalNodeValue(u.Value)
|
||||
if err != nil || c.Proposal != proposal || (c.Placement != nil && c.Placement.Node != node) {
|
||||
if err != nil || c.Proposal != proposal || (c.Assignments != nil && c.Assignments.Node != node) {
|
||||
// Not one: read as gone.
|
||||
if _, ok := s.candidates[proposal][node]; ok {
|
||||
delete(s.candidates[proposal], node)
|
||||
@@ -345,7 +345,7 @@ func (s *Shadow) notReady(ctx context.Context, ins []input, caught []bool) {
|
||||
}
|
||||
}
|
||||
why := fmt.Sprintf("the node has not read %s after %s; it answers once it has", strings.Join(waits, ", "), notReadyAfter)
|
||||
s.c.env.Say("this node answers no placement candidate yet: " + why)
|
||||
s.c.env.Say("this node answers no assignment candidate yet: " + why)
|
||||
s.mu.Lock()
|
||||
next := map[string]nodeengine.CandidateAnswer{}
|
||||
for proposal, keys := range s.candidates {
|
||||
@@ -428,7 +428,7 @@ func (s *Shadow) answerAll(ctx context.Context) {
|
||||
given[g] = a
|
||||
}
|
||||
s.answer(ctx, given)
|
||||
c.env.Say(fmt.Sprintf("%s answered %d placement candidate proposal(s)", node, len(given)))
|
||||
c.env.Say(fmt.Sprintf("%s answered %d assignment candidate proposal(s)", node, len(given)))
|
||||
}
|
||||
}
|
||||
for _, l := range proposals {
|
||||
@@ -444,7 +444,7 @@ func (s *Shadow) answerAll(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
began := time.Now()
|
||||
c.env.Say(fmt.Sprintf("answering placement candidate %s (revision %d; woken by %s)", proposal, l.rev, wokenBy(at, done, retry, l.at)))
|
||||
c.env.Say(fmt.Sprintf("answering assignment candidate %s (revision %d; woken by %s)", proposal, l.rev, wokenBy(at, done, retry, l.at)))
|
||||
stop := make(chan struct{})
|
||||
go func() {
|
||||
tick := time.NewTicker(progressEvery)
|
||||
@@ -474,10 +474,10 @@ func (s *Shadow) answerAll(ctx context.Context) {
|
||||
if ok {
|
||||
a = judge.FitAnswer(a, engcomp.MaxAnswerBytes)
|
||||
next[proposal] = a
|
||||
c.env.Say(fmt.Sprintf("answered placement candidate %s in %s: %s", proposal, took.Round(time.Millisecond), a.Verdict))
|
||||
c.env.Say(fmt.Sprintf("answered assignment candidate %s in %s: %s", proposal, took.Round(time.Millisecond), a.Verdict))
|
||||
} else {
|
||||
delete(next, proposal)
|
||||
c.env.Say(fmt.Sprintf("placement candidate %s does not place this node: nothing to answer", proposal))
|
||||
c.env.Say(fmt.Sprintf("assignment candidate %s does not place this node: nothing to answer", proposal))
|
||||
}
|
||||
s.mu.Lock()
|
||||
if provisional {
|
||||
@@ -507,12 +507,12 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (nodeengin
|
||||
defer c.waiting("composing")()
|
||||
s.mu.Lock()
|
||||
keys := s.candidates[proposal]
|
||||
given := make(map[string]*meshmoduleassigner.Placement, len(keys))
|
||||
given := make(map[string]*meshmoduleassigner.Assignments, len(keys))
|
||||
for n, k := range keys {
|
||||
if k.c.Reached && k.c.Placement == nil {
|
||||
if k.c.Reached && k.c.Assignments == nil {
|
||||
continue // reached, with no key of its own: nothing laid over it
|
||||
}
|
||||
given[n] = k.c.Placement
|
||||
given[n] = k.c.Assignments
|
||||
}
|
||||
// The revision answered is the node's own candidate key's; a node with none is not asked.
|
||||
own, mine := keys[node]
|
||||
@@ -534,26 +534,26 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (nodeengin
|
||||
running = c.env.Running()
|
||||
}
|
||||
if p, ok := given[node]; ok && p == nil && !own.c.Reached {
|
||||
// The proposal deletes this node's placement key: the node would compose nothing and keep running what it runs,
|
||||
// The proposal deletes this node's assignment key: the node would compose nothing and keep running what it runs,
|
||||
// since no composition made elsewhere is applied (issue 1126: the controller ended by its unassignment). Refused,
|
||||
// so the catalogue does not take a node's placement away by a merge.
|
||||
// so the catalogue does not take a node's assignment away by a merge.
|
||||
return nodeengine.CandidateAnswer{Proposal: proposal, Revision: int64(rev), Verdict: nodeengine.CandidateAnswerVerdictRefused,
|
||||
Refused: "the proposal takes this node's placement away: it then composes nothing, and no composition made " +
|
||||
Refused: "the proposal takes this node's assignment away: it then composes nothing, and no composition made " +
|
||||
"elsewhere is applied, so what runs here would be followed by nothing"}, true
|
||||
}
|
||||
|
||||
placement := given[node]
|
||||
if placement == nil {
|
||||
assignment := given[node]
|
||||
if assignment == nil {
|
||||
c.mu.Lock()
|
||||
p := c.placements[node]
|
||||
p := c.assignments[node]
|
||||
c.mu.Unlock()
|
||||
placement = &p
|
||||
assignment = &p
|
||||
}
|
||||
// What it runs inside its own composition, of what the candidate places (the seed's trunk places nothing).
|
||||
c.mu.Lock()
|
||||
inOwn := c.inOwnOf(*placement)
|
||||
inOwn := c.inOwnOf(*assignment)
|
||||
c.mu.Unlock()
|
||||
fps := shadowBuilds(running, *placement, modules, inOwn)
|
||||
fps := shadowBuilds(running, *assignment, modules, inOwn)
|
||||
kept := map[string]string{}
|
||||
for module, fp := range fps {
|
||||
if mod, ok := modules[module]; ok && mod.Fp2 == fp {
|
||||
@@ -570,7 +570,7 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (nodeengin
|
||||
c.mu.Lock()
|
||||
c.wants = held
|
||||
c.mu.Unlock()
|
||||
a := frombus.Answer(ctx, reader{c: c}, c.env.Mesh, frombus.Scope{Node: node, KeptAt: kept, Placements: given, InOwn: inOwn,
|
||||
a := frombus.Answer(ctx, reader{c: c}, c.env.Mesh, frombus.Scope{Node: node, KeptAt: kept, Assignments: given, InOwn: inOwn,
|
||||
SameValue: c.sameValue, HeldToday: s.heldToday()}, fps, running, proposal, rev)
|
||||
s.mu.Lock()
|
||||
ran := make(map[string]string, len(s.ran))
|
||||
@@ -664,7 +664,7 @@ func asHavesSay(a nodeengine.CandidateAnswer, ran map[string]string) nodeengine.
|
||||
// of the old form) is given none, so it is folded there again, as the node's own composer folds it, rather than
|
||||
// composed at the trunk's build as if the candidate added it (issue 1126: nats and photos have a trunk build that no
|
||||
// want names).
|
||||
func shadowBuilds(running []engcomp.Composed, placement meshmoduleassigner.Placement, modules map[string]meshcatalog.ModulesByModuleValue, inOwn []string) map[string]string {
|
||||
func shadowBuilds(running []engcomp.Composed, assignment meshmoduleassigner.Assignments, modules map[string]meshcatalog.ModulesByModuleValue, inOwn []string) map[string]string {
|
||||
fps := map[string]string{}
|
||||
for _, r := range running {
|
||||
if r.Module != "" && r.FP != "" {
|
||||
@@ -675,7 +675,7 @@ func shadowBuilds(running []engcomp.Composed, placement meshmoduleassigner.Place
|
||||
for _, m := range inOwn {
|
||||
folded[m] = true
|
||||
}
|
||||
for _, m := range placement.Modules {
|
||||
for _, m := range assignment.Modules {
|
||||
if _, runs := fps[m.Module]; !runs && !folded[m.Module] {
|
||||
if mod, ok := modules[m.Module]; ok && mod.Fp2 != "" {
|
||||
fps[m.Module] = mod.Fp2
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
]
|
||||
},
|
||||
"reads": [
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-delivery-assigner.assignments",
|
||||
"mesh-provisioner.plans",
|
||||
"mesh-provisioner.provisions"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user