diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index e44a938..6a9ff5b 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -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) { diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index c7f964b..9e6e94f 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -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 }) diff --git a/cmd/mesh-host/trust.go b/cmd/mesh-host/trust.go index 0f36859..74f7f10 100644 --- a/cmd/mesh-host/trust.go +++ b/cmd/mesh-host/trust.go @@ -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" } diff --git a/cmd/mesh-host/whatruns_test.go b/cmd/mesh-host/whatruns_test.go index 31e08ff..17067bc 100644 --- a/cmd/mesh-host/whatruns_test.go +++ b/cmd/mesh-host/whatruns_test.go @@ -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) diff --git a/go.mod b/go.mod index e5ee449..cbda85e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/internal/agent/agent.go b/internal/agent/agent.go index cda5919..80c13cb 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -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 } diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 2d205b4..8627176 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -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) diff --git a/internal/agent/composition.go b/internal/agent/composition.go index 48e3bd2..d626fb9 100644 --- a/internal/agent/composition.go +++ b/internal/agent/composition.go @@ -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 diff --git a/internal/buckets/buckets.go b/internal/buckets/buckets.go index ae8c8ac..878f4a9 100644 --- a/internal/buckets/buckets.go +++ b/internal/buckets/buckets.go @@ -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`, and each of its modules, `.`. + Placement = "mesh-delivery-assigner_placement" + // PlacementCandidates holds a node's candidate of a proposal, `.`. + PlacementCandidates = "mesh-delivery-assigner_candidates" + // Wants holds what each node should run, `want..` and `want..node`. + Wants = "mesh-delivery-assigner_wants" +) diff --git a/internal/buckets/bucketstest/bucketstest.go b/internal/buckets/bucketstest/bucketstest.go new file mode 100644 index 0000000..9848e23 --- /dev/null +++ b/internal/buckets/bucketstest/bucketstest.go @@ -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) +} diff --git a/internal/compose/frombus/answer_rows_test.go b/internal/compose/frombus/answer_rows_test.go index 784624e..e1e2747 100644 --- a/internal/compose/frombus/answer_rows_test.go +++ b/internal/compose/frombus/answer_rows_test.go @@ -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 diff --git a/internal/compose/frombus/export.go b/internal/compose/frombus/export.go index 432e7fb..7d82bf1 100644 --- a/internal/compose/frombus/export.go +++ b/internal/compose/frombus/export.go @@ -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 } diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index 557d842..27030e9 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -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` 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 diff --git a/internal/compose/frombus/frombus_test.go b/internal/compose/frombus/frombus_test.go index efd0e77..8b112ba 100644 --- a/internal/compose/frombus/frombus_test.go +++ b/internal/compose/frombus/frombus_test.go @@ -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) } diff --git a/internal/compose/frombus/frombustest/frombustest.go b/internal/compose/frombus/frombustest/frombustest.go index 432e05a..beda4ff 100644 --- a/internal/compose/frombus/frombustest/frombustest.go +++ b/internal/compose/frombus/frombustest/frombustest.go @@ -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{}, diff --git a/internal/compose/frombus/frombustest/frombustest_test.go b/internal/compose/frombus/frombustest/frombustest_test.go index 8769825..a071e3b 100644 --- a/internal/compose/frombus/frombustest/frombustest_test.go +++ b/internal/compose/frombus/frombustest/frombustest_test.go @@ -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 { diff --git a/internal/compose/frombus/held_rows_test.go b/internal/compose/frombus/held_rows_test.go index 8a083b0..09ab4c8 100644 --- a/internal/compose/frombus/held_rows_test.go +++ b/internal/compose/frombus/held_rows_test.go @@ -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 } diff --git a/internal/compose/frombus/states.go b/internal/compose/frombus/states.go index 0c2a163..4c5dcab 100644 --- a/internal/compose/frombus/states.go +++ b/internal/compose/frombus/states.go @@ -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 }) diff --git a/internal/composer/composer.go b/internal/composer/composer.go index 81fb035..984bbcd 100644 --- a/internal/composer/composer.go +++ b/internal/composer/composer.go @@ -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 +} diff --git a/internal/selfcompose/candidate.go b/internal/selfcompose/candidate.go index 5ca69a1..c56f6b4 100644 --- a/internal/selfcompose/candidate.go +++ b/internal/selfcompose/candidate.go @@ -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 diff --git a/internal/selfcompose/candidate_test.go b/internal/selfcompose/candidate_test.go index 6b1adcf..1d481a0 100644 --- a/internal/selfcompose/candidate_test.go +++ b/internal/selfcompose/candidate_test.go @@ -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"} diff --git a/internal/selfcompose/composedstate.go b/internal/selfcompose/composedstate.go index 5288b12..af2c1f4 100644 --- a/internal/selfcompose/composedstate.go +++ b/internal/selfcompose/composedstate.go @@ -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 { diff --git a/internal/selfcompose/composedstate_test.go b/internal/selfcompose/composedstate_test.go index 86ac8a7..d971e0e 100644 --- a/internal/selfcompose/composedstate_test.go +++ b/internal/selfcompose/composedstate_test.go @@ -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) } diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index 81dff9c..3f39ef9 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -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 `.>`). // 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 diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 9cada98..d029b67 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -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{} diff --git a/internal/selfcompose/shadow.go b/internal/selfcompose/shadow.go index c2d03b1..4ca4646 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -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 != "" {