From 5906e18028442adc25cc9e496ffbe71520b718a6 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:10:45 +0100 Subject: [PATCH] =?UTF-8?q?Node-engine:=20a=20composition,=20its=20codec,?= =?UTF-8?q?=20digest=20and=20keys=20come=20from=20the=20node-engine=20clie?= =?UTF-8?q?nt;=20the=20candidate=20answer=20and=20its=20limits=20from=20th?= =?UTF-8?q?e=20seat's=20contract;=20the=20declared=20packages=20are=20the?= =?UTF-8?q?=20engine's=20own=20(H-2,=20H-3,=20ADR=200634=20=C2=A71;=20issu?= =?UTF-8?q?e=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine's Composed keeps the catalogue's Plan and the declaration types and goes through nodeengineclient's codec; the fingerprint key part is meshcatalogclient's. The candidate answer is nodeengine.CandidateAnswer; the have still holds it through an interim JSON bridge (link.interimAnswers) until the Have group moves. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- cmd/mesh-compose/main_test.go | 4 +- cmd/mesh-host/authority.go | 4 +- cmd/mesh-host/authority_test.go | 5 +- cmd/mesh-host/empty_write_test.go | 7 +- cmd/mesh-host/file_references_test.go | 4 +- cmd/mesh-host/inactive.go | 6 +- cmd/mesh-host/inactive_test.go | 4 +- cmd/mesh-host/nodeagent.go | 41 +-- cmd/mesh-host/nodeagent_integration_test.go | 27 +- cmd/mesh-host/recorded_test.go | 7 +- cmd/mesh-host/runs_as_written.go | 6 +- cmd/mesh-host/seatlist.go | 2 +- cmd/mesh-host/userlist.go | 3 +- cmd/mesh-host/whatruns_test.go | 25 +- go.mod | 3 + internal/agent/agent.go | 60 ++--- internal/agent/agent_test.go | 82 +++--- internal/agent/composition.go | 13 +- internal/compose/composed.go | 29 +- internal/compose/composed_test.go | 29 +- internal/compose/frombus/answer.go | 43 +-- internal/compose/frombus/answer_rows_test.go | 56 ++-- internal/compose/frombus/frombus.go | 17 +- internal/compose/frombus/frombus_test.go | 59 ++-- .../frombus/frombustest/frombustest_test.go | 9 +- internal/compose/frombus/held_rows_test.go | 11 +- internal/compose/frombus/keep.go | 4 +- internal/compose/input.go | 6 +- internal/composer/composer_test.go | 3 +- internal/compositions/answer_paths.go | 7 +- internal/compositions/candidate.go | 53 ++++ internal/compositions/compare_test.go | 39 ++- internal/compositions/composed.go | 251 ++++++++++++++++++ internal/compositions/composed_compare.go | 54 ++-- internal/compositions/composed_seals.go | 5 +- internal/compositions/packages.go | 84 ++++++ internal/judge/aliases.go | 14 +- internal/judge/answer_test.go | 5 +- internal/link/composedstate.go | 6 +- internal/link/enginepackages.go | 17 +- internal/link/have.go | 25 +- internal/link/have_test.go | 3 +- internal/selfcompose/candidate.go | 13 +- internal/selfcompose/candidate_test.go | 2 +- internal/selfcompose/composedstate.go | 31 +-- internal/selfcompose/composedstate_test.go | 9 +- internal/selfcompose/data.go | 3 +- internal/selfcompose/data_test.go | 17 +- internal/selfcompose/packages.go | 17 +- internal/selfcompose/selfcompose.go | 35 +-- internal/selfcompose/selfcompose_test.go | 125 ++++----- internal/selfcompose/shadow.go | 43 +-- internal/userlist/userlist.go | 7 +- internal/userlist/userlist_test.go | 13 +- 54 files changed, 949 insertions(+), 498 deletions(-) create mode 100644 internal/compositions/candidate.go create mode 100644 internal/compositions/composed.go create mode 100644 internal/compositions/packages.go diff --git a/cmd/mesh-compose/main_test.go b/cmd/mesh-compose/main_test.go index 0b7de35..f70cf66 100644 --- a/cmd/mesh-compose/main_test.go +++ b/cmd/mesh-compose/main_test.go @@ -280,7 +280,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 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 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 +292,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, "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, "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/authority.go b/cmd/mesh-host/authority.go index 25f0903..dd9e3c6 100644 --- a/cmd/mesh-host/authority.go +++ b/cmd/mesh-host/authority.go @@ -2,12 +2,12 @@ package main import ( "encoding/json" + engcomp "github.com/novox/mesh-host/internal/compositions" "os" "path/filepath" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // authorityPath is where this engine keeps the mesh's own certificate as it last applied it (public): beside the @@ -17,7 +17,7 @@ func authorityPath(state string) string { return filepath.Join(filepath.Dir(stat // keepAuthority keeps the mesh's own certificate a composition carries (the resource a module's certificate.authority // asks for, catalogue.AuthorityID), so the record says the authority this machine holds. A composition carrying none // changes nothing. -func keepAuthority(state string, c delivery.Composed) error { +func keepAuthority(state string, c engcomp.Composed) error { for _, raw := range c.Resources { var r struct { ID string `json:"id"` diff --git a/cmd/mesh-host/authority_test.go b/cmd/mesh-host/authority_test.go index 7ff84f8..09f7333 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" + engcomp "github.com/novox/mesh-host/internal/compositions" "os" "path/filepath" "strings" @@ -29,13 +30,13 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) { raw, _ := json.Marshal(map[string]any{"id": id, "type": "file", "path": "/etc/x", "content": content}) return raw } - if err := keepAuthority(state, delivery.Composed{Resources: []json.RawMessage{res("web.conf", "x")}}); err != nil { + if err := keepAuthority(state, engcomp.Composed{Resources: []json.RawMessage{res("web.conf", "x")}}); err != nil { t.Fatal(err) } if _, err := os.Stat(authorityPath(state)); !os.IsNotExist(err) { t.Fatalf("a composition with no authority kept one: %v", err) } - if err := keepAuthority(state, delivery.Composed{Resources: []json.RawMessage{res("ca-trust.certificate-authority", "CA-1")}}); err != nil { + if err := keepAuthority(state, engcomp.Composed{Resources: []json.RawMessage{res("ca-trust.certificate-authority", "CA-1")}}); err != nil { t.Fatal(err) } if got := recordKeys(state, nil)(); got.Authority != "CA-1" { diff --git a/cmd/mesh-host/empty_write_test.go b/cmd/mesh-host/empty_write_test.go index 80a7f76..cac07d5 100644 --- a/cmd/mesh-host/empty_write_test.go +++ b/cmd/mesh-host/empty_write_test.go @@ -2,10 +2,9 @@ package main import ( "encoding/json" + engcomp "github.com/novox/mesh-host/internal/compositions" "strings" "testing" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // A composition that names a file the provisioner fills without saying it is filled would have this node-engine write @@ -13,7 +12,7 @@ import ( // when ace, g14 and shanks wrote claude-code's, node-env's and power's files empty). func TestAFileItCannotProduceIsNeverWrittenEmpty(t *testing.T) { unit := json.RawMessage(`{"id":"power.after-boot-unit","type":"file","path":"/etc/systemd/system/mesh-power-after-boot.service","mode":"0644","owner-at-runtime":"owned"}`) - c := delivery.Composed{Node: "ace", Resources: []json.RawMessage{unit}} + c := engcomp.Composed{Node: "ace", Resources: []json.RawMessage{unit}} d, err := declarationOf(c) if err != nil { t.Fatal(err) @@ -25,7 +24,7 @@ func TestAFileItCannotProduceIsNeverWrittenEmpty(t *testing.T) { if d, _ = declarationOf(c); refuseEmptyWrites(d) != nil { t.Fatalf("a filled file was refused: %v", refuseEmptyWrites(d)) } - plain := delivery.Composed{Node: "ace", Resources: []json.RawMessage{json.RawMessage( + plain := engcomp.Composed{Node: "ace", Resources: []json.RawMessage{json.RawMessage( `{"id":"x.f","type":"file","path":"/etc/x","mode":"0644","content":"x"}`)}} if d, _ = declarationOf(plain); refuseEmptyWrites(d) != nil { t.Fatalf("a file with its content was refused: %v", refuseEmptyWrites(d)) diff --git a/cmd/mesh-host/file_references_test.go b/cmd/mesh-host/file_references_test.go index 0be46f6..891dd4f 100644 --- a/cmd/mesh-host/file_references_test.go +++ b/cmd/mesh-host/file_references_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "os" "path/filepath" "strconv" @@ -11,7 +12,6 @@ import ( "testing" "git.novox.be/novox/mesh-sdk/go/declaration" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" "github.com/novox/mesh-host/internal/link" ) @@ -140,7 +140,7 @@ func TestADeclarationWithAFileByDigestAppliesFromTheObject(t *testing.T) { t.Fatal(err) } } - runs := runsAsWritten([]delivery.Composed{{Node: "n", Module: "m", Resources: []json.RawMessage{stale}}}, opts.state) + runs := runsAsWritten([]engcomp.Composed{{Node: "n", Module: "m", Resources: []json.RawMessage{stale}}}, opts.state) var got struct { Digest string `json:"digest"` } diff --git a/cmd/mesh-host/inactive.go b/cmd/mesh-host/inactive.go index 2d19e98..3113026 100644 --- a/cmd/mesh-host/inactive.go +++ b/cmd/mesh-host/inactive.go @@ -2,9 +2,9 @@ package main import ( "context" + engcomp "github.com/novox/mesh-host/internal/compositions" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/apply" "github.com/novox/mesh-host/internal/liveness" ) @@ -26,8 +26,8 @@ func heldBy(windows *apply.Windows) func(time.Time) map[string]bool { // long-running resource the composition declares running not active, read from the service manager and the container // runtime and from no record. A resource held by an open maintenance window, or waiting for a library, is not counted; // one that cannot be read is not counted either (unknown is not inactive), and neither is one the manager is restarting. -func inactiveIn(rt liveness.Runtime, held func(time.Time) map[string]bool) func(context.Context, delivery.Composed) bool { - return func(ctx context.Context, c delivery.Composed) bool { +func inactiveIn(rt liveness.Runtime, held func(time.Time) map[string]bool) func(context.Context, engcomp.Composed) bool { + return func(ctx context.Context, c engcomp.Composed) bool { d, err := declarationOf(c) if err != nil { return false diff --git a/cmd/mesh-host/inactive_test.go b/cmd/mesh-host/inactive_test.go index 24fb726..802fdfc 100644 --- a/cmd/mesh-host/inactive_test.go +++ b/cmd/mesh-host/inactive_test.go @@ -4,9 +4,9 @@ import ( "context" "encoding/json" "errors" + engcomp "github.com/novox/mesh-host/internal/compositions" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/liveness" ) @@ -77,7 +77,7 @@ func testTheMachineShowsAResourceNotActive(t *testing.T) { {"unread", fakeRuntime{err: errors.New("the service manager could not be read")}, nil, false}, } { t.Run(c.name, func(t *testing.T) { - d, err := declarationOf(delivery.Composed{Node: "n1", Module: "dnsmasq", Resources: []json.RawMessage{svc, ctr}}) + d, err := declarationOf(engcomp.Composed{Node: "n1", Module: "dnsmasq", Resources: []json.RawMessage{svc, ctr}}) if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index 30d4c74..395d6a9 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "github.com/novox/mesh-host/internal/compositions" "os" "path/filepath" @@ -256,16 +257,16 @@ func awaitBackup(ctx context.Context, events <-chan []byte, node, module string, // the controller put modules it knew no fingerprint of the new form for into the node's own composition while their // wants named their own, and the declaration joined of both declared each such file twice, which the filling refuses // ("2 resources declare X") on every node. The node's own copy is left out, and which were left out is said once. -func declarationOf(cs ...delivery.Composed) (*declaration.Declaration, error) { +func declarationOf(cs ...compositions.Composed) (*declaration.Declaration, error) { d := &declaration.Declaration{Version: declaration.Version} - var modules []delivery.Composed + var modules []compositions.Composed for _, c := range cs { if c.Module != "" { modules = append(modules, c) } } if len(modules) > 0 { - joined := make([]delivery.Composed, 0, len(cs)) + joined := make([]compositions.Composed, 0, len(cs)) for _, c := range cs { if c.Module == "" { var dropped []string @@ -300,7 +301,7 @@ func declarationOf(cs ...delivery.Composed) (*declaration.Declaration, error) { // the same kind and id. A resource that shares only its target (a unit or container of the same name, another id) is // not the module's, and stays: dropped, its record would be an orphan the apply removes, stopping the module's unit. It // answers what it left out, as " ()". -func ownWithout(own delivery.Composed, modules []delivery.Composed) (delivery.Composed, []string) { +func ownWithout(own compositions.Composed, modules []compositions.Composed) (compositions.Composed, []string) { byID := map[string]string{} for _, c := range modules { if c.Module == "" { @@ -356,14 +357,14 @@ func sayDroppedOnce(dropped []string) { "declares, and they are left out of it: %s (issue 1945)\n", len(dropped), key) } -func scopeName(c delivery.Composed) string { +func scopeName(c compositions.Composed) string { if c.Module == "" { return agent.NodeModule } return c.Module } -func resourcesOf(cs []delivery.Composed) []declaration.Resource { +func resourcesOf(cs []compositions.Composed) []declaration.Resource { var out []declaration.Resource for _, c := range cs { for _, raw := range c.Resources { @@ -445,7 +446,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus } began := time.Now() node := session.Membership.Node - composedBucket, err := delivery.ComposedBucketFor(node) + composedBucket, err := compositions.BucketFor(node) if err != nil { cancel() say("the node agent cannot name this node's composition bucket: " + err.Error()) @@ -469,7 +470,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus // composes its own (ADR 0555 §3): without it a module that asks for a certificate would be stood in for. if byModule, _ := kept(); byModule != nil { for module, resources := range byModule { - if err := keepAuthority(opts.state, delivery.Composed{Module: module, Resources: resources}); err != nil { + if err := keepAuthority(opts.state, compositions.Composed{Module: module, Resources: resources}); err != nil { say("the mesh's certificate the kept declaration carries could not be kept for this machine's record: " + err.Error()) } } @@ -478,7 +479,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus // bus module this node runs declares, as its agent holds what runs (its own composition, or the declaration kept // before the switch): never read from the node's composition bucket, which the controller wrote. On every node; // a node that runs no list writes nothing. Looked at again whenever what runs changed (RunsChanged, Settled). - users := userListFollower(opts, session, func() []delivery.Composed { + users := userListFollower(opts, session, func() []compositions.Composed { if now := current.get(); now != nil { return now.Everything() } @@ -545,7 +546,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus Say: say, // A failure of the bus not answering for a moment is tried again: 2 s, 5 s, 15 s, 60 s, then every 5 min. Backoff: link.Backoff, - Apply: func(ctx context.Context, c delivery.Composed, others []delivery.Composed) agent.Result { + Apply: func(ctx context.Context, c compositions.Composed, others []compositions.Composed) agent.Result { actx, done := applyContext(session.WithLink(ctx)) // While the grants part's list is followed, the controller's list in the bus module's composition is not // written: the apply keeps the list on the machine (ADR 0547 clause 8, never both). @@ -571,7 +572,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus sayFacts(ctx, ag) return res }, - Remove: func(ctx context.Context, module string, others []delivery.Composed) agent.Result { + Remove: func(ctx context.Context, module string, others []compositions.Composed) agent.Result { actx, done := applyContext(session.WithLink(ctx)) res := removeComposition(actx, opts, module, others, ag.Placed(module), sched, say) done() @@ -595,7 +596,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus } hooks := composerHooks{ seatList: seats, - running: func() []delivery.Composed { + running: func() []compositions.Composed { if now := own.get(); now != nil { return runsAsWritten(now.Everything(), opts.state) } @@ -639,8 +640,8 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus shadowHooks.refused, shadowHooks.kept, shadowHooks.certify, shadowHooks.seatList = nil, nil, nil, nil shadowEnv := composerEnv(session, node, say, shadowHooks) shadow := selfcompose.NewShadow(shadowEnv, - func(ctx context.Context, answers map[string]delivery.CandidateAnswer) { - session.NoteCandidates(ctx, delivery.WithValuesDiffer(answers)) + func(ctx context.Context, answers map[string]nodeengine.CandidateAnswer) { + session.NoteCandidates(ctx, compositions.WithValuesDiffer(answers)) }) go shadow.Keep(run, link.Backoff) // this run's alone: a switch begins a run, and its shadow, again shadowNow.Store(shadow) // what the verb compose-candidate composes a check's candidate with @@ -712,7 +713,7 @@ func (c *agentNow) get() *agent.Agent { // // removed is the modules whose want says removed: in the node's own apply, a resource of any other module goes only when // its module is among them (issue 774). -func applyComposition(ctx context.Context, opts options, c delivery.Composed, others []delivery.Composed, placed, removed map[string]bool, +func applyComposition(ctx context.Context, opts options, c compositions.Composed, others []compositions.Composed, placed, removed map[string]bool, sched *apply.Scheduler, say link.Announce) agent.Result { name := scopeName(c) if c.Module == "" { @@ -760,7 +761,7 @@ func applyComposition(ctx context.Context, opts options, c delivery.Composed, ot } // removeComposition removes one module's own resources and nothing of another's. -func removeComposition(ctx context.Context, opts options, module string, others []delivery.Composed, placed map[string]bool, +func removeComposition(ctx context.Context, opts options, module string, others []compositions.Composed, placed map[string]bool, sched *apply.Scheduler, say link.Announce) agent.Result { scope := apply.Scope{Module: module, Others: resourcesOf(others), Placed: placed} got := applyModuleHeld(ctx, opts, &declaration.Declaration{Version: declaration.Version}, &scope, sched, say) @@ -833,7 +834,7 @@ func keptDeclaration(statePath string, signer ed25519.PublicKey) (*declaration.D // (adoption, identities, groups, libraries) from the node's composition once the agent holds it and from the kept // declaration until then. What the node judges its health by, and what the filling, the take act and the judgement of // directories read. -func whatRuns(everything []delivery.Composed, holdsNode bool, kept func() (*declaration.Declaration, error)) (*declaration.Declaration, error) { +func whatRuns(everything []compositions.Composed, holdsNode bool, kept func() (*declaration.Declaration, error)) (*declaration.Declaration, error) { d, err := declarationOf(everything...) if err != nil { return nil, err @@ -919,8 +920,8 @@ func refuseEmptyWrites(d *declaration.Declaration) error { // recordedIn says the engine's applied state holds a record of every resource of a composition: what the agent believes // of a have or the kept declaration only when it does, so a resource removed since is made again (issue 1945). A state // that cannot be read says yes: the agent then believes what it was told, as before. -func recordedIn(statePath string, say func(string)) func(ctx context.Context, c delivery.Composed) bool { - return func(_ context.Context, c delivery.Composed) bool { +func recordedIn(statePath string, say func(string)) func(ctx context.Context, c compositions.Composed) bool { + return func(_ context.Context, c compositions.Composed) bool { known, err := store.Load(statePath) if err != nil { if say != nil { @@ -982,7 +983,7 @@ func applyContext(ctx context.Context) (context.Context, context.CancelFunc) { // composerHooks are what the composer reads of the agent and the machine (selfcompose.Env Running, Unseal, Refused). type composerHooks struct { - running func() []delivery.Composed + running func() []compositions.Composed unseal func(string) ([]byte, error) refused func(why string, held map[string]string) kept func(kept map[string][]string) diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index a175086..1d4d9d3 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -7,6 +7,8 @@ import ( "crypto/ed25519" "encoding/json" "errors" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + engcomp "github.com/novox/mesh-host/internal/compositions" "os" "path/filepath" "strings" @@ -62,7 +64,7 @@ type itBed struct { seatSilent atomic.Bool // seatAsked counts the node's asks of the stand-in seat, answered or refused: the node reached the bus and asked. seatAsked atomic.Int32 - findings nats.KeyValue + findings nats.KeyValue } // aServer is an embedded bus with JetStream and two users: the test's, unrestricted, and the node's, granted exactly what @@ -128,12 +130,12 @@ func newItBedAs(t *testing.T, perms nodegrant.Permissions) *itBed { if b.nodeJS, err = b.nodeConn.JetStream(); err != nil { t.Fatal(err) } - composedBucket, _ := delivery.ComposedBucketFor(itNode) + composedBucket, _ := engcomp.BucketFor(itNode) for _, c := range []struct { name string into *nats.KeyValue history uint8 - }{{delivery.AssignerWantsBucket, &b.wants, 8}, {composedBucket, &b.composed, delivery.ComposedHistory}, + }{{delivery.AssignerWantsBucket, &b.wants, 8}, {composedBucket, &b.composed, engcomp.History}, {delivery.HaveBucket, &b.haves, delivery.HaveHistory}} { kv, err := b.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: c.name, History: c.history}) if err != nil { @@ -255,7 +257,7 @@ func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) { within(t, 5*time.Second, "the node did not compose and apply its own module once placed", func() bool { return b.file("web") == "greeting=bonjour\n" }) // ADR 0565 §6 (1953): the node's engine writes its own key of the state packages, under the grant the grants part // composes for it, with each package its compositions declare and the module declaring it; once, while nothing changes. - wantPkgs := `{"node":"` + itNode + `","packages":[{"package":"web-tools","modules":["web"],"absent":false}]}` + wantPkgs := `{"node":"` + itNode + `","packages":[{"absent":false,"modules":["web"],"package":"web-tools"}]}` within(t, 5*time.Second, "the node did not write its key of the state packages", func() bool { e, err := st.packages.Get(itNode) return err == nil && string(e.Value()) == wantPkgs @@ -353,7 +355,7 @@ func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValu } within(t, 10*time.Second, "the node did not refuse a candidate taking its placement away", func() bool { a := node().Candidates["revert-x"] - return a.Proposal == "revert-x" && a.Returns == "" && a.Verdict == delivery.CandidateRefused && + return a.Proposal == "revert-x" && a.Returns == "" && a.Verdict == nodeengine.CandidateAnswerVerdictRefused && strings.Contains(a.Refused, "composes nothing") }) ran := b.have("web").Composed @@ -506,17 +508,17 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str // ADR 0593 §6: the node publishes what it composed in its composition state, under its own grant: web at the build // its want names, with the digest its have names (the plan left out of it), and the node's own. first := b.have("web").Composed - webKey, _ := delivery.ComposedKey(itNode, "web", fp) + webKey, _ := engcomp.Key(itNode, "web", fp) within(t, 5*time.Second, "the node did not publish web's composition in its composition state", func() bool { e, err := b.composed.Get(webKey) if err != nil { return false } - c, err := delivery.DecodeComposed(e.Value()) + c, err := engcomp.Decode(e.Value()) if err != nil { return false } - d, err := delivery.ComposedDigestOf(c) + d, err := engcomp.DigestOf(c) return err == nil && d == first }) if _, err := resolved.Put(itNode+".web.greeting", []byte(`{"value":"salut","source":"module"}`)); err != nil { @@ -543,17 +545,18 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str if err != nil { t.Fatal(err) } - var answer delivery.CandidateAnswer + var answer nodeengine.CandidateAnswer within(t, 10*time.Second, "the node did not answer its candidate", func() bool { e, err := b.haves.Get(hkey) if err != nil { return false } n, err := delivery.DecodeHaveNode(e.Value()) - answer = n.Candidates["feat-x"] + wire, _ := json.Marshal(n.Candidates["feat-x"]) + _ = json.Unmarshal(wire, &answer) return err == nil && answer.Proposal == "feat-x" && n.Record != nil }) - if answer.Verdict != delivery.CandidateSame || answer.Revision != crev || len(answer.Same) != 1 || answer.Same[0] != "web" { + if answer.Verdict != nodeengine.CandidateAnswerVerdictSame || answer.Revision != int64(crev) || len(answer.Same) != 1 || answer.Same[0] != "web" { t.Errorf("the answer to a candidate equal to the placement: %+v", answer) } if h := b.have("web"); h.Composed != second || b.file("web") != "greeting=salut\n" { @@ -631,7 +634,7 @@ func h8candidate(t *testing.T, b *itBed, fp, running string) { func mustComposedKey(t *testing.T, module, fp string) string { t.Helper() - key, err := delivery.ComposedKey(itNode, module, fp) + key, err := engcomp.Key(itNode, module, fp) if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-host/recorded_test.go b/cmd/mesh-host/recorded_test.go index a1f4476..849e8e9 100644 --- a/cmd/mesh-host/recorded_test.go +++ b/cmd/mesh-host/recorded_test.go @@ -3,11 +3,10 @@ package main import ( "context" "encoding/json" + engcomp "github.com/novox/mesh-host/internal/compositions" "path/filepath" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/novox/mesh-host/internal/liveness" "github.com/novox/mesh-host/internal/store" ) @@ -21,7 +20,7 @@ func TestACompositionIsRecordedOnlyWhenEveryResourceIs(t *testing.T) { if err := store.Save(path, s); err != nil { t.Fatal(err) } - c := delivery.Composed{Node: "ace", Module: "ca-trust", Resources: []json.RawMessage{ + c := engcomp.Composed{Node: "ace", Module: "ca-trust", Resources: []json.RawMessage{ json.RawMessage(`{"id":"ca-trust.trust","type":"file","module":"ca-trust","path":"/etc/ca","mode":"0644","content":"x"}`)}} recorded := recordedIn(path, func(string) {}) if !recorded(context.Background(), c) { @@ -87,7 +86,7 @@ func TestAFilledFileDoesNotMakeItsCompositionUnrecorded(t *testing.T) { if err := store.Save(path, s); err != nil { t.Fatal(err) } - c := delivery.Composed{Node: "ace", Module: "ca-trust", Filled: []string{"/etc/ca-filled"}, Resources: []json.RawMessage{ + c := engcomp.Composed{Node: "ace", Module: "ca-trust", Filled: []string{"/etc/ca-filled"}, Resources: []json.RawMessage{ json.RawMessage(`{"id":"ca-trust.trust","type":"file","module":"ca-trust","path":"/etc/ca","mode":"0644","content":"x"}`), json.RawMessage(`{"id":"ca-trust.filled","type":"file","module":"ca-trust","path":"/etc/ca-filled","mode":"0644","owner-at-runtime":"owned"}`)}} if !recordedIn(path, func(string) {})(context.Background(), c) { diff --git a/cmd/mesh-host/runs_as_written.go b/cmd/mesh-host/runs_as_written.go index a2bca2a..8de19dd 100644 --- a/cmd/mesh-host/runs_as_written.go +++ b/cmd/mesh-host/runs_as_written.go @@ -4,10 +4,10 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + engcomp "github.com/novox/mesh-host/internal/compositions" "os" "git.novox.be/novox/mesh-sdk/go/declaration" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/store" ) @@ -20,7 +20,7 @@ import ( // names what is on disk: **only when the node's record says it wrote exactly that content there** (store Applied.Wrote, // Target), never a file someone else changed. A file that cannot be read, or that the record does not vouch for, is // left as the composition names it. -func runsAsWritten(running []delivery.Composed, statePath string) []delivery.Composed { +func runsAsWritten(running []engcomp.Composed, statePath string) []engcomp.Composed { if len(running) == 0 || statePath == "" { return running } @@ -28,7 +28,7 @@ func runsAsWritten(running []delivery.Composed, statePath string) []delivery.Com if err != nil { return running } - out := make([]delivery.Composed, len(running)) + out := make([]engcomp.Composed, len(running)) for i, c := range running { out[i] = c var changed []json.RawMessage diff --git a/cmd/mesh-host/seatlist.go b/cmd/mesh-host/seatlist.go index 6ad0ce2..f9b8072 100644 --- a/cmd/mesh-host/seatlist.go +++ b/cmd/mesh-host/seatlist.go @@ -110,7 +110,7 @@ func (w findingsOfTheNode) encode(f views.Finding) (string, []byte, error) { key := conditions.Key(conditions.ScopeMachine, f.ID, f.Kind) v := condseat.FindingsByKeyValue{V: 1, Key: key, Kind: f.Kind, Observer: f.Observer, Severity: conditions.Warning, Subject: condseat.Subject{Scope: conditions.ScopeMachine, ID: f.ID, Machine: f.ID}, Summary: f.Summary, - Headline: f.Headline, Explanation: f.Explanation, Source: "seat-list-unread", At: f.At.UTC().Format(time.RFC3339)} + Headline: f.Headline, Explanation: f.Explanation, Source: "seat-list-unread", At: f.At.UTC()} if err := conditions.CheckFinding(v); err != nil { return "", nil, err } diff --git a/cmd/mesh-host/userlist.go b/cmd/mesh-host/userlist.go index 54cb6f1..8f531fb 100644 --- a/cmd/mesh-host/userlist.go +++ b/cmd/mesh-host/userlist.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "os" "path/filepath" @@ -25,7 +26,7 @@ import ( // userListFollower is the follower of the bus's user list for one link: the node's composition bucket, the keys the // agent trusts, the write alone, and the have. -func userListFollower(opts options, session *link.Session, running func() []delivery.Composed, +func userListFollower(opts options, session *link.Session, running func() []engcomp.Composed, say link.Announce, wrote func()) *userlist.Follower { return userlist.New(userlist.Env{ Node: session.Membership.Node, diff --git a/cmd/mesh-host/whatruns_test.go b/cmd/mesh-host/whatruns_test.go index c756df7..bfbb992 100644 --- a/cmd/mesh-host/whatruns_test.go +++ b/cmd/mesh-host/whatruns_test.go @@ -5,6 +5,7 @@ import ( "crypto/ed25519" "encoding/json" "errors" + engcomp "github.com/novox/mesh-host/internal/compositions" "path/filepath" "sort" "strings" @@ -31,7 +32,7 @@ func aFileOf(module, id string) json.RawMessage { func TestWhatRunsIsTheAgentsCompositionsAndTheKeptRest(t *testing.T) { kept := &declaration.Declaration{Identities: []declaration.Identity{{Module: "db", Name: "mesh-db"}}, Libraries: []declaration.Library{{Library: "media"}}} - everything := []delivery.Composed{ + everything := []engcomp.Composed{ {Node: "n1", Module: "web", Resources: []json.RawMessage{aFileOf("web", "a")}}, {Node: "n1", Module: "db", Resources: []json.RawMessage{aFileOf("db", "a")}}, // still the kept declaration's } @@ -45,7 +46,7 @@ func TestWhatRunsIsTheAgentsCompositionsAndTheKeptRest(t *testing.T) { } // Once the agent holds the node's composition, its parts are the node's. - node := delivery.Composed{Node: "n1", Own: delivery.NodeOwn{Identities: []declaration.Identity{{Module: "web", Name: "mesh-web"}}}} + node := engcomp.Composed{Node: "n1", Own: engcomp.NodeOwn{Identities: []declaration.Identity{{Module: "web", Name: "mesh-web"}}}} d, err = whatRuns(append(everything, node), true, func() (*declaration.Declaration, error) { return nil, errors.New("not read") }) if err != nil { t.Fatal(err) @@ -88,7 +89,7 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) { for _, name := range row.runs { raws = append(raws, process(name)) } - runs, err := whatRuns([]delivery.Composed{{Node: "n1", Module: "restic", Resources: raws}}, false, + runs, err := whatRuns([]engcomp.Composed{{Node: "n1", Module: "restic", Resources: raws}}, false, func() (*declaration.Declaration, error) { return nil, errors.New("none kept") }) if err != nil { t.Fatal(err) @@ -161,9 +162,9 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) { // the module's: the declaration joined of both names each file once, so the filling writes it; the node's own resources // no module declares stay. (Live from 2026-10-25 12:43: "2 resources declare X" on every node.) func TestTheNodesOwnCopyOfAModulesResourceIsLeftOut(t *testing.T) { - own := delivery.Composed{Node: "n1", Resources: []json.RawMessage{aFileOf("zsh", "rc"), aFileOf("mesh-wireguard", "conf")}, + own := engcomp.Composed{Node: "n1", Resources: []json.RawMessage{aFileOf("zsh", "rc"), aFileOf("mesh-wireguard", "conf")}, Filled: []string{"/tmp/r6/zsh/rc"}} - zsh := delivery.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")}, + zsh := engcomp.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")}, Filled: []string{"/tmp/r6/zsh/rc"}} d, err := declarationOf(own, zsh) if err != nil { @@ -181,7 +182,7 @@ func TestTheNodesOwnCopyOfAModulesResourceIsLeftOut(t *testing.T) { } // The same at the node's own apply: what the others declare is theirs. - trimmed, dropped := ownWithout(own, []delivery.Composed{zsh}) + trimmed, dropped := ownWithout(own, []engcomp.Composed{zsh}) if len(trimmed.Resources) != 1 || len(dropped) != 1 || dropped[0] != "zsh.rc (zsh)" { t.Errorf("the node's own without zsh's: %d resources, dropped %v", len(trimmed.Resources), dropped) } @@ -211,9 +212,9 @@ func TestTheKeptDeclarationsFilledPathsGoToTheirModules(t *testing.T) { strings.Join(filled[agent.NodeModule], ",") != "/etc/motd" || len(filled["zsh"]) != 0 { t.Fatalf("filled by module: %v", filled) } - cc := delivery.Composed{Node: "n1", Module: "claude-code", Resources: by["claude-code"], Filled: filled["claude-code"]} - node := delivery.Composed{Node: "n1"} - d, err := whatRuns([]delivery.Composed{cc, node}, true, func() (*declaration.Declaration, error) { return nil, errors.New("not read") }) + cc := engcomp.Composed{Node: "n1", Module: "claude-code", Resources: by["claude-code"], Filled: filled["claude-code"]} + node := engcomp.Composed{Node: "n1"} + d, err := whatRuns([]engcomp.Composed{cc, node}, true, func() (*declaration.Declaration, error) { return nil, errors.New("not read") }) if err != nil || !d.Fills("/etc/cc.json") { t.Fatalf("what runs does not name claude-code's file filled: %v (%v)", d, err) } @@ -226,9 +227,9 @@ func TestTheKeptDeclarationsFilledPathsGoToTheirModules(t *testing.T) { // unit or a container of the same name) is not the module's, and is neither dropped nor left to be removed. func TestASharedTargetWithAnotherIDIsNotDropped(t *testing.T) { ownFile, _ := json.Marshal(map[string]any{"id": "host.zshrc", "type": "file", "path": "/tmp/r6/zsh/rc", "content": "x"}) - own := delivery.Composed{Node: "n1", Resources: []json.RawMessage{ownFile}} - zsh := delivery.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")}} - trimmed, dropped := ownWithout(own, []delivery.Composed{zsh}) + own := engcomp.Composed{Node: "n1", Resources: []json.RawMessage{ownFile}} + zsh := engcomp.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")}} + trimmed, dropped := ownWithout(own, []engcomp.Composed{zsh}) if len(dropped) != 0 || len(trimmed.Resources) != 1 { t.Errorf("a resource sharing only its target was dropped: %v", dropped) } diff --git a/go.mod b/go.mod index 17ac313..e5ee449 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/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 git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go v0.0.0-20261105000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f github.com/nats-io/nats-server/v2 v2.11.17 diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 198a966..a80bd7b 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -126,9 +126,9 @@ type Env struct { // Apply applies one module's composition (scope NodeModule for the node's own), given the compositions the node's // other modules run so a path, unit or group they hold is not removed or taken. It is called with one module at a // time. - Apply func(ctx context.Context, c delivery.Composed, others []delivery.Composed) Result + Apply func(ctx context.Context, c compositions.Composed, others []compositions.Composed) Result // Remove removes the module's own resources and nothing of another's. - Remove func(ctx context.Context, module string, others []delivery.Composed) Result + Remove func(ctx context.Context, module string, others []compositions.Composed) Result // Backup backs a module up before a new composition of it is applied; nil backs up nothing. Backup func(ctx context.Context, module string) Backup // Kept is the resources the node last applied, by module, from the declaration it kept before the switch (ADR 0539 @@ -143,12 +143,12 @@ type Env struct { KeptFilled func() map[string][]string // Recorded says the engine holds a record of every resource of the composition, so it was applied and not removed // since; nil says yes. A composition the kept declaration or a have says runs is applied again when it says no. - Recorded func(ctx context.Context, c delivery.Composed) bool + Recorded func(ctx context.Context, c compositions.Composed) bool // Inactive says a resource the composition declares running is not active on the machine now (a unit stopped, a // container not running), read from the machine and not from any record (ADR 0568 §1). A have or a kept declaration // that says the module runs is believed at the agent's start only when this says no; nil says no, and an unreadable // machine says no (unknown is not inactive). - Inactive func(ctx context.Context, c delivery.Composed) bool + Inactive func(ctx context.Context, c compositions.Composed) bool // Revisions are a composition key's revisions the bucket keeps, oldest first; nil reads none. The composition a want // names is taken from them when a newer one replaced it under the same key (ADR 0539 clause 1). Revisions func(ctx context.Context, key string) ([]Update, error) @@ -282,7 +282,7 @@ type wantNow struct { type compNow struct { Raw []byte - C delivery.Composed + C compositions.Composed Dig string Rev uint64 Err error @@ -722,7 +722,7 @@ func (a *Agent) sealedOf(ctx context.Context, module, fp, composed string) compo // Running is the compositions the node runs now: each module's that the agent applied or found already running, and // the node's own. What the node judges its health by and says of itself is built from them. -func (a *Agent) Running() []delivery.Composed { +func (a *Agent) Running() []compositions.Composed { a.mu.Lock() defer a.mu.Unlock() mods := make([]string, 0, len(a.running)) @@ -730,7 +730,7 @@ func (a *Agent) Running() []delivery.Composed { mods = append(mods, m) } sort.Strings(mods) - var out []delivery.Composed + var out []compositions.Composed for _, m := range mods { h := a.running[m] if h.Phase == delivery.PhaseRemoved { @@ -781,7 +781,7 @@ func (a *Agent) takeWant(u Update) string { // takeComposition takes a change of a composition key and returns the module it concerns. func (a *Agent) takeComposition(u Update) string { - node, module, _, isNode, err := delivery.ParseComposedKey(u.Key) + node, module, _, isNode, err := compositions.ParseKey(u.Key) if err != nil || node != a.env.Node { return "" } @@ -795,14 +795,14 @@ func (a *Agent) takeComposition(u Update) string { return module } now := compNow{Raw: u.Value, Rev: u.Revision} - c, err := delivery.DecodeComposed(u.Value) + c, err := compositions.Decode(u.Value) if err == nil { // The node composed it itself: it is applied as composed, never verified against a signer, since no composition // read from the bus is applied (novox/hq ADR 0547 §2, ADR 0593 §6). now.C = c - now.Dig, err = delivery.ComposedDigestOf(c) + now.Dig, err = compositions.DigestOf(c) if err != nil { - now.C = delivery.Composed{} + now.C = compositions.Composed{} } } else { a.sayOnce("decode:"+u.Key, fmt.Sprintf("the composition %s cannot be read, and nothing of it is applied: %v", u.Key, err)) @@ -844,9 +844,9 @@ func (a *Agent) compositionFor(module, fp string) (compNow, bool) { var key string var err error if module == NodeModule { - key, err = delivery.ComposedNodeKey(a.env.Node) + key, err = compositions.NodeKey(a.env.Node) } else { - key, err = delivery.ComposedKey(a.env.Node, module, fp) + key, err = compositions.Key(a.env.Node, module, fp) } if err != nil { return compNow{}, false @@ -879,8 +879,8 @@ func (a *Agent) revisionNamed(ctx context.Context, module, fp, digest string) (c // others are what the node's other modules run now, so what they hold is not taken: the compositions the agent runs and, // for every module the agent does not hold yet, the resources the kept declaration has for it (ADR 0429 §4). -func (a *Agent) others(except string) []delivery.Composed { - var out []delivery.Composed +func (a *Agent) others(except string) []compositions.Composed { + var out []compositions.Composed mods := make([]string, 0, len(a.running)) for m := range a.running { if m != except { @@ -1011,7 +1011,7 @@ func (a *Agent) loadApplied(ctx context.Context) { if rs, ok := a.env.Kept()[NodeModule]; ok && len(rs) > 0 { c, _ := a.keptComposed(NodeModule, rs) c.Module = "" - raw, eerr := delivery.EncodeComposed(c) + raw, eerr := compositions.Encode(c) if eerr == nil { eerr = a.env.Applied.Keep(NodeModule, raw) } @@ -1035,10 +1035,10 @@ func (a *Agent) loadApplied(ctx context.Context) { if ctx.Err() != nil { return } - c, err := delivery.DecodeComposed(kept[m]) + c, err := compositions.Decode(kept[m]) var dig string if err == nil { - dig, err = delivery.ComposedDigestOf(c) + dig, err = compositions.DigestOf(c) } if err != nil { a.env.Say(fmt.Sprintf("the composition this engine kept of %s cannot be read, so what runs of it is read from its have: %v", m, err)) @@ -1077,7 +1077,7 @@ func (a *Agent) keepApplied(module string, comp compNow) { raw := comp.Raw if len(raw) == 0 { var err error - if raw, err = delivery.EncodeComposed(comp.C); err != nil { + if raw, err = compositions.Encode(comp.C); err != nil { a.env.Say(fmt.Sprintf("the composition %s was applied from cannot be kept: %v", module, err)) return } @@ -1126,7 +1126,7 @@ func (a *Agent) unreadableModules() []string { } current := map[string]pick{} for key, c := range a.comps { - _, m, fp, isNode, err := delivery.ParseComposedKey(key) + _, m, fp, isNode, err := compositions.ParseKey(key) if err != nil || isNode { continue } @@ -1159,7 +1159,7 @@ func (a *Agent) unreadableModules() []string { // until that one is applied, so the node's own apply never removes them in between (issue 1945, live on 2026-10-25: // shanks lost systemd-resolved's guard while its composition waited for a credential). The composition its want names, // else its newest. Not part of Everything: what has not been applied is not what runs. -func (a *Agent) heldOut(except string) []delivery.Composed { +func (a *Agent) heldOut(except string) []compositions.Composed { out := a.others(except) held := map[string]bool{} for _, c := range out { @@ -1167,7 +1167,7 @@ func (a *Agent) heldOut(except string) []delivery.Composed { } waiting := map[string]compNow{} for key, c := range a.comps { - _, m, fp, isNode, err := delivery.ParseComposedKey(key) + _, m, fp, isNode, err := compositions.ParseKey(key) if err != nil || isNode || c.Err != nil || m == except || held[m] { continue } @@ -1203,7 +1203,7 @@ func (a *Agent) heldOut(except string) []delivery.Composed { if len(a.heldAlone) == 0 { return out } - var own *delivery.Composed + var own *compositions.Composed for _, c := range a.others("\x00") { if c.Module == "" { own = &c @@ -1237,7 +1237,7 @@ func (a *Agent) heldOut(except string) []delivery.Composed { } if len(rs) > 0 { // Held out, never judged: its Data from its manifest where known, as every other composition says it. - out = append(out, delivery.Composed{Node: a.env.Node, Module: m, Resources: rs, Data: a.keptData[m]}) + out = append(out, compositions.Composed{Node: a.env.Node, Module: m, Resources: rs, Data: a.keptData[m]}) } } return out @@ -1246,7 +1246,7 @@ func (a *Agent) heldOut(except string) []delivery.Composed { // Everything is what the node runs: the compositions the agent runs, and the kept declaration's resources for each module // the agent does not hold yet. What the node judges its health by, and what the filling, the take act and the judgement // of directories read, is built from it. -func (a *Agent) Everything() []delivery.Composed { +func (a *Agent) Everything() []compositions.Composed { a.mu.Lock() defer a.mu.Unlock() return a.others("\x00") @@ -1550,7 +1550,7 @@ func (a *Agent) inactiveAtStart(ctx context.Context, module string, comp compNow return true } -func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNow, others []delivery.Composed) { +func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNow, others []compositions.Composed) { fp, dig := w.Want.FP, w.Want.Composed a.mu.Lock() _, ranBefore := a.running[module] @@ -1840,7 +1840,7 @@ func (a *Agent) buildAlone(ctx context.Context, module string, prev delivery.Hav // The composition its have named at the start comes before the kept declaration (issue 924). From the kept declaration, // what runs carries Data as its manifest declares it; a module whose manifest could not be read cannot be judged against // it (false), and that is said, rather than taking it to hold no data and judging a false move of its data. -func (a *Agent) runningComposition(module string) (delivery.Composed, bool) { +func (a *Agent) runningComposition(module string) (compositions.Composed, bool) { if c, ok := a.applied[module]; ok && c.Err == nil { return c.C, true } @@ -1855,14 +1855,14 @@ func (a *Agent) runningComposition(module string) (delivery.Composed, bool) { } return c, known } - return delivery.Composed{}, false + return compositions.Composed{}, false } // keptComposed is what runs of a module from the kept declaration: its resources there, the paths it names as filled, // and Data as the module's manifest declares it; false when the manifest could not be read, so Data is not known. Called // with a.mu held. -func (a *Agent) keptComposed(module string, rs []json.RawMessage) (delivery.Composed, bool) { - c := delivery.Composed{Node: a.env.Node, Module: module, Resources: rs} +func (a *Agent) keptComposed(module string, rs []json.RawMessage) (compositions.Composed, bool) { + c := compositions.Composed{Node: a.env.Node, Module: module, Resources: rs} if a.env.KeptFilled != nil { c.Filled = a.env.KeptFilled()[module] } diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 4431c3b..c34699a 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -123,7 +123,7 @@ type bed struct { during func() backup func(module string) Backup kept map[string][]json.RawMessage - others [][]delivery.Composed + others [][]compositions.Composed said []string // settled is each module Settled was told, and settledRan whether what runs held it then. settled []string @@ -135,7 +135,7 @@ type bed struct { func newBed(t *testing.T) *bed { b := &bed{t: t, wants: newFeed(), comps: newFeed(), haves: &haves{}} b.env = Env{Node: node, Wants: b.wants, Composed: b.comps, Haves: b.haves, - Apply: func(_ context.Context, c delivery.Composed, others []delivery.Composed) Result { + Apply: func(_ context.Context, c compositions.Composed, others []compositions.Composed) Result { if b.during != nil { b.during() } @@ -156,7 +156,7 @@ func newBed(t *testing.T) *bed { } return Result{} }, - Remove: func(_ context.Context, m string, _ []delivery.Composed) Result { + Remove: func(_ context.Context, m string, _ []compositions.Composed) Result { b.mu.Lock() defer b.mu.Unlock() b.removed = append(b.removed, m) @@ -222,13 +222,13 @@ func file(module, id, content string) json.RawMessage { // composition is a module's composition at a fingerprint: its key, its bytes and its digest, from the writer's code. func composition(t *testing.T, module, fp string, data bool, rs ...json.RawMessage) (key string, raw []byte, digest string) { t.Helper() - c := delivery.Composed{Node: node, Module: module, FP: fp, Data: data, Resources: rs} + c := compositions.Composed{Node: node, Module: module, FP: fp, Data: data, Resources: rs} var err error if module == NodeModule { c.Module, c.FP = "", "" - key, err = delivery.ComposedNodeKey(node) + key, err = compositions.NodeKey(node) } else { - key, err = delivery.ComposedKey(node, module, fp) + key, err = compositions.Key(node, module, fp) } if err != nil { t.Fatal(err) @@ -237,13 +237,13 @@ func composition(t *testing.T, module, fp string, data bool, rs ...json.RawMessa } // encodedAs is c as the node's composer hands it: unsigned (ADR 0593 §6), its key, its bytes and its digest. -func encodedAs(t *testing.T, c delivery.Composed, key string) (string, []byte, string) { +func encodedAs(t *testing.T, c compositions.Composed, key string) (string, []byte, string) { t.Helper() - raw, err := delivery.EncodeComposed(c) + raw, err := compositions.Encode(c) if err != nil { t.Fatal(err) } - digest, err := delivery.ComposedDigestOf(c) + digest, err := compositions.DigestOf(c) if err != nil { t.Fatal(err) } @@ -987,20 +987,20 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) { b.env.Applied = store // A folded module's resource in the node's own composition: its id names the module, as the controller folds it. natsConf, _ := json.Marshal(map[string]any{"id": "nats.conf", "type": "file", "path": "/tmp/r6/nats/conf", "content": "listen 4222"}) - own := delivery.Composed{Node: node, Resources: []json.RawMessage{natsConf}} - ownRaw, err := delivery.EncodeComposed(own) + own := compositions.Composed{Node: node, Resources: []json.RawMessage{natsConf}} + ownRaw, err := compositions.Encode(own) if err != nil { t.Fatal(err) } - ownDig, _ := delivery.ComposedDigestOf(own) - web := delivery.Composed{Node: node, Module: "web", FP: fpOf("a"), Resources: []json.RawMessage{file("web", "x", "1")}} - webRaw, _ := delivery.EncodeComposed(web) - webDig, _ := delivery.ComposedDigestOf(web) + ownDig, _ := compositions.DigestOf(own) + web := compositions.Composed{Node: node, Module: "web", FP: fpOf("a"), Resources: []json.RawMessage{file("web", "x", "1")}} + webRaw, _ := compositions.Encode(web) + webDig, _ := compositions.DigestOf(web) store.kept[NodeModule], store.kept["web"] = ownRaw, webRaw b.haves.Write(context.Background(), Have{Module: NodeModule, Composed: ownDig}) b.haves.Write(context.Background(), Have{Module: "web", FP: fpOf("a"), Composed: webDig, Phase: string(delivery.PhaseRunning)}) // The composer's first pass reads what runs when its watch begins, before the agent reads any have. - firstCh := make(chan []delivery.Composed, 1) + firstCh := make(chan []compositions.Composed, 1) comps := b.comps var ag *Agent b.env.Composed = watchFunc(func(ctx context.Context, p string) (<-chan Update, error) { @@ -1013,7 +1013,7 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) { done := make(chan struct{}) go func() { defer close(done); _ = ag.Run(ctx) }() defer func() { cancel(); <-done }() - var first []delivery.Composed + var first []compositions.Composed select { case first = <-firstCh: case <-time.After(5 * time.Second): @@ -1029,9 +1029,9 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) { ranOwn, ranWeb, len(first)) } // An apply keeps the composition it applied; a removal forgets it. - web2 := delivery.Composed{Node: node, Module: "web", FP: fpOf("b"), Resources: []json.RawMessage{file("web", "x", "2")}} - ck, _ := delivery.ComposedKey(node, "web", fpOf("b")) - raw2, _ := delivery.EncodeComposed(web2) + web2 := compositions.Composed{Node: node, Module: "web", FP: fpOf("b"), Resources: []json.RawMessage{file("web", "x", "2")}} + 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}) @@ -1050,10 +1050,10 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) { store := &memApplied{kept: map[string][]byte{}} h := &haves{} h.Write(context.Background(), Have{Module: NodeModule, Composed: "sha256:" + strings.Repeat("c", 64)}) - firstPass := func(kept map[string][]json.RawMessage) []delivery.Composed { + firstPass := func(kept map[string][]json.RawMessage) []compositions.Composed { b := newBed(t) b.env.SelfComposed, b.kept, b.env.Applied, b.env.Haves = true, kept, store, h - firstCh := make(chan []delivery.Composed, 1) + firstCh := make(chan []compositions.Composed, 1) comps := b.comps var ag *Agent b.env.Composed = watchFunc(func(ctx context.Context, p string) (<-chan Update, error) { @@ -1073,7 +1073,7 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) { } return nil } - runsNats := func(cs []delivery.Composed) bool { + runsNats := func(cs []compositions.Composed) bool { for _, c := range cs { if c.Module == "" && len(c.Resources) == 1 && strings.Contains(string(c.Resources[0]), "4222") { return true @@ -1281,7 +1281,7 @@ func testARemovedModulesCompositionHoldsNothingBack(t *testing.T) { // it (issue 1945, the review's gap 2). It is said, naming the composition, and applied once that one reads. func TestTheNodesOwnWaitsWhileAModulesCompositionCannotBeRead(t *testing.T) { b := newBed(t) - bad, err := delivery.ComposedKey(node, "broken", fpOf("c")) + bad, err := compositions.Key(node, "broken", fpOf("c")) if err != nil { t.Fatal(err) } @@ -1317,7 +1317,7 @@ func TestTheNodesOwnWaitsWhileAModulesCompositionCannotBeRead(t *testing.T) { // compositions written before the filled paths, which the SDK now refuses, and nobody applies them). func TestAStaleRefusedCompositionDoesNotHoldTheNodesOwnBack(t *testing.T) { b := newBed(t) - stale, err := delivery.ComposedKey(node, "power", fpOf("a")) + stale, err := compositions.Key(node, "power", fpOf("a")) if err != nil { t.Fatal(err) } @@ -1353,7 +1353,7 @@ func TestACompositionWhoseResourcesWereRemovedIsAppliedAgain(t *testing.T) { } else { b.haves.stored = map[string]Have{"ca-trust": {Module: "ca-trust", FP: fp, Composed: dig, Phase: "running"}} } - b.env.Recorded = func(context.Context, delivery.Composed) bool { return false } + b.env.Recorded = func(context.Context, compositions.Composed) bool { return false } stop := b.run() defer stop() eventually(t, "not applied", func() bool { return len(b.appliedList()) == 1 }) @@ -1402,8 +1402,8 @@ func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) { `","mode":"0644","owner-at-runtime":"owned"}`) } fp := fpOf("a") - ck, _ := delivery.ComposedKey(node, "power", fp) - applied := delivery.Composed{Node: node, Module: "power", FP: fp, Filled: []string{old, added}, Data: true, + ck, _ := compositions.Key(node, "power", fp) + applied := compositions.Composed{Node: node, Module: "power", FP: fp, Filled: []string{old, added}, Data: true, Resources: []json.RawMessage{filledFile("settings"), filledFile("bound")}} _, appliedRaw, appliedDig := encodedAs(t, applied, ck) newer := applied @@ -1527,13 +1527,13 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( stop := b.run() defer stop() unsigned := func(module, fp string, rs ...json.RawMessage) (string, []byte, string) { - c := delivery.Composed{Node: node, Module: module, FP: fp, Resources: rs} - key, _ := delivery.ComposedKey(node, module, fp) - raw, err := delivery.EncodeComposed(c) + c := compositions.Composed{Node: node, Module: module, FP: fp, Resources: rs} + key, _ := compositions.Key(node, module, fp) + raw, err := compositions.Encode(c) if err != nil { t.Fatal(err) } - dig, _ := delivery.ComposedDigestOf(c) + dig, _ := compositions.DigestOf(c) return key, raw, dig } want := func(module, fp, yes string) { @@ -1567,10 +1567,10 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( // The node's own composition carries its sealed values in its key, as a module's have does, so a restart reuses its // core boxes and applies nothing again (review of L2, round 3, finding 1). box, _ := json.Marshal(map[string]any{"id": "core-box", "type": "file", "path": "/tmp/r6/core-box", "sealed": "core"}) - own := delivery.Composed{Node: node, Resources: []json.RawMessage{box}} - nk, _ := delivery.ComposedNodeKey(node) - nraw, _ := delivery.EncodeComposed(own) - ndig, _ := delivery.ComposedDigestOf(own) + own := compositions.Composed{Node: node, Resources: []json.RawMessage{box}} + nk, _ := compositions.NodeKey(node) + nraw, _ := compositions.Encode(own) + ndig, _ := compositions.DigestOf(own) b.comps.put(nk, nraw) nwk, _ := delivery.WantNodeKey(node) nwraw, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true}) @@ -1707,8 +1707,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( } // Issue 774 (ace, 2026-11-04 01:29): the node's own revision shows its own fields, the provisioner client's account // and the adoption among them, and what the composer's last pass held. - ownKey, ownRaw, ownDig := encodedAs(t, delivery.Composed{Node: node, Own: delivery.NodeOwn{Provisioner: "mesh-provisioner-client", - Adoption: &declaration.Adoption{Taken: []string{"web"}}}}, func() string { k, _ := delivery.ComposedNodeKey(node); return k }()) + ownKey, ownRaw, ownDig := encodedAs(t, compositions.Composed{Node: node, Own: compositions.NodeOwn{Provisioner: "mesh-provisioner-client", + Adoption: &declaration.Adoption{Taken: []string{"web"}}}}, func() string { k, _ := compositions.NodeKey(node); return k }()) keep(ownKey, ownRaw) if view, err := b.ag.Composition(context.Background(), NodeModule, ownDig, false); err != nil || view.Own == nil || view.Own.Provisioner != "mesh-provisioner-client" || view.Own.Adoption == nil || view.Own.Adoption.Taken[0] != "web" || @@ -1717,9 +1717,9 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( } // novox/hq ADR 0593 §6: the engine publishes each composition with its plan in its composition state; the verb reads // that revision by the digest its have and want name, its resources' (the plan left out of the digest). - planned, _ := delivery.DecodeComposed(draw) + planned, _ := compositions.Decode(draw) planned.Plan = &catalogue.Plan{} - praw, err := delivery.EncodeComposed(planned) + praw, err := compositions.Encode(planned) if err != nil { t.Fatal(err) } @@ -1823,7 +1823,7 @@ func testAnEngineStartReconcilesAModuleFromTheMachinesFacts(t *testing.T) { } var mu sync.Mutex asked := false - b.env.Inactive = func(context.Context, delivery.Composed) bool { + b.env.Inactive = func(context.Context, compositions.Composed) bool { mu.Lock() defer mu.Unlock() asked = true diff --git a/internal/agent/composition.go b/internal/agent/composition.go index 11d00e5..a3f86f4 100644 --- a/internal/agent/composition.go +++ b/internal/agent/composition.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/declaration" @@ -45,7 +46,7 @@ type CompositionView struct { Own *OwnView `json:"own,omitempty"` } -// OwnView is the node's own fields of a composition (delivery.NodeOwn) beside what runs of them, and what the node's +// OwnView is the node's own fields of a composition (engcomp.NodeOwn) beside what runs of them, and what the node's // composer held in its last pass. type OwnView struct { // Provisioner is the account the provisioner's client runs as, as the revision read names it; RunsProvisioner as @@ -218,7 +219,7 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo view.Composition.Refused = read.Err.Error() return view, nil } - var running *delivery.Composed + var running *engcomp.Composed if known { running = &from rv := &RunsView{From: source, Resources: len(from.Resources), FP: from.FP} @@ -255,9 +256,9 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo // compositionKey is a module's composition key at a build, or the node's own. func (a *Agent) compositionKey(module, fp string) (string, error) { if module == NodeModule { - return delivery.ComposedNodeKey(a.env.Node) + return engcomp.NodeKey(a.env.Node) } - return delivery.ComposedKey(a.env.Node, module, fp) + return engcomp.Key(a.env.Node, module, fp) } // revisionsOf is every revision of a composition key the state keeps, oldest first, each with its digest (unsigned, ADR @@ -276,12 +277,12 @@ func (a *Agent) revisionsOf(ctx context.Context, key string) ([]compNow, error) if u.Deleted || u.Key != key { continue } - c, err := delivery.DecodeComposed(u.Value) + c, err := engcomp.Decode(u.Value) if err != nil { out = append(out, compNow{Raw: u.Value, Rev: u.Revision, Err: err}) continue } - dig, err := delivery.ComposedDigestOf(c) // unsigned: a record, never verified (ADR 0593 §6) + dig, err := engcomp.DigestOf(c) // unsigned: a record, never verified (ADR 0593 §6) out = append(out, compNow{Raw: u.Value, C: c, Dig: dig, Rev: u.Revision, Err: err}) } return out, nil diff --git a/internal/compose/composed.go b/internal/compose/composed.go index 024f287..c30b539 100644 --- a/internal/compose/composed.go +++ b/internal/compose/composed.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "slices" "sort" "strings" @@ -17,14 +18,14 @@ import ( // Compositions is one node's compositions (novox/hq ADR 0539 clauses 1 and 2): its declaration composed over in, as // the controller composes it, split into each module's composition at the fingerprint fps gives it and the node's own -// (`.node`), by key (delivery.ComposedKey). Each resource says its module, whatever the node reported it reads. +// (`.node`), by key (engcomp.Key). Each resource says its module, whatever the node reported it reads. // A module fps gives no fingerprint for is not composed and is named in skipped, so a caller composing one module // gets that one; but for a module the node runs inside its own composition (Node.InOwn, and the // private network's always), which fps names at no fingerprint of the new form: its resources go into the node's own, // their module taken off and their ids still naming it, and its filled paths with them, as the controller's composed // writer folds them (issue 1126). Each module's Data says whether its manifest declares data. The compositions are unsigned // (ADR 0593 §6) and not yet by digest (Composed.ByDigest). -func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]delivery.Composed, skipped []string, err error) { +func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped []string, err error) { out, skipped, _, err = compositions(in, node, fps, mode, false) return out, skipped, err } @@ -41,7 +42,7 @@ const BusUsersStandIn = "the bus's user list" // 1126): a declaration that stands in for anything (Declared.StoodIn, Foreseen, ForeseenPorts) is refused with // ErrStoodIn, naming each, so a stand-in is never applied as if it were the value; but for the bus's user list // (BusUsersStandIn), which the bus node holds out of every apply of a composition it made itself. -func CompositionsApplied(in Input, node string, fps map[string]string) (map[string]delivery.Composed, []string, error) { +func CompositionsApplied(in Input, node string, fps map[string]string) (map[string]engcomp.Composed, []string, error) { out, skipped, _, err := compositions(in, node, fps, Reading, true) return out, skipped, err } @@ -52,14 +53,14 @@ func CompositionsApplied(in Input, node string, fps map[string]string) (map[stri // the bus's user list left out) rather than refused, so the caller judges each stand-in: a value not yet issued for a // module the candidate adds, or one missing for a module the node runs. A module whose every value is held composes // as CompositionsApplied composes it: the modes differ only in an own secret never issued. -func CompositionsShadow(in Input, node string, fps map[string]string) (out map[string]delivery.Composed, skipped, stood []string, err error) { +func CompositionsShadow(in Input, node string, fps map[string]string) (out map[string]engcomp.Composed, skipped, stood []string, err error) { return compositions(in, node, fps, Foreseeing, false) } // CompositionsIn is the node's compositions in mode with what they stand in for answered (stood, sorted, the bus's user // list left out) rather than refused: CompositionsShadow in Foreseeing, and in Reading what CompositionsApplied refuses // before it is refused, so a caller that applies refuses each stand-in to the module it is for alone (issue 1126). -func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (out map[string]delivery.Composed, skipped, stood []string, err error) { +func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped, stood []string, err error) { return compositions(in, node, fps, mode, false) } @@ -72,7 +73,7 @@ func Folds(inOwn bool, fp string) bool { return inOwn && !strings.HasPrefix(fp, delivery.SourcePrefix2) } -func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]delivery.Composed, skipped, stood []string, err error) { +func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]engcomp.Composed, skipped, stood []string, err error) { reading := in reading.Nodes = append([]Node(nil), in.Nodes...) found := false @@ -102,7 +103,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli // (Carried), so what it provides and requires still shapes the rest and what the others read of it (its published // ports, the paths it owns) is what runs, and its resources are then the ones the node runs. carried := map[string]bool{} - var runsOwn *delivery.Composed + var runsOwn *engcomp.Composed for i := range reading.Nodes { n := &reading.Nodes[i] if n.Name != node || n.RunsOwn == nil { @@ -264,13 +265,13 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli filled = append(filled, p) } } - out, err = delivery.SplitComposed(node, kept, fps, filled, - delivery.NodeOwn{Adoption: envelope.Adoption, Provisioner: envelope.Provisioner}) + out, err = engcomp.Split(node, kept, fps, filled, + engcomp.NodeOwn{Adoption: envelope.Adoption, Provisioner: envelope.Provisioner}) if err != nil { return nil, nil, nil, err } if len(ownFilled) > 0 { - own, err := delivery.ComposedNodeKey(node) + own, err := engcomp.NodeKey(node) if err != nil { return nil, nil, nil, err } @@ -296,7 +297,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli } // RunsAnyOf says runs (a composition as it runs) holds a resource of module, by its module or the module its id names. -func RunsAnyOf(runs *delivery.Composed, module string) bool { +func RunsAnyOf(runs *engcomp.Composed, module string) bool { if runs == nil { return false } @@ -324,7 +325,7 @@ func RunsAnyOf(runs *delivery.Composed, module string) bool { // (catalogue.RuntimeToolModules, catalogue.RuntimeToolEnv, the mesh's own words left out). A bundle the runtime loads // nothing of has no Loads: the runtime dropped it, and the composition reads its loads again from the module's build // (issue 1126, review: shanks had already applied the drop of systemd-resolved). Nil when running holds no runtime process. -func ToolsRunning(running []delivery.Composed) map[string][]catalogue.Bundle { +func ToolsRunning(running []engcomp.Composed) map[string][]catalogue.Bundle { type resource struct { ID string `json:"id"` Module string `json:"module"` @@ -476,7 +477,7 @@ func Stub(m catalogue.Manifest) catalogue.Manifest { // is composed into what is applied either way (compositions leaves a carried module's out, and carries the ones that // run). What it provides, requires, listens on and grants is its manifest's. A module whose resources runs holds none // of is its Stub, as before. -func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catalogue.Manifest { +func Carried(m catalogue.Manifest, module string, runs *engcomp.Composed) catalogue.Manifest { if runs == nil { return Stub(m) } @@ -521,7 +522,7 @@ func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catal // carry puts into the node's resources (kept, in the declaration's order) the resources of the node's own composition as // it runs (runs) that belong to a carried module, each after the resource it runs after, or first when that one is // not among them; a file among them is a folded path. -func carry(kept []json.RawMessage, foldedPaths map[string]bool, runs *delivery.Composed, carried func(module, id string) bool) ([]json.RawMessage, map[string]bool, error) { +func carry(kept []json.RawMessage, foldedPaths map[string]bool, runs *engcomp.Composed, carried func(module, id string) bool) ([]json.RawMessage, map[string]bool, error) { idOf := func(raw json.RawMessage) (string, string, string, string, error) { var head struct { ID string `json:"id"` diff --git a/internal/compose/composed_test.go b/internal/compose/composed_test.go index 09dacb5..ccbb3d4 100644 --- a/internal/compose/composed_test.go +++ b/internal/compose/composed_test.go @@ -2,6 +2,7 @@ package compose import ( "encoding/json" + engcomp "github.com/novox/mesh-host/internal/compositions" "sort" "strings" "testing" @@ -36,14 +37,14 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { } var got []string for key, c := range out { - node, module, fp, isNode, err := delivery.ParseComposedKey(key) + node, module, fp, isNode, err := engcomp.ParseKey(key) if err != nil || node != "laptop" || module != c.Module || fp != c.FP || (isNode != (module == "")) { t.Errorf("key %s holds %s at %s (%v)", key, c.Module, c.FP, err) } if !isNode && fp != fps[module] { t.Errorf("%s is composed at %s, given %s", module, fp, fps[module]) } - if _, err := delivery.EncodeComposed(c); err != nil { + if _, err := engcomp.Encode(c); err != nil { t.Errorf("%s: %v", key, err) } for _, raw := range c.Resources { @@ -81,7 +82,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { } } all, skipped, err := Compositions(folding, "laptop", nil, Foreseeing) - nodeKey, _ := delivery.ComposedNodeKey("laptop") + nodeKey, _ := engcomp.NodeKey("laptop") if err != nil || len(skipped) != 0 || len(all) != 1 || len(all[nodeKey].Resources) != len(want) { t.Fatalf("folded: %v, skipped %v, %d compositions, %d of %d resources in the node's own", err, skipped, len(all), len(all[nodeKey].Resources), len(want)) } @@ -92,7 +93,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { t.Errorf("folded resource %d is %v, the declaration's %v", i, r["id"], d.Resources[i]["id"]) } } - if _, err := delivery.EncodeComposed(all[nodeKey]); err != nil { + if _, err := engcomp.Encode(all[nodeKey]); err != nil { t.Errorf("the node's own with the folded modules: %v", err) } if _, _, err := Compositions(in, "nowhere", fps, Foreseeing); err == nil { @@ -121,35 +122,35 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { t.Fatal(err) } served := "lamp=" + resolution.BundlePath("lamp", "tools") + "/lamp-tools" - runtimeRuns := func(dropped bool) delivery.Composed { + runtimeRuns := func(dropped bool) engcomp.Composed { env := `"MESH_TOOL_MODULES":"` + served + `","MESH_TOOL_ENV":"{\"lamp\":{\"LAMP_CONF\":\"/var/lib/lamp/conf\"}}"` if dropped { env = `"MESH_TOOL_MODULES":""` // the drop already applied (shanks, 2026-10-29) } - return delivery.Composed{Node: "anchor", Module: "node-tools", Resources: []json.RawMessage{json.RawMessage( + return engcomp.Composed{Node: "anchor", Module: "node-tools", Resources: []json.RawMessage{json.RawMessage( `{"id":"node-tools.runtime","module":"node-tools","type":"process","name":"node-tools","env":{` + env + `}}`)}} } lampRuns := `{"id":"lamp.bundle-tools","type":"archive","source":"` + catalogue.ArtifactStoreScheme + `lamp/tools/blobs/` + dg('b') + `","digest":"` + dg('b') + `","path":"` + resolution.BundlePath("lamp", "tools") + `"}` for _, row := range []struct { name string - set func(n *Node) delivery.Composed // and what runs of lamp + set func(n *Node) engcomp.Composed // and what runs of lamp fps map[string]string dropped bool }{ - {"folded at a build of the old form", func(n *Node) delivery.Composed { + {"folded at a build of the old form", func(n *Node) engcomp.Composed { n.InOwn = map[string]bool{"lamp": true} - n.RunsOwn = &delivery.Composed{Node: "anchor", Resources: []json.RawMessage{json.RawMessage(lampRuns)}} + n.RunsOwn = &engcomp.Composed{Node: "anchor", Resources: []json.RawMessage{json.RawMessage(lampRuns)}} return *n.RunsOwn }, map[string]string{}, false}, - {"folded, the runtime already dropped it", func(n *Node) delivery.Composed { + {"folded, the runtime already dropped it", func(n *Node) engcomp.Composed { n.InOwn = map[string]bool{"lamp": true} - n.RunsOwn = &delivery.Composed{Node: "anchor", Resources: []json.RawMessage{json.RawMessage(lampRuns)}} + n.RunsOwn = &engcomp.Composed{Node: "anchor", Resources: []json.RawMessage{json.RawMessage(lampRuns)}} return *n.RunsOwn }, map[string]string{}, true}, - {"held, its new build not delivered", func(n *Node) delivery.Composed { + {"held, its new build not delivered", func(n *Node) engcomp.Composed { n.Held = map[string]bool{"lamp": true} - lr := delivery.Composed{Node: "anchor", Module: "lamp", Resources: []json.RawMessage{json.RawMessage( + lr := engcomp.Composed{Node: "anchor", Module: "lamp", Resources: []json.RawMessage{json.RawMessage( strings.Replace(lampRuns, `"type"`, `"module":"lamp","type"`, 1))}} n.Kept = map[string]catalogue.Manifest{"lamp": Carried(newLamp, "lamp", &lr)} return lr @@ -161,7 +162,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { anchor.Assigned = append(anchor.Assigned, "artifacts", "node-tools", "lamp") tools.BusMinted = []string{"anchor." + "node-tools"} anchor.SealingKey = keyOf(t, "anchor") - anchor.RunsTools = ToolsRunning([]delivery.Composed{runtimeRuns(row.dropped), row.set(anchor)}) + anchor.RunsTools = ToolsRunning([]engcomp.Composed{runtimeRuns(row.dropped), row.set(anchor)}) row.fps[overlay.Name], row.fps["artifacts"], row.fps["node-tools"] = fpFor('a'), fpFor('5'), fpFor('e') out, _, _, err := CompositionsIn(tools, "anchor", row.fps, Foreseeing) if err != nil { diff --git a/internal/compose/frombus/answer.go b/internal/compose/frombus/answer.go index 14b1b93..be62825 100644 --- a/internal/compose/frombus/answer.go +++ b/internal/compose/frombus/answer.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "github.com/novox/mesh-host/internal/compositions" "maps" "regexp" @@ -27,11 +28,11 @@ import ( // composition stands in for is judged: a value of a module the proposal adds (on this node or another: its credential, own // secrets and boxes are made only once it is placed, ADR 0549 §5) is not yet issued, which is no refusal; any other // stand-in is a value missing for a module the node runs, a refusal naming it ("node" when it is the node's own). -func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string, running []delivery.Composed, proposal string, revision uint64) delivery.CandidateAnswer { - a := delivery.CandidateAnswer{Proposal: proposal, Revision: revision} - refuse := func(err error) delivery.CandidateAnswer { +func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string, running []compositions.Composed, proposal string, revision uint64) nodeengine.CandidateAnswer { + a := nodeengine.CandidateAnswer{Proposal: proposal, Revision: int64(revision)} + refuse := func(err error) nodeengine.CandidateAnswer { a.Refused = err.Error() - a.Verdict = delivery.CandidateRefused + a.Verdict = nodeengine.CandidateAnswerVerdictRefused return a } trunk, err := r.Placements(ctx) @@ -99,16 +100,16 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st if _, whole := failed[delivery.HaveNodeWord]; whole { // Nothing composed to compare: every refusal is said, and nothing else. for _, m := range sortedKeys(failed) { - a.Refusals = append(a.Refusals, delivery.ModuleRefused{Module: m, Why: failed[m]}) + a.Refusals = append(a.Refusals, nodeengine.CandidateRefusal{Module: m, Why: failed[m]}) } heldAsToday(&a, s.HeldToday) - a.Verdict = delivery.VerdictOf(a) + a.Verdict = compositions.VerdictOf(a) return a } // A module the composition folds inside the node's own is judged there: the agent also says it runs by module, from // the declaration it kept before the switch (agent.Everything), which is no removal (issue 1126, seed #531). if len(s.InOwn) > 0 { - var judged []delivery.Composed + var judged []compositions.Composed for _, c := range running { if c.Module == "" || !slices.Contains(s.InOwn, c.Module) { judged = append(judged, c) @@ -126,7 +127,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st budget -= n kept = append(kept, b) } else { - kept = append(kept, delivery.ResourceBodies{ID: b.ID, Why: b.Why}) + kept = append(kept, nodeengine.CandidateBodies{ID: b.ID, Why: b.Why}) a.Note = "some differing resources are named without their bodies: the answer holds at most " + fmt.Sprint(MaxShown) + " bytes of them" } @@ -137,8 +138,8 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st notFailed := func(m string) bool { _, ok := failed[m]; return !ok } a.Same = slices.DeleteFunc(cmp.Same, func(m string) bool { return !notFailed(m) }) a.Removed = slices.DeleteFunc(cmp.Removed, func(m string) bool { return !notFailed(m) }) - a.Differs = slices.DeleteFunc(cmp.Differs, func(d delivery.ModuleDiffers) bool { return !notFailed(d.Module) }) - cmp.Added = slices.DeleteFunc(cmp.Added, func(d delivery.ModuleAdded) bool { return !notFailed(d.Module) }) + a.Differs = slices.DeleteFunc(cmp.Differs, func(d nodeengine.CandidateDiffers) bool { return !notFailed(d.Module) }) + cmp.Added = slices.DeleteFunc(cmp.Added, func(d nodeengine.CandidateAdded) bool { return !notFailed(d.Module) }) skipped = slices.DeleteFunc(skipped, func(m string) bool { return !notFailed(m) }) // What the proposal adds, by node and module: placed by a candidate and not by the trunk on that node. @@ -177,12 +178,12 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st refusals[module] = append(refusals[module], x) } } - addedHere := map[string]delivery.ModuleAdded{} + addedHere := map[string]nodeengine.CandidateAdded{} for _, m := range cmp.Added { addedHere[m.Module] = m } for _, m := range skipped { // placed, and no build given to compose it at - addedHere[m] = delivery.ModuleAdded{Module: m, Unissued: []string{"no build of " + m + " is known to compose it at"}} + addedHere[m] = nodeengine.CandidateAdded{Module: m, Unissued: []string{"no build of " + m + " is known to compose it at"}} } for m, xs := range unissued { got := addedHere[m] @@ -200,14 +201,14 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st if _, isFailed := failed[m]; isFailed { continue // said below, as why it could not be composed } - a.Refusals = append(a.Refusals, delivery.ModuleRefused{Module: m, Why: "stands in for " + strings.Join(refusals[m], ", ")}) + a.Refusals = append(a.Refusals, nodeengine.CandidateRefusal{Module: m, Why: "stands in for " + strings.Join(refusals[m], ", ")}) } for _, m := range sortedKeys(failed) { - a.Refusals = append(a.Refusals, delivery.ModuleRefused{Module: m, Why: failed[m]}) + a.Refusals = append(a.Refusals, nodeengine.CandidateRefusal{Module: m, Why: failed[m]}) } sort.Slice(a.Refusals, func(i, j int) bool { return a.Refusals[i].Module < a.Refusals[j].Module }) heldAsToday(&a, s.HeldToday) - a.Verdict = delivery.VerdictOf(a) + a.Verdict = compositions.VerdictOf(a) return a } @@ -215,7 +216,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st // held, failed, running a build of the old form): the candidate keeps it as the node does, which is no change (issue // 1126: a seed reproduces today, and today keeps them so). Each says how it stands today (ModuleRefused.Today). No // module is held behind another (issue 1960): a module that reads from a held one composes with what it last offered. -func heldAsToday(a *delivery.CandidateAnswer, today map[string]string) { +func heldAsToday(a *nodeengine.CandidateAnswer, today map[string]string) { if len(today) == 0 { return } @@ -237,7 +238,7 @@ func heldAsToday(a *delivery.CandidateAnswer, today map[string]string) { // maxCulprits bounds the modules one refusal is tried on, each a whole composition. const maxCulprits = 8 -// MaxShown bounds the bytes of shown resource bodies in one answer (delivery.ModuleDiffers.Bodies). +// MaxShown bounds the bytes of shown resource bodies in one answer (nodeengine.CandidateDiffers.Bodies). const MaxShown = 256 << 10 var ( @@ -291,13 +292,13 @@ func standsInFor(x, node string) (string, string) { // else the manifest it is composed from. Only the part of the offer that cannot compose is left out, by provision // (culprit): de-spiegel's routes, its names unset, leave route-proxy without them while mailu keeps its smtp account, // and the hold says so. -func composeEach(in compose.Input, node string, fps map[string]string, applied bool, running []delivery.Composed, failed map[string]string, - ran func(module string) (catalogue.Manifest, bool)) (out map[string]delivery.Composed, skipped, stood []string, err error) { +func composeEach(in compose.Input, node string, fps map[string]string, applied bool, running []compositions.Composed, failed map[string]string, + ran func(module string) (catalogue.Manifest, bool)) (out map[string]compositions.Composed, skipped, stood []string, err error) { mode := compose.Foreseeing if applied { mode = compose.Reading } - runs := map[string]*delivery.Composed{} + runs := map[string]*compositions.Composed{} for i := range running { if m := running[i].Module; m != "" { runs[m] = &running[i] @@ -543,7 +544,7 @@ func stubbed(in compose.Input, node, module, leave string) compose.Input { // its offer: a provision's contributions, with leaveOwn its own secrets alone, or with leaveAll everything it contributes // and its own secrets, none of a held module's own composition being applied, so what it has not been issued (restic's // repository on a node it never ran on) is no refusal of the rest. -func heldAs(in compose.Input, node, module string, man catalogue.Manifest, left map[string]bool, runs *delivery.Composed) compose.Input { +func heldAs(in compose.Input, node, module string, man catalogue.Manifest, left map[string]bool, runs *compositions.Composed) compose.Input { out := in out.Nodes = append([]compose.Node(nil), in.Nodes...) for i := range out.Nodes { diff --git a/internal/compose/frombus/answer_rows_test.go b/internal/compose/frombus/answer_rows_test.go index 113c4f0..134e3ae 100644 --- a/internal/compose/frombus/answer_rows_test.go +++ b/internal/compose/frombus/answer_rows_test.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + engcomp "github.com/novox/mesh-host/internal/compositions" "os" "reflect" "slices" @@ -76,7 +78,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { for _, r := range a.Refusals { refused = append(refused, r.Module) } - if a.Verdict != delivery.CandidateRefused || a.Refused != "" || !reflect.DeepEqual(refused, []string{"api", "spiegel", "web"}) { + if a.Verdict != nodeengine.CandidateAnswerVerdictRefused || a.Refused != "" || !reflect.DeepEqual(refused, []string{"api", "spiegel", "web"}) { t.Errorf("every refusal: %+v, want api, spiegel and web refused, each named", a) } for _, r := range a.Refusals { @@ -86,27 +88,31 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { t.Errorf("%s refused as %q, want %q", r.Module, r.Why, want) } } - if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}}); err != nil { + var wireAnswer delivery.CandidateAnswer // the form mesh-sdk's have still holds, until the Have group moves + if wire, err := json.Marshal(a); err != nil || json.Unmarshal(wire, &wireAnswer) != nil { + t.Fatalf("the answer does not read back: %v", err) + } + if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": wireAnswer}}); err != nil { t.Errorf("the answer is not one a have holds: %v (%s)", err, raw) } - if lemursOK := slices.ContainsFunc(a.Added, func(m delivery.ModuleAdded) bool { return m.Module == "lemurs" }); !lemursOK { + if lemursOK := slices.ContainsFunc(a.Added, func(m nodeengine.CandidateAdded) bool { return m.Module == "lemurs" }); !lemursOK { t.Errorf("lemurs, which composes, is not answered beside the refusals: %+v", a) } // Issue 1126: a module the node holds today that the candidate holds too is held as today, no refusal: with every // refused module held today the verdict is no refusal, and each is named under Held. today := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: map[string]string{"api": "held", "spiegel": "held", "web": "held"}}, map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3) - if today.Verdict == delivery.CandidateRefused || len(today.Refusals) != 0 || len(today.Held) != 3 || today.Held[0].Why == "" { + if today.Verdict == nodeengine.CandidateAnswerVerdictRefused || len(today.Refusals) != 0 || len(today.Held) != 3 || today.Held[0].Why == "" { t.Errorf("held as today: %+v", today) } // A refused module the node does not hold today stays a refusal: holding only api today leaves spiegel and web refused. part := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: map[string]string{"api": "held"}}, map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3) - if part.Verdict != delivery.CandidateRefused || len(part.Refusals) != 2 || len(part.Held) != 1 || part.Held[0].Module != "api" { + if part.Verdict != nodeengine.CandidateAnswerVerdictRefused || len(part.Refusals) != 2 || len(part.Held) != 1 || part.Held[0].Module != "api" { t.Errorf("held as today in part: %+v", part) } // No module is held behind one kept today (issue 1960): mailu's own refusal stays a refusal beside de-spiegel's hold. - if a := (delivery.CandidateAnswer{Refusals: []delivery.ModuleRefused{{Module: "de-spiegel", Why: "a value is unset"}, + if a := (nodeengine.CandidateAnswer{Refusals: []nodeengine.CandidateRefusal{{Module: "de-spiegel", Why: "a value is unset"}, {Module: "mailu", Why: "no build"}}}); true { heldAsToday(&a, map[string]string{"de-spiegel": "runs a build of the old form"}) if len(a.Refusals) != 1 || a.Refusals[0].Module != "mailu" || len(a.Held) != 1 { @@ -136,14 +142,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { if m, _ := culprit(in, "n1", fps, compose.Foreseeing, map[string]string{}, fmt.Errorf("nothing in this mesh provides \"artifact-store\"")); m != "" { t.Errorf("a refusal naming no placed module was laid on %s", m) } - ownKey, _ := delivery.ComposedNodeKey("n1") + ownKey, _ := engcomp.NodeKey("n1") own := base[ownKey] carriedRes := []json.RawMessage{ json.RawMessage(`{"id":"spiegel.state","type":"directory","path":"/var/lib/spiegel","mode":"0700"}`), json.RawMessage(`{"id":"spiegel.server","type":"container","name":"spiegel","image":"registry.example/spiegel@sha256:` + strings.Repeat("c", 64) + `"}`)} // They run after the node's first own resource. own.Resources = append(append(append([]json.RawMessage{}, own.Resources[:1]...), carriedRes...), own.Resources[1:]...) - var running []delivery.Composed + var running []engcomp.Composed for k, c := range base { if k == ownKey { c = own @@ -164,26 +170,26 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { t.Errorf("the node's own composition is\n%s\nwant what runs\n%s", gotOwn, wantOwn) } a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, running, "p-h1", 3) - if a.Verdict != delivery.CandidateSame { + if a.Verdict != nodeengine.CandidateAnswerVerdictSame { t.Errorf("the fold's answer: %+v, want the same", a) } // (A) The agent also says spiegel runs by module, from the declaration kept before the switch (agent.Everything): a // module the composition folds is judged inside the node's own, not as removed (issue 1126, seed #531's answer). - keptToo := append(append([]delivery.Composed{}, running...), delivery.Composed{Node: "n1", Module: "spiegel", Resources: carriedRes}) - if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, keptToo, "p-h1", 3); a.Verdict != delivery.CandidateSame || len(a.Removed) != 0 { + keptToo := append(append([]engcomp.Composed{}, running...), engcomp.Composed{Node: "n1", Module: "spiegel", Resources: carriedRes}) + if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, keptToo, "p-h1", 3); a.Verdict != nodeengine.CandidateAnswerVerdictSame || len(a.Removed) != 0 { t.Errorf("a folded module running from the kept declaration is answered %+v, want the same", a) } // (B) The node's own composition carries its bus membership as it runs it (the controller's, from its store): composed // the same, not 'in what runs, not composed' (seed #531, novox). membership := json.RawMessage(`{"id":"bus-membership","type":"file","path":"` + catalogue.BusMembershipPath + `","sealed":"{\"box\":\"m\"}","mode":"0600"}`) - withMembership := make([]delivery.Composed, 0, len(running)) + withMembership := make([]engcomp.Composed, 0, len(running)) for _, c := range running { if c.Module == "" { c.Resources = append(append([]json.RawMessage{}, c.Resources...), membership) } withMembership = append(withMembership, c) } - if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, withMembership, "p-h1", 3); a.Verdict != delivery.CandidateSame { + if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, withMembership, "p-h1", 3); a.Verdict != nodeengine.CandidateAnswerVerdictSame { t.Errorf("the node's own with its bus membership is answered %+v, want the same", a) } // Without what runs, the fold composes spiegel from its manifest, as before, and the composition refuses it. @@ -309,7 +315,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { if err != nil { t.Fatal(err) } - f1runs := delivery.Composed{Node: "ace", Resources: []json.RawMessage{ + f1runs := engcomp.Composed{Node: "ace", Resources: []json.RawMessage{ json.RawMessage(`{"id":"f1-circuits.state","module":"f1-circuits","type":"directory","path":"/var/lib/f1-circuits","mode":"0700"}`), json.RawMessage(`{"id":"f1-circuits.cache","module":"f1-circuits","type":"directory","path":"/var/lib/f1-circuits/cache","mode":"0700"}`), json.RawMessage(`{"id":"f1-circuits.site-env","module":"f1-circuits","type":"file","path":"/var/lib/f1-circuits/site.env","mode":"0644","content":"F1_CIRCUITS_BASE_URL=https://f1.example\n"}`), @@ -385,7 +391,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { t.Errorf("a module's own refusal: %+v, want de-spiegel refused alone, the node, mailu and proxy not", a) } for _, m := range []string{"lemurs", "mailu", "proxy"} { - if !slices.ContainsFunc(a.Added, func(x delivery.ModuleAdded) bool { return x.Module == m }) { + if !slices.ContainsFunc(a.Added, func(x nodeengine.CandidateAdded) bool { return x.Module == m }) { t.Errorf("%s, which composes, is not judged beside de-spiegel's refusal: %+v", m, a) } } @@ -425,11 +431,11 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { t.Errorf("held %v, want de-spiegel alone, in its config form's words, composed without its routes", held) } for _, m := range []string{"lemurs", "mailu", "proxy"} { - if key, _ := delivery.ComposedKey("n1", m, fps[m]); got[key].Module != m { + if key, _ := engcomp.Key("n1", m, fps[m]); got[key].Module != m { t.Errorf("%s is not composed beside de-spiegel's hold: %v", m, sortedKeys(got)) } } - carries := func(c delivery.Composed, what string) { + carries := func(c engcomp.Composed, what string) { t.Helper() ownRes, _ := json.Marshal(c.Resources) for _, raw := range spiegelRuns { @@ -450,7 +456,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { if err != nil || !strings.Contains(held["de-spiegel"], "${config:praktijk-name}") { t.Fatalf("de-spiegel leaving the fold: %v, held %v", err, held) } - spiegelKey, _ := delivery.ComposedKey("n1", "de-spiegel", fps["de-spiegel"]) + spiegelKey, _ := engcomp.Key("n1", "de-spiegel", fps["de-spiegel"]) if _, ok := got[spiegelKey]; ok { t.Errorf("held de-spiegel's own composition is handed") } @@ -481,11 +487,11 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { rec.OutwardLinks = []string{"eth0"} b.records["n1"] = rec fps = map[string]string{"lemurs": fp("e"), "nft": fp("f"), "site": fp("8")} - siteRuns := delivery.Composed{Node: "n1", Module: "site", FP: fps["site"], Resources: []json.RawMessage{ + siteRuns := engcomp.Composed{Node: "n1", Module: "site", FP: fps["site"], Resources: []json.RawMessage{ json.RawMessage(`{"id":"site.state","module":"site","type":"directory","path":"/var/lib/site","mode":"0700"}`), json.RawMessage(`{"id":"site.server","module":"site","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["10.77.0.1:20005:8080"]}`)}} - got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps) - nftKey, _ := delivery.ComposedKey("n1", "nft", fps["nft"]) + got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []engcomp.Composed{siteRuns}}, fps) + nftKey, _ := engcomp.Key("n1", "nft", fps["nft"]) if err != nil || held["site"] == "" || held["nft"] != "" || got[nftKey].Module != "nft" { t.Fatalf("site, its own secret never issued: %v, held %v; want site held alone, and the filter composed", err, held) } @@ -521,8 +527,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { b2.records["n1"] = rec b2.held = true fps["app"] = fp("9") - got, _, held, err = Compose(ctx, b2, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps) - appKey, _ := delivery.ComposedKey("n1", "app", fps["app"]) + got, _, held, err = Compose(ctx, b2, Mesh{}, Scope{Node: "n1", Running: []engcomp.Composed{siteRuns}}, fps) + appKey, _ := engcomp.Key("n1", "app", fps["app"]) if _, composed := got[appKey]; err != nil || held["site"] == "" || held["app"] != "" || !composed { t.Errorf("a consumer of held site: %v, held %v; want site held, and app composed against what runs of it", err, held) } @@ -560,7 +566,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { siteRuns.FP = fp("7") b.manifests[delivery.ManifestDigest(site)] = site b.builds["site "+fp("7")] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: delivery.ManifestDigest(site)} - got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps) + 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) } @@ -597,7 +603,7 @@ func otherNodeNotResolvedIsNamed(t *testing.T) { given[n] = &p } a := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given}, map[string]string{"cons": "src2:" + strings.Repeat("1", 64)}, nil, "p-h1", 1) - if a.Verdict != delivery.CandidateRefused || !strings.Contains(a.Refused, "prov on n2 lacks container-runtime") { + if a.Verdict != nodeengine.CandidateAnswerVerdictRefused || !strings.Contains(a.Refused, "prov on n2 lacks container-runtime") { t.Errorf("a refusal for want of n2's provider = %+v", a) } } diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index e793a40..4e6cb09 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -35,6 +35,7 @@ import ( "encoding/json" "errors" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "net/netip" "slices" @@ -146,7 +147,7 @@ type Scope struct { KeptAt map[string]string Placements map[string]*delivery.Placement InOwn []string - Running []delivery.Composed + Running []engcomp.Composed // HeldToday is, in an answer, the modules the node keeps today as they run ("node" for its own composition), each with // how ("held", "failed", "runs a build of the old form"): a candidate that cannot compose one, or holds one only // behind one of them, keeps it as today, which is said under CandidateAnswer.Held and is no refusal (issue 1126). @@ -165,7 +166,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 []delivery.Composed, p delivery.Placement) []string { +func InOwn(running []engcomp.Composed, p delivery.Placement) []string { placed := map[string]bool{} for _, m := range p.Modules { placed[m.Module] = true @@ -554,7 +555,7 @@ func (b base) finish(ctx context.Context, r Reader, build func(ctx context.Conte // why, and none of its own composition is in out, so the node keeps what it last applied of it and applies the rest; a // node never applies a stand-in. Only a refusal that is no single module's (the node's own resources, a value served // elsewhere, a stand-in for the node's own) refuses the node, err. -func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]delivery.Composed, skipped []string, held map[string]string, err error) { +func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]engcomp.Composed, skipped []string, held map[string]string, err error) { held = map[string]string{} // A node refused whole says what it held on the way, since the refusal can be of a held module's stub (issue 774, // 2026-10-30 19:30: node-tools held, then "its build produced 0 bundle(s)" with its own refusal unsaid). @@ -608,7 +609,7 @@ func ComposingInput(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[s // ComposeSent composes node from an input ComposingInput read, through the path Compose takes (composeEach, the read's holds // first): what a check's sandbox runs. False when the input says no way of composing node (an input from a node-engine // before ComposedHow): the caller cannot compose it as the node does. -func ComposeSent(in compose.Input, node string) (map[string]delivery.Composed, bool, error) { +func ComposeSent(in compose.Input, node string) (map[string]engcomp.Composed, bool, error) { c := in.ComposedHow if c == nil || c.Node != node { return nil, false, nil @@ -763,7 +764,7 @@ func parser(Mesh) func([]byte) (catalogue.Manifest, error) { // the builder holds it done. False where nothing is known of it (it runs no composition of its own, it runs the build // it is composed at, or its build is unread): the manifest it is composed from is its offer then. Only what it says to // others is taken of it, which needs no take-in of its artifacts. -func ranOf(ctx context.Context, r Reader, mesh Mesh, running []delivery.Composed, fps map[string]string) func(string) (catalogue.Manifest, bool) { +func ranOf(ctx context.Context, r Reader, mesh Mesh, running []engcomp.Composed, fps map[string]string) func(string) (catalogue.Manifest, bool) { parse := parser(mesh) return func(module string) (catalogue.Manifest, bool) { for _, c := range running { @@ -1168,7 +1169,7 @@ func claimsStore(m catalogue.Manifest) bool { } // keptOf is the operator export's file (catalogue.KeptID) of a module's composition as it runs, or nil. -func keptOf(c delivery.Composed) map[string]any { +func keptOf(c engcomp.Composed) map[string]any { for _, raw := range c.Resources { var r map[string]any if json.Unmarshal(raw, &r) == nil && r["id"] == c.Module+"."+resolution.KeptID() { @@ -1180,7 +1181,7 @@ func keptOf(c delivery.Composed) map[string]any { // membershipOf is the sealed value of the bus membership resource (catalogue.BusMembershipID) of a node's own // composition, or "". -func membershipOf(own delivery.Composed) string { +func membershipOf(own engcomp.Composed) string { for _, raw := range own.Resources { var r struct { ID string `json:"id"` @@ -1244,7 +1245,7 @@ func withoutHeldData(in compose.Input, node string) compose.Input { // runningProvisioner names, on node, the account its provisioner client runs as now (compose.Node.RunningProvisioner): the // account its own composition as it runs names, else the user of the running process of the module that runs the client. -func runningProvisioner(in *compose.Input, node string, running []delivery.Composed) { +func runningProvisioner(in *compose.Input, node string, running []engcomp.Composed) { account := "" for _, c := range running { if c.Module == "" && c.Own.Provisioner != "" { diff --git a/internal/compose/frombus/frombus_test.go b/internal/compose/frombus/frombus_test.go index 0ead30e..78f1a46 100644 --- a/internal/compose/frombus/frombus_test.go +++ b/internal/compose/frombus/frombus_test.go @@ -11,6 +11,7 @@ import ( "encoding/json" "errors" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "github.com/novox/mesh-host/internal/compositions" "os" "reflect" @@ -282,7 +283,7 @@ func TestInput(t *testing.T) { // Issue 1126: a candidate placement equal to the node's composes what it runs, and CompareComposed says so; one // thing changed in the group's candidate of another node (the hub's endpoint) names exactly the module it moves // here. Placed puts the same placement over the store's input unchanged. - running := make([]delivery.Composed, 0, len(fromBus)) + running := make([]compositions.Composed, 0, len(fromBus)) for _, c := range fromBus { running = append(running, c) } @@ -346,7 +347,7 @@ func TestInput(t *testing.T) { if err != nil { t.Fatal(err) } - var runsNow []delivery.Composed + var runsNow []compositions.Composed for _, c := range runs { runsNow = append(runsNow, c) } @@ -357,12 +358,12 @@ func TestInput(t *testing.T) { if err != nil { t.Fatal(err) } - var consumerRuns []delivery.Composed + var consumerRuns []compositions.Composed for _, c := range objectsRun { consumerRuns = append(consumerRuns, c) } - withoutOwn := func(cs []delivery.Composed) []delivery.Composed { - var out []delivery.Composed + withoutOwn := func(cs []compositions.Composed) []compositions.Composed { + var out []compositions.Composed for _, c := range cs { if c.Module != "" { out = append(out, c) @@ -384,7 +385,7 @@ func TestInput(t *testing.T) { p := tunnelled().placements["n1"] return map[string]*delivery.Placement{"n1": &p} } - folded := func() []delivery.Composed { + folded := func() []compositions.Composed { b := tunnelled() in, err := Input(ctx, b, Mesh{}, Scope{Node: "n1"}) if err != nil { @@ -417,11 +418,11 @@ func TestInput(t *testing.T) { raw, _ := json.Marshal(r) resources = append(resources, raw) } - split, err := delivery.SplitComposed("n1", resources, map[string]string{"keeper": oneFps["keeper"]}, nil, delivery.NodeOwn{}) + split, err := compositions.Split("n1", resources, map[string]string{"keeper": oneFps["keeper"]}, nil, compositions.NodeOwn{}) if err != nil { t.Fatal(err) } - var out []delivery.Composed + var out []compositions.Composed for _, c := range split { out = append(out, c) } @@ -437,20 +438,20 @@ func TestInput(t *testing.T) { for name, c := range map[string]struct { b *bus given map[string]*delivery.Placement - running []delivery.Composed + running []compositions.Composed verdict string - check func(delivery.CandidateAnswer) bool + check func(nodeengine.CandidateAnswer) bool }{ - "the same": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), runsNow, delivery.CandidateSame, - func(a delivery.CandidateAnswer) bool { + "the same": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), runsNow, nodeengine.CandidateAnswerVerdictSame, + func(a nodeengine.CandidateAnswer) bool { return len(a.Same) == 3 && a.Proposal == "p-h1" && a.Revision == 7 }}, - "the node runs no own composition": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), withoutOwn(runsNow), delivery.CandidateSame, - func(a delivery.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }}, - "one removed": {one("keeper", "lemurs"), placedAs("keeper"), runsNow, delivery.CandidateDiffers, - func(a delivery.CandidateAnswer) bool { return reflect.DeepEqual(a.Removed, []string{"lemurs"}) }}, + "the node runs no own composition": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), withoutOwn(runsNow), nodeengine.CandidateAnswerVerdictSame, + func(a nodeengine.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }}, + "one removed": {one("keeper", "lemurs"), placedAs("keeper"), runsNow, nodeengine.CandidateAnswerVerdictDiffers, + func(a nodeengine.CandidateAnswer) bool { return reflect.DeepEqual(a.Removed, []string{"lemurs"}) }}, "one added, its secret not yet issued": {func() *bus { b := one("lemurs"); b.own = nil; return b }(), placedAs("keeper", "lemurs"), - nil, delivery.CandidateDiffers, func(a delivery.CandidateAnswer) bool { + nil, nodeengine.CandidateAnswerVerdictDiffers, func(a nodeengine.CandidateAnswer) bool { for _, m := range a.Added { if m.Module == "keeper" { return reflect.DeepEqual(m.Unissued, []string{"keeper/password"}) && len(a.Refusals) == 0 @@ -459,27 +460,27 @@ func TestInput(t *testing.T) { return false }}, "a value missing for a module that runs": {func() *bus { b := one("keeper", "lemurs"); b.own = nil; return b }(), placedAs("keeper", "lemurs"), - runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool { + runsNow, nodeengine.CandidateAnswerVerdictRefused, func(a nodeengine.CandidateAnswer) bool { return len(a.Refusals) == 1 && a.Refusals[0].Module == "keeper" && strings.Contains(a.Refusals[0].Why, "keeper/password") }}, // Review of L2b, round 1, finding 1: a consumer the candidate adds has no credential yet, and the vault refuses an // ask for a module not on the node; it is not asked, and the module is added, its credential not yet issued. - "a consumer added, the vault not asked for it": {one("objects"), placedAs("objects", "files"), consumerRuns, delivery.CandidateDiffers, - func(a delivery.CandidateAnswer) bool { + "a consumer added, the vault not asked for it": {one("objects"), placedAs("objects", "files"), consumerRuns, nodeengine.CandidateAnswerVerdictDiffers, + func(a nodeengine.CandidateAnswer) bool { return len(a.Added) == 1 && a.Added[0].Module == "files" && len(a.Added[0].Unissued) > 0 && len(a.Refusals) == 0 }}, // And with no trunk at all (the seed), a stand-in of a module the node runs is a refusal, not an addition. "the seed, a value missing for a module that runs": {func() *bus { b := one(); b.own = nil; return b }(), placedAs("keeper", "lemurs"), - runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool { + 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, delivery.CandidateRefused, - func(a delivery.CandidateAnswer) bool { return a.Refused != "" }}, + "cannot compose": {one("keeper"), map[string]*delivery.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 // neither is added. "mesh-wireguard and an old-form module, folded": {tunnelled(), tunnelledAs(), - folded, delivery.CandidateSame, func(a delivery.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }}, + folded, nodeengine.CandidateAnswerVerdictSame, func(a nodeengine.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }}, } { fps := oneFps if name == "mesh-wireguard and an old-form module, folded" { @@ -489,7 +490,11 @@ func TestInput(t *testing.T) { if a.Verdict != c.verdict || !c.check(a) { t.Errorf("%s: %+v", name, a) } - if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}}); err != nil { + var wireAnswer delivery.CandidateAnswer // the form mesh-sdk's have still holds, until the Have group moves + if wire, err := json.Marshal(a); err != nil || json.Unmarshal(wire, &wireAnswer) != nil { + t.Fatalf("%s: the answer does not read back: %v", name, err) + } + if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": wireAnswer}}); err != nil { t.Errorf("%s: the answer is not one a have holds: %v (%s)", name, err, raw) } } @@ -658,7 +663,7 @@ func TestInput(t *testing.T) { if !reflect.DeepEqual(builtBus.fetched, []string{"http://127.0.0.1:5000/v2/restic/files/blobs/" + archiveDigest}) { t.Errorf("the templates archive was read at %v", builtBus.fetched) } - resticKey, _ := delivery.ComposedKey("n1", "restic", movedFps["restic"]) + resticKey, _ := compositions.Key("n1", "restic", movedFps["restic"]) if c := fromBuild[resticKey]; len(c.Filled) != 4 { // its three templated files and its own secret t.Errorf("restic is not handed to the provisioner: its filled files are %v", c.Filled) } @@ -875,7 +880,7 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates { // composed as what runs shows, said, by what runs and never by a name; a manifest that says it, or nothing running so, marks // nothing. func whatRunsIsKeptWhileAManifestDoesNotSayIt(t *testing.T) { - running := []delivery.Composed{ + running := []compositions.Composed{ {Node: "a", Module: "runner-x", Resources: []json.RawMessage{json.RawMessage(`{"id":"runner-x.runtime","type":"process"}`)}}, {Node: "a", Module: "prov-y", Resources: []json.RawMessage{json.RawMessage(`{"id":"prov-y.need-broker","type":"file","vault-sealed":{"box":"b"}}`)}}, {Node: "a", Module: "app", Resources: []json.RawMessage{json.RawMessage(`{"id":"app.need-broker","type":"file"}`)}}, diff --git a/internal/compose/frombus/frombustest/frombustest_test.go b/internal/compose/frombus/frombustest/frombustest_test.go index b6b8cdf..9598824 100644 --- a/internal/compose/frombus/frombustest/frombustest_test.go +++ b/internal/compose/frombus/frombustest/frombustest_test.go @@ -3,6 +3,7 @@ package frombustest import ( "context" "encoding/json" + engcomp "github.com/novox/mesh-host/internal/compositions" "reflect" "slices" "strings" @@ -105,7 +106,7 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { if err != nil { t.Fatalf("%s: the node's composition: %v", row.name, err) } - if key, _ := delivery.ComposedKey(Node, "restic", fps["restic"]); out[key].Module != "restic" { + if key, _ := engcomp.Key(Node, "restic", fps["restic"]); out[key].Module != "restic" { t.Errorf("%s: restic is not composed (held %v)", row.name, held) } if why, ok := held["backup"]; (row.bad == "" && len(held) > 0) || (row.bad != "" && (!ok || !strings.Contains(why, row.why) || len(held) != 1)) { @@ -214,14 +215,14 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) { t.Fatal(err) } fps, ownKey := FPs(), "" - ownKey, _ = delivery.ComposedNodeKey(Node) - resticKey, _ := delivery.ComposedKey(Node, "restic", fps["restic"]) + ownKey, _ = engcomp.NodeKey(Node) + resticKey, _ := engcomp.Key(Node, "restic", fps["restic"]) before, _, _, err := frombus.Compose(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node}, fps) account := before[ownKey].Own.Provisioner if err != nil || account == "" || len(before[resticKey].Filled) == 0 { t.Fatalf("the node composes with its client: account %q, restic's filled files %v: %v", account, before[resticKey].Filled, err) } - var running []delivery.Composed + var running []engcomp.Composed for _, c := range before { running = append(running, c) } diff --git a/internal/compose/frombus/held_rows_test.go b/internal/compose/frombus/held_rows_test.go index 46cd176..0aa2fe2 100644 --- a/internal/compose/frombus/held_rows_test.go +++ b/internal/compose/frombus/held_rows_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "strings" "testing" @@ -122,7 +123,7 @@ func aRunningModuleWaitsForItsBuild(t *testing.T) { store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"claims":[{"name":"container-image-registry","scope":"mesh"}],"listens":[{"name":"registry","port":5100,"from":"mesh"}],` + `"receives":{"artifact-store":"/var/lib/artifacts/received.json"}}` old, next := "src2:"+strings.Repeat("a", 64), "src2:"+strings.Repeat("b", 64) - running := []delivery.Composed{{Node: "n1", Module: "node-tools", FP: old, Resources: []json.RawMessage{ + running := []engcomp.Composed{{Node: "n1", Module: "node-tools", FP: old, Resources: []json.RawMessage{ json.RawMessage(`{"id":"node-tools.runtime","module":"node-tools","type":"process","name":"node-tools","env":{"MESH_TOOL_MODULES":""}}`), json.RawMessage(`{"id":"node-tools.bundle-runtime","module":"node-tools","type":"archive","source":"artifact-store://node-tools/runtime/blobs/` + dg("1") + `","digest":"` + dg("1") + `","path":"` + resolution.BundlePath("node-tools", "runtime") + `"}`)}}} @@ -156,14 +157,14 @@ func aRunningModuleWaitsForItsBuild(t *testing.T) { t.Errorf("%s: the node is refused: %v", row.name, err) continue } - if lampKey, _ := delivery.ComposedKey("n1", "lamp", fps["lamp"]); out[lampKey].Module != "lamp" { + if lampKey, _ := engcomp.Key("n1", "lamp", fps["lamp"]); out[lampKey].Module != "lamp" { t.Errorf("%s: lamp is not composed (held %v)", row.name, held) } if why := held["node-tools"]; (row.held == "") != (why == "") || !strings.Contains(why, row.held) { t.Errorf("%s: node-tools held %q, want %q", row.name, why, row.held) } if row.digest != "" { - key, _ := delivery.ComposedKey("n1", "node-tools", row.want) + key, _ := engcomp.Key("n1", "node-tools", row.want) if c := out[key]; !strings.Contains(fmt.Sprint(rawStrings(c.Resources)), row.digest) { t.Errorf("%s: node-tools is composed as %v, want its bundle %s", row.name, rawStrings(c.Resources), row.digest) } @@ -267,7 +268,7 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) { // A held module keeps what it contributes (issue 774, 2026-10-31: nextcloud held for its admin secret dropped out // of postgres's received file, and postgres's helper would have locked its login): only its own secrets are left // out of its stand-in, never its contributions, so the provider's received file names every consumer. - storeKey, _ := delivery.ComposedKey("n1", "artifacts", fps["artifacts"]) + storeKey, _ := engcomp.Key("n1", "artifacts", fps["artifacts"]) // And never a stand-in for a grant not yet issued: the provider is applied, so a held module whose grant is // not issued is composed without that contribution, never with "foreseen: made by the next send". for key, c := range out { @@ -284,7 +285,7 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) { } } for _, m := range []string{"node-tools", "check", "lamp", "heal"} { - key, _ := delivery.ComposedKey("n1", m, fps[m]) + key, _ := engcomp.Key("n1", m, fps[m]) _, composed := out[key] why, isHeld := held[m] want, wantHeld := row.held[m] diff --git a/internal/compose/frombus/keep.go b/internal/compose/frombus/keep.go index 08f11fa..8f70c60 100644 --- a/internal/compose/frombus/keep.go +++ b/internal/compose/frombus/keep.go @@ -3,13 +3,13 @@ package frombus import ( "encoding/json" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "slices" "sort" "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" ) @@ -25,7 +25,7 @@ import ( // keepWhatRuns marks, in in, each module placed on node that what runs shows is the tool runner or carries the core's // credentials, where its manifest does not say so; it returns, sorted by module, what it marked, in words. The tool runner is // marked only where no placed module's manifest says it is one. -func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) []KeptAsRuns { +func keepWhatRuns(in *compose.Input, node string, running []engcomp.Composed) []KeptAsRuns { placed := assignedOn(*in, node) if len(placed) == 0 || len(running) == 0 { return nil diff --git a/internal/compose/input.go b/internal/compose/input.go index 826e1e8..c33657e 100644 --- a/internal/compose/input.go +++ b/internal/compose/input.go @@ -19,11 +19,11 @@ import ( "encoding/json" "errors" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // Input is everything the composition reads. Each field is what one read of the controller's inventory answers, named @@ -224,7 +224,7 @@ type Node struct { // there (InOwn) whose manifest names a build (NamesBuild) runs a build of the old form only the controller holds, // so it is left to the node: Compositions composes it as a stub (Stub) and carries its resources as they run, each // after the resource it runs after (issue 1126). Nil composes every folded module from its manifest, as before. - RunsOwn *delivery.Composed + RunsOwn *engcomp.Composed // RunningProvisioner is the account the provisioner's client runs as on this node now, read from what runs (its own // composition's provisioner account, else the user of the provisioner client's running process), empty when nothing // says it. A client held as its stub this pass composes no process, so the node keeps this account and its handing @@ -254,7 +254,7 @@ type ComposedHow struct { // Held is each module the read held, with why: composed as what runs of it, and the rest without it. Held map[string]string // Running is what runs of each held module, its own composition, by which it is carried as it runs. - Running []delivery.Composed + Running []engcomp.Composed // HeldBefore is each module the node holds composing without the change (the candidate's base), held by the read or // blamed later for a refusal: the change refuses none of them (issue 774, review round 1). HeldBefore map[string]string diff --git a/internal/composer/composer_test.go b/internal/composer/composer_test.go index 18c5309..a2a2101 100644 --- a/internal/composer/composer_test.go +++ b/internal/composer/composer_test.go @@ -3,6 +3,7 @@ package composer import ( "context" "encoding/json" + engcomp "github.com/novox/mesh-host/internal/compositions" "os" "reflect" "strings" @@ -122,7 +123,7 @@ func TestARequestItCannotRunIsRefused(t *testing.T) { t.Fatal(err) } node := compose.Node{Name: "a", Assigned: []string{"bus"}, InOwn: map[string]bool{"bus": true}, - RunsOwn: &delivery.Composed{Node: "a"}} + RunsOwn: &engcomp.Composed{Node: "a"}} in := compose.Input{Catalogue: map[string]catalogue.Manifest{"bus": man}, Nodes: []compose.Node{node}} request := func(in compose.Input) Request { return withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose, diff --git a/internal/compositions/answer_paths.go b/internal/compositions/answer_paths.go index a3217a4..001ed30 100644 --- a/internal/compositions/answer_paths.go +++ b/internal/compositions/answer_paths.go @@ -3,7 +3,6 @@ package compositions import ( "bytes" "encoding/json" - "git.novox.be/novox/mesh-sdk/go/delivery" "sort" "strconv" "strings" @@ -31,9 +30,9 @@ func DifferingPaths(composed, running json.RawMessage) []string { var out []string walkDiffering(a, b, "", &out) sort.Strings(out) - if len(out) > delivery.MaxPaths { - more := len(out) - (delivery.MaxPaths - 1) - out = append(out[:delivery.MaxPaths-1], "… "+strconv.Itoa(more)+" more") + if len(out) > MaxPaths { + more := len(out) - (MaxPaths - 1) + out = append(out[:MaxPaths-1], "… "+strconv.Itoa(more)+" more") } return out } diff --git a/internal/compositions/candidate.go b/internal/compositions/candidate.go new file mode 100644 index 0000000..0bed69d --- /dev/null +++ b/internal/compositions/candidate.go @@ -0,0 +1,53 @@ +package compositions + +import ( + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient" +) + +// The bounds of a module's shown bodies in a candidate answer, and of one answer as the node's key holds it (the node-engine +// seat's state have, key `.node`, takes a value of about 1 MiB and holds every proposal's answer beside the node's +// facts, so one answer keeps to a sixth). +const ( + MaxBodies = 20 + MaxBody = 8192 + MaxPaths = 64 + MaxAnswerBytes = 160 << 10 +) + +// ReturnsToNothing is what a node answers a proposal that takes its placement key away. +const ReturnsToNothing = "the proposal takes this node's placement away: it composes nothing then" + +// NodeWord is the last part of a node's own key, and the name of its own composition in an answer. +const NodeWord = "node" + +// VerdictOf is the verdict of an answer's lists: refused on any refusal, differs on any difference, addition or +// removal, same otherwise. +func VerdictOf(a seat.CandidateAnswer) string { + switch { + case a.Refused != "" || len(a.Refusals) > 0: + return seat.CandidateAnswerVerdictRefused + case len(a.Differs)+len(a.Added)+len(a.Removed) > 0: + return seat.CandidateAnswerVerdictDiffers + } + return seat.CandidateAnswerVerdictSame +} + +// WithValuesDiffer is answers with each one's ValuesDiffer counted: how many shown bodies hold a value the node opened on +// both sides to two values (the vault's value and the one that runs drifted apart; issue 1126, cause C4). +func WithValuesDiffer(answers map[string]seat.CandidateAnswer) map[string]seat.CandidateAnswer { + out := make(map[string]seat.CandidateAnswer, len(answers)) + for g, a := range answers { + var shown []meshvaultclient.ModuleBodies + for _, d := range a.Differs { + m := meshvaultclient.ModuleBodies{Module: d.Module} + for _, b := range d.Bodies { + m.Bodies = append(m.Bodies, meshvaultclient.Body{ID: b.ID, Composed: b.Composed, Running: b.Running}) + } + shown = append(shown, m) + } + a.ValuesDiffer = int64(meshvaultclient.ValuesDiffer(shown)) + out[g] = a + } + return out +} diff --git a/internal/compositions/compare_test.go b/internal/compositions/compare_test.go index fa5bc42..8a3c1e5 100644 --- a/internal/compositions/compare_test.go +++ b/internal/compositions/compare_test.go @@ -4,11 +4,10 @@ import ( "bytes" "encoding/json" "fmt" + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "slices" "strings" "testing" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // What the node-engine finds when it compares the compositions it made with the ones it runs (issue 1126): the bus's user @@ -21,11 +20,11 @@ func TestComparingCompositions(t *testing.T) { return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"0600","content":"` + content + `"}`) } fp := "src2:" + strings.Repeat("a", 64) - comp := func(rs ...json.RawMessage) delivery.Composed { - return delivery.Composed{Node: "novox", Module: "nats", FP: fp, Resources: rs} + comp := func(rs ...json.RawMessage) Composed { + return Composed{Node: "novox", Module: "nats", FP: fp, Resources: rs} } for name, tc := range map[string]struct { - composed, running delivery.Composed + composed, running Composed resource, why string }{ "user list alone": {comp(file("conf", "a"), file("nats.bus-users", "stand-in")), comp(file("conf", "a"), file("nats.bus-users", "real")), "", ""}, @@ -33,7 +32,7 @@ func TestComparingCompositions(t *testing.T) { "one side": {comp(file("conf", "a"), file("more", "x")), comp(file("conf", "a")), "more", "composed, and not in what runs"}, "order": {comp(file("conf", "a"), file("more", "x")), comp(file("more", "x"), file("conf", "a")), "", "the same resources in another order"}, } { - c := CompareComposed(map[string]delivery.Composed{"novox.nats." + fp: tc.composed}, []delivery.Composed{tc.running}) + c := CompareComposed(map[string]Composed{"novox.nats." + fp: tc.composed}, []Composed{tc.running}) if tc.why == "" { if len(c.Differs) != 0 || len(c.Same) != 1 { t.Errorf("%s: %+v", name, c) @@ -54,15 +53,15 @@ func TestComparingCompositions(t *testing.T) { sameBox := func(a, b string) (same, known bool) { return (a == "box-2" && b == "box-3") || a == b, true } - shown := CompareComposedWith(map[string]delivery.Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"), + shown := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"), file("c", "x"), sealedFile("d", "box-2", "0600"))}, - []delivery.Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"), sealedFile("d", "box-4", "0600"))}, + []Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"), sealedFile("d", "box-4", "0600"))}, sameBox) if len(shown.Differs) != 1 || len(shown.Differs[0].Bodies) != 3 { t.Fatalf("every differing resource is not shown, or b sealed again is shown: %+v", shown.Differs) } - if c := CompareComposedWith(map[string]delivery.Composed{"k": comp(sealedFile("b", "box-2", "0600"))}, - []delivery.Composed{comp(sealedFile("b", "box-3", "0600"))}, sameBox); len(c.Same) != 1 { + if c := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("b", "box-2", "0600"))}, + []Composed{comp(sealedFile("b", "box-3", "0600"))}, sameBox); len(c.Same) != 1 { t.Errorf("a value sealed again at its own slot is not the same: %+v", c) } for _, row := range []struct { @@ -73,7 +72,7 @@ func TestComparingCompositions(t *testing.T) { {"c", `"content":"x"`, "", []string{"content"}}, {"d", "", "box-2", []string{"sealed"}}, } { - var body delivery.ResourceBodies + var body seat.CandidateBodies for _, b := range shown.Differs[0].Bodies { if b.ID == row.id { body = b @@ -93,8 +92,8 @@ func TestComparingCompositions(t *testing.T) { `"id":"grafana.ini","mode":"0600","module":"nats","path":"/etc/grafana/grafana.ini","secrets":{"postgres-database":"` + db + `","secret-key":"` + key + `"},"type":"file"}`) } - if c := CompareComposedWith(map[string]delivery.Composed{"k": comp(withSecrets("box-2", "box-1"))}, - []delivery.Composed{comp(withSecrets("box-3", "box-1"))}, sameBox); len(c.Same) != 1 { + if c := CompareComposedWith(map[string]Composed{"k": comp(withSecrets("box-2", "box-1"))}, + []Composed{comp(withSecrets("box-3", "box-1"))}, sameBox); len(c.Same) != 1 { t.Errorf("a secret sealed again at its own entry is not the same: %+v", c) } if kept := KeepSeals(comp(withSecrets("box-2", "box-1")), SealsIn(comp(withSecrets("box-3", "box-1"))), sameBox); !bytes.Equal(kept.Resources[0], withSecrets("box-3", "box-1")) { @@ -105,8 +104,8 @@ func TestComparingCompositions(t *testing.T) { } // Another value at one entry differs there alone, each entry masked by what it opens to. // box-2 and box-4 open to one value, so the box that runs is kept there; box-5 and box-6 are two values. - mixed := CompareComposedWith(map[string]delivery.Composed{"k": comp(withSecrets("box-2", "box-5"))}, - []delivery.Composed{comp(withSecrets("box-4", "box-6"))}, func(a, b string) (bool, bool) { + mixed := CompareComposedWith(map[string]Composed{"k": comp(withSecrets("box-2", "box-5"))}, + []Composed{comp(withSecrets("box-4", "box-6"))}, func(a, b string) (bool, bool) { return (a == "box-2" && b == "box-4") || a == b, true }) if len(mixed.Differs) != 1 || len(mixed.Differs[0].Bodies) != 1 { @@ -128,7 +127,7 @@ func TestComparingCompositions(t *testing.T) { return json.RawMessage(`{"id":"core-box","type":"file","module":"nats","path":"/etc/core","mode":"0600","vault-sealed":{"v":1,"eph":"e-` + ct + `","ct":"` + ct + `"}}`) } - vault := CompareComposedWith(map[string]delivery.Composed{"k": comp(box("c1"))}, []delivery.Composed{comp(box("c2"))}, + vault := CompareComposedWith(map[string]Composed{"k": comp(box("c1"))}, []Composed{comp(box("c2"))}, func(a, b string) (bool, bool) { return false, strings.Contains(a, `"ct":"c1"`) && strings.Contains(b, `"ct":"c2"`) }) @@ -141,14 +140,14 @@ func TestComparingCompositions(t *testing.T) { } // Issue 1126, T1: the paths are found on the whole bodies, before they are cut; JSON in a string is walked into, a // key a secret could be under is named and never shown. - long := strings.Repeat("x", delivery.MaxBody) + long := strings.Repeat("x", MaxBody) conf := func(link, password string) json.RawMessage { inner, _ := json.Marshal(map[string]any{"pad": long, "links": []string{"a", link}, "password": password}) outer, _ := json.Marshal(map[string]any{"id": "conf", "type": "file", "module": "nats", "path": "/etc/conf", "mode": "0600", "content": string(inner)}) return outer } - far := CompareComposedWith(map[string]delivery.Composed{"k": comp(conf("b", "hunter2"))}, []delivery.Composed{comp(conf("c", "hunter3"))}, nil) + far := CompareComposedWith(map[string]Composed{"k": comp(conf("b", "hunter2"))}, []Composed{comp(conf("c", "hunter3"))}, nil) if len(far.Differs) != 1 || len(far.Differs[0].Bodies) != 1 || !slices.Equal(far.Differs[0].Bodies[0].Paths, []string{"content.links.1", "content.password"}) || !strings.Contains(far.Differs[0].Bodies[0].Composed, "cut at") || strings.Contains(fmt.Sprint(far), "hunter") { @@ -160,10 +159,10 @@ func TestComparingCompositions(t *testing.T) { !strings.Contains(c, "") { t.Errorf("masked as %s and %s", c, r) } - if c := CompareComposed(nil, []delivery.Composed{{Node: "novox", Resources: []json.RawMessage{}}}); len(c.Removed) != 1 || c.Removed[0] != "node" { + if c := CompareComposed(nil, []Composed{{Node: "novox", Resources: []json.RawMessage{}}}); len(c.Removed) != 1 || c.Removed[0] != "node" { t.Errorf("the node's own composition run and not composed is removed: %+v", c) } - if c := CompareComposed(map[string]delivery.Composed{"k": comp(file("conf", "a"))}, nil); len(c.Added) != 1 || c.Added[0].Module != "nats" || c.Added[0].Composed == "" { + if c := CompareComposed(map[string]Composed{"k": comp(file("conf", "a"))}, nil); len(c.Added) != 1 || c.Added[0].Module != "nats" || c.Added[0].Composed == "" { t.Errorf("a composition not run is added: %+v", c) } } diff --git a/internal/compositions/composed.go b/internal/compositions/composed.go new file mode 100644 index 0000000..8107422 --- /dev/null +++ b/internal/compositions/composed.go @@ -0,0 +1,251 @@ +package compositions + +import ( + "encoding/json" + "fmt" + "regexp" + + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/declaration" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +var bucketNode = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + +// NodeOwn is the node's own parts in a composition (the node-engine seat's state composed, key `.node`): its layout +// is the seat's contract (nodeengine.NodeOwn); the engine holds them as the declaration package's types. +type NodeOwn struct { + Adoption *declaration.Adoption `json:"adoption,omitempty"` + Identities []declaration.Identity `json:"identities,omitempty"` + Groups []declaration.Group `json:"groups,omitempty"` + Libraries []declaration.Library `json:"libraries,omitempty"` + Provisioner string `json:"provisioner_account,omitempty"` +} + +// Composed is a composition (the node-engine seat's state composed): one module's resources composed for one node at one +// fingerprint, or, with Module and FP empty, the node's own. Its codec, digest and keys are the node-engine client's +// (nodeengineclient); the engine holds the plan as the catalogue's Plan, which the client carries as JSON. +type Composed struct { + V int + Node string + Module string + FP string + Data bool + Resources []json.RawMessage + Filled []string + Own NodeOwn + Plan *catalogue.Plan + Refused string +} + +// Bucket constants of a node's compositions (the state composed is one bucket per node). +const ( + // BucketPrefix starts the name of a node's bucket of compositions, BucketFor(node). + BucketPrefix = "composed-" + // History is the revisions a node's composition bucket keeps per key. + History = 5 + // Keep is how many of a module's newest fingerprints keep their compositions though no want names them. + Keep = 5 +) + +// BucketFor is the name of a node's bucket of compositions, "composed-". +func BucketFor(node string) (string, error) { + if node == "" { + return "", fmt.Errorf("compositions: the node name is empty; a node's composition bucket is named for its node") + } + if !bucketNode.MatchString(node) { + return "", fmt.Errorf("compositions: the node name %q is not one a bucket accepts: letters, digits, '_' and '-' only", node) + } + return BucketPrefix + node, nil +} + +func toClient(c Composed) (nodeengineclient.Composition, error) { + out := nodeengineclient.Composition{V: c.V, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data, + Resources: c.Resources, Filled: c.Filled, Refused: c.Refused} + raw, err := json.Marshal(c.Own) + if err != nil { + return out, err + } + if err := json.Unmarshal(raw, &out.Own); err != nil { + return out, fmt.Errorf("compositions: the node's own parts do not fit the seat's layout: %w", err) + } + if c.Plan != nil { + if out.Plan, err = json.Marshal(c.Plan); err != nil { + return out, err + } + } + return out, nil +} + +func fromClient(c nodeengineclient.Composition) (Composed, error) { + out := Composed{V: c.V, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data, Resources: c.Resources, + Filled: c.Filled, Refused: c.Refused} + raw, err := json.Marshal(c.Own) + if err != nil { + return out, err + } + if err := json.Unmarshal(raw, &out.Own); err != nil { + return out, fmt.Errorf("compositions: the node's own parts do not fit the engine's: %w", err) + } + if len(c.Plan) > 0 { + out.Plan = &catalogue.Plan{} + if err := json.Unmarshal(c.Plan, out.Plan); err != nil { + return out, fmt.Errorf("compositions: the plan does not fit: %w", err) + } + } + return out, nil +} + +// Encode is c as the client writes it. +func Encode(c Composed) ([]byte, error) { + cc, err := toClient(c) + if err != nil { + return nil, err + } + return nodeengineclient.EncodeComposition(cc) +} + +// Decode reads a composition as the client reads it. +func Decode(data []byte) (Composed, error) { + cc, err := nodeengineclient.DecodeComposition(data) + if err != nil { + return Composed{}, err + } + return fromClient(cc) +} + +// DigestOf is a composition's digest, "sha256:", of its encoding without its plan; a refused one has none. +func DigestOf(c Composed) (string, error) { + cc, err := toClient(c) + if err != nil { + return "", err + } + return nodeengineclient.ComposedDigest(cc) +} + +// Same says whether two compositions are the same value, their plans included. +func Same(a, b Composed) bool { + ca, errA := toClient(a) + cb, errB := toClient(b) + return errA == nil && errB == nil && nodeengineclient.SameComposition(ca, cb) +} + +// Key is the key of a module's composition for a node at a fingerprint: `..`. +func Key(node, module, fingerprint string) (string, error) { + part, err := meshcatalogclient.KeyPart(fingerprint) + if err != nil { + return "", err + } + return nodeengineclient.ComposedKey(node, module, part) +} + +// NodeKey is the key of a node's own composition: `.node`. +func NodeKey(node string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + return node + "." + nodeengineclient.NodeWord, nil +} + +// ParseKey is the inverse of Key and NodeKey: isNode is true for `.node`, with module and fingerprint empty. +func ParseKey(key string) (node, module, fingerprint string, isNode bool, err error) { + node, module, part, isNode, err := nodeengineclient.ParseComposedKey(key) + if err != nil || isNode { + return node, module, "", isNode, err + } + return node, module, meshcatalogclient.SourcePrefix2 + part, false, nil +} + +// ByDigest is c with every file over 64 KiB given by its digest and size, and the content of each by digest, for the +// writer to put in the node's object bucket (declaration.FileBucketFor) under declaration.ObjectName before it writes +// the composition. A file that is sealed, filled with secrets or written into another stays as it is. +func (c Composed) ByDigest() (Composed, map[string][]byte, error) { + out := c + out.Resources = make([]json.RawMessage, len(c.Resources)) + objects := map[string][]byte{} + for i, raw := range c.Resources { + out.Resources[i] = raw + r, err := declaration.ParseResource(raw) + if err != nil { + return Composed{}, nil, err + } + f, ok := r.(*declaration.File) + if !ok || !f.TravelsByDigest() { + continue + } + ref, body, err := f.AsReference() + if err != nil { + return Composed{}, nil, err + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(raw, &fields); err != nil { + return Composed{}, nil, err + } + delete(fields, "content") + delete(fields, "bytes") + fields["digest"], _ = json.Marshal(ref.Digest) + fields["size"], _ = json.Marshal(ref.Size) + rewritten, err := json.Marshal(fields) + if err != nil { + return Composed{}, nil, err + } + out.Resources[i] = rewritten + objects[ref.Digest] = body + } + return out, objects, nil +} + +// Split splits a node's composed resources into its compositions, by key (Key, NodeKey): each resource into the +// composition of the module it says, under that module's fingerprint in fps, and a resource that says no module into the +// node's own, with own. Each filled path goes with the module whose file declares it. The order of the resources is kept +// within each composition. A resource of a module fps names no fingerprint for is refused, naming it. +func Split(node string, resources []json.RawMessage, fps map[string]string, filled []string, own NodeOwn) (map[string]Composed, error) { + nodeKey, err := NodeKey(node) + if err != nil { + return nil, err + } + out := map[string]Composed{nodeKey: {Node: node, Own: own, Resources: []json.RawMessage{}}} + pathOf := map[string]string{} + for _, raw := range resources { + var head struct { + ID string `json:"id"` + Type string `json:"type"` + Module string `json:"module"` + Path string `json:"path"` + } + if err := json.Unmarshal(raw, &head); err != nil { + return nil, fmt.Errorf("compositions: a resource of %s is not JSON: %w", node, err) + } + key := nodeKey + if head.Module != "" { + fp, ok := fps[head.Module] + if !ok { + return nil, fmt.Errorf("compositions: the resource %q is of %q, whose fingerprint on %s is not given", head.ID, head.Module, node) + } + if key, err = Key(node, head.Module, fp); err != nil { + return nil, err + } + if _, ok := out[key]; !ok { + out[key] = Composed{Node: node, Module: head.Module, FP: fp} + } + } + if head.Type == string(declaration.TypeFile) && head.Module != "" { + pathOf[head.Path] = key + } + c := out[key] + c.Resources = append(c.Resources, raw) + out[key] = c + } + for _, path := range filled { + key, ok := pathOf[path] + if !ok { + return nil, fmt.Errorf("compositions: the provisioner fills %q on %s, and no module's file declares it", path, node) + } + c := out[key] + c.Filled = append(c.Filled, path) + out[key] = c + } + return out, nil +} diff --git a/internal/compositions/composed_compare.go b/internal/compositions/composed_compare.go index fc244e0..446021b 100644 --- a/internal/compositions/composed_compare.go +++ b/internal/compositions/composed_compare.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "errors" - "git.novox.be/novox/mesh-sdk/go/delivery" + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "io" "maps" "slices" @@ -18,8 +18,8 @@ import ( // now) and removed by it (run now, not composed). Each list is sorted by module. type Comparison struct { Same []string - Differs []delivery.ModuleDiffers - Added []delivery.ModuleAdded + Differs []seat.CandidateDiffers + Added []seat.CandidateAdded Removed []string } @@ -32,25 +32,25 @@ type Comparison struct { // difference names the first resource, by its id, whose content differs or that one side alone holds; when every // resource is equal it says what else differs (their order, the files filled, the data, the node's own parts, a file // carried by digest). -func CompareComposed(composed map[string]delivery.Composed, running []delivery.Composed) Comparison { +func CompareComposed(composed map[string]Composed, running []Composed) Comparison { return CompareComposedWith(composed, running, nil) } // CompareComposedWith is CompareComposed showing, for each module that differs, every differing resource with both // bodies masked (MaskedPair); sameValue, when given, says whether two sealed values open to the same value (the node // opens them with its own key), so a value sealed again reads as such. -func CompareComposedWith(composed map[string]delivery.Composed, running []delivery.Composed, sameValue func(a, b string) (same, known bool)) Comparison { - name := func(c delivery.Composed) string { +func CompareComposedWith(composed map[string]Composed, running []Composed, sameValue func(a, b string) (same, known bool)) Comparison { + name := func(c Composed) string { if c.Module == "" { - return delivery.HaveNodeWord + return NodeWord } return c.Module } - ours := map[string]delivery.Composed{} + ours := map[string]Composed{} for _, c := range composed { ours[name(c)] = c } - theirs := map[string]delivery.Composed{} + theirs := map[string]Composed{} for _, c := range running { theirs[name(c)] = c } @@ -67,8 +67,8 @@ func CompareComposedWith(composed map[string]delivery.Composed, running []delive b, runs := theirs[m] switch { case !runs: - dig, _ := delivery.ComposedDigestOf(withoutBusUsers(a)) - out.Added = append(out.Added, delivery.ModuleAdded{Module: m, Composed: dig}) + dig, _ := DigestOf(withoutBusUsers(a)) + out.Added = append(out.Added, seat.CandidateAdded{Module: m, Composed: dig}) case !composedHere: out.Removed = append(out.Removed, m) default: @@ -83,21 +83,21 @@ func CompareComposedWith(composed map[string]delivery.Composed, running []delive } // compareOne is one module's comparison: true when the digests agree, else what differs first. -func compareOne(module string, composed, running delivery.Composed, sameValue func(a, b string) (bool, bool)) (delivery.ModuleDiffers, bool) { +func compareOne(module string, composed, running Composed, sameValue func(a, b string) (bool, bool)) (seat.CandidateDiffers, bool) { composed, running = withoutBusUsers(composed), withoutBusUsers(running) // A sealed value is judged by the value it opens to, slot by slot (issue 1126, C1): a value sealed again at the same // field of the same resource is the box that runs, never a difference; and a box is never matched across resources. composed = KeepSeals(composed, SealsIn(running), sameValue) - dc, errC := delivery.ComposedDigestOf(composed) - dr, errR := delivery.ComposedDigestOf(running) + dc, errC := DigestOf(composed) + dr, errR := DigestOf(running) if errC != nil { - return delivery.ModuleDiffers{Module: module, Running: dr, Why: "the composition has no digest: " + errC.Error()}, false + return seat.CandidateDiffers{Module: module, Running: dr, Why: "the composition has no digest: " + errC.Error()}, false } if errR == nil && dc == dr { - return delivery.ModuleDiffers{}, true + return seat.CandidateDiffers{}, true } - differs := func(resource, why string) (delivery.ModuleDiffers, bool) { - return delivery.ModuleDiffers{Module: module, Composed: dc, Running: dr, Resource: resource, Why: why}, false + differs := func(resource, why string) (seat.CandidateDiffers, bool) { + return seat.CandidateDiffers{Module: module, Composed: dc, Running: dr, Resource: resource, Why: why}, false } // Words for the difference: resource by resource, each side with its large files by digest, so a file carried whole // and the same file carried by digest read as one. @@ -114,9 +114,9 @@ func compareOne(module string, composed, running delivery.Composed, sameValue fu for id := range rb { ids[id] = true } - var first delivery.ModuleDiffers + var first seat.CandidateDiffers found := false - var bodies []delivery.ResourceBodies + var bodies []seat.CandidateBodies for _, id := range slices.Sorted(maps.Keys(ids)) { x, inA := ra[id] y, inB := rb[id] @@ -135,11 +135,11 @@ func compareOne(module string, composed, running delivery.Composed, sameValue fu first, _ = differs(id, why) found = true } - if len(bodies) < delivery.MaxBodies { + if len(bodies) < MaxBodies { // The paths are found on the whole bodies, before either is cut (issue 1126, T1: three resources over // MaxBody could not be told apart). c, r := MaskedPair(x, y, sameValue) - bodies = append(bodies, delivery.ResourceBodies{ID: id, Why: why, Paths: DifferingPaths(x, y), + bodies = append(bodies, seat.CandidateBodies{ID: id, Why: why, Paths: DifferingPaths(x, y), Composed: cut(c), Running: cut(r)}) } } @@ -157,8 +157,8 @@ func compareOne(module string, composed, running delivery.Composed, sameValue fu case a.FP != b.FP: return differs("", "the fingerprint differs") } - if dA, err := delivery.ComposedDigestOf(a); err == nil { - if dB, err := delivery.ComposedDigestOf(b); err == nil && dA == dB { + if dA, err := DigestOf(a); err == nil { + if dB, err := DigestOf(b); err == nil && dA == dB { return differs("", "the same content, a large file carried whole on one side and by digest on the other") } } @@ -166,7 +166,7 @@ func compareOne(module string, composed, running delivery.Composed, sameValue fu } // withoutBusUsers is c without the bus's user list resource (its id `bus-users`, or `.bus-users`). -func withoutBusUsers(c delivery.Composed) delivery.Composed { +func withoutBusUsers(c Composed) Composed { out := c out.Resources = nil for _, raw := range c.Resources { @@ -204,10 +204,10 @@ func resourcesByID(resources []json.RawMessage) map[string][]byte { // cut is s cut at MaxBody bytes, saying so. func cut(s string) string { - if len(s) <= delivery.MaxBody { + if len(s) <= MaxBody { return s } - return s[:delivery.MaxBody] + "… (cut at " + strconv.Itoa(delivery.MaxBody) + " bytes)" + return s[:MaxBody] + "… (cut at " + strconv.Itoa(MaxBody) + " bytes)" } // secretWords are the words of a key whose value may hold a secret or a sealed one: masked in a shown body. diff --git a/internal/compositions/composed_seals.go b/internal/compositions/composed_seals.go index a3374da..da5e0f6 100644 --- a/internal/compositions/composed_seals.go +++ b/internal/compositions/composed_seals.go @@ -3,7 +3,6 @@ package compositions import ( "bytes" "encoding/json" - "git.novox.be/novox/mesh-sdk/go/delivery" "sort" "strconv" @@ -63,7 +62,7 @@ func sealsAt(fields map[string]json.RawMessage) []sealAt { } // SealsIn is every sealed value c carries, by slot; nil when it carries none. -func SealsIn(c delivery.Composed) Seals { +func SealsIn(c Composed) Seals { var out Seals for i, raw := range c.Resources { id, fields, ok := sealedFieldsOf(raw, i) @@ -86,7 +85,7 @@ func SealsIn(c delivery.Composed) Seals { // same value (known and same): a value the vault sealed again is then the box that runs, so the composition is the // one that runs (ADR 0547 §3, issue 1126). A slot kept holds nothing for, or whose value differs or cannot be opened, // keeps c's. Every other byte of a resource is kept as it is; c itself is not changed. -func KeepSeals(c delivery.Composed, kept Seals, same func(a, b string) (same, known bool)) delivery.Composed { +func KeepSeals(c Composed, kept Seals, same func(a, b string) (same, known bool)) Composed { if len(kept) == 0 || same == nil { return c } diff --git a/internal/compositions/packages.go b/internal/compositions/packages.go new file mode 100644 index 0000000..c371abe --- /dev/null +++ b/internal/compositions/packages.go @@ -0,0 +1,84 @@ +package compositions + +import ( + "encoding/json" + "fmt" + "sort" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/declaration" +) + +// PackagesBucket is the node-engine's state packages (the seat's state of the same name), one key per node. +const PackagesBucket = "mesh-host_" + seat.StatePackages + +// DeclaredPackages is a node's key of `packages` from the compositions its engine composed for the node's wants: +// each resource of type package, by its name and whether it is declared absent, with the modules that declare it so, +// in the mirror's layout and order (names sorted, absent after present, modules sorted). A resource's module is the +// one it names, else its composition's, else the guess from its id (declaration.ModuleGuess); a package of no module +// lists none. +func DeclaredPackages(node string, comps []Composed) (seat.PackagesByNodeValue, error) { + by := map[packageKey]map[string]bool{} + for _, c := range comps { + for _, raw := range c.Resources { + var r struct { + ID string `json:"id"` + Type string `json:"type"` + Package string `json:"package"` + Absent bool `json:"absent"` + Module string `json:"module"` + } + if err := json.Unmarshal(raw, &r); err != nil { + return seat.PackagesByNodeValue{}, fmt.Errorf("compositions: a resource of %s's composition %q is not readable: %w", node, c.Module, err) + } + if r.Type != "package" || r.Package == "" { + continue + } + k := packageKey{r.Package, r.Absent} + if by[k] == nil { + by[k] = map[string]bool{} + } + module := r.Module + if module == "" { + module = c.Module + } + if module == "" { + module, _ = declaration.ModuleGuess(r.ID) + } + if module != "" { + by[k][module] = true + } + } + } + v := seat.PackagesByNodeValue{Node: node, Packages: []seat.PackagesByNodeValuePackagesItem{}} + for _, k := range sortedPackageKeys(by) { + modules := make([]string, 0, len(by[k])) + for m := range by[k] { + modules = append(modules, m) + } + sort.Strings(modules) + v.Packages = append(v.Packages, seat.PackagesByNodeValuePackagesItem{Package: k.name, Modules: modules, Absent: k.absent}) + } + return v, nil +} + +// packageKey is a package as the layout tells it apart: its name, and whether it is declared absent. +type packageKey struct { + name string + absent bool +} + +// sortedPackageKeys is the keys of m by name, absent after present. +func sortedPackageKeys[V any](m map[packageKey]V) []packageKey { + keys := make([]packageKey, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + if keys[i].name != keys[j].name { + return keys[i].name < keys[j].name + } + return !keys[i].absent && keys[j].absent + }) + return keys +} diff --git a/internal/judge/aliases.go b/internal/judge/aliases.go index 589de07..bf0f30e 100644 --- a/internal/judge/aliases.go +++ b/internal/judge/aliases.go @@ -4,15 +4,19 @@ // the shapes it judges (a composition, a judgement's words, a candidate's answer) stay in mesh-sdk. package judge -import "git.novox.be/novox/mesh-sdk/go/delivery" +import ( + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/delivery" + engcomp "github.com/novox/mesh-host/internal/compositions" +) // The shapes of mesh-sdk's delivery package this package judges, by the names its code was written with. type ( - Composed = delivery.Composed + Composed = engcomp.Composed Judgement = delivery.Judgement - CandidateAnswer = delivery.CandidateAnswer - ModuleDiffers = delivery.ModuleDiffers - ResourceBodies = delivery.ResourceBodies + CandidateAnswer = nodeengine.CandidateAnswer + ModuleDiffers = nodeengine.CandidateDiffers + ResourceBodies = nodeengine.CandidateBodies ) const ( diff --git a/internal/judge/answer_test.go b/internal/judge/answer_test.go index 62535c6..bdd7d9f 100644 --- a/internal/judge/answer_test.go +++ b/internal/judge/answer_test.go @@ -1,6 +1,7 @@ package judge import ( + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "strings" "testing" @@ -11,7 +12,7 @@ import ( // 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) - big := CandidateAnswer{Proposal: "p-x1", Revision: 1, Verdict: delivery.CandidateDiffers, Differs: []ModuleDiffers{{ + 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}}}}} if same := FitAnswer(big, 1<<20); same.Note != "" || same.Differs[0].Bodies[0].Composed != long { @@ -19,7 +20,7 @@ func TestFitAnswer(t *testing.T) { } fitted := FitAnswer(big, 4<<10) if len(fitted.Differs[0].Bodies[0].Composed) != 0 || len(fitted.Differs[0].Bodies[0].Paths) != 2 || - fitted.Verdict != delivery.CandidateDiffers || !strings.Contains(fitted.Note, "cut") || big.Differs[0].Bodies[0].Composed == "" { + fitted.Verdict != nodeengine.CandidateAnswerVerdictDiffers || !strings.Contains(fitted.Note, "cut") || big.Differs[0].Bodies[0].Composed == "" { t.Errorf("fitted as %+v", fitted) } } diff --git a/internal/link/composedstate.go b/internal/link/composedstate.go index b069208..d639720 100644 --- a/internal/link/composedstate.go +++ b/internal/link/composedstate.go @@ -4,8 +4,8 @@ import ( "context" "errors" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" "github.com/novox/mesh-host/internal/selfcompose" @@ -14,9 +14,9 @@ import ( // ---- this node's composition state, in the bus's own storage (novox/hq ADR 0593 §6) -------------------------------- // composedStore binds the node's composition state `composed-` over the link open now: bound, never created (the -// grants part creates it, create-only, history delivery.ComposedHistory). +// grants part creates it, create-only, history engcomp.History). func composedStore(js nats.JetStreamContext, node string) (nats.KeyValue, error) { - bucket, err := delivery.ComposedBucketFor(node) + bucket, err := engcomp.BucketFor(node) if err != nil { return nil, err } diff --git a/internal/link/enginepackages.go b/internal/link/enginepackages.go index 3e7d16c..6f54cc4 100644 --- a/internal/link/enginepackages.go +++ b/internal/link/enginepackages.go @@ -5,8 +5,9 @@ import ( "encoding/json" "errors" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/compositions" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" ) @@ -14,8 +15,8 @@ import ( // PackagesBucket is the node-engine's declared state packages, created by the grants part from the engine's manifest as // its states health and data are: one key, the node's name, written by that node's engine alone, read by pacman as -// mesh-host.packages (delivery.EnginePackagesRead). -const PackagesBucket = "mesh-host_" + delivery.EnginePackagesState +// mesh-host.packages ("mesh-host." + nodeengine.StatePackages). +const PackagesBucket = compositions.PackagesBucket // packagesStore binds PackagesBucket over the link open now: bound, never created. func (l *natsLink) packagesStore() (nats.KeyValue, error) { @@ -84,9 +85,9 @@ func (p EnginePackages) Put(_ context.Context, key string, value []byte) error { type PackagesView struct { Node string `json:"node"` // Key is the node's key as the bus holds it (Source); null when the engine has written none. - Source string `json:"source"` - Key *delivery.DeclaredPackages `json:"key"` - KeyError string `json:"key_error,omitempty"` + Source string `json:"source"` + Key *nodeengine.PackagesByNodeValue `json:"key"` + KeyError string `json:"key_error,omitempty"` // Wrote says, for write, whether the key was written again (false: it already held what the engine composed). Wrote *bool `json:"wrote,omitempty"` Note string `json:"note,omitempty"` @@ -94,9 +95,9 @@ type PackagesView struct { // PackagesAnswer is the verb's answer from the key as read (nil: no key), with its read's error. func PackagesAnswer(node string, key []byte, keyErr error) PackagesView { - v := PackagesView{Node: node, Source: delivery.EnginePackagesRead} + v := PackagesView{Node: node, Source: "mesh-host." + nodeengine.StatePackages} if keyErr == nil && key != nil { - var d delivery.DeclaredPackages + var d nodeengine.PackagesByNodeValue if keyErr = json.Unmarshal(key, &d); keyErr == nil { v.Key = &d } diff --git a/internal/link/have.go b/internal/link/have.go index af8ac34..656de22 100644 --- a/internal/link/have.go +++ b/internal/link/have.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" sdkkeys "git.novox.be/novox/mesh-sdk/go/keys" "github.com/novox/mesh-host/internal/compositions" "sort" @@ -53,7 +54,7 @@ type haveState struct { // issue 1126): kept in its key so a restart reuses them and applies nothing again. nodeSealed compositions.Seals // nodeCandidates is the node's answer to each placement candidate proposal (novox/hq ADR 0559 §4), written into its key. - nodeCandidates map[string]delivery.CandidateAnswer + nodeCandidates map[string]nodeengine.CandidateAnswer // nodeComposes and nodeComposesWhy are whose compositions the node applies and why (HaveNode.Composes), written into // its key. nodeComposes, nodeComposesWhy string @@ -433,7 +434,7 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) { facts := *s.node facts.Composed = s.nodeComposed facts.SealedAt = s.nodeSealed - facts.Candidates = s.nodeCandidates + facts.Candidates = interimAnswers(s.nodeCandidates) facts.Composes, facts.ComposesWhy = s.nodeComposes, s.nodeComposesWhy if raw, err = delivery.EncodeHaveNode(facts); err == nil { want := facts @@ -743,7 +744,7 @@ func (q *Session) NoteNode(ctx context.Context, composed string, sealed composit // NoteCandidates takes the node's answers to the placement candidates, by proposal (its shadow composer's, novox/hq ADR // 0559 §4), and writes the node's key where it differs; a proposal left out is no longer answered. -func (q *Session) NoteCandidates(ctx context.Context, answers map[string]delivery.CandidateAnswer) { +func (q *Session) NoteCandidates(ctx context.Context, answers map[string]nodeengine.CandidateAnswer) { q.init() s := &q.haves s.mu.Lock() @@ -878,3 +879,21 @@ func nothingJudged(f ModuleFact, looked bool) delivery.Health { } return delivery.HealthUnknown } + +// interimAnswers is the node's candidate answers in the form mesh-sdk's HaveNode still holds them, until the node-engine's +// have is written through the seat's contract types (the Have group of the go/delivery rewrite): the two shapes share +// their wire names, so the JSON carries one into the other. +func interimAnswers(in map[string]nodeengine.CandidateAnswer) map[string]delivery.CandidateAnswer { + if in == nil { + return nil + } + raw, err := json.Marshal(in) + if err != nil { + return nil + } + var out map[string]delivery.CandidateAnswer + if json.Unmarshal(raw, &out) != nil { + return nil + } + return out +} diff --git a/internal/link/have_test.go b/internal/link/have_test.go index 11669f6..af02cab 100644 --- a/internal/link/have_test.go +++ b/internal/link/have_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" sdkkeys "git.novox.be/novox/mesh-sdk/go/keys" "github.com/novox/mesh-host/internal/compositions" "slices" @@ -612,7 +613,7 @@ func TestAnAppliedModulesHaveNamesTheFingerprintAndTheDigestAndFollowsItsHealth( t.Fatalf("the node key read back: %+v %v", n, ok) } // The node's answers to placement candidates ride in the same key (ADR 0559 §4), and go when no group is answered. - answers := map[string]delivery.CandidateAnswer{"p-1": {Proposal: "p-1", Revision: 1, Verdict: delivery.CandidateSame, Same: []string{"web"}}} + answers := map[string]nodeengine.CandidateAnswer{"p-1": {Proposal: "p-1", Revision: 1, Verdict: nodeengine.CandidateAnswerVerdictSame, Same: []string{"web"}}} q.NoteCandidates(context.Background(), answers) if n, err := delivery.DecodeHaveNode(l.kv["n1.node"].value); err != nil || n.Candidates["p-1"].Proposal != "p-1" || n.Composed != dig { t.Fatalf("the node key with its answers: %+v %v", n, err) diff --git a/internal/selfcompose/candidate.go b/internal/selfcompose/candidate.go index a2c102d..aaf30a3 100644 --- a/internal/selfcompose/candidate.go +++ b/internal/selfcompose/candidate.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "slices" "sort" @@ -173,7 +174,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi // reads every manifest in the mesh one by one, which off the control-node took about 35 s each (issue 1126). base.warm(ctx, c, cand) - var running []delivery.Composed + var running []engcomp.Composed if c.env.Running != nil { running = c.env.Running() } @@ -232,8 +233,8 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi case laidErr != nil: return refuse(laidErr.Error()) } - byModule := func(comps map[string]delivery.Composed) map[string]delivery.Composed { - out := map[string]delivery.Composed{} + byModule := func(comps map[string]engcomp.Composed) map[string]engcomp.Composed { + out := map[string]engcomp.Composed{} for _, comp := range comps { out[comp.Module] = comp } @@ -256,7 +257,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi if !ok { return m, false } - dig, err := delivery.ComposedDigestOf(comp) + dig, err := engcomp.DigestOf(comp) if err != nil { m.State, m.Words = nec.ModuleRefused, err.Error() return m, true @@ -264,7 +265,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi m.State, m.Fp, m.Digest = nec.ModuleComposed, comp.FP, dig if before, ok := was[module]; !ok { m.Changed = true - } else if d, err := delivery.ComposedDigestOf(before); err != nil || d != dig { + } else if d, err := engcomp.DigestOf(before); err != nil || d != dig { m.Changed = true } return m, true @@ -426,7 +427,7 @@ func (c *Composer) layCandidate(ctx context.Context, cand delivery.Candidate) (m // composeShadow composes this node from what c holds, as the shadow composes it (shadowBuilds): each module at the build // it runs, one placed and not run at the indexer's, and one the candidate laid (laid) at the candidate's. input, when // given, is handed the scope. -func (c *Composer) composeShadow(ctx context.Context, running []delivery.Composed, laid map[string]bool, input func(frombus.Scope, map[string]string)) (map[string]delivery.Composed, map[string]string, error) { +func (c *Composer) composeShadow(ctx context.Context, running []engcomp.Composed, laid map[string]bool, input func(frombus.Scope, map[string]string)) (map[string]engcomp.Composed, map[string]string, error) { node := c.env.Node c.mu.Lock() placement, placed := c.placements[node] diff --git a/internal/selfcompose/candidate_test.go b/internal/selfcompose/candidate_test.go index 9b8ed93..18433af 100644 --- a/internal/selfcompose/candidate_test.go +++ b/internal/selfcompose/candidate_test.go @@ -74,7 +74,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { var applied atomic.Int64 env.Refused = func(string, map[string]string) { applied.Add(1) } var handed atomic.Int64 - s := NewShadow(env, func(context.Context, map[string]delivery.CandidateAnswer) { handed.Add(1) }) + s := NewShadow(env, func(context.Context, map[string]nodeengine.CandidateAnswer) { handed.Add(1) }) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go func() { _ = s.Run(ctx) }() diff --git a/internal/selfcompose/composedstate.go b/internal/selfcompose/composedstate.go index 8da3ea6..5288b12 100644 --- a/internal/selfcompose/composedstate.go +++ b/internal/selfcompose/composedstate.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "sort" "strings" @@ -17,7 +18,7 @@ import ( ) // **The compositions this node makes, published in its composition state** (novox/hq ADR 0593 §6). The node's engine -// alone composes, and it writes every composition it makes into `composed-` (delivery.ComposedBucketFor), a +// alone composes, and it writes every composition it makes into `composed-` (engcomp.BucketFor), a // record for the provisioner, the assigner and the view, never an instruction: the engine applies only what it composes // itself, never a value read from the state, so a value carries no signature. After a pass that composed, it writes: // @@ -30,7 +31,7 @@ import ( // - a module refused alone at a fingerprint as that key with its refusal (Refused), and no resources. // // A key is written only when its value differs from what the state holds (read once per run), and a module's key is -// deleted once its fingerprint is not among the five it wrote last (delivery.ComposedKeep) and no want names it. A pass +// deleted once its fingerprint is not among the five it wrote last (engcomp.Keep) and no want names it. A pass // refused whole, or a bus that did not answer, writes nothing: what was written stands, and each module's have says why. // ComposedStore is this node's composition state, bound and never created (the grants part creates it). @@ -51,7 +52,7 @@ type ComposedEntry struct { type published struct { scope frombus.Scope fps map[string]string // by module, the wanted fingerprint - comps map[string]delivery.Composed + comps map[string]engcomp.Composed held map[string]string // placed is the modules the node's placement puts here, ran what runs of each module (by its composition's FP). placed []string @@ -151,7 +152,7 @@ func (c *Composer) writeComposed(ctx context.Context, at *published) (wrote, del func (c *Composer) composedValues(ctx context.Context, at *published) (map[string][]byte, error) { r := reader{c: c} out := map[string][]byte{} - put := func(comp delivery.Composed, plans map[string]catalogue.Plan) error { + put := func(comp engcomp.Composed, plans map[string]catalogue.Plan) error { if comp.Module != "" && comp.Refused == "" { if p, ok := plans[comp.Module]; ok { p := p @@ -162,7 +163,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin if err != nil { return err } - raw, err := delivery.EncodeComposed(comp) + raw, err := engcomp.Encode(comp) if err != nil { return fmt.Errorf("the composition %s cannot be written: %w", key, err) } @@ -172,7 +173,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin runs := []struct { scope frombus.Scope fps map[string]string - comps map[string]delivery.Composed + comps map[string]engcomp.Composed held map[string]string }{{at.scope, at.fps, at.comps, at.held}} // The newest builds, composed only where they differ from the wanted ones. @@ -209,7 +210,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin runs = append(runs, struct { scope frombus.Scope fps map[string]string - comps map[string]delivery.Composed + comps map[string]engcomp.Composed held map[string]string }{scope, newest, comps, held}) } @@ -234,7 +235,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin if !ok || module == agent.NodeModule { continue } - if err := put(delivery.Composed{Node: c.env.Node, Module: module, FP: fp, Refused: why}, nil); err != nil { + if err := put(engcomp.Composed{Node: c.env.Node, Module: module, FP: fp, Refused: why}, nil); err != nil { return nil, err } } @@ -243,15 +244,15 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin } // composedKeyOf is a composition's key in the state. -func composedKeyOf(comp delivery.Composed) (string, error) { +func composedKeyOf(comp engcomp.Composed) (string, error) { if comp.Module == "" { - return delivery.ComposedNodeKey(comp.Node) + return engcomp.NodeKey(comp.Node) } - return delivery.ComposedKey(comp.Node, comp.Module, comp.FP) + return engcomp.Key(comp.Node, comp.Module, comp.FP) } // pastKeeping is, of the module keys the state holds, those whose fingerprint is not among the five its module wrote -// last (delivery.ComposedKeep, by revision) and that no want names (ADR 0593 §6). Under pubWrite. +// last (engcomp.Keep, by revision) and that no want names (ADR 0593 §6). Under pubWrite. func (c *Composer) pastKeeping() []string { c.mu.Lock() wanted := map[string]string{} @@ -266,7 +267,7 @@ func (c *Composer) pastKeeping() []string { } byModule := map[string][]kept{} for key, e := range c.pubKept { - _, module, fp, isNode, err := delivery.ParseComposedKey(key) + _, module, fp, isNode, err := engcomp.ParseKey(key) if err != nil || isNode { continue } @@ -276,7 +277,7 @@ func (c *Composer) pastKeeping() []string { for module, ks := range byModule { sort.Slice(ks, func(i, j int) bool { return ks[i].rev > ks[j].rev }) for i, k := range ks { - if i < delivery.ComposedKeep || wanted[module] == k.fp { + if i < engcomp.Keep || wanted[module] == k.fp { continue } out = append(out, k.key) @@ -309,7 +310,7 @@ func (c *Composer) sayComposed(line string) { } // ranOf is, of what runs, each module's fingerprint by its composition (the node's own left out). -func ranOf(running []delivery.Composed) map[string]string { +func ranOf(running []engcomp.Composed) map[string]string { out := map[string]string{} for _, comp := range running { if comp.Module != "" && comp.FP != "" && !strings.EqualFold(comp.Module, agent.NodeModule) { diff --git a/internal/selfcompose/composedstate_test.go b/internal/selfcompose/composedstate_test.go index 0ea6d69..48d3211 100644 --- a/internal/selfcompose/composedstate_test.go +++ b/internal/selfcompose/composedstate_test.go @@ -2,6 +2,7 @@ package selfcompose import ( "context" + engcomp "github.com/novox/mesh-host/internal/compositions" "slices" "strings" "sync" @@ -100,8 +101,8 @@ func TestTheNodePublishesItsCompositions(t *testing.T) { } for u := next(t, out); !u.Caught; u = next(t, out) { } - lemursKey, _ := delivery.ComposedKey("n1", "lemurs", fp) - oldieKey, _ := delivery.ComposedKey("n1", "oldie", ofp) + lemursKey, _ := engcomp.Key("n1", "lemurs", fp) + oldieKey, _ := engcomp.Key("n1", "oldie", ofp) waitFor := func(what string, ok func([]string) bool) []string { t.Helper() deadline := time.Now().Add(5 * time.Second) @@ -122,7 +123,7 @@ func TestTheNodePublishesItsCompositions(t *testing.T) { t.Errorf("the first pass wrote %v, want the wanted module's key and the node's own", first) } if raw := state.kv[lemursKey].Value; raw != nil { - comp, err := delivery.DecodeComposed(raw) + comp, err := engcomp.Decode(raw) if err != nil || comp.Module != "lemurs" || comp.FP != fp || len(comp.Resources) == 0 { t.Errorf("the published composition of lemurs: %+v, %v", comp, err) } @@ -158,7 +159,7 @@ func TestTheNodePublishesItsCompositions(t *testing.T) { keep.pubKept = map[string]ComposedEntry{} var keys []string for i, ch := range "123456" { - key, _ := delivery.ComposedKey("n1", "lemurs", delivery.SourcePrefix2+strings.Repeat(string(ch), 64)) + key, _ := engcomp.Key("n1", "lemurs", delivery.SourcePrefix2+strings.Repeat(string(ch), 64)) keys = append(keys, key) keep.pubKept[key] = ComposedEntry{Revision: uint64(10 + i)} } diff --git a/internal/selfcompose/data.go b/internal/selfcompose/data.go index 6c1e86d..fb57685 100644 --- a/internal/selfcompose/data.go +++ b/internal/selfcompose/data.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "sort" "strings" "time" @@ -126,7 +127,7 @@ func (c *Composer) sayData(line string) { func (c *Composer) digestsByModule() map[string]string { out := map[string]string{} for key, dig := range c.handed { - _, module, _, isNode, err := delivery.ParseComposedKey(key) + _, module, _, isNode, err := engcomp.ParseKey(key) if err != nil { continue } diff --git a/internal/selfcompose/data_test.go b/internal/selfcompose/data_test.go index d88f0a1..c92e25b 100644 --- a/internal/selfcompose/data_test.go +++ b/internal/selfcompose/data_test.go @@ -3,6 +3,7 @@ package selfcompose import ( "context" "errors" + engcomp "github.com/novox/mesh-host/internal/compositions" "slices" "strings" "sync" @@ -89,8 +90,8 @@ 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, _ := delivery.ComposedKey("n1", "web", delivery.SourcePrefix2+strings.Repeat("a", 64)) - nodeKey, _ := delivery.ComposedNodeKey("n1") + webKey, _ := engcomp.Key("n1", "web", delivery.SourcePrefix2+strings.Repeat("a", 64)) + nodeKey, _ := engcomp.NodeKey("n1") for _, step := range []struct { name string due bool @@ -183,8 +184,8 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { // the key again, so a key lost on the bus is written back. Another node's key is never touched. pkgStore := &fakeData{kv: map[string][]byte{"n2": []byte(`{"node":"n2","packages":[]}`)}} pc := New(Env{Node: "n1", Packages: pkgStore}) - web := func(pkgs ...string) delivery.Composed { - comp := delivery.Composed{Node: "n1", Module: "web"} + web := func(pkgs ...string) engcomp.Composed { + comp := engcomp.Composed{Node: "n1", Module: "web"} for _, p := range pkgs { comp.Resources = append(comp.Resources, []byte(`{"id":"`+p+`","type":"package","package":"`+p+`"}`)) } @@ -193,25 +194,25 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { } for _, step := range []struct { name string - comp delivery.Composed + comp engcomp.Composed fail error puts int owed bool holds string }{ {name: "the run's first pass writes the key", comp: web("curl"), puts: 1, - holds: `{"node":"n1","packages":[{"package":"curl","modules":["web"],"absent":false}]}`}, + holds: `{"node":"n1","packages":[{"absent":false,"modules":["web"],"package":"curl"}]}`}, {name: "a pass that changes no package writes nothing", comp: web("curl"), puts: 1}, {name: "a refused write is owed after the bound", comp: web("curl", "jq"), fail: errors.New("no grant yet"), puts: 1, owed: true}, {name: "a composition adding a package writes the key once", comp: web("curl", "jq"), puts: 2, - holds: `{"node":"n1","packages":[{"package":"curl","modules":["web"],"absent":false},{"package":"jq","modules":["web"],"absent":false}]}`}, + holds: `{"node":"n1","packages":[{"absent":false,"modules":["web"],"package":"curl"},{"absent":false,"modules":["web"],"package":"jq"}]}`}, } { pkgStore.mu.Lock() pkgStore.fail = step.fail pkgStore.mu.Unlock() pc.paid() pc.mu.Lock() - pc.pkgs, pc.pkgsComposed, pc.pkgsDue = map[string]delivery.Composed{webKey: step.comp}, true, true + pc.pkgs, pc.pkgsComposed, pc.pkgsDue = map[string]engcomp.Composed{webKey: step.comp}, true, true pc.mu.Unlock() pc.publishPackages(context.Background()) if len(pkgStore.puts) != step.puts || pc.owed.Load() != step.owed || diff --git a/internal/selfcompose/packages.go b/internal/selfcompose/packages.go index 38a3a11..45b4801 100644 --- a/internal/selfcompose/packages.go +++ b/internal/selfcompose/packages.go @@ -6,10 +6,11 @@ import ( "encoding/json" "errors" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + engcomp "github.com/novox/mesh-host/internal/compositions" "sort" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // **The packages this node's compositions declare, kept by its engine** (novox/hq ADR 0565 §6, §15; issue 1953). The @@ -28,8 +29,8 @@ type PackagesStore interface { } // packagesOf is a composition's package resources alone, with its module: what the node's key is worked out from. -func packagesOf(comp delivery.Composed) delivery.Composed { - out := delivery.Composed{Node: comp.Node, Module: comp.Module} +func packagesOf(comp engcomp.Composed) engcomp.Composed { + out := engcomp.Composed{Node: comp.Node, Module: comp.Module} for _, raw := range comp.Resources { var head struct { Type string `json:"type"` @@ -42,24 +43,24 @@ func packagesOf(comp delivery.Composed) delivery.Composed { } // DeclaredPackages is the node's key as this composer would write it now, from what it handed. -func (c *Composer) DeclaredPackages() (delivery.DeclaredPackages, error) { +func (c *Composer) DeclaredPackages() (nodeengine.PackagesByNodeValue, error) { c.mu.Lock() keys := make([]string, 0, len(c.pkgs)) for k := range c.pkgs { keys = append(keys, k) } sort.Strings(keys) - comps := make([]delivery.Composed, 0, len(keys)) + comps := make([]engcomp.Composed, 0, len(keys)) for _, k := range keys { comps = append(comps, c.pkgs[k]) } composed := c.pkgsComposed c.mu.Unlock() if !composed { - return delivery.DeclaredPackages{}, errors.New("this node's composer has handed no composition yet in this run, so it " + + return nodeengine.PackagesByNodeValue{}, errors.New("this node's composer has handed no composition yet in this run, so it " + "declares no packages yet") } - return delivery.DeclaredPackagesOf(c.env.Node, comps) + return engcomp.DeclaredPackages(c.env.Node, comps) } // publishPackages writes the node's key of the state packages when a pass asked it (pkgsDue) and the value differs. @@ -86,7 +87,7 @@ func (c *Composer) publishPackages(ctx context.Context) { } // enginePackagesBucket is the engine's state packages as the bus names it, for what the composer says. -const enginePackagesBucket = "mesh-host_" + delivery.EnginePackagesState +const enginePackagesBucket = engcomp.PackagesBucket // WritePackages writes the node's key again from what the composer handed, when it differs from what the bus holds // now (the node seat's verb packages with write, ADR 0565 §15): the key is read again rather than trusted, so a key lost diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index e9e9c0f..4b115df 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "github.com/novox/mesh-host/internal/compositions" + engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "reflect" "slices" @@ -71,7 +72,7 @@ type Env struct { // switch to composing its own nor a restart changes a composition (ADR 0547 §3); a box held at another slot is // never taken, though it opens to the same value (issue 1126, C1: a provider's grant and its consumer's secret). // Either nil keeps the vault's box. - Running func() []delivery.Composed + Running func() []compositions.Composed Unseal func(sealed string) ([]byte, error) // Held is the sealed values a module's have says it runs, kept by the agent on the bus: what runs after a restart, // before the agent applied anything, and after the switch once the kept declaration is old. By slot @@ -195,7 +196,7 @@ type Composer struct { // pkgs is the package resources of each composition handed, by its composed key, and pkgsComposed whether this run // handed any (ADR 0565 §6); pkgsDue, pkgsTries and pkgsSaid are as the data items' are. Under mu. pkgsKept is what // the node's key of the state packages held when last read or written (pkgsRead), under pkgsWrite. - pkgs map[string]delivery.Composed + pkgs map[string]compositions.Composed pkgsComposed bool pkgsDue bool pkgsTries int @@ -299,7 +300,7 @@ func New(env Env) *Composer { return &Composer{env: env, wants: map[string]delivery.Want{}, placements: map[string]delivery.Placement{}, placementKeys: delivery.NewPlacementKeys(), records: map[string]delivery.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]delivery.Composed{}, asked: make(chan chan Pass, 8)} + pkgs: map[string]compositions.Composed{}, asked: make(chan chan Pass, 8)} } // ComposeNow asks the composer for a pass now (the node-engine's verb compose, issue 774): the growing bound after a seat @@ -880,7 +881,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update { } // What it runs: a folded module of an old-form build is carried in its own composition as it runs (issue 1126). - var running []delivery.Composed + var running []compositions.Composed if c.env.Running != nil { running = c.env.Running() } @@ -999,7 +1000,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update { continue } now[key] = true - dig, err := delivery.ComposedDigestOf(comp) + dig, err := engcomp.DigestOf(comp) if err != nil { c.env.Say(fmt.Sprintf("the composition %s has no digest, and is not handed: %v", key, err)) continue @@ -1007,7 +1008,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update { if c.handed[key] == dig { continue } - raw, err := delivery.EncodeComposed(comp) + raw, err := engcomp.Encode(comp) if err != nil { c.env.Say(fmt.Sprintf("the composition %s cannot be written, and is not handed: %v", key, err)) continue @@ -1018,7 +1019,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update { out = append(out, agent.Update{Key: key, Value: raw, Revision: c.rev}) } for key := range c.handed { - if _, module, _, _, err := delivery.ParseComposedKey(key); err == nil && module != "" { + if _, module, _, _, err := engcomp.ParseKey(key); err == nil && module != "" { if _, isHeld := held[module]; isHeld { continue // held: what was handed of it stays, so the node keeps running it } @@ -1038,7 +1039,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update { placedMods = append(placedMods, m.Module) } sort.Strings(placedMods) - pubComps := make(map[string]delivery.Composed, len(compositions)) + pubComps := make(map[string]engcomp.Composed, len(compositions)) for key, comp := range compositions { if comp.Module == "" && !nodeWanted { continue @@ -1097,7 +1098,7 @@ func (c *Composer) heldNow() []string { // value (compositions.KeepSeals, issue 1126, C1): of what runs (running, the agent's), else of what its module's have says // it runs (Held). A have written before slots names no slot: its boxes are reused within their own module alone, at a // slot no box is kept for, and only one opening to the slot's value. -func (c *Composer) keepRunning(comp delivery.Composed, running []delivery.Composed) delivery.Composed { +func (c *Composer) keepRunning(comp compositions.Composed, running []compositions.Composed) compositions.Composed { if c.env.Unseal == nil { return comp } @@ -1731,7 +1732,7 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string if !placed { return "", false } - var running []delivery.Composed + var running []compositions.Composed if c.env.Running != nil { running = c.env.Running() } @@ -1742,7 +1743,7 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string } for _, comp := range comps { if comp.Module == module && comp.FP == fp { - dig, err := delivery.ComposedDigestOf(c.keepRunning(comp, running)) + dig, err := compositions.DigestOf(c.keepRunning(comp, running)) return dig, err == nil } } @@ -1750,7 +1751,7 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string } // ownProvisioner is the provisioner client's account the node's own composition among cs names, or "". -func ownProvisioner(cs []delivery.Composed) string { +func ownProvisioner(cs []compositions.Composed) string { for _, c := range cs { if c.Module == "" { return c.Own.Provisioner @@ -1759,8 +1760,8 @@ func ownProvisioner(cs []delivery.Composed) string { return "" } -func mapValues(m map[string]delivery.Composed) []delivery.Composed { - out := make([]delivery.Composed, 0, len(m)) +func mapValues(m map[string]compositions.Composed) []compositions.Composed { + out := make([]compositions.Composed, 0, len(m)) for _, k := range slices.Sorted(maps.Keys(m)) { out = append(out, m[k]) } @@ -1778,18 +1779,18 @@ const WaitsForTheSeatListOwn = "the node has not read the seat list, so it compo // digest) while the reader may not act on its seats, and names the module in held: it keeps running what it runs, and // the pass after the list was read hands it. The node's own composition is held the same way, whatever it folds, under // the scope agent.NodeModule; a composition that runs already is left as it is. -func (c *Composer) holdUntilListed(ctx context.Context, r reader, compositions map[string]delivery.Composed, held map[string]string, running []delivery.Composed) map[string]string { +func (c *Composer) holdUntilListed(ctx context.Context, r reader, compositions map[string]compositions.Composed, held map[string]string, running []compositions.Composed) map[string]string { if c.env.SeatList == nil { return held } ran := map[string]bool{} for _, comp := range running { - if dig, err := delivery.ComposedDigestOf(comp); err == nil { + if dig, err := engcomp.DigestOf(comp); err == nil { ran[comp.Module+"/"+dig] = true } } for key, comp := range compositions { - if dig, err := delivery.ComposedDigestOf(comp); err == nil && ran[comp.Module+"/"+dig] { + if dig, err := engcomp.DigestOf(comp); err == nil && ran[comp.Module+"/"+dig] { continue } module, why := comp.Module, WaitsForTheSeatList diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index eef2e64..7ae5ad3 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "github.com/novox/mesh-host/internal/compositions" "os" "reflect" @@ -172,7 +173,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { return secretChanged, func() {}, nil } var composed atomic.Int64 // passes made: each reads what runs once - env.Running = func() []delivery.Composed { composed.Add(1); return nil } + env.Running = func() []compositions.Composed { composed.Add(1); return nil } c := New(env) out, err := c.Watch(ctx, "n1.>") if err != nil { @@ -204,13 +205,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { b.send(bucket, agent.Update{Caught: true}) } got := map[string]bool{} - var own delivery.Composed + var own compositions.Composed for { u := next(t, out) if u.Caught { break } - c, err := delivery.DecodeComposed(u.Value) + c, err := compositions.Decode(u.Value) if err != nil { t.Fatalf("%s: %v", u.Key, err) } @@ -261,8 +262,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Errorf("a composer that watches nothing answered compose now: %v", err) } icancel() - lemursKey, _ := delivery.ComposedKey("n1", "lemurs", fp) - nodeKey, _ := delivery.ComposedNodeKey("n1") + lemursKey, _ := compositions.Key("n1", "lemurs", fp) + nodeKey, _ := compositions.NodeKey("n1") if !got[lemursKey] || !got[nodeKey] || len(got) != 2 { b.mu.Lock() t.Logf("said: %v", b.said) @@ -436,14 +437,14 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { r3 := c3.records["n1"] r3.Capabilities = []string{"systemd", "container-runtime"} c3.records["n1"] = r3 - spiegelKey, _ := delivery.ComposedKey("n1", "de-spiegel", c3.wants["de-spiegel"].FP) - proxyKey, _ := delivery.ComposedKey("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, _ := delivery.ComposedKey("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) } @@ -507,7 +508,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { p5.Decided.Endpoint = "n1.example:51820" c5.placements["n1"] = p5 c5.inOwn[overlay.Name] = true - nodeKey5, _ := delivery.ComposedNodeKey("n1") + nodeKey5, _ := compositions.NodeKey("n1") var own5 string for _, u := range c5.compose(ctx) { if u.Key == nodeKey5 { @@ -678,8 +679,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { defer hmu.Unlock() held = append(held, why+fmt.Sprint(h)) } - tenv.Running = func() []delivery.Composed { - return []delivery.Composed{{Node: "n1", Module: "tool", FP: old, Resources: []json.RawMessage{ + tenv.Running = func() []compositions.Composed { + return []compositions.Composed{{Node: "n1", Module: "tool", FP: old, Resources: []json.RawMessage{ json.RawMessage(`{"id":"service","module":"tool","type":"service","unit":"tool.service","state":"running","boot":"enabled"}`)}}} } tc := New(tenv) @@ -702,7 +703,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { ResolvedBucket, delivery.BuilderBuildsBucket} { tb.send(bucket, agent.Update{Caught: true}) } - toolKey, _ := delivery.ComposedKey("n1", "tool", next2) + toolKey, _ := compositions.Key("n1", "tool", next2) for u := next(t, tout); !u.Caught; u = next(t, tout) { if u.Key == toolKey { t.Errorf("tool is handed at a build not yet done") @@ -781,7 +782,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } } b.put(t, delivery.AssignerWantsBucket, "want.n1.keeper", want) - keeperKey, _ := delivery.ComposedKey("n1", "keeper", fp) + keeperKey, _ := compositions.Key("n1", "keeper", fp) var handed string for handed == "" { if u := next(t, out); u.Key == keeperKey { @@ -823,7 +824,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { // or another resource of keeper holds is never taken, though it opens to the same value; a have written before slots // (a list) is reused within its own module alone. boxOf := func(raw string) (string, string) { - c, err := delivery.DecodeComposed([]byte(raw)) + c, err := compositions.Decode([]byte(raw)) if err != nil { t.Fatal(err) } @@ -852,7 +853,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { {"the bus does not answer for the have", "keeper", rotatedBox, nil, false, true}, } { restarted := env - restarted.Running = func() []delivery.Composed { return nil } + restarted.Running = func() []compositions.Composed { return nil } restarted.Held = func(module string) (compositions.Seals, []string, error) { if module != row.in { return nil, nil, nil @@ -917,7 +918,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } // And the node's own key (review of L2, round 3, finding 1): a core box its own composition carries is held there, and // kept after a restart as a module's is, so the node's own composition does not move either. - ownComp := delivery.Composed{Node: "n1", Resources: []json.RawMessage{json.RawMessage(`{"id":"core-box","sealed":"{\"ask\":999}"}`)}} + ownComp := compositions.Composed{Node: "n1", Resources: []json.RawMessage{json.RawMessage(`{"id":"core-box","sealed":"{\"ask\":999}"}`)}} b.mu.Lock() sealedAt[`{"ask":999}`] = generation + 1 b.mu.Unlock() @@ -946,8 +947,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { sealedAt["box-again"] = sealedAt[rotatedBox] sealedAt["box-other"] = sealedAt[rotatedBox] + 100 b.mu.Unlock() - ini := func(db, key string) delivery.Composed { - return delivery.Composed{Node: "n1", Module: "grafana", Resources: []json.RawMessage{json.RawMessage( + ini := func(db, key string) compositions.Composed { + return compositions.Composed{Node: "n1", Module: "grafana", Resources: []json.RawMessage{json.RawMessage( `{"content":"[database]\npassword = ${secret:postgres-database}\n[security]\nsecret_key = ${secret:secret-key}\n",` + `"id":"grafana.ini","mode":"0600","module":"grafana","path":"/etc/grafana/grafana.ini","secrets":{"postgres-database":` + strconv.Quote(db) + `,"secret-key":` + strconv.Quote(key) + `},"type":"file"}`)}} @@ -985,8 +986,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } // The node runs the consumer's credential already, sealed otherwise (the controller sealed it, or the vault at // another ask): that box opens to the same value, so it is kept, and the composition does not move. - pairEnv.Running = func() []delivery.Composed { - return []delivery.Composed{{Module: "web", Resources: []json.RawMessage{json.RawMessage(`{"id":"web.secret-pg","sealed":"{\"run\":1}"}`)}}} + pairEnv.Running = func() []compositions.Composed { + return []compositions.Composed{{Module: "web", Resources: []json.RawMessage{json.RawMessage(`{"id":"web.secret-pg","sealed":"{\"run\":1}"}`)}}} } opened := map[string]string{`{"c":1}`: "pg-pw", `{"run":1}`: "pg-pw", `{"p":1}`: "s3-pw"} pairEnv.Unseal = func(box string) ([]byte, error) { @@ -1009,7 +1010,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Errorf("credentials %+v issued %+v", creds, issued) } // What runs at the credential's slot is kept, after the composition (keepRunning, issue 1126, C1). - webComp := delivery.Composed{Node: "n1", Module: "web", Resources: []json.RawMessage{json.RawMessage(`{"id":"web.secret-pg","sealed":"{\"c\":1}"}`)}} + webComp := compositions.Composed{Node: "n1", Module: "web", Resources: []json.RawMessage{json.RawMessage(`{"id":"web.secret-pg","sealed":"{\"c\":1}"}`)}} if got := compositions.SealsIn(pc.keepRunning(webComp, pairEnv.Running()))["web.secret-pg#sealed"]; got != `{"run":1}` { t.Errorf("web's credential composed as %s, want the box that runs", got) } @@ -1053,8 +1054,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { if err != nil { t.Fatal(err) } - senv.Running = func() []delivery.Composed { - var out []delivery.Composed + senv.Running = func() []compositions.Composed { + var out []compositions.Composed for _, c := range running { out = append(out, c) } @@ -1062,9 +1063,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } // The controller's compositions at n1's wants, in n1's composition bucket: what a head deleting its placement // returns it to (the seed's revert); here the ones it runs. - composedBucket, _ := delivery.ComposedBucketFor("n1") + composedBucket, _ := compositions.BucketFor("n1") for key, c := range running { - raw, err := delivery.EncodeComposed(c) + raw, err := compositions.Encode(c) if err != nil { t.Fatal(err) } @@ -1072,8 +1073,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { readOnly.kv[composedBucket+"/"+key] = raw readOnly.mu.Unlock() } - answered := make(chan map[string]delivery.CandidateAnswer, 8) - sh := NewShadow(senv, func(_ context.Context, a map[string]delivery.CandidateAnswer) { answered <- a }) + answered := make(chan map[string]nodeengine.CandidateAnswer, 8) + sh := NewShadow(senv, func(_ context.Context, a map[string]nodeengine.CandidateAnswer) { answered <- a }) sctx, scancel := context.WithCancel(ctx) defer scancel() // Review of L2b, round 1, finding 2: the candidates' state is not there when the node starts (a delivery before the @@ -1096,7 +1097,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { var lemursDigest string for _, c := range running { if c.Module == "lemurs" { - lemursDigest, _ = delivery.ComposedDigestOf(c) + lemursDigest, _ = compositions.DigestOf(c) } } wantRaw, err := delivery.EncodeWant(delivery.Want{FP: fp, Composed: lemursDigest}) @@ -1142,8 +1143,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { sb.put(delivery.PlacementCandidatesBucket, k, raw, 8) } sb.caught() - var answers map[string]delivery.CandidateAnswer - waitFor := func(why string, ok func(map[string]delivery.CandidateAnswer) bool) { + var answers map[string]nodeengine.CandidateAnswer + waitFor := func(why string, ok func(map[string]nodeengine.CandidateAnswer) bool) { t.Helper() deadline := time.After(10 * time.Second) for answers == nil || !ok(answers) { @@ -1154,7 +1155,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } } } - waitFor("every group answered", func(a map[string]delivery.CandidateAnswer) bool { return len(a) == 6 }) + waitFor("every group answered", func(a map[string]nodeengine.CandidateAnswer) bool { return len(a) == 6 }) // Every watch and get the shadow makes is one the node's grant allows (issue 1126, 2026-10-27 09:21). sb.mu.Lock() for _, w := range sb.watches { @@ -1170,28 +1171,28 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } } readOnly.mu.Unlock() - for group, want := range map[string]func(delivery.CandidateAnswer) bool{ - "same": func(a delivery.CandidateAnswer) bool { - return a.Verdict == delivery.CandidateSame && a.Proposal == "same" && a.Revision == 3 && len(a.Same) == 2 + for group, want := range map[string]func(nodeengine.CandidateAnswer) bool{ + "same": func(a nodeengine.CandidateAnswer) bool { + return a.Verdict == nodeengine.CandidateAnswerVerdictSame && a.Proposal == "same" && a.Revision == 3 && len(a.Same) == 2 }, - "adds": func(a delivery.CandidateAnswer) bool { - return a.Verdict == delivery.CandidateDiffers && len(a.Added) == 1 && a.Added[0].Module == "keeper" && + "adds": func(a nodeengine.CandidateAnswer) bool { + return a.Verdict == nodeengine.CandidateAnswerVerdictDiffers && len(a.Added) == 1 && a.Added[0].Module == "keeper" && len(a.Added[0].Unissued) == 1 && len(a.Refusals) == 0 }, - "drops": func(a delivery.CandidateAnswer) bool { - return a.Verdict == delivery.CandidateDiffers && len(a.Removed) == 1 && a.Removed[0] == "lemurs" + "drops": func(a nodeengine.CandidateAnswer) bool { + return a.Verdict == nodeengine.CandidateAnswerVerdictDiffers && len(a.Removed) == 1 && a.Removed[0] == "lemurs" }, - "ghost": func(a delivery.CandidateAnswer) bool { - return a.Verdict == delivery.CandidateRefused && a.Refused != "" + "ghost": func(a nodeengine.CandidateAnswer) bool { + return a.Verdict == nodeengine.CandidateAnswerVerdictRefused && a.Refused != "" }, - "reach": func(a delivery.CandidateAnswer) bool { - return a.Verdict == delivery.CandidateSame && a.Returns == "" && a.Revision == 8 && len(a.Removed) == 0 && + "reach": func(a nodeengine.CandidateAnswer) bool { + return a.Verdict == nodeengine.CandidateAnswerVerdictSame && a.Returns == "" && a.Revision == 8 && len(a.Removed) == 0 && fmt.Sprint(a.Same) == "[lemurs node]" }, // Issue 1126, A7: a candidate deleting the node's placement is refused, never answered as a return to the // controller's compositions: no composition made elsewhere is applied, so the node would compose nothing. - "deletes": func(a delivery.CandidateAnswer) bool { - return a.Verdict == delivery.CandidateRefused && a.Returns == "" && a.Revision == 7 && + "deletes": func(a nodeengine.CandidateAnswer) bool { + return a.Verdict == nodeengine.CandidateAnswerVerdictRefused && a.Returns == "" && a.Revision == 7 && strings.Contains(a.Refused, "composes nothing") }, } { @@ -1252,7 +1253,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Errorf("answered %d more time(s) with no candidate revision and no input of the node changed", got-settled) } candidate("same", 8, "lemurs") - waitFor("the new revision answered", func(a map[string]delivery.CandidateAnswer) bool { return a["same"].Revision == 8 }) + waitFor("the new revision answered", func(a map[string]nodeengine.CandidateAnswer) bool { return a["same"].Revision == 8 }) drain() if got := answering(); got != settled+1 { t.Errorf("a new revision of one group answered %d group(s), want 1", got-settled) @@ -1286,12 +1287,12 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { setFor(t, &laterAgain, []time.Duration{20 * time.Millisecond}) ownFails.Store(true) candidate("adds", 9, "lemurs", "keeper") - waitFor("the answer met the vault not answering", func(a map[string]delivery.CandidateAnswer) bool { + waitFor("the answer met the vault not answering", func(a map[string]nodeengine.CandidateAnswer) bool { return a["adds"].Revision == 9 && strings.Contains(a["adds"].Refused, "no responders") }) ownFails.Store(false) - waitFor("answered again once the bus answers, with nothing else moved", func(a map[string]delivery.CandidateAnswer) bool { - return a["adds"].Revision == 9 && a["adds"].Verdict == delivery.CandidateDiffers + waitFor("answered again once the bus answers, with nothing else moved", func(a map[string]nodeengine.CandidateAnswer) bool { + return a["adds"].Revision == 9 && a["adds"].Verdict == nodeengine.CandidateAnswerVerdictDiffers }) drain() if got := lastSaid("answering placement candidate adds "); got != "answering placement candidate adds (revision 9; woken by a retry after a read the bus did not answer)" { @@ -1299,7 +1300,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } k, _ := delivery.PlacementCandidateKey("same", "n1") sb.del(delivery.PlacementCandidatesBucket, k) - waitFor("the deleted group's answer stays", func(a map[string]delivery.CandidateAnswer) bool { _, ok := a["same"]; return !ok && len(a) == 5 }) + 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. @@ -1309,7 +1310,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { sb.del(delivery.PlacementCandidatesBucket, dropsKey) time.Sleep(30 * time.Millisecond) sb.missing(delivery.PlacementCandidatesBucket, false) - waitFor("a group deleted while the shadow was down is still answered", func(a map[string]delivery.CandidateAnswer) bool { + 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 }) @@ -1342,8 +1343,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { var nsaid []string var nmu sync.Mutex nenv.Say = func(l string) { nmu.Lock(); nsaid = append(nsaid, l); nmu.Unlock() } - nanswered := make(chan map[string]delivery.CandidateAnswer, 8) - nsh := NewShadow(nenv, func(_ context.Context, a map[string]delivery.CandidateAnswer) { nanswered <- a }) + nanswered := make(chan map[string]nodeengine.CandidateAnswer, 8) + nsh := NewShadow(nenv, func(_ context.Context, a map[string]nodeengine.CandidateAnswer) { nanswered <- a }) nctx, ncancel := context.WithCancel(ctx) defer ncancel() go func() { _ = nsh.Run(nctx) }() @@ -1366,7 +1367,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } select { case a := <-nanswered: - if g := a["same"]; g.Verdict != delivery.CandidateRefused || !strings.Contains(g.Refused, ResolvedBucket) || g.Proposal != "same" { + if g := a["same"]; g.Verdict != nodeengine.CandidateAnswerVerdictRefused || !strings.Contains(g.Refused, ResolvedBucket) || g.Proposal != "same" { t.Errorf("a shadow waiting for its values answered %+v", g) } case <-time.After(5 * time.Second): @@ -1384,14 +1385,14 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { nb.send(ResolvedBucket, agent.Update{Caught: true}) select { case a := <-nanswered: - if g := a["same"]; g.Verdict != delivery.CandidateSame || !strings.Contains(g.Note, "this answer took over") { + if g := a["same"]; g.Verdict != nodeengine.CandidateAnswerVerdictSame || !strings.Contains(g.Note, "this answer took over") { t.Errorf("the shadow, once ready, answered %+v", g) } // The newest first, handed at once: the older group still holds the answer it had. - if g := a["older"]; g.Verdict != delivery.CandidateRefused { + if g := a["older"]; g.Verdict != nodeengine.CandidateAnswerVerdictRefused { t.Errorf("the older group was answered before the newest was handed: %+v", g) } - if g := (<-nanswered)["older"]; g.Verdict != delivery.CandidateSame { + if g := (<-nanswered)["older"]; g.Verdict != nodeengine.CandidateAnswerVerdictSame { t.Errorf("the older group, answered next: %+v", g) } // Issue 1126, T2: an answer says when it starts and when it ends (and, every minute between, what it waits on). @@ -1419,8 +1420,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { // Issue 1126, probe #532 on shanks: what runs is the composition the have names, never the copy of the declaration // kept before the switch (no fp, no digest): a module composed at the digest its have says runs is the same. { - probe := delivery.CandidateAnswer{Proposal: "pro", Revision: 68, Verdict: delivery.CandidateDiffers, Same: []string{"adwaita"}, - Differs: []delivery.ModuleDiffers{ + probe := nodeengine.CandidateAnswer{Proposal: "pro", Revision: 68, Verdict: nodeengine.CandidateAnswerVerdictDiffers, Same: []string{"adwaita"}, + Differs: []nodeengine.CandidateDiffers{ {Module: "xterm", Composed: "sha256:3c0a31df64283c0678b1cdbeff8ac030609cab65cc7e47faaf714da743d9340e", Why: "the fingerprint differs"}, {Module: "zsh", Composed: "sha256:6093f31056d39eaf604410b7289ec6b5fe602b19c8547283562fcfc078c4844f", Resource: "zsh.bundle-tools", Why: "the resource differs"}, {Module: "brother", Composed: "sha256:0702b13991288da72d230460624e949d3c1d7d833eac2ec2134073c82e5a61cf", @@ -1429,11 +1430,11 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { "xterm": "sha256:3c0a31df64283c0678b1cdbeff8ac030609cab65cc7e47faaf714da743d9340e", "zsh": "sha256:6093f31056d39eaf604410b7289ec6b5fe602b19c8547283562fcfc078c4844f", "brother": "sha256:c11c14af2738e3fcdb04da4004bbe84728bdefaf8c986d76601ffbe502435871"}) - if fmt.Sprint(got.Same) != "[adwaita xterm zsh]" || len(got.Differs) != 1 || got.Differs[0].Module != "brother" || got.Verdict != delivery.CandidateDiffers { + if fmt.Sprint(got.Same) != "[adwaita xterm zsh]" || len(got.Differs) != 1 || got.Differs[0].Module != "brother" || got.Verdict != nodeengine.CandidateAnswerVerdictDiffers { t.Errorf("shanks' probe answered %+v", got) } - if one := asHavesSay(delivery.CandidateAnswer{Proposal: "pro", Revision: 1, Verdict: delivery.CandidateDiffers, Differs: probe.Differs[:1]}, - map[string]string{"xterm": probe.Differs[0].Composed}); one.Verdict != delivery.CandidateSame { + if one := asHavesSay(nodeengine.CandidateAnswer{Proposal: "pro", Revision: 1, Verdict: nodeengine.CandidateAnswerVerdictDiffers, Differs: probe.Differs[:1]}, + map[string]string{"xterm": probe.Differs[0].Composed}); one.Verdict != nodeengine.CandidateAnswerVerdictSame { t.Errorf("a module the same as its have says is answered %+v", one) } } @@ -1441,7 +1442,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { // Issue 1126, the seed's review: a module n1 runs inside its own composition (nats, at a build of the old form) is // composed there again, not at the trunk's build as one added; lemurs at the build it runs, keeper (added) at the // trunk's. - inOwnRunning := []delivery.Composed{{Node: "n1", Module: "lemurs", FP: fp}} + 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"}}}, map[string]delivery.Module{"lemurs": {FP2: other}, "nats": {FP2: other}, "keeper": {FP2: other}}, []string{"nats"}) diff --git a/internal/selfcompose/shadow.go b/internal/selfcompose/shadow.go index 4feed7c..85a539d 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -3,7 +3,10 @@ package selfcompose import ( "bytes" "context" + "encoding/json" "fmt" + nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + engcomp "github.com/novox/mesh-host/internal/compositions" "sort" "strings" "sync" @@ -29,11 +32,11 @@ import ( // keys go. type Shadow struct { c *Composer - answer func(ctx context.Context, answers map[string]delivery.CandidateAnswer) + answer func(ctx context.Context, answers map[string]nodeengine.CandidateAnswer) mu sync.Mutex candidates map[string]map[string]candidateKey // by proposal, by node - answers map[string]delivery.CandidateAnswer + answers map[string]nodeengine.CandidateAnswer // ran is, by module, the digest the module's have says runs: what runs, where the agent's list holds only the copy // from the declaration kept before the switch (issue 1126, probe #532 on shanks). ran map[string]string @@ -99,9 +102,9 @@ type candidateKey struct { } // NewShadow is the shadow composer of one node; answer writes the node's answers into its own have key. -func NewShadow(env Env, answer func(ctx context.Context, answers map[string]delivery.CandidateAnswer)) *Shadow { +func NewShadow(env Env, answer func(ctx context.Context, answers map[string]nodeengine.CandidateAnswer)) *Shadow { return &Shadow{c: New(env), answer: answer, candidates: map[string]map[string]candidateKey{}, - answers: map[string]delivery.CandidateAnswer{}, ran: map[string]string{}} + answers: map[string]nodeengine.CandidateAnswer{}, ran: map[string]string{}} } // Keep runs the shadow until ctx ends, beginning again after backoff(try) whenever it stops: an input it cannot watch @@ -341,13 +344,13 @@ func (s *Shadow) notReady(ctx context.Context, ins []input, caught []bool) { why := fmt.Sprintf("the node has not read %s after %s; it answers once it has", strings.Join(waits, ", "), notReadyAfter) s.c.env.Say("this node answers no placement candidate yet: " + why) s.mu.Lock() - next := map[string]delivery.CandidateAnswer{} + next := map[string]nodeengine.CandidateAnswer{} for proposal, keys := range s.candidates { own, ok := keys[s.c.env.Node] if !ok { continue // no key of this node's: nothing asks it to answer } - next[proposal] = delivery.CandidateAnswer{Proposal: proposal, Revision: own.rev, Refused: why, Verdict: delivery.CandidateRefused} + next[proposal] = nodeengine.CandidateAnswer{Proposal: proposal, Revision: int64(own.rev), Refused: why, Verdict: nodeengine.CandidateAnswerVerdictRefused} } s.answers = next s.mu.Unlock() @@ -386,7 +389,7 @@ func (s *Shadow) answerAll(ctx context.Context) { } } // Each proposal's last answer stands until it is answered again; a proposal whose keys went is dropped. - next := map[string]delivery.CandidateAnswer{} + next := map[string]nodeengine.CandidateAnswer{} for _, l := range proposals { if a, ok := s.answers[l.proposal]; ok { next[l.proposal] = a @@ -408,7 +411,7 @@ func (s *Shadow) answerAll(ctx context.Context) { same = false } } - out := make(map[string]delivery.CandidateAnswer, len(next)) + out := make(map[string]nodeengine.CandidateAnswer, len(next)) for g, a := range next { out[g] = a } @@ -417,7 +420,7 @@ func (s *Shadow) answerAll(ctx context.Context) { } s.mu.Unlock() if !same && s.answer != nil { - given := make(map[string]delivery.CandidateAnswer, len(out)) + given := make(map[string]nodeengine.CandidateAnswer, len(out)) for g, a := range out { given[g] = a } @@ -466,7 +469,7 @@ func (s *Shadow) answerAll(ctx context.Context) { } } if ok { - a = judge.FitAnswer(a, delivery.MaxAnswerBytes) + a = judge.FitAnswer(a, engcomp.MaxAnswerBytes) next[proposal] = a c.env.Say(fmt.Sprintf("answered placement candidate %s in %s: %s", proposal, took.Round(time.Millisecond), a.Verdict)) } else { @@ -495,7 +498,7 @@ var progressEvery = time.Minute // answerProposal is the node's answer to one proposal, and false when the node has nothing to answer: neither the trunk nor // the proposal places it. -func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery.CandidateAnswer, bool) { +func (s *Shadow) answerProposal(ctx context.Context, proposal string) (nodeengine.CandidateAnswer, bool) { c := s.c node := c.env.Node defer c.waiting("composing")() @@ -520,10 +523,10 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery. } c.mu.Unlock() if !mine { - return delivery.CandidateAnswer{}, false + return nodeengine.CandidateAnswer{}, false } - var running []delivery.Composed + var running []engcomp.Composed if c.env.Running != nil { running = c.env.Running() } @@ -531,7 +534,7 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery. // The proposal deletes this node's placement key: the node would compose nothing and keep running what it runs, // since no composition made elsewhere is applied (issue 1126: the controller ended by its unassignment). Refused, // so the catalogue does not take a node's placement away by a merge. - return delivery.CandidateAnswer{Proposal: proposal, Revision: rev, Verdict: delivery.CandidateRefused, + return nodeengine.CandidateAnswer{Proposal: proposal, Revision: int64(rev), Verdict: nodeengine.CandidateAnswerVerdictRefused, Refused: "the proposal takes this node's placement away: it then composes nothing, and no composition made " + "elsewhere is applied, so what runs here would be followed by nothing"}, true } @@ -635,7 +638,7 @@ func (s *Shadow) takeRan(u agent.Update) bool { // counted the same where the composed digest is the one its have says runs: what runs is the composition at that // digest, never the kept copy (issue 1126, probe #532: shanks' xterm, zsh, zsh-autosuggestions and // zsh-syntax-highlighting). -func asHavesSay(a delivery.CandidateAnswer, ran map[string]string) delivery.CandidateAnswer { +func asHavesSay(a nodeengine.CandidateAnswer, ran map[string]string) nodeengine.CandidateAnswer { if a.Refused != "" || len(a.Differs) == 0 { return a } @@ -649,7 +652,7 @@ func asHavesSay(a delivery.CandidateAnswer, ran map[string]string) delivery.Cand } a.Differs = kept sort.Strings(a.Same) - a.Verdict = delivery.VerdictOf(a) + a.Verdict = engcomp.VerdictOf(a) return a } @@ -658,7 +661,7 @@ func asHavesSay(a delivery.CandidateAnswer, ran map[string]string) delivery.Cand // 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 []delivery.Composed, placement delivery.Placement, modules map[string]delivery.Module, inOwn []string) map[string]string { +func shadowBuilds(running []engcomp.Composed, placement delivery.Placement, modules map[string]delivery.Module, inOwn []string) map[string]string { fps := map[string]string{} for _, r := range running { if r.Module != "" && r.FP != "" { @@ -679,9 +682,9 @@ func shadowBuilds(running []delivery.Composed, placement delivery.Placement, mod return fps } -func sameAnswer(a, b delivery.CandidateAnswer) bool { - x, errA := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}}) - y, errB := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": b}}) +func sameAnswer(a, b nodeengine.CandidateAnswer) bool { + x, errA := json.Marshal(a) + y, errB := json.Marshal(b) return errA == nil && errB == nil && string(x) == string(y) } diff --git a/internal/userlist/userlist.go b/internal/userlist/userlist.go index b818729..b072aac 100644 --- a/internal/userlist/userlist.go +++ b/internal/userlist/userlist.go @@ -31,6 +31,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + engcomp "github.com/novox/mesh-host/internal/compositions" "regexp" "sort" "strings" @@ -55,7 +56,7 @@ type Env struct { Node string // Running is the compositions this node runs now, as its agent holds them (agent.Agent.Everything): where the user // list's file is is read from these alone (issue 1126). Nil runs nothing. - Running func() []delivery.Composed + Running func() []engcomp.Composed // OnDisk is what the resource's file holds on the machine now; nil when there is none. OnDisk func(resource json.RawMessage) ([]byte, error) // Write writes the user list's file resource of a module's composition alone, with body as its content, when it @@ -233,7 +234,7 @@ func (f *Follower) busNode() bool { // look reads what this node runs now for the bus's user list: each module whose composition carries it. func (f *Follower) look() { - var running []delivery.Composed + var running []engcomp.Composed if f.env.Running != nil { running = f.env.Running() } @@ -410,7 +411,7 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) { // A list on the machine that cannot be read is an error: left out, the apply would remove the file as an orphan and the // bus would refuse every connection at its next reload, so the bus module's apply waits instead. A composition without // a user list is c unchanged. -func (f *Follower) Hold(c delivery.Composed) (delivery.Composed, error) { +func (f *Follower) Hold(c engcomp.Composed) (engcomp.Composed, error) { out := c out.Resources = nil for _, raw := range c.Resources { diff --git a/internal/userlist/userlist_test.go b/internal/userlist/userlist_test.go index a891016..5ca5b44 100644 --- a/internal/userlist/userlist_test.go +++ b/internal/userlist/userlist_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + engcomp "github.com/novox/mesh-host/internal/compositions" "strings" "sync" "testing" @@ -59,7 +60,7 @@ func list(users ...string) string { // comp is a composition of module at fp as this node runs it, holding the user list content (none when empty); by // digest when byDigest, as a file over 64 KiB is given; for another node than this one when asked. -func comp(module, fp, content string, p put) delivery.Composed { +func comp(module, fp, content string, p put) engcomp.Composed { server, _ := json.Marshal(map[string]any{"id": module + ".server-conf", "type": "file", "module": module, "path": "/var/lib/nats-module/conf/nats.conf", "content": "include accounts.conf\n"}) rs := []json.RawMessage{server} @@ -73,7 +74,7 @@ func comp(module, fp, content string, p put) delivery.Composed { users, _ := json.Marshal(r) rs = append(rs, users) } - c := delivery.Composed{Node: node, Module: module, FP: fp, Resources: rs} + c := engcomp.Composed{Node: node, Module: module, FP: fp, Resources: rs} if p.asNode != "" { c.Node = p.asNode } @@ -171,7 +172,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) { } { t.Run(c.name, func(t *testing.T) { var mu sync.Mutex - var running []delivery.Composed + var running []engcomp.Composed var writes []string var reports []Written reported := make(chan struct{}, 64) @@ -226,10 +227,10 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) { } } fl := New(Env{Node: node, Grants: gf, - Running: func() []delivery.Composed { + Running: func() []engcomp.Composed { mu.Lock() defer mu.Unlock() - return append([]delivery.Composed(nil), running...) + return append([]engcomp.Composed(nil), running...) }, Kept: func() ([]byte, uint64) { return keptRaw, 9 }, Keep: func([]byte, uint64) {}, @@ -288,7 +289,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) { } for _, p := range c.after { mu.Lock() - running = []delivery.Composed{comp(p.module, p.fp, p.content, p)} + running = []engcomp.Composed{comp(p.module, p.fp, p.content, p)} mu.Unlock() fl.RunsChanged() }