Node-engine: the placement, the wants and the placement candidates are the seats' contract types and the assigner client's keys and codecs; the found tunnel is the placement seat's typed record (ADR 0634 §1, §4; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:34:02 +01:00
parent 7eb7b9fa62
commit c35cc1d9e4
26 changed files with 489 additions and 390 deletions
+3 -3
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"github.com/novox/mesh-host/internal/buckets"
"github.com/novox/mesh-host/internal/compositions"
"os"
"path/filepath"
@@ -19,7 +20,6 @@ import (
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/compose/frombus"
@@ -501,7 +501,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
}()
env := agent.Env{
Node: node,
Wants: bucketWatch{session, delivery.AssignerWantsBucket},
Wants: bucketWatch{session, buckets.Wants},
Composed: bucketWatch{session, composedBucket},
Haves: sessionHaves{session},
Kept: func() map[string][]json.RawMessage { by, _ := kept(); return by },
@@ -1017,7 +1017,7 @@ func selfComposer(session *link.Session, node string, hooks composerHooks, say f
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 == delivery.PlacementBucket || bucket == delivery.PlacementCandidatesBucket {
if bucket == buckets.Placement || bucket == buckets.PlacementCandidates {
// 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) {
+34 -30
View File
@@ -8,7 +8,11 @@ import (
"encoding/json"
"errors"
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"
"github.com/novox/mesh-host/internal/buckets"
"github.com/novox/mesh-host/internal/buckets/bucketstest"
engcomp "github.com/novox/mesh-host/internal/compositions"
"os"
"path/filepath"
@@ -136,7 +140,7 @@ func newItBedAs(t *testing.T, perms nodegrant.Permissions) *itBed {
name string
into *nats.KeyValue
history uint8
}{{delivery.AssignerWantsBucket, &b.wants, 8}, {composedBucket, &b.composed, engcomp.History},
}{{buckets.Wants, &b.wants, 8}, {composedBucket, &b.composed, engcomp.History},
{buckets.Have, &b.haves, buckets.HaveHistory}} {
kv, err := b.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: c.name, History: c.history})
if err != nil {
@@ -186,11 +190,11 @@ func fpDigest(c string) string { return "sha256:" + strings.Repeat(c, 64) }
func (b *itBed) want(module, fp, digest, backup string) {
b.t.Helper()
raw, err := delivery.EncodeWant(delivery.Want{FP: fp, Composed: digest, Backup: backup})
raw, err := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Composed: digest, Backup: backup})
if err != nil {
b.t.Fatal(err)
}
key, _ := delivery.WantKey(itNode, module)
key := meshwants.WantsByWantNodeModuleKey(itNode, module)
if _, err := b.wants.Put(key, raw); err != nil {
b.t.Fatal(err)
}
@@ -313,12 +317,12 @@ 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, _ := delivery.DecodePlacement(place)
craw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "feat-x", Placement: &same})
same, _ := meshmoduleassignerclient.DecodePlacement(place)
craw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "feat-x", Placement: &same})
if err != nil {
t.Fatal(err)
}
ckey, _ := delivery.PlacementCandidateKey("feat-x", itNode)
ckey := meshmoduleassigner.CandidatesByProposalNodeKey("feat-x", itNode)
if _, err := candidates.Put(ckey, craw); err != nil {
t.Fatal(err)
}
@@ -346,11 +350,11 @@ func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValu
if n := node(); n.Composes != nec.ComposesOwn {
t.Errorf("a node composing its own says it composes %q", n.Composes)
}
craw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "revert-x"})
craw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "revert-x"})
if err != nil {
t.Fatal(err)
}
ckey, _ := delivery.PlacementCandidateKey("revert-x", itNode)
ckey := meshmoduleassigner.CandidatesByProposalNodeKey("revert-x", itNode)
if _, err := candidates.Put(ckey, craw); err != nil {
t.Fatal(err)
}
@@ -360,7 +364,7 @@ func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValu
strings.Contains(a.Refused, "composes nothing")
})
ran := b.have("web").Composed
pkey, _ := delivery.PlacementNodeKey(itNode)
pkey, _ := meshmoduleassignerclient.NodeKey(itNode)
// A pass in flight when the key goes (issue 1126, 2026-10-28): a config value changed starts one, and the key goes at once.
if _, err := resolved.Put(itNode+".web.greeting", []byte(`{"value":"salut","source":"module"}`)); err != nil {
t.Fatal(err)
@@ -407,10 +411,10 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
}
return kv
}
placement, modules, manifests := kvOf(delivery.PlacementBucket), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket)
placement, modules, manifests := kvOf(buckets.Placement), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket)
resolved := kvOf(selfcompose.ResolvedBucket)
builds := kvOf(delivery.BuilderBuildsBucket)
candidates := kvOf(delivery.PlacementCandidatesBucket)
candidates := kvOf(buckets.PlacementCandidates)
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"}},` +
@@ -422,11 +426,11 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
mkey, _ := delivery.ManifestsKey(digest)
fp := fpIt("a")
modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)})
place, _ := delivery.EncodePlacement(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{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, _ := delivery.PlacementNodeKey(itNode)
places, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
pkey, _ := meshmoduleassignerclient.NodeKey(itNode)
places, _ := meshmoduleassignerclient.PlacementValues(meshmoduleassigner.Placement{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.
@@ -436,9 +440,9 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
mlraw, _ := delivery.EncodeManifest(delivery.Manifest{Module: "mailer", JSON: mailer})
mlkey, _ := delivery.ManifestsKey(mdigest)
mlmod, _ := delivery.EncodeModule(delivery.Module{FP2: fpIt("b"), Manifest: mdigest, Repository: "novox/mailer", Commit: strings.Repeat("2", 40)})
place2, _ := delivery.EncodePlacement(delivery.Placement{Node: "it2", Modules: []delivery.PlacedModule{{Module: "mailer"}}})
place2, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "it2", Modules: []meshmoduleassigner.PlacedModule{{Module: "mailer"}}})
rec2, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "it2", Capabilities: []string{"service-manager"}}})
pkey2, _ := delivery.LegacyPlacementKey("it2")
pkey2, _ := meshmoduleassignerclient.LegacyKey("it2")
hkey2, _ := nec.HaveNodeKey("it2")
buildKey, _ := delivery.BuilderBuildKey("web", fp)
done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest,
@@ -459,8 +463,8 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) {
t.Fatal(err)
}
}
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
wkey, _ := delivery.WantKey(itNode, "web")
want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true})
wkey := meshwants.WantsByWantNodeModuleKey(itNode, "web")
if _, err := b.wants.Put(wkey, want); err != nil {
t.Fatal(err)
}
@@ -536,12 +540,12 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str
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, _ := delivery.DecodePlacement(place)
craw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "feat-x", Placement: &same})
same, _ := meshmoduleassignerclient.DecodePlacement(place)
craw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "feat-x", Placement: &same})
if err != nil {
t.Fatal(err)
}
ckey, _ := delivery.PlacementCandidateKey("feat-x", itNode)
ckey := meshmoduleassigner.CandidatesByProposalNodeKey("feat-x", itNode)
crev, err := candidates.Put(ckey, craw)
if err != nil {
t.Fatal(err)
@@ -761,10 +765,10 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test
}
return kv
}
placement, modules, manifests := kvOf(delivery.PlacementBucket), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket)
placement, modules, manifests := kvOf(buckets.Placement), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket)
kvOf(selfcompose.ResolvedBucket)
builds := kvOf(delivery.BuilderBuildsBucket)
kvOf(delivery.PlacementCandidatesBucket)
kvOf(buckets.PlacementCandidates)
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) {
@@ -791,7 +795,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, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "nats"}}})
places, _ := meshmoduleassignerclient.PlacementValues(meshmoduleassigner.Placement{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})
@@ -800,12 +804,12 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test
t.Fatal(err)
}
}
busWant := func(w delivery.Want) {
raw, err := delivery.EncodeWant(w)
busWant := func(w meshwants.WantsByWantNodeModuleValue) {
raw, err := bucketstest.Want(w)
if err != nil {
t.Fatal(err)
}
key, _ := delivery.WantKey(itNode, "nats")
key := meshwants.WantsByWantNodeModuleKey(itNode, "nats")
if _, err := b.wants.Put(key, raw); err != nil {
t.Fatal(err)
}
@@ -837,7 +841,7 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test
}
first := fpIt("e")
publish(first, "")
busWant(delivery.Want{FP: first, Self: true})
busWant(meshwants.WantsByWantNodeModuleValue{Fp: first, Self: true})
stop := b.start()
defer stop()
for key, value := range places {
@@ -869,11 +873,11 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test
// Approve for a move the node judged to recreate it (ADR 0599 §2, §4).
next := fpIt("f")
publish(next, `,"env":{"CHANGED":"1"}`)
busWant(delivery.Want{FP: next, Self: true})
busWant(meshwants.WantsByWantNodeModuleValue{Fp: next, Self: true})
within(t, 10*time.Second, "the build that changes the server was not applied", func() bool {
h := b.have("nats")
if h.Phase == nec.PhaseWaitingYes && h.Waits != "" {
busWant(delivery.Want{FP: next, Self: true, Yes: h.Waits})
busWant(meshwants.WantsByWantNodeModuleValue{Fp: next, Self: true, Yes: h.Waits})
}
return h.Fp == next && h.Phase == nec.PhaseRunning
})
+16 -13
View File
@@ -9,6 +9,9 @@ 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/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"github.com/novox/mesh-host/internal/buckets"
"os"
"path/filepath"
"strings"
@@ -179,15 +182,15 @@ func followTrust(ctx context.Context, session *link.Session, trust *meshTrust, s
// bus: then the kept declaration is read no more (keptRetired). A node the seat does not place, or whose placement or haves
// cannot be read, keeps reading it.
func placedAllHave(ctx context.Context, session *link.Session, node string) (bool, string) {
key, err := delivery.PlacementNodeKey(node)
key, err := meshmoduleassignerclient.NodeKey(node)
if err != nil {
return false, err.Error()
}
legacy, _ := delivery.LegacyPlacementKey(node)
legacy, _ := meshmoduleassignerclient.LegacyKey(node)
// Its node key names every module placed (research 072 row 37); before the holder converted it, its legacy key does.
values := map[string][]byte{}
for _, k := range []string{key, legacy} {
if h, err := session.KVHistory(ctx, delivery.PlacementBucket, k); err == nil && len(h) > 0 && !h[len(h)-1].Deleted {
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
}
}
@@ -202,25 +205,25 @@ 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) (delivery.Placement, error) {
key, _ := delivery.PlacementNodeKey(node)
func placedModules(node string, values map[string][]byte) (meshmoduleassigner.Placement, error) {
key, _ := meshmoduleassignerclient.NodeKey(node)
if raw, ok := values[key]; ok {
n, err := delivery.DecodePlacementNode(raw)
n, err := meshmoduleassignerclient.DecodeNode(raw)
if err != nil {
return delivery.Placement{}, err
return meshmoduleassigner.Placement{}, err
}
p := delivery.Placement{Node: node}
p := meshmoduleassigner.Placement{Node: node}
for _, r := range n.Modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: r.Module})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: r.Module})
}
return p, nil
}
legacy, _ := delivery.LegacyPlacementKey(node)
legacy, _ := meshmoduleassignerclient.LegacyKey(node)
raw, ok := values[legacy]
if !ok {
return delivery.Placement{}, errors.New("its placement is not read")
return meshmoduleassigner.Placement{}, errors.New("its placement is not read")
}
return delivery.DecodePlacement(raw)
return meshmoduleassignerclient.DecodePlacement(raw)
}
// allHave says whether has finds, for every module p places, a have that names a fingerprint of the new form, and why
@@ -228,7 +231,7 @@ func placedModules(node string, values map[string][]byte) (delivery.Placement, e
// 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 delivery.Placement, has func(module string) (nodeengine.HaveByNodeModuleValue, bool, error)) (bool, string) {
func allHave(p meshmoduleassigner.Placement, has func(module string) (nodeengine.HaveByNodeModuleValue, bool, error)) (bool, string) {
if len(p.Modules) == 0 {
return false, "the seat mesh-placement places no module here"
}
+4 -3
View File
@@ -7,6 +7,7 @@ import (
"errors"
"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/meshmoduleassigner"
engcomp "github.com/novox/mesh-host/internal/compositions"
"path/filepath"
"sort"
@@ -119,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 := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: "lemurs"}}}
placed := meshmoduleassigner.Placement{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: delivery.SourcePrefix2 + strings.Repeat("a", 64), Phase: nodeengineclient.PhaseRunning}
@@ -138,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 := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: overlay.Name}}}
withWireguard := meshmoduleassigner.Placement{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
@@ -147,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(delivery.Placement{Node: "n1"}, nil); got {
if got, _ := allHave(meshmoduleassigner.Placement{Node: "n1"}, nil); got {
t.Error("a node with nothing placed retires the read")
}
keptRetired.Store(true)
+3
View File
@@ -3,6 +3,9 @@ module github.com/novox/mesh-host
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-20261105000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-20261105000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-20261105000000-000000000000
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-20261105000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-20261105000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-20261105000000-000000000000
+30 -30
View File
@@ -19,6 +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"
"github.com/novox/mesh-host/internal/compositions"
"slices"
"sort"
@@ -29,7 +30,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/judge"
)
@@ -276,7 +276,7 @@ type againAsk struct {
}
type wantNow struct {
Want delivery.Want
Want meshwants.WantsByWantNodeModuleValue
Node bool // the node's own want
Present bool
Rev uint64
@@ -751,26 +751,26 @@ func (a *Agent) Running() []compositions.Composed {
func (a *Agent) takeWant(u Update) string {
a.mu.Lock()
defer a.mu.Unlock()
if _, err := delivery.ParseWantNodeKey(u.Key); err == nil {
if _, ok := meshwants.ParseWantsByWantNodeNodeKey(u.Key); ok {
w := wantNow{Node: true, Rev: u.Revision}
if !u.Deleted {
nw, err := delivery.DecodeNodeWant(u.Value)
nw, err := meshwants.DecodeWantsByWantNodeNodeValue(u.Value)
if err != nil {
a.sayOnce("want:node", fmt.Sprintf("the node's own want is not one: %v", err))
return ""
}
w.Present, w.Want = true, delivery.Want{FP: "", Composed: nw.Composed}
w.Present, w.Want = true, meshwants.WantsByWantNodeModuleValue{Fp: "", Composed: nw.Composed}
}
a.wants[NodeModule] = w
return NodeModule
}
node, module, err := delivery.ParseWantKey(u.Key)
if err != nil || node != a.env.Node {
node, module, ok := meshwants.ParseWantsByWantNodeModuleKey(u.Key)
if !ok || node != a.env.Node {
return ""
}
w := wantNow{Rev: u.Revision}
if !u.Deleted {
want, err := delivery.DecodeWant(u.Value)
want, err := meshwants.DecodeWantsByWantNodeModuleValue(u.Value)
if err != nil {
a.sayOnce("want:"+module, fmt.Sprintf("the want of %s is not one: %v", module, err))
return ""
@@ -1136,8 +1136,8 @@ func (a *Agent) unreadableModules() []string {
if w.Present && w.Want.Removed {
continue
}
if w.Present && w.Want.FP != "" {
if w.Want.FP == fp {
if w.Present && w.Want.Fp != "" {
if w.Want.Fp == fp {
current[m] = pick{key, c}
}
continue
@@ -1179,11 +1179,11 @@ func (a *Agent) heldOut(except string) []compositions.Composed {
if w := a.wants[m]; w.Present && w.Want.Removed {
continue // a module the assigner removed holds nothing back
}
if w := a.wants[m]; w.Present && !w.Want.Removed && w.Want.FP == fp {
if w := a.wants[m]; w.Present && !w.Want.Removed && w.Want.Fp == fp {
waiting[m] = c
continue
}
if w := a.wants[m]; w.Present && !w.Want.Removed && waiting[m].Rev != 0 && waiting[m].C.FP == w.Want.FP {
if w := a.wants[m]; w.Present && !w.Want.Removed && waiting[m].Rev != 0 && waiting[m].C.FP == w.Want.Fp {
continue
}
if prev, ok := waiting[m]; !ok || c.Rev > prev.Rev {
@@ -1283,7 +1283,7 @@ func (a *Agent) RetryFailed(module string) error {
return fmt.Errorf("%s has no want on this machine, so there is no apply of it to try again", module)
case tried && att.Held:
return fmt.Errorf("%s is held for a failed backup: the operator's answer releases it", module)
case !tried && ran && r.Fp == w.Want.FP && r.Phase != nodeengineclient.PhaseRemoved:
case !tried && ran && r.Fp == w.Want.Fp && r.Phase != nodeengineclient.PhaseRemoved:
return fmt.Errorf("%s runs what its want names, nothing to retry", module)
case !tried:
return fmt.Errorf("%s has no failed apply to try again: it waits for its composition, or settles on its own", module)
@@ -1331,7 +1331,7 @@ func (a *Agent) settleOne(ctx context.Context, module string) {
return
}
a.mu.Lock()
comp, have := a.compositionFor(module, w.Want.FP)
comp, have := a.compositionFor(module, w.Want.Fp)
a.mu.Unlock()
if a.env.SelfComposed {
if !have || comp.Err != nil {
@@ -1345,7 +1345,7 @@ func (a *Agent) settleOne(ctx context.Context, module string) {
} else if (!have || comp.Err != nil || comp.Dig != w.Want.Composed) && w.Want.Composed != "" {
// The composition the want names may be an older revision of the key, replaced by a newer one at the same
// fingerprint (a want held there keeps its digest): taken from the revisions the bucket keeps (ADR 0539 clause 1).
if old, found := a.revisionNamed(ctx, module, w.Want.FP, w.Want.Composed); found {
if old, found := a.revisionNamed(ctx, module, w.Want.Fp, w.Want.Composed); found {
comp, have = old, true
}
}
@@ -1356,7 +1356,7 @@ func (a *Agent) settleOne(ctx context.Context, module string) {
// A composition whose digest no want names is not applied.
a.mu.Lock()
a.sayOnce("digest:"+module, fmt.Sprintf("the composition of %s at %s has digest %s, and its want names %s; nothing is applied until they agree",
module, w.Want.FP, comp.Dig, w.Want.Composed))
module, w.Want.Fp, comp.Dig, w.Want.Composed))
a.mu.Unlock()
return
}
@@ -1372,19 +1372,19 @@ func (a *Agent) settleOne(ctx context.Context, module string) {
}
a.mu.Lock()
r, runs := a.running[module]
runsWant := runs && r.Fp == w.Want.FP && r.Composed == w.Want.Composed && r.Phase != nodeengineclient.PhaseRemoved
runsWant := runs && r.Fp == w.Want.Fp && r.Composed == w.Want.Composed && r.Phase != nodeengineclient.PhaseRemoved
att, tried := a.attempt[module]
same := tried && att.FP == w.Want.FP && att.Composed == w.Want.Composed
same := tried && att.FP == w.Want.Fp && att.Composed == w.Want.Composed
if same && att.Held {
// Held for a failed backup: looked at before what runs, since a hold may name what the want names (its have kept
// the composition it held, nats on novox, 2026-11-04), and an answer in a later revision of the want would then
// be swallowed by "it runs what its want names" (issue 1585, the second failure). The operator's answer, again or
// skip, in a revision after the one the hold took, is acted on whatever runs; any other new revision of the want
// is the answer only for a move (ADR 0568 §4), as before.
answered := w.Rev > att.AnsweredRev && (w.Want.Backup == delivery.BackupAgain || w.Want.Backup == delivery.BackupSkip)
answered := w.Rev > att.AnsweredRev && (w.Want.Backup == meshwants.WantsByWantNodeModuleValueBackupAgain || w.Want.Backup == meshwants.WantsByWantNodeModuleValueBackupSkip)
if !answered && (runsWant || (att.AnsweredRev >= w.Rev && att.CompRev == comp.Rev)) {
a.sayOnce("held:"+module, fmt.Sprintf("%s is held for a failed backup before %s: it waits for the operator's "+
"answer, and its want (revision %d) carries none since the hold (revision %d)", module, w.Want.FP, w.Rev, att.AnsweredRev))
"answer, and its want (revision %d) carries none since the hold (revision %d)", module, w.Want.Fp, w.Rev, att.AnsweredRev))
a.mu.Unlock()
return
}
@@ -1447,7 +1447,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
a.mu.Lock()
a.prior[module] = true
a.mu.Unlock()
if h.Phase == string(nodeengineclient.PhaseHeld) && h.Detail == BackupInterrupted && w.Want.Backup != delivery.BackupSkip {
if h.Phase == string(nodeengineclient.PhaseHeld) && h.Detail == BackupInterrupted && w.Want.Backup != meshwants.WantsByWantNodeModuleValueBackupSkip {
// Held only because this node agent stopped while the backup ran (postgres on novox, 2026-10-31): no
// failure and nothing for the operator to answer. It is backed up again and applied (a.prior makes the
// apply back it up first); the want still asking a backup is all the answer needed.
@@ -1456,7 +1456,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
a.mu.Unlock()
return false
}
if h.Phase == string(nodeengineclient.PhaseHeld) && (w.Want.Backup == delivery.BackupAgain || w.Want.Backup == delivery.BackupSkip) {
if h.Phase == string(nodeengineclient.PhaseHeld) && (w.Want.Backup == meshwants.WantsByWantNodeModuleValueBackupAgain || w.Want.Backup == meshwants.WantsByWantNodeModuleValueBackupSkip) {
// Held for a failed backup, and the want already carries the operator's answer, written before this engine
// started (nats on novox, 2026-11-04: "Back up again" answered, then the engine restarted and took the
// answer for the hold's own revision, so it was never acted on; issue 1585). The answer is acted on now:
@@ -1477,7 +1477,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
a.sayOnce("held:"+module, fmt.Sprintf("%s is held for a failed backup (%s) at this start: it keeps what "+
"its have names, %s, and waits for the operator's answer; its want (revision %d) carries none",
module, h.Detail, short(h.Composed), w.Rev))
a.attempt[module] = attempt{FP: w.Want.FP, Composed: w.Want.Composed, WantRev: w.Rev, CompRev: comp.Rev, Held: true,
a.attempt[module] = attempt{FP: w.Want.Fp, Composed: w.Want.Composed, WantRev: w.Rev, CompRev: comp.Rev, Held: true,
AnsweredRev: w.Rev}
if h.Composed != "" {
a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: h.FP, Composed: h.Composed, Phase: nodeengineclient.PhaseRunning}
@@ -1496,7 +1496,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
if marked {
h.Detail = ""
}
if h.FP == w.Want.FP && h.Composed == w.Want.Composed &&
if h.FP == w.Want.Fp && h.Composed == w.Want.Composed &&
(h.Phase == "" || h.Phase == string(nodeengineclient.PhaseRunning) || marked) {
if a.inactiveAtStart(ctx, module, comp) {
return false
@@ -1531,10 +1531,10 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
return false
}
a.mu.Lock()
a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: w.Want.FP, Composed: w.Want.Composed, Phase: nodeengineclient.PhaseRunning}
a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: w.Want.Fp, Composed: w.Want.Composed, Phase: nodeengineclient.PhaseRunning}
a.mu.Unlock()
a.runsChanged(module + " was taken from the kept declaration")
a.writeHave(ctx, Have{Module: module, FP: w.Want.FP, Composed: w.Want.Composed, Phase: string(nodeengineclient.PhaseRunning)})
a.writeHave(ctx, Have{Module: module, FP: w.Want.Fp, Composed: w.Want.Composed, Phase: string(nodeengineclient.PhaseRunning)})
return true
}
@@ -1553,7 +1553,7 @@ func (a *Agent) inactiveAtStart(ctx context.Context, module string, comp compNow
}
func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNow, others []compositions.Composed) {
fp, dig := w.Want.FP, w.Want.Composed
fp, dig := w.Want.Fp, w.Want.Composed
a.mu.Lock()
_, ranBefore := a.running[module]
ranBefore = ranBefore || a.prior[module]
@@ -1561,7 +1561,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
// A data module is backed up before a new composition of it moves (ADR 0429 §14), unless the want says to go on
// without one (ADR 0539 §16).
if comp.C.Data && ranBefore && a.env.Backup != nil && w.Want.Backup != delivery.BackupSkip {
if comp.C.Data && ranBefore && a.env.Backup != nil && w.Want.Backup != meshwants.WantsByWantNodeModuleValueBackupSkip {
// What runs, read before the have says backing-up: a module known only from its have (a.prior) has no running
// entry, and its held have kept naming nothing (postgres on novox, 2026-10-31), as the not-composed mark keeps it.
a.mu.Lock()
@@ -1811,11 +1811,11 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
return false
}
a.mu.Lock()
a.sayOnce("yes:"+module+":"+comp.Dig, fmt.Sprintf("%s at %s waits for a person's yes: %s", module, w.Want.FP, j.Words))
a.sayOnce("yes:"+module+":"+comp.Dig, fmt.Sprintf("%s at %s waits for a person's yes: %s", module, w.Want.Fp, j.Words))
prev := a.running[module]
a.mu.Unlock()
a.writeHave(ctx, Have{Module: module, FP: prev.Fp, Composed: prev.Composed, Phase: string(nodeengineclient.PhaseWaitingYes),
Detail: j.Words, Judgement: string(j.Judgement), Waits: comp.Dig, BuildAlone: a.buildAlone(ctx, module, prev, w.Want.FP)})
Detail: j.Words, Judgement: string(j.Judgement), Waits: comp.Dig, BuildAlone: a.buildAlone(ctx, module, prev, w.Want.Fp)})
return true
}
+36 -46
View File
@@ -6,6 +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"
"github.com/novox/mesh-host/internal/buckets/bucketstest"
"github.com/novox/mesh-host/internal/compositions"
"maps"
"slices"
@@ -254,11 +256,8 @@ func encodedAs(t *testing.T, c compositions.Composed, key string) (string, []byt
func wantFor(t *testing.T, module, fp, digest, backup string) (string, []byte) {
t.Helper()
key, err := delivery.WantKey(node, module)
if err != nil {
t.Fatal(err)
}
raw, err := delivery.EncodeWant(delivery.Want{FP: fp, Composed: digest, Backup: backup})
key := meshwants.WantsByWantNodeModuleKey(node, module)
raw, err := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Composed: digest, Backup: backup})
if err != nil {
t.Fatal(err)
}
@@ -684,7 +683,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
}
// The answer arrives in the module's own want: back up again (it fails again), then skip.
wk, again := wantFor(t, "db", fp2, dig2, delivery.BackupAgain)
wk, again := wantFor(t, "db", fp2, dig2, meshwants.WantsByWantNodeModuleValueBackupAgain)
b.wants.put(wk, again)
eventually(t, "back up again did not ask the backup", func() bool {
b.mu.Lock()
@@ -694,7 +693,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
if got := b.appliedList(); len(got) != 0 {
t.Fatalf("applied behind a second failed backup: %v", got)
}
_, skip := wantFor(t, "db", fp2, dig2, delivery.BackupSkip)
_, skip := wantFor(t, "db", fp2, dig2, meshwants.WantsByWantNodeModuleValueBackupSkip)
b.wants.put(wk, skip)
eventually(t, "skip did not apply the module", func() bool { return len(b.appliedList()) == 1 })
b.mu.Lock()
@@ -713,7 +712,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
ib := newBed(t)
ib.comps.before(ck1, craw1)
ib.comps.before(ck2, craw2)
iwk, iwraw := wantFor(t, "db", fp2, dig2, delivery.BackupAgain)
iwk, iwraw := wantFor(t, "db", fp2, dig2, meshwants.WantsByWantNodeModuleValueBackupAgain)
ib.wants.before(iwk, iwraw)
ib.haves.stored = map[string]Have{"db": {Module: "db", FP: fp1, Composed: dig1, Phase: string(nodeengineclient.PhaseRunning)}}
began := make(chan struct{})
@@ -738,13 +737,13 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
phase string
namesNothing bool // the have names no fp and no composition
}{
{BackupInterrupted, delivery.BackupAgain, 1, 1, nodeengineclient.PhaseRunning, false},
{BackupInterrupted, meshwants.WantsByWantNodeModuleValueBackupAgain, 1, 1, nodeengineclient.PhaseRunning, false},
// novox's postgres: its held have, written before mesh-host #189, names nothing it runs.
{BackupInterrupted, "", 1, 1, nodeengineclient.PhaseRunning, true},
// A backup that failed, and the want carries the operator's answer, written before this start (nats on novox,
// 2026-11-04, issue 1585): again backs it up and applies, skip applies without one; no answer keeps the hold.
{"restic: the repository is locked", delivery.BackupAgain, 1, 1, nodeengineclient.PhaseRunning, false},
{"restic: the repository is locked", delivery.BackupSkip, 0, 1, nodeengineclient.PhaseRunning, false},
{"restic: the repository is locked", meshwants.WantsByWantNodeModuleValueBackupAgain, 1, 1, nodeengineclient.PhaseRunning, false},
{"restic: the repository is locked", meshwants.WantsByWantNodeModuleValueBackupSkip, 0, 1, nodeengineclient.PhaseRunning, false},
{"restic: the repository is locked", "", 0, 0, nodeengineclient.PhaseHeld, false},
} {
r := newBed(t)
@@ -783,9 +782,9 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
backups int
holdsTill bool // it is held, and said held, before the later revision
}{
{"answered before the start", delivery.BackupAgain, "", 1, false},
{"answered while held", "", delivery.BackupAgain, 1, true},
{"skipped while held", "", delivery.BackupSkip, 0, true},
{"answered before the start", meshwants.WantsByWantNodeModuleValueBackupAgain, "", 1, false},
{"answered while held", "", meshwants.WantsByWantNodeModuleValueBackupAgain, 1, true},
{"skipped while held", "", meshwants.WantsByWantNodeModuleValueBackupSkip, 0, true},
} {
t.Run(row.name, func(t *testing.T) {
r := newBed(t)
@@ -793,9 +792,9 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
r.env.Policy = func(context.Context, string) string { return catalogue.PolicyRecord }
ck, craw, dig := composition(t, "nats", fp2, true, file("nats", "x", "2"))
r.comps.before(ck, craw)
wk, _ := delivery.WantKey(node, "nats")
wk := meshwants.WantsByWantNodeModuleKey(node, "nats")
selfWant := func(backup string) []byte {
raw, err := delivery.EncodeWant(delivery.Want{FP: fp2, Self: true, Yes: dig, Backup: backup})
raw, err := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp2, Self: true, Yes: dig, Backup: backup})
if err != nil {
t.Fatal(err)
}
@@ -888,7 +887,7 @@ func TestARemovedWantRemovesThatModuleOnly(t *testing.T) {
wk, wraw := wantFor(t, "web", fp, dig, "")
b.wants.put(wk, wraw)
eventually(t, "not applied", func() bool { return len(b.appliedList()) == 1 })
gone, _ := delivery.EncodeWant(delivery.Want{Removed: true})
gone, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Removed: true})
b.wants.put(wk, gone)
eventually(t, "not removed", func() bool {
b.mu.Lock()
@@ -909,11 +908,8 @@ func TestTheNodesOwnCompositionComesBeforeAnyModules(t *testing.T) {
ck, craw, dig := composition(t, "web", fp, false, file("web", "x", "1"))
wk, wraw := wantFor(t, "web", fp, dig, "")
nk, nraw, ndig := composition(t, NodeModule, "", false)
nwk, err := delivery.WantNodeKey(node)
if err != nil {
t.Fatal(err)
}
nwraw, err := delivery.EncodeNodeWant(delivery.NodeWant{Composed: ndig})
nwk := meshwants.WantsByWantNodeNodeKey(node)
nwraw, err := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Composed: ndig})
if err != nil {
t.Fatal(err)
}
@@ -1035,11 +1031,11 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
ck, _ := compositions.Key(node, "web", fpOf("b"))
raw2, _ := compositions.Encode(web2)
comps.put(ck, raw2)
wk, _ := delivery.WantKey(node, "web")
wraw, _ := delivery.EncodeWant(delivery.Want{FP: fpOf("b"), Self: true})
wk := meshwants.WantsByWantNodeModuleKey(node, "web")
wraw, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fpOf("b"), Self: true})
b.wants.put(wk, wraw)
eventually(t, "the applied composition was not kept", func() bool { return string(store.get("web")) == string(raw2) })
rraw, _ := delivery.EncodeWant(delivery.Want{Removed: true})
rraw, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Removed: true})
b.wants.put(wk, rraw)
eventually(t, "the removed module's composition was not forgotten", func() bool { return store.get("web") == nil })
})
@@ -1182,11 +1178,8 @@ func TestTheNodesOwnApplyHoldsOutAModuleThatMovedToItsOwnComposition(t *testing.
rk, rraw, _ := composition(t, "systemd-resolved", fp, false, file("systemd-resolved", "guard", "g"))
b.comps.before(rk, rraw)
nk, nraw, ndig := composition(t, NodeModule, "", false)
nwk, err := delivery.WantNodeKey(node)
if err != nil {
t.Fatal(err)
}
nwraw, err := delivery.EncodeNodeWant(delivery.NodeWant{Composed: ndig})
nwk := meshwants.WantsByWantNodeNodeKey(node)
nwraw, err := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Composed: ndig})
if err != nil {
t.Fatal(err)
}
@@ -1216,7 +1209,7 @@ func TestTheNodesOwnApplyHoldsOutAModuleThatMovedToItsOwnComposition(t *testing.
spiegel, _ = json.Marshal(bare)
b2.env.Kept = func() map[string][]json.RawMessage { return map[string][]json.RawMessage{NodeModule: {spiegel}} }
nk2, nraw2, ndig2 := composition(t, NodeModule, "", false)
nwraw2, _ := delivery.EncodeNodeWant(delivery.NodeWant{Composed: ndig2})
nwraw2, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Composed: ndig2})
b2.comps.before(nk2, nraw2)
b2.wants.before(nwk, nwraw2)
ctx2, cancel2 := context.WithCancel(context.Background())
@@ -1244,18 +1237,15 @@ func testARemovedModulesCompositionHoldsNothingBack(t *testing.T) {
fp := fpOf("b")
rk, rraw, _ := composition(t, "gone", fp, false, file("gone", "x", "1"))
b.comps.before(rk, rraw)
gk, err := delivery.WantKey(node, "gone")
if err != nil {
t.Fatal(err)
}
graw, err := delivery.EncodeWant(delivery.Want{Removed: true})
gk := meshwants.WantsByWantNodeModuleKey(node, "gone")
graw, err := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Removed: true})
if err != nil {
t.Fatal(err)
}
b.wants.before(gk, graw)
nk, nraw, ndig := composition(t, NodeModule, "", false)
nwk, _ := delivery.WantNodeKey(node)
nwraw, _ := delivery.EncodeNodeWant(delivery.NodeWant{Composed: ndig})
nwk := meshwants.WantsByWantNodeNodeKey(node)
nwraw, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Composed: ndig})
b.comps.before(nk, nraw)
b.wants.before(nwk, nwraw)
stop := b.run()
@@ -1289,8 +1279,8 @@ func TestTheNodesOwnWaitsWhileAModulesCompositionCannotBeRead(t *testing.T) {
}
b.comps.before(bad, []byte(`{"v":1,"not":"a composition"}`))
nk, nraw, ndig := composition(t, NodeModule, "", false)
nwk, _ := delivery.WantNodeKey(node)
nwraw, _ := delivery.EncodeNodeWant(delivery.NodeWant{Composed: ndig})
nwk := meshwants.WantsByWantNodeNodeKey(node)
nwraw, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Composed: ndig})
b.comps.before(nk, nraw)
b.wants.before(nwk, nwraw)
stop := b.run()
@@ -1327,8 +1317,8 @@ func TestAStaleRefusedCompositionDoesNotHoldTheNodesOwnBack(t *testing.T) {
gk, graw, _ := composition(t, "power", fpOf("b"), false, file("power", "x", "1"))
b.comps.before(gk, graw)
nk, nraw, ndig := composition(t, NodeModule, "", false)
nwk, _ := delivery.WantNodeKey(node)
nwraw, _ := delivery.EncodeNodeWant(delivery.NodeWant{Composed: ndig})
nwk := meshwants.WantsByWantNodeNodeKey(node)
nwraw, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Composed: ndig})
b.comps.before(nk, nraw)
b.wants.before(nwk, nwraw)
stop := b.run()
@@ -1539,8 +1529,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
return key, raw, dig
}
want := func(module, fp, yes string) {
key, _ := delivery.WantKey(node, module)
raw, err := delivery.EncodeWant(delivery.Want{FP: fp, Yes: yes, Self: true})
key := meshwants.WantsByWantNodeModuleKey(node, module)
raw, err := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Yes: yes, Self: true})
if err != nil {
t.Fatal(err)
}
@@ -1574,8 +1564,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
nraw, _ := compositions.Encode(own)
ndig, _ := compositions.DigestOf(own)
b.comps.put(nk, nraw)
nwk, _ := delivery.WantNodeKey(node)
nwraw, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
nwk := meshwants.WantsByWantNodeNodeKey(node)
nwraw, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true})
b.wants.put(nwk, nwraw)
eventually(t, "the node's own key does not carry its sealed values", func() bool {
h := b.haves.last(NodeModule)
+2 -2
View File
@@ -156,8 +156,8 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo
w := a.wants[module]
fp := ""
if w.Present {
fp = w.Want.FP
view.Want = &WantView{FP: w.Want.FP, Composed: w.Want.Composed, Yes: w.Want.Yes, Removed: w.Want.Removed,
fp = w.Want.Fp
view.Want = &WantView{FP: w.Want.Fp, Composed: w.Want.Composed, Yes: w.Want.Yes, Removed: w.Want.Removed,
Self: w.Want.Self, Backup: w.Want.Backup, Words: w.Want.Words, Revision: w.Rev}
} else if have != nil {
fp = have.FP
+11
View File
@@ -15,3 +15,14 @@ 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
// 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 = "mesh-delivery-assigner_placement"
// PlacementCandidates holds a node's candidate of a proposal, `<proposal>.<node>`.
PlacementCandidates = "mesh-delivery-assigner_candidates"
// Wants holds what each node should run, `want.<node>.<module>` and `want.<node>.node`.
Wants = "mesh-delivery-assigner_wants"
)
@@ -0,0 +1,36 @@
// Package bucketstest writes the values of the states the engine only reads (a want, a placement candidate) as their
// holders do, for the tests that stand in for the holders.
package bucketstest
import (
"encoding/json"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
)
// Version is the value version the holders write.
const Version = 1
// Want is a module's want as the wants seat's holder writes it.
func Want(w meshwants.WantsByWantNodeModuleValue) ([]byte, error) {
w.V = Version
return json.Marshal(w)
}
// NodeWant is a node's own want as the wants seat's holder writes it.
func NodeWant(w meshwants.WantsByWantNodeNodeValue) ([]byte, error) {
w.V = Version
return json.Marshal(w)
}
// Candidate is a node's candidate of a proposal as the placement seat's holder writes it.
func Candidate(c meshmoduleassigner.CandidatesByProposalNodeValue) ([]byte, error) {
c.V = Version
if c.Placement != nil {
p := *c.Placement
p.V = Version
c.Placement = &p
}
return json.Marshal(c)
}
+13 -12
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
engcomp "github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
@@ -38,11 +39,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 := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
for _, m := range placed {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
}
b := &bus{placements: map[string]delivery.Placement{"n1": p},
b := &bus{placements: map[string]meshmoduleassigner.Placement{"n1": p},
records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd", "container-runtime"}}},
modules: map[string]delivery.Module{"lemurs": {Manifest: delivery.ManifestDigest(lemurs)}},
manifests: map[string][]byte{delivery.ManifestDigest(lemurs): lemurs},
@@ -66,9 +67,9 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
}
return b
}
given := func(b *bus) map[string]*delivery.Placement {
given := func(b *bus) map[string]*meshmoduleassigner.Placement {
p := b.placements["n1"]
return map[string]*delivery.Placement{"n1": &p}
return map[string]*meshmoduleassigner.Placement{"n1": &p}
}
// Every refusal: two builds taken in, and a module the composition refuses (its image never built), each named.
@@ -238,7 +239,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
p.Decided.Endpoint = "n1.example:51820"
for i := range p.Modules {
if p.Modules[i].Module == "spiegel" {
p.Modules[i].Ports = map[string]int{"8080": 20005}
p.Modules[i].Ports = map[string]int64{"8080": 20005}
}
}
b.placements["n1"] = p
@@ -476,7 +477,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
p.Decided.Endpoint = "n1.example:51820"
for i := range p.Modules {
if p.Modules[i].Module == "site" {
p.Modules[i].Ports = map[string]int{"8080": 20005}
p.Modules[i].Ports = map[string]int64{"8080": 20005}
}
}
b.placements["n1"] = p
@@ -517,7 +518,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
p2.Decided.Endpoint = "n1.example:51820"
for i := range p2.Modules {
if p2.Modules[i].Module == "site" {
p2.Modules[i].Ports = map[string]int{"8080": 20005}
p2.Modules[i].Ports = map[string]int64{"8080": 20005}
}
}
b2.placements["n1"] = p2
@@ -588,13 +589,13 @@ 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]delivery.Placement{
"n1": {Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1", Endpoint: "n1.example:51820"}, Modules: []delivery.PlacedModule{{Module: "cons"}, {Module: overlay.Name}}},
"n2": {Node: "n2", Decided: delivery.NodeDecided{Address: "10.77.0.2"}, Modules: []delivery.PlacedModule{{Module: "prov"}, {Module: overlay.Name}}}},
b := &bus{placements: map[string]meshmoduleassigner.Placement{
"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]delivery.Module{"prov": {Manifest: delivery.ManifestDigest(prov)}, "cons": {Manifest: delivery.ManifestDigest(cons)}},
manifests: map[string][]byte{delivery.ManifestDigest(prov): prov, delivery.ManifestDigest(cons): cons}}
given := map[string]*delivery.Placement{}
given := map[string]*meshmoduleassigner.Placement{}
for n, p := range b.placements {
p := p
given[n] = &p
+18 -18
View File
@@ -2,12 +2,12 @@ package frombus
import (
"fmt"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
"sort"
"strconv"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose"
)
@@ -16,7 +16,7 @@ import (
// 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) (delivery.Placement, error) {
func Export(in compose.Input, node string) (meshmoduleassigner.Placement, error) {
var n *compose.Node
for i := range in.Nodes {
if in.Nodes[i].Name == node {
@@ -24,14 +24,14 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
}
}
if n == nil {
return delivery.Placement{}, fmt.Errorf("frombus: the input has no node %q", node)
return meshmoduleassigner.Placement{}, 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
// seed's value, and the reader refuses a value of version 0 or without its modules.
p := delivery.Placement{V: sdkkeys.Version, Node: node, Modules: []delivery.PlacedModule{}, Decided: delivery.NodeDecided{
p := meshmoduleassigner.Placement{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,
AgentHome: n.AgentAccountHome, Adopted: n.Adopted, Taken: n.Taken}}
AgentAccountHome: n.AgentAccountHome, Adopted: n.Adopted, Taken: n.Taken}}
// The tunnel found at the node's adoption: the one its private network took over, or, on the hub, the mesh's adopted
// one whether or not the hub took it; and the names a person gave the hub's carried peers.
if t, ok := in.Tunnels[node]; ok {
@@ -52,27 +52,27 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
}
}
for _, module := range n.Assigned {
m := delivery.PlacedModule{Module: module}
m := meshmoduleassigner.PlacedModule{Module: module}
for port, at := range n.Ports[module] {
if m.Ports == nil {
m.Ports = map[string]int{}
m.Ports = map[string]int64{}
}
m.Ports[strconv.Itoa(port)] = at
m.Ports[strconv.Itoa(port)] = int64(at)
}
for provision, to := range n.Bound[module] {
if m.Bindings == nil {
m.Bindings = map[string]delivery.Provider{}
m.Bindings = map[string]meshmoduleassigner.Provider{}
}
m.Bindings[provision] = delivery.Provider{Node: to.Node, Module: to.Module}
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.
settingsForm := !in.Catalogue[module].DeclaresConfig()
for _, layer := range n.Settings[module] {
if layer.From == catalogue.MeshWideLayer {
m.HasMesh, m.MeshWide = true, layer.Placement
m.HasMeshWide, m.MeshWide = true, layer.Placement
if settingsForm && len(layer.Values) > 0 {
m.MeshValues = layer.Values
m.MeshWideValues = layer.Values
}
} else {
m.HasLayer, m.Layer = true, layer.Placement
@@ -85,13 +85,13 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
}
for provision, to := range n.Pins {
if p.Pins == nil {
p.Pins = map[string]delivery.Provider{}
p.Pins = map[string]meshmoduleassigner.Provider{}
}
p.Pins[provision] = delivery.Provider{Node: to.Node, Module: to.Module}
p.Pins[provision] = meshmoduleassigner.Provider{Node: to.Node, Module: to.Module}
}
for _, h := range in.Holdings {
if h.Node == node {
p.Holds = append(p.Holds, delivery.Holding{Claim: h.Claim, Scope: h.Scope, Module: h.Module, Site: h.Site, Kind: h.Kind})
p.Holds = append(p.Holds, meshmoduleassigner.Holding{Claim: h.Claim, Scope: h.Scope, Module: h.Module, Site: h.Site, Kind: h.Kind})
}
}
sort.SliceStable(p.Holds, func(i, j int) bool {
@@ -103,11 +103,11 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
return p, nil
}
func recordTunnelOf(t compose.Tunnel) delivery.RecordTunnel {
out := delivery.RecordTunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU,
func recordTunnelOf(t compose.Tunnel) meshmoduleassigner.FoundTunnel {
out := meshmoduleassigner.FoundTunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: int64(t.Port), Mtu: int64(t.MTU),
Address: t.Address, Range: t.Range, PublicKey: t.PublicKey}
for _, peer := range t.Peers {
out.Peers = append(out.Peers, delivery.RecordTunnelPeer{PublicKey: peer.PublicKey, Address: peer.Address})
out.Peers = append(out.Peers, meshmoduleassigner.FoundTunnelPeer{PublicKey: peer.PublicKey, Address: peer.Address})
}
return out
}
+24 -22
View File
@@ -36,6 +36,8 @@ import (
"errors"
"fmt"
"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"
engcomp "github.com/novox/mesh-host/internal/compositions"
"maps"
"net/netip"
@@ -55,8 +57,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 (delivery.PlacementBucket).
Placements(ctx context.Context) (map[string]delivery.Placement, error)
// Placements is every node's placement, by node (buckets.Placement).
Placements(ctx context.Context) (map[string]meshmoduleassigner.Placement, 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)
@@ -133,7 +135,7 @@ type Mesh struct {
// read it: the check of every node judges placement, and reads no archive.
//
// Placements, when set, stands in for the placement of the nodes it names: a proposal's candidates
// (delivery.PlacementCandidate), so a node composes in shadow with a proposal laid over and compares the result with
// (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.
//
// InOwn is the modules the composing node runs inside its own composition (InOwn of what it runs), composed there again
@@ -146,7 +148,7 @@ type Scope struct {
Node string
Every bool
KeptAt map[string]string
Placements map[string]*delivery.Placement
Placements map[string]*meshmoduleassigner.Placement
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
@@ -167,7 +169,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 delivery.Placement) []string {
func InOwn(running []engcomp.Composed, p meshmoduleassigner.Placement) []string {
placed := map[string]bool{}
for _, m := range p.Modules {
placed[m.Module] = true
@@ -252,7 +254,7 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
// 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]delivery.Placement
placements map[string]meshmoduleassigner.Placement
records map[string]nodeengine.NodeRecord
modules map[string]delivery.Module
// ownRefused is each module placed on the composing node whose manifest the indexer refused, with why, in the order
@@ -292,7 +294,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
return b, fmt.Errorf("frombus: placement: %w", err)
}
if len(s.Placements) > 0 {
given := make(map[string]delivery.Placement, len(placements)+len(s.Placements))
given := make(map[string]meshmoduleassigner.Placement, len(placements)+len(s.Placements))
for node, p := range placements {
given[node] = p
}
@@ -419,7 +421,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
strings.Join(keys, ", "), err)
}
}
if layers := m.Layers(node, values); layers != nil {
if layers := meshmoduleassignerclient.Layers(m, node, values); layers != nil {
if n.Settings == nil {
n.Settings = map[string][]catalogue.Layer{}
}
@@ -468,7 +470,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
}
}
in.Nodes = append(in.Nodes, n)
in.Holdings = append(in.Holdings, p.Held()...)
in.Holdings = append(in.Holdings, meshmoduleassignerclient.Held(p)...)
// A tunnel found at adoption is the node's private network's when the node's tunnel key is the tunnel's (the
// controller's inv.Tunnels); the hub's found tunnel is the mesh's adopted one whether or not the hub took it over
// (inv.AdoptedTunnel), and its peers are carried once it did (inv.CarriedPeers).
@@ -792,7 +794,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) (*delivery.BuilderBuild, error),
in *compose.Input, s Scope, p delivery.Placement, modules map[string]delivery.Module,
in *compose.Input, s Scope, p meshmoduleassigner.Placement, modules map[string]delivery.Module,
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
@@ -999,7 +1001,7 @@ func attachedOf(in []nodeengine.AttachedNetwork) []catalogue.AttachedNetwork {
// nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person
// decided of it, and where its modules run).
func nodeOf(rec nodeengine.NodeRecord, p delivery.Placement) compose.Node {
func nodeOf(rec nodeengine.NodeRecord, p meshmoduleassigner.Placement) compose.Node {
n := compose.Node{
Name: rec.Node,
Place: compose.Place{Key: rec.TunnelKey},
@@ -1019,13 +1021,13 @@ func nodeOf(rec nodeengine.NodeRecord, p delivery.Placement) compose.Node {
// placeNode sets what a person decided of n and where its modules run, from its placement; what the machine observes
// (its keys, links, networks, certificate) and its settings layers are left as they are.
func placeNode(n *compose.Node, p delivery.Placement) {
func placeNode(n *compose.Node, p meshmoduleassigner.Placement) {
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 = p.Assigned(), d.PublicDomain
n.Account, n.AccountHome, n.AgentAccount, n.AgentAccountHome = d.Account, d.AccountHome, d.AgentAccount, d.AgentHome
n.Assigned, n.PublicDomain = meshmoduleassignerclient.Assigned(p), d.PublicDomain
n.Account, n.AccountHome, n.AgentAccount, n.AgentAccountHome = d.Account, d.AccountHome, d.AgentAccount, d.AgentAccountHome
n.Adopted, n.Taken = d.Adopted, d.Taken
n.Pins, n.Bound, n.Ports = p.ChosenPins(), p.Bound(), p.HostPorts()
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
@@ -1035,7 +1037,7 @@ func placeNode(n *compose.Node, p delivery.Placement) {
// 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]*delivery.Placement) (compose.Input, error) {
func Placed(in compose.Input, placements map[string]*meshmoduleassigner.Placement) (compose.Input, error) {
if len(placements) == 0 {
return in, nil
}
@@ -1052,7 +1054,7 @@ func Placed(in compose.Input, placements map[string]*delivery.Placement) (compos
return compose.Input{}, fmt.Errorf("frombus: a placement is given for %s, which the input holds no node for", node)
}
if p == nil {
p = &delivery.Placement{Node: node}
p = &meshmoduleassigner.Placement{Node: node}
}
if p.Node != node {
return compose.Input{}, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node)
@@ -1067,7 +1069,7 @@ func Placed(in compose.Input, placements map[string]*delivery.Placement) (compos
values = l.Values
}
}
if layers := m.Layers(node, values); layers != nil {
if layers := meshmoduleassignerclient.Layers(m, node, values); layers != nil {
settings[m.Module] = layers
}
}
@@ -1085,14 +1087,14 @@ func Placed(in compose.Input, placements map[string]*delivery.Placement) (compos
}
for _, node := range sortedKeys(placements) {
if p := placements[node]; p != nil {
out.Holdings = append(out.Holdings, p.Held()...)
out.Holdings = append(out.Holdings, meshmoduleassignerclient.Held(*p)...)
}
}
return out, nil
}
func tunnelOf(t delivery.RecordTunnel) compose.Tunnel {
out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU,
func tunnelOf(t meshmoduleassigner.FoundTunnel) compose.Tunnel {
out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: int(t.Port), MTU: int(t.Mtu),
Address: t.Address, Range: t.Range, PublicKey: t.PublicKey}
for _, p := range t.Peers {
out.Peers = append(out.Peers, compose.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address})
@@ -1126,7 +1128,7 @@ func carriedPeers(t compose.Tunnel, names map[string]string, records map[string]
}
// 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]delivery.Placement, records map[string]nodeengine.NodeRecord) []string {
func nodesOf(placements map[string]meshmoduleassigner.Placement, records map[string]nodeengine.NodeRecord) []string {
seen := map[string]bool{}
for n := range placements {
seen[n] = true
+34 -32
View File
@@ -13,6 +13,8 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
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"
"github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
@@ -31,7 +33,7 @@ import (
type bus struct {
vaultAuthority string
handed map[string]bool
placements map[string]delivery.Placement
placements map[string]meshmoduleassigner.Placement
records map[string]nodeengine.NodeRecord
modules map[string]delivery.Module
manifests map[string][]byte
@@ -79,7 +81,7 @@ func (b *bus) Served(_ context.Context, node, module string, keys []string) (map
return out, nil
}
func (b *bus) Placements(context.Context) (map[string]delivery.Placement, error) {
func (b *bus) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
return b.placements, nil
}
func (b *bus) VaultAuthority(context.Context) string { return b.vaultAuthority }
@@ -168,19 +170,19 @@ func TestInput(t *testing.T) {
mediaDigest := delivery.ManifestDigest(media)
keys := map[string]string{"anchor": publicKey(t), "laptop": publicKey(t)}
caps := []string{"container-runtime", "systemd", "wireguard"}
tunnel := &delivery.RecordTunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24",
Peers: []delivery.RecordTunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}, {PublicKey: keys["laptop"], Address: "10.77.0.2/32"}}}
tunnel := &meshmoduleassigner.FoundTunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24",
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]delivery.Placement{
"anchor": {Node: "anchor", Decided: delivery.NodeDecided{Endpoint: "anchor.example:51820", Site: "here", Hub: true, Address: "10.77.0.1",
placements: map[string]meshmoduleassigner.Placement{
"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: []delivery.PlacedModule{{Module: overlay.Name}},
Holds: []delivery.Holding{{Claim: "mesh-private-network", Scope: "mesh", Module: overlay.Name}}},
"laptop": {Node: "laptop", Decided: delivery.NodeDecided{Endpoint: "laptop.example:51820", Site: "here", Address: "10.77.0.2"},
Modules: []delivery.PlacedModule{{Module: overlay.Name},
{Module: "xterm", HasLayer: true, Ports: map[string]int{"80": 8080}},
{Module: "media", HasMesh: true, MeshValues: map[string]any{"name": "mesh"}, HasLayer: true,
Modules: []meshmoduleassigner.PlacedModule{{Module: overlay.Name}},
Holds: []meshmoduleassigner.Holding{{Claim: "mesh-private-network", Scope: "mesh", Module: overlay.Name}}},
"laptop": {Node: "laptop", Decided: meshmoduleassigner.NodeDecided{Endpoint: "laptop.example:51820", Site: "here", Address: "10.77.0.2"},
Modules: []meshmoduleassigner.PlacedModule{{Module: overlay.Name},
{Module: "xterm", HasLayer: true, Ports: map[string]int64{"80": 8080}},
{Module: "media", HasMeshWide: true, MeshWideValues: map[string]any{"name": "mesh"}, HasLayer: true,
Layer: map[string]any{"places": map[string]any{"data": "/srv/data"}}, Values: map[string]any{"lines": float64(1000)}}}},
},
records: map[string]nodeengine.NodeRecord{
@@ -291,7 +293,7 @@ func TestInput(t *testing.T) {
running = append(running, c)
}
laptop := mk().placements["laptop"]
asIs, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop}}, fps)
asIs, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*meshmoduleassigner.Placement{"laptop": &laptop}}, fps)
if err == nil && len(held) > 0 {
err = fmt.Errorf("modules held: %v", held)
}
@@ -303,7 +305,7 @@ func TestInput(t *testing.T) {
}
moved := mk().placements["anchor"]
moved.Decided.Endpoint = "anchor.example:51821"
changed, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop, "anchor": &moved}}, fps)
changed, _, held, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*meshmoduleassigner.Placement{"laptop": &laptop, "anchor": &moved}}, fps)
if err == nil && len(held) > 0 {
err = fmt.Errorf("modules held: %v", held)
}
@@ -329,11 +331,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 := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}}
for _, m := range placed {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
}
return &bus{placements: map[string]delivery.Placement{"n1": p},
return &bus{placements: map[string]meshmoduleassigner.Placement{"n1": p},
records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: keys["anchor"], SealingKey: "sk", Capabilities: []string{"systemd"}}},
modules: map[string]delivery.Module{"keeper": {Manifest: delivery.ManifestDigest(keeper)}, "lemurs": {Manifest: delivery.ManifestDigest(lemurs)},
"objects": {Manifest: delivery.ManifestDigest(objects)}, "files": {Manifest: delivery.ManifestDigest(files)}},
@@ -384,9 +386,9 @@ func TestInput(t *testing.T) {
b.placements["n1"] = p
return b
}
tunnelledAs := func() map[string]*delivery.Placement {
tunnelledAs := func() map[string]*meshmoduleassigner.Placement {
p := tunnelled().placements["n1"]
return map[string]*delivery.Placement{"n1": &p}
return map[string]*meshmoduleassigner.Placement{"n1": &p}
}
folded := func() []compositions.Composed {
b := tunnelled()
@@ -434,13 +436,13 @@ func TestInput(t *testing.T) {
if got := InOwn(folded, tunnelled().placements["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]*delivery.Placement {
placedAs := func(modules ...string) map[string]*meshmoduleassigner.Placement {
p := one(modules...).placements["n1"]
return map[string]*delivery.Placement{"n1": &p}
return map[string]*meshmoduleassigner.Placement{"n1": &p}
}
for name, c := range map[string]struct {
b *bus
given map[string]*delivery.Placement
given map[string]*meshmoduleassigner.Placement
running []compositions.Composed
verdict string
check func(nodeengine.CandidateAnswer) bool
@@ -477,7 +479,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]*delivery.Placement{"n1": {Node: "n2"}}, runsNow, nodeengine.CandidateAnswerVerdictRefused,
"cannot compose": {one("keeper"), map[string]*meshmoduleassigner.Placement{"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
@@ -497,16 +499,16 @@ 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]*delivery.Placement{"laptop": &laptop}); err != nil || !reflect.DeepEqual(got, want) {
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]*delivery.Placement{"laptop": nil}); err != nil || len(got.Nodes[1].Assigned) != 0 || got.Nodes[1].Place.Key != keys["laptop"] {
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 _, err := Placed(want, map[string]*delivery.Placement{"stranger": {Node: "stranger"}}); err == nil {
if _, err := Placed(want, map[string]*meshmoduleassigner.Placement{"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]*delivery.Placement{"laptop": {Node: "anchor"}}}); err == nil {
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")
}
@@ -517,8 +519,8 @@ func TestInput(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if gj, _ := delivery.EncodePlacement(got); !delivery.SamePlacement(got, placed) {
pj, _ := delivery.EncodePlacement(placed)
if gj, _ := meshmoduleassignerclient.EncodePlacement(got); !meshmoduleassignerclient.SamePlacement(got, placed) {
pj, _ := meshmoduleassignerclient.EncodePlacement(placed)
t.Errorf("%s exported as %s, want %s", node, gj, pj)
}
}
@@ -617,7 +619,7 @@ func TestInput(t *testing.T) {
movedMesh := func(built bool) *bus {
b := one()
p := b.placements["n1"]
p.Modules = []delivery.PlacedModule{{Module: "store"}, {Module: "mesh-provisioner-client"}, {Module: "restic"}}
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
rec := b.records["n1"]
@@ -766,7 +768,7 @@ func TestInput(t *testing.T) {
withMailer := func(set bool) *bus {
b := mk()
p := b.placements["anchor"]
p.Modules = append(p.Modules, delivery.PlacedModule{Module: "mailer"})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: "mailer"})
b.placements["anchor"] = p
b.modules["mailer"] = delivery.Module{Manifest: delivery.ManifestDigest(mailer)}
b.manifests[delivery.ManifestDigest(mailer)] = mailer
@@ -836,7 +838,7 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates {
}
placements, _ := b.Placements(ctx)
for _, p := range placements {
values, err := delivery.PlacementValues(p)
values, err := meshmoduleassignerclient.PlacementValues(p)
for k, raw := range values {
put(PlacementRead, k, raw, err)
}
@@ -13,6 +13,7 @@ import (
"encoding/json"
"errors"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -25,7 +26,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]delivery.Placement
Placement map[string]meshmoduleassigner.Placement
Record map[string]nodeengine.NodeRecord
Module map[string]delivery.Module
Manifests map[string][]byte
@@ -39,7 +40,7 @@ type Bus struct {
var _ frombus.Reader = (*Bus)(nil)
func (b *Bus) Placements(context.Context) (map[string]delivery.Placement, error) {
func (b *Bus) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
return b.Placement, nil
}
func (b *Bus) Records(context.Context) (map[string]nodeengine.NodeRecord, error) {
@@ -152,9 +153,9 @@ func Provisioned() (*Bus, error) {
digestOf := func(c string) string { return "sha256:" + strings.Repeat(c, 64) }
fps := FPs()
b := &Bus{
Placement: map[string]delivery.Placement{Node: {Node: Node,
Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1", Account: "operator", AccountHome: "/home/operator"},
Modules: []delivery.PlacedModule{{Module: "store"}, {Module: "postgres"}, {Module: "mesh-provisioner-client"}, {Module: "restic"}}}},
Placement: map[string]meshmoduleassigner.Placement{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=",
SealingKey: "sk", Contract: compose.HandedContract, Capabilities: []string{"systemd", "package-manager"}}},
Module: map[string]delivery.Module{}, Manifests: map[string][]byte{},
@@ -5,6 +5,8 @@ import (
"encoding/json"
"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/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
engcomp "github.com/novox/mesh-host/internal/compositions"
"reflect"
"slices"
@@ -67,7 +69,7 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
if row.bad != "" {
if !row.elsewhere {
p := b.Placement[Node]
p.Modules = append(p.Modules, delivery.PlacedModule{Module: "backup"})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: "backup"})
b.Placement[Node] = p
}
digest := delivery.ManifestDigest([]byte(row.bad))
@@ -277,7 +279,7 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
} 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]
p.Modules = slices.DeleteFunc(slices.Clone(p.Modules), func(m delivery.PlacedModule) bool { return m.Module == "mesh-provisioner-client" })
p.Modules = slices.DeleteFunc(slices.Clone(p.Modules), func(m meshmoduleassigner.PlacedModule) bool { return m.Module == "mesh-provisioner-client" })
bb.Placement[Node] = p
want = ""
}
@@ -311,7 +313,7 @@ func statesOf(t *testing.T, b *Bus) frombus.States {
s[state][key] = raw
}
for node, p := range b.Placement {
raw, err := delivery.EncodePlacement(p)
raw, err := meshmoduleassignerclient.EncodePlacement(p)
put(frombus.PlacementRead, "placement."+node, raw, err)
}
for node, rec := range b.Record {
+4 -3
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
engcomp "github.com/novox/mesh-host/internal/compositions"
"strings"
"testing"
@@ -18,18 +19,18 @@ 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 := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
p := meshmoduleassigner.Placement{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]delivery.Module{}, manifests: map[string][]byte{}, builds: map[string]*delivery.BuilderBuild{}}
fps := map[string]string{}
for i, raw := range manifests {
module := strings.SplitN(strings.TrimPrefix(raw, `{"module":"`), `"`, 2)[0]
p.Modules = append(p.Modules, delivery.PlacedModule{Module: module})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: module})
b.modules[module] = delivery.Module{Manifest: delivery.ManifestDigest([]byte(raw))}
b.manifests[delivery.ManifestDigest([]byte(raw))] = []byte(raw)
fps[module] = "src2:" + strings.Repeat(string(rune('1'+i)), 64)
}
b.placements = map[string]delivery.Placement{"n1": p}
b.placements = map[string]meshmoduleassigner.Placement{"n1": p}
return b, fps
}
+10 -6
View File
@@ -14,6 +14,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/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"strings"
"time"
@@ -24,10 +26,12 @@ import (
// The states a module names in its manifest's `reads` to compose through StatesReader.
const (
PlacementRead = delivery.PlacementRead
ModulesRead = delivery.IndexerModule + ".modules"
ManifestsRead = delivery.IndexerModule + ".manifests"
BuildsRead = delivery.BuilderModule + ".builds"
// 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 = "mesh-delivery-assigner.placement"
ModulesRead = "mesh-delivery-indexer.modules"
ManifestsRead = "mesh-delivery-indexer.manifests"
BuildsRead = "mesh-delivery-builder.builds"
HaveRead = "node-engine.have"
ResolvedRead = "mesh-config.resolved"
)
@@ -78,8 +82,8 @@ func (r statesReader) each(read string, visit func(key string, value []byte)) er
return nil
}
func (r statesReader) Placements(context.Context) (map[string]delivery.Placement, error) {
keys := delivery.NewPlacementKeys()
func (r statesReader) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
keys := meshmoduleassignerclient.NewKeys()
err := r.each(PlacementRead, func(k string, v []byte) {
_, _ = keys.Put(k, v, 0, false) // a key that does not read places nothing, as before
})
+23 -1
View File
@@ -10,6 +10,7 @@ package composer
import (
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"slices"
"sort"
"strings"
@@ -131,7 +132,11 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) {
if err != nil {
return CaseVerdict{}, err
}
in, err := frombus.Placed(base, c.Placements)
placements, err := interimPlacements(c.Placements)
if err != nil {
return CaseVerdict{}, fmt.Errorf("composer: the case %q: %w", c.Name, err)
}
in, err := frombus.Placed(base, placements)
if err != nil {
return CaseVerdict{}, fmt.Errorf("composer: the case %q: %w", c.Name, err)
}
@@ -340,3 +345,20 @@ func SentInput(in compose.Input, node string) compose.Input {
}
return in
}
// interimPlacements carries the case's placements, typed as mesh-sdk's composer still types them, over to the seat's
// type through JSON. It goes when this package takes the case from the delivery SDK's composer, which holds them raw.
func interimPlacements(in map[string]*delivery.Placement) (map[string]*meshmoduleassigner.Placement, error) {
if in == nil {
return nil, nil
}
raw, err := json.Marshal(in)
if err != nil {
return nil, err
}
var out map[string]*meshmoduleassigner.Placement
if err := json.Unmarshal(raw, &out); err != nil {
return nil, err
}
return out, nil
}
+3 -2
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
engcomp "github.com/novox/mesh-host/internal/compositions"
"maps"
"slices"
@@ -451,9 +452,9 @@ func (c *Composer) composeShadow(ctx context.Context, running []engcomp.Composed
}
kept[module] = fp
}
wants := map[string]delivery.Want{agent.NodeModule: {}}
wants := map[string]meshwants.WantsByWantNodeModuleValue{agent.NodeModule: {}}
for module, fp := range fps {
wants[module] = delivery.Want{FP: fp}
wants[module] = meshwants.WantsByWantNodeModuleValue{Fp: fp}
}
c.mu.Lock()
c.wants = wants
+8 -6
View File
@@ -3,6 +3,8 @@ 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"
"github.com/novox/mesh-host/internal/buckets"
"strings"
"sync/atomic"
@@ -82,8 +84,8 @@ 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, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}}})
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{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, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
deadline := time.Now().Add(5 * time.Second)
@@ -91,10 +93,10 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
for watching() < 6 && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond)
}
b.put(t, delivery.PlacementBucket, "placement.n1", place)
b.put(t, buckets.Placement, "placement.n1", place)
b.put(t, buckets.Have, "n1.node", have)
b.put(t, delivery.ModulesBucket, "lemurs", mod)
for _, bucket := range []string{delivery.PlacementCandidatesBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, ResolvedBucket} {
for _, bucket := range []string{buckets.PlacementCandidates, buckets.Placement, buckets.Have, delivery.ModulesBucket, ResolvedBucket} {
b.send(bucket, agent.Update{Caught: true})
}
for !s.ready.Load() && time.Now().Before(deadline) {
@@ -180,8 +182,8 @@ 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"] = delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}}}
s.c.placements["n1"] = meshmoduleassigner.Placement{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"] = delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}
s.c.modules["other"] = delivery.Module{FP2: fp, Manifest: digestOther, Repository: "novox/x", Commit: "c"}
+1 -1
View File
@@ -257,7 +257,7 @@ func (c *Composer) pastKeeping() []string {
c.mu.Lock()
wanted := map[string]string{}
for module, w := range c.wants {
wanted[module] = w.FP
wanted[module] = w.Fp
}
c.mu.Unlock()
type kept struct {
+13 -9
View File
@@ -4,7 +4,11 @@ import (
"context"
"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/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"github.com/novox/mesh-host/internal/buckets"
"github.com/novox/mesh-host/internal/buckets/bucketstest"
engcomp "github.com/novox/mesh-host/internal/compositions"
"slices"
"strings"
@@ -86,20 +90,20 @@ func TestTheNodePublishesItsCompositions(t *testing.T) {
if err != nil {
t.Fatal(err)
}
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}})
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{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, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
omod, _ := delivery.EncodeModule(delivery.Module{FP2: ofp, Manifest: odigest, Repository: "novox/o", Commit: "c"})
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
b.put(t, delivery.PlacementBucket, "placement.n1", place)
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.put(t, buckets.Have, "n1.node", have)
b.put(t, delivery.ModulesBucket, "lemurs", mod)
b.put(t, delivery.ModulesBucket, "oldie", omod)
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant)
for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} {
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, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} {
b.send(bucket, agent.Update{Caught: true})
}
for u := next(t, out); !u.Caught; u = next(t, out) {
@@ -170,7 +174,7 @@ func TestTheNodePublishesItsCompositions(t *testing.T) {
if past := keep.pastKeeping(); !slices.Equal(past, []string{keys[0]}) {
t.Errorf("past keeping: %v, want the oldest of six, %s", past, keys[0])
}
keep.wants = map[string]delivery.Want{"lemurs": {FP: delivery.SourcePrefix2 + strings.Repeat("1", 64)}}
keep.wants = map[string]meshwants.WantsByWantNodeModuleValue{"lemurs": {Fp: delivery.SourcePrefix2 + strings.Repeat("1", 64)}}
if past := keep.pastKeeping(); len(past) != 0 {
t.Errorf("past keeping with a want naming the oldest: %v, want none", past)
}
+35 -32
View File
@@ -21,6 +21,9 @@ 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/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"github.com/novox/mesh-host/internal/buckets"
"github.com/novox/mesh-host/internal/compositions"
engcomp "github.com/novox/mesh-host/internal/compositions"
@@ -129,11 +132,11 @@ type Composer struct {
env Env
mu sync.Mutex
wants map[string]delivery.Want // by module, the node's own under agent.NodeModule
placements map[string]delivery.Placement
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
// (delivery.PlacementKeys; research 072 row 37): a write of the holder's under way is never composed half.
placementKeys *delivery.PlacementKeys
// (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
// 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).
@@ -300,7 +303,7 @@ func New(env Env) *Composer {
if env.Say == nil {
env.Say = func(string) {}
}
return &Composer{env: env, wants: map[string]delivery.Want{}, placements: map[string]delivery.Placement{}, placementKeys: delivery.NewPlacementKeys(),
return &Composer{env: env, wants: map[string]meshwants.WantsByWantNodeModuleValue{}, placements: map[string]meshmoduleassigner.Placement{}, placementKeys: meshmoduleassignerclient.NewKeys(),
records: map[string]nodeengine.NodeRecord{}, inOwn: map[string]bool{}, modules: map[string]delivery.Module{}, values: map[string]map[string]any{},
manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]delivery.BuilderBuild{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{},
pkgs: map[string]compositions.Composed{}, asked: make(chan chan Pass, 8)}
@@ -355,8 +358,8 @@ type input struct {
func (c *Composer) inputs() []input {
node := c.env.Node
return []input{
{delivery.AssignerWantsBucket, "want." + node + ".>", (*Composer).takeWant, nil},
{delivery.PlacementBucket, ">", (*Composer).takePlacement, nil},
{buckets.Wants, "want." + node + ".>", (*Composer).takeWant, nil},
{buckets.Placement, ">", (*Composer).takePlacement, 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).
@@ -395,7 +398,7 @@ func (c *Composer) takeBuildDone(u agent.Update) bool {
c.mu.Unlock()
// A module placed here is composed at its trunk's build once that is done, for the composition state, before its
// want moves (ADR 0593 §6).
if !(wanted && w.FP == fp) && !((wanted || placedHere) && m.FP2 == fp) {
if !(wanted && w.Fp == fp) && !((wanted || placedHere) && m.FP2 == fp) {
return false
}
b, err := delivery.DecodeBuilderBuild(u.Value)
@@ -604,22 +607,22 @@ func (c *Composer) entry(bucket, key string) (v any, has, known bool) {
c.mu.Lock()
defer c.mu.Unlock()
switch bucket {
case delivery.AssignerWantsBucket:
case buckets.Wants:
// A want is composed by its fingerprint alone (fingerprints); its words, rule and newest change nothing composed.
module := agent.NodeModule
if _, err := delivery.ParseWantNodeKey(key); err != nil {
node, m, err := delivery.ParseWantKey(key)
if err != nil || node != c.env.Node {
if _, ok := meshwants.ParseWantsByWantNodeNodeKey(key); !ok {
node, m, ok := meshwants.ParseWantsByWantNodeModuleKey(key)
if !ok || node != c.env.Node {
return nil, false, true
}
module = m
}
w, ok := c.wants[module]
return w.FP, ok, true
case delivery.PlacementBucket:
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
// module key written before its node key names it changes nothing composed.
node, _, _, err := delivery.ParsePlacementKey(key)
node, _, _, err := meshmoduleassignerclient.ParseKey(key)
if err != nil {
return nil, false, true
}
@@ -650,20 +653,20 @@ func (c *Composer) entry(bucket, key string) (v any, has, known bool) {
func (c *Composer) takeWant(u agent.Update) {
c.mu.Lock()
defer c.mu.Unlock()
if _, err := delivery.ParseWantNodeKey(u.Key); err == nil {
if _, ok := meshwants.ParseWantsByWantNodeNodeKey(u.Key); ok {
if u.Deleted {
delete(c.wants, agent.NodeModule)
} else {
c.wants[agent.NodeModule] = delivery.Want{}
c.wants[agent.NodeModule] = meshwants.WantsByWantNodeModuleValue{}
}
return
}
node, module, err := delivery.ParseWantKey(u.Key)
if err != nil || node != c.env.Node {
node, module, ok := meshwants.ParseWantsByWantNodeModuleKey(u.Key)
if !ok || node != c.env.Node {
return
}
w, err := delivery.DecodeWant(u.Value)
if u.Deleted || err != nil || w.Removed || w.FP == "" {
w, err := meshwants.DecodeWantsByWantNodeModuleValue(u.Value)
if u.Deleted || err != nil || w.Removed || w.Fp == "" {
delete(c.wants, module)
return
}
@@ -676,7 +679,7 @@ func (c *Composer) takePlacement(u agent.Update) {
}
c.mu.Lock()
if c.placementKeys == nil {
c.placementKeys = delivery.NewPlacementKeys()
c.placementKeys = meshmoduleassignerclient.NewKeys()
}
node, err := c.placementKeys.Put(u.Key, u.Value, u.Revision, u.Deleted || len(u.Value) == 0)
if node != "" {
@@ -769,7 +772,7 @@ func (c *Composer) Machines() (byAddress map[string]string, hub string) {
return byAddress, hub
}
func (c *Composer) inOwnOf(p delivery.Placement) []string {
func (c *Composer) inOwnOf(p meshmoduleassigner.Placement) []string {
var out []string
for _, m := range p.Modules {
if c.inOwn[m.Module] {
@@ -859,8 +862,8 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
if module == agent.NodeModule {
continue
}
fps[module] = w.FP
if m, ok := c.modules[module]; ok && m.FP2 != w.FP {
fps[module] = w.Fp
if m, ok := c.modules[module]; ok && m.FP2 != w.Fp {
behind = append(behind, module)
}
}
@@ -1310,7 +1313,7 @@ var _ frombus.SignersReader = reader{}
// certificateWanted says whether a module the placement 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 delivery.Placement) bool {
func (c *Composer) certificateWanted(ctx context.Context, r reader, p meshmoduleassigner.Placement) bool {
for _, m := range p.Modules {
c.mu.Lock()
digest := c.modules[m.Module].Manifest
@@ -1338,7 +1341,7 @@ func (c *Composer) certificateWanted(ctx context.Context, r reader, p delivery.P
// 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 delivery.Placement) {
func (c *Composer) certifyIfWanted(ctx context.Context, r reader, p meshmoduleassigner.Placement) {
if c.env.Certify == nil || !c.certificateWanted(ctx, r, p) {
return
}
@@ -1367,10 +1370,10 @@ func (c *Composer) certifyIfWanted(ctx context.Context, r reader, p delivery.Pla
c.mu.Unlock()
}
func (r reader) Placements(context.Context) (map[string]delivery.Placement, error) {
func (r reader) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
r.c.mu.Lock()
defer r.c.mu.Unlock()
out := make(map[string]delivery.Placement, len(r.c.placements))
out := make(map[string]meshmoduleassigner.Placement, len(r.c.placements))
for k, v := range r.c.placements {
out[k] = v
}
@@ -1719,9 +1722,9 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string
if m == agent.NodeModule {
continue
}
fps[m] = w.FP
if mm, ok := c.modules[m]; ok && mm.FP2 != w.FP {
kept[m] = w.FP
fps[m] = w.Fp
if mm, ok := c.modules[m]; ok && mm.FP2 != w.Fp {
kept[m] = w.Fp
}
}
fps[module] = fp
+105 -101
View File
@@ -10,7 +10,11 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
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"
"github.com/novox/mesh-host/internal/buckets"
"github.com/novox/mesh-host/internal/buckets/bucketstest"
"github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
@@ -189,21 +193,21 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
okey, _ := delivery.ManifestsKey(odigest)
b.kv[delivery.ManifestsBucket+"/"+okey] = oman
omod, _ := delivery.EncodeModule(delivery.Module{FP2: delivery.SourcePrefix2 + strings.Repeat("c", 64), Manifest: odigest, Repository: "novox/o", Commit: "c"})
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}})
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{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, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
b.put(t, delivery.PlacementBucket, "placement.n1", place)
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.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)
b.put(t, delivery.ModulesBucket, "lemurs", mod)
b.put(t, delivery.ModulesBucket, "oldie", omod)
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant)
for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} {
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, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} {
b.send(bucket, agent.Update{Caught: true})
}
got := map[string]bool{}
@@ -431,22 +435,22 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
b.mu.Unlock()
mfp := delivery.SourcePrefix2 + strings.Repeat(map[string]string{"de-spiegel": "d", "proxy": "e"}[m], 64)
c3.modules[m] = delivery.Module{FP2: mfp, Manifest: dig, Repository: "novox/" + m, Commit: "c"}
c3.wants[m] = delivery.Want{FP: mfp, Self: true}
c3.wants[m] = meshwants.WantsByWantNodeModuleValue{Fp: mfp, Self: true}
}
p3 := c3.placements["n1"]
p3.Modules = append(append([]delivery.PlacedModule{}, p3.Modules...), delivery.PlacedModule{Module: "proxy"}, delivery.PlacedModule{Module: "de-spiegel"})
p3.Modules = append(append([]meshmoduleassigner.PlacedModule{}, p3.Modules...), meshmoduleassigner.PlacedModule{Module: "proxy"}, meshmoduleassigner.PlacedModule{Module: "de-spiegel"})
c3.placements["n1"] = p3
r3 := c3.records["n1"]
r3.Capabilities = []string{"systemd", "container-runtime"}
c3.records["n1"] = r3
spiegelKey, _ := compositions.Key("n1", "de-spiegel", c3.wants["de-spiegel"].FP)
proxyKey, _ := compositions.Key("n1", "proxy", c3.wants["proxy"].FP)
spiegelKey, _ := compositions.Key("n1", "de-spiegel", c3.wants["de-spiegel"].Fp)
proxyKey, _ := compositions.Key("n1", "proxy", c3.wants["proxy"].Fp)
c3.handed[spiegelKey] = "sha256:" + strings.Repeat("5", 64) // what the node runs of it, handed before
handed3 := map[string]agent.Update{}
for _, u := range c3.compose(ctx) {
handed3[u.Key] = u
}
lemursKey3, _ := compositions.Key("n1", "lemurs", c3.wants["lemurs"].FP)
lemursKey3, _ := compositions.Key("n1", "lemurs", c3.wants["lemurs"].Fp)
if u, ok := handed3[lemursKey3]; !ok || u.Deleted {
t.Errorf("lemurs, which composes, is not handed beside de-spiegel's refusal: %v (told %q)", handed3, *told)
}
@@ -486,9 +490,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
c4.modules["power"] = delivery.Module{FP2: powerFP, Manifest: powerDig, Repository: "novox/mesh-catalog", Commit: "ca8d6f33",
// The indexer's verdict on the captured manifest (ADR 0578): the engine judges nothing, it takes this.
Verdict: delivery.VerdictRefused, Problems: []string{`bar item "shows" is none of them`}}
c4.wants["power"] = delivery.Want{FP: powerFP, Self: true}
c4.wants["power"] = meshwants.WantsByWantNodeModuleValue{Fp: powerFP, Self: true}
p4 := c4.placements["n1"]
p4.Modules = append(append([]delivery.PlacedModule{}, p4.Modules...), delivery.PlacedModule{Module: "power"})
p4.Modules = append(append([]meshmoduleassigner.PlacedModule{}, p4.Modules...), meshmoduleassigner.PlacedModule{Module: "power"})
c4.placements["n1"] = p4
handed4 := map[string]agent.Update{}
for _, u := range c4.compose(ctx) {
@@ -506,7 +510,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
// still carries its resources, so retiring the read removes nothing of it.
c5, told5 := fresh(env.Get)
p5 := c5.placements["n1"]
p5.Modules = append(append([]delivery.PlacedModule{}, p5.Modules...), delivery.PlacedModule{Module: overlay.Name})
p5.Modules = append(append([]meshmoduleassigner.PlacedModule{}, p5.Modules...), meshmoduleassigner.PlacedModule{Module: overlay.Name})
p5.Decided.Endpoint = "n1.example:51820"
c5.placements["n1"] = p5
c5.inOwn[overlay.Name] = true
@@ -587,14 +591,14 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
// Issue 774, 2026-10-31: every put under want.n1.> made a pass, and each pass asked the vault for the node's own
// secrets. A key written again with what it held, or a want whose words moved and not its build, makes none.
worded, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true, Words: "lemurs is held at aaaaaaaaaaaa"})
elsewhere, _ := delivery.EncodeWant(delivery.Want{FP: delivery.SourcePrefix2 + strings.Repeat("e", 64), Self: true})
worded, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true, Words: "lemurs is held at aaaaaaaaaaaa"})
elsewhere, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: delivery.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 := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}}
placeKeys := delivery.NewPlacementKeys()
keyRows := func(after delivery.Placement, what string) []struct {
placeBase := meshmoduleassigner.Placement{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 {
name, bucket, key string
value []byte
pass bool
@@ -614,13 +618,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
name, bucket, key string
value []byte
pass bool
}{what + ": " + w.Key, delivery.PlacementBucket, w.Key, w.Value, w.Commit && what != "converted"})
}{what + ": " + w.Key, buckets.Placement, w.Key, w.Value, w.Commit && what != "converted"})
}
return rows
}
_, _ = placeKeys.Put("placement.n1", place, 1, false)
lemursMoved := placeBase
lemursMoved.Modules = []delivery.PlacedModule{{Module: "lemurs", Values: map[string]any{"greeting": "hoi"}}, {Module: "oldie"}}
lemursMoved.Modules = []meshmoduleassigner.PlacedModule{{Module: "lemurs", Values: map[string]any{"greeting": "hoi"}}, {Module: "oldie"}}
var placementRows []struct {
name, bucket, key string
value []byte
@@ -628,7 +632,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
for _, step := range []struct {
what string
p delivery.Placement
p meshmoduleassigner.Placement
}{{"converted", placeBase}, {"lemurs moved", lemursMoved}, {"lemurs back", placeBase}} {
placementRows = append(placementRows, keyRows(step.p, step.what)...)
}
@@ -637,13 +641,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
value []byte
pass bool
}{
{"lemurs's want again", delivery.AssignerWantsBucket, "want.n1.lemurs", want, false},
{"the node's own want again", delivery.AssignerWantsBucket, "want.n1.node", nodeWant, false},
{"lemurs's want with other words", delivery.AssignerWantsBucket, "want.n1.lemurs", worded, false},
{"the placement again", delivery.PlacementBucket, "placement.n1", place, false},
{"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},
{"lemurs's module again", delivery.ModulesBucket, "lemurs", mod, false},
{"lemurs's want at another build", delivery.AssignerWantsBucket, "want.n1.lemurs", elsewhere, true},
{"lemurs's want back at its build", delivery.AssignerWantsBucket, "want.n1.lemurs", want, true},
{"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...) {
before := passes()
b.put(t, row.bucket, row.key, row.value)
@@ -690,18 +694,18 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
if err != nil {
t.Fatal(err)
}
tplace, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "tool"}}})
tplace, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []meshmoduleassigner.PlacedModule{{Module: "tool"}}})
tmod, _ := delivery.EncodeModule(delivery.Module{FP2: next2, Manifest: tdigest, Repository: "novox/t", Commit: "c"})
twant, _ := delivery.EncodeWant(delivery.Want{FP: next2, Self: true})
twant, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: next2, Self: true})
building, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: "building"})
bkey, _ := delivery.BuilderBuildKey("tool", next2)
tb.kv[delivery.BuilderBuildsBucket+"/"+bkey] = building
tb.put(t, delivery.PlacementBucket, "placement.n1", tplace)
tb.put(t, buckets.Placement, "placement.n1", tplace)
tb.put(t, buckets.Have, "n1.node", have)
tb.put(t, delivery.ModulesBucket, "tool", tmod)
tb.put(t, delivery.AssignerWantsBucket, "want.n1.tool", twant)
for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket,
tb.put(t, buckets.Wants, "want.n1.tool", twant)
for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, delivery.ModulesBucket,
ResolvedBucket, delivery.BuilderBuildsBucket} {
tb.send(bucket, agent.Update{Caught: true})
}
@@ -729,18 +733,18 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
// A want removed takes its composition back.
removed, _ := delivery.EncodeWant(delivery.Want{Removed: true})
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", removed)
removed, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Removed: true})
b.put(t, buckets.Wants, "want.n1.lemurs", removed)
if u := next(t, out); u.Key != lemursKey || !u.Deleted {
t.Fatalf("a removed want handed %+v", u)
}
// A want of a module whose manifest cannot be read: said once, nothing handed, what was handed stands.
gone := delivery.SourcePrefix2 + strings.Repeat("b", 64)
missing, _ := delivery.EncodeWant(delivery.Want{FP: gone, Self: true})
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", missing)
missing, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: gone, Self: true})
b.put(t, buckets.Wants, "want.n1.lemurs", missing)
quiet(t, out)
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", missing)
b.put(t, buckets.Wants, "want.n1.lemurs", missing)
quiet(t, out)
b.mu.Lock()
n := 0
@@ -756,7 +760,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
// An own secret comes sealed from the vault into the composition; sealed again at the same generation it is the value
// first had, so nothing is handed again; a new generation is handed.
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
b.put(t, buckets.Wants, "want.n1.lemurs", want)
if u := next(t, out); u.Key != lemursKey {
t.Fatalf("lemurs wanted back handed %s", u.Key)
}
@@ -769,21 +773,21 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
b.kv[delivery.ManifestsBucket+"/"+kkey] = kman
b.mu.Unlock()
kmod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: kdigest, Repository: "novox/k", Commit: "c"})
both, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}, {Module: "keeper"}}})
both, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}, {Module: "keeper"}}})
b.put(t, delivery.ModulesBucket, "keeper", kmod)
// n1 is held in keys per module and node since the rows above, which a legacy key no longer moves.
bothPlaced, _ := delivery.DecodePlacement(both)
bothPlaced, _ := meshmoduleassignerclient.DecodePlacement(both)
bothWrites, err := placeKeys.Writes("n1", &bothPlaced)
if err != nil {
t.Fatal(err)
}
for _, w := range bothWrites {
if w.Value != nil {
b.put(t, delivery.PlacementBucket, w.Key, w.Value)
b.put(t, buckets.Placement, w.Key, w.Value)
}
}
b.put(t, delivery.AssignerWantsBucket, "want.n1.keeper", want)
b.put(t, buckets.Wants, "want.n1.keeper", want)
keeperKey, _ := compositions.Key("n1", "keeper", fp)
var handed string
for handed == "" {
@@ -1031,12 +1035,12 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
// 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 == delivery.PlacementCandidatesBucket {
if in.bucket == buckets.PlacementCandidates {
t.Fatal("the node's own composer reads placement candidates")
}
}
sb := newPatternBus()
trunk := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []delivery.PlacedModule{{Module: "lemurs"}}}
trunk := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}}
mods := map[string]delivery.Module{"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{delivery.ManifestsBucket + "/" + key: man, delivery.ManifestsBucket + "/" + kkey: kman}}
@@ -1050,7 +1054,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]delivery.Placement{"n1": trunk},
running, _, _, err := frombus.Compose(ctx, fixedReader{placements: map[string]meshmoduleassigner.Placement{"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 {
@@ -1081,47 +1085,47 @@ 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(delivery.PlacementCandidatesBucket, true)
sb.missing(buckets.PlacementCandidates, 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(delivery.PlacementCandidatesBucket, false)
sb.missing(buckets.PlacementCandidates, false)
sb.made(6)
rec, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
tp, _ := delivery.EncodePlacement(trunk)
sb.put(delivery.PlacementBucket, "placement.n1", tp, 1)
tp, _ := meshmoduleassignerclient.EncodePlacement(trunk)
sb.put(buckets.Placement, "placement.n1", tp, 1)
sb.put(buckets.Have, "n1.node", rec, 1)
for name, m := range mods {
raw, _ := delivery.EncodeModule(m)
sb.put(delivery.ModulesBucket, name, raw, 1)
}
wantLemurs, _ := delivery.WantKey("n1", "lemurs")
wantLemurs := meshwants.WantsByWantNodeModuleKey("n1", "lemurs")
var lemursDigest string
for _, c := range running {
if c.Module == "lemurs" {
lemursDigest, _ = compositions.DigestOf(c)
}
}
wantRaw, err := delivery.EncodeWant(delivery.Want{FP: fp, Composed: lemursDigest})
wantRaw, err := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Composed: lemursDigest})
if err != nil {
t.Fatal(err)
}
sb.put(delivery.AssignerWantsBucket, wantLemurs, wantRaw, 1)
wantNode, _ := delivery.WantNodeKey("n1")
nodeRaw, _ := delivery.EncodeWant(delivery.Want{})
sb.put(delivery.AssignerWantsBucket, wantNode, nodeRaw, 1)
sb.put(buckets.Wants, wantLemurs, wantRaw, 1)
wantNode := meshwants.WantsByWantNodeNodeKey("n1")
nodeRaw, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{})
sb.put(buckets.Wants, wantNode, nodeRaw, 1)
candidate := func(group string, rev uint64, modules ...string) {
p := trunk
p.Modules = nil
for _, m := range modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
}
raw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: group, Placement: &p})
raw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: group, Placement: &p})
if err != nil {
t.Fatal(err)
}
k, _ := delivery.PlacementCandidateKey(group, "n1")
sb.put(delivery.PlacementCandidatesBucket, k, raw, rev)
k := meshmoduleassigner.CandidatesByProposalNodeKey(group, "n1")
sb.put(buckets.PlacementCandidates, k, raw, rev)
}
candidate("same", 3, "lemurs")
candidate("adds", 4, "lemurs", "keeper")
@@ -1129,20 +1133,20 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
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
// compositions, answered as such, not as every module removed.
if raw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "deletes"}); err != nil {
if raw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "deletes"}); err != nil {
t.Fatal(err)
} else {
k, _ := delivery.PlacementCandidateKey("deletes", "n1")
sb.put(delivery.PlacementCandidatesBucket, k, raw, 7)
k := meshmoduleassigner.CandidatesByProposalNodeKey("deletes", "n1")
sb.put(buckets.PlacementCandidates, 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
// the proposal takes away.
if raw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "reach", Reached: true}); err != nil {
if raw, err := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "reach", Reached: true}); err != nil {
t.Fatal(err)
} else {
k, _ := delivery.PlacementCandidateKey("reach", "n1")
sb.put(delivery.PlacementCandidatesBucket, k, raw, 8)
k := meshmoduleassigner.CandidatesByProposalNodeKey("reach", "n1")
sb.put(buckets.PlacementCandidates, k, raw, 8)
}
sb.caught()
var answers map[string]nodeengine.CandidateAnswer
@@ -1247,7 +1251,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(delivery.PlacementBucket, "placement.n1", tp, 2)
sb.put(buckets.Placement, "placement.n1", tp, 2)
lraw, _ := delivery.EncodeModule(mods["lemurs"])
sb.put(delivery.ModulesBucket, "lemurs", lraw, 2)
drain()
@@ -1300,18 +1304,18 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
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)" {
t.Errorf("an answer composed again after the bus did not answer said %q", got)
}
k, _ := delivery.PlacementCandidateKey("same", "n1")
sb.del(delivery.PlacementCandidatesBucket, k)
k := meshmoduleassigner.CandidatesByProposalNodeKey("same", "n1")
sb.del(buckets.PlacementCandidates, 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(delivery.PlacementCandidatesBucket, true)
sb.missing(buckets.PlacementCandidates, true)
sb.end(delivery.ModulesBucket)
dropsKey, _ := delivery.PlacementCandidateKey("drops", "n1")
sb.del(delivery.PlacementCandidatesBucket, dropsKey)
dropsKey := meshmoduleassigner.CandidatesByProposalNodeKey("drops", "n1")
sb.del(buckets.PlacementCandidates, dropsKey)
time.Sleep(30 * time.Millisecond)
sb.missing(delivery.PlacementCandidatesBucket, false)
sb.missing(buckets.PlacementCandidates, 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
@@ -1324,13 +1328,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
env := refused.env()
watch := env.Watch
env.Watch = func(ctx context.Context, bucket, pattern string) (<-chan agent.Update, error) {
if bucket == delivery.PlacementBucket {
if bucket == buckets.Placement {
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(), delivery.PlacementBucket) || !strings.Contains(err.Error(), "cannot be watched") {
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)
}
}
@@ -1351,20 +1355,20 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
defer ncancel()
go func() { _ = nsh.Run(nctx) }()
nb.made(6)
nb.put(delivery.PlacementBucket, "placement.n1", tp, 1)
nb.put(buckets.Placement, "placement.n1", tp, 1)
nb.put(buckets.Have, "n1.node", rec, 1)
for name, m := range mods {
raw, _ := delivery.EncodeModule(m)
nb.put(delivery.ModulesBucket, name, raw, 1)
}
sameRaw, _ := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "same", Placement: &trunk})
sameKey, _ := delivery.PlacementCandidateKey("same", "n1")
nb.put(delivery.PlacementCandidatesBucket, sameKey, sameRaw, 3)
sameRaw, _ := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "same", Placement: &trunk})
sameKey := meshmoduleassigner.CandidatesByProposalNodeKey("same", "n1")
nb.put(buckets.PlacementCandidates, sameKey, sameRaw, 3)
// Issue 1126, T3: an older group (a closed pull request's candidate, rev 2) is answered after the newest.
oldRaw, _ := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "older", Placement: &trunk})
oldKey, _ := delivery.PlacementCandidateKey("older", "n1")
nb.put(delivery.PlacementCandidatesBucket, oldKey, oldRaw, 2)
for _, bucket := range []string{delivery.PlacementCandidatesBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, delivery.BuilderBuildsBucket} {
oldRaw, _ := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "older", Placement: &trunk})
oldKey := meshmoduleassigner.CandidatesByProposalNodeKey("older", "n1")
nb.put(buckets.PlacementCandidates, oldKey, oldRaw, 2)
for _, bucket := range []string{buckets.PlacementCandidates, buckets.Placement, buckets.Have, delivery.ModulesBucket, delivery.BuilderBuildsBucket} {
nb.send(bucket, agent.Update{Caught: true})
}
select {
@@ -1446,7 +1450,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, delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "nats"}, {Module: "keeper"}}},
gotFps := shadowBuilds(inOwnRunning, meshmoduleassigner.Placement{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "nats"}, {Module: "keeper"}}},
map[string]delivery.Module{"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)
@@ -1511,12 +1515,12 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
cc.manifests[cdigest] = []byte(withCert)
cc.modules = map[string]delivery.Module{"lemurs": {Manifest: digest}, "web": {Manifest: cdigest}}
cc.records["n1"] = nodeengine.NodeRecord{Node: "n1", ServingKey: base64.StdEncoding.EncodeToString(make([]byte, 32))}
onlyLemurs := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "lemurs"}}}
onlyLemurs := meshmoduleassigner.Placement{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 := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "web"}}}
withWeb := meshmoduleassigner.Placement{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 {
@@ -1657,13 +1661,13 @@ func (b *patternBus) caught() {
// fixedReader is frombus's Reader over values held, for composing what a node runs.
type fixedReader struct {
placements map[string]delivery.Placement
placements map[string]meshmoduleassigner.Placement
records map[string]nodeengine.NodeRecord
modules map[string]delivery.Module
manifests map[string][]byte
}
func (r fixedReader) Placements(context.Context) (map[string]delivery.Placement, error) {
func (r fixedReader) Placements(context.Context) (map[string]meshmoduleassigner.Placement, error) {
return r.placements, nil
}
func (r fixedReader) Records(context.Context) (map[string]nodeengine.NodeRecord, error) {
@@ -1783,18 +1787,18 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) {
if err != nil {
t.Fatal(err)
}
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}}})
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{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, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
b.put(t, delivery.PlacementBucket, "placement.n1", place)
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.put(t, buckets.Have, "n1.node", have)
b.put(t, delivery.ModulesBucket, "lemurs", mod)
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant)
for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} {
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, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} {
b.send(bucket, agent.Update{Caught: true})
}
handed := map[string]bool{}
+12 -10
View File
@@ -7,6 +7,8 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
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"
"github.com/novox/mesh-host/internal/buckets"
engcomp "github.com/novox/mesh-host/internal/compositions"
"sort"
@@ -99,7 +101,7 @@ func wokenBy(had answeredAt, done, retry bool, at answeredAt) string {
// candidateKey is one candidate key as last read: its value and revision.
type candidateKey struct {
c delivery.PlacementCandidate
c meshmoduleassigner.CandidatesByProposalNodeValue
rev uint64
}
@@ -160,8 +162,8 @@ func (s *Shadow) Run(ctx context.Context) error {
node := c.env.Node
// In inputWords' order.
ins := []input{
{delivery.PlacementCandidatesBucket, ">", nil, nil},
{delivery.PlacementBucket, ">", (*Composer).takePlacement, nil},
{buckets.PlacementCandidates, ">", nil, nil},
{buckets.Placement, ">", (*Composer).takePlacement, nil},
{buckets.Have, "*." + nodeengineclient.NodeWord, nil, nil},
{delivery.ModulesBucket, ">", (*Composer).takeModule, nil},
{ResolvedBucket, node + ".>", (*Composer).takeValue, nil},
@@ -282,8 +284,8 @@ func revsOf(keys map[string]candidateKey) string {
// takeCandidate takes one candidate key, and says whether it changed anything.
func (s *Shadow) takeCandidate(u agent.Update) bool {
proposal, node, err := delivery.ParsePlacementCandidateKey(u.Key)
if err != nil {
proposal, node, ok := meshmoduleassigner.ParseCandidatesByProposalNodeKey(u.Key)
if !ok {
return false
}
s.mu.Lock()
@@ -298,7 +300,7 @@ func (s *Shadow) takeCandidate(u agent.Update) bool {
}
return true
}
c, err := delivery.DecodePlacementCandidate(u.Value)
c, err := meshmoduleassigner.DecodeCandidatesByProposalNodeValue(u.Value)
if err != nil || c.Proposal != proposal || (c.Placement != nil && c.Placement.Node != node) {
// Not one: read as gone.
if _, ok := s.candidates[proposal][node]; ok {
@@ -506,7 +508,7 @@ 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]*delivery.Placement, len(keys))
given := make(map[string]*meshmoduleassigner.Placement, len(keys))
for n, k := range keys {
if k.c.Reached && k.c.Placement == nil {
continue // reached, with no key of its own: nothing laid over it
@@ -562,9 +564,9 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (nodeengin
}
// The boxes it runs, by what each module's have and its own key say after a restart (Held), are reused for the
// modules it runs: a value sealed again is no difference.
held := map[string]delivery.Want{agent.NodeModule: {}}
held := map[string]meshwants.WantsByWantNodeModuleValue{agent.NodeModule: {}}
for module := range fps {
held[module] = delivery.Want{FP: fps[module]}
held[module] = meshwants.WantsByWantNodeModuleValue{Fp: fps[module]}
}
c.mu.Lock()
c.wants = held
@@ -663,7 +665,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 delivery.Placement, modules map[string]delivery.Module, inOwn []string) map[string]string {
func shadowBuilds(running []engcomp.Composed, placement meshmoduleassigner.Placement, modules map[string]delivery.Module, inOwn []string) map[string]string {
fps := map[string]string{}
for _, r := range running {
if r.Module != "" && r.FP != "" {