From c35cc1d9e4cf5469637274dabafe59a160ba9015 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:34:02 +0100 Subject: [PATCH 1/5] =?UTF-8?q?Node-engine:=20the=20placement,=20the=20wan?= =?UTF-8?q?ts=20and=20the=20placement=20candidates=20are=20the=20seats'=20?= =?UTF-8?q?contract=20types=20and=20the=20assigner=20client's=20keys=20and?= =?UTF-8?q?=20codecs;=20the=20found=20tunnel=20is=20the=20placement=20seat?= =?UTF-8?q?'s=20typed=20record=20(ADR=200634=20=C2=A71,=20=C2=A74;=20issue?= =?UTF-8?q?=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- cmd/mesh-host/nodeagent.go | 6 +- cmd/mesh-host/nodeagent_integration_test.go | 64 +++--- cmd/mesh-host/trust.go | 29 +-- cmd/mesh-host/whatruns_test.go | 7 +- go.mod | 3 + internal/agent/agent.go | 60 ++--- internal/agent/agent_test.go | 82 +++---- internal/agent/composition.go | 4 +- internal/buckets/buckets.go | 11 + internal/buckets/bucketstest/bucketstest.go | 36 +++ internal/compose/frombus/answer_rows_test.go | 25 ++- internal/compose/frombus/export.go | 36 +-- internal/compose/frombus/frombus.go | 46 ++-- internal/compose/frombus/frombus_test.go | 66 +++--- .../frombus/frombustest/frombustest.go | 11 +- .../frombus/frombustest/frombustest_test.go | 8 +- internal/compose/frombus/held_rows_test.go | 7 +- internal/compose/frombus/states.go | 16 +- internal/composer/composer.go | 24 +- internal/selfcompose/candidate.go | 5 +- internal/selfcompose/candidate_test.go | 14 +- internal/selfcompose/composedstate.go | 2 +- internal/selfcompose/composedstate_test.go | 22 +- internal/selfcompose/selfcompose.go | 67 +++--- internal/selfcompose/selfcompose_test.go | 206 +++++++++--------- internal/selfcompose/shadow.go | 22 +- 26 files changed, 489 insertions(+), 390 deletions(-) create mode 100644 internal/buckets/bucketstest/bucketstest.go 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 != "" { From 669589b76baa8d7fe1c5f1cda733a9aac0d2f23b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:36:05 +0100 Subject: [PATCH 2/5] =?UTF-8?q?Node-engine:=20the=20placement's=20bucket?= =?UTF-8?q?=20and=20read=20names=20come=20from=20the=20assigner=20client?= =?UTF-8?q?=20(ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- internal/buckets/buckets.go | 12 +++++++++--- internal/compose/frombus/states.go | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/buckets/buckets.go b/internal/buckets/buckets.go index 878f4a9..00214c2 100644 --- a/internal/buckets/buckets.go +++ b/internal/buckets/buckets.go @@ -3,6 +3,12 @@ // reads through grants composed in code, so the holder's name is kept here, in its own code (ADR 0634 §1, finding F4). package buckets +import ( + assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + wants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" +) + // The node-engine's own states and the have state, written by every node's engine for its own keys. const ( // Have holds a module's have on a node, `.`, and the node's own facts, `.node`. @@ -20,9 +26,9 @@ const HaveHistory = 20 // 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" + Placement = assigner.PlacementBucket // PlacementCandidates holds a node's candidate of a proposal, `.`. - PlacementCandidates = "mesh-delivery-assigner_candidates" + PlacementCandidates = assigner.Module + "_" + seat.StateCandidates // Wants holds what each node should run, `want..` and `want..node`. - Wants = "mesh-delivery-assigner_wants" + Wants = assigner.Module + "_" + wants.StateWants ) diff --git a/internal/compose/frombus/states.go b/internal/compose/frombus/states.go index 4c5dcab..2c5d38b 100644 --- a/internal/compose/frombus/states.go +++ b/internal/compose/frombus/states.go @@ -28,7 +28,7 @@ import ( const ( // The holders' names are kept here, in the engine's own code, as the names of the states a manifest's reads use // (ADR 0634 §1, finding F4); a seat's contract names a state locally, not the module that holds it. - PlacementRead = "mesh-delivery-assigner.placement" + PlacementRead = meshmoduleassignerclient.PlacementRead ModulesRead = "mesh-delivery-indexer.modules" ManifestsRead = "mesh-delivery-indexer.manifests" BuildsRead = "mesh-delivery-builder.builds" From 10852161c90d44b678bc6621f1a036906c58f158 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:42:34 +0100 Subject: [PATCH 3/5] =?UTF-8?q?Node-engine:=20the=20catalogue's=20modules,?= =?UTF-8?q?=20manifests,=20verdicts=20and=20candidates=20are=20the=20mesh-?= =?UTF-8?q?catalog=20seat's=20types=20and=20its=20client's=20keys=20and=20?= =?UTF-8?q?codecs;=20the=20composer=20case=20is=20the=20delivery=20SDK's,?= =?UTF-8?q?=20its=20verdicts=20and=20placements=20raw;=20the=20provided=20?= =?UTF-8?q?module=20is=20the=20private=20network's=20(ADR=200634=20=C2=A71?= =?UTF-8?q?,=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- cmd/mesh-compose/main_test.go | 10 +- cmd/mesh-host/nodeagent_integration_test.go | 59 ++++--- cmd/mesh-host/trust.go | 5 +- cmd/mesh-host/whatruns_test.go | 4 +- go.mod | 3 + internal/agent/agent_test.go | 3 +- internal/buckets/buckets.go | 25 +++ internal/buckets/buckets_test.go | 22 +++ internal/buckets/bucketstest/bucketstest.go | 26 +++ internal/compose/composed.go | 4 +- internal/compose/composed_test.go | 19 +-- internal/compose/frombus/answer_rows_test.go | 50 +++--- internal/compose/frombus/frombus.go | 36 ++-- internal/compose/frombus/frombus_test.go | 46 ++--- .../frombus/frombustest/frombustest.go | 26 +-- .../frombus/frombustest/frombustest_test.go | 20 ++- internal/compose/frombus/held_rows_test.go | 14 +- internal/compose/frombus/states.go | 16 +- internal/composer/composer.go | 53 +++--- internal/composer/composer_test.go | 16 +- internal/judge/answer_test.go | 5 +- internal/judge/judge_test.go | 5 +- internal/selfcompose/candidate.go | 54 +++--- internal/selfcompose/candidate_test.go | 107 ++++++------ internal/selfcompose/composedstate.go | 4 +- internal/selfcompose/composedstate_test.go | 30 ++-- internal/selfcompose/data_test.go | 11 +- internal/selfcompose/selfcompose.go | 50 +++--- internal/selfcompose/selfcompose_test.go | 160 +++++++++--------- internal/selfcompose/shadow.go | 19 +-- internal/userlist/userlist_test.go | 3 +- 31 files changed, 503 insertions(+), 402 deletions(-) diff --git a/cmd/mesh-compose/main_test.go b/cmd/mesh-compose/main_test.go index f70cf66..2ce1355 100644 --- a/cmd/mesh-compose/main_test.go +++ b/cmd/mesh-compose/main_test.go @@ -3,6 +3,9 @@ package main import ( "bytes" "encoding/json" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "github.com/novox/mesh-host/internal/buckets/bucketstest" "go/build" "os" "os/exec" @@ -14,7 +17,6 @@ import ( "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/beside" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/composer" @@ -94,7 +96,7 @@ func TestTheCommandWritesOneVerdictPerManifestAndMachine(t *testing.T) { r := request(t, composer.DepthResolve) r.Cases[0].Manifests["later"] = composer.Manifest{Raw: json.RawMessage(`{"module":"later","version":"1","shares":["x"]}`)} r.Cases[0].Manifests["refused"] = composer.Manifest{Raw: json.RawMessage(`{"module":"refused","version":"1"}`)} - r.Cases[0].Verdicts = map[string]delivery.ManifestVerdict{"refused": {State: delivery.VerdictRefused, Problems: []string{"it is wrong"}}} + r.Cases[0].Verdicts = bucketstest.Raw(map[string]meshcatalog.VerdictsByDigestValue{"refused": {V: 1, Module: "refused", Judge: "src2:j", State: meshcatalogclient.VerdictRefused, Problems: []string{"it is wrong"}}}) code, v, stderr := runJSON(t, nil, encode(t, r)) if code != 0 { t.Fatalf("exit %d: %s", code, stderr) @@ -280,7 +282,7 @@ func ownPackages(t *testing.T) map[string][]string { } // 1621/AC2 and AC4: the command imports, through every package of this repository it reaches, only the standard -// library, mesh-sdk, the contracts modules' pure packages (seats' types, clients' codecs), the private network's overlay package (mesh-wireguard's own pure package: names, graph and +// library, mesh-sdk, the contracts modules' pure packages (seats' types, clients' codecs), the delivery SDK's composer (the command's own request and verdict types), the private network's overlay package (mesh-wireguard's own pure package: names, graph and // generator, no bus) and this repository's own packages, and none that reaches a bus, a store, a network service or // another process. func TestTheCommandImportsNoBusStoreNetworkOrProcess(t *testing.T) { @@ -292,7 +294,7 @@ func TestTheCommandImportsNoBusStoreNetworkOrProcess(t *testing.T) { for path, imports := range own { for _, imp := range imports { std := !strings.Contains(strings.SplitN(imp, "/", 2)[0], ".") - if !std && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-sdk/go") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/contracts/") && !strings.HasPrefix(imp, "github.com/novox/mesh-host/") { + if !std && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-sdk/go") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/sdk/composer") && !strings.HasPrefix(imp, "github.com/novox/mesh-host/") { t.Errorf("%s imports %s, which is neither the standard library, mesh-sdk nor this repository", path, imp) } for _, f := range forbidden { diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index 9e6e94f..eda3474 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -11,6 +11,9 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/buckets/bucketstest" engcomp "github.com/novox/mesh-host/internal/compositions" @@ -184,7 +187,7 @@ func (b *itBed) say(line string) { b.t.Log(line) } -func fpIt(c string) string { return delivery.SourcePrefix2 + strings.Repeat(c, 64) } +func fpIt(c string) string { return meshcatalogclient.SourcePrefix2 + strings.Repeat(c, 64) } func fpDigest(c string) string { return "sha256:" + strings.Repeat(c, 64) } @@ -411,7 +414,7 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { } return kv } - placement, modules, manifests := kvOf(buckets.Placement), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket) + placement, modules, manifests := kvOf(buckets.Placement), kvOf(buckets.Modules), kvOf(buckets.Manifests) resolved := kvOf(selfcompose.ResolvedBucket) builds := kvOf(delivery.BuilderBuildsBucket) candidates := kvOf(buckets.PlacementCandidates) @@ -421,11 +424,11 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { `"resources":[{"id":"conf","type":"file","path":"` + path + `","mode":"0644","content":"greeting=${config:greeting}\n"},` + `{"id":"tools","type":"package","package":"web-tools"}],` + `"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}` - digest := delivery.ManifestDigest([]byte(man)) - mraw, _ := delivery.EncodeManifest(delivery.Manifest{Module: "web", JSON: man}) - mkey, _ := delivery.ManifestsKey(digest) + digest := meshcatalogclient.ManifestDigest([]byte(man)) + mraw, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "web", Manifest: man}) + mkey, _ := meshcatalogclient.ManifestsKey(digest) fp := fpIt("a") - modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)}) + modv, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)}) place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: itNode, Modules: []meshmoduleassigner.PlacedModule{{Module: "web"}}}) 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. @@ -436,10 +439,10 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { // A provider on it2 serving from its own config value: a mail domain, which it1's composition settles. mailer := `{"module":"mailer","version":"1","config":{"domain":{"type":"string","description":"the mail domain","required":true}},` + `"provides":[{"name":"smtp","scope":"mesh"}],"serves":{"smtp":{"port":587,"domain":"${config:domain}"}}}` - mdigest := delivery.ManifestDigest([]byte(mailer)) - 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)}) + mdigest := meshcatalogclient.ManifestDigest([]byte(mailer)) + mlraw, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "mailer", Manifest: mailer}) + mlkey, _ := meshcatalogclient.ManifestsKey(mdigest) + mlmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fpIt("b"), Manifest: mdigest, Repository: "novox/mailer", Commit: strings.Repeat("2", 40)}) place2, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "it2", Modules: []meshmoduleassigner.PlacedModule{{Module: "mailer"}}}) rec2, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "it2", Capabilities: []string{"service-manager"}}}) pkey2, _ := meshmoduleassignerclient.LegacyKey("it2") @@ -582,8 +585,8 @@ func h8candidate(t *testing.T, b *itBed, fp, running string) { } return kv } - cands, _ := kvOf(delivery.CandidatesBucket), kvOf(delivery.VerdictsBucket) - manifests, err := b.js.KeyValue(delivery.ManifestsBucket) + cands, _ := kvOf(buckets.Candidates), kvOf(buckets.Verdicts) + manifests, err := b.js.KeyValue(buckets.Manifests) if err != nil { t.Fatal(err) } @@ -591,25 +594,25 @@ func h8candidate(t *testing.T, b *itBed, fp, running string) { man := `{"module":"web","version":"2","config":{"greeting":{"type":"string","description":"what the file says","default":"hello"}},` + `"resources":[{"id":"conf","type":"file","path":"` + path + `","mode":"0640","content":"greeting=${config:greeting}\n"},` + `{"id":"tools","type":"package","package":"web-tools"}]}` - digest := delivery.ManifestDigest([]byte(man)) - mraw, _ := delivery.EncodeManifest(delivery.Manifest{Module: "web", JSON: man}) - mkey, _ := delivery.ManifestsKey(digest) + digest := meshcatalogclient.ManifestDigest([]byte(man)) + mraw, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "web", Manifest: man}) + mkey, _ := meshcatalogclient.ManifestsKey(digest) if _, err := manifests.Put(mkey, mraw); err != nil { t.Fatal(err) } - head := delivery.Head{Repository: "novox/web", Number: 3, Head: strings.Repeat("3", 40)} - heads, _ := delivery.HeadsHash([]delivery.Head{head}) - pulls := []delivery.CandidatePull{{Head: head, Modules: []string{"web"}, - Fps: map[string]delivery.CandidateModule{"web": {FP2: fpIt("c"), Manifest: digest}}}} + head := meshdeliveryclient.Head{Repository: "novox/web", Number: 3, Head: strings.Repeat("3", 40)} + heads, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{head}) + pulls := []meshcatalog.CandidatesByGroupValuePullsItem{bucketstest.Pull(head, []string{"web"}, + map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"web": {Fp2: fpIt("c"), Manifest: digest}})} // The indexer keys the candidate by its group (a branch name, slash allowed); the checker asks by the composition key. const group = "fix/3-web" - craw, err := delivery.EncodeCandidate(delivery.Candidate{Group: group, HeadsHash: heads, Pulls: pulls, - FPSet: delivery.FingerprintSet(pulls)}) + craw, err := bucketstest.GroupCandidate(meshcatalog.CandidatesByGroupValue{Group: group, Heads: heads, Pulls: pulls, + Fpset: meshcatalogclient.FingerprintSet(pulls)}) if err != nil { t.Fatal(err) } - ckey, _ := delivery.CandidatesKey(group) - key, _ := delivery.CompositionKey(group, heads) + ckey, _ := buckets.CandidateKey(group) + key, _ := nec.CompositionKey(group, heads) if _, err := cands.Put(ckey, craw); err != nil { t.Fatal(err) } @@ -765,7 +768,7 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test } return kv } - placement, modules, manifests := kvOf(buckets.Placement), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket) + placement, modules, manifests := kvOf(buckets.Placement), kvOf(buckets.Modules), kvOf(buckets.Manifests) kvOf(selfcompose.ResolvedBucket) builds := kvOf(delivery.BuilderBuildsBucket) kvOf(buckets.PlacementCandidates) @@ -776,10 +779,10 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test `"data":{"own":[{"id":"jetstream","path":"` + b.dir + `","class":"valuable","why":"the bus's streams"}]},"resources":[` + `{"id":"server-conf","type":"file","path":"` + conf + `","mode":"0644","content":"listen: new\n"},` + `{"id":"server","type":"container","name":"` + server + `","image":"` + image + `"` + env + `}]}` - digest := delivery.ManifestDigest([]byte(man)) - mraw, _ := delivery.EncodeManifest(delivery.Manifest{Module: "nats", JSON: man}) - mkey, _ := delivery.ManifestsKey(digest) - modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/mesh-catalog", Commit: strings.Repeat("3", 40)}) + digest := meshcatalogclient.ManifestDigest([]byte(man)) + mraw, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "nats", Manifest: man}) + mkey, _ := meshcatalogclient.ManifestsKey(digest) + modv, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/mesh-catalog", Commit: strings.Repeat("3", 40)}) if _, err := manifests.Put(mkey, mraw); err != nil { t.Fatal(err) } diff --git a/cmd/mesh-host/trust.go b/cmd/mesh-host/trust.go index 74f7f10..4eb1e7d 100644 --- a/cmd/mesh-host/trust.go +++ b/cmd/mesh-host/trust.go @@ -11,6 +11,7 @@ import ( "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "github.com/novox/mesh-host/internal/buckets" "os" "path/filepath" @@ -244,8 +245,8 @@ func allHave(p meshmoduleassigner.Placement, has func(module string) (nodeengine return false, m.Module + " has no have yet" } // A module the mesh provides (mesh-wireguard) never has a fingerprint: the node's own composes it from the manifest - // mesh-sdk provides (delivery.ProvidedManifest), never from the kept declaration, so it keeps no read. - if h.Phase != nodeengineclient.PhaseRemoved && !strings.HasPrefix(h.Fp, delivery.SourcePrefix2) && !delivery.ProvidedByMesh(m.Module) { + // mesh-sdk provides (overlay.ProvidedManifest), never from the kept declaration, so it keeps no read. + if h.Phase != nodeengineclient.PhaseRemoved && !strings.HasPrefix(h.Fp, meshcatalogclient.SourcePrefix2) && !overlay.ProvidedByMesh(m.Module) { return false, m.Module + " runs at a build of the old form (" + h.Build + "), which only the kept declaration records" } } diff --git a/cmd/mesh-host/whatruns_test.go b/cmd/mesh-host/whatruns_test.go index 17067bc..79bbda2 100644 --- a/cmd/mesh-host/whatruns_test.go +++ b/cmd/mesh-host/whatruns_test.go @@ -8,6 +8,7 @@ import ( "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" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" engcomp "github.com/novox/mesh-host/internal/compositions" "path/filepath" "sort" @@ -16,7 +17,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/liveness" @@ -123,7 +123,7 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) { 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} + newForm := nodeengine.HaveByNodeModuleValue{Fp: meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64), Phase: nodeengineclient.PhaseRunning} oldForm := nodeengine.HaveByNodeModuleValue{Build: "build-1791404191702719760", Phase: nodeengineclient.PhaseRunning} for _, row := range []struct { name string diff --git a/go.mod b/go.mod index cbda85e..46b4065 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-delivery/sdk v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-20261105000000-000000000000 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 diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 8627176..46155a0 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -7,6 +7,7 @@ import ( "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "github.com/novox/mesh-host/internal/buckets/bucketstest" "github.com/novox/mesh-host/internal/compositions" "maps" @@ -24,7 +25,7 @@ import ( const node = "n1" -func fpOf(c string) string { return delivery.SourcePrefix2 + strings.Repeat(c, 64) } +func fpOf(c string) string { return meshcatalogclient.SourcePrefix2 + strings.Repeat(c, 64) } // A watcher the test writes to; it says Caught once the keys written before the watch began are out. type feed struct { diff --git a/internal/buckets/buckets.go b/internal/buckets/buckets.go index 00214c2..53b0479 100644 --- a/internal/buckets/buckets.go +++ b/internal/buckets/buckets.go @@ -4,9 +4,13 @@ package buckets import ( + "errors" + assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" wants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/keys" ) // The node-engine's own states and the have state, written by every node's engine for its own keys. @@ -32,3 +36,24 @@ const ( // Wants holds what each node should run, `want..` and `want..node`. Wants = assigner.Module + "_" + wants.StateWants ) + +// The states of the catalogue seat's holder that the engine reads, named by the holder's module (ADR 0634 §1, finding F4). +const ( + // Modules holds a module's source, ``; Manifests a manifest by its digest; Verdicts the judge's verdict of a + // manifest by its digest; Candidates a group's candidate, by the group's key part (CandidateKey). + Modules = catalogHolder + "_modules" + Manifests = catalogHolder + "_manifests" + Verdicts = catalogHolder + "_verdicts" + Candidates = catalogHolder + "_candidates" + + catalogHolder = "mesh-delivery-indexer" +) + +// CandidateKey is the key of a group's candidate in Candidates: the group as a key part, a branch such as `fix/1.2` +// escaped so it keys. +func CandidateKey(group string) (string, error) { + if group == "" { + return "", errors.New("buckets: the group name is empty") + } + return catalog.CandidatesByGroupKey(keys.EscapeName(group)), nil +} diff --git a/internal/buckets/buckets_test.go b/internal/buckets/buckets_test.go index db981d6..d24750b 100644 --- a/internal/buckets/buckets_test.go +++ b/internal/buckets/buckets_test.go @@ -1,7 +1,10 @@ package buckets import ( + "strings" + "encoding/json" + "git.novox.be/novox/mesh-sdk/go/keys" "os" "testing" ) @@ -32,3 +35,22 @@ func TestTheOwnStatesAreTheManifestsStates(t *testing.T) { } } } + +// A group's candidate key is its name as a key part: a branch with a slash keeps it, a dot is escaped, and an empty group does not. +func TestACandidateKeyEscapesTheGroup(t *testing.T) { + for _, group := range []string{"mesh-sdk#7", "fix/940-no-retire", "fix/1.2"} { + key, err := CandidateKey(group) + if err != nil { + t.Fatalf("%s: %v", group, err) + } + if strings.Contains(key, ".") { + t.Errorf("the key %q of %q holds a dot", key, group) + } + if back, err := keys.UnescapeName(key); err != nil || back != group { + t.Errorf("%q came back as %q (%v)", group, back, err) + } + } + if _, err := CandidateKey(""); err == nil { + t.Error("a key was made for no group") + } +} diff --git a/internal/buckets/bucketstest/bucketstest.go b/internal/buckets/bucketstest/bucketstest.go index 9848e23..03912cc 100644 --- a/internal/buckets/bucketstest/bucketstest.go +++ b/internal/buckets/bucketstest/bucketstest.go @@ -4,6 +4,8 @@ package bucketstest import ( "encoding/json" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" @@ -34,3 +36,27 @@ func Candidate(c meshmoduleassigner.CandidatesByProposalNodeValue) ([]byte, erro } return json.Marshal(c) } + +// GroupCandidate is a group's candidate as the catalogue seat's holder writes it. +func GroupCandidate(c meshcatalog.CandidatesByGroupValue) ([]byte, error) { + c.V = Version + return json.Marshal(c) +} + +// Pull is one pull request of a candidate, from its head. +func Pull(head meshdeliveryclient.Head, modules []string, fps map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue) meshcatalog.CandidatesByGroupValuePullsItem { + return meshcatalog.CandidatesByGroupValuePullsItem{Repository: head.Repository, Number: int64(head.Number), Head: head.Head, Modules: modules, Fps: fps} +} + +// Raw is each value as JSON, as a composer case carries verdicts and placements. +func Raw[V any](in map[string]V) map[string]json.RawMessage { + out := map[string]json.RawMessage{} + for k, v := range in { + raw, err := json.Marshal(v) + if err != nil { + panic(err) + } + out[k] = raw + } + return out +} diff --git a/internal/compose/composed.go b/internal/compose/composed.go index c30b539..a045795 100644 --- a/internal/compose/composed.go +++ b/internal/compose/composed.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" engcomp "github.com/novox/mesh-host/internal/compositions" "slices" "sort" @@ -12,7 +13,6 @@ import ( "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "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/compose/resolution" ) @@ -70,7 +70,7 @@ func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (ou // not folded (issue 1585: nats on novox, folded by the reader and composed at src2: here, refused for its server // artifact no build had published). func Folds(inOwn bool, fp string) bool { - return inOwn && !strings.HasPrefix(fp, delivery.SourcePrefix2) + return inOwn && !strings.HasPrefix(fp, meshcatalogclient.SourcePrefix2) } func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]engcomp.Composed, skipped, stood []string, err error) { diff --git a/internal/compose/composed_test.go b/internal/compose/composed_test.go index ccbb3d4..c33c749 100644 --- a/internal/compose/composed_test.go +++ b/internal/compose/composed_test.go @@ -2,6 +2,7 @@ package compose import ( "encoding/json" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" engcomp "github.com/novox/mesh-host/internal/compositions" "sort" "strings" @@ -9,11 +10,10 @@ import ( "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose/resolution" ) -func fpFor(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) } +func fpFor(c byte) string { return meshcatalogclient.SourcePrefix2 + strings.Repeat(string(c), 64) } // 1943/AC5: a node's compositions are its declaration as Compose gives it, split by the module each resource belongs // to: every resource once, each in its module's composition at the fingerprint given, the module's ownership said on @@ -186,18 +186,3 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { } } } - -// 1943/AC1: the upgrade policies and sources a composition's value may name are the catalogue's own words. -func TestTheComposedPoliciesAreTheCatalogues(t *testing.T) { - for _, p := range []string{catalogue.PolicyRoll, catalogue.PolicyTogether, catalogue.PolicyRecord} { - for _, from := range []string{catalogue.FromPerson, catalogue.FromModule, "the bus", catalogue.FromData, catalogue.FromDefault} { // "the bus": an older reader's word, still read - v := delivery.ComposedDigest{Digest: "sha256:" + strings.Repeat("a", 64), Judgement: delivery.JudgedNeither, Policy: p, PolicyFrom: from} - if _, err := delivery.EncodeComposedDigest(v); err != nil { - t.Errorf("%s from %s: %v", p, from, err) - } - } - } - if delivery.PolicyRecord != catalogue.PolicyRecord { - t.Errorf("delivery.PolicyRecord = %q, the catalogue's %q", delivery.PolicyRecord, catalogue.PolicyRecord) - } -} diff --git a/internal/compose/frombus/answer_rows_test.go b/internal/compose/frombus/answer_rows_test.go index e1e2747..ebaaf31 100644 --- a/internal/compose/frombus/answer_rows_test.go +++ b/internal/compose/frombus/answer_rows_test.go @@ -7,6 +7,8 @@ import ( "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-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" engcomp "github.com/novox/mesh-host/internal/compositions" "os" "reflect" @@ -45,24 +47,24 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { } 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}, + modules: map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Manifest: meshcatalogclient.ManifestDigest(lemurs)}}, + manifests: map[string][]byte{meshcatalogclient.ManifestDigest(lemurs): lemurs}, builds: map[string]*delivery.BuilderBuild{}} for _, m := range []string{"web", "api", "spiegel"} { b.own = append(b.own, compose.OwnSecretRow{Node: "n1", Module: m, Name: "token", Origin: "made", SealedTo: "sk", Sealed: `"box"`}) } for _, m := range []string{"web", "api", "spiegel"} { raw := manifest(m) - b.manifests[delivery.ManifestDigest(raw)] = raw - b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)} + b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw + b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)} } // web and api are built, their keys done and waiting for the reference of their image; spiegel has no build of // the new form at all. for i, m := range []string{"web", "api"} { mod := b.modules[m] - mod.FP2 = fp(string(rune('1' + i))) + mod.Fp2 = fp(string(rune('1' + i))) b.modules[m] = mod - b.builds[m+" "+mod.FP2] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest, + b.builds[m+" "+mod.Fp2] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest, Artifacts: []delivery.BuiltArtifact{{Name: "site", Kind: "image", Digest: "sha256:" + strings.Repeat("b", 64)}}} } return b @@ -204,16 +206,16 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { bp.Decided.Endpoint = "n1.example:51820" bb.placements["n1"] = bp sp := bb.modules["spiegel"] - sp.FP2 = fp("3") + sp.Fp2 = fp("3") bb.modules["spiegel"] = sp image := "registry.example:5100/spiegel/site@sha256:" + strings.Repeat("7", 64) - bb.builds["spiegel "+sp.FP2] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: sp.Manifest, + bb.builds["spiegel "+sp.Fp2] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: sp.Manifest, Artifacts: []delivery.BuiltArtifact{{Name: "site", Kind: "upstream", Reference: image, Digest: "sha256:" + strings.Repeat("7", 64)}}} read, err := ComposingInput(ctx, bb, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}}, - map[string]string{"lemurs": fp("e"), "spiegel": sp.FP2}) + map[string]string{"lemurs": fp("e"), "spiegel": sp.Fp2}) raw, _ := json.Marshal(read.Catalogue["spiegel"]) if err != nil || read.ComposedHow.Held["spiegel"] != "" || !strings.Contains(string(raw), strings.Repeat("7", 64)) { - t.Errorf("spiegel in InOwn wanted at %s: %v, held %v, read as %s; want its build's image", sp.FP2, err, + t.Errorf("spiegel in InOwn wanted at %s: %v, held %v, read as %s; want its build's image", sp.Fp2, err, read.ComposedHow, raw) } } @@ -232,8 +234,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { `"resources":[{"id":"service","type":"service","unit":"vault.service","state":"running","boot":"enabled"}]}`) b = mk("lemurs", overlay.Name, "spiegel", "nft", "vault") for m, raw := range map[string][]byte{"spiegel": listening, "nft": nft, "vault": vault} { - b.manifests[delivery.ManifestDigest(raw)] = raw - b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)} + b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw + b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)} } p = b.placements["n1"] p.Decided.Endpoint = "n1.example:51820" @@ -370,8 +372,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { `"resources":[{"id":"service","type":"service","unit":"mailu.service","state":"running","boot":"enabled"}]}`) b = mk("lemurs", overlay.Name, "mailu", "proxy", "de-spiegel") for m, raw := range map[string][]byte{"de-spiegel": spiegel, "proxy": proxy, "mailu": mailu} { - b.manifests[delivery.ManifestDigest(raw)] = raw - b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)} + b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw + b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)} } p = b.placements["n1"] p.Decided.Endpoint = "n1.example:51820" @@ -470,8 +472,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { `{"id":"server","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["8080"]}]}`) b = mk("lemurs", overlay.Name, "site", "nft") for m, raw := range map[string][]byte{"site": site, "nft": nft} { - b.manifests[delivery.ManifestDigest(raw)] = raw - b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)} + b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw + b.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)} } p = b.placements["n1"] p.Decided.Endpoint = "n1.example:51820" @@ -511,8 +513,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { `"resources":[{"id":"service","type":"service","unit":"app.service","state":"running","boot":"enabled"}]}`) b2 := mk("lemurs", overlay.Name, "site", "nft", "app") for m, raw := range map[string][]byte{"site": provider, "nft": nft, "app": app} { - b2.manifests[delivery.ManifestDigest(raw)] = raw - b2.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)} + b2.manifests[meshcatalogclient.ManifestDigest(raw)] = raw + b2.modules[m] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(raw)} } p2 := b2.placements["n1"] p2.Decided.Endpoint = "n1.example:51820" @@ -554,16 +556,16 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { // build that runs (7) listens on 8080. The filter composes with what site last offered, its manifest at the build // that runs (issue 1960: a hold never spreads), so it opens 8080 and nothing applied opens 9090. moved := []byte(strings.Replace(string(site), `"port":8080`, `"port":9090`, 1)) - b.manifests[delivery.ManifestDigest(moved)] = moved - b.modules["site"] = delivery.Module{Manifest: delivery.ManifestDigest(moved)} + b.manifests[meshcatalogclient.ManifestDigest(moved)] = moved + b.modules["site"] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(moved)} for i := range p.Modules { p.Modules[i].Ports = nil } b.placements["n1"] = p siteRuns.Resources[1] = json.RawMessage(`{"id":"site.server","module":"site","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["8080"]}`) siteRuns.FP = fp("7") - b.manifests[delivery.ManifestDigest(site)] = site - b.builds["site "+fp("7")] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: delivery.ManifestDigest(site)} + b.manifests[meshcatalogclient.ManifestDigest(site)] = site + b.builds["site "+fp("7")] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(site)} got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []engcomp.Composed{siteRuns}}, fps) if err != nil || held["site"] == "" || held["nft"] != "" { t.Errorf("site moved to 9090 in its new manifest: %v, held %v; want site held alone", err, held) @@ -593,8 +595,8 @@ func otherNodeNotResolvedIsNamed(t *testing.T) { "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}} + modules: map[string]meshcatalog.ModulesByModuleValue{"prov": {Manifest: meshcatalogclient.ManifestDigest(prov)}, "cons": {Manifest: meshcatalogclient.ManifestDigest(cons)}}, + manifests: map[string][]byte{meshcatalogclient.ManifestDigest(prov): prov, meshcatalogclient.ManifestDigest(cons): cons}} given := map[string]*meshmoduleassigner.Placement{} for n, p := range b.placements { p := p diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index 27030e9..899e057 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -38,6 +38,8 @@ import ( "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "net/netip" @@ -62,9 +64,9 @@ type Reader interface { // 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) - // Modules is every module the indexer holds, by module (delivery.ModulesBucket). - Modules(ctx context.Context) (map[string]delivery.Module, error) - // Manifest is the raw manifest of a digest (delivery.ManifestsBucket). + // Modules is every module the indexer holds, by module (buckets.Modules). + Modules(ctx context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) + // Manifest is the raw manifest of a digest (buckets.Manifests). Manifest(ctx context.Context, digest string) ([]byte, error) // Values is one module's config values on one node as mesh-config resolved them (mesh-config's state `resolved`, // keys `..`), decoded; nil when it has none. @@ -198,7 +200,7 @@ func InOwn(running []engcomp.Composed, p meshmoduleassigner.Placement) []string // Input is the composition input for s, read through r. // // **A refused manifest never ends the read** (novox/hq ADR 0578 §3): each manifest is decoded leniently -// (catalogue.Decode) and the indexer's verdict on it taken from its modules value (delivery.TakeManifestVerdict, a missing +// (catalogue.Decode) and the indexer's verdict on it taken from its modules value (meshcatalogclient.TakeVerdict, a missing // one accepted); a module whose manifest the indexer's judge refused, or whose bytes do not decode, stands in the // catalogue as its stub (unusable), so what it provides and requires still shapes the rest, and is named with why in // the input's Refused; a field set aside is named in its SetAside. Every other module of the read goes on. @@ -209,10 +211,10 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er // taken is a module's manifest as a reader takes it (novox/hq ADR 0578 §3): decoded leniently, with the indexer's verdict // from its modules value. A refused one, or bytes that do not decode, stands as its stub (unusable) and why says so; a // manifest not read past a field it set aside holds its name alone and is held the same. -func taken(name string, raw []byte, m delivery.Module) (man catalogue.Manifest, why string) { - if v := m.VerdictOf(name); v != nil { - if refused, _ := delivery.TakeManifestVerdict(v); refused { - return unusable(name, raw), v.Why() +func taken(name string, raw []byte, m meshcatalog.ModulesByModuleValue) (man catalogue.Manifest, why string) { + if v := meshcatalogclient.ModuleVerdict(name, m); v != nil { + if refused, _ := meshcatalogclient.TakeVerdict(v); refused { + return unusable(name, raw), meshcatalogclient.VerdictWhy(*v) } } man, err := catalogue.Decode(raw) @@ -256,7 +258,7 @@ type base struct { in compose.Input placements map[string]meshmoduleassigner.Placement records map[string]nodeengine.NodeRecord - modules map[string]delivery.Module + modules map[string]meshcatalog.ModulesByModuleValue // ownRefused is each module placed on the composing node whose manifest the indexer refused, with why, in the order // read: every scope holds it. ownRefused [][2]string @@ -383,7 +385,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) { // The module the mesh provides itself (the private network) has no repository the indexer reads: its manifest comes // with mesh-sdk and always wins, as the controller's providedModules and the grants part have it. - for _, name := range delivery.ProvidedModules() { + for _, name := range overlay.ProvidedModules() { raw, err := json.Marshal(overlay.Manifest()) if err != nil { return b, err @@ -794,7 +796,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 meshmoduleassigner.Placement, modules map[string]delivery.Module, + in *compose.Input, s Scope, p meshmoduleassigner.Placement, modules map[string]meshcatalog.ModulesByModuleValue, read func(module, digest string) (catalogue.Manifest, error), parse func([]byte) (catalogue.Manifest, error), refuse func(module string, err error), wanted map[string]string) error { // stub keeps a refused module in the composition as a stub for this node alone (compose.Stub): what it provides @@ -881,7 +883,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m // the scope keeps at another build is. keptAt := maps.Clone(s.KeptAt) for _, pm := range p.Modules { - if m, ok := modules[pm.Module]; ok && m.FP2 != "" && wanted[pm.Module] != "" && wanted[pm.Module] != m.FP2 && + if m, ok := modules[pm.Module]; ok && m.Fp2 != "" && wanted[pm.Module] != "" && wanted[pm.Module] != m.Fp2 && keptAt[pm.Module] == "" && !compose.Folds(slices.Contains(s.InOwn, pm.Module), wanted[pm.Module]) { if keptAt == nil { keptAt = map[string]string{} @@ -897,7 +899,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m } for _, pm := range p.Modules { m, ok := modules[pm.Module] - if !ok || m.FP2 == "" || keptAt[pm.Module] != "" || compose.Folds(slices.Contains(s.InOwn, pm.Module), wanted[pm.Module]) { + if !ok || m.Fp2 == "" || keptAt[pm.Module] != "" || compose.Folds(slices.Contains(s.InOwn, pm.Module), wanted[pm.Module]) { // A module it runs inside its own composition is composed there as before while it is folded: no build of the // new form is what it runs (Scope.InOwn). Wanted at a build of the new form it is no longer folded (compose.Folds, // the composition's own rule), so its build is taken in (issue 1585). @@ -909,9 +911,9 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m if man, ok := in.Catalogue[pm.Module]; !ok || (man.Build == nil && !man.Templated()) { continue // nothing is built of it: its manifest is what it composes from } - b, err := build(ctx, pm.Module, m.FP2) + b, err := build(ctx, pm.Module, m.Fp2) if err != nil { - err = fmt.Errorf("frombus: the build of %s at %s: %w", pm.Module, m.FP2, err) + err = fmt.Errorf("frombus: the build of %s at %s: %w", pm.Module, m.Fp2, err) if refuse == nil { return err } @@ -924,11 +926,11 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m // runtime's bundle): it is held, keeping what runs, until its build lands (issue 774, 2026-10-30 19:30: // node-tools on ace, g14 and shanks, "its build produced 0 bundle(s)", and every module refused). stub(pm.Module, fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build yet; it "+ - "keeps running what it runs", pm.Module, m.FP2)) + "keeps running what it runs", pm.Module, m.Fp2)) } continue // nothing of it runs: composed from the indexer's manifest, as before } - resolved, err := built(pm.Module, m.FP2, b) + resolved, err := built(pm.Module, m.Fp2, b) if err != nil { if refuse == nil { return err diff --git a/internal/compose/frombus/frombus_test.go b/internal/compose/frombus/frombus_test.go index 8b112ba..33a75fe 100644 --- a/internal/compose/frombus/frombus_test.go +++ b/internal/compose/frombus/frombus_test.go @@ -15,6 +15,8 @@ import ( nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "github.com/novox/mesh-host/internal/compositions" "os" "reflect" @@ -35,7 +37,7 @@ type bus struct { handed map[string]bool placements map[string]meshmoduleassigner.Placement records map[string]nodeengine.NodeRecord - modules map[string]delivery.Module + modules map[string]meshcatalog.ModulesByModuleValue manifests map[string][]byte values map[string]map[string]any // by node.module valuesRead []string @@ -89,7 +91,9 @@ func (b *bus) Handed(context.Context, string) map[string]bool { return b.handed func (b *bus) Records(context.Context) (map[string]nodeengine.NodeRecord, error) { return b.records, nil } -func (b *bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.modules, nil } +func (b *bus) Modules(context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) { + return b.modules, nil +} func (b *bus) Manifest(_ context.Context, digest string) ([]byte, error) { raw, ok := b.manifests[digest] if !ok { @@ -161,13 +165,13 @@ func TestInput(t *testing.T) { if err != nil { t.Fatal(err) } - digest := delivery.ManifestDigest(xterm) + digest := meshcatalogclient.ManifestDigest(xterm) // A module in the settings form (plex and the media modules on one node, 2026-10-26): its config values are in its // layers, not mesh-config, so the placement carries them (MeshValues, Values) and mesh-config's are not read for it. media := []byte(`{"module":"media","version":"1","settings":{"lines":{"kind":"preference","default":0,"why":"kept"},` + `"name":{"kind":"preference","default":"x","why":"shown"}},"own-secrets":{"token":"/srv/media/token"},"resources":[{"id":"conf","type":"file",` + `"path":"/srv/media/conf","mode":"0644","content":"uid=${setting:lines} name=${setting:name}\n"}]}`) - mediaDigest := delivery.ManifestDigest(media) + mediaDigest := meshcatalogclient.ManifestDigest(media) keys := map[string]string{"anchor": publicKey(t), "laptop": publicKey(t)} caps := []string{"container-runtime", "systemd", "wireguard"} tunnel := &meshmoduleassigner.FoundTunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24", @@ -189,7 +193,7 @@ func TestInput(t *testing.T) { "anchor": {Node: "anchor", TunnelKey: keys["anchor"], Capabilities: caps, Contract: 2}, "laptop": {Node: "laptop", TunnelKey: keys["laptop"], Capabilities: caps, Authority: "the mesh's certificate"}, }, - modules: map[string]delivery.Module{"xterm": {Manifest: digest}, "media": {Manifest: mediaDigest}, "unread": {}}, + modules: map[string]meshcatalog.ModulesByModuleValue{"xterm": {Manifest: digest}, "media": {Manifest: mediaDigest}, "unread": {}}, manifests: map[string][]byte{digest: xterm, mediaDigest: media}, values: map[string]map[string]any{"laptop.xterm": {"font": "mono"}, "anchor.xterm": {"font": "serif"}, "laptop.media": {"lines": "not mesh-config's"}}, @@ -337,10 +341,10 @@ func TestInput(t *testing.T) { } 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)}}, - manifests: map[string][]byte{delivery.ManifestDigest(keeper): keeper, delivery.ManifestDigest(lemurs): lemurs, - delivery.ManifestDigest(objects): objects, delivery.ManifestDigest(files): files}, + modules: map[string]meshcatalog.ModulesByModuleValue{"keeper": {Manifest: meshcatalogclient.ManifestDigest(keeper)}, "lemurs": {Manifest: meshcatalogclient.ManifestDigest(lemurs)}, + "objects": {Manifest: meshcatalogclient.ManifestDigest(objects)}, "files": {Manifest: meshcatalogclient.ManifestDigest(files)}}, + manifests: map[string][]byte{meshcatalogclient.ManifestDigest(keeper): keeper, meshcatalogclient.ManifestDigest(lemurs): lemurs, + meshcatalogclient.ManifestDigest(objects): objects, meshcatalogclient.ManifestDigest(files): files}, own: []compose.OwnSecretRow{{Node: "n1", Module: "keeper", Name: "password", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}} } oneFps := map[string]string{"keeper": "src2:" + strings.Repeat("d", 64), "lemurs": "src2:" + strings.Repeat("e", 64), @@ -609,7 +613,7 @@ func TestInput(t *testing.T) { if err := zw.Close(); err != nil { t.Fatal(err) } - archiveDigest := delivery.ManifestDigest(packed.Bytes()) + archiveDigest := meshcatalogclient.ManifestDigest(packed.Bytes()) digestOf := func(c string) string { return "sha256:" + strings.Repeat(c, 64) } movedFps := map[string]string{"restic": "src2:" + strings.Repeat("3", 64), "mesh-provisioner-client": "src2:" + strings.Repeat("4", 64), "store": "src2:" + strings.Repeat("5", 64)} @@ -625,19 +629,19 @@ func TestInput(t *testing.T) { rec := b.records["n1"] rec.Contract, rec.Capabilities = compose.HandedContract, []string{"systemd", "package-manager"} b.records["n1"] = rec - b.modules = map[string]delivery.Module{} + b.modules = map[string]meshcatalog.ModulesByModuleValue{} b.manifests = map[string][]byte{} for module, raw := range map[string][]byte{"restic": resticRaw, "mesh-provisioner-client": clientRaw, "store": store} { - b.modules[module] = delivery.Module{FP2: movedFps[module], Manifest: delivery.ManifestDigest(raw)} - b.manifests[delivery.ManifestDigest(raw)] = raw + b.modules[module] = meshcatalog.ModulesByModuleValue{Fp2: movedFps[module], Manifest: meshcatalogclient.ManifestDigest(raw)} + b.manifests[meshcatalogclient.ManifestDigest(raw)] = raw } // The vault's own answer: the client's own secret; restic's is the client's to ask, so own leaves it out. b.own = []compose.OwnSecretRow{{Node: "n1", Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}} if built { b.builds = map[string]*delivery.BuilderBuild{ - "restic " + movedFps["restic"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(resticRaw), Artifacts: resticArtifacts}, - "mesh-provisioner-client " + movedFps["mesh-provisioner-client"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(clientRaw), Artifacts: clientArtifacts}, - "store " + movedFps["store"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(store)}, + "restic " + movedFps["restic"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(resticRaw), Artifacts: resticArtifacts}, + "mesh-provisioner-client " + movedFps["mesh-provisioner-client"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(clientRaw), Artifacts: clientArtifacts}, + "store " + movedFps["store"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(store)}, } b.archives = map[string][]byte{archiveDigest: packed.Bytes()} } @@ -770,8 +774,8 @@ func TestInput(t *testing.T) { p := b.placements["anchor"] 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 + b.modules["mailer"] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest(mailer)} + b.manifests[meshcatalogclient.ManifestDigest(mailer)] = mailer if set { b.served = map[string]map[string]any{"anchor.mailer": {"domain": "example.org"}} } @@ -852,12 +856,12 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates { } modules, _ := b.Modules(ctx) for name, m := range modules { - raw, err := delivery.EncodeModule(m) + raw, err := meshcatalogclient.EncodeModule(m) put(ModulesRead, name, raw, err) var mf struct{ Module string } _ = json.Unmarshal(b.manifests[m.Manifest], &mf) - k, _ := delivery.ManifestsKey(m.Manifest) - raw, err = delivery.EncodeManifest(delivery.Manifest{Module: mf.Module, JSON: string(b.manifests[m.Manifest])}) + k, _ := meshcatalogclient.ManifestsKey(m.Manifest) + raw, err = meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: mf.Module, Manifest: string(b.manifests[m.Manifest])}) put(ManifestsRead, k, raw, err) } for key, build := range b.builds { diff --git a/internal/compose/frombus/frombustest/frombustest.go b/internal/compose/frombus/frombustest/frombustest.go index beda4ff..e0016d0 100644 --- a/internal/compose/frombus/frombustest/frombustest.go +++ b/internal/compose/frombus/frombustest/frombustest.go @@ -14,6 +14,8 @@ import ( "errors" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "strings" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -28,7 +30,7 @@ var testdata embed.FS type Bus struct { Placement map[string]meshmoduleassigner.Placement Record map[string]nodeengine.NodeRecord - Module map[string]delivery.Module + Module map[string]meshcatalog.ModulesByModuleValue Manifests map[string][]byte Value map[string]map[string]any // by node.module Own []compose.OwnSecretRow @@ -46,8 +48,10 @@ func (b *Bus) Placements(context.Context) (map[string]meshmoduleassigner.Placeme func (b *Bus) Records(context.Context) (map[string]nodeengine.NodeRecord, error) { return b.Record, nil } -func (b *Bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.Module, nil } -func (b *Bus) VaultAuthority(context.Context) string { return "" } +func (b *Bus) Modules(context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) { + return b.Module, nil +} +func (b *Bus) VaultAuthority(context.Context) string { return "" } func (b *Bus) Manifest(_ context.Context, digest string) ([]byte, error) { raw, ok := b.Manifests[digest] @@ -149,7 +153,7 @@ func Provisioned() (*Bus, error) { if err := zw.Close(); err != nil { return nil, err } - archive := delivery.ManifestDigest(packed.Bytes()) + archive := meshcatalogclient.ManifestDigest(packed.Bytes()) digestOf := func(c string) string { return "sha256:" + strings.Repeat(c, 64) } fps := FPs() b := &Bus{ @@ -158,24 +162,24 @@ func Provisioned() (*Bus, error) { 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{}, + Module: map[string]meshcatalog.ModulesByModuleValue{}, Manifests: map[string][]byte{}, Own: []compose.OwnSecretRow{{Node: Node, Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}, Builds: map[string]*delivery.BuilderBuild{ - "restic " + fps["restic"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(resticRaw), Artifacts: []delivery.BuiltArtifact{ + "restic " + fps["restic"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(resticRaw), Artifacts: []delivery.BuiltArtifact{ {Name: "tools", Kind: "bundle", Reference: "artifact-store://restic/tools/blobs/" + digestOf("6"), Digest: digestOf("6")}, {Name: "files", Kind: "archive", Reference: "artifact-store://restic/files/blobs/" + archive, Digest: archive}}}, - "mesh-provisioner-client " + fps["mesh-provisioner-client"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(clientRaw), + "mesh-provisioner-client " + fps["mesh-provisioner-client"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(clientRaw), Artifacts: []delivery.BuiltArtifact{{Name: "client", Kind: "bundle", Reference: "artifact-store://mesh-provisioner-client/client/blobs/" + digestOf("7"), Digest: digestOf("7")}}}, - "store " + fps["store"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(store)}, - "postgres " + fps["postgres"]: {State: delivery.BuildDone, Manifest: delivery.ManifestDigest(postgres)}, + "store " + fps["store"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(store)}, + "postgres " + fps["postgres"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(postgres)}, }, Archives: map[string][]byte{archive: packed.Bytes()}, Held: true, } for module, raw := range map[string][]byte{"restic": resticRaw, "mesh-provisioner-client": clientRaw, "store": store, "postgres": postgres} { - b.Module[module] = delivery.Module{FP2: fps[module], Manifest: delivery.ManifestDigest(raw)} - b.Manifests[delivery.ManifestDigest(raw)] = raw + b.Module[module] = meshcatalog.ModulesByModuleValue{Fp2: fps[module], Manifest: meshcatalogclient.ManifestDigest(raw)} + b.Manifests[meshcatalogclient.ManifestDigest(raw)] = raw } return b, nil } diff --git a/internal/compose/frombus/frombustest/frombustest_test.go b/internal/compose/frombus/frombustest/frombustest_test.go index a071e3b..e0ac451 100644 --- a/internal/compose/frombus/frombustest/frombustest_test.go +++ b/internal/compose/frombus/frombustest/frombustest_test.go @@ -7,6 +7,8 @@ import ( "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" engcomp "github.com/novox/mesh-host/internal/compositions" "reflect" "slices" @@ -72,15 +74,15 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: "backup"}) b.Placement[Node] = p } - digest := delivery.ManifestDigest([]byte(row.bad)) + digest := meshcatalogclient.ManifestDigest([]byte(row.bad)) b.Manifests[digest] = []byte(row.bad) - b.Module["backup"] = delivery.Module{FP2: fp, Manifest: digest} + b.Module["backup"] = meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest} if row.refused != "" { - b.Module["backup"] = delivery.Module{FP2: fp, Manifest: digest, Verdict: delivery.VerdictRefused, + b.Module["backup"] = meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Verdict: meshcatalogclient.VerdictRefused, Problems: []string{row.refused}} } if row.built != "" { - builtDigest := delivery.ManifestDigest([]byte(row.built)) + builtDigest := meshcatalogclient.ManifestDigest([]byte(row.built)) b.Manifests[builtDigest] = []byte(row.built) b.Builds["backup "+fp] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: builtDigest, Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://backup/files/blobs/x", Digest: "sha256:x"}}} @@ -265,7 +267,7 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { delete(r.(map[string]any), "user") } raw, _ := json.Marshal(m) - mod.Manifest = delivery.ManifestDigest(raw) + mod.Manifest = meshcatalogclient.ManifestDigest(raw) bb.Module["mesh-provisioner-client"], bb.Manifests[mod.Manifest] = mod, raw built := *bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] built.Manifest = mod.Manifest @@ -321,13 +323,13 @@ func statesOf(t *testing.T, b *Bus) frombus.States { put(frombus.HaveRead, node+".node", raw, err) } for module, m := range b.Module { - key, _ := delivery.ModulesKey(module) - raw, err := delivery.EncodeModule(m) + key, _ := meshcatalogclient.ModulesKey(module) + raw, err := meshcatalogclient.EncodeModule(m) put(frombus.ModulesRead, key, raw, err) } for digest, man := range b.Manifests { - key, _ := delivery.ManifestsKey(digest) - raw, err := delivery.EncodeManifest(delivery.Manifest{Module: "m", JSON: string(man)}) + key, _ := meshcatalogclient.ManifestsKey(digest) + raw, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "m", Manifest: string(man)}) put(frombus.ManifestsRead, key, raw, err) } for at, build := range b.Builds { diff --git a/internal/compose/frombus/held_rows_test.go b/internal/compose/frombus/held_rows_test.go index 09ab4c8..a60ec47 100644 --- a/internal/compose/frombus/held_rows_test.go +++ b/internal/compose/frombus/held_rows_test.go @@ -7,6 +7,8 @@ import ( "fmt" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" engcomp "github.com/novox/mesh-host/internal/compositions" "strings" "testing" @@ -21,13 +23,13 @@ func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) { t.Helper() p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}} 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{}} + modules: map[string]meshcatalog.ModulesByModuleValue{}, 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, meshmoduleassigner.PlacedModule{Module: module}) - b.modules[module] = delivery.Module{Manifest: delivery.ManifestDigest([]byte(raw))} - b.manifests[delivery.ManifestDigest([]byte(raw))] = []byte(raw) + b.modules[module] = meshcatalog.ModulesByModuleValue{Manifest: meshcatalogclient.ManifestDigest([]byte(raw))} + b.manifests[meshcatalogclient.ManifestDigest([]byte(raw))] = []byte(raw) fps[module] = "src2:" + strings.Repeat(string(rune('1'+i)), 64) } b.placements = map[string]meshmoduleassigner.Placement{"n1": p} @@ -51,7 +53,7 @@ func aNodeRefusedWholeSaysWhatItHeld(t *testing.T) { // refuse writes the indexer's refusal of module's manifest into its modules value (novox/hq ADR 0578 §1). func refuse(b *bus, module string, problems ...string) { m := b.modules[module] - m.Verdict, m.Problems = delivery.VerdictRefused, problems + m.Verdict, m.Problems = meshcatalogclient.VerdictRefused, problems b.modules[module] = m } @@ -144,7 +146,7 @@ func aRunningModuleWaitsForItsBuild(t *testing.T) { b.held = true // every grant held, so a contribution's credential stands in for nothing b.own = []compose.OwnSecretRow{{Node: "n1", Module: "node-tools", Name: "broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}} mod := b.modules["node-tools"] - mod.FP2 = next + mod.Fp2 = next b.modules["node-tools"] = mod b.builds["node-tools "+old] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest, Artifacts: []delivery.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("1"), Digest: dg("1")}}} @@ -254,7 +256,7 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) { name = "runtime" } mod := b.modules[m] - mod.FP2 = fps[m] + mod.Fp2 = fps[m] b.modules[m] = mod b.builds[m+" "+fps[m]] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest, Artifacts: []delivery.BuiltArtifact{{Name: name, Kind: "bundle", Reference: "artifact-store://" + m + "/" + name + "/blobs/" + dg, Digest: dg}}} diff --git a/internal/compose/frombus/states.go b/internal/compose/frombus/states.go index 2c5d38b..501005a 100644 --- a/internal/compose/frombus/states.go +++ b/internal/compose/frombus/states.go @@ -16,6 +16,8 @@ import ( "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "strings" "time" @@ -102,11 +104,11 @@ func (r statesReader) Records(context.Context) (map[string]nodeengine.NodeRecord return out, err } -func (r statesReader) Modules(context.Context) (map[string]delivery.Module, error) { - out := map[string]delivery.Module{} +func (r statesReader) Modules(context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) { + out := map[string]meshcatalog.ModulesByModuleValue{} err := r.each(ModulesRead, func(k string, v []byte) { - if module, err := delivery.ParseModulesKey(k); err == nil { - if m, err := delivery.DecodeModule(v); err == nil { + if module, err := meshcatalogclient.ParseModulesKey(k); err == nil { + if m, err := meshcatalogclient.DecodeModule(v); err == nil { out[module] = m } } @@ -115,7 +117,7 @@ func (r statesReader) Modules(context.Context) (map[string]delivery.Module, erro } func (r statesReader) Manifest(_ context.Context, digest string) ([]byte, error) { - key, err := delivery.ManifestsKey(digest) + key, err := meshcatalogclient.ManifestsKey(digest) if err != nil { return nil, err } @@ -126,11 +128,11 @@ func (r statesReader) Manifest(_ context.Context, digest string) ([]byte, error) if !found { return nil, fmt.Errorf("the indexer holds no manifest %s", digest) } - m, err := delivery.DecodeManifest(v) + m, err := meshcatalogclient.DecodeManifest(v) if err != nil { return nil, err } - return []byte(m.JSON), nil + return []byte(m.Manifest), nil } func (r statesReader) Values(_ context.Context, node, module string) (map[string]any, error) { diff --git a/internal/composer/composer.go b/internal/composer/composer.go index 984bbcd..3e5f3f2 100644 --- a/internal/composer/composer.go +++ b/internal/composer/composer.go @@ -10,14 +10,16 @@ package composer import ( "encoding/json" "fmt" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "slices" "sort" "strings" + sdk "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/catalogue" - sdk "git.novox.be/novox/mesh-sdk/go/composer" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/sdkversion" "github.com/novox/mesh-host/internal/compose" @@ -92,7 +94,7 @@ func Run(r Request) (Verdict, error) { // parse decodes every manifest leniently and takes its verdict (novox/hq ADR 0578 §3, §5): the composer judges none. A // manifest refused by its verdict is held, composed as its stub without its items (as the bus reader holds one); bytes // that do not decode are left out. Both are named in reads. -func parse(m map[string]Manifest, reads map[string]ManifestRead, verdicts map[string]delivery.ManifestVerdict) (map[string]catalogue.Manifest, error) { +func parse(m map[string]Manifest, reads map[string]ManifestRead, verdicts map[string]json.RawMessage) (map[string]catalogue.Manifest, error) { out := map[string]catalogue.Manifest{} for name, man := range m { if len(man.Raw) == 0 { @@ -102,12 +104,16 @@ func parse(m map[string]Manifest, reads map[string]ManifestRead, verdicts map[st read := ManifestRead{Accepted: err == nil, SetAside: decoded.SetAside()} if err != nil { read.Refused = fmt.Sprintf("the manifest of %s does not decode: %v", name, err) - } else if v, ok := verdicts[name]; ok { - if refused, _ := delivery.TakeManifestVerdict(&v); refused { + } else if raw, ok := verdicts[name]; ok { + v, verr := meshcatalogclient.DecodeVerdict(raw) + if verr != nil { + return nil, fmt.Errorf("composer: the verdict of %s is not one: %w", name, verr) + } + if refused, _ := meshcatalogclient.TakeVerdict(&v); refused { if v.Module == "" { v.Module = name } - read = ManifestRead{Refused: v.Why()} + read = ManifestRead{Refused: meshcatalogclient.VerdictWhy(v)} decoded.Items = nil decoded = compose.Stub(decoded) } @@ -132,9 +138,17 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) { if err != nil { return CaseVerdict{}, err } - placements, err := interimPlacements(c.Placements) - if err != nil { - return CaseVerdict{}, fmt.Errorf("composer: the case %q: %w", c.Name, err) + placements := map[string]*meshmoduleassigner.Placement{} + for node, raw := range c.Placements { + if string(raw) == "null" { + placements[node] = nil + continue + } + p, perr := meshmoduleassignerclient.DecodePlacement(raw) + if perr != nil { + return CaseVerdict{}, fmt.Errorf("composer: the case %q: the placement of %s is not one: %w", c.Name, node, perr) + } + placements[node] = &p } in, err := frombus.Placed(base, placements) if err != nil { @@ -143,8 +157,8 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) { // The module the mesh provides itself (the private network) has no repository the indexer reads, so no case carries // it: its manifest comes with mesh-sdk and always wins, as the bus reader has it (frombus readBase). Without it a node // it is assigned to composed nothing (issue 1126, check of mesh-host #200: "nothing provides mesh-wireguard"). - for _, name := range delivery.ProvidedModules() { - m, _, err := delivery.ProvidedManifest(name) + for _, name := range overlay.ProvidedModules() { + m, _, err := overlay.ProvidedManifest(name) if err != nil { return CaseVerdict{}, fmt.Errorf("composer: %w", err) } @@ -345,20 +359,3 @@ 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/composer/composer_test.go b/internal/composer/composer_test.go index a2a2101..c9ff7e1 100644 --- a/internal/composer/composer_test.go +++ b/internal/composer/composer_test.go @@ -3,6 +3,11 @@ package composer import ( "context" "encoding/json" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" + "github.com/novox/mesh-host/internal/buckets/bucketstest" engcomp "github.com/novox/mesh-host/internal/compositions" "os" "reflect" @@ -10,7 +15,6 @@ import ( "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/frombus" "github.com/novox/mesh-host/internal/compose/frombus/frombustest" @@ -55,20 +59,20 @@ func TestARequestItCannotRunIsRefused(t *testing.T) { placed := withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthResolve, Cases: []Case{{Name: "trunk", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}, {Name: "change", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}, - Placements: map[string]*delivery.Placement{"a": {Node: "a", Modules: []delivery.PlacedModule{{Module: "ghost"}}}}}}}, + Placements: bucketstest.Raw(map[string]meshmoduleassigner.Placement{"a": {V: 1, Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "ghost"}}}})}}}, compose.Input{Nodes: []compose.Node{{Name: "a"}}}) if v, err := Run(placed); err != nil || !v.Cases[0].Machines["a"].Composes || v.Cases[1].Machines["a"].Composes || !strings.Contains(v.Cases[1].Machines["a"].Reason, "ghost") { t.Errorf("a case composes its own placement: %+v, %v", v, err) } - placed.Cases[1].Placements = map[string]*delivery.Placement{"z": {Node: "z"}} + placed.Cases[1].Placements = bucketstest.Raw(map[string]meshmoduleassigner.Placement{"z": {V: 1, Node: "z"}}) if _, err := Run(placed); err == nil || !strings.Contains(err.Error(), "holds no node for") { t.Errorf("a case placing a node the input lacks: %v", err) } // Issue 1126 (check of mesh-host #200): the module the mesh provides itself (the private network) comes from mesh-sdk, // a second source beside the indexer's manifests the cases carry, and the input carries no catalogue: a node it is // assigned to still composes, and what it provides is provided. - wg := delivery.ProvidedModules()[0] + wg := overlay.ProvidedModules()[0] provided := withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthResolve, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}}, compose.Input{Nodes: []compose.Node{{Name: "a", Assigned: []string{wg, "demo"}}}}) @@ -245,8 +249,8 @@ func TestTheComposerTakesTheVerdictsOfItsInput(t *testing.T) { if got := v.Cases[0].Manifests["xterm"]; !got.Accepted { t.Fatalf("xterm: %+v, want accepted", got) } - r.Cases[0].Verdicts = map[string]delivery.ManifestVerdict{"xterm": {Module: "xterm", State: delivery.VerdictRefused, - Problems: []string{"falls back to a key not listed"}, Judge: "src2:j"}} + r.Cases[0].Verdicts = bucketstest.Raw(map[string]meshcatalog.VerdictsByDigestValue{"xterm": {V: 1, Module: "xterm", State: meshcatalogclient.VerdictRefused, + Problems: []string{"falls back to a key not listed"}, Judge: "src2:j"}}) shelf, err := parse(r.Cases[0].Manifests, map[string]ManifestRead{}, r.Cases[0].Verdicts) if err != nil || !shelf["xterm"].Stubbed { t.Fatalf("the refused module is not held as its stub: %+v %v", shelf["xterm"], err) diff --git a/internal/judge/answer_test.go b/internal/judge/answer_test.go index bdd7d9f..70e0cc7 100644 --- a/internal/judge/answer_test.go +++ b/internal/judge/answer_test.go @@ -2,16 +2,15 @@ package judge import ( nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/compositions" "strings" "testing" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // An answer too large for the node's key is fitted: the texts go first, the verdict and the paths stay, and the answer // given is not changed (moved from mesh-sdk's placement test with FitAnswer, novox/hq ADR 0593 §7). func TestFitAnswer(t *testing.T) { - long := strings.Repeat("x", delivery.MaxBody) + long := strings.Repeat("x", compositions.MaxBody) big := CandidateAnswer{Proposal: "p-x1", Revision: 1, Verdict: nodeengine.CandidateAnswerVerdictDiffers, Differs: []ModuleDiffers{{ Module: "nats", Why: "differs", Bodies: []ResourceBodies{{ID: "conf", Why: "changed", Paths: []string{"content.links.1", "content.password"}, Composed: long, Running: long}}}}} diff --git a/internal/judge/judge_test.go b/internal/judge/judge_test.go index 2a5b7e3..12829b9 100644 --- a/internal/judge/judge_test.go +++ b/internal/judge/judge_test.go @@ -3,14 +3,13 @@ package judge import ( "encoding/json" "fmt" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "strings" "testing" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) func composedOf(module string, c byte, data bool, resources ...string) Composed { - out := Composed{Node: "ace", Module: module, FP: delivery.SourcePrefix2 + strings.Repeat(string(c), 64), Data: data} + out := Composed{Node: "ace", Module: module, FP: meshcatalogclient.SourcePrefix2 + strings.Repeat(string(c), 64), Data: data} for _, r := range resources { out.Resources = append(out.Resources, json.RawMessage(r)) } diff --git a/internal/selfcompose/candidate.go b/internal/selfcompose/candidate.go index c56f6b4..023db96 100644 --- a/internal/selfcompose/candidate.go +++ b/internal/selfcompose/candidate.go @@ -7,6 +7,9 @@ import ( "errors" "fmt" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "github.com/novox/mesh-host/internal/buckets" engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "slices" @@ -16,7 +19,6 @@ import ( nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/compose/frombus" @@ -25,7 +27,7 @@ import ( // **A check's candidate, composed in shadow on a call** (novox/hq ADR 0593 §7): the node seat's verb compose-candidate. // Given the check's composition key (`.`), it reads the group's candidate from the indexer -// (delivery.CandidatesBucket, keyed by the group alone, delivery.CandidatesKey) at those heads, and composes itself +// (buckets.Candidates, keyed by the group alone, buckets.CandidateKey) at those heads, and composes itself // twice from what its shadow holds (placement, records, the indexer's modules, its config values), with its own sealed // values: once as it is, and once with the candidate's modules laid over the indexer's (each at the candidate's // fingerprint, its manifest by digest and the indexer's verdict on it, ADR 0578 §3; a module the candidate removes left @@ -141,13 +143,13 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi a.Verdict = nec.Unready return a } - group, heads, err := delivery.ParseCompositionKey(ask.Candidate) + group, heads, err := nec.ParseCompositionKey(ask.Candidate) if err != nil { return refuse(err.Error()) } // The ask names the check (the composition key, `.`); the indexer keys the group's candidate by - // the group alone (delivery.CandidatesKey) and holds its heads hash in the value. - ckey, err := delivery.CandidatesKey(group) + // the group alone (buckets.CandidateKey) and holds its heads hash in the value. + ckey, err := buckets.CandidateKey(group) if err != nil { return refuse(err.Error()) } @@ -155,21 +157,21 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi return unready(errNotReady.Error()) } base := run.add(c.scratch()) - raw, err := base.get(ctx, delivery.CandidatesBucket, ckey) + raw, err := base.get(ctx, buckets.Candidates, ckey) if err != nil { return unready(fmt.Sprintf("the indexer's candidate %s cannot be read: %v", ask.Candidate, err)) } if raw == nil { return refuse("the indexer holds no candidate " + ask.Candidate) } - cand, err := delivery.DecodeCandidate(raw) + cand, err := meshcatalog.DecodeCandidatesByGroupValue(raw) if err != nil { return refuse(fmt.Sprintf("the indexer's candidate %s cannot be read: %v", ask.Candidate, err)) } - if cand.HeadsHash != heads { + if cand.Heads != heads { // The group moved to other heads: the check asked is replaced, and this node composes not a candidate it was // not asked about. No answer (ADR 0593 §9), never a verdict on the wrong heads. - return unready(fmt.Sprintf("the indexer's candidate of the group %s is at heads %s, not %s: the check asked is replaced", group, cand.HeadsHash, heads)) + return unready(fmt.Sprintf("the indexer's candidate of the group %s is at heads %s, not %s: the check asked is replaced", group, cand.Heads, heads)) } // Every module's manifest is read before either composition, several at once, into the shadow's own: a composition // reads every manifest in the mesh one by one, which off the control-node took about 35 s each (issue 1126). @@ -332,7 +334,7 @@ func (c *Composer) scratch() *Composer { // warm reads, several at once, each manifest the indexer's modules and the candidate name that the shadow (origin) does // not hold yet, keeping each in origin and in c; one not read is left to the composition, which reads it and says why. // Its reads are not noted as the bus failing: the composition that reads again notes its own. -func (c *Composer) warm(ctx context.Context, origin *Composer, cand delivery.Candidate) { +func (c *Composer) warm(ctx context.Context, origin *Composer, cand meshcatalog.CandidatesByGroupValue) { origin.mu.Lock() want := map[string]bool{} for _, m := range origin.modules { @@ -351,11 +353,11 @@ func (c *Composer) warm(ctx context.Context, origin *Composer, cand delivery.Can if len(want) == 0 { return } - defer c.waiting(fmt.Sprintf("%d of the indexer's manifests (%s)", len(want), delivery.ManifestsBucket))() + defer c.waiting(fmt.Sprintf("%d of the indexer's manifests (%s)", len(want), buckets.Manifests))() slots := make(chan struct{}, warmAtOnce) var wg sync.WaitGroup for digest := range want { - key, err := delivery.ManifestsKey(digest) + key, err := meshcatalogclient.ManifestsKey(digest) if err != nil { continue } @@ -368,15 +370,15 @@ func (c *Composer) warm(ctx context.Context, origin *Composer, cand delivery.Can return } defer func() { <-slots }() - value, err := c.env.Get(ctx, delivery.ManifestsBucket, key) + value, err := c.env.Get(ctx, buckets.Manifests, key) if err != nil || value == nil { return } - m, err := delivery.DecodeManifest(value) + m, err := meshcatalogclient.DecodeManifest(value) if err != nil { return } - c.keepRead(func(k *Composer) { k.manifests[digest] = []byte(m.JSON) }) + c.keepRead(func(k *Composer) { k.manifests[digest] = []byte(m.Manifest) }) }() } wg.Wait() @@ -385,31 +387,31 @@ func (c *Composer) warm(ctx context.Context, origin *Composer, cand delivery.Can // layCandidate lays the candidate's modules over the indexer's: each at its fingerprint, with its manifest's digest and // the indexer's verdict on it (read from its verdicts, none read taken as accepted); a module the candidate removes goes. // It answers the modules laid. -func (c *Composer) layCandidate(ctx context.Context, cand delivery.Candidate) (map[string]bool, error) { +func (c *Composer) layCandidate(ctx context.Context, cand meshcatalog.CandidatesByGroupValue) (map[string]bool, error) { touched := map[string]bool{} for _, pull := range cand.Pulls { for module, cm := range pull.Fps { - if cm.None2 == delivery.CandidateNoneNoModule { + if cm.None2 == candidateNoneNoModule { c.mu.Lock() delete(c.modules, module) c.mu.Unlock() continue } - if cm.FP2 == "" { + if cm.Fp2 == "" { continue // no fingerprint at the preview: composed as the trunk holds it } - m := delivery.Module{FP2: cm.FP2, Manifest: cm.Manifest, Repository: pull.Repository, Commit: pull.Head.Head} + m := meshcatalog.ModulesByModuleValue{Fp2: cm.Fp2, Manifest: cm.Manifest, Repository: pull.Repository, Commit: pull.Head} if cm.Manifest != "" { - key, err := delivery.VerdictsKey(cm.Manifest) + key, err := meshcatalogclient.VerdictsKey(cm.Manifest) if err != nil { return nil, err } - raw, err := c.get(ctx, delivery.VerdictsBucket, key) + raw, err := c.get(ctx, buckets.Verdicts, key) if err != nil { return nil, fmt.Errorf("the indexer's verdict on %s's manifest cannot be read: %w", module, err) } if raw != nil { - v, err := delivery.DecodeManifestVerdict(raw) + v, err := meshcatalogclient.DecodeVerdict(raw) if err != nil { return nil, fmt.Errorf("the indexer's verdict on %s's manifest cannot be read: %w", module, err) } @@ -442,12 +444,12 @@ func (c *Composer) composeShadow(ctx context.Context, running []engcomp.Composed // A module the laid candidate changes is composed at the candidate's build, though the node runs another. for module := range fps { if mod, ok := modules[module]; ok && laid[module] { - fps[module] = mod.FP2 + fps[module] = mod.Fp2 } } kept := map[string]string{} for module, fp := range fps { - if mod, ok := modules[module]; ok && mod.FP2 == fp { + if mod, ok := modules[module]; ok && mod.Fp2 == fp { continue } kept[module] = fp @@ -472,3 +474,7 @@ func (c *Composer) composeShadow(ctx context.Context, running []engcomp.Composed } return comps, held, nil } + +// candidateNoneNoModule is the words the indexer writes in a candidate's module that the merge preview names no manifest +// for: the change removes the module. It is the indexer's text, kept here until the catalogue client names it. +const candidateNoneNoModule = "the merge preview names no manifest for the module's directory" diff --git a/internal/selfcompose/candidate_test.go b/internal/selfcompose/candidate_test.go index 1d481a0..e659478 100644 --- a/internal/selfcompose/candidate_test.go +++ b/internal/selfcompose/candidate_test.go @@ -5,7 +5,11 @@ import ( "encoding/json" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "github.com/novox/mesh-host/internal/buckets" + "github.com/novox/mesh-host/internal/buckets/bucketstest" "strings" "sync/atomic" "testing" @@ -13,7 +17,6 @@ import ( nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/compose" @@ -26,48 +29,48 @@ import ( // within the bound; a shadow not ready and an ended bound answer unready at once, which the checker takes as no answer. func TestTheNodeComposesACandidateOnAsk(t *testing.T) { b := newFakeBus() - fp := delivery.SourcePrefix2 + strings.Repeat("a", 64) - fpNew := delivery.SourcePrefix2 + strings.Repeat("b", 64) - digest := delivery.ManifestDigest([]byte(lemurs)) + fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64) + fpNew := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64) + digest := meshcatalogclient.ManifestDigest([]byte(lemurs)) lemurs2 := strings.Replace(lemurs, `"version":"1"`, `"version":"2"`, 1) - digest2 := delivery.ManifestDigest([]byte(lemurs2)) + digest2 := meshcatalogclient.ManifestDigest([]byte(lemurs2)) for d, body := range map[string]string{digest: lemurs, digest2: lemurs2} { - man, _ := delivery.EncodeManifest(delivery.Manifest{Module: "lemurs", JSON: body}) - key, _ := delivery.ManifestsKey(d) - b.kv[delivery.ManifestsBucket+"/"+key] = man + man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: body}) + key, _ := meshcatalogclient.ManifestsKey(d) + b.kv[buckets.Manifests+"/"+key] = man } - // The indexer keys a candidate by its group alone (delivery.CandidatesKey, the branch name escaped, a slash + // The indexer keys a candidate by its group alone (buckets.CandidateKey, the branch name escaped, a slash // allowed), its heads hash inside the value; the checker asks by the composition key, `.`. candidateOf := func(group, manifest string) string { - head := delivery.Head{Repository: "novox/x", Number: 7, Head: "c0ffee" + manifest[len(manifest)-6:]} - heads, _ := delivery.HeadsHash([]delivery.Head{head}) - pulls := []delivery.CandidatePull{{Head: head, Modules: []string{"lemurs"}, - Fps: map[string]delivery.CandidateModule{"lemurs": {FP2: fpNew, Manifest: manifest}}}} - raw, err := delivery.EncodeCandidate(delivery.Candidate{Group: group, HeadsHash: heads, Pulls: pulls, - FPSet: delivery.FingerprintSet(pulls)}) + head := meshdeliveryclient.Head{Repository: "novox/x", Number: 7, Head: "c0ffee" + manifest[len(manifest)-6:]} + heads, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{head}) + pulls := []meshcatalog.CandidatesByGroupValuePullsItem{bucketstest.Pull(head, []string{"lemurs"}, + map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"lemurs": {Fp2: fpNew, Manifest: manifest}})} + raw, err := bucketstest.GroupCandidate(meshcatalog.CandidatesByGroupValue{Group: group, Heads: heads, Pulls: pulls, + Fpset: meshcatalogclient.FingerprintSet(pulls)}) if err != nil { t.Fatal(err) } - ckey, _ := delivery.CandidatesKey(group) - b.kv[delivery.CandidatesBucket+"/"+ckey] = raw - key, _ := delivery.CompositionKey(group, heads) + ckey, _ := buckets.CandidateKey(group) + b.kv[buckets.Candidates+"/"+ckey] = raw + key, _ := nec.CompositionKey(group, heads) return key } accepted, refusedKey := candidateOf("fix/940-no-retire-on-partial-read", digest2), "" { // The same change, its manifest refused by the indexer's judge. refusedMan := strings.Replace(lemurs2, `"version":"2"`, `"version":"3"`, 1) - d3 := delivery.ManifestDigest([]byte(refusedMan)) - man, _ := delivery.EncodeManifest(delivery.Manifest{Module: "lemurs", JSON: refusedMan}) - key, _ := delivery.ManifestsKey(d3) - b.kv[delivery.ManifestsBucket+"/"+key] = man - vkey, _ := delivery.VerdictsKey(d3) - v, err := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: "lemurs", State: delivery.VerdictRefused, - Problems: []string{"a field the judge refuses"}, Judge: "src2:j", At: time.Now()}) + d3 := meshcatalogclient.ManifestDigest([]byte(refusedMan)) + man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: refusedMan}) + key, _ := meshcatalogclient.ManifestsKey(d3) + b.kv[buckets.Manifests+"/"+key] = man + vkey, _ := meshcatalogclient.VerdictsKey(d3) + v, err := meshcatalogclient.EncodeVerdict(meshcatalog.VerdictsByDigestValue{Module: "lemurs", State: meshcatalogclient.VerdictRefused, + Problems: []string{"a field the judge refuses"}, Judge: "src2:j", At: func(t time.Time) *time.Time { return &t }(time.Now())}) if err != nil { t.Fatal(err) } - b.kv[delivery.VerdictsBucket+"/"+vkey] = v + b.kv[buckets.Verdicts+"/"+vkey] = v refusedKey = candidateOf("fix/941-refused", d3) } @@ -87,7 +90,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { 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"}) + mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}) deadline := time.Now().Add(5 * time.Second) watching := func() int { b.mu.Lock(); defer b.mu.Unlock(); return len(b.watches) } for watching() < 6 && time.Now().Before(deadline) { @@ -95,8 +98,8 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { } 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{buckets.PlacementCandidates, buckets.Placement, buckets.Have, delivery.ModulesBucket, ResolvedBucket} { + b.put(t, buckets.Modules, "lemurs", mod) + for _, bucket := range []string{buckets.PlacementCandidates, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket} { b.send(bucket, agent.Update{Caught: true}) } for !s.ready.Load() && time.Now().Before(deadline) { @@ -129,13 +132,13 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { if _, err := nec.EncodeAnswer(r); err != nil { t.Errorf("the refused answer does not fit the seat's shape: %v", err) } - missing, _ := delivery.CompositionKey("novox/x#8", "0123456789ab") + missing, _ := nec.CompositionKey("novox/x#8", "0123456789ab") if m := s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: missing}); !strings.Contains(m.Refused, "holds no candidate") { t.Errorf("a candidate the indexer does not hold answered %+v", m) } // The group's candidate moved to other heads since the ask: the node composes not the one asked, and answers // unready (no answer), naming the heads it holds. - moved, _ := delivery.CompositionKey("fix/940-no-retire-on-partial-read", "0123456789ab") + moved, _ := nec.CompositionKey("fix/940-no-retire-on-partial-read", "0123456789ab") if m := s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: moved}); m.Verdict != nec.Unready || !strings.Contains(m.Refused, "heads") { t.Errorf("a candidate at other heads answered %+v", m) @@ -154,30 +157,30 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { // composition, into the shadow, and a later ask reads none; an ask whose read does not answer is answered unready at // its bound, naming what the node waits on; and every answer is said in one line with what it waited on. t.Run("each manifest is read once, and the answer comes within its bound", func(t *testing.T) { - fp := delivery.SourcePrefix2 + strings.Repeat("a", 64) - fpNew := delivery.SourcePrefix2 + strings.Repeat("b", 64) + fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64) + fpNew := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64) lemurs2 := strings.Replace(lemurs, `"version":"1"`, `"version":"2"`, 1) other := strings.NewReplacer(`"module":"lemurs"`, `"module":"other"`, "lemurs.service", "other.service").Replace(lemurs) - digest, digest2, digestOther := delivery.ManifestDigest([]byte(lemurs)), delivery.ManifestDigest([]byte(lemurs2)), - delivery.ManifestDigest([]byte(other)) - otherKey, _ := delivery.ManifestsKey(digestOther) + digest, digest2, digestOther := meshcatalogclient.ManifestDigest([]byte(lemurs)), meshcatalogclient.ManifestDigest([]byte(lemurs2)), + meshcatalogclient.ManifestDigest([]byte(other)) + otherKey, _ := meshcatalogclient.ManifestsKey(digestOther) // shadow is a ready shadow of n1 over b: lemurs placed on it, other placed nowhere (its manifest read for the mesh's // catalogue alone), and a candidate laying lemurs at a new build; its clock moves a second at each reading. shadow := func(b *fakeBus) (*Shadow, string) { for d, body := range map[string]string{digest: lemurs, digest2: lemurs2, digestOther: other} { - man, _ := delivery.EncodeManifest(delivery.Manifest{Module: "x", JSON: body}) - key, _ := delivery.ManifestsKey(d) - b.kv[delivery.ManifestsBucket+"/"+key] = man + man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "x", Manifest: body}) + key, _ := meshcatalogclient.ManifestsKey(d) + b.kv[buckets.Manifests+"/"+key] = man } - head := delivery.Head{Repository: "novox/x", Number: 7, Head: "c0ffee"} - heads, _ := delivery.HeadsHash([]delivery.Head{head}) - pulls := []delivery.CandidatePull{{Head: head, Modules: []string{"lemurs"}, - Fps: map[string]delivery.CandidateModule{"lemurs": {FP2: fpNew, Manifest: digest2}}}} - raw, _ := delivery.EncodeCandidate(delivery.Candidate{Group: "fix/1126", HeadsHash: heads, Pulls: pulls, - FPSet: delivery.FingerprintSet(pulls)}) - ckey, _ := delivery.CandidatesKey("fix/1126") - b.kv[delivery.CandidatesBucket+"/"+ckey] = raw + head := meshdeliveryclient.Head{Repository: "novox/x", Number: 7, Head: "c0ffee"} + heads, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{head}) + pulls := []meshcatalog.CandidatesByGroupValuePullsItem{bucketstest.Pull(head, []string{"lemurs"}, + map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"lemurs": {Fp2: fpNew, Manifest: digest2}})} + raw, _ := bucketstest.GroupCandidate(meshcatalog.CandidatesByGroupValue{Group: "fix/1126", Heads: heads, Pulls: pulls, + Fpset: meshcatalogclient.FingerprintSet(pulls)}) + ckey, _ := buckets.CandidateKey("fix/1126") + b.kv[buckets.Candidates+"/"+ckey] = raw env := b.env() var tick atomic.Int64 env.Now = func() time.Time { return time.Unix(0, 0).Add(time.Duration(tick.Add(1)) * time.Second) } @@ -185,17 +188,17 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { 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"} + s.c.modules["lemurs"] = meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"} + s.c.modules["other"] = meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digestOther, Repository: "novox/x", Commit: "c"} s.ready.Store(true) - key, _ := delivery.CompositionKey("fix/1126", heads) + key, _ := nec.CompositionKey("fix/1126", heads) return s, key } readsOf := func(b *fakeBus, key string) (n int) { b.mu.Lock() defer b.mu.Unlock() for _, r := range b.reads { - if r == [2]string{delivery.ManifestsBucket, key} { + if r == [2]string{buckets.Manifests, key} { n++ } } @@ -235,7 +238,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { if tc.stuck { get := s.c.env.Get s.c.env.Get = func(rctx context.Context, bucket, k string) ([]byte, error) { - if bucket == delivery.ManifestsBucket && k == otherKey { + if bucket == buckets.Manifests && k == otherKey { b.mu.Lock() b.reads = append(b.reads, [2]string{bucket, k}) b.mu.Unlock() diff --git a/internal/selfcompose/composedstate.go b/internal/selfcompose/composedstate.go index af2c1f4..5693f90 100644 --- a/internal/selfcompose/composedstate.go +++ b/internal/selfcompose/composedstate.go @@ -90,7 +90,7 @@ func (c *Composer) newestFPs(ctx context.Context, at *published) map[string]stri r := reader{c: c} for _, module := range at.placed { c.mu.Lock() - fp2 := c.modules[module].FP2 + fp2 := c.modules[module].Fp2 c.mu.Unlock() if fp2 != "" { if b, err := r.Build(ctx, module, fp2); err == nil && b != nil && b.State == delivery.BuildDone { @@ -184,7 +184,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin var inOwn []string c.mu.Lock() for module, fp := range newest { - if fp != c.modules[module].FP2 { + if fp != c.modules[module].Fp2 { scope.KeptAt[module] = fp } } diff --git a/internal/selfcompose/composedstate_test.go b/internal/selfcompose/composedstate_test.go index d971e0e..4cf6e82 100644 --- a/internal/selfcompose/composedstate_test.go +++ b/internal/selfcompose/composedstate_test.go @@ -7,6 +7,8 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/buckets/bucketstest" engcomp "github.com/novox/mesh-host/internal/compositions" @@ -70,15 +72,15 @@ func (f *fakeComposed) written() []string { // by a want, and deleted otherwise. func TestTheNodePublishesItsCompositions(t *testing.T) { b := newFakeBus() - fp := delivery.SourcePrefix2 + strings.Repeat("a", 64) - ofp := delivery.SourcePrefix2 + strings.Repeat("c", 64) - digest := delivery.ManifestDigest([]byte(lemurs)) + fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64) + ofp := meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64) + digest := meshcatalogclient.ManifestDigest([]byte(lemurs)) oldie := `{"module":"oldie","version":"1","capabilities":["systemd"],"resources":[{"id":"service","type":"service","unit":"oldie.service","state":"running","boot":"enabled"}]}` - odigest := delivery.ManifestDigest([]byte(oldie)) + odigest := meshcatalogclient.ManifestDigest([]byte(oldie)) for d, json := range map[string]string{digest: lemurs, odigest: oldie} { - man, _ := delivery.EncodeManifest(delivery.Manifest{Module: map[string]string{digest: "lemurs", odigest: "oldie"}[d], JSON: json}) - key, _ := delivery.ManifestsKey(d) - b.kv[delivery.ManifestsBucket+"/"+key] = man + man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: map[string]string{digest: "lemurs", odigest: "oldie"}[d], Manifest: json}) + key, _ := meshcatalogclient.ManifestsKey(d) + b.kv[buckets.Manifests+"/"+key] = man } state := &fakeComposed{kv: map[string]ComposedEntry{}} env := b.env() @@ -93,17 +95,17 @@ func TestTheNodePublishesItsCompositions(t *testing.T) { 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"}) + mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}) + omod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: ofp, Manifest: odigest, Repository: "novox/o", Commit: "c"}) want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true}) nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true}) b.put(t, buckets.Placement, "placement.n1", place) b.put(t, buckets.Have, "n1.node", have) - b.put(t, delivery.ModulesBucket, "lemurs", mod) - b.put(t, delivery.ModulesBucket, "oldie", omod) + b.put(t, buckets.Modules, "lemurs", mod) + b.put(t, buckets.Modules, "oldie", omod) b.put(t, buckets.Wants, "want.n1.lemurs", want) b.put(t, buckets.Wants, "want.n1.node", nodeWant) - for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, delivery.BuilderBuildsBucket} { b.send(bucket, agent.Update{Caught: true}) } for u := next(t, out); !u.Caught; u = next(t, out) { @@ -166,7 +168,7 @@ func TestTheNodePublishesItsCompositions(t *testing.T) { keep.pubKept = map[string]ComposedEntry{} var keys []string for i, ch := range "123456" { - key, _ := engcomp.Key("n1", "lemurs", delivery.SourcePrefix2+strings.Repeat(string(ch), 64)) + key, _ := engcomp.Key("n1", "lemurs", meshcatalogclient.SourcePrefix2+strings.Repeat(string(ch), 64)) keys = append(keys, key) keep.pubKept[key] = ComposedEntry{Revision: uint64(10 + i)} } @@ -174,7 +176,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]meshwants.WantsByWantNodeModuleValue{"lemurs": {Fp: delivery.SourcePrefix2 + strings.Repeat("1", 64)}} + keep.wants = map[string]meshwants.WantsByWantNodeModuleValue{"lemurs": {Fp: meshcatalogclient.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/data_test.go b/internal/selfcompose/data_test.go index dc32133..d2273d3 100644 --- a/internal/selfcompose/data_test.go +++ b/internal/selfcompose/data_test.go @@ -5,6 +5,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-delivery/contracts/go/clients/meshcatalogclient" "github.com/novox/mesh-host/internal/buckets" engcomp "github.com/novox/mesh-host/internal/compositions" "slices" @@ -93,7 +94,7 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { t.Error("the node's grant reads other keys of the engine's state data than its own") } c := New(env) - webKey, _ := engcomp.Key("n1", "web", delivery.SourcePrefix2+strings.Repeat("a", 64)) + webKey, _ := engcomp.Key("n1", "web", meshcatalogclient.SourcePrefix2+strings.Repeat("a", 64)) nodeKey, _ := engcomp.NodeKey("n1") for _, step := range []struct { name string @@ -234,9 +235,9 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { // The items are worked out from what the pass read: a done build is read over the bus once, not again for the data // items after the pass (a module in a running build is read again, since it may land). b := newFakeBus() - done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: delivery.ManifestDigest([]byte(lemurs))}) + done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest([]byte(lemurs))}) running, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: "building"}) - fp := delivery.SourcePrefix2 + strings.Repeat("b", 64) + fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64) doneKey, _ := delivery.BuilderBuildKey("web", fp) runningKey, _ := delivery.BuilderBuildKey("pg", fp) b.kv[delivery.BuilderBuildsBucket+"/"+doneKey], b.kv[delivery.BuilderBuildsBucket+"/"+runningKey] = done, running @@ -257,12 +258,12 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { t.Errorf("the builds were read %v times over the bus; want the done one once, the running one each time", reads) } // A done key the builder claims again (a gone step, issue 1937) and lands done again is read again, not served kept. - again, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: delivery.ManifestDigest([]byte("{}"))}) + again, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest([]byte("{}"))}) b.mu.Lock() b.kv[delivery.BuilderBuildsBucket+"/"+doneKey] = again b.mu.Unlock() r.c.takeBuildDone(agent.Update{Key: doneKey, Value: running}) - if got, err := r.Build(context.Background(), "web", fp); err != nil || got.Manifest != delivery.ManifestDigest([]byte("{}")) { + if got, err := r.Build(context.Background(), "web", fp); err != nil || got.Manifest != meshcatalogclient.ManifestDigest([]byte("{}")) { t.Errorf("a done build written again is served as %+v (%v), not read again", got, err) } } diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index 3f39ef9..ce02dfd 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -24,6 +24,8 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/compositions" engcomp "github.com/novox/mesh-host/internal/compositions" @@ -141,7 +143,7 @@ type Composer struct { // inOwn is, by module, whether this node's have of it names no fingerprint of the new form (mesh-wireguard, a module // at a build of the old form): the node runs it inside its own composition, and composes it there again (issue 1126). inOwn map[string]bool - modules map[string]delivery.Module + modules map[string]meshcatalog.ModulesByModuleValue told map[string]bool // lines said once each (sayOnceEach) values map[string]map[string]any // by module, this node's manifests map[string][]byte // by digest, read once @@ -304,7 +306,7 @@ func New(env Env) *Composer { env.Say = func(string) {} } 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{}, + records: map[string]nodeengine.NodeRecord{}, inOwn: map[string]bool{}, modules: map[string]meshcatalog.ModulesByModuleValue{}, values: map[string]map[string]any{}, manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]delivery.BuilderBuild{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{}, pkgs: map[string]compositions.Composed{}, asked: make(chan chan Pass, 8)} } @@ -365,7 +367,7 @@ func (c *Composer) inputs() []input { // A watch of every key is refused by the bus (issue 1126, 2026-10-27 09:21: the fold's `>` stopped every agent). {buckets.Have, "*." + nodeengineclient.NodeWord, nil, (*Composer).takeRecordChanged}, {buckets.Have, node + ".>", nil, (*Composer).takeModuleHave}, - {delivery.ModulesBucket, ">", (*Composer).takeModule, nil}, + {buckets.Modules, ">", (*Composer).takeModule, nil}, {ResolvedBucket, node + ".>", (*Composer).takeValue, nil}, // The builder's builds: a pass that held a module for a build not yet done is made again when it lands (issue // 774, 2026-10-30 19:30: node-tools' build landed and ace, g14 and shanks stayed refused until another input moved). @@ -398,7 +400,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) @@ -628,8 +630,8 @@ func (c *Composer) entry(bucket, key string) (v any, has, known bool) { } p, ok := c.placements[node] return p, ok, true - case delivery.ModulesBucket: - name, err := delivery.ParseModulesKey(key) + case buckets.Modules: + name, err := meshcatalogclient.ParseModulesKey(key) if err != nil { return nil, false, true } @@ -737,7 +739,7 @@ func (c *Composer) takeModuleHave(u agent.Update) bool { // (waiting for a yes inside the node's own stays in). interrupted := h.Phase == nodeengineclient.PhaseHeld && h.Detail == agent.BackupInterrupted runsNothing := (h.Phase == nodeengineclient.PhaseFailed || interrupted) && h.Fp == "" && h.Composed == "" - in = h.Phase != nodeengineclient.PhaseRemoved && !runsNothing && !strings.HasPrefix(h.Fp, delivery.SourcePrefix2) + in = h.Phase != nodeengineclient.PhaseRemoved && !runsNothing && !strings.HasPrefix(h.Fp, meshcatalogclient.SourcePrefix2) } } c.mu.Lock() @@ -784,13 +786,13 @@ func (c *Composer) inOwnOf(p meshmoduleassigner.Placement) []string { } func (c *Composer) takeModule(u agent.Update) { - name, err := delivery.ParseModulesKey(u.Key) + name, err := meshcatalogclient.ParseModulesKey(u.Key) if err != nil { return } c.mu.Lock() defer c.mu.Unlock() - m, err := delivery.DecodeModule(u.Value) + m, err := meshcatalogclient.DecodeModule(u.Value) if u.Deleted || err != nil { delete(c.modules, name) return @@ -863,7 +865,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update { continue } fps[module] = w.Fp - if m, ok := c.modules[module]; ok && m.FP2 != w.Fp { + if m, ok := c.modules[module]; ok && m.Fp2 != w.Fp { behind = append(behind, module) } } @@ -1263,10 +1265,10 @@ func (c *Composer) HoldsData(ctx context.Context, module string) (data, known bo // the indexer's verdict from the module's value (a missing one is accepted). held is why the module is held, "" when it is // not: a manifest the indexer refused, or one not read past a field this engine does not know. A field set aside is said // once, and an error is returned only for bytes that are no manifest, which the caller says in its own words. -func (c *Composer) taken(module string, m delivery.Module, raw []byte) (man catalogue.Manifest, held string, err error) { - if v := m.VerdictOf(module); v != nil { - if refused, _ := delivery.TakeManifestVerdict(v); refused { - return catalogue.Manifest{}, v.Why(), nil +func (c *Composer) taken(module string, m meshcatalog.ModulesByModuleValue, raw []byte) (man catalogue.Manifest, held string, err error) { + if v := meshcatalogclient.ModuleVerdict(module, m); v != nil { + if refused, _ := meshcatalogclient.TakeVerdict(v); refused { + return catalogue.Manifest{}, meshcatalogclient.VerdictWhy(*v), nil } } man, err = catalogue.Decode(raw) @@ -1390,10 +1392,10 @@ func (r reader) Records(context.Context) (map[string]nodeengine.NodeRecord, erro return out, nil } -func (r reader) Modules(context.Context) (map[string]delivery.Module, error) { +func (r reader) Modules(context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) { r.c.mu.Lock() defer r.c.mu.Unlock() - out := make(map[string]delivery.Module, len(r.c.modules)) + out := make(map[string]meshcatalog.ModulesByModuleValue, len(r.c.modules)) for k, v := range r.c.modules { out[k] = v } @@ -1407,24 +1409,24 @@ func (r reader) Manifest(ctx context.Context, digest string) ([]byte, error) { if ok { return raw, nil } - key, err := delivery.ManifestsKey(digest) + key, err := meshcatalogclient.ManifestsKey(digest) if err != nil { return nil, err } - defer r.c.waiting("the manifest " + digest + " (" + delivery.ManifestsBucket + ")")() - value, err := r.c.get(ctx, delivery.ManifestsBucket, key) + defer r.c.waiting("the manifest " + digest + " (" + buckets.Manifests + ")")() + value, err := r.c.get(ctx, buckets.Manifests, key) if err != nil { return nil, err } if value == nil { return nil, fmt.Errorf("the indexer holds no manifest %s", digest) } - m, err := delivery.DecodeManifest(value) + m, err := meshcatalogclient.DecodeManifest(value) if err != nil { return nil, err } - r.c.keepRead(func(k *Composer) { k.manifests[digest] = []byte(m.JSON) }) - return []byte(m.JSON), nil + r.c.keepRead(func(k *Composer) { k.manifests[digest] = []byte(m.Manifest) }) + return []byte(m.Manifest), nil } func (r reader) Build(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) { @@ -1723,13 +1725,13 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string continue } fps[m] = w.Fp - if mm, ok := c.modules[m]; ok && mm.FP2 != w.Fp { + if mm, ok := c.modules[m]; ok && mm.Fp2 != w.Fp { kept[m] = w.Fp } } fps[module] = fp delete(kept, module) - if mm, ok := c.modules[module]; !ok || mm.FP2 != fp { + if mm, ok := c.modules[module]; !ok || mm.Fp2 != fp { kept[module] = fp } own, placed := c.placements[c.env.Node] diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index d029b67..dcbaa2c 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -13,6 +13,8 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/buckets/bucketstest" "github.com/novox/mesh-host/internal/compositions" @@ -151,11 +153,11 @@ func quiet(t *testing.T, out <-chan agent.Update) { // names any more is taken back; a node that cannot compose says so once and hands nothing. func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { b := newFakeBus() - fp := delivery.SourcePrefix2 + strings.Repeat("a", 64) - digest := delivery.ManifestDigest([]byte(lemurs)) - man, _ := delivery.EncodeManifest(delivery.Manifest{Module: "lemurs", JSON: lemurs}) - key, _ := delivery.ManifestsKey(digest) - b.kv[delivery.ManifestsBucket+"/"+key] = man + fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64) + digest := meshcatalogclient.ManifestDigest([]byte(lemurs)) + man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: lemurs}) + key, _ := meshcatalogclient.ManifestsKey(digest) + b.kv[buckets.Manifests+"/"+key] = man ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -188,26 +190,26 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { // oldie is placed and no want names it (a module at a build of the old form, nats on novox): the node runs it inside // its own composition, and composes it there again, even before the agent has applied anything (Running empty). oldie := `{"module":"oldie","version":"1","capabilities":["systemd"],"resources":[{"id":"service","type":"service","unit":"oldie.service","state":"running","boot":"enabled"}]}` - odigest := delivery.ManifestDigest([]byte(oldie)) - oman, _ := delivery.EncodeManifest(delivery.Manifest{Module: "oldie", JSON: oldie}) - 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"}) + odigest := meshcatalogclient.ManifestDigest([]byte(oldie)) + oman, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "oldie", Manifest: oldie}) + okey, _ := meshcatalogclient.ManifestsKey(odigest) + b.kv[buckets.Manifests+"/"+okey] = oman + omod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64), Manifest: odigest, Repository: "novox/o", Commit: "c"}) place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, 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"}) + mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}) want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true}) nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true}) b.put(t, buckets.Placement, "placement.n1", place) b.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, buckets.Modules, "lemurs", mod) + b.put(t, buckets.Modules, "oldie", omod) b.put(t, buckets.Wants, "want.n1.lemurs", want) b.put(t, buckets.Wants, "want.n1.node", nodeWant) - for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, delivery.BuilderBuildsBucket} { b.send(bucket, agent.Update{Caught: true}) } got := map[string]bool{} @@ -285,17 +287,17 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { if dig, ok := c.ComposedToday(ctx, "lemurs", fp); !ok || dig != handedDig { t.Errorf("composed today at the build it runs: %s %v, want %s", dig, ok, handedDig) } - if dig, ok := c.ComposedToday(ctx, "lemurs", delivery.SourcePrefix2+strings.Repeat("9", 64)); ok { + if dig, ok := c.ComposedToday(ctx, "lemurs", meshcatalogclient.SourcePrefix2+strings.Repeat("9", 64)); ok { t.Errorf("a build that cannot be composed was composed today: %s", dig) } chgJSON := strings.Replace(lemurs, `"state":"running"`, `"state":"stopped"`, 1) - chgDigest := delivery.ManifestDigest([]byte(chgJSON)) - chgMan, _ := delivery.EncodeManifest(delivery.Manifest{Module: "lemurs", JSON: chgJSON}) - chgKey, _ := delivery.ManifestsKey(chgDigest) + chgDigest := meshcatalogclient.ManifestDigest([]byte(chgJSON)) + chgMan, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: chgJSON}) + chgKey, _ := meshcatalogclient.ManifestsKey(chgDigest) b.mu.Lock() - b.kv[delivery.ManifestsBucket+"/"+chgKey] = chgMan + b.kv[buckets.Manifests+"/"+chgKey] = chgMan b.mu.Unlock() - chgMod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: chgDigest, Repository: "novox/x", Commit: "c"}) + chgMod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: chgDigest, Repository: "novox/x", Commit: "c"}) c.takeModule(agent.Update{Key: "lemurs", Value: chgMod}) if dig, ok := c.ComposedToday(ctx, "lemurs", fp); !ok || dig == handedDig { t.Errorf("composed today with a changed input: %s %v, want another digest than %s", dig, ok, handedDig) @@ -427,14 +429,14 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { `{"id":"pkg","type":"package","package":"haproxy"}]}` c3, told := fresh(env.Get) for m, raw := range map[string]string{"de-spiegel": string(spiegel), "proxy": proxy} { - dig := delivery.ManifestDigest([]byte(raw)) - enc, _ := delivery.EncodeManifest(delivery.Manifest{Module: m, JSON: raw}) - k, _ := delivery.ManifestsKey(dig) + dig := meshcatalogclient.ManifestDigest([]byte(raw)) + enc, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: m, Manifest: raw}) + k, _ := meshcatalogclient.ManifestsKey(dig) b.mu.Lock() - b.kv[delivery.ManifestsBucket+"/"+k] = enc + b.kv[buckets.Manifests+"/"+k] = enc 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"} + mfp := meshcatalogclient.SourcePrefix2 + strings.Repeat(map[string]string{"de-spiegel": "d", "proxy": "e"}[m], 64) + c3.modules[m] = meshcatalog.ModulesByModuleValue{Fp2: mfp, Manifest: dig, Repository: "novox/" + m, Commit: "c"} c3.wants[m] = meshwants.WantsByWantNodeModuleValue{Fp: mfp, Self: true} } p3 := c3.placements["n1"] @@ -480,16 +482,16 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Fatal(err) } c4, told4 := fresh(env.Get) - powerDig := delivery.ManifestDigest(power) - powerEnc, _ := delivery.EncodeManifest(delivery.Manifest{Module: "power", JSON: string(power)}) - powerKey, _ := delivery.ManifestsKey(powerDig) + powerDig := meshcatalogclient.ManifestDigest(power) + powerEnc, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "power", Manifest: string(power)}) + powerKey, _ := meshcatalogclient.ManifestsKey(powerDig) b.mu.Lock() - b.kv[delivery.ManifestsBucket+"/"+powerKey] = powerEnc + b.kv[buckets.Manifests+"/"+powerKey] = powerEnc b.mu.Unlock() - powerFP := delivery.SourcePrefix2 + strings.Repeat("c", 64) - c4.modules["power"] = delivery.Module{FP2: powerFP, Manifest: powerDig, Repository: "novox/mesh-catalog", Commit: "ca8d6f33", + powerFP := meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64) + c4.modules["power"] = meshcatalog.ModulesByModuleValue{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`}} + Verdict: meshcatalogclient.VerdictRefused, Problems: []string{`bar item "shows" is none of them`}} c4.wants["power"] = meshwants.WantsByWantNodeModuleValue{Fp: powerFP, Self: true} p4 := c4.placements["n1"] p4.Modules = append(append([]meshmoduleassigner.PlacedModule{}, p4.Modules...), meshmoduleassigner.PlacedModule{Module: "power"}) @@ -505,7 +507,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Errorf("the agent was told %q, want power held alone, naming its item", *told4) } // Review of issue 774 (the kept read): mesh-wireguard, which the mesh provides and which never has a fingerprint, is - // folded in the node's own and composed from the manifest mesh-sdk provides (delivery.ProvidedManifest), not from + // folded in the node's own and composed from the manifest mesh-sdk provides (overlay.ProvidedManifest), not from // what runs nor the kept declaration: with nothing running (the kept read retired, Running empty) the node's own // still carries its resources, so retiring the read removes nothing of it. c5, told5 := fresh(env.Get) @@ -569,9 +571,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { }{ {"lemurs's wanted build, not done", "lemurs", fp, "building", false}, {"lemurs's wanted build lands", "lemurs", fp, delivery.BuildDone, true}, - {"another build of lemurs", "lemurs", delivery.SourcePrefix2 + strings.Repeat("e", 64), delivery.BuildDone, false}, - {"a placed module no want names, at its trunk's build", "oldie", delivery.SourcePrefix2 + strings.Repeat("c", 64), delivery.BuildDone, true}, - {"a module neither wanted nor placed here", "ghost", delivery.SourcePrefix2 + strings.Repeat("c", 64), delivery.BuildDone, false}, + {"another build of lemurs", "lemurs", meshcatalogclient.SourcePrefix2 + strings.Repeat("e", 64), delivery.BuildDone, false}, + {"a placed module no want names, at its trunk's build", "oldie", meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64), delivery.BuildDone, true}, + {"a module neither wanted nor placed here", "ghost", meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64), delivery.BuildDone, false}, } { before := passes() key, _ := delivery.BuilderBuildKey(row.module, row.fp) @@ -592,7 +594,7 @@ 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, _ := 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}) + elsewhere, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: meshcatalogclient.SourcePrefix2 + strings.Repeat("e", 64), Self: true}) // Research 072 row 37: the placement converted to one key per module and node moves nothing composed; a write in // keys makes its pass at its node key, the commit, and not at a module key written before it nor at the mirror. placeBase := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, @@ -645,7 +647,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { {"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 module again", buckets.Modules, "lemurs", mod, false}, {"lemurs's want at another build", buckets.Wants, "want.n1.lemurs", elsewhere, true}, {"lemurs's want back at its build", buckets.Wants, "want.n1.lemurs", want, true}, }, placementRows...) { @@ -672,11 +674,11 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { tb := newFakeBus() tool := `{"module":"tool","version":"1","capabilities":["systemd"],"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` + `"resources":[{"id":"service","type":"service","unit":"tool.service","state":"running","boot":"enabled"}]}` - tdigest := delivery.ManifestDigest([]byte(tool)) - tman, _ := delivery.EncodeManifest(delivery.Manifest{Module: "tool", JSON: tool}) - tkey, _ := delivery.ManifestsKey(tdigest) - tb.kv[delivery.ManifestsBucket+"/"+tkey] = tman - old, next2 := delivery.SourcePrefix2+strings.Repeat("1", 64), delivery.SourcePrefix2+strings.Repeat("2", 64) + tdigest := meshcatalogclient.ManifestDigest([]byte(tool)) + tman, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "tool", Manifest: tool}) + tkey, _ := meshcatalogclient.ManifestsKey(tdigest) + tb.kv[buckets.Manifests+"/"+tkey] = tman + old, next2 := meshcatalogclient.SourcePrefix2+strings.Repeat("1", 64), meshcatalogclient.SourcePrefix2+strings.Repeat("2", 64) tenv := tb.env() var held []string var hmu sync.Mutex @@ -696,16 +698,16 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } 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"}) + tmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: next2, Manifest: tdigest, Repository: "novox/t", Commit: "c"}) 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, buckets.Placement, "placement.n1", tplace) tb.put(t, buckets.Have, "n1.node", have) - tb.put(t, delivery.ModulesBucket, "tool", tmod) + tb.put(t, buckets.Modules, "tool", tmod) tb.put(t, buckets.Wants, "want.n1.tool", twant) - for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, delivery.ModulesBucket, + for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, delivery.BuilderBuildsBucket} { tb.send(bucket, agent.Update{Caught: true}) } @@ -740,7 +742,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } // 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) + gone := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64) missing, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: gone, Self: true}) b.put(t, buckets.Wants, "want.n1.lemurs", missing) quiet(t, out) @@ -766,16 +768,16 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } keeper := `{"module":"keeper","version":"1","own-secrets":{"password":"/var/lib/mesh/keeper/password"},` + `"resources":[{"id":"state","type":"directory","path":"/var/lib/mesh/keeper","mode":"0700"}]}` - kdigest := delivery.ManifestDigest([]byte(keeper)) - kman, _ := delivery.EncodeManifest(delivery.Manifest{Module: "keeper", JSON: keeper}) - kkey, _ := delivery.ManifestsKey(kdigest) + kdigest := meshcatalogclient.ManifestDigest([]byte(keeper)) + kman, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "keeper", Manifest: keeper}) + kkey, _ := meshcatalogclient.ManifestsKey(kdigest) b.mu.Lock() - b.kv[delivery.ManifestsBucket+"/"+kkey] = kman + b.kv[buckets.Manifests+"/"+kkey] = kman b.mu.Unlock() - kmod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: kdigest, Repository: "novox/k", Commit: "c"}) + kmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: kdigest, Repository: "novox/k", Commit: "c"}) both, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}, {Module: "keeper"}}}) - b.put(t, delivery.ModulesBucket, "keeper", kmod) + b.put(t, buckets.Modules, "keeper", kmod) // n1 is held in keys per module and node since the rows above, which a legacy key no longer moves. bothPlaced, _ := meshmoduleassignerclient.DecodePlacement(both) bothWrites, err := placeKeys.Writes("n1", &bothPlaced) @@ -1041,9 +1043,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } sb := newPatternBus() 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}} + mods := map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}, + "keeper": {Fp2: fp, Manifest: kdigest, Repository: "novox/k", Commit: "c"}} + readOnly := &fakeBus{kv: map[string][]byte{buckets.Manifests + "/" + key: man, buckets.Manifests + "/" + kkey: kman}} senv := readOnly.env() senv.Watch = sb.watch var ownFails atomic.Bool // the vault's own on a bus that does not answer (2026-10-28, ace's answer to seed-7) @@ -1096,8 +1098,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { 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) + raw, _ := meshcatalogclient.EncodeModule(m) + sb.put(buckets.Modules, name, raw, 1) } wantLemurs := meshwants.WantsByWantNodeModuleKey("n1", "lemurs") var lemursDigest string @@ -1252,8 +1254,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { lemursHave("its tool lemurs_health: no answer within 10s", 11) lemursHave("its tool lemurs_health: no answer within 20s", 12) sb.put(buckets.Placement, "placement.n1", tp, 2) - lraw, _ := delivery.EncodeModule(mods["lemurs"]) - sb.put(delivery.ModulesBucket, "lemurs", lraw, 2) + lraw, _ := meshcatalogclient.EncodeModule(mods["lemurs"]) + sb.put(buckets.Modules, "lemurs", lraw, 2) drain() if got := answering(); got != settled { t.Errorf("answered %d more time(s) with no candidate revision and no input of the node changed", got-settled) @@ -1311,7 +1313,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { // Review of L2b, round 2: a watch ends while the link stays; a group's key is deleted before the shadow watches // again. The new run reads afresh, so that group is no longer answered. sb.missing(buckets.PlacementCandidates, true) - sb.end(delivery.ModulesBucket) + sb.end(buckets.Modules) dropsKey := meshmoduleassigner.CandidatesByProposalNodeKey("drops", "n1") sb.del(buckets.PlacementCandidates, dropsKey) time.Sleep(30 * time.Millisecond) @@ -1358,8 +1360,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { 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) + raw, _ := meshcatalogclient.EncodeModule(m) + nb.put(buckets.Modules, name, raw, 1) } sameRaw, _ := bucketstest.Candidate(meshmoduleassigner.CandidatesByProposalNodeValue{Proposal: "same", Placement: &trunk}) sameKey := meshmoduleassigner.CandidatesByProposalNodeKey("same", "n1") @@ -1368,7 +1370,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { 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} { + for _, bucket := range []string{buckets.PlacementCandidates, buckets.Placement, buckets.Have, buckets.Modules, delivery.BuilderBuildsBucket} { nb.send(bucket, agent.Update{Caught: true}) } select { @@ -1451,7 +1453,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { inOwnRunning := []compositions.Composed{{Node: "n1", Module: "lemurs", FP: fp}} other := "src2:" + strings.Repeat("9", 64) gotFps := shadowBuilds(inOwnRunning, meshmoduleassigner.Placement{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "nats"}, {Module: "keeper"}}}, - map[string]delivery.Module{"lemurs": {FP2: other}, "nats": {FP2: other}, "keeper": {FP2: other}}, []string{"nats"}) + map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Fp2: other}, "nats": {Fp2: other}, "keeper": {Fp2: other}}, []string{"nats"}) if fmt.Sprint(gotFps) != fmt.Sprint(map[string]string{"lemurs": fp, "keeper": other}) { t.Errorf("shadow builds %v; want lemurs at what runs, keeper at the trunk's, nats at none (folded)", gotFps) } @@ -1510,10 +1512,10 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { certEnv.VaultAuthority = func() string { return "" } cc := New(certEnv) withCert := `{"module":"web","version":"1","certificate":{"into":"/etc/web/tls.crt"}}` - cdigest := delivery.ManifestDigest([]byte(withCert)) + cdigest := meshcatalogclient.ManifestDigest([]byte(withCert)) cc.manifests[digest] = []byte(lemurs) cc.manifests[cdigest] = []byte(withCert) - cc.modules = map[string]delivery.Module{"lemurs": {Manifest: digest}, "web": {Manifest: cdigest}} + cc.modules = map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Manifest: digest}, "web": {Manifest: cdigest}} cc.records["n1"] = nodeengine.NodeRecord{Node: "n1", ServingKey: base64.StdEncoding.EncodeToString(make([]byte, 32))} onlyLemurs := meshmoduleassigner.Placement{Node: "n1", Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}}} cc.certifyIfWanted(context.Background(), reader{c: cc}, onlyLemurs) @@ -1663,7 +1665,7 @@ func (b *patternBus) caught() { type fixedReader struct { placements map[string]meshmoduleassigner.Placement records map[string]nodeengine.NodeRecord - modules map[string]delivery.Module + modules map[string]meshcatalog.ModulesByModuleValue manifests map[string][]byte } @@ -1673,7 +1675,7 @@ func (r fixedReader) Placements(context.Context) (map[string]meshmoduleassigner. func (r fixedReader) Records(context.Context) (map[string]nodeengine.NodeRecord, error) { return r.records, nil } -func (r fixedReader) Modules(context.Context) (map[string]delivery.Module, error) { +func (r fixedReader) Modules(context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) { return r.modules, nil } func (r fixedReader) Manifest(_ context.Context, d string) ([]byte, error) { @@ -1717,7 +1719,7 @@ func TestAHaveSaysWhetherItsModuleRunsInsideTheNodesOwn(t *testing.T) { Health: nodeengineclient.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, true, true}, // The live have of novox's de-spiegel (2026-10-28): a build of the old form, named by `build`, and no `fp`. {"running a build of the old form", nodeengine.HaveByNodeModuleValue{Build: "build-1790368817085312877", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, true, true}, - {"running a build of the new form", nodeengine.HaveByNodeModuleValue{Fp: delivery.SourcePrefix2 + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("c", 64), + {"running a build of the new form", nodeengine.HaveByNodeModuleValue{Fp: meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("c", 64), Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, false, false}, {"running with no fingerprint (mesh-wireguard)", nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, true, false}, {"held today", nodeengine.HaveByNodeModuleValue{Fp: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: nodeengineclient.PhaseHeld, @@ -1768,11 +1770,11 @@ func (g *gate) MayAct(...string) bool { return g.open.Load() } // already runs with the same composition is not held. func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) { b := newFakeBus() - fp := delivery.SourcePrefix2 + strings.Repeat("a", 64) - digest := delivery.ManifestDigest([]byte(lemurs)) - man, _ := delivery.EncodeManifest(delivery.Manifest{Module: "lemurs", JSON: lemurs}) - key, _ := delivery.ManifestsKey(digest) - b.kv[delivery.ManifestsBucket+"/"+key] = man + fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64) + digest := meshcatalogclient.ManifestDigest([]byte(lemurs)) + man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: lemurs}) + key, _ := meshcatalogclient.ManifestsKey(digest) + b.kv[buckets.Manifests+"/"+key] = man ctx, cancel := context.WithCancel(context.Background()) defer cancel() env := b.env() @@ -1790,15 +1792,15 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) { 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"}) + mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}) want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true}) nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true}) b.put(t, buckets.Placement, "placement.n1", place) b.put(t, buckets.Have, "n1.node", have) - b.put(t, delivery.ModulesBucket, "lemurs", mod) + b.put(t, buckets.Modules, "lemurs", mod) b.put(t, buckets.Wants, "want.n1.lemurs", want) b.put(t, buckets.Wants, "want.n1.node", nodeWant) - for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, 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 4ca4646..abc73a1 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -9,18 +9,17 @@ import ( nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "github.com/novox/mesh-host/internal/buckets" + "github.com/novox/mesh-host/internal/compose/frombus" engcomp "github.com/novox/mesh-host/internal/compositions" + "github.com/novox/mesh-host/internal/judge" "sort" "strings" "sync" "sync/atomic" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/novox/mesh-host/internal/compose/frombus" - "github.com/novox/mesh-host/internal/judge" - "github.com/novox/mesh-host/internal/agent" ) @@ -165,7 +164,7 @@ func (s *Shadow) Run(ctx context.Context) error { {buckets.PlacementCandidates, ">", nil, nil}, {buckets.Placement, ">", (*Composer).takePlacement, nil}, {buckets.Have, "*." + nodeengineclient.NodeWord, nil, nil}, - {delivery.ModulesBucket, ">", (*Composer).takeModule, nil}, + {buckets.Modules, ">", (*Composer).takeModule, nil}, {ResolvedBucket, node + ".>", (*Composer).takeValue, nil}, // What this node runs moved: an apply ends in its module's have. {buckets.Have, node + ".>", nil, nil}, @@ -521,7 +520,7 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (nodeengin s.mu.Unlock() c.mu.Lock() - modules := make(map[string]delivery.Module, len(c.modules)) + modules := make(map[string]meshcatalog.ModulesByModuleValue, len(c.modules)) for k, v := range c.modules { modules[k] = v } @@ -557,7 +556,7 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (nodeengin fps := shadowBuilds(running, *placement, modules, inOwn) kept := map[string]string{} for module, fp := range fps { - if mod, ok := modules[module]; ok && mod.FP2 == fp { + if mod, ok := modules[module]; ok && mod.Fp2 == fp { continue } kept[module] = fp // composed from that build (frombus.Scope.KeptAt, issue 1126) @@ -665,7 +664,7 @@ func asHavesSay(a nodeengine.CandidateAnswer, ran map[string]string) nodeengine. // of the old form) is given none, so it is folded there again, as the node's own composer folds it, rather than // composed at the trunk's build as if the candidate added it (issue 1126: nats and photos have a trunk build that no // want names). -func shadowBuilds(running []engcomp.Composed, placement meshmoduleassigner.Placement, modules map[string]delivery.Module, inOwn []string) map[string]string { +func shadowBuilds(running []engcomp.Composed, placement meshmoduleassigner.Placement, modules map[string]meshcatalog.ModulesByModuleValue, inOwn []string) map[string]string { fps := map[string]string{} for _, r := range running { if r.Module != "" && r.FP != "" { @@ -678,8 +677,8 @@ func shadowBuilds(running []engcomp.Composed, placement meshmoduleassigner.Place } for _, m := range placement.Modules { if _, runs := fps[m.Module]; !runs && !folded[m.Module] { - if mod, ok := modules[m.Module]; ok && mod.FP2 != "" { - fps[m.Module] = mod.FP2 + if mod, ok := modules[m.Module]; ok && mod.Fp2 != "" { + fps[m.Module] = mod.Fp2 } } } diff --git a/internal/userlist/userlist_test.go b/internal/userlist/userlist_test.go index 7aad01e..3fdecc0 100644 --- a/internal/userlist/userlist_test.go +++ b/internal/userlist/userlist_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" engcomp "github.com/novox/mesh-host/internal/compositions" "strings" "sync" @@ -17,7 +18,7 @@ import ( const node = "novox" -func fpOf(c string) string { return delivery.SourcePrefix2 + strings.Repeat(c, 64) } +func fpOf(c string) string { return meshcatalogclient.SourcePrefix2 + strings.Repeat(c, 64) } // feed is a watch the test writes to: the keys there before the watch, Caught, then each put. watching is closed once // the watch began, so a put after it is never handed before the keys there before it. From 4bfa94dfefcd07b3c0c330a45328ecd6813777d6 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:44:31 +0100 Subject: [PATCH 4/5] =?UTF-8?q?Node-engine:=20a=20builder's=20build,=20its?= =?UTF-8?q?=20keys=20and=20codec=20come=20from=20the=20builder's=20builds?= =?UTF-8?q?=20package=20(ADR=200634=20=C2=A71,=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- cmd/mesh-compose/main_test.go | 4 +- cmd/mesh-host/nodeagent_integration_test.go | 16 +++--- go.mod | 1 + internal/compose/frombus/answer_rows_test.go | 14 +++--- internal/compose/frombus/frombus.go | 17 ++++--- internal/compose/frombus/frombus_test.go | 28 +++++------ .../frombus/frombustest/frombustest.go | 25 +++++----- .../frombus/frombustest/frombustest_test.go | 14 +++--- internal/compose/frombus/held_rows_test.go | 25 +++++----- internal/compose/frombus/states.go | 13 +++-- internal/compose/frombus/twice.go | 10 ++-- internal/selfcompose/composedstate.go | 4 +- internal/selfcompose/composedstate_test.go | 15 +++--- internal/selfcompose/data_test.go | 18 +++---- internal/selfcompose/selfcompose.go | 25 +++++----- internal/selfcompose/selfcompose_test.go | 50 +++++++++---------- internal/takein/takein.go | 8 +-- 17 files changed, 143 insertions(+), 144 deletions(-) diff --git a/cmd/mesh-compose/main_test.go b/cmd/mesh-compose/main_test.go index 2ce1355..0b69063 100644 --- a/cmd/mesh-compose/main_test.go +++ b/cmd/mesh-compose/main_test.go @@ -282,7 +282,7 @@ func ownPackages(t *testing.T) map[string][]string { } // 1621/AC2 and AC4: the command imports, through every package of this repository it reaches, only the standard -// library, mesh-sdk, the contracts modules' pure packages (seats' types, clients' codecs), the delivery SDK's composer (the command's own request and verdict types), the private network's overlay package (mesh-wireguard's own pure package: names, graph and +// library, mesh-sdk, the contracts modules' pure packages (seats' types, clients' codecs), the delivery SDK's composer (the command's own request and verdict types), the builder's builds package (a build's value and its keys), the private network's overlay package (mesh-wireguard's own pure package: names, graph and // generator, no bus) and this repository's own packages, and none that reaches a bus, a store, a network service or // another process. func TestTheCommandImportsNoBusStoreNetworkOrProcess(t *testing.T) { @@ -294,7 +294,7 @@ func TestTheCommandImportsNoBusStoreNetworkOrProcess(t *testing.T) { for path, imports := range own { for _, imp := range imports { std := !strings.Contains(strings.SplitN(imp, "/", 2)[0], ".") - if !std && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-sdk/go") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/sdk/composer") && !strings.HasPrefix(imp, "github.com/novox/mesh-host/") { + if !std && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-sdk/go") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/sdk/composer") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds") && !strings.HasPrefix(imp, "github.com/novox/mesh-host/") { t.Errorf("%s imports %s, which is neither the standard library, mesh-sdk nor this repository", path, imp) } for _, f := range forbidden { diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index eda3474..f8fcfbc 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -14,6 +14,7 @@ import ( "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + builderbuilds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/buckets/bucketstest" engcomp "github.com/novox/mesh-host/internal/compositions" @@ -28,7 +29,6 @@ import ( nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/declaration" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/seatlist" "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go" @@ -416,7 +416,7 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { } placement, modules, manifests := kvOf(buckets.Placement), kvOf(buckets.Modules), kvOf(buckets.Manifests) resolved := kvOf(selfcompose.ResolvedBucket) - builds := kvOf(delivery.BuilderBuildsBucket) + builds := kvOf(builderbuilds.BuilderBuildsBucket) candidates := kvOf(buckets.PlacementCandidates) packages := kvOf(link.PackagesBucket) path := filepath.Join(b.dir, "web.conf") @@ -447,9 +447,9 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { rec2, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "it2", Capabilities: []string{"service-manager"}}}) pkey2, _ := meshmoduleassignerclient.LegacyKey("it2") hkey2, _ := nec.HaveNodeKey("it2") - buildKey, _ := delivery.BuilderBuildKey("web", fp) - done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest, - Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://web/files/blobs/" + fpDigest("f"), Digest: fpDigest("f")}}}) + buildKey, _ := builderbuilds.BuilderBuildKey("web", fp) + done, _ := builderbuilds.EncodeBuilderBuild(builderbuilds.BuilderBuild{State: builderbuilds.BuildDone, Manifest: digest, + Artifacts: []builderbuilds.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://web/files/blobs/" + fpDigest("f"), Digest: fpDigest("f")}}}) firstBuild := done if checkBuild { firstBuild = []byte(`{"v":1,"state":"no such state"}`) @@ -770,7 +770,7 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test } placement, modules, manifests := kvOf(buckets.Placement), kvOf(buckets.Modules), kvOf(buckets.Manifests) kvOf(selfcompose.ResolvedBucket) - builds := kvOf(delivery.BuilderBuildsBucket) + builds := kvOf(builderbuilds.BuilderBuildsBucket) 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. @@ -786,8 +786,8 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test if _, err := manifests.Put(mkey, mraw); err != nil { t.Fatal(err) } - bkey, _ := delivery.BuilderBuildKey("nats", fp) - done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest}) + bkey, _ := builderbuilds.BuilderBuildKey("nats", fp) + done, _ := builderbuilds.EncodeBuilderBuild(builderbuilds.BuilderBuild{State: builderbuilds.BuildDone, Manifest: digest}) if _, err := builds.Put(bkey, done); err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index 46b4065..bf5e482 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0 git.novox.be/novox/mesh-delivery/sdk v0.0.0-20261105000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-20261105000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-20261105000000-000000000000 diff --git a/internal/compose/frombus/answer_rows_test.go b/internal/compose/frombus/answer_rows_test.go index ebaaf31..261f490 100644 --- a/internal/compose/frombus/answer_rows_test.go +++ b/internal/compose/frombus/answer_rows_test.go @@ -9,6 +9,7 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" engcomp "github.com/novox/mesh-host/internal/compositions" "os" "reflect" @@ -18,7 +19,6 @@ import ( "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/resolution" ) @@ -49,7 +49,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd", "container-runtime"}}}, modules: map[string]meshcatalog.ModulesByModuleValue{"lemurs": {Manifest: meshcatalogclient.ManifestDigest(lemurs)}}, manifests: map[string][]byte{meshcatalogclient.ManifestDigest(lemurs): lemurs}, - builds: map[string]*delivery.BuilderBuild{}} + builds: map[string]*builds.BuilderBuild{}} for _, m := range []string{"web", "api", "spiegel"} { b.own = append(b.own, compose.OwnSecretRow{Node: "n1", Module: m, Name: "token", Origin: "made", SealedTo: "sk", Sealed: `"box"`}) } @@ -64,8 +64,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { mod := b.modules[m] mod.Fp2 = fp(string(rune('1' + i))) b.modules[m] = mod - b.builds[m+" "+mod.Fp2] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest, - Artifacts: []delivery.BuiltArtifact{{Name: "site", Kind: "image", Digest: "sha256:" + strings.Repeat("b", 64)}}} + b.builds[m+" "+mod.Fp2] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: mod.Manifest, + Artifacts: []builds.BuiltArtifact{{Name: "site", Kind: "image", Digest: "sha256:" + strings.Repeat("b", 64)}}} } return b } @@ -209,8 +209,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { sp.Fp2 = fp("3") bb.modules["spiegel"] = sp image := "registry.example:5100/spiegel/site@sha256:" + strings.Repeat("7", 64) - bb.builds["spiegel "+sp.Fp2] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: sp.Manifest, - Artifacts: []delivery.BuiltArtifact{{Name: "site", Kind: "upstream", Reference: image, Digest: "sha256:" + strings.Repeat("7", 64)}}} + bb.builds["spiegel "+sp.Fp2] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: sp.Manifest, + Artifacts: []builds.BuiltArtifact{{Name: "site", Kind: "upstream", Reference: image, Digest: "sha256:" + strings.Repeat("7", 64)}}} read, err := ComposingInput(ctx, bb, Mesh{}, Scope{Node: "n1", InOwn: []string{"spiegel"}}, map[string]string{"lemurs": fp("e"), "spiegel": sp.Fp2}) raw, _ := json.Marshal(read.Catalogue["spiegel"]) @@ -565,7 +565,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { siteRuns.Resources[1] = json.RawMessage(`{"id":"site.server","module":"site","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["8080"]}`) siteRuns.FP = fp("7") b.manifests[meshcatalogclient.ManifestDigest(site)] = site - b.builds["site "+fp("7")] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(site)} + b.builds["site "+fp("7")] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(site)} got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []engcomp.Composed{siteRuns}}, fps) if err != nil || held["site"] == "" || held["nft"] != "" { t.Errorf("site moved to 9090 in its new manifest: %v, held %v; want site held alone", err, held) diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index 899e057..9e7b302 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -40,6 +40,7 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "net/netip" @@ -78,9 +79,9 @@ type Reader interface { // and each it grants as a provider, with the copy sealed to it (the vault's `own` with the keys, ADR 0550 §5). A key // the vault holds no value for is left out; the composition then stands in for it, and Compose refuses it. Credentials(ctx context.Context, node string, asConsumer, asProvider []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) - // Build is a module's build at a fingerprint, the builder's key (delivery.BuilderBuildsBucket, BuilderBuildKey); nil + // Build is a module's build at a fingerprint, the builder's key (builds.BuilderBuildsBucket, BuilderBuildKey); nil // when the builder holds none. The composing node composes each of its modules from its build (issue 1126). - Build(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) + Build(ctx context.Context, module, fp string) (*builds.BuilderBuild, error) // Archive is the archive at url, a kept reference routed to the artifact store as the composing node reaches it // (catalogue.Routed), checked against digest: a moved module's templates archive, read back for its texts as the // controller's take-in reads it (takein.Built). @@ -501,7 +502,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) { // finish is input from its base on: the composing node's modules from their builds (build reads one), its own secrets. // in is the base's input, or a copy of it (base.cloned) where one base is finished more than once. -func (b base) finish(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error), +func (b base) finish(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*builds.BuilderBuild, error), mesh Mesh, s Scope, in compose.Input, refuse func(module string, err error), wanted map[string]string) (compose.Input, error) { var err error parse := parser(mesh) @@ -777,7 +778,7 @@ func ranOf(ctx context.Context, r Reader, mesh Mesh, running []engcomp.Composed, continue } b, err := r.Build(ctx, module, c.FP) - if err != nil || b == nil || b.State != delivery.BuildDone || b.Manifest == "" { + if err != nil || b == nil || b.State != builds.BuildDone || b.Manifest == "" { return catalogue.Manifest{}, false } raw, err := r.Manifest(ctx, b.Manifest) @@ -795,7 +796,7 @@ func ranOf(ctx context.Context, r Reader, mesh Mesh, running []engcomp.Composed, // module it keeps at another build (Scope.KeptAt, into its node's Kept), read from the builder's done key and resolved // 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), +func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*builds.BuilderBuild, error), in *compose.Input, s Scope, p meshmoduleassigner.Placement, modules map[string]meshcatalog.ModulesByModuleValue, 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 { @@ -835,7 +836,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m } return r.Archive(ctx, catalogue.Routed(reference, address), digest) } - built := func(module, fp string, b *delivery.BuilderBuild) (catalogue.Manifest, error) { + built := func(module, fp string, b *builds.BuilderBuild) (catalogue.Manifest, error) { if b.Manifest == "" { return catalogue.Manifest{}, fmt.Errorf("frombus: the build of %s at %s names no manifest", module, fp) } @@ -920,7 +921,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m stub(pm.Module, err) continue } - if b == nil || b.State != delivery.BuildDone { + if b == nil || b.State != builds.BuildDone { if refuse != nil && runs[pm.Module] { // A module that runs is never composed from the indexer's manifest, which names no build's artifacts (no // runtime's bundle): it is held, keeping what runs, until its build lands (issue 774, 2026-10-30 19:30: @@ -950,7 +951,7 @@ func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, m switch { case err != nil: err = fmt.Errorf("frombus: the build of %s at %s: %w", module, fp, err) - case b == nil || b.State != delivery.BuildDone: + case b == nil || b.State != builds.BuildDone: err = fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build", module, fp) } var resolved catalogue.Manifest diff --git a/internal/compose/frombus/frombus_test.go b/internal/compose/frombus/frombus_test.go index 33a75fe..c0a4cae 100644 --- a/internal/compose/frombus/frombus_test.go +++ b/internal/compose/frombus/frombus_test.go @@ -17,6 +17,7 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "github.com/novox/mesh-host/internal/compositions" "os" "reflect" @@ -27,7 +28,6 @@ import ( "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose" ) @@ -48,7 +48,7 @@ type bus struct { asked []compose.CredentialKey // builds are the builder's keys, by " "; archives the artifact store's blobs, by digest, and fetched // every URL asked; served another node's values by ".", and servedRead every ask with its keys. - builds map[string]*delivery.BuilderBuild + builds map[string]*builds.BuilderBuild archives map[string][]byte fetched []string served map[string]map[string]any @@ -58,7 +58,7 @@ type bus struct { held bool } -func (b *bus) Build(_ context.Context, module, fp string) (*delivery.BuilderBuild, error) { +func (b *bus) Build(_ context.Context, module, fp string) (*builds.BuilderBuild, error) { b.buildsRead = append(b.buildsRead, module) return b.builds[module+" "+fp], nil } @@ -560,8 +560,8 @@ func TestInput(t *testing.T) { kept.manifests["old"] = oldXterm oldFP := "src2:" + strings.Repeat("9", 64) toolsRef := "artifact-store://xterm/tools/blobs/sha256:" + strings.Repeat("1", 64) - kept.builds = map[string]*delivery.BuilderBuild{"xterm " + oldFP: {State: delivery.BuildDone, Manifest: "old", - Artifacts: []delivery.BuiltArtifact{{Name: "tools", Kind: "bundle", Reference: toolsRef, Digest: "sha256:" + strings.Repeat("1", 64)}}}} + kept.builds = map[string]*builds.BuilderBuild{"xterm " + oldFP: {State: builds.BuildDone, Manifest: "old", + Artifacts: []builds.BuiltArtifact{{Name: "tools", Kind: "bundle", Reference: toolsRef, Digest: "sha256:" + strings.Repeat("1", 64)}}}} parsedOld, err := catalogue.Decode(oldXterm) if err != nil { t.Fatal(err) @@ -617,9 +617,9 @@ func TestInput(t *testing.T) { digestOf := func(c string) string { return "sha256:" + strings.Repeat(c, 64) } movedFps := map[string]string{"restic": "src2:" + strings.Repeat("3", 64), "mesh-provisioner-client": "src2:" + strings.Repeat("4", 64), "store": "src2:" + strings.Repeat("5", 64)} - resticArtifacts := []delivery.BuiltArtifact{{Name: "tools", Kind: "bundle", Reference: "artifact-store://restic/tools/blobs/" + digestOf("6"), Digest: digestOf("6")}, + resticArtifacts := []builds.BuiltArtifact{{Name: "tools", Kind: "bundle", Reference: "artifact-store://restic/tools/blobs/" + digestOf("6"), Digest: digestOf("6")}, {Name: "files", Kind: "archive", Reference: "artifact-store://restic/files/blobs/" + archiveDigest, Digest: archiveDigest}} - clientArtifacts := []delivery.BuiltArtifact{{Name: "client", Kind: "bundle", Reference: "artifact-store://mesh-provisioner-client/client/blobs/" + digestOf("7"), Digest: digestOf("7")}} + clientArtifacts := []builds.BuiltArtifact{{Name: "client", Kind: "bundle", Reference: "artifact-store://mesh-provisioner-client/client/blobs/" + digestOf("7"), Digest: digestOf("7")}} movedMesh := func(built bool) *bus { b := one() p := b.placements["n1"] @@ -638,10 +638,10 @@ func TestInput(t *testing.T) { // The vault's own answer: the client's own secret; restic's is the client's to ask, so own leaves it out. b.own = []compose.OwnSecretRow{{Node: "n1", Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}} if built { - b.builds = map[string]*delivery.BuilderBuild{ - "restic " + movedFps["restic"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(resticRaw), Artifacts: resticArtifacts}, - "mesh-provisioner-client " + movedFps["mesh-provisioner-client"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(clientRaw), Artifacts: clientArtifacts}, - "store " + movedFps["store"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(store)}, + b.builds = map[string]*builds.BuilderBuild{ + "restic " + movedFps["restic"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(resticRaw), Artifacts: resticArtifacts}, + "mesh-provisioner-client " + movedFps["mesh-provisioner-client"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(clientRaw), Artifacts: clientArtifacts}, + "store " + movedFps["store"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(store)}, } b.archives = map[string][]byte{archiveDigest: packed.Bytes()} } @@ -674,7 +674,7 @@ func TestInput(t *testing.T) { } // A done key an artifact of which names no reference is waited for, in the controller's words (ADR 0498 §8). unreferenced := movedMesh(true) - unreferenced.builds["restic "+movedFps["restic"]].Artifacts = []delivery.BuiltArtifact{{Name: "tools", Kind: "bundle", Digest: digestOf("6")}, resticArtifacts[1]} + unreferenced.builds["restic "+movedFps["restic"]].Artifacts = []builds.BuiltArtifact{{Name: "tools", Kind: "bundle", Digest: digestOf("6")}, resticArtifacts[1]} if _, _, held, err := Compose(ctx, unreferenced, Mesh{}, Scope{Node: "n1"}, movedFps); err != nil || !strings.Contains(held["restic"], "the build of restic is done and waits for the reference of tools") { t.Errorf("a done key with an unreferenced artifact: %v %v, want restic held alone", err, held) } @@ -866,8 +866,8 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates { } for key, build := range b.builds { module, fp, _ := strings.Cut(key, " ") - k, _ := delivery.BuilderBuildKey(module, fp) - raw, err := delivery.EncodeBuilderBuild(*build) + k, _ := builds.BuilderBuildKey(module, fp) + raw, err := builds.EncodeBuilderBuild(*build) put(BuildsRead, k, raw, err) } for nm, values := range b.values { diff --git a/internal/compose/frombus/frombustest/frombustest.go b/internal/compose/frombus/frombustest/frombustest.go index e0016d0..70a7a56 100644 --- a/internal/compose/frombus/frombustest/frombustest.go +++ b/internal/compose/frombus/frombustest/frombustest.go @@ -16,11 +16,10 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" - "strings" - - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/frombus" + "strings" ) //go:embed testdata @@ -34,9 +33,9 @@ type Bus struct { Manifests map[string][]byte Value map[string]map[string]any // by node.module Own []compose.OwnSecretRow - Builds map[string]*delivery.BuilderBuild // by " " - Archives map[string][]byte // by digest - Serves map[string]map[string]any // another node's values, by node.module + Builds map[string]*builds.BuilderBuild // by " " + Archives map[string][]byte // by digest + Serves map[string]map[string]any // another node's values, by node.module Held bool } @@ -81,7 +80,7 @@ func (b *Bus) Credentials(_ context.Context, _ string, asConsumer, asProvider [] return out, issued, nil } -func (b *Bus) Build(_ context.Context, module, fp string) (*delivery.BuilderBuild, error) { +func (b *Bus) Build(_ context.Context, module, fp string) (*builds.BuilderBuild, error) { return b.Builds[module+" "+fp], nil } @@ -164,15 +163,15 @@ func Provisioned() (*Bus, error) { SealingKey: "sk", Contract: compose.HandedContract, Capabilities: []string{"systemd", "package-manager"}}}, Module: map[string]meshcatalog.ModulesByModuleValue{}, Manifests: map[string][]byte{}, Own: []compose.OwnSecretRow{{Node: Node, Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}, - Builds: map[string]*delivery.BuilderBuild{ - "restic " + fps["restic"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(resticRaw), Artifacts: []delivery.BuiltArtifact{ + Builds: map[string]*builds.BuilderBuild{ + "restic " + fps["restic"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(resticRaw), Artifacts: []builds.BuiltArtifact{ {Name: "tools", Kind: "bundle", Reference: "artifact-store://restic/tools/blobs/" + digestOf("6"), Digest: digestOf("6")}, {Name: "files", Kind: "archive", Reference: "artifact-store://restic/files/blobs/" + archive, Digest: archive}}}, - "mesh-provisioner-client " + fps["mesh-provisioner-client"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(clientRaw), - Artifacts: []delivery.BuiltArtifact{{Name: "client", Kind: "bundle", + "mesh-provisioner-client " + fps["mesh-provisioner-client"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(clientRaw), + Artifacts: []builds.BuiltArtifact{{Name: "client", Kind: "bundle", Reference: "artifact-store://mesh-provisioner-client/client/blobs/" + digestOf("7"), Digest: digestOf("7")}}}, - "store " + fps["store"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(store)}, - "postgres " + fps["postgres"]: {State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest(postgres)}, + "store " + fps["store"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(store)}, + "postgres " + fps["postgres"]: {State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest(postgres)}, }, Archives: map[string][]byte{archive: packed.Bytes()}, Held: true, diff --git a/internal/compose/frombus/frombustest/frombustest_test.go b/internal/compose/frombus/frombustest/frombustest_test.go index e0ac451..3593057 100644 --- a/internal/compose/frombus/frombustest/frombustest_test.go +++ b/internal/compose/frombus/frombustest/frombustest_test.go @@ -9,6 +9,7 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" engcomp "github.com/novox/mesh-host/internal/compositions" "reflect" "slices" @@ -17,7 +18,6 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose/frombus" "github.com/novox/mesh-host/internal/compose/fromruntime" ) @@ -84,8 +84,8 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { if row.built != "" { builtDigest := meshcatalogclient.ManifestDigest([]byte(row.built)) b.Manifests[builtDigest] = []byte(row.built) - b.Builds["backup "+fp] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: builtDigest, - Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://backup/files/blobs/x", Digest: "sha256:x"}}} + b.Builds["backup "+fp] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: builtDigest, + Artifacts: []builds.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://backup/files/blobs/x", Digest: "sha256:x"}}} } if row.dataOnly { items, _, iheld, err := frombus.DataItemsHeld(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node}) @@ -274,10 +274,10 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &built want = "" } else if row.filledHeld { - bb.Builds["restic "+fps["restic"]] = &delivery.BuilderBuild{State: delivery.BuildBuilding} + bb.Builds["restic "+fps["restic"]] = &builds.BuilderBuild{State: builds.BuildBuilding} } else if row.placed { // held: its build not done while it runs - bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &delivery.BuilderBuild{State: delivery.BuildBuilding} + bb.Builds["mesh-provisioner-client "+fps["mesh-provisioner-client"]] = &builds.BuilderBuild{State: builds.BuildBuilding} } else { // taken out of the node's placement while what runs still names it: the account goes with it, never kept stale p := bb.Placement[Node] @@ -334,8 +334,8 @@ func statesOf(t *testing.T, b *Bus) frombus.States { } for at, build := range b.Builds { module, fp, _ := strings.Cut(at, " ") - key, _ := delivery.BuilderBuildKey(module, fp) - raw, err := delivery.EncodeBuilderBuild(*build) + key, _ := builds.BuilderBuildKey(module, fp) + raw, err := builds.EncodeBuilderBuild(*build) put(frombus.BuildsRead, key, raw, err) } return s diff --git a/internal/compose/frombus/held_rows_test.go b/internal/compose/frombus/held_rows_test.go index a60ec47..4936bb1 100644 --- a/internal/compose/frombus/held_rows_test.go +++ b/internal/compose/frombus/held_rows_test.go @@ -9,13 +9,12 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "github.com/novox/mesh-host/internal/compose" + "github.com/novox/mesh-host/internal/compose/resolution" engcomp "github.com/novox/mesh-host/internal/compositions" "strings" "testing" - - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/novox/mesh-host/internal/compose" - "github.com/novox/mesh-host/internal/compose/resolution" ) // aNode is a bus with n1 the hub, each of manifests placed on it and held at the indexer, in the order given. @@ -23,7 +22,7 @@ func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) { t.Helper() p := meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}} b := &bus{records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd"}}}, - modules: map[string]meshcatalog.ModulesByModuleValue{}, manifests: map[string][]byte{}, builds: map[string]*delivery.BuilderBuild{}} + modules: map[string]meshcatalog.ModulesByModuleValue{}, manifests: map[string][]byte{}, builds: map[string]*builds.BuilderBuild{}} fps := map[string]string{} for i, raw := range manifests { module := strings.SplitN(strings.TrimPrefix(raw, `{"module":"`), `"`, 2)[0] @@ -148,12 +147,12 @@ func aRunningModuleWaitsForItsBuild(t *testing.T) { mod := b.modules["node-tools"] mod.Fp2 = next b.modules["node-tools"] = mod - b.builds["node-tools "+old] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest, - Artifacts: []delivery.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("1"), Digest: dg("1")}}} - b.builds["node-tools "+next] = &delivery.BuilderBuild{State: "building", Manifest: mod.Manifest} + b.builds["node-tools "+old] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: mod.Manifest, + Artifacts: []builds.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("1"), Digest: dg("1")}}} + b.builds["node-tools "+next] = &builds.BuilderBuild{State: "building", Manifest: mod.Manifest} if row.done { - b.builds["node-tools "+next] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest, - Artifacts: []delivery.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("2"), Digest: dg("2")}}} + b.builds["node-tools "+next] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: mod.Manifest, + Artifacts: []builds.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("2"), Digest: dg("2")}}} } fps["node-tools"] = row.want out, _, held, err := Compose(context.Background(), b, Mesh{}, Scope{Node: "n1", Running: running}, fps) @@ -258,10 +257,10 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) { mod := b.modules[m] mod.Fp2 = fps[m] b.modules[m] = mod - b.builds[m+" "+fps[m]] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest, - Artifacts: []delivery.BuiltArtifact{{Name: name, Kind: "bundle", Reference: "artifact-store://" + m + "/" + name + "/blobs/" + dg, Digest: dg}}} + b.builds[m+" "+fps[m]] = &builds.BuilderBuild{State: builds.BuildDone, Manifest: mod.Manifest, + Artifacts: []builds.BuiltArtifact{{Name: name, Kind: "bundle", Reference: "artifact-store://" + m + "/" + name + "/blobs/" + dg, Digest: dg}}} if m == "node-tools" && row.building { - b.builds[m+" "+fps[m]] = &delivery.BuilderBuild{State: "building", Manifest: mod.Manifest} + b.builds[m+" "+fps[m]] = &builds.BuilderBuild{State: "building", Manifest: mod.Manifest} } } out, _, held, err := Compose(context.Background(), b, Mesh{}, Scope{Node: "n1"}, fps) diff --git a/internal/compose/frombus/states.go b/internal/compose/frombus/states.go index 501005a..2ae2480 100644 --- a/internal/compose/frombus/states.go +++ b/internal/compose/frombus/states.go @@ -18,12 +18,11 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" - "strings" - "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/resolution" + "strings" + "time" ) // The states a module names in its manifest's `reads` to compose through StatesReader. @@ -161,8 +160,8 @@ func (statesReader) Credentials(context.Context, string, []compose.CredentialKey return nil, nil, nil } -func (r statesReader) Build(_ context.Context, module, fp string) (*delivery.BuilderBuild, error) { - key, err := delivery.BuilderBuildKey(module, fp) +func (r statesReader) Build(_ context.Context, module, fp string) (*builds.BuilderBuild, error) { + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return nil, err } @@ -170,7 +169,7 @@ func (r statesReader) Build(_ context.Context, module, fp string) (*delivery.Bui if err != nil || !found { return nil, err } - b, err := delivery.DecodeBuilderBuild(v) + b, err := builds.DecodeBuilderBuild(v) if err != nil { return nil, err } diff --git a/internal/compose/frombus/twice.go b/internal/compose/frombus/twice.go index e9738dd..4411c51 100644 --- a/internal/compose/frombus/twice.go +++ b/internal/compose/frombus/twice.go @@ -12,11 +12,11 @@ import ( "encoding/hex" "encoding/json" "fmt" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "sync" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // HeldPlans is one scope's plans, as PlansHeld answers them: err a refusal that is no single module's. @@ -75,13 +75,13 @@ func PlansHeldTwice(ctx context.Context, r Reader, mesh Mesh, s Scope) (at, newe // onceBuilds is r's Build, each module's build at a fingerprint read once: both scopes of one composition read the // builds of the same moment. -func onceBuilds(r Reader) func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) { +func onceBuilds(r Reader) func(ctx context.Context, module, fp string) (*builds.BuilderBuild, error) { type answer struct { - b *delivery.BuilderBuild + b *builds.BuilderBuild err error } read := map[string]answer{} - return func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) { + return func(ctx context.Context, module, fp string) (*builds.BuilderBuild, error) { key := module + " " + fp if a, ok := read[key]; ok { return a.b, a.err @@ -101,7 +101,7 @@ type takenInKeeper interface { // takenInKey is what a take-in is made from: the artifact store's address and the build's record (its manifest's digest, its artifacts by digest), read // whole; an archive is checked against its digest on every read, so the same record takes in to the same bytes. -func takenInKey(module, fp, address string, b delivery.BuilderBuild) (string, bool) { +func takenInKey(module, fp, address string, b builds.BuilderBuild) (string, bool) { raw, err := json.Marshal(b) if err != nil { return "", false diff --git a/internal/selfcompose/composedstate.go b/internal/selfcompose/composedstate.go index 5693f90..5c4dab7 100644 --- a/internal/selfcompose/composedstate.go +++ b/internal/selfcompose/composedstate.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "sort" @@ -11,7 +12,6 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/compose/frombus" @@ -93,7 +93,7 @@ func (c *Composer) newestFPs(ctx context.Context, at *published) map[string]stri fp2 := c.modules[module].Fp2 c.mu.Unlock() if fp2 != "" { - if b, err := r.Build(ctx, module, fp2); err == nil && b != nil && b.State == delivery.BuildDone { + if b, err := r.Build(ctx, module, fp2); err == nil && b != nil && b.State == builds.BuildDone { out[module] = fp2 continue } diff --git a/internal/selfcompose/composedstate_test.go b/internal/selfcompose/composedstate_test.go index 4cf6e82..9f57406 100644 --- a/internal/selfcompose/composedstate_test.go +++ b/internal/selfcompose/composedstate_test.go @@ -9,6 +9,7 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/buckets/bucketstest" engcomp "github.com/novox/mesh-host/internal/compositions" @@ -18,8 +19,6 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/novox/mesh-host/internal/agent" ) @@ -105,7 +104,7 @@ func TestTheNodePublishesItsCompositions(t *testing.T) { b.put(t, buckets.Modules, "oldie", omod) b.put(t, buckets.Wants, "want.n1.lemurs", want) b.put(t, buckets.Wants, "want.n1.node", nodeWant) - for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} { b.send(bucket, agent.Update{Caught: true}) } for u := next(t, out); !u.Caught; u = next(t, out) { @@ -147,17 +146,17 @@ func TestTheNodePublishesItsCompositions(t *testing.T) { t.Errorf("a pass that changed nothing wrote %v", got[2:]) } // oldie's trunk build is done: its key at that build is written once, though no want names it. - build, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: odigest}) - bkey, _ := delivery.BuilderBuildKey("oldie", ofp) + build, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: odigest}) + bkey, _ := builds.BuilderBuildKey("oldie", ofp) b.mu.Lock() - b.kv[delivery.BuilderBuildsBucket+"/"+bkey] = build + b.kv[builds.BuilderBuildsBucket+"/"+bkey] = build b.mu.Unlock() - b.put(t, delivery.BuilderBuildsBucket, bkey, build) + b.put(t, builds.BuilderBuildsBucket, bkey, build) got := waitFor("oldie's done build", func(got []string) bool { return len(got) >= 3 }) if got[2] != oldieKey { t.Errorf("the done build of oldie wrote %v, want %s", got[2:], oldieKey) } - b.put(t, delivery.BuilderBuildsBucket, bkey, build) + b.put(t, builds.BuilderBuildsBucket, bkey, build) time.Sleep(100 * time.Millisecond) if got := state.written(); len(got) != 3 { t.Errorf("the same build landing again wrote %v", got[3:]) diff --git a/internal/selfcompose/data_test.go b/internal/selfcompose/data_test.go index d2273d3..04bca2b 100644 --- a/internal/selfcompose/data_test.go +++ b/internal/selfcompose/data_test.go @@ -6,6 +6,7 @@ import ( "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "github.com/novox/mesh-host/internal/buckets" engcomp "github.com/novox/mesh-host/internal/compositions" "slices" @@ -15,7 +16,6 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose/frombus" "github.com/novox/mesh-host/internal/agent" @@ -235,15 +235,15 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { // The items are worked out from what the pass read: a done build is read over the bus once, not again for the data // items after the pass (a module in a running build is read again, since it may land). b := newFakeBus() - done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest([]byte(lemurs))}) - running, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: "building"}) + done, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest([]byte(lemurs))}) + running, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: "building"}) fp := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64) - doneKey, _ := delivery.BuilderBuildKey("web", fp) - runningKey, _ := delivery.BuilderBuildKey("pg", fp) - b.kv[delivery.BuilderBuildsBucket+"/"+doneKey], b.kv[delivery.BuilderBuildsBucket+"/"+runningKey] = done, running + doneKey, _ := builds.BuilderBuildKey("web", fp) + runningKey, _ := builds.BuilderBuildKey("pg", fp) + b.kv[builds.BuilderBuildsBucket+"/"+doneKey], b.kv[builds.BuilderBuildsBucket+"/"+runningKey] = done, running r := reader{c: New(b.env())} for range 3 { - if got, err := r.Build(context.Background(), "web", fp); err != nil || got.State != delivery.BuildDone { + if got, err := r.Build(context.Background(), "web", fp); err != nil || got.State != builds.BuildDone { t.Fatalf("the done build: %+v, %v", got, err) } if got, err := r.Build(context.Background(), "pg", fp); err != nil || got.State != "building" { @@ -258,9 +258,9 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { t.Errorf("the builds were read %v times over the bus; want the done one once, the running one each time", reads) } // A done key the builder claims again (a gone step, issue 1937) and lands done again is read again, not served kept. - again, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: meshcatalogclient.ManifestDigest([]byte("{}"))}) + again, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: meshcatalogclient.ManifestDigest([]byte("{}"))}) b.mu.Lock() - b.kv[delivery.BuilderBuildsBucket+"/"+doneKey] = again + b.kv[builds.BuilderBuildsBucket+"/"+doneKey] = again b.mu.Unlock() r.c.takeBuildDone(agent.Update{Key: doneKey, Value: running}) if got, err := r.Build(context.Background(), "web", fp); err != nil || got.Manifest != meshcatalogclient.ManifestDigest([]byte("{}")) { diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index ce02dfd..cee73ed 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -26,6 +26,7 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/compositions" engcomp "github.com/novox/mesh-host/internal/compositions" @@ -150,7 +151,7 @@ type Composer struct { archives map[string][]byte // templates archives by digest, read once // builds are the builder's done builds by key, read once: a done build at a fingerprint never changes, so a pass and // the data items after it (publishData) do not read each placed module's build again over the bus (ADR 0577). - builds map[string]delivery.BuilderBuild + builds map[string]builds.BuilderBuild handed map[string]string // composed key to the digest handed // sealedOwn is each own secret's sealed value as first handed, by module/name and generation: a value sealed again // is other bytes, so the one first had is kept while the generation stands, and the composition stays the same. @@ -307,7 +308,7 @@ func New(env Env) *Composer { } 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]meshcatalog.ModulesByModuleValue{}, 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{}, + manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]builds.BuilderBuild{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{}, pkgs: map[string]compositions.Composed{}, asked: make(chan chan Pass, 8)} } @@ -371,7 +372,7 @@ func (c *Composer) inputs() []input { {ResolvedBucket, node + ".>", (*Composer).takeValue, nil}, // The builder's builds: a pass that held a module for a build not yet done is made again when it lands (issue // 774, 2026-10-30 19:30: node-tools' build landed and ace, g14 and shanks stayed refused until another input moved). - {delivery.BuilderBuildsBucket, ">", nil, (*Composer).takeBuildDone}, + {builds.BuilderBuildsBucket, ">", nil, (*Composer).takeBuildDone}, } } @@ -386,7 +387,7 @@ func (c *Composer) takeBuildDone(u agent.Update) bool { if u.Deleted { return false } - module, fp, err := delivery.ParseBuilderBuildKey(u.Key) + module, fp, err := builds.ParseBuilderBuildKey(u.Key) if err != nil { return false } @@ -403,8 +404,8 @@ func (c *Composer) takeBuildDone(u agent.Update) bool { if !(wanted && w.Fp == fp) && !((wanted || placedHere) && m.Fp2 == fp) { return false } - b, err := delivery.DecodeBuilderBuild(u.Value) - return err == nil && b.State == delivery.BuildDone + b, err := builds.DecodeBuilderBuild(u.Value) + return err == nil && b.State == builds.BuildDone } // Watch starts the composer and answers the compositions it makes: each as a key of `composed-` would be, then a @@ -1429,8 +1430,8 @@ func (r reader) Manifest(ctx context.Context, digest string) ([]byte, error) { return []byte(m.Manifest), nil } -func (r reader) Build(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error) { - key, err := delivery.BuilderBuildKey(module, fp) +func (r reader) Build(ctx context.Context, module, fp string) (*builds.BuilderBuild, error) { + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return nil, err } @@ -1440,16 +1441,16 @@ func (r reader) Build(ctx context.Context, module, fp string) (*delivery.Builder if ok { return &done, nil } - defer r.c.waiting("the build of " + module + " at " + fp + " (" + delivery.BuilderBuildsBucket + ")")() - raw, err := r.c.get(ctx, delivery.BuilderBuildsBucket, key) + defer r.c.waiting("the build of " + module + " at " + fp + " (" + builds.BuilderBuildsBucket + ")")() + raw, err := r.c.get(ctx, builds.BuilderBuildsBucket, key) if err != nil || raw == nil { return nil, err } - b, err := delivery.DecodeBuilderBuild(raw) + b, err := builds.DecodeBuilderBuild(raw) if err != nil { return nil, fmt.Errorf("the builder's %s: %w", key, err) } - if b.State == delivery.BuildDone { + if b.State == builds.BuildDone { r.c.keepRead(func(k *Composer) { k.builds[key] = b }) } return &b, nil diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index dcbaa2c..ecce6b7 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -15,6 +15,7 @@ import ( "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/buckets/bucketstest" "github.com/novox/mesh-host/internal/compositions" @@ -29,7 +30,6 @@ import ( "time" "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/frombus" @@ -209,7 +209,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { b.put(t, buckets.Modules, "oldie", omod) b.put(t, buckets.Wants, "want.n1.lemurs", want) b.put(t, buckets.Wants, "want.n1.node", nodeWant) - for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} { b.send(bucket, agent.Update{Caught: true}) } got := map[string]bool{} @@ -570,22 +570,22 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { pass bool }{ {"lemurs's wanted build, not done", "lemurs", fp, "building", false}, - {"lemurs's wanted build lands", "lemurs", fp, delivery.BuildDone, true}, - {"another build of lemurs", "lemurs", meshcatalogclient.SourcePrefix2 + strings.Repeat("e", 64), delivery.BuildDone, false}, - {"a placed module no want names, at its trunk's build", "oldie", meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64), delivery.BuildDone, true}, - {"a module neither wanted nor placed here", "ghost", meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64), delivery.BuildDone, false}, + {"lemurs's wanted build lands", "lemurs", fp, builds.BuildDone, true}, + {"another build of lemurs", "lemurs", meshcatalogclient.SourcePrefix2 + strings.Repeat("e", 64), builds.BuildDone, false}, + {"a placed module no want names, at its trunk's build", "oldie", meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64), builds.BuildDone, true}, + {"a module neither wanted nor placed here", "ghost", meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64), builds.BuildDone, false}, } { before := passes() - key, _ := delivery.BuilderBuildKey(row.module, row.fp) - build := delivery.BuilderBuild{State: row.state} - if row.state == delivery.BuildDone { + key, _ := builds.BuilderBuildKey(row.module, row.fp) + build := builds.BuilderBuild{State: row.state} + if row.state == builds.BuildDone { build.Manifest = digest } - raw, err := delivery.EncodeBuilderBuild(build) + raw, err := builds.EncodeBuilderBuild(build) if err != nil { t.Fatal(err) } - b.put(t, delivery.BuilderBuildsBucket, key, raw) + b.put(t, builds.BuilderBuildsBucket, key, raw) quiet(t, out) if got := passes() > before; got != row.pass { t.Errorf("%s: a pass made %v, want %v", row.name, got, row.pass) @@ -700,15 +700,15 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { Modules: []meshmoduleassigner.PlacedModule{{Module: "tool"}}}) tmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: next2, Manifest: tdigest, Repository: "novox/t", Commit: "c"}) twant, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: next2, Self: true}) - building, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: "building"}) - bkey, _ := delivery.BuilderBuildKey("tool", next2) - tb.kv[delivery.BuilderBuildsBucket+"/"+bkey] = building + building, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: "building"}) + bkey, _ := builds.BuilderBuildKey("tool", next2) + tb.kv[builds.BuilderBuildsBucket+"/"+bkey] = building tb.put(t, buckets.Placement, "placement.n1", tplace) tb.put(t, buckets.Have, "n1.node", have) tb.put(t, buckets.Modules, "tool", tmod) tb.put(t, buckets.Wants, "want.n1.tool", twant) for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, - ResolvedBucket, delivery.BuilderBuildsBucket} { + ResolvedBucket, builds.BuilderBuildsBucket} { tb.send(bucket, agent.Update{Caught: true}) } toolKey, _ := compositions.Key("n1", "tool", next2) @@ -722,13 +722,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Errorf("the node was told %q, want tool held for its build not yet done", held) } hmu.Unlock() - done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: tdigest, - Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://tool/files/blobs/sha256:" + strings.Repeat("9", 64), + done, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: tdigest, + Artifacts: []builds.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://tool/files/blobs/sha256:" + strings.Repeat("9", 64), Digest: "sha256:" + strings.Repeat("9", 64)}}}) tb.mu.Lock() - tb.kv[delivery.BuilderBuildsBucket+"/"+bkey] = done + tb.kv[builds.BuilderBuildsBucket+"/"+bkey] = done tb.mu.Unlock() - tb.put(t, delivery.BuilderBuildsBucket, bkey, done) + tb.put(t, builds.BuilderBuildsBucket, bkey, done) if u := next(t, tout); u.Key != toolKey || u.Deleted { t.Errorf("the build landed and the node handed %s, want tool at its new build", u.Key) } @@ -1370,7 +1370,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { 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, buckets.Modules, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{buckets.PlacementCandidates, buckets.Placement, buckets.Have, buckets.Modules, builds.BuilderBuildsBucket} { nb.send(bucket, agent.Update{Caught: true}) } select { @@ -1461,10 +1461,10 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { // Issue 1126, the seed's refusals: what a node composes from its builds and reads of a provider elsewhere. A build // is the builder's key; a templates archive is fetched once per digest and refused when it is not that digest; another // node's served key is read alone, as the node's grant allows it (buscompose Principal.ServedReads). - built, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest}) - buildKey, _ := delivery.BuilderBuildKey("lemurs", fp) + built, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: digest}) + buildKey, _ := builds.BuilderBuildKey("lemurs", fp) b.mu.Lock() - b.kv[delivery.BuilderBuildsBucket+"/"+buildKey] = built + b.kv[builds.BuilderBuildsBucket+"/"+buildKey] = built b.kv[ResolvedBucket+"/n2.mailer.domain"] = []byte(`{"value":"example.org","source":"module"}`) b.reads = nil b.mu.Unlock() @@ -1691,7 +1691,7 @@ func (r fixedReader) OwnSecrets(context.Context, string) ([]compose.OwnSecretRow func (r fixedReader) Credentials(context.Context, string, []compose.CredentialKey, []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) { return nil, nil, nil } -func (r fixedReader) Build(context.Context, string, string) (*delivery.BuilderBuild, error) { +func (r fixedReader) Build(context.Context, string, string) (*builds.BuilderBuild, error) { return nil, nil } func (r fixedReader) Archive(_ context.Context, url, _ string) ([]byte, error) { @@ -1800,7 +1800,7 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) { b.put(t, buckets.Modules, "lemurs", mod) b.put(t, buckets.Wants, "want.n1.lemurs", want) b.put(t, buckets.Wants, "want.n1.node", nodeWant) - for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} { b.send(bucket, agent.Update{Caught: true}) } handed := map[string]bool{} diff --git a/internal/takein/takein.go b/internal/takein/takein.go index 657ab45..a47bb27 100644 --- a/internal/takein/takein.go +++ b/internal/takein/takein.go @@ -18,11 +18,11 @@ import ( "context" "errors" "fmt" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "io" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // Archive reads the archive a recorded reference names (catalogue.Recorded: the artifact store's address left out), @@ -33,8 +33,8 @@ type Archive func(ctx context.Context, reference, digest string) ([]byte, error) // and every artifact must name its reference. // A templated module's templates archive is read through archive; a module without templates reads none, and archive // may then be nil. -func Manifest(ctx context.Context, m catalogue.Manifest, b delivery.BuilderBuild, archive Archive) (catalogue.Manifest, error) { - if b.State != delivery.BuildDone { +func Manifest(ctx context.Context, m catalogue.Manifest, b builds.BuilderBuild, archive Archive) (catalogue.Manifest, error) { + if b.State != builds.BuildDone { return catalogue.Manifest{}, fmt.Errorf("takein: the build of %s is %s, not done: nothing to resolve it with", m.Module, b.State) } // **A done key whose artifact names no reference is waited for, never composed from** (novox/hq ADR 0498 §8), in the @@ -64,7 +64,7 @@ func Manifest(ctx context.Context, m catalogue.Manifest, b delivery.BuilderBuild // Built is each of a done key's artifacts as catalogue.Manifest.Resolve reads it: its recorded reference and digest, a // TypeScript bundle's launchers by the build modules' one rule (catalogue.Launchers), and, for the archive a module packs // its templates into, each template's text read back from that archive. -func Built(ctx context.Context, m catalogue.Manifest, artifacts []delivery.BuiltArtifact, archive Archive) ([]catalogue.Built, error) { +func Built(ctx context.Context, m catalogue.Manifest, artifacts []builds.BuiltArtifact, archive Archive) ([]catalogue.Built, error) { declared := map[string]catalogue.Artifact{} if m.Build != nil { for _, a := range m.Build.Artifacts { From 13e2fe14aa7c7ab6c8f142f2a50f084b33d8f20f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:47:59 +0100 Subject: [PATCH 5/5] =?UTF-8?q?Node-engine:=20logins,=20signers,=20enrolme?= =?UTF-8?q?nt=20tokens=20and=20redeems,=20and=20the=20grants=20part's=20se?= =?UTF-8?q?aled=20user=20list=20come=20from=20the=20vault=20and=20node-enr?= =?UTF-8?q?oller=20clients=20and=20the=20grants=20state=20package;=20no=20?= =?UTF-8?q?file=20imports=20go/delivery=20(ADR=200634=20=C2=A71,=20=C2=A74?= =?UTF-8?q?;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- cmd/mesh-host/authority_test.go | 40 ++++++------ cmd/mesh-host/enrol_seat.go | 13 ++-- cmd/mesh-host/logins.go | 13 ++-- cmd/mesh-host/main.go | 4 +- cmd/mesh-host/trust.go | 40 ++++++------ cmd/mesh-host/userlist.go | 4 +- go.mod | 3 + internal/agent/agent_test.go | 4 +- internal/buckets/buckets.go | 13 ++++ internal/compose/frombus/frombus.go | 10 +-- internal/identity/token.go | 16 ++--- internal/link/enrol_seat.go | 68 +++++++++++---------- internal/link/enrol_seat_test.go | 41 +++++++------ internal/link/files.go | 4 +- internal/link/logins.go | 16 ++--- internal/link/logins_test.go | 9 ++- internal/link/rotate.go | 19 +++--- internal/link/rotate_integration_test.go | 16 ++--- internal/link/rotate_test.go | 24 ++++---- internal/selfcompose/selfcompose.go | 4 +- internal/userlist/userlist.go | 29 +++++---- internal/userlist/userlist_test.go | 77 ++++++++++++------------ 22 files changed, 248 insertions(+), 219 deletions(-) diff --git a/cmd/mesh-host/authority_test.go b/cmd/mesh-host/authority_test.go index 09f7333..1287491 100644 --- a/cmd/mesh-host/authority_test.go +++ b/cmd/mesh-host/authority_test.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "encoding/json" "encoding/pem" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" engcomp "github.com/novox/mesh-host/internal/compositions" "os" "path/filepath" @@ -16,7 +17,6 @@ import ( "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/identity" ) @@ -52,38 +52,38 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) { told := 0 trust.onChange(func() { told++ }) keys := recordKeys(state, trust.Trusts) - ctrlFP, vaultFP := delivery.SignerFingerprint(controllerPub), delivery.SignerFingerprint(vaultPub) - enrol, _ := delivery.SignContext(delivery.KindEnrolment) + ctrlFP, vaultFP := meshvaultclient.SignerFingerprint(controllerPub), meshvaultclient.SignerFingerprint(vaultPub) + enrol, _ := meshvaultclient.SignContext(meshvaultclient.KindEnrolment) msg := []byte("a reply") if got := keys().Trusts; len(got) != 0 { t.Errorf("before the signers are read the record trusts %v", got) } - if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(controller, enrol, msg)); err == nil { + if _, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(controller, enrol, msg)); err == nil { t.Error("a controller-signed message is accepted") } - if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(vault, enrol, msg)); err == nil { + if _, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(vault, enrol, msg)); err == nil { t.Error("a vault-signed message is accepted before the vault's key is read") } vaultCA := testAuthority(t, vaultPub, vault) - keyValue, _ := json.Marshal(delivery.NewSignerKey(vaultPub, time.Now())) - fp, _ := delivery.AuthorityFingerprint(vaultCA) - authValue, _ := json.Marshal(delivery.SignerAuthority{Certificate: vaultCA, Fingerprint: fp, Made: time.Now()}) - trust.heardSigner(delivery.SignersKeyKey, keyValue, false, func(string) {}) - trust.heardSigner(delivery.SignersAuthorityKey, authValue, false, func(string) {}) - if s, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(vault, enrol, msg)); err != nil || s.Name != delivery.SignerVault { + keyValue, _ := json.Marshal(meshvaultclient.NewSignerKey(vaultPub, time.Now())) + fp, _ := meshvaultclient.AuthorityFingerprint(vaultCA) + authValue, _ := json.Marshal(meshvault.SignersByAuthorityValue{Certificate: vaultCA, Fingerprint: fp, Made: time.Now()}) + trust.heardSigner(meshvault.SignersByKeyKey(), keyValue, false, func(string) {}) + trust.heardSigner(meshvault.SignersByAuthorityKey(), authValue, false, func(string) {}) + if s, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(vault, enrol, msg)); err != nil || s.Name != meshvaultclient.SignerVault { t.Errorf("a vault-signed message on a node that read signers: %+v, %v", s, err) } - if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(controller, enrol, msg)); err == nil { + if _, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(controller, enrol, msg)); err == nil { t.Error("a controller-signed message is accepted once the vault's key is read") } - if got := keys().Trusts; strings.Contains(strings.Join(got, " "), ctrlFP) || !delivery.TrustsKey(got, delivery.SignerVault, vaultFP) || + if got := keys().Trusts; strings.Contains(strings.Join(got, " "), ctrlFP) || !meshvaultclient.TrustsKey(got, meshvaultclient.SignerVault, vaultFP) || trust.VaultAuthority() != vaultCA || told != 2 { t.Errorf("after the signers are read the record trusts %v (told %d)", got, told) } // A signers' key naming another fingerprint is not trusted. - bad, _ := json.Marshal(delivery.SignerKey{Key: base64.StdEncoding.EncodeToString(controllerPub), Fingerprint: vaultFP}) - trust.heardSigner(delivery.SignersKeyKey, bad, false, func(string) {}) - if !delivery.TrustsKey(keys().Trusts, delivery.SignerVault, vaultFP) { + bad, _ := json.Marshal(meshvault.SignersByKeyValue{Key: base64.StdEncoding.EncodeToString(controllerPub), Fingerprint: vaultFP}) + trust.heardSigner(meshvault.SignersByKeyKey(), bad, false, func(string) {}) + if !meshvaultclient.TrustsKey(keys().Trusts, meshvaultclient.SignerVault, vaultFP) { t.Error("a signers' key that does not check replaced the vault's") } @@ -98,8 +98,8 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) { } servingPub, _ := base64.StdEncoding.DecodeString(serving.Public) signedBy := func(ca string, key ed25519.PrivateKey) string { - parent, _ := delivery.ParseCertificate(ca) - der, err := x509.CreateCertificate(nil, delivery.NodeCertificateTemplate(overlay.InternalName("n1"), servingPub, time.Now().Add(-time.Hour)), + parent, _ := meshvaultclient.ParseCertificate(ca) + der, err := x509.CreateCertificate(nil, meshvaultclient.NodeCertificateTemplate(overlay.InternalName("n1"), servingPub, time.Now().Add(-time.Hour)), parent, ed25519.PublicKey(servingPub), key) if err != nil { t.Fatal(err) @@ -110,7 +110,7 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) { answer := signedBy(vaultCA, vault) ask := func(_ context.Context, verb string, args any) (json.RawMessage, error) { asked = append(asked, verb, args) - return json.Marshal(meshvault.SignAnswer{Kind: delivery.KindNodeCertificate, Node: "n1", Certificate: answer}) + return json.Marshal(meshvault.SignAnswer{Kind: meshvaultclient.KindNodeCertificate, Node: "n1", Certificate: answer}) } written := 0 if err := certifyThisNode(context.Background(), state, "n1", trust, ask, func() { written++ }); err != nil { @@ -160,7 +160,7 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) { // testAuthority is a self-signed authority certificate for key, as the vault makes its own. func testAuthority(t *testing.T, pub ed25519.PublicKey, key ed25519.PrivateKey) string { t.Helper() - tpl := delivery.AuthorityTemplate(pub, time.Now().Add(-time.Hour)) + tpl := meshvaultclient.AuthorityTemplate(pub, time.Now().Add(-time.Hour)) der, err := x509.CreateCertificate(nil, tpl, tpl, pub, key) if err != nil { t.Fatal(err) diff --git a/cmd/mesh-host/enrol_seat.go b/cmd/mesh-host/enrol_seat.go index 94cb23b..005dc44 100644 --- a/cmd/mesh-host/enrol_seat.go +++ b/cmd/mesh-host/enrol_seat.go @@ -6,10 +6,9 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" "os" - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/novox/mesh-host/internal/identity" "github.com/novox/mesh-host/internal/link" ) @@ -21,7 +20,7 @@ import ( func enrolThroughSeat(ctx context.Context, opts options, token identity.Token, node string, mine identity.Identity, sealing identity.SealingKey, serving identity.ServingKey, reported map[string]any, found *link.Tunnel, identityPath string) error { - req := delivery.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(mine.Public), OverlayKey: mine.Overlay.Public, + req := meshnodeenrollerclient.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(mine.Public), OverlayKey: mine.Overlay.Public, SealingKey: sealing.Public, ServingKey: serving.Public, Profile: reported} if found != nil { var err error @@ -29,18 +28,18 @@ func enrolThroughSeat(ctx context.Context, opts options, token identity.Token, n return err } } - req.Proof = base64.StdEncoding.EncodeToString(mine.Sign(delivery.RedeemProof(node, req.PublicKey, req.OverlayKey, req.SealingKey, req.ServingKey))) + req.Proof = base64.StdEncoding.EncodeToString(mine.Sign(meshnodeenrollerclient.RedeemProof(node, req.PublicKey, req.OverlayKey, req.SealingKey, req.ServingKey))) to := link.Approach{Address: token.Broker, Fingerprint: token.Fingerprint, Transport: link.OnNATS} membership := link.Membership{Node: node, Broker: token.Broker, Fingerprint: token.Fingerprint, Transport: link.OnNATS} j := link.Joining{Node: node, Signer: token.SignerKey(), - Redeem: func(ctx context.Context) (delivery.RedeemAnswer, error) { + Redeem: func(ctx context.Context) (meshnodeenrollerclient.RedeemAnswer, error) { return link.Redeem(ctx, to, node, token.Secret, req, opts.timeout) }, - Open: func(a delivery.RedeemAnswer) (string, error) { + Open: func(a meshnodeenrollerclient.RedeemAnswer) (string, error) { value, err := sealing.Open(a.Account) return string(value), err }, - Keep: func(a delivery.RedeemAnswer, password string) error { + Keep: func(a meshnodeenrollerclient.RedeemAnswer, password string) error { // The mesh's name for this node wins over what the machine called itself, as on the controller's path. mine.Node = a.Node mine.Membership = identity.Membership{Broker: token.Broker, Fingerprint: token.Fingerprint, Signer: token.Signer, diff --git a/cmd/mesh-host/logins.go b/cmd/mesh-host/logins.go index ab29a6f..a8ba164 100644 --- a/cmd/mesh-host/logins.go +++ b/cmd/mesh-host/logins.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "fmt" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" + "github.com/novox/mesh-host/internal/buckets" "os" "path/filepath" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/novox/mesh-host/internal/identity" "github.com/novox/mesh-host/internal/link" ) @@ -48,7 +49,7 @@ var loginsFollowAgain = 2 * time.Minute // followLogins follows this node's key of the vault's node-logins on every link, as a watch (never a poll), for as long as ctx // lasts, and hands each value to hear. A watch that cannot be made is said once and tried again. -func followLogins(ctx context.Context, session *link.Session, node string, hear func(delivery.NodeLogin), say link.Announce) { +func followLogins(ctx context.Context, session *link.Session, node string, hear func(meshvault.NodeLoginsByNodeValue), say link.Announce) { said := "" for ctx.Err() == nil { linked, cancel, err := session.WhenLinked(ctx) @@ -56,7 +57,7 @@ func followLogins(ctx context.Context, session *link.Session, node string, hear return } watching, stop := context.WithCancel(linked) - updates, werr := session.KVWatch(watching, delivery.NodeLoginsBucket, node) + updates, werr := session.KVWatch(watching, buckets.NodeLogins, node) if werr != nil { stop() if line := "this node's login is not followed yet (" + werr.Error() + "): it dials as it last did, and looks again in " + loginsFollowAgain.String(); line != said { @@ -69,9 +70,9 @@ func followLogins(ctx context.Context, session *link.Session, node string, hear if u.Caught || u.Deleted || len(u.Value) == 0 { continue } - nl, err := delivery.DecodeNodeLogin(u.Value) + nl, err := meshvaultclient.DecodeNodeLogin(u.Value) if err != nil { - say("the key " + node + " of " + delivery.NodeLoginsRead + " is not read: " + err.Error()) + say("the key " + node + " of " + buckets.NodeLoginsRead + " is not read: " + err.Error()) continue } hear(nl) diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index 812222f..dd0940d 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -16,6 +16,7 @@ import ( "errors" "flag" "fmt" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "io" "net" "os" @@ -30,7 +31,6 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/declaration" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/accounts" "github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/apply" @@ -1075,7 +1075,7 @@ func runLink(ctx context.Context, opts options) error { }, say) // And the login it dials as, which the vault's node-logins names for this node (novox/hq ADR 0583 §1). rotator := newRotator(queue, opts.state, opts.timeout, say) - go followLogins(aside, queue, mine.Node, func(nl delivery.NodeLogin) { + go followLogins(aside, queue, mine.Node, func(nl meshvault.NodeLoginsByNodeValue) { if line := membership.Logins.Heard(nl); line != "" { say(line) } diff --git a/cmd/mesh-host/trust.go b/cmd/mesh-host/trust.go index 4eb1e7d..5857944 100644 --- a/cmd/mesh-host/trust.go +++ b/cmd/mesh-host/trust.go @@ -9,6 +9,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/identity-and-access/go/clients/meshvaultclient" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" @@ -21,7 +22,6 @@ import ( "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/identity" "github.com/novox/mesh-host/internal/link" @@ -51,23 +51,23 @@ func (t *meshTrust) onChange(f func()) { } // Signers are the keys a mesh signature is verified against now: the vault's once read, and none before. -func (t *meshTrust) Signers() []delivery.Signer { +func (t *meshTrust) Signers() []meshvaultclient.Signer { t.mu.Lock() defer t.mu.Unlock() - var out []delivery.Signer + var out []meshvaultclient.Signer if t.vault != nil { - out = append(out, delivery.Signer{Name: delivery.SignerVault, Key: t.vault}) + out = append(out, meshvaultclient.Signer{Name: meshvaultclient.SignerVault, Key: t.vault}) } return out } // Verify is the signer that signed msg as kind, or why none it trusts did. -func (t *meshTrust) Verify(kind string, msg, sig []byte) (delivery.Signer, error) { - context, err := delivery.SignContext(kind) +func (t *meshTrust) Verify(kind string, msg, sig []byte) (meshvaultclient.Signer, error) { + context, err := meshvaultclient.SignContext(kind) if err != nil { - return delivery.Signer{}, err + return meshvaultclient.Signer{}, err } - return delivery.VerifyBy(t.Signers(), context, msg, sig) + return meshvaultclient.VerifyBy(t.Signers(), context, msg, sig) } // VaultAuthority is the vault's authority certificate as read from `signers`, or "". @@ -87,9 +87,9 @@ func (t *meshTrust) Trusts() []string { } authorities := map[string]string{} if a := t.VaultAuthority(); a != "" { - authorities[delivery.SignerVault] = a + authorities[meshvaultclient.SignerVault] = a } - return delivery.Trusts(t.Signers(), authorities) + return meshvaultclient.Trusts(t.Signers(), authorities) } // change applies f under the lock and tells who follows when Trusts differs after it. @@ -108,25 +108,25 @@ func (t *meshTrust) change(f func()) { // check; a deleted key is no longer trusted. func (t *meshTrust) heardSigner(key string, value []byte, deleted bool, say link.Announce) { switch key { - case delivery.SignersKeyKey: + case meshvault.SignersByKeyKey(): var pub ed25519.PublicKey if !deleted && len(value) > 0 { - k, err := delivery.DecodeSignerKey(value) + k, err := meshvaultclient.DecodeSignerKey(value) if err == nil { - pub, err = k.PublicKey() + pub, err = meshvaultclient.SignerPublicKey(k) } if err != nil { - say("the vault's signing key in " + delivery.SignersRead + " is not trusted: " + err.Error()) + say("the vault's signing key in " + buckets.SignersRead + " is not trusted: " + err.Error()) return } } t.change(func() { t.vault = pub }) - case delivery.SignersAuthorityKey: + case meshvault.SignersByAuthorityKey(): pem := "" if !deleted && len(value) > 0 { - a, err := delivery.DecodeSignerAuthority(value) + a, err := meshvaultclient.DecodeSignerAuthority(value) if err != nil { - say("the vault's authority in " + delivery.SignersRead + " is not trusted: " + err.Error()) + say("the vault's authority in " + buckets.SignersRead + " is not trusted: " + err.Error()) return } pem = a.Certificate @@ -157,7 +157,7 @@ func followTrust(ctx context.Context, session *link.Session, trust *meshTrust, s return } watching, stop := context.WithCancel(linked) - signers, serr := session.KVWatch(watching, delivery.SignersBucket, ">") + signers, serr := session.KVWatch(watching, buckets.Signers, ">") if serr != nil { stop() sayOnce(fmt.Sprintf("what this node trusts a mesh signature from is not followed yet (the vault's signers: %v): "+ @@ -266,7 +266,7 @@ func certifyThisNode(ctx context.Context, state, node string, trust *meshTrust, if err != nil || len(key) != ed25519.PublicKeySize { return fmt.Errorf("this node's serving key is not an ed25519 key") } - raw, err := ask(ctx, meshvault.VerbSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node}) + raw, err := ask(ctx, meshvault.VerbSign, map[string]any{"kind": meshvaultclient.KindNodeCertificate, "node": node}) if err != nil { return err } @@ -278,7 +278,7 @@ func certifyThisNode(ctx context.Context, state, node string, trust *meshTrust, if trusted == "" { return fmt.Errorf("this node has read no authority of the vault's to check the certificate against") } - if err := delivery.CheckNodeCertificate(a.Certificate, []string{trusted}, overlay.InternalName(node), + if err := meshvaultclient.CheckNodeCertificate(a.Certificate, []string{trusted}, overlay.InternalName(node), ed25519.PublicKey(key), time.Now()); err != nil { return err } diff --git a/cmd/mesh-host/userlist.go b/cmd/mesh-host/userlist.go index 8f531fb..a998447 100644 --- a/cmd/mesh-host/userlist.go +++ b/cmd/mesh-host/userlist.go @@ -5,12 +5,12 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" engcomp "github.com/novox/mesh-host/internal/compositions" "os" "path/filepath" "git.novox.be/novox/mesh-sdk/go/declaration" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/seal" "github.com/novox/mesh-host/internal/apply" "github.com/novox/mesh-host/internal/identity" @@ -44,7 +44,7 @@ func userListFollower(opts options, session *link.Session, running func() []engc }, // From take-over, the grants part's list (ADR 0547 clauses 7, 8): its one key watched, its object read by name and // opened with this machine's sealing key, the key's public half said in the have so the grants part seals to it. - Grants: bucketWatch{session, delivery.GrantsUserListBucket}, + Grants: bucketWatch{session, grantsstate.GrantsUserListBucket}, Fetch: func(ctx context.Context, name string) ([]byte, error) { return session.GrantsObject(ctx, name, grantsListLimit) }, diff --git a/go.mod b/go.mod index bf5e482..4165d0b 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/identity-and-access/go/clients/meshnodeenrollerclient v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate v0.0.0-20261105000000-000000000000 git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0 git.novox.be/novox/mesh-delivery/sdk v0.0.0-20261105000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-20261105000000-000000000000 diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 46155a0..36d826c 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -10,6 +10,7 @@ import ( "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "github.com/novox/mesh-host/internal/buckets/bucketstest" "github.com/novox/mesh-host/internal/compositions" + "github.com/novox/mesh-host/internal/judge" "maps" "slices" "strings" @@ -20,7 +21,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" ) const node = "n1" @@ -1692,7 +1692,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( t.Errorf("the composition of db waiting for a yes reads %+v (%v)", view, err) } if view, err := b.ag.Composition(context.Background(), "db", ddig, false); err != nil || view.Composition.Digest != ddig || - view.Judged.Judgement != string(delivery.JudgedUnchanged) || view.Judged.WaitsForYes || view.Diff != nil { + view.Judged.Judgement != judge.JudgedUnchanged || view.Judged.WaitsForYes || view.Diff != nil { t.Errorf("db's running revision, asked by its digest, reads %+v (%v)", view, err) } if _, err := b.ag.Composition(context.Background(), "db", "sha256:"+strings.Repeat("0", 64), false); err == nil { diff --git a/internal/buckets/buckets.go b/internal/buckets/buckets.go index 53b0479..13f551a 100644 --- a/internal/buckets/buckets.go +++ b/internal/buckets/buckets.go @@ -5,6 +5,7 @@ package buckets import ( "errors" + vault "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" @@ -57,3 +58,15 @@ func CandidateKey(group string) (string, error) { } return catalog.CandidatesByGroupKey(keys.EscapeName(group)), nil } + +// The states of the vault seat's holder that the engine reads, named by the holder's module (ADR 0634 §1, finding F4). +const ( + // Signers holds the vault's public signing halves; NodeLogins each node's logins, ``. + Signers = vaultHolder + "_" + vault.StateSigners + NodeLogins = vaultHolder + "_" + vault.StateNodeLogins + // SignersRead and NodeLoginsRead are the states as a manifest's reads names them. + SignersRead = vaultHolder + "." + vault.StateSigners + NodeLoginsRead = vaultHolder + "." + vault.StateNodeLogins + + vaultHolder = "mesh-vault" +) diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index 9e7b302..e3971b8 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -36,6 +36,7 @@ import ( "errors" "fmt" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" @@ -51,7 +52,6 @@ import ( "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "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/compose" "github.com/novox/mesh-host/internal/compose/resolution" "github.com/novox/mesh-host/internal/takein" @@ -93,11 +93,11 @@ type Reader interface { Served(ctx context.Context, node, module string, keys []string) (map[string]any, error) } -// SignersReader is a Reader that also reads the vault's public authority from its state `signers` (delivery.SignersBucket, -// key delivery.SignersAuthorityKey; ADR 0550 §2). A Reader that has it composes the mesh's authority as a bundle, the +// SignersReader is a Reader that also reads the vault's public authority from its state `signers` (buckets.Signers, +// key meshvault.SignersByAuthorityKey(); ADR 0550 §2). A Reader that has it composes the mesh's authority as a bundle, the // controller's and the vault's; one that has not, or that answers none, composes the controller's alone, as before. type SignersReader interface { - // VaultAuthority is the vault's authority certificate as PEM, checked (delivery.DecodeSignerAuthority); empty when the + // VaultAuthority is the vault's authority certificate as PEM, checked (meshvaultclient.DecodeSignerAuthority); empty when the // vault has published none. A read that fails is said by the reader and answered empty: a node that may not read the // state yet composes as before rather than not at all. VaultAuthority(ctx context.Context) string @@ -345,7 +345,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) { // against the bundle. The controller's stays while a certificate it issued is in a node's record. if sr, ok := r.(SignersReader); ok { if vault := sr.VaultAuthority(ctx); vault != "" { - in.Authority = delivery.AuthorityBundle(in.Authority, vault) + in.Authority = meshvaultclient.AuthorityBundle(in.Authority, vault) } } named := func(field *map[string]string, module, why string) { diff --git a/internal/identity/token.go b/internal/identity/token.go index f024bb8..efec00f 100644 --- a/internal/identity/token.go +++ b/internal/identity/token.go @@ -1,15 +1,17 @@ package identity -import "git.novox.be/novox/mesh-sdk/go/delivery" +import ( + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" +) // Token is what a person carries to a machine that is joining (novox/hq ADR 0004): where the broker is, what certificate to // expect there, whose signature to believe afterwards, and a one-time right to join. Its shape and its reading are mesh-sdk's -// (delivery.Token), which the vault that makes it, mesh-cli that opens it and this engine that parses it share (novox/hq ADR +// (meshnodeenrollerclient.Token), which the vault that makes it, mesh-cli that opens it and this engine that parses it share (novox/hq ADR // 0587 §3, issue 774); these are the names this package has always had for them. -type Token = delivery.Token +type Token = meshnodeenrollerclient.Token -// TokenTunnel is the joining machine's side of its first tunnel (delivery.TokenTunnel). -type TokenTunnel = delivery.TokenTunnel +// TokenTunnel is the joining machine's side of its first tunnel (meshnodeenrollerclient.TokenTunnel). +type TokenTunnel = meshnodeenrollerclient.TokenTunnel -// ParseToken reads a token a person pasted (delivery.ParseToken). -func ParseToken(encoded string) (Token, error) { return delivery.ParseToken(encoded) } +// ParseToken reads a token a person pasted (meshnodeenrollerclient.ParseToken). +func ParseToken(encoded string) (Token, error) { return meshnodeenrollerclient.ParseToken(encoded) } diff --git a/internal/link/enrol_seat.go b/internal/link/enrol_seat.go index d6eb34f..43b8556 100644 --- a/internal/link/enrol_seat.go +++ b/internal/link/enrol_seat.go @@ -7,12 +7,14 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" + "github.com/novox/mesh-host/internal/buckets" "strings" "time" "github.com/nats-io/nats.go" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // Joining a mesh through the seat mesh-enrolment (novox/hq ADR 0587 §6, issue 774): the node-engine dials the bus as enrol. @@ -40,7 +42,7 @@ var ErrNoSeat = errors.New("the seat mesh-enrolment is not served to this machin // VerifyRedeemAnswer holds an answer to what it must be before anything is kept (ADR 0587 §6): it names the node asked for, // carries the signing key the token carries, and is signed by that key under the context of an enrolment's answer. An answer // signed by another key is refused, and so is one that names another signer. -func VerifyRedeemAnswer(a delivery.RedeemAnswer, node string, signer ed25519.PublicKey) error { +func VerifyRedeemAnswer(a meshnodeenrollerclient.RedeemAnswer, node string, signer ed25519.PublicKey) error { if a.Node != node { return fmt.Errorf("the answer is for %q, and this machine asked as %q", a.Node, node) } @@ -51,11 +53,11 @@ func VerifyRedeemAnswer(a delivery.RedeemAnswer, node string, signer ed25519.Pub if err != nil || len(sig) == 0 { return errors.New("the answer is not signed") } - kindContext, err := delivery.SignContext(delivery.KindEnrolment) + kindContext, err := meshvaultclient.SignContext(meshvaultclient.KindEnrolment) if err != nil { return err } - if _, err := delivery.VerifyBy([]delivery.Signer{{Name: "the token's", Key: signer}}, kindContext, a.SignedBytes(), sig); err != nil { + if _, err := meshvaultclient.VerifyBy([]meshvaultclient.Signer{{Name: "the token's", Key: signer}}, kindContext, a.SignedBytes(), sig); err != nil { return fmt.Errorf("the answer is not signed by the key the token carries: %w", err) } if a.Login == "" || a.Account == "" { @@ -83,52 +85,52 @@ func seatAnswer(reply []byte) (json.RawMessage, error) { // asks again with the same keys while the holder says to (a stopped redeem finishes; the bus node loads the account), , for // EnrolPatience. An answer is returned as the holder gave it: the caller // verifies it (VerifyRedeemAnswer) before it keeps anything. -func Redeem(ctx context.Context, to Approach, node, secret string, req delivery.Redeem, timeout time.Duration) (delivery.RedeemAnswer, error) { +func Redeem(ctx context.Context, to Approach, node, secret string, req meshnodeenrollerclient.Redeem, timeout time.Duration) (meshnodeenrollerclient.RedeemAnswer, error) { if to.Transport != OnNATS { - return delivery.RedeemAnswer{}, fmt.Errorf("this token is for the %q bus, and the mesh's bus is %s", to.Transport, OnNATS) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("this token is for the %q bus, and the mesh's bus is %s", to.Transport, OnNATS) } config, err := PinnedConfig(to.Fingerprint) if err != nil { - return delivery.RedeemAnswer{}, err + return meshnodeenrollerclient.RedeemAnswer{}, err } - user := delivery.EnrolmentUser(node) + user := meshnodeenrollerclient.User(node) conn, err := nats.Connect(natsURL(to.Address), nats.Secure(config), nats.CustomInboxPrefix("_INBOX."+user), nats.UserInfo(user, secret), nats.Name("mesh-host/enrol/"+node), nats.Timeout(timeout), nats.NoReconnect()) if err != nil { if errors.Is(err, ErrWrongCertificate) { - return delivery.RedeemAnswer{}, err + return meshnodeenrollerclient.RedeemAnswer{}, err } - return delivery.RedeemAnswer{}, fmt.Errorf("cannot reach the bus at %s as %s: %w", to.Address, user, err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("cannot reach the bus at %s as %s: %w", to.Address, user, err) } defer conn.Close() body, err := json.Marshal(req) if err != nil { - return delivery.RedeemAnswer{}, err + return meshnodeenrollerclient.RedeemAnswer{}, err } began := time.Now() for { msg, err := conn.RequestWithContext(ctx, CalledSeatSubject(EnrolmentSeat, EnrolmentRedeem, user), body) switch { case errors.Is(err, nats.ErrNoResponders) || (err != nil && strings.Contains(err.Error(), "permissions violation")): - return delivery.RedeemAnswer{}, fmt.Errorf("%w: %v", ErrNoSeat, err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("%w: %v", ErrNoSeat, err) case err != nil: - return delivery.RedeemAnswer{}, fmt.Errorf("the seat did not answer redeem: %w", err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the seat did not answer redeem: %w", err) } result, aerr := seatAnswer(msg.Data) if aerr != nil { if strings.Contains(aerr.Error(), "call redeem again with the same keys") && time.Since(began) < EnrolPatience { select { case <-ctx.Done(): - return delivery.RedeemAnswer{}, ctx.Err() + return meshnodeenrollerclient.RedeemAnswer{}, ctx.Err() case <-time.After(AskAgainAfter): continue } } - return delivery.RedeemAnswer{}, fmt.Errorf("%w: %v", ErrRefused, aerr) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("%w: %v", ErrRefused, aerr) } - var answer delivery.RedeemAnswer + var answer meshnodeenrollerclient.RedeemAnswer if err := json.Unmarshal(result, &answer); err != nil { - return delivery.RedeemAnswer{}, fmt.Errorf("the seat's answer to redeem is not one: %w", err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the seat's answer to redeem is not one: %w", err) } return answer, nil } @@ -137,7 +139,7 @@ func Redeem(ctx context.Context, to Approach, node, secret string, req delivery. // Confirming is this machine dialled as its own login after it redeemed: it reads its key of node-logins and calls confirm. type Confirming interface { // Login is this node's key of the vault's node-logins. - Login(ctx context.Context) (delivery.NodeLogin, error) + Login(ctx context.Context) (meshvault.NodeLoginsByNodeValue, error) // Confirm calls the seat's confirm; the runtime's proof of the caller as node. is the confirmation (ADR 0583 §3). Confirm(ctx context.Context) error Close() @@ -149,29 +151,29 @@ type Joining struct { Signer ed25519.PublicKey // Redeem calls the seat; Open opens the account sealed to this node's sealing key; Keep writes the membership, the login // and the keys down; Dial dials the bus as login and answers it ready to read node-logins and confirm. - Redeem func(ctx context.Context) (delivery.RedeemAnswer, error) - Open func(a delivery.RedeemAnswer) (string, error) - Keep func(a delivery.RedeemAnswer, password string) error + Redeem func(ctx context.Context) (meshnodeenrollerclient.RedeemAnswer, error) + Open func(a meshnodeenrollerclient.RedeemAnswer) (string, error) + Keep func(a meshnodeenrollerclient.RedeemAnswer, password string) error Dial func(ctx context.Context, login, password string) (Confirming, error) } // Run joins: the answer is verified before anything is kept (an answer not signed by the key the token carries keeps nothing), // kept, and then the machine dials as the login its key of node-logins names and confirms. Nothing is applied here: the engine // applies once its placement names a module. -func (j Joining) Run(ctx context.Context) (delivery.RedeemAnswer, error) { +func (j Joining) Run(ctx context.Context) (meshnodeenrollerclient.RedeemAnswer, error) { answer, err := j.Redeem(ctx) if err != nil { - return delivery.RedeemAnswer{}, err + return meshnodeenrollerclient.RedeemAnswer{}, err } if err := VerifyRedeemAnswer(answer, j.Node, j.Signer); err != nil { - return delivery.RedeemAnswer{}, fmt.Errorf("%w: %v; nothing was kept", ErrRefused, err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("%w: %v; nothing was kept", ErrRefused, err) } password, err := j.Open(answer) if err != nil { - return delivery.RedeemAnswer{}, fmt.Errorf("the account in the answer cannot be opened: %w; nothing was kept", err) + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the account in the answer cannot be opened: %w; nothing was kept", err) } if err := j.Keep(answer, password); err != nil { - return delivery.RedeemAnswer{}, fmt.Errorf("the mesh accepted this node as %q and what it was given could not be kept: %w\n"+ + return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the mesh accepted this node as %q and what it was given could not be kept: %w\n"+ "That token is spent, so getting back needs a new one", answer.Node, err) } c, err := j.Dial(ctx, answer.Login, password) @@ -213,20 +215,20 @@ func DialConfirming(ctx context.Context, m Membership, login, password string, t return &confirmingNats{conn: conn, node: m.Node, user: login}, nil } -func (c *confirmingNats) Login(ctx context.Context) (delivery.NodeLogin, error) { +func (c *confirmingNats) Login(ctx context.Context) (meshvault.NodeLoginsByNodeValue, error) { js, err := c.conn.JetStream() if err != nil { - return delivery.NodeLogin{}, err + return meshvault.NodeLoginsByNodeValue{}, err } - kv, err := js.KeyValue(delivery.NodeLoginsBucket) + kv, err := js.KeyValue(buckets.NodeLogins) if err != nil { - return delivery.NodeLogin{}, err + return meshvault.NodeLoginsByNodeValue{}, err } e, err := kv.Get(c.node) if err != nil { - return delivery.NodeLogin{}, err + return meshvault.NodeLoginsByNodeValue{}, err } - return delivery.DecodeNodeLogin(e.Value()) + return meshvaultclient.DecodeNodeLogin(e.Value()) } func (c *confirmingNats) Confirm(ctx context.Context) error { diff --git a/internal/link/enrol_seat_test.go b/internal/link/enrol_seat_test.go index b1393a9..b594d8d 100644 --- a/internal/link/enrol_seat_test.go +++ b/internal/link/enrol_seat_test.go @@ -6,33 +6,34 @@ import ( "crypto/ed25519" "encoding/base64" "errors" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "slices" "strings" "testing" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // signedBy is an answer for ace, signed under the enrolment context by the key seeded, naming that key as its signer. -func signedBy(t *testing.T, seed byte) (delivery.RedeemAnswer, ed25519.PublicKey) { +func signedBy(t *testing.T, seed byte) (meshnodeenrollerclient.RedeemAnswer, ed25519.PublicKey) { t.Helper() priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, ed25519.SeedSize)) pub := priv.Public().(ed25519.PublicKey) - a := delivery.RedeemAnswer{Node: "ace", Login: "node.ace", Account: `{"sealed":"box"}`, Certificate: "PEM", Signer: base64.StdEncoding.EncodeToString(pub)} - ctx, err := delivery.SignContext(delivery.KindEnrolment) + a := meshnodeenrollerclient.RedeemAnswer{Node: "ace", Login: "node.ace", Account: `{"sealed":"box"}`, Certificate: "PEM", Signer: base64.StdEncoding.EncodeToString(pub)} + ctx, err := meshvaultclient.SignContext(meshvaultclient.KindEnrolment) if err != nil { t.Fatal(err) } - a.Signature = base64.StdEncoding.EncodeToString(delivery.SignMessage(priv, ctx, a.SignedBytes())) + a.Signature = base64.StdEncoding.EncodeToString(meshvaultclient.SignMessage(priv, ctx, a.SignedBytes())) return a, pub } type standInConfirming struct { steps *[]string - nl delivery.NodeLogin + nl meshvault.NodeLoginsByNodeValue } -func (c standInConfirming) Login(context.Context) (delivery.NodeLogin, error) { +func (c standInConfirming) Login(context.Context) (meshvault.NodeLoginsByNodeValue, error) { *c.steps = append(*c.steps, "read node-logins") return c.nl, nil } @@ -52,18 +53,21 @@ func TestTheEngineKeepsOnlyAnAnswerSignedByTheKeyItsTokenCarries(t *testing.T) { badNode := good badNode.Node = "g14" for name, c := range map[string]struct { - answer delivery.RedeemAnswer + answer meshnodeenrollerclient.RedeemAnswer want string }{ "signed by another key": {forged, "signing key other than the one the token carries"}, "another node": {badNode, `is for "g14"`}, - "altered after signing": {func() delivery.RedeemAnswer { a := good; a.Account = "other"; return a }(), "not signed by the key the token carries"}, + "altered after signing": {func() meshnodeenrollerclient.RedeemAnswer { a := good; a.Account = "other"; return a }(), "not signed by the key the token carries"}, } { var steps []string j := Joining{Node: "ace", Signer: pub, - Redeem: func(context.Context) (delivery.RedeemAnswer, error) { return c.answer, nil }, - Open: func(delivery.RedeemAnswer) (string, error) { steps = append(steps, "open"); return "pw", nil }, - Keep: func(delivery.RedeemAnswer, string) error { steps = append(steps, "keep"); return nil }, + Redeem: func(context.Context) (meshnodeenrollerclient.RedeemAnswer, error) { return c.answer, nil }, + Open: func(meshnodeenrollerclient.RedeemAnswer) (string, error) { + steps = append(steps, "open") + return "pw", nil + }, + Keep: func(meshnodeenrollerclient.RedeemAnswer, string) error { steps = append(steps, "keep"); return nil }, Dial: func(context.Context, string, string) (Confirming, error) { steps = append(steps, "dial") return nil, errors.New("must not dial") @@ -75,18 +79,21 @@ func TestTheEngineKeepsOnlyAnAnswerSignedByTheKeyItsTokenCarries(t *testing.T) { var steps []string login := "node.ace" j := Joining{Node: "ace", Signer: pub, - Redeem: func(context.Context) (delivery.RedeemAnswer, error) { + Redeem: func(context.Context) (meshnodeenrollerclient.RedeemAnswer, error) { steps = append(steps, "redeem") return good, nil }, - Open: func(delivery.RedeemAnswer) (string, error) { steps = append(steps, "open"); return "pw", nil }, - Keep: func(a delivery.RedeemAnswer, pw string) error { + Open: func(meshnodeenrollerclient.RedeemAnswer) (string, error) { + steps = append(steps, "open") + return "pw", nil + }, + Keep: func(a meshnodeenrollerclient.RedeemAnswer, pw string) error { steps = append(steps, "keep "+a.Login+" "+pw) return nil }, Dial: func(_ context.Context, l, pw string) (Confirming, error) { steps = append(steps, "dial "+l+" "+pw) - return standInConfirming{steps: &steps, nl: delivery.NodeLogin{Node: "ace", Login: login}}, nil + return standInConfirming{steps: &steps, nl: meshvault.NodeLoginsByNodeValue{Node: "ace", Login: login}}, nil }} if _, err := j.Run(ctx); err != nil { t.Fatal(err) diff --git a/internal/link/files.go b/internal/link/files.go index b4a251c..44d02dd 100644 --- a/internal/link/files.go +++ b/internal/link/files.go @@ -7,10 +7,10 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" "time" "git.novox.be/novox/mesh-sdk/go/declaration" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" ) @@ -114,7 +114,7 @@ func (b OverNATS) GrantsObject(ctx context.Context, name string, limit int64) ([ } js = bounded } - return objectByName(ctx, js, delivery.GrantsUserListObjects, delivery.GrantsUserListObjectStream, name, limit) + return objectByName(ctx, js, grantsstate.GrantsUserListObjects, grantsstate.GrantsUserListObjectStream, name, limit) } // objectByName is one object of bucket (its stream stream) by its name, no larger than limit, verified against its digest. diff --git a/internal/link/logins.go b/internal/link/logins.go index 1ee7fa7..a9df4a4 100644 --- a/internal/link/logins.go +++ b/internal/link/logins.go @@ -2,12 +2,12 @@ package link import ( "errors" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "strings" "sync" "github.com/nats-io/nats.go" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // Logins is which of its two logins this node-engine dials the bus as, and the password of each it holds (novox/hq ADR 0583 §1, @@ -42,12 +42,12 @@ func NewLogins(node, first string, saved LoginsState, save func(LoginsState) err l.values[login] = password } if first != "" { - if _, ok := l.values[delivery.FirstLogin(node)]; !ok { - l.values[delivery.FirstLogin(node)] = first + if _, ok := l.values[meshvaultclient.FirstLogin(node)]; !ok { + l.values[meshvaultclient.FirstLogin(node)] = first } } if l.current == "" { - l.current = delivery.FirstLogin(node) + l.current = meshvaultclient.FirstLogin(node) } return l } @@ -71,7 +71,7 @@ func (l *Logins) Value(login string) (string, bool) { func (l *Logins) Hold(login, password string) error { l.mu.Lock() defer l.mu.Unlock() - if n, ok := delivery.LoginNode(login); !ok || n != l.node { + if n, ok := meshvaultclient.LoginNode(login); !ok || n != l.node { return &loginError{login: login, node: l.node} } l.values[login] = password @@ -164,7 +164,7 @@ func (m Membership) User() string { // Heard takes this node's key of the vault's node-logins as read (a key read, or the empty value of a key gone): the login // in use becomes the one it names when its password is held. A key naming a login whose password is not held changes // nothing and says so; no key leaves the login as it is. -func (l *Logins) Heard(nl delivery.NodeLogin) (said string) { +func (l *Logins) Heard(nl meshvault.NodeLoginsByNodeValue) (said string) { if nl.Node != l.node || nl.Login == "" { return "" } @@ -182,7 +182,7 @@ func (l *Logins) Heard(nl delivery.NodeLogin) (said string) { } // rotatingToUs says the key's rotating login is the one in use already: the engine switched ahead of the vault's own state. -func rotatingToUs(nl delivery.NodeLogin, current string) bool { +func rotatingToUs(nl meshvault.NodeLoginsByNodeValue, current string) bool { return nl.Rotating != "" && nl.Rotating == current } diff --git a/internal/link/logins_test.go b/internal/link/logins_test.go index 38ffd41..3af430e 100644 --- a/internal/link/logins_test.go +++ b/internal/link/logins_test.go @@ -1,10 +1,9 @@ package link import ( + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "strings" "testing" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // 774/S61/AC1 and AC2 (ADR 0583 §1): with no node-logins key, or one that names the first login, the engine dials as node. @@ -20,10 +19,10 @@ func TestTheLoginDialledIsTheOneNodeLoginsNames(t *testing.T) { if u, p := (Membership{Node: "ace", Password: "plain"}).Credential(); u != "node.ace" || p != "plain" { t.Fatalf("no logins at all: %s %s", u, p) } - if said := l.Heard(delivery.NodeLogin{Node: "ace", Login: "node.ace"}); said != "" || m.User() != "node.ace" { + if said := l.Heard(meshvault.NodeLoginsByNodeValue{Node: "ace", Login: "node.ace"}); said != "" || m.User() != "node.ace" { t.Errorf("the first login named: %q, %s", said, m.User()) } - if said := l.Heard(delivery.NodeLogin{Node: "ace", Login: "node.ace--b"}); !strings.Contains(said, "cannot dial as it") || m.User() != "node.ace" { + if said := l.Heard(meshvault.NodeLoginsByNodeValue{Node: "ace", Login: "node.ace--b"}); !strings.Contains(said, "cannot dial as it") || m.User() != "node.ace" { t.Errorf("a login held no value for: %q, %s", said, m.User()) } if err := l.Hold("node.ace--b", "second-secret"); err != nil { @@ -32,7 +31,7 @@ func TestTheLoginDialledIsTheOneNodeLoginsNames(t *testing.T) { if err := l.Hold("node.g14", "x"); err == nil { t.Error("another node's login was kept") } - if said := l.Heard(delivery.NodeLogin{Node: "ace", Login: "node.ace--b"}); !strings.Contains(said, "dialled as it") { + if said := l.Heard(meshvault.NodeLoginsByNodeValue{Node: "ace", Login: "node.ace--b"}); !strings.Contains(said, "dialled as it") { t.Errorf("said %q", said) } if u, p := m.Credential(); u != "node.ace--b" || p != "second-secret" || saved.Current != "node.ace--b" { diff --git a/internal/link/rotate.go b/internal/link/rotate.go index e0474e9..f9a1a78 100644 --- a/internal/link/rotate.go +++ b/internal/link/rotate.go @@ -4,10 +4,11 @@ import ( "context" "fmt" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "sync" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // Rotator is the engine's side of a node's account rotation (novox/hq ADR 0583 §3-§6, issue 774). The vault begins a rotation @@ -49,7 +50,7 @@ func (r *Rotator) say(f string, a ...any) { } // Heard takes this node's key of node-logins as read. -func (r *Rotator) Heard(ctx context.Context, nl delivery.NodeLogin) { +func (r *Rotator) Heard(ctx context.Context, nl meshvault.NodeLoginsByNodeValue) { r.mu.Lock() defer r.mu.Unlock() if r.fallen != nil && nl.Since.String() != r.fellBackSince { @@ -61,7 +62,7 @@ func (r *Rotator) Heard(ctx context.Context, nl delivery.NodeLogin) { // Done (the switch happened and the old login is retired), or no rotation at all: the other login's value goes (§4), // only now that the vault says so. r.old, r.failures = "", 0 - if other := delivery.OtherLogin(nl.Node, cur); other != "" { + if other := meshvaultclient.OtherLogin(nl.Node, cur); other != "" { if _, held := r.Logins.Value(other); held { if err := r.Logins.Drop(other); err != nil { r.say("the old login %s's value is not dropped: %v", other, err) @@ -86,18 +87,18 @@ func (r *Rotator) Heard(ctx context.Context, nl delivery.NodeLogin) { if err := r.Logins.Drop(cur); err != nil { r.say("the abandoned login %s's value is not dropped: %v", cur, err) } - case nl.Login == cur && nl.Stage != delivery.RotationBegun: + case nl.Login == cur && nl.Stage != meshvaultclient.RotationBegun: if r.fellBackSince != "" && r.fellBackSince == nl.Since.String() { return // fallen back for this rotation: not tried again until the vault begins another } r.switchTo(ctx, nl) - case nl.Rotating == cur && nl.Stage != delivery.RotationConfirmed: + case nl.Rotating == cur && nl.Stage != meshvaultclient.RotationConfirmed: r.confirm(ctx, nl.Rotating) } } // switchTo fetches the new value, test-dials it and, only if it authenticates, switches and confirms (§3). -func (r *Rotator) switchTo(ctx context.Context, nl delivery.NodeLogin) { +func (r *Rotator) switchTo(ctx context.Context, nl meshvault.NodeLoginsByNodeValue) { next := nl.Rotating password, held := r.Logins.Value(next) if !held { @@ -158,7 +159,7 @@ func (r *Rotator) AuthFailed(login string) { return } text := fmt.Sprintf("%s failed to authenticate %d times before the vault said rotation-done: this node dials as %s again", login, r.failures, r.old) - r.say("%s: %s", delivery.NodeAccountFallbackFinding, text) + r.say("%s: %s", NodeAccountFallbackFinding, text) now := time.Now if r.Now != nil { now = r.Now @@ -201,3 +202,7 @@ func (r *Rotator) Watch() { } } } + +// NodeAccountFallbackFinding is what the node-engine says when it fell back to its old login after repeated +// authentication failures with the new one, before the rotation is done; the watchman raises it (ADR 0550). +const NodeAccountFallbackFinding = "node-account-fallback" diff --git a/internal/link/rotate_integration_test.go b/internal/link/rotate_integration_test.go index a3d42ea..8636495 100644 --- a/internal/link/rotate_integration_test.go +++ b/internal/link/rotate_integration_test.go @@ -5,14 +5,14 @@ package link import ( "context" "errors" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "strings" "testing" "time" natsserver "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // 774/S65/AC1 (ADR 0583 §2-§6): the engine's rotation against a real bus whose list of users a stand-in vault reloads step by @@ -114,19 +114,19 @@ func TestARotationAgainstARealBusWhoseListIsReloadedStepByStep(t *testing.T) { } ctx := context.Background() since := time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC) - key := func(login, rotating, stage string) delivery.NodeLogin { - return delivery.NodeLogin{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: since} + key := func(login, rotating, stage string) meshvault.NodeLoginsByNodeValue { + return meshvault.NodeLoginsByNodeValue{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: since} } // A whole rotation: begun, the list holds both logins, ready, the switch and the confirmation, the old login retired, done. srv, reload := start(t) r, l, confirmed := build(t, srv) oneAuthenticates(t, srv, "before the rotation") - r.Heard(ctx, key(first, second, delivery.RotationBegun)) + r.Heard(ctx, key(first, second, meshvaultclient.RotationBegun)) oneAuthenticates(t, srv, "begun") reload(first, second) oneAuthenticates(t, srv, "the list holds both") - r.Heard(ctx, key(first, second, delivery.RotationReady)) + r.Heard(ctx, key(first, second, meshvaultclient.RotationReady)) if cur, _ := l.Current(); cur != second { t.Fatalf("the engine did not switch once the list held both logins: %s", cur) } @@ -149,7 +149,7 @@ func TestARotationAgainstARealBusWhoseListIsReloadedStepByStep(t *testing.T) { // With the list never loaded the engine never switches: the test-dial is refused. srv, _ = start(t) r, l, _ = build(t, srv) - r.Heard(ctx, key(first, second, delivery.RotationReady)) + r.Heard(ctx, key(first, second, meshvaultclient.RotationReady)) if cur, _ := l.Current(); cur != first || !authenticates(srv, first) { t.Errorf("the list was never loaded and the engine is on %s", cur) } @@ -158,7 +158,7 @@ func TestARotationAgainstARealBusWhoseListIsReloadedStepByStep(t *testing.T) { srv, reload = start(t) r, l, _ = build(t, srv) reload(first, second) - r.Heard(ctx, key(first, second, delivery.RotationReady)) + r.Heard(ctx, key(first, second, meshvaultclient.RotationReady)) reload(first) r.Heard(ctx, key(first, "", "")) if cur, _ := l.Current(); cur != first || !authenticates(srv, first) { diff --git a/internal/link/rotate_test.go b/internal/link/rotate_test.go index 0b3c1fc..ceb0191 100644 --- a/internal/link/rotate_test.go +++ b/internal/link/rotate_test.go @@ -5,12 +5,12 @@ 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/identity-and-access/go/clients/meshvaultclient" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "slices" "strings" "testing" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // rotRig is a Rotator over stand-ins: what the vault hands, what a test-dial answers, and what it was asked, in order. @@ -53,8 +53,8 @@ func newRotRig() *rotRig { return g } -func key(login, rotating, stage string) delivery.NodeLogin { - return delivery.NodeLogin{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)} +func key(login, rotating, stage string) meshvault.NodeLoginsByNodeValue { + return meshvault.NodeLoginsByNodeValue{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)} } // 774/S62/AC1 and AC2 (ADR 0583 §3): at ready the engine fetches the new value and test-dials it; one that does not @@ -63,18 +63,18 @@ func key(login, rotating, stage string) delivery.NodeLogin { func TestTheEngineSwitchesOnlyAfterATestDialAndConfirmsAsTheNewLogin(t *testing.T) { g := newRotRig() ctx := context.Background() - g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationBegun)) + g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationBegun)) if len(g.steps) != 0 { t.Fatalf("before ready: %v", g.steps) } g.dialErr = errors.New("authorization violation") - g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady)) + g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady)) if cur, _ := g.logins.Current(); cur != "node.ace" || !strings.Contains(strings.Join(g.said, "|"), "stays on node.ace") || slices.Contains(g.steps, "confirm as node.ace--b") { t.Fatalf("a test-dial that failed: %s, said %v, steps %v", cur, g.said, g.steps) } g.dialErr = nil - g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady)) + g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady)) cur, pw := g.logins.Current() if cur != "node.ace--b" || pw != "secret-of-node.ace--b" { t.Fatalf("after a good test-dial: %s %s", cur, pw) @@ -99,9 +99,9 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t * ctx := context.Background() for _, stage := range []string{"before confirming", "after confirming"} { g := newRotRig() - g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady)) + g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady)) if stage == "after confirming" { - g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationConfirmed)) + g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationConfirmed)) } if _, held := g.logins.Value("node.ace"); !held { t.Fatalf("%s: the old value was dropped before rotation-done", stage) @@ -121,7 +121,7 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t * } } g := newRotRig() - g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady)) + g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady)) g.r.Heard(ctx, key("node.ace--b", "", "")) // rotation-done: the vault retired the old login if _, held := g.logins.Value("node.ace"); held { t.Error("the old value is kept after rotation-done") @@ -131,7 +131,7 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t * } // Fallback: two failures with the new value (the bound) before rotation-done. f := newRotRig() - f.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady)) + f.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady)) f.logins.dialed("node.ace--b", errors.New("nats: authorization violation")) if cur, _ := f.logins.Current(); cur != "node.ace--b" { t.Fatalf("fell back after one failure: %s", cur) @@ -146,7 +146,7 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t * t.Errorf("the fall back is no health report's: %v", err) } steps := len(f.steps) - f.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady)) + f.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady)) if len(f.steps) != steps { t.Errorf("the rotation fallen back from was tried again: %v", f.steps[steps:]) } diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index cee73ed..3ba5644 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -21,6 +21,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/identity-and-access/go/clients/meshvaultclient" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" @@ -41,7 +42,6 @@ import ( "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/frombus" "github.com/novox/mesh-host/internal/compose/resolution" @@ -1360,7 +1360,7 @@ func (c *Composer) certifyIfWanted(ctx context.Context, r reader, p meshmoduleas return } trusted := []string{rec.Authority, r.VaultAuthority(ctx)} - if rec.Certificate != "" && delivery.CheckNodeCertificate(rec.Certificate, trusted, overlay.InternalName(c.env.Node), + if rec.Certificate != "" && meshvaultclient.CheckNodeCertificate(rec.Certificate, trusted, overlay.InternalName(c.env.Node), ed25519.PublicKey(key), time.Now()) == nil { return } diff --git a/internal/userlist/userlist.go b/internal/userlist/userlist.go index de4daa9..fd75b1f 100644 --- a/internal/userlist/userlist.go +++ b/internal/userlist/userlist.go @@ -32,16 +32,15 @@ import ( "encoding/json" "fmt" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" + "git.novox.be/novox/mesh-sdk/go/seal" + "github.com/novox/mesh-host/internal/agent" engcomp "github.com/novox/mesh-host/internal/compositions" "regexp" "sort" "strings" "sync" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/seal" - "github.com/novox/mesh-host/internal/agent" ) // ResourceID is the id the composer gives the bus's user list (mesh-sdk catalogue.BusUsersID); in a composition it may @@ -66,7 +65,7 @@ type Env struct { Write func(ctx context.Context, module string, resource json.RawMessage, body []byte) (digest string, changed bool, err error) // Report says what was written, in the module's have. Report func(ctx context.Context, module string, w Written) - // Grants watches the grants part's state (its key delivery.GrantsUserListKey); nil writes nothing. + // Grants watches the grants part's state (its key grantsstate.GrantsUserListKey); nil writes nothing. Grants agent.Watcher // Fetch reads an object of the grants part's bucket by its name; Open opens a box sealed to this node's key; Key is // this node's sealing key's public half (SPKI PEM), said in the have so the grants part seals to it ("" unknown). @@ -103,7 +102,7 @@ type Follower struct { mod string said map[string]bool // grants is the grants part's key as last heard (nil when none). - grants *delivery.GrantsUserList + grants *grantsstate.GrantsUserList grantsRev uint64 // unwatched is why the grants part's key is not watched now; empty while it is, or where it need not be. unwatched string @@ -124,7 +123,7 @@ func New(env Env) *Follower { said: map[string]bool{}} if env.Grants != nil && env.Kept != nil { if raw, rev := env.Kept(); raw != nil { - if g, err := delivery.DecodeGrantsUserList(raw); err == nil { + if g, err := grantsstate.DecodeGrantsUserList(raw); err == nil { f.grants, f.grantsRev = &g, rev } } @@ -180,7 +179,7 @@ func (f *Follower) Run(ctx context.Context) error { return } opened = true - ch, err := f.env.Grants.Watch(ctx, delivery.GrantsUserListKey) + ch, err := f.env.Grants.Watch(ctx, grantsstate.GrantsUserListKey) if err != nil { failed(err.Error()) return @@ -268,7 +267,7 @@ func (f *Follower) takeGrants(u agent.Update) { f.sayOnceLocked("the grants part's user-list key is deleted, so the list loaded is kept") return } - g, err := delivery.DecodeGrantsUserList(u.Value) + g, err := grantsstate.DecodeGrantsUserList(u.Value) if err != nil { f.sayOnceLocked("the grants part's user-list key cannot be read, so the list loaded is kept: " + err.Error()) return @@ -312,7 +311,7 @@ func reportable(prev, w Written) bool { // says why in the have. func (f *Follower) settleGrants(ctx context.Context, again bool) { f.mu.Lock() - var g delivery.GrantsUserList + var g grantsstate.GrantsUserList heard := f.grants != nil if heard { g = *f.grants @@ -343,8 +342,8 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) { } f.mu.Unlock() - w := Written{Source: delivery.GrantsUserListSource, Revision: int64(rev), Wanted: g.Digest, At: f.env.Now().UTC(), - Follows: delivery.FollowGrants, Key: f.key(), Unwatched: f.unwatchedNow()} + w := Written{Source: grantsstate.GrantsUserListSource, Revision: int64(rev), Wanted: g.Digest, At: f.env.Now().UTC(), + Follows: grantsstate.FollowGrants, Key: f.key(), Unwatched: f.unwatchedNow()} var err error var changed bool resource, rerr := mode0600(file.resource) @@ -357,14 +356,14 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) { err = fmt.Errorf("the grants part's key names no list yet; the list on the machine is kept") case f.env.Fetch == nil || f.env.Open == nil: err = fmt.Errorf("this engine cannot read or open the grants part's list") - case g.SealedTo != "" && w.Key != "" && g.SealedTo != delivery.SealingKeyID(w.Key): + case g.SealedTo != "" && w.Key != "" && g.SealedTo != grantsstate.SealingKeyID(w.Key): err = fmt.Errorf("the grants part's list is sealed to another key than this node's (%s); the list on the machine is kept until it seals to this one", g.SealedTo) } var body []byte if err == nil { var object []byte if object, err = f.env.Fetch(ctx, g.Object); err == nil { - body, err = delivery.OpenUserList(object, g.Digest, f.env.Open) + body, err = grantsstate.OpenUserList(object, g.Digest, f.env.Open) } if err != nil { err = fmt.Errorf("the grants part's list %s could not be had: %w", short(g.Digest), err) @@ -395,7 +394,7 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) { f.mod, f.state = file.module, w f.written = err == nil f.mu.Unlock() - at := fmt.Sprintf("%s@%d", delivery.GrantsUserListSource, rev) + at := fmt.Sprintf("%s@%d", grantsstate.GrantsUserListSource, rev) if err != nil { f.sayOnce(fmt.Sprintf("the bus's user list of %s could not be written: %v", at, err)) } else if changed { diff --git a/internal/userlist/userlist_test.go b/internal/userlist/userlist_test.go index 3fdecc0..f07ed61 100644 --- a/internal/userlist/userlist_test.go +++ b/internal/userlist/userlist_test.go @@ -5,15 +5,14 @@ import ( "encoding/json" "errors" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate" + "git.novox.be/novox/mesh-sdk/go/seal" + "github.com/novox/mesh-host/internal/agent" engcomp "github.com/novox/mesh-host/internal/compositions" "strings" "sync" "testing" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/seal" - "github.com/novox/mesh-host/internal/agent" ) const node = "novox" @@ -126,50 +125,50 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) { // this node runs declares; the list a composition carries is never written, whatever becomes of the grants key. // B17: the file's place is read from what runs, as the agent holds it, never from a composition bucket. {name: "the grants part's list is written into the file the bus module it runs declares", - before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three}}, - wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, + before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}}, + wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants}, {name: "a bus module it runs by digest still names the file", - before: []put{{module: "nats", fp: fpOf("a"), content: two, byDigest: true}}, grants: []gput{{follow: delivery.FollowGrants, content: three}}, - wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, + before: []put{{module: "nats", fp: fpOf("a"), content: two, byDigest: true}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}}, + wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants}, {name: "a newer composition's list is not written", before: []put{{module: "nats", fp: fpOf("a"), content: two}}, after: []put{{module: "nats", fp: fpOf("b"), content: five}}, - grants: []gput{{follow: delivery.FollowGrants, content: three}}, - wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, + grants: []gput{{follow: grantsstate.FollowGrants, content: three}}, + wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants}, {name: "a composition without the list is ignored", before: []put{{module: "web", fp: fpOf("a"), content: ""}}, - grants: []gput{{follow: delivery.FollowGrants, content: three}}}, + grants: []gput{{follow: grantsstate.FollowGrants, content: three}}}, {name: "another node's composition declares no file", - before: []put{{module: "nats", fp: fpOf("a"), content: two, asNode: "ace"}}, grants: []gput{{follow: delivery.FollowGrants, content: three}}}, + before: []put{{module: "nats", fp: fpOf("a"), content: two, asNode: "ace"}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}}}, {name: "an unreadable grants key with none heard before writes nothing, never the composition's list", before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{unreadable: true}}, - onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: delivery.GrantsUserListSource}, + onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: grantsstate.GrantsUserListSource}, {name: "a refused watch with no key kept writes nothing and says it is not watched", before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grantsRefused: true, wantUnwatched: true, - onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: delivery.GrantsUserListSource}, + onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: grantsstate.GrantsUserListSource}, {name: "a refused watch keeps the key last heard", before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grantsRefused: true, wantUnwatched: true, - kept: &gput{follow: delivery.FollowGrants, content: three}, - wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, + kept: &gput{follow: grantsstate.FollowGrants, content: three}, + wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants}, {name: "a grants key that turns unreadable keeps the loaded list", before: []put{{module: "nats", fp: fpOf("a"), content: two}}, - grants: []gput{{follow: delivery.FollowGrants, content: three}, {unreadable: true, after: true}}, - wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, + grants: []gput{{follow: grantsstate.FollowGrants, content: three}, {unreadable: true, after: true}}, + wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants}, {name: "a grants key that is deleted keeps the loaded list", before: []put{{module: "nats", fp: fpOf("a"), content: two}}, - grants: []gput{{follow: delivery.FollowGrants, content: three}, {deleted: true, after: true}}, - wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, + grants: []gput{{follow: grantsstate.FollowGrants, content: three}, {deleted: true, after: true}}, + wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants}, {name: "a grants list that drops more than half is refused and the file kept", - before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: one}}, - onDisk: five, wantUsers: 2, wantErr: "drops more than half", wantRefused: true, wantSource: delivery.GrantsUserListSource}, + before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: grantsstate.FollowGrants, content: one}}, + onDisk: five, wantUsers: 2, wantErr: "drops more than half", wantRefused: true, wantSource: grantsstate.GrantsUserListSource}, {name: "a failed write is said and kept", before: []put{{module: "nats", fp: fpOf("a"), content: two}}, - grants: []gput{{follow: delivery.FollowGrants, content: three}}, failWrite: true, - wantWrites: []string{three}, wantUsers: 4, wantErr: "disk full", wantSource: delivery.GrantsUserListSource}, + grants: []gput{{follow: grantsstate.FollowGrants, content: three}}, failWrite: true, + wantWrites: []string{three}, wantUsers: 4, wantErr: "disk full", wantSource: grantsstate.GrantsUserListSource}, {name: "a grants list that does not open to its digest is refused and the file kept", - before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three, badDigest: true}}, - onDisk: two, wantErr: "could not be had", wantRefused: true, wantSource: delivery.GrantsUserListSource}, + before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three, badDigest: true}}, + onDisk: two, wantErr: "could not be had", wantRefused: true, wantSource: grantsstate.GrantsUserListSource}, {name: "a grants list sealed to another key is refused and the file kept", - before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three, otherKey: true}}, - onDisk: two, wantErr: "sealed to another key", wantRefused: true, wantSource: delivery.GrantsUserListSource}, + before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three, otherKey: true}}, + onDisk: two, wantErr: "sealed to another key", wantRefused: true, wantSource: grantsstate.GrantsUserListSource}, } { t.Run(c.name, func(t *testing.T) { var mu sync.Mutex @@ -194,7 +193,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) { } for i, g := range gs { if g.unreadable || g.deleted { - u := agent.Update{Key: delivery.GrantsUserListKey, Value: []byte("not a user-list key"), Revision: uint64(i + 1), Deleted: g.deleted} + u := agent.Update{Key: grantsstate.GrantsUserListKey, Value: []byte("not a user-list key"), Revision: uint64(i + 1), Deleted: g.deleted} grantsAfter = append(grantsAfter, u) continue } @@ -203,21 +202,21 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) { other, _ := seal.GenerateKeyPair() to = other.PublicKey } - digest := delivery.UserListDigest([]byte(g.content)) + digest := grantsstate.UserListDigest([]byte(g.content)) if g.badDigest { - digest = delivery.UserListDigest([]byte("another list")) + digest = grantsstate.UserListDigest([]byte("another list")) } - object, err := delivery.SealUserList([]byte(g.content), to) + object, err := grantsstate.SealUserList([]byte(g.content), to) if err != nil { t.Fatal(err) } - sealed[delivery.UserListObjectName(digest)] = object - raw, err := delivery.EncodeGrantsUserList(delivery.GrantsUserList{Follow: g.follow, Digest: digest, - Object: delivery.UserListObjectName(digest), SealedTo: delivery.SealingKeyID(to), Node: node, Revision: uint64(i + 1)}) + sealed[grantsstate.UserListObjectName(digest)] = object + raw, err := grantsstate.EncodeGrantsUserList(grantsstate.GrantsUserList{Follow: g.follow, Digest: digest, + Object: grantsstate.UserListObjectName(digest), SealedTo: grantsstate.SealingKeyID(to), Node: node, Revision: uint64(i + 1)}) if err != nil { t.Fatal(err) } - u := agent.Update{Key: delivery.GrantsUserListKey, Value: raw, Revision: uint64(i + 1)} + u := agent.Update{Key: grantsstate.GrantsUserListKey, Value: raw, Revision: uint64(i + 1)} switch { case c.kept != nil && i == len(gs)-1: keptRaw = raw @@ -296,7 +295,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) { } if len(grantsAfter) > 0 { for until := time.Now().Add(5 * time.Second); ; time.Sleep(5 * time.Millisecond) { - if _, st := fl.State(); st.Source == delivery.GrantsUserListSource || time.Now().After(until) { + if _, st := fl.State(); st.Source == grantsstate.GrantsUserListSource || time.Now().After(until) { break } } @@ -355,11 +354,11 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) { t.Fatalf("an apply while following %s holds the composition's list: %v", c.wantFollows, hasList) } // A list on the machine that cannot be read stops the apply; it is never left out to be removed. - if c.wantFollows == delivery.FollowGrants { + if c.wantFollows == grantsstate.FollowGrants { readErr := New(Env{Node: node, OnDisk: func(json.RawMessage) ([]byte, error) { return nil, errors.New("permission denied") }}) - readErr.grants = &delivery.GrantsUserList{Follow: delivery.FollowGrants} + readErr.grants = &grantsstate.GrantsUserList{Follow: grantsstate.FollowGrants} if _, err := readErr.Hold(cmp); err == nil || !strings.Contains(err.Error(), "could not be read") { t.Fatalf("an unreadable list on the machine was held as %v", err) }