diff --git a/cmd/mesh-host/healthowed_test.go b/cmd/mesh-host/healthowed_test.go index a46871c..dcffb5a 100644 --- a/cmd/mesh-host/healthowed_test.go +++ b/cmd/mesh-host/healthowed_test.go @@ -3,15 +3,15 @@ package main import ( "context" "errors" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/link" + "github.com/novox/mesh-host/internal/liveness" "path/filepath" "strings" "sync" "testing" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/novox/mesh-host/internal/link" - "github.com/novox/mesh-host/internal/liveness" ) // owedBus is a link that takes reports and statements and keeps a have bucket in memory. @@ -44,10 +44,10 @@ func (b *owedBus) Set(_ context.Context, key string, value []byte, revision uint return nil } -func (b *owedBus) have(key string) (delivery.Have, bool) { +func (b *owedBus) have(key string) (nodeengine.HaveByNodeModuleValue, bool) { b.mu.Lock() defer b.mu.Unlock() - h, err := delivery.DecodeHave(b.kv[key]) + h, err := nodeengineclient.DecodeHave(b.kv[key]) return h, err == nil } @@ -66,19 +66,19 @@ func TestAModuleWithNothingLongRunningIsOkWithinALookOfItsApply(t *testing.T) { } fact := link.ModuleFact{Module: "xorg", FP: "src2:" + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("b", 64)} applying := fact - applying.Phase = string(delivery.PhaseApplying) + applying.Phase = string(nodeengineclient.PhaseApplying) queue.NoteModule(ctx, applying) go judgeWhatRuns(ctx, j, queue, func(string) {}, nil, nil) // The facts look inside the apply, while the module is still applying. queue.SayFacts(ctx, link.Report{Health: &link.Health{Contract: link.LivenessContract, At: time.Now().UTC()}}) - if h, _ := bus.have("n1.xorg"); h.Health != delivery.HealthUnknown { + if h, _ := bus.have("n1.xorg"); h.Health != nodeengineclient.HealthUnknown { t.Fatalf("judged while applying: %+v", h) } queue.NoteModule(ctx, fact) deadline := time.Now().Add(2 * time.Second) // well within one look (liveness.LookEvery, 15 s) for { h, _ := bus.have("n1.xorg") - if h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK { + if h.Phase == nodeengineclient.PhaseRunning && h.Health == nodeengineclient.HealthOK { return } if time.Now().After(deadline) { diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index 3df09c0..be93a42 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -1413,7 +1413,7 @@ func healthAsReported(st liveness.Statement, ns *network.Statement) *link.Health h.Network = &link.NetworkHealth{State: ns.State, Since: ns.Since.UTC(), Parts: []link.NetworkPart{}} for _, p := range ns.Parts { h.Network.Parts = append(h.Network.Parts, link.NetworkPart{Part: p.Part, State: p.State, Reason: p.Reason, - Said: p.Said, Writer: p.Writer, Owner: p.Owner, Toward: p.Toward, Since: p.Since.UTC(), Streak: p.Streak}) + Said: p.Said, Writer: p.Writer, Owner: p.Owner, Toward: p.Toward, Since: p.Since.UTC(), Streak: int64(p.Streak)}) } } return h diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index 395d6a9..e44a938 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "github.com/novox/mesh-host/internal/compositions" "os" @@ -86,7 +87,7 @@ func (h sessionHaves) Read(ctx context.Context, module string) (agent.Have, bool if !ok { return agent.Have{}, false } - return agent.Have{Module: module, FP: got.FP, Composed: got.Composed, Phase: string(got.Phase), Detail: got.Detail, + return agent.Have{Module: module, FP: got.Fp, Composed: got.Composed, Phase: string(got.Phase), Detail: got.Detail, Sealed: got.SealedAt}, true } @@ -104,24 +105,24 @@ func (h sessionHaves) ReadErr(ctx context.Context, module string) (agent.Have, b if !ok || err != nil { return agent.Have{}, false, err } - return agent.Have{Module: module, FP: got.FP, Composed: got.Composed, Phase: string(got.Phase), Detail: got.Detail, + return agent.Have{Module: module, FP: got.Fp, Composed: got.Composed, Phase: string(got.Phase), Detail: got.Detail, Sealed: got.SealedAt}, true, nil } func (h sessionHaves) Write(ctx context.Context, x agent.Have) { if x.Module == agent.NodeModule { // The node's own key holds its facts and the digest of the composition it runs. - if x.Phase == "" || x.Phase == string(delivery.PhaseRunning) { + if x.Phase == "" || x.Phase == string(nodeengineclient.PhaseRunning) { h.session.NoteNode(ctx, x.Composed, x.Sealed) } return } phase := x.Phase - if phase == string(delivery.PhaseRunning) { + if phase == string(nodeengineclient.PhaseRunning) { phase = "" } h.session.NoteModule(ctx, link.ModuleFact{Module: x.Module, FP: x.FP, Composed: x.Composed, Phase: phase, - Failed: phase == string(delivery.PhaseFailed), Detail: shorten(x.Detail), Judgement: x.Judgement, Waits: x.Waits, + Failed: phase == string(nodeengineclient.PhaseFailed), Detail: shorten(x.Detail), Judgement: x.Judgement, Waits: x.Waits, BuildAlone: x.BuildAlone, Kept: x.Kept, Sealed: x.Sealed, PullWait: x.PullWait}) } @@ -653,7 +654,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus composerNow.Store(composer) // what the verb compose asks a pass of, this run's alone env.Composed, env.SelfComposed, env.Policy, env.ComposedToday = composer, true, composer.Policy, composer.ComposedToday env.HoldsData = composer.HoldsData // what runs from the kept declaration says Data as its manifest does - session.NoteComposes(linked, delivery.ComposesOwn, "it composes its own modules, and applies no composition made elsewhere") + session.NoteComposes(linked, nodeengineclient.ComposesOwn, "it composes its own modules, and applies no composition made elsewhere") ag = agent.New(env) own.set(ag) current.set(ag) diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index 1d4d9d3..c7f964b 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/buckets" engcomp "github.com/novox/mesh-host/internal/compositions" "os" "path/filepath" @@ -136,7 +137,7 @@ func newItBedAs(t *testing.T, perms nodegrant.Permissions) *itBed { into *nats.KeyValue history uint8 }{{delivery.AssignerWantsBucket, &b.wants, 8}, {composedBucket, &b.composed, engcomp.History}, - {delivery.HaveBucket, &b.haves, delivery.HaveHistory}} { + {buckets.Have, &b.haves, buckets.HaveHistory}} { kv, err := b.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: c.name, History: c.history}) if err != nil { t.Fatal(err) @@ -200,13 +201,13 @@ func (b *itBed) file(module string) string { return string(got) } -func (b *itBed) have(module string) delivery.Have { - key, _ := delivery.HaveKey(itNode, module) +func (b *itBed) have(module string) nodeengine.HaveByNodeModuleValue { + key, _ := nec.HaveKey(itNode, module) e, err := b.haves.Get(key) if err != nil { - return delivery.Have{} + return nodeengine.HaveByNodeModuleValue{} } - h, _ := delivery.DecodeHave(e.Value()) + h, _ := nec.DecodeHave(e.Value()) return h } @@ -293,11 +294,11 @@ func h8once(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, place [] if settled == 0 { t.Fatal("feat-x was never answered") } - hkey, _ := delivery.HaveKey(itNode, "web") + hkey, _ := nec.HaveKey(itNode, "web") for _, detail := range []string{"its tool web_health: no link to the bus is open", "its tool web_health: no answer within 10s"} { h := b.have("web") - h.Health, h.Detail = delivery.HealthFailed, detail - raw, err := delivery.EncodeHave(h) + h.Health, h.Detail = nec.HealthFailed, detail + raw, err := nec.EncodeHave(h) if err != nil { t.Fatal(err) } @@ -334,15 +335,15 @@ func h8once(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, place [] // composes its own. func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValue, hkey string) { t.Helper() - node := func() delivery.HaveNode { + node := func() nodeengine.HaveByNodeNodeValue { e, err := b.haves.Get(hkey) if err != nil { - return delivery.HaveNode{} + return nodeengine.HaveByNodeNodeValue{} } - n, _ := delivery.DecodeHaveNode(e.Value()) + n, _ := nec.DecodeHaveNode(e.Value()) return n } - if n := node(); n.Composes != delivery.ComposesOwn { + if n := node(); n.Composes != nec.ComposesOwn { t.Errorf("a node composing its own says it composes %q", n.Composes) } craw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "revert-x"}) @@ -377,10 +378,10 @@ func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValu b.mu.Lock() said := strings.Join(b.said, "\n") b.mu.Unlock() - if n := node(); n.Composes != delivery.ComposesOwn || strings.Contains(said, "applying the controller's compositions") { + if n := node(); n.Composes != nec.ComposesOwn || strings.Contains(said, "applying the controller's compositions") { t.Errorf("without its placement the node turned to the controller's compositions: %q", n.Composes) } - if h := b.have("web"); h.Composed != ran || h.Phase == delivery.PhaseFailed || b.file("web") != "greeting=salut\n" { + if h := b.have("web"); h.Composed != ran || h.Phase == nec.PhaseFailed || b.file("web") != "greeting=salut\n" { t.Errorf("without its placement what runs moved: %+v, %q", h, b.file("web")) } } @@ -422,12 +423,12 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { fp := fpIt("a") modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)}) place, _ := delivery.EncodePlacement(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}}) - rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: itNode, Capabilities: []string{"service-manager", "package-manager"}}}) + rec, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: itNode, Capabilities: []string{"service-manager", "package-manager"}}}) // it1 in one key per module and node (research 072 row 37), it2 in the form before: a node reads both. pkey, _ := delivery.PlacementNodeKey(itNode) places, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}}) place = places[pkey] - hkey, _ := delivery.HaveNodeKey(itNode) + hkey, _ := nec.HaveNodeKey(itNode) // A provider on it2 serving from its own config value: a mail domain, which it1's composition settles. mailer := `{"module":"mailer","version":"1","config":{"domain":{"type":"string","description":"the mail domain","required":true}},` + `"provides":[{"name":"smtp","scope":"mesh"}],"serves":{"smtp":{"port":587,"domain":"${config:domain}"}}}` @@ -436,9 +437,9 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { mlkey, _ := delivery.ManifestsKey(mdigest) mlmod, _ := delivery.EncodeModule(delivery.Module{FP2: fpIt("b"), Manifest: mdigest, Repository: "novox/mailer", Commit: strings.Repeat("2", 40)}) place2, _ := delivery.EncodePlacement(delivery.Placement{Node: "it2", Modules: []delivery.PlacedModule{{Module: "mailer"}}}) - rec2, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "it2", Capabilities: []string{"service-manager"}}}) + rec2, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "it2", Capabilities: []string{"service-manager"}}}) pkey2, _ := delivery.LegacyPlacementKey("it2") - hkey2, _ := delivery.HaveNodeKey("it2") + hkey2, _ := nec.HaveNodeKey("it2") buildKey, _ := delivery.BuilderBuildKey("web", fp) done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest, Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Reference: "artifact-store://web/files/blobs/" + fpDigest("f"), Digest: fpDigest("f")}}}) @@ -503,7 +504,7 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str t.Helper() within(t, 2*time.Second, "the have does not name the build", func() bool { h := b.have("web") - return h.FP == fp && h.Composed != "" && h.Phase == delivery.PhaseRunning + return h.Fp == fp && h.Composed != "" && h.Phase == nec.PhaseRunning }) // 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. @@ -527,7 +528,7 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str within(t, 3*time.Second, "a changed config value was not composed and applied again", func() bool { return b.file("web") == "greeting=salut\n" }) within(t, 2*time.Second, "the have does not name the revision", func() bool { h := b.have("web") - return h.FP == fp && h.Composed != first && h.Phase == delivery.PhaseRunning + return h.Fp == fp && h.Composed != first && h.Phase == nec.PhaseRunning }) // The node says its facts (its record, as the test wrote it), then a candidate equal to its placement is written: the @@ -551,7 +552,7 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str if err != nil { return false } - n, err := delivery.DecodeHaveNode(e.Value()) + n, err := nec.DecodeHaveNode(e.Value()) wire, _ := json.Marshal(n.Candidates["feat-x"]) _ = json.Unmarshal(wire, &answer) return err == nil && answer.Proposal == "feat-x" && n.Record != nil @@ -787,13 +788,13 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test t.Fatal(err) } } - rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: itNode, + rec, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: itNode, Capabilities: []string{"service-manager", "package-manager", "container-runtime"}}}) - hkey, _ := delivery.HaveNodeKey(itNode) + hkey, _ := nec.HaveNodeKey(itNode) places, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "nats"}}}) // The have the engine wrote of the folded bus: the old-form build, no fingerprint. - natsHave, _ := delivery.HaveKey(itNode, "nats") - oldHave, _ := delivery.EncodeHave(delivery.Have{Build: "build-1792674595610176744", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) + natsHave, _ := nec.HaveKey(itNode, "nats") + oldHave, _ := nec.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: "build-1792674595610176744", Phase: nec.PhaseRunning, Health: nec.HealthOK}) for key, value := range map[string][]byte{hkey: rec, natsHave: oldHave} { if _, err := b.haves.Put(key, value); err != nil { t.Fatal(err) @@ -848,7 +849,7 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test within(t, 10*time.Second, "the bus module's first want was not applied", func() bool { h := b.have("nats") got, _ := os.ReadFile(conf) - return h.FP == first && h.Phase == delivery.PhaseRunning && string(got) == "listen: new\n" + return h.Fp == first && h.Phase == nec.PhaseRunning && string(got) == "listen: new\n" }) if moved := runtime.moved(server); len(moved) != 0 || runtime.containers[server] != made { t.Fatalf("the first want moved the bus server its build does not change: %v", moved) @@ -871,10 +872,10 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test busWant(delivery.Want{FP: next, Self: true}) within(t, 10*time.Second, "the build that changes the server was not applied", func() bool { h := b.have("nats") - if h.Phase == delivery.PhaseWaitingYes && h.Waits != "" { + if h.Phase == nec.PhaseWaitingYes && h.Waits != "" { busWant(delivery.Want{FP: next, Self: true, Yes: h.Waits}) } - return h.FP == next && h.Phase == delivery.PhaseRunning + return h.Fp == next && h.Phase == nec.PhaseRunning }) var runs int for _, a := range runtime.moved(server) { diff --git a/cmd/mesh-host/trust.go b/cmd/mesh-host/trust.go index 4ff1901..32001fe 100644 --- a/cmd/mesh-host/trust.go +++ b/cmd/mesh-host/trust.go @@ -7,6 +7,8 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "os" "path/filepath" "strings" @@ -193,7 +195,7 @@ func placedAllHave(ctx context.Context, session *link.Session, node string) (boo if err != nil { return false, err.Error() } - return allHave(p, func(module string) (delivery.Have, bool, error) { + return allHave(p, func(module string) (nodeengine.HaveByNodeModuleValue, bool, error) { return session.ReadHaveErr(ctx, module) }) } @@ -226,7 +228,7 @@ func placedModules(node string, values map[string][]byte) (delivery.Placement, e // declaration is the one record of what runs of it: retired then, the node's own composed it as nothing and refused to // remove what runs (issue 774, 2026-10-30 19:43: systemd-resolved on shanks, nats and restic on novox, each node's own // apply refused from then on). -func allHave(p delivery.Placement, has func(module string) (delivery.Have, bool, error)) (bool, string) { +func allHave(p delivery.Placement, has func(module string) (nodeengine.HaveByNodeModuleValue, bool, error)) (bool, string) { if len(p.Modules) == 0 { return false, "the seat mesh-module-assigner places no module here" } @@ -240,7 +242,7 @@ func allHave(p delivery.Placement, has func(module string) (delivery.Have, bool, } // A module the mesh provides (mesh-wireguard) never has a fingerprint: the node's own composes it from the manifest // mesh-sdk provides (delivery.ProvidedManifest), never from the kept declaration, so it keeps no read. - if h.Phase != delivery.PhaseRemoved && !strings.HasPrefix(h.FP, delivery.SourcePrefix2) && !delivery.ProvidedByMesh(m.Module) { + if h.Phase != nodeengineclient.PhaseRemoved && !strings.HasPrefix(h.Fp, delivery.SourcePrefix2) && !delivery.ProvidedByMesh(m.Module) { return false, m.Module + " runs at a build of the old form (" + h.Build + "), which only the kept declaration records" } } diff --git a/cmd/mesh-host/whatruns_test.go b/cmd/mesh-host/whatruns_test.go index bfbb992..31e08ff 100644 --- a/cmd/mesh-host/whatruns_test.go +++ b/cmd/mesh-host/whatruns_test.go @@ -5,6 +5,8 @@ import ( "crypto/ed25519" "encoding/json" "errors" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" engcomp "github.com/novox/mesh-host/internal/compositions" "path/filepath" "sort" @@ -120,24 +122,24 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) { placed := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: "lemurs"}}} // Issue 774, 2026-10-30 19:43: a have naming only a build of the old form (systemd-resolved on shanks, folded in the // node's own) keeps the kept declaration read, since it is the one record of what runs of it. - newForm := delivery.Have{FP: delivery.SourcePrefix2 + strings.Repeat("a", 64), Phase: delivery.PhaseRunning} - oldForm := delivery.Have{Build: "build-1791404191702719760", Phase: delivery.PhaseRunning} + newForm := nodeengine.HaveByNodeModuleValue{Fp: delivery.SourcePrefix2 + strings.Repeat("a", 64), Phase: nodeengineclient.PhaseRunning} + oldForm := nodeengine.HaveByNodeModuleValue{Build: "build-1791404191702719760", Phase: nodeengineclient.PhaseRunning} for _, row := range []struct { name string - has map[string]delivery.Have + has map[string]nodeengine.HaveByNodeModuleValue want bool }{ - {"every placed module has a have of the new form", map[string]delivery.Have{"zsh": newForm, "lemurs": newForm}, true}, - {"one has none yet", map[string]delivery.Have{"zsh": newForm}, false}, - {"one runs at a build of the old form", map[string]delivery.Have{"zsh": newForm, "lemurs": oldForm}, false}, + {"every placed module has a have of the new form", map[string]nodeengine.HaveByNodeModuleValue{"zsh": newForm, "lemurs": newForm}, true}, + {"one has none yet", map[string]nodeengine.HaveByNodeModuleValue{"zsh": newForm}, false}, + {"one runs at a build of the old form", map[string]nodeengine.HaveByNodeModuleValue{"zsh": newForm, "lemurs": oldForm}, false}, } { - if got, why := allHave(placed, func(m string) (delivery.Have, bool, error) { h, ok := row.has[m]; return h, ok, nil }); got != row.want { + if got, why := allHave(placed, func(m string) (nodeengine.HaveByNodeModuleValue, bool, error) { h, ok := row.has[m]; return h, ok, nil }); got != row.want { t.Errorf("%s: %v (%s)", row.name, got, why) } } // The module the mesh provides never has a fingerprint, and keeps no read (review of issue 774). withWireguard := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: overlay.Name}}} - if got, why := allHave(withWireguard, func(m string) (delivery.Have, bool, error) { + if got, why := allHave(withWireguard, func(m string) (nodeengine.HaveByNodeModuleValue, bool, error) { if m == overlay.Name { return oldForm, true, nil } diff --git a/internal/agent/agent.go b/internal/agent/agent.go index a80bd7b..cda5919 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -17,6 +17,8 @@ import ( "context" "encoding/json" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "github.com/novox/mesh-host/internal/compositions" "slices" "sort" @@ -61,18 +63,18 @@ type Have struct { // waits for a person's yes (ADR 0547 §3). Judgement string Waits string - // BuildAlone says, in phase waiting-yes, the move changes the build alone (novox/hq ADR 0601 §1, delivery.Have.BuildAlone). + // BuildAlone says, in phase waiting-yes, the move changes the build alone (novox/hq ADR 0601 §1, nodeengine.HaveByNodeModuleValue.BuildAlone). BuildAlone bool // Kept says, in words, what the node keeps of the module as it runs because its manifest does not yet say it (ADR 0601 - // §4, delivery.Have.Kept): the watchman raises a condition from it. + // §4, nodeengine.HaveByNodeModuleValue.Kept): the watchman raises a condition from it. Kept []string // Sealed is, on a node composing its own, each value the composition it names carries sealed to the node's key, by - // slot (delivery.Have.SealedAt): kept in the have so the composer reuses them after a restart (ADR 0547 §3), each at + // slot (nodeengine.HaveByNodeModuleValue.SealedAt): kept in the have so the composer reuses them after a restart (ADR 0547 §3), each at // its own slot alone (issue 1126, C1). Sealed compositions.Seals // PullWait is, on a failed have, the pull from a store its apply failed on and waits on the holder of (issue 1966 - // ENG-9, delivery.Have.PullWait): read through the have, as node_haves shows it. - PullWait *delivery.PullWait + // ENG-9, nodeengine.HaveByNodeModuleValue.PullWait): read through the have, as node_haves shows it. + PullWait *nodeengine.PullWait } // Haves is the node's have bucket as the agent uses it. @@ -220,7 +222,7 @@ type Agent struct { mu sync.Mutex wants map[string]wantNow // by module comps map[string]compNow // by composed key - running map[string]delivery.Have + running map[string]nodeengine.HaveByNodeModuleValue // applied is the composition each module was last applied from: a revision a want named from the key's history is // not the key's latest value, and what it holds is still held out of the others' applies. applied map[string]compNow @@ -303,7 +305,7 @@ func New(env Env) *Agent { if env.Say == nil { env.Say = func(string) {} } - return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]delivery.Have{}, applied: map[string]compNow{}, had: map[string]compNow{}, keptData: map[string]bool{}, + return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]nodeengine.HaveByNodeModuleValue{}, applied: map[string]compNow{}, had: map[string]compNow{}, keptData: map[string]bool{}, attempt: map[string]attempt{}, checked: map[string]bool{}, prior: map[string]bool{}, said: map[string]string{}, notComposed: map[string]notComposedMark{}, pulls: map[string]*pullWait{}, settledAt: map[string]time.Time{}, settledSeq: map[string]int{}, retry: make(chan []string, 8), reconcile: make(chan []string, 64), failedAgain: make(chan string, 8), tries: map[string]int{}, again: make(chan againAsk, 64), @@ -495,7 +497,7 @@ const NotComposedDetail = "the node cannot compose it, and keeps running what it // composerMark says h is a mark NotComposed wrote: failed, its detail opening with NotComposedDetail. func composerMark(h Have) bool { - return h.Phase == string(delivery.PhaseFailed) && strings.HasPrefix(h.Detail, NotComposedDetail) + return h.Phase == string(nodeengineclient.PhaseFailed) && strings.HasPrefix(h.Detail, NotComposedDetail) } // HavesReadErr is what a Haves may answer besides Read: a have absent (false, nil) told from one that could not be read @@ -590,7 +592,7 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string a.mu.Unlock() back := mark.prior if ran { - back = Have{Module: module, FP: r.FP, Composed: r.Composed, Phase: string(delivery.PhaseRunning)} + back = Have{Module: module, FP: r.Fp, Composed: r.Composed, Phase: string(nodeengineclient.PhaseRunning)} if r.Phase != "" { back.Phase = string(r.Phase) } @@ -603,9 +605,9 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string } if !marked { if ran { - mark.prior = Have{Module: module, FP: r.FP, Composed: r.Composed, Phase: string(r.Phase)} + mark.prior = Have{Module: module, FP: r.Fp, Composed: r.Composed, Phase: string(r.Phase)} if mark.prior.Phase == "" { - mark.prior.Phase = string(delivery.PhaseRunning) + mark.prior.Phase = string(nodeengineclient.PhaseRunning) } } else if h, found, unread := a.readHave(ctx, module); found { mark.prior = h // what its last have said runs: this agent has run nothing of it since its start @@ -624,7 +626,7 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string // A have that already says another cause (an apply that failed, a backup held, an apply under way) keeps it: a // composer's mark over it would hide that cause, and baseline would take the mark for what runs (review of the // switch, round 1). - if p := mark.prior.Phase; !marked && p != "" && p != string(delivery.PhaseRunning) && !composerMark(mark.prior) { + if p := mark.prior.Phase; !marked && p != "" && p != string(nodeengineclient.PhaseRunning) && !composerMark(mark.prior) { a.mu.Lock() a.sayOnce("unmarked-cause:"+module, fmt.Sprintf("%s is not marked: its have says %s (%s), and the node cannot compose it: %s", module, p, mark.prior.Detail, why)) @@ -639,7 +641,7 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string a.notComposed[module] = mark a.mu.Unlock() a.writeHave(ctx, Have{Module: module, FP: mark.prior.FP, Composed: mark.prior.Composed, Sealed: mark.prior.Sealed, - Phase: string(delivery.PhaseFailed), Detail: NotComposedDetail + why}) + Phase: string(nodeengineclient.PhaseFailed), Detail: NotComposedDetail + why}) } } @@ -696,7 +698,7 @@ func (a *Agent) KeptNow(ctx context.Context, kept map[string][]string) { a.mu.Unlock() slices.Sort(changed) for _, m := range changed { - if h, ok := a.env.Haves.Read(ctx, m); ok && m != NodeModule && h.Phase != string(delivery.PhaseWaitingYes) { + if h, ok := a.env.Haves.Read(ctx, m); ok && m != NodeModule && h.Phase != string(nodeengineclient.PhaseWaitingYes) { h.Module = m a.writeHave(ctx, h) } @@ -733,10 +735,10 @@ func (a *Agent) Running() []compositions.Composed { var out []compositions.Composed for _, m := range mods { h := a.running[m] - if h.Phase == delivery.PhaseRemoved { + if h.Phase == nodeengineclient.PhaseRemoved { continue } - if c, ok := a.compositionFor(m, h.FP); ok && c.Err == nil && c.Dig == h.Composed { + if c, ok := a.compositionFor(m, h.Fp); ok && c.Err == nil && c.Dig == h.Composed { out = append(out, c.C) } else if c, ok := a.applied[m]; ok && c.Dig == h.Composed { out = append(out, c.C) @@ -890,10 +892,10 @@ func (a *Agent) others(except string) []compositions.Composed { sort.Strings(mods) for _, m := range mods { h := a.running[m] - if h.Phase == delivery.PhaseRemoved { + if h.Phase == nodeengineclient.PhaseRemoved { continue } - if c, ok := a.compositionFor(m, h.FP); ok && c.Err == nil && c.Dig == h.Composed { + if c, ok := a.compositionFor(m, h.Fp); ok && c.Err == nil && c.Dig == h.Composed { out = append(out, c.C) } else if c, ok := a.applied[m]; ok && c.Dig == h.Composed { out = append(out, c.C) @@ -1091,8 +1093,8 @@ func (a *Agent) keepApplied(module string, comp compNow) { // backup, or a composer's mark over what still runs. A have backing up, applying, checking, failed or waiting names a // composition not applied, or not yet (issue 924, review of 031e106). func haveRuns(h Have) bool { - switch delivery.Phase(h.Phase) { - case "", delivery.PhaseRunning, delivery.PhaseHeld: + switch string(h.Phase) { + case "", nodeengineclient.PhaseRunning, nodeengineclient.PhaseHeld: return true } return composerMark(h) @@ -1171,7 +1173,7 @@ func (a *Agent) heldOut(except string) []compositions.Composed { if err != nil || isNode || c.Err != nil || m == except || held[m] { continue } - if h, ok := a.running[m]; ok && h.Phase != delivery.PhaseRemoved { + if h, ok := a.running[m]; ok && h.Phase != nodeengineclient.PhaseRemoved { continue } if w := a.wants[m]; w.Present && w.Want.Removed { @@ -1281,7 +1283,7 @@ func (a *Agent) RetryFailed(module string) error { return fmt.Errorf("%s has no want on this machine, so there is no apply of it to try again", module) case tried && att.Held: return fmt.Errorf("%s is held for a failed backup: the operator's answer releases it", module) - case !tried && ran && r.FP == w.Want.FP && r.Phase != delivery.PhaseRemoved: + case !tried && ran && r.Fp == w.Want.FP && r.Phase != nodeengineclient.PhaseRemoved: return fmt.Errorf("%s runs what its want names, nothing to retry", module) case !tried: return fmt.Errorf("%s has no failed apply to try again: it waits for its composition, or settles on its own", module) @@ -1314,7 +1316,7 @@ func (a *Agent) HoldsNode() bool { a.mu.Lock() defer a.mu.Unlock() h, ok := a.running[NodeModule] - return ok && h.Phase != delivery.PhaseRemoved + return ok && h.Phase != nodeengineclient.PhaseRemoved } func (a *Agent) settleOne(ctx context.Context, module string) { @@ -1370,7 +1372,7 @@ func (a *Agent) settleOne(ctx context.Context, module string) { } a.mu.Lock() r, runs := a.running[module] - runsWant := runs && r.FP == w.Want.FP && r.Composed == w.Want.Composed && r.Phase != delivery.PhaseRemoved + runsWant := runs && r.Fp == w.Want.FP && r.Composed == w.Want.Composed && r.Phase != nodeengineclient.PhaseRemoved att, tried := a.attempt[module] same := tried && att.FP == w.Want.FP && att.Composed == w.Want.Composed if same && att.Held { @@ -1445,7 +1447,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com a.mu.Lock() a.prior[module] = true a.mu.Unlock() - if h.Phase == string(delivery.PhaseHeld) && h.Detail == BackupInterrupted && w.Want.Backup != delivery.BackupSkip { + if h.Phase == string(nodeengineclient.PhaseHeld) && h.Detail == BackupInterrupted && w.Want.Backup != delivery.BackupSkip { // Held only because this node agent stopped while the backup ran (postgres on novox, 2026-10-31): no // failure and nothing for the operator to answer. It is backed up again and applied (a.prior makes the // apply back it up first); the want still asking a backup is all the answer needed. @@ -1454,7 +1456,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com a.mu.Unlock() return false } - if h.Phase == string(delivery.PhaseHeld) && (w.Want.Backup == delivery.BackupAgain || w.Want.Backup == delivery.BackupSkip) { + if h.Phase == string(nodeengineclient.PhaseHeld) && (w.Want.Backup == delivery.BackupAgain || w.Want.Backup == delivery.BackupSkip) { // Held for a failed backup, and the want already carries the operator's answer, written before this engine // started (nats on novox, 2026-11-04: "Back up again" answered, then the engine restarted and took the // answer for the hold's own revision, so it was never acted on; issue 1585). The answer is acted on now: @@ -1467,7 +1469,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com a.mu.Unlock() return false } - if h.Phase == string(delivery.PhaseHeld) { + if h.Phase == string(nodeengineclient.PhaseHeld) { // Held for a failed backup, and the operator's answer is still to come: the hold is kept, nothing is // backed up again or applied, and the next revision of the want is the answer (ADR 0568 §4). Said, so // why it holds is read from the journal and not guessed (issue 1585: this branch was silent). @@ -1478,7 +1480,7 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com a.attempt[module] = attempt{FP: w.Want.FP, Composed: w.Want.Composed, WantRev: w.Rev, CompRev: comp.Rev, Held: true, AnsweredRev: w.Rev} if h.Composed != "" { - a.running[module] = delivery.Have{FP: h.FP, Composed: h.Composed, Phase: delivery.PhaseRunning} + a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: h.FP, Composed: h.Composed, Phase: nodeengineclient.PhaseRunning} } a.mu.Unlock() a.runsChanged(module + " was taken as held") @@ -1495,18 +1497,18 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com h.Detail = "" } if h.FP == w.Want.FP && h.Composed == w.Want.Composed && - (h.Phase == "" || h.Phase == string(delivery.PhaseRunning) || marked) { + (h.Phase == "" || h.Phase == string(nodeengineclient.PhaseRunning) || marked) { if a.inactiveAtStart(ctx, module, comp) { return false } a.mu.Lock() - a.running[module] = delivery.Have{FP: h.FP, Composed: h.Composed, Phase: delivery.PhaseRunning} + a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: h.FP, Composed: h.Composed, Phase: nodeengineclient.PhaseRunning} a.mu.Unlock() a.runsChanged(module + " was taken from its have") // Said again, so the session holds the module and its key carries the health this engine judges, // not the one an older engine left there (xorg's unknown on g14 and shanks, 2026-10-25). a.writeHave(ctx, Have{Module: module, FP: h.FP, Composed: h.Composed, - Phase: string(delivery.PhaseRunning), Detail: h.Detail, Sealed: h.Sealed}) + Phase: string(nodeengineclient.PhaseRunning), Detail: h.Detail, Sealed: h.Sealed}) return true } return false @@ -1529,10 +1531,10 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com return false } a.mu.Lock() - a.running[module] = delivery.Have{FP: w.Want.FP, Composed: w.Want.Composed, Phase: delivery.PhaseRunning} + a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: w.Want.FP, Composed: w.Want.Composed, Phase: nodeengineclient.PhaseRunning} a.mu.Unlock() a.runsChanged(module + " was taken from the kept declaration") - a.writeHave(ctx, Have{Module: module, FP: w.Want.FP, Composed: w.Want.Composed, Phase: string(delivery.PhaseRunning)}) + a.writeHave(ctx, Have{Module: module, FP: w.Want.FP, Composed: w.Want.Composed, Phase: string(nodeengineclient.PhaseRunning)}) return true } @@ -1567,10 +1569,10 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo a.mu.Unlock() if !runs { if h, found, _ := a.readHave(ctx, module); found { - prev = delivery.Have{FP: h.FP, Composed: h.Composed} + prev = nodeengine.HaveByNodeModuleValue{Fp: h.FP, Composed: h.Composed} } } - a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseBackingUp)}) + a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseBackingUp)}) if got := a.env.Backup(ctx, module); !got.Done { words := got.Words if got.Interrupted || ctx.Err() != nil { @@ -1585,12 +1587,12 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo a.attempt[module] = attempt{FP: fp, Composed: dig, WantRev: w.Rev, CompRev: comp.Rev, Held: true, AnsweredRev: w.Rev} a.mu.Unlock() // It keeps running what it has: the have says what it holds and why it waits. - a.writeHave(ctx, Have{Module: module, FP: prev.FP, Composed: prev.Composed, Phase: string(delivery.PhaseHeld), + a.writeHave(ctx, Have{Module: module, FP: prev.Fp, Composed: prev.Composed, Phase: string(nodeengineclient.PhaseHeld), Detail: words}) return } } - a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseApplying)}) + a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseApplying)}) res := a.env.Apply(ctx, comp.C, others) a.mu.Lock() if !res.Failed || res.Transient { @@ -1607,7 +1609,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo gen := a.gen a.attempt[module] = attempt{FP: fp, Composed: dig, WantRev: w.Rev, CompRev: comp.Rev, Transient: true, Gen: gen} a.mu.Unlock() - phase, when := delivery.PhaseApplying, "when the bus answers again" + phase, when := nodeengineclient.PhaseApplying, "when the bus answers again" if a.env.Backoff != nil { wait := a.env.Backoff(try) when = "in " + wait.String() @@ -1622,7 +1624,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo }) } if try > transientFor { - phase = delivery.PhaseFailed // still retried, and said as failed: no longer a moment + phase = nodeengineclient.PhaseFailed // still retried, and said as failed: no longer a moment } a.env.Say(fmt.Sprintf("applying %s failed because the bus did not answer (try %d), retried %s: %s", what, try, when, res.Detail)) a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(phase), @@ -1640,7 +1642,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo wait := a.pullWaitOf(module) a.mu.Unlock() a.env.Say(fmt.Sprintf("applying %s failed: %s", what, res.Detail)) - a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseFailed), Detail: detail, + a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseFailed), Detail: detail, PullWait: wait}) if again != "" { a.askPullAgain(module, again) @@ -1650,14 +1652,14 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo delete(a.pulls, module) delete(a.attempt, module) delete(a.notComposed, module) // the apply says what it runs now - a.running[module] = delivery.Have{FP: fp, Composed: dig, Phase: delivery.PhaseRunning} + a.running[module] = nodeengine.HaveByNodeModuleValue{Fp: fp, Composed: dig, Phase: nodeengineclient.PhaseRunning} a.applied[module] = comp a.mu.Unlock() a.keepApplied(module, comp) if a.env.Settled != nil { a.env.Settled(module) } - a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseRunning), Detail: res.Detail}) + a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseRunning), Detail: res.Detail}) } func (a *Agent) remove(ctx context.Context, module string, w wantNow) { @@ -1668,7 +1670,7 @@ func (a *Agent) remove(ctx context.Context, module string, w wantNow) { a.mu.Unlock() return } - if ran && r.Phase == delivery.PhaseRemoved { + if ran && r.Phase == nodeengineclient.PhaseRemoved { a.mu.Unlock() return } @@ -1685,7 +1687,7 @@ func (a *Agent) remove(ctx context.Context, module string, w wantNow) { delete(a.attempt, module) delete(a.pulls, module) delete(a.had, module) - a.running[module] = delivery.Have{Phase: delivery.PhaseRemoved} + a.running[module] = nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseRemoved} a.mu.Unlock() if a.env.Applied != nil { if err := a.env.Applied.Drop(module); err != nil { @@ -1766,17 +1768,17 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp a.readKeptData(ctx, module) // the manifest may read now though it did not at the start a.mu.Lock() r, ran := a.running[module] - if ran && r.Composed == comp.Dig && r.Phase != delivery.PhaseRemoved { + if ran && r.Composed == comp.Dig && r.Phase != nodeengineclient.PhaseRemoved { a.mu.Unlock() // Composed back to what runs: a have still waiting for a yes for a move since gone says running again, or it waits // for a yes nobody needs (issue 774, 2026-10-31: four providers stayed waiting-yes once their consumers composed). - if h, ok := a.env.Haves.Read(ctx, module); ok && h.Phase == string(delivery.PhaseWaitingYes) { + if h, ok := a.env.Haves.Read(ctx, module); ok && h.Phase == string(nodeengineclient.PhaseWaitingYes) { // Its sealed values only when they are of what runs; else writeHave reads them (sealedOf). var sealed compositions.Seals if h.Composed == r.Composed { sealed = h.Sealed } - a.writeHave(ctx, Have{Module: module, FP: r.FP, Composed: r.Composed, Phase: string(delivery.PhaseRunning), Sealed: sealed}) + a.writeHave(ctx, Have{Module: module, FP: r.Fp, Composed: r.Composed, Phase: string(nodeengineclient.PhaseRunning), Sealed: sealed}) } return false } @@ -1799,12 +1801,12 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp case known: j = judge.JudgeMove(&from, comp.C) case prior: - j = judge.Judged{Judgement: delivery.JudgedCannot, Words: "what " + module + " runs on this node cannot be read"} + j = judge.Judged{Judgement: nodeengineclient.JudgedCannot, Words: "what " + module + " runs on this node cannot be read"} default: j = judge.JudgeMove(nil, comp.C) } switch j.Judgement { - case delivery.JudgedRecreates, delivery.JudgedMovesData, delivery.JudgedCannot: + case nodeengineclient.JudgedRecreates, nodeengineclient.JudgedMovesData, nodeengineclient.JudgedCannot: default: return false } @@ -1812,7 +1814,7 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp a.sayOnce("yes:"+module+":"+comp.Dig, fmt.Sprintf("%s at %s waits for a person's yes: %s", module, w.Want.FP, j.Words)) prev := a.running[module] a.mu.Unlock() - a.writeHave(ctx, Have{Module: module, FP: prev.FP, Composed: prev.Composed, Phase: string(delivery.PhaseWaitingYes), + a.writeHave(ctx, Have{Module: module, FP: prev.Fp, Composed: prev.Composed, Phase: string(nodeengineclient.PhaseWaitingYes), Detail: j.Words, Judgement: string(j.Judgement), Waits: comp.Dig, BuildAlone: a.buildAlone(ctx, module, prev, w.Want.FP)}) return true } @@ -1820,17 +1822,17 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp // buildAlone says a move of module from what runs to the build want changes the build alone (ADR 0601 §1): another build than // the one it runs, and the build it runs composed with today's settings and secrets is the composition it runs. Anything it // cannot tell is no: the move is then asked of a person. -func (a *Agent) buildAlone(ctx context.Context, module string, prev delivery.Have, want string) bool { - if prev.FP == "" && prev.Composed == "" { +func (a *Agent) buildAlone(ctx context.Context, module string, prev nodeengine.HaveByNodeModuleValue, want string) bool { + if prev.Fp == "" && prev.Composed == "" { // Nothing of it run since this agent's start: its last have says what runs, as the have written keeps it. if h, ok := a.env.Haves.Read(ctx, module); ok { - prev.FP, prev.Composed = h.FP, h.Composed + prev.Fp, prev.Composed = h.FP, h.Composed } } - if a.env.ComposedToday == nil || prev.FP == "" || prev.Composed == "" || prev.FP == want { + if a.env.ComposedToday == nil || prev.Fp == "" || prev.Composed == "" || prev.Fp == want { return false } - dig, ok := a.env.ComposedToday(ctx, module, prev.FP) + dig, ok := a.env.ComposedToday(ctx, module, prev.Fp) return ok && dig == prev.Composed } @@ -2001,11 +2003,15 @@ func (a *Agent) Settled(holder string) { // pullWaitOf is the pull a failed apply of module waits on, as its have carries it (ENG-9): the image, the seat and its // holder, when the pull began, and the last retry with what triggered it; nil when it waits on no holder. a.mu is held. -func (a *Agent) pullWaitOf(module string) *delivery.PullWait { +func (a *Agent) pullWaitOf(module string) *nodeengine.PullWait { pw := a.pulls[module] if pw == nil { return nil } - return &delivery.PullWait{Image: pw.Image, Seat: pw.Seat, Holder: pw.Holder, Began: pw.Started, Retried: pw.last, - RetriedAt: pw.lastAt} + out := &nodeengine.PullWait{Image: pw.Image, Seat: pw.Seat, Holder: pw.Holder, Began: pw.Started, Retried: pw.last} + if !pw.lastAt.IsZero() { + at := pw.lastAt + out.RetriedAt = &at + } + return out } diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index c34699a..2d205b4 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "github.com/novox/mesh-host/internal/compositions" "maps" "slices" @@ -300,7 +302,7 @@ func TestAWantAndItsCompositionApplyTheModuleAndTheHaveNamesBoth(t *testing.T) { eventually(t, "the module was not applied", func() bool { return len(b.appliedList()) == 1 }) eventually(t, "the have does not name both", func() bool { h := b.haves.last("web") - return h.FP == fp && h.Composed == dig && h.Phase == string(delivery.PhaseRunning) + return h.FP == fp && h.Composed == dig && h.Phase == string(nodeengineclient.PhaseRunning) }) // Settled is told once what runs holds the module, so a declaration read then names its files (the filling's Knock; // ace, 2026-10-25 21:17). @@ -337,7 +339,7 @@ func TestStartingOnWhatTheKeptDeclarationHoldsAppliesNothing(t *testing.T) { stop := b.run() defer stop() eventually(t, "the have was not written", func() bool { return b.haves.last("web").Composed == dig }) - if h := b.haves.last("web"); h.FP != fp || h.Phase != string(delivery.PhaseRunning) { + if h := b.haves.last("web"); h.FP != fp || h.Phase != string(nodeengineclient.PhaseRunning) { t.Fatalf("the have: %+v", h) } time.Sleep(50 * time.Millisecond) @@ -357,7 +359,7 @@ func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) { fp := fpOf("a") ck, craw, dig := composition(t, "avahi", fp, false, file("avahi", "x", "1")) wk, wraw := wantFor(t, "avahi", fp, dig, "") - b.haves.stored = map[string]Have{"avahi": {Module: "avahi", FP: fp, Composed: dig, Phase: string(delivery.PhaseFailed), + b.haves.stored = map[string]Have{"avahi": {Module: "avahi", FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseFailed), Detail: "apply: unit failed to start"}} b.comps.before(ck, craw) b.wants.before(wk, wraw) @@ -372,7 +374,7 @@ func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) { fp := fpOf("a") ck, craw, dig := composition(t, "avahi", fp, false, file("avahi", "x", "1")) wk, wraw := wantFor(t, "avahi", fp, dig, "") - b.haves.stored = map[string]Have{"avahi": {Module: "avahi", FP: fp, Composed: dig, Phase: string(delivery.PhaseFailed), + b.haves.stored = map[string]Have{"avahi": {Module: "avahi", FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseFailed), Detail: NotComposedDetail + "frombus: the credentials of ace: context canceled"}} b.comps.before(ck, craw) b.wants.before(wk, wraw) @@ -380,7 +382,7 @@ func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) { defer stop() eventually(t, "the mark was not cleared", func() bool { h := b.haves.last("avahi") - return h.Phase == string(delivery.PhaseRunning) && h.FP == fp && h.Composed == dig && h.Detail == "" + return h.Phase == string(nodeengineclient.PhaseRunning) && h.FP == fp && h.Composed == dig && h.Detail == "" }) if got := b.appliedList(); len(got) != 0 { t.Fatalf("a module whose mark names what the want names was applied: %v", got) @@ -427,7 +429,7 @@ func testStartingOnItsOwnHaveSaysTheHaveAgainSoItsHealthIsThisEngines(t *testing if len(log) != 1 { t.Fatalf("the have was said %d times, want once: %+v", len(log), log) } - if h := log[0]; h.Module != "xorg" || h.FP != fp || h.Composed != dig || h.Phase != string(delivery.PhaseRunning) || h.Detail != "kept" { + if h := log[0]; h.Module != "xorg" || h.FP != fp || h.Composed != dig || h.Phase != string(nodeengineclient.PhaseRunning) || h.Detail != "kept" { t.Fatalf("the have said again: %+v", h) } if got := b.appliedList(); len(got) != 0 { @@ -454,7 +456,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) { eventually(t, "not applied", func() bool { return len(b.appliedList()) == 1 }) eventually(t, "the have does not say failed", func() bool { h := b.haves.last("web") - return h.Phase == string(delivery.PhaseFailed) && strings.Contains(h.Detail, "no space") && h.FP == fp + return h.Phase == string(nodeengineclient.PhaseFailed) && strings.Contains(h.Detail, "no space") && h.FP == fp }) // An unrelated key changes: not a retry. ck9, craw9, _ := composition(t, "web", fpOf("9"), false, file("web", "x", "9")) @@ -472,7 +474,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) { t.Fatalf("a failed apply was refused a retry: %v", err) } eventually(t, "the asked retry did not apply", func() bool { return len(b.appliedList()) == 2 }) - eventually(t, "not running", func() bool { return b.haves.last("web").Phase == string(delivery.PhaseRunning) }) + eventually(t, "not running", func() bool { return b.haves.last("web").Phase == string(nodeengineclient.PhaseRunning) }) for module, words := range map[string]string{"web": "runs what its want names", "ghost": "has no want"} { if err := b.ag.RetryFailed(module); err == nil || !strings.Contains(err.Error(), words) { t.Errorf("a retry of %s: %v, want refused saying %q", module, err, words) @@ -509,7 +511,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) { eventually(t, "not applied", func() bool { return len(b.appliedList()) == 1 }) eventually(t, "the have does not say whom it waits on", func() bool { h := b.haves.last("web") - return h.Phase == string(delivery.PhaseFailed) && strings.Contains(h.Detail, "tried again once distribution has settled") + return h.Phase == string(nodeengineclient.PhaseFailed) && strings.Contains(h.Detail, "tried again once distribution has settled") }) b.idle() if n := len(b.appliedList()); n != 1 { @@ -527,7 +529,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) { eventually(t, "not tried again on the holder's settled", func() bool { return len(b.appliedList()) == 2 }) eventually(t, "the have does not say what triggered the retry", func() bool { h := b.haves.last("web") - return h.Phase == string(delivery.PhaseFailed) && strings.Contains(h.Detail, "last tried again at") && strings.Contains(h.Detail, "distribution settled") + return h.Phase == string(nodeengineclient.PhaseFailed) && strings.Contains(h.Detail, "last tried again at") && strings.Contains(h.Detail, "distribution settled") }) b.idle() if n := len(b.appliedList()); n != 2 { @@ -536,7 +538,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) { // The failed have carries the pull it waits on (ENG-9, read through node_haves): the image, whose settled it waits // on, and what triggered the last retry. if w := b.haves.last("web").PullWait; w == nil || w.Holder != "distribution" || w.Image != "reg/x@sha256:1" || - w.Began.IsZero() || !strings.Contains(w.Retried, "distribution settled") || w.RetriedAt.IsZero() { + w.Began.IsZero() || !strings.Contains(w.Retried, "distribution settled") || w.RetriedAt == nil { t.Fatalf("the failed have says it waits on %+v", w) } // The next settled answers once more, and the pull works: the module runs, and waits on nothing. @@ -546,7 +548,7 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) { tick() b.ag.Settled("distribution") eventually(t, "not tried again on the next settled", func() bool { return len(b.appliedList()) == 3 }) - eventually(t, "not running", func() bool { return b.haves.last("web").Phase == string(delivery.PhaseRunning) }) + eventually(t, "not running", func() bool { return b.haves.last("web").Phase == string(nodeengineclient.PhaseRunning) }) if w := b.haves.last("web").PullWait; w != nil { t.Fatalf("a running module's have still waits on %+v", w) } @@ -620,11 +622,11 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) { b.ag.RetryTransient() } eventually(t, "not applied again without a new revision", func() bool { return len(b.appliedList()) == 2 }) - eventually(t, "not running", func() bool { return b.haves.last("vault").Phase == string(delivery.PhaseRunning) }) + eventually(t, "not running", func() bool { return b.haves.last("vault").Phase == string(nodeengineclient.PhaseRunning) }) b.haves.mu.Lock() defer b.haves.mu.Unlock() for _, h := range b.haves.log { - if h.Phase == string(delivery.PhaseFailed) { + if h.Phase == string(nodeengineclient.PhaseFailed) { t.Errorf("a transient failure was written failed: %+v", h) } } @@ -664,7 +666,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) { }) eventually(t, "the have does not say held", func() bool { h := b.haves.last("db") - return h.Phase == string(delivery.PhaseHeld) && strings.Contains(h.Detail, "repository is locked") && h.FP == fp1 + return h.Phase == string(nodeengineclient.PhaseHeld) && strings.Contains(h.Detail, "repository is locked") && h.FP == fp1 }) if got := b.appliedList(); len(got) != 0 { t.Fatalf("something was applied behind a failed backup: %v", got) @@ -703,7 +705,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) { } eventually(t, "not running at the new fingerprint", func() bool { h := b.haves.last("db") - return h.Phase == string(delivery.PhaseRunning) && h.FP == fp2 && h.Composed == dig2 + return h.Phase == string(nodeengineclient.PhaseRunning) && h.FP == fp2 && h.Composed == dig2 }) // The node agent stopped while the backup ran, the module known only from its have: the held have names what runs @@ -713,7 +715,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) { ib.comps.before(ck2, craw2) iwk, iwraw := wantFor(t, "db", fp2, dig2, delivery.BackupAgain) ib.wants.before(iwk, iwraw) - ib.haves.stored = map[string]Have{"db": {Module: "db", FP: fp1, Composed: dig1, Phase: string(delivery.PhaseRunning)}} + ib.haves.stored = map[string]Have{"db": {Module: "db", FP: fp1, Composed: dig1, Phase: string(nodeengineclient.PhaseRunning)}} began := make(chan struct{}) ib.env.Backup = func(ctx context.Context, _ string) Backup { close(began) @@ -723,7 +725,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) { stopI := ib.run() <-began stopI() - if h := ib.haves.last("db"); h.Phase != string(delivery.PhaseHeld) || h.FP != fp1 || h.Composed != dig1 || h.Detail != BackupInterrupted { + if h := ib.haves.last("db"); h.Phase != string(nodeengineclient.PhaseHeld) || h.FP != fp1 || h.Composed != dig1 || h.Detail != BackupInterrupted { t.Errorf("held after an interrupted backup: %+v", h) } @@ -733,24 +735,24 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) { for _, row := range []struct { detail, backup string backups, applied int - phase delivery.Phase + phase string namesNothing bool // the have names no fp and no composition }{ - {BackupInterrupted, delivery.BackupAgain, 1, 1, delivery.PhaseRunning, false}, + {BackupInterrupted, delivery.BackupAgain, 1, 1, nodeengineclient.PhaseRunning, false}, // novox's postgres: its held have, written before mesh-host #189, names nothing it runs. - {BackupInterrupted, "", 1, 1, delivery.PhaseRunning, true}, + {BackupInterrupted, "", 1, 1, nodeengineclient.PhaseRunning, true}, // A backup that failed, and the want carries the operator's answer, written before this start (nats on novox, // 2026-11-04, issue 1585): again backs it up and applies, skip applies without one; no answer keeps the hold. - {"restic: the repository is locked", delivery.BackupAgain, 1, 1, delivery.PhaseRunning, false}, - {"restic: the repository is locked", delivery.BackupSkip, 0, 1, delivery.PhaseRunning, false}, - {"restic: the repository is locked", "", 0, 0, delivery.PhaseHeld, false}, + {"restic: the repository is locked", delivery.BackupAgain, 1, 1, nodeengineclient.PhaseRunning, false}, + {"restic: the repository is locked", delivery.BackupSkip, 0, 1, nodeengineclient.PhaseRunning, false}, + {"restic: the repository is locked", "", 0, 0, nodeengineclient.PhaseHeld, false}, } { r := newBed(t) r.comps.before(ck1, craw1) r.comps.before(ck2, craw2) wk, wraw := wantFor(t, "db", fp2, dig2, row.backup) r.wants.before(wk, wraw) - held := Have{Module: "db", FP: fp1, Composed: dig1, Phase: string(delivery.PhaseHeld), Detail: row.detail} + held := Have{Module: "db", FP: fp1, Composed: dig1, Phase: string(nodeengineclient.PhaseHeld), Detail: row.detail} if row.namesNothing { held.FP, held.Composed = "", "" } @@ -800,7 +802,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) { return raw } r.wants.before(wk, selfWant(row.atStart)) - r.haves.stored = map[string]Have{"nats": {Module: "nats", FP: fp2, Composed: dig, Phase: string(delivery.PhaseHeld), + r.haves.stored = map[string]Have{"nats": {Module: "nats", FP: fp2, Composed: dig, Phase: string(nodeengineclient.PhaseHeld), Detail: "the backup failed: sh: exit status 1"}} stopR := r.run() defer stopR() @@ -831,7 +833,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) { eventually(t, "the answer was not acted on", func() bool { return len(r.appliedList()) == 1 }) eventually(t, "not running after the answer", func() bool { h := r.haves.last("nats") - return h.Phase == string(delivery.PhaseRunning) && h.Composed == dig + return h.Phase == string(nodeengineclient.PhaseRunning) && h.Composed == dig }) r.mu.Lock() n := len(r.backups) @@ -959,9 +961,9 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) { if got := a.Everything(); len(got) != 2 { t.Fatalf("before the agent holds anything: %d compositions, want the 2 kept", len(got)) } - a.running["web"] = delivery.Have{FP: fpOf("a"), Composed: "sha256:" + strings.Repeat("1", 64), Phase: delivery.PhaseRunning} + a.running["web"] = nodeengine.HaveByNodeModuleValue{Fp: fpOf("a"), Composed: "sha256:" + strings.Repeat("1", 64), Phase: nodeengineclient.PhaseRunning} ck, craw, dig := composition(t, "web", fpOf("a"), false, file("web", "x", "new")) - a.running["web"] = delivery.Have{FP: fpOf("a"), Composed: dig, Phase: delivery.PhaseRunning} + a.running["web"] = nodeengine.HaveByNodeModuleValue{Fp: fpOf("a"), Composed: dig, Phase: nodeengineclient.PhaseRunning} a.takeComposition(Update{Key: ck, Value: craw, Revision: 1}) got := a.Everything() if len(got) != 2 { @@ -998,7 +1000,7 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) { 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)}) + b.haves.Write(context.Background(), Have{Module: "web", FP: fpOf("a"), Composed: webDig, Phase: string(nodeengineclient.PhaseRunning)}) // The composer's first pass reads what runs when its watch begins, before the agent reads any have. firstCh := make(chan []compositions.Composed, 1) comps := b.comps @@ -1143,7 +1145,7 @@ func TestAFailedApplyIsAppliedAgainAfterARestartWhateverIsKept(t *testing.T) { b.comps.before(ck, craw) b.wants.before(wk, wraw) b.kept = map[string][]json.RawMessage{"web": {file("web", "x", "1")}} - b.haves.stored = map[string]Have{"web": {Module: "web", FP: fp, Composed: dig, Phase: string(delivery.PhaseFailed)}} + b.haves.stored = map[string]Have{"web": {Module: "web", FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseFailed)}} stop := b.run() defer stop() eventually(t, "a failed apply was marked running from the kept declaration", func() bool { return len(b.appliedList()) == 1 }) @@ -1425,7 +1427,7 @@ func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) { {name: "a have naming the bucket's value", have: &Have{Module: "power", FP: fp, Composed: appliedDig}, bucket: appliedRaw, filled: old + "," + added, told: "the haves were read at the start", data: true}, {name: "a have applying names nothing that runs yet", have: &Have{Module: "power", FP: fp, Composed: appliedDig, - Phase: string(delivery.PhaseApplying)}, bucket: appliedRaw, filled: old, told: "the haves were read at the start"}, + Phase: string(nodeengineclient.PhaseApplying)}, bucket: appliedRaw, filled: old, told: "the haves were read at the start"}, {name: "a data module from the kept declaration says Data as its manifest does", filled: old, told: "the haves were read at the start", data: true, holdsData: func(context.Context, string) (bool, bool) { return true, true }}, @@ -1583,7 +1585,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( // names what runs; composed again, the have is what it runs. web was applied by this agent; idle was not since the // agent's start (the switch and every delivery of the node-engine restart it), so what runs is what its have said. idleFP, idleDig := fpOf("c"), "sha256:"+strings.Repeat("9", 64) - b.haves.Write(context.Background(), Have{Module: "idle", FP: idleFP, Composed: idleDig, Phase: string(delivery.PhaseRunning), + b.haves.Write(context.Background(), Have{Module: "idle", FP: idleFP, Composed: idleDig, Phase: string(nodeengineclient.PhaseRunning), Sealed: compositions.Seals{"idle.key#sealed": `{"box":"idle"}`}}) want("idle", idleFP, "") eventually(t, "idle's want was not taken", func() bool { @@ -1599,7 +1601,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( module, fp, composed, slot, sealed string }{{"web", fp, dig, "web.pw#sealed", "1"}, {"idle", idleFP, idleDig, "idle.key#sealed", `{"box":"idle"}`}} { h := b.haves.last(row.module) - if h.Phase != string(delivery.PhaseFailed) || h.FP != row.fp || h.Composed != row.composed || + if h.Phase != string(nodeengineclient.PhaseFailed) || h.FP != row.fp || h.Composed != row.composed || len(h.Sealed) != 1 || h.Sealed[row.slot] != row.sealed || !strings.Contains(h.Detail, "web's password") { t.Errorf("%s's have while the node cannot compose: %+v", row.module, h) } @@ -1614,7 +1616,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( want("fresh", fpOf("e"), "") // never ran, its have absent: marked, naming nothing it runs // A have that already says another cause keeps it (review of the switch, round 1): an apply that failed. brokenFP, brokenDig := fpOf("f"), "sha256:"+strings.Repeat("8", 64) - b.haves.Write(context.Background(), Have{Module: "broken", FP: brokenFP, Composed: brokenDig, Phase: string(delivery.PhaseFailed), + b.haves.Write(context.Background(), Have{Module: "broken", FP: brokenFP, Composed: brokenDig, Phase: string(nodeengineclient.PhaseFailed), Detail: "apply: unit failed to start"}) want("broken", brokenFP, "") eventually(t, "unread's want was not taken", func() bool { @@ -1626,7 +1628,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( if h := b.haves.last("unread"); h.Module != "" { t.Errorf("a module whose have could not be read was marked: %+v", h) } - if h := b.haves.last("fresh"); h.Phase != string(delivery.PhaseFailed) || h.FP != "" { + if h := b.haves.last("fresh"); h.Phase != string(nodeengineclient.PhaseFailed) || h.FP != "" { t.Errorf("a module that never ran, its have absent, is not marked: %+v", h) } if h := b.haves.last("broken"); h.Detail != "apply: unit failed to start" || h.Composed != brokenDig { @@ -1642,7 +1644,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( } b.ag.NotComposed(context.Background(), "") for _, row := range []struct{ module, composed string }{{"web", dig}, {"idle", idleDig}} { - if h := b.haves.last(row.module); h.Phase != string(delivery.PhaseRunning) || h.Composed != row.composed || len(h.Sealed) != 1 { + if h := b.haves.last(row.module); h.Phase != string(nodeengineclient.PhaseRunning) || h.Composed != row.composed || len(h.Sealed) != 1 { t.Errorf("%s's have once the node composes again: %+v", row.module, h) } } @@ -1650,14 +1652,14 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( // novox) is marked alone, naming its own words; web, which composes, keeps running, unmarked. Composed again, idle's // have is what it runs. b.ag.NotComposedEach(context.Background(), "", map[string]string{"idle": "idle says ${config:name}, and nothing sets it"}) - if h := b.haves.last("idle"); h.Phase != string(delivery.PhaseFailed) || h.Composed != idleDig || !strings.Contains(h.Detail, "${config:name}") { + if h := b.haves.last("idle"); h.Phase != string(nodeengineclient.PhaseFailed) || h.Composed != idleDig || !strings.Contains(h.Detail, "${config:name}") { t.Errorf("idle's have while it alone is held: %+v", h) } - if h := b.haves.last("web"); h.Phase != string(delivery.PhaseRunning) || h.Composed != dig { + if h := b.haves.last("web"); h.Phase != string(nodeengineclient.PhaseRunning) || h.Composed != dig { t.Errorf("web, which composes, is marked beside idle: %+v", h) } b.ag.NotComposedEach(context.Background(), "", nil) - if h := b.haves.last("idle"); h.Phase != string(delivery.PhaseRunning) || h.Composed != idleDig { + if h := b.haves.last("idle"); h.Phase != string(nodeengineclient.PhaseRunning) || h.Composed != idleDig { t.Errorf("idle's have once it composes again: %+v", h) } // A revision at the same build (a config value changed) is applied. @@ -1682,7 +1684,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( b.comps.put(dk, drawMoved) eventually(t, "db's revision did not wait for a yes", func() bool { h := b.haves.last("db") - return h.Phase == string(delivery.PhaseWaitingYes) && h.Waits == ddigMoved + return h.Phase == string(nodeengineclient.PhaseWaitingYes) && h.Waits == ddigMoved }) if h := b.haves.last("db"); h.BuildAlone { t.Errorf("a revision at the build it runs is said to change the build alone: %+v", h) @@ -1692,8 +1694,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( keep(dk, draw) keep(dk, drawMoved) view, err := b.ag.Composition(context.Background(), "db", "", true) - if err != nil || view.Have == nil || view.Have.Phase != string(delivery.PhaseWaitingYes) || view.Composition.Digest != ddigMoved || - view.Runs == nil || view.Runs.Composed != ddig || view.Judged.Judgement != string(delivery.JudgedRecreates) || + if err != nil || view.Have == nil || view.Have.Phase != string(nodeengineclient.PhaseWaitingYes) || view.Composition.Digest != ddigMoved || + view.Runs == nil || view.Runs.Composed != ddig || view.Judged.Judgement != string(nodeengineclient.JudgedRecreates) || !view.Judged.WaitsForYes || len(view.Diff) != 1 || view.Diff[0].ID != "db.server" || view.Diff[0].Change != "changed" || len(view.Revisions) != 2 { t.Errorf("the composition of db waiting for a yes reads %+v (%v)", view, err) @@ -1731,7 +1733,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( b.comps.put(dk, draw) eventually(t, "db's have stays waiting-yes after its composition went back to what runs", func() bool { h := b.haves.last("db") - return h.Phase == string(delivery.PhaseRunning) && h.Composed == ddig && h.Waits == "" + return h.Phase == string(nodeengineclient.PhaseRunning) && h.Composed == ddig && h.Waits == "" }) if got := len(b.appliedList()); got != 4 { t.Fatalf("a composition equal to what runs was applied again: %d applies", got) @@ -1742,7 +1744,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( // node-engine restart it): its have alone says what runs. Its move waits for a yes as db's does, and its have keeps // what runs (fingerprint, digest, sealed values) rather than naming nothing (review of L2, round 3). recFP, recDig := fpOf("d"), "sha256:"+strings.Repeat("8", 64) - b.haves.Write(context.Background(), Have{Module: "rec", FP: recFP, Composed: recDig, Phase: string(delivery.PhaseRunning), + b.haves.Write(context.Background(), Have{Module: "rec", FP: recFP, Composed: recDig, Phase: string(nodeengineclient.PhaseRunning), Sealed: compositions.Seals{"rec.key#sealed": `{"box":"rec"}`}}) rk, rraw, rdig := unsigned("rec", fpB, container("rec@sha256:"+strings.Repeat("3", 64))) b.comps.put(dk2, draw2) @@ -1757,7 +1759,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( }{{"db", fp, "", ddig2, nil, false}, {"rec", recFP, recDig, rdig, compositions.Seals{"rec.key#sealed": `{"box":"rec"}`}, true}} { eventually(t, row.module+"'s move did not wait for a yes", func() bool { h := b.haves.last(row.module) - return h.Phase == string(delivery.PhaseWaitingYes) && h.Waits == row.waits + return h.Phase == string(nodeengineclient.PhaseWaitingYes) && h.Waits == row.waits }) h := b.haves.last(row.module) if h.FP != row.fp || (row.composed != "" && h.Composed != row.composed) || h.Composed == "" || @@ -1775,8 +1777,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes( want("rec", fpB, rdig) eventually(t, "the yes did not apply the move", func() bool { h, r := b.haves.last("db"), b.haves.last("rec") - return len(b.appliedList()) == applied+2 && h.Phase == string(delivery.PhaseRunning) && h.FP == fpB && - r.Phase == string(delivery.PhaseRunning) && r.FP == fpB && r.Composed == rdig + return len(b.appliedList()) == applied+2 && h.Phase == string(nodeengineclient.PhaseRunning) && h.FP == fpB && + r.Phase == string(nodeengineclient.PhaseRunning) && r.FP == fpB && r.Composed == rdig }) } @@ -1816,7 +1818,7 @@ func testAnEngineStartReconcilesAModuleFromTheMachinesFacts(t *testing.T) { } if c.held { b.haves.stored = map[string]Have{"web": {Module: "web", FP: fpOf("0"), Composed: "sha256:" + strings.Repeat("0", 64), - Phase: string(delivery.PhaseHeld), Detail: "restic: the repository is locked"}} + Phase: string(nodeengineclient.PhaseHeld), Detail: "restic: the repository is locked"}} } if c.kept { b.kept = map[string][]json.RawMessage{"web": rs} @@ -1867,7 +1869,7 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone( stop := b.run() defer stop() eventually(t, "web was not settled", func() bool { return b.haves.last("web").Composed == digW }) - eventually(t, "db was not held", func() bool { return b.haves.last("db").Phase == string(delivery.PhaseHeld) }) + eventually(t, "db was not held", func() bool { return b.haves.last("db").Phase == string(nodeengineclient.PhaseHeld) }) if got := b.appliedList(); len(got) != 0 { t.Fatalf("applied at the start: %v", got) } @@ -1882,13 +1884,13 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone( if got := b.appliedList(); len(got) != 0 { t.Fatalf("a held module and an unwanted one were applied: %v", got) } - if h := b.haves.last("db"); h.Phase != string(delivery.PhaseHeld) { + if h := b.haves.last("db"); h.Phase != string(nodeengineclient.PhaseHeld) { t.Fatalf("the held module's have changed: %+v", h) } b.ag.Reconcile("web") eventually(t, "web was not applied again", func() bool { return len(b.appliedList()) == 1 }) - eventually(t, "web is not running again", func() bool { return b.haves.last("web").Phase == string(delivery.PhaseRunning) }) + eventually(t, "web is not running again", func() bool { return b.haves.last("web").Phase == string(nodeengineclient.PhaseRunning) }) b.mu.Lock() b.failWith = "systemctl start web.service: exit status 1" @@ -1896,7 +1898,7 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone( b.ag.Reconcile("web") eventually(t, "the failed re-apply did not write the have failed with its words", func() bool { h := b.haves.last("web") - return h.Phase == string(delivery.PhaseFailed) && h.Detail == "systemctl start web.service: exit status 1" + return h.Phase == string(nodeengineclient.PhaseFailed) && h.Detail == "systemctl start web.service: exit status 1" }) time.Sleep(50 * time.Millisecond) if got := b.appliedList(); len(got) != 2 { diff --git a/internal/agent/composition.go b/internal/agent/composition.go index a3f86f4..48e3bd2 100644 --- a/internal/agent/composition.go +++ b/internal/agent/composition.go @@ -4,11 +4,11 @@ import ( "context" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" engcomp "github.com/novox/mesh-host/internal/compositions" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/declaration" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/judge" ) @@ -147,7 +147,7 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo have = &h phase := h.Phase if phase == "" { - phase = string(delivery.PhaseRunning) + phase = string(nodeengineclient.PhaseRunning) } view.Have = &HaveView{FP: h.FP, Composed: h.Composed, Phase: phase, Detail: h.Detail, Judgement: h.Judgement, Waits: h.Waits} } @@ -224,7 +224,7 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo running = &from rv := &RunsView{From: source, Resources: len(from.Resources), FP: from.FP} if ran { - rv.FP, rv.Composed, rv.Phase = r.FP, r.Composed, string(r.Phase) + rv.FP, rv.Composed, rv.Phase = r.Fp, r.Composed, string(r.Phase) } view.Runs = rv } @@ -243,7 +243,7 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo view.Judged = JudgedView{Judgement: string(j.Judgement), Words: j.Words} if a.env.SelfComposed && a.env.Policy != nil && module != NodeModule && (!w.Present || w.Want.Yes != read.Dig) { switch j.Judgement { - case delivery.JudgedRecreates, delivery.JudgedMovesData, delivery.JudgedCannot: + case nodeengineclient.JudgedRecreates, nodeengineclient.JudgedMovesData, nodeengineclient.JudgedCannot: view.Judged.WaitsForYes = a.env.Policy(ctx, module) == catalogue.PolicyRecord } } diff --git a/internal/buckets/buckets.go b/internal/buckets/buckets.go new file mode 100644 index 0000000..ae8c8ac --- /dev/null +++ b/internal/buckets/buckets.go @@ -0,0 +1,17 @@ +// Package buckets holds the names of the bus buckets the node-engine reads and writes, and how many revisions it keeps of +// each. A contract carries the local name of a state, not the module that holds it (ADR 0625 §4, §7); the node-engine +// reads through grants composed in code, so the holder's name is kept here, in its own code (ADR 0634 §1, finding F4). +package buckets + +// The node-engine's own states and the have state, written by every node's engine for its own keys. +const ( + // Have holds a module's have on a node, `.`, and the node's own facts, `.node`. + Have = "node-engine_have" + // Data holds a module's data items on a node. + Data = "mesh-host_data" + // Health holds a node's own health report. + Health = "mesh-host_health" +) + +// HaveHistory is the number of revisions the have bucket keeps per key. +const HaveHistory = 20 diff --git a/internal/buckets/buckets_test.go b/internal/buckets/buckets_test.go new file mode 100644 index 0000000..db981d6 --- /dev/null +++ b/internal/buckets/buckets_test.go @@ -0,0 +1,34 @@ +package buckets + +import ( + "encoding/json" + "os" + "testing" +) + +// The engine's own states are named _ by the manifest; a constant that drifts from it reads and writes a +// bucket nobody else knows (ADR 0634 §1, finding F4: the holder's name is kept in code and held to the manifest by this row). +func TestTheOwnStatesAreTheManifestsStates(t *testing.T) { + raw, err := os.ReadFile("../../module.json") + if err != nil { + t.Fatal(err) + } + var m struct { + Module string `json:"module"` + State []struct { + Name string `json:"name"` + } `json:"state"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + declared := map[string]bool{} + for _, s := range m.State { + declared[m.Module+"_"+s.Name] = true + } + for name, bucket := range map[string]string{"data": Data, "health": Health} { + if !declared[bucket] { + t.Errorf("the bucket %s for the state %s is not one the manifest of %s declares (%v)", bucket, name, m.Module, declared) + } + } +} diff --git a/internal/compose/frombus/answer.go b/internal/compose/frombus/answer.go index be62825..a3b9093 100644 --- a/internal/compose/frombus/answer.go +++ b/internal/compose/frombus/answer.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "github.com/novox/mesh-host/internal/compositions" "maps" @@ -13,7 +14,6 @@ import ( "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/resolution" ) @@ -81,7 +81,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st if len(failed) == 0 { return refuse(err) } - failed[delivery.HaveNodeWord] = err.Error() + failed[nodeengineclient.NodeWord] = err.Error() composed, skipped, stood = nil, nil, nil } // The node's own composition is judged only where the node runs one: one that runs none applies it only once a want @@ -97,7 +97,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st } } } - if _, whole := failed[delivery.HaveNodeWord]; whole { + if _, whole := failed[nodeengineclient.NodeWord]; whole { // Nothing composed to compare: every refusal is said, and nothing else. for _, m := range sortedKeys(failed) { a.Refusals = append(a.Refusals, nodeengine.CandidateRefusal{Module: m, Why: failed[m]}) @@ -173,7 +173,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st } // another node's addition: the module here that carries it differs, which the assigner judges case module == "" || node != s.Node: // The node's own, or a value this node holds for a module elsewhere (a grant it provides): said as the node's. - refusals[delivery.HaveNodeWord] = append(refusals[delivery.HaveNodeWord], x) + refusals[nodeengineclient.NodeWord] = append(refusals[nodeengineclient.NodeWord], x) default: refusals[module] = append(refusals[module], x) } diff --git a/internal/compose/frombus/answer_rows_test.go b/internal/compose/frombus/answer_rows_test.go index 134e3ae..784624e 100644 --- a/internal/compose/frombus/answer_rows_test.go +++ b/internal/compose/frombus/answer_rows_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" engcomp "github.com/novox/mesh-host/internal/compositions" "os" @@ -42,7 +43,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { p.Modules = append(p.Modules, delivery.PlacedModule{Module: m}) } b := &bus{placements: map[string]delivery.Placement{"n1": p}, - records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd", "container-runtime"}}}, + records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd", "container-runtime"}}}, modules: map[string]delivery.Module{"lemurs": {Manifest: delivery.ManifestDigest(lemurs)}}, manifests: map[string][]byte{delivery.ManifestDigest(lemurs): lemurs}, builds: map[string]*delivery.BuilderBuild{}} @@ -88,11 +89,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { t.Errorf("%s refused as %q, want %q", r.Module, r.Why, want) } } - 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 { + if raw, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Candidates: map[string]nodeengine.CandidateAnswer{"g": a}}); err != nil { t.Errorf("the answer is not one a have holds: %v (%s)", err, raw) } if lemursOK := slices.ContainsFunc(a.Added, func(m nodeengine.CandidateAdded) bool { return m.Module == "lemurs" }); !lemursOK { @@ -594,7 +591,7 @@ func otherNodeNotResolvedIsNamed(t *testing.T) { b := &bus{placements: map[string]delivery.Placement{ "n1": {Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1", Endpoint: "n1.example:51820"}, Modules: []delivery.PlacedModule{{Module: "cons"}, {Module: overlay.Name}}}, "n2": {Node: "n2", Decided: delivery.NodeDecided{Address: "10.77.0.2"}, Modules: []delivery.PlacedModule{{Module: "prov"}, {Module: overlay.Name}}}}, - records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t)}, "n2": {Node: "n2", TunnelKey: publicKey(t)}}, + records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t)}, "n2": {Node: "n2", TunnelKey: publicKey(t)}}, modules: map[string]delivery.Module{"prov": {Manifest: delivery.ManifestDigest(prov)}, "cons": {Manifest: delivery.ManifestDigest(cons)}}, manifests: map[string][]byte{delivery.ManifestDigest(prov): prov, delivery.ManifestDigest(cons): cons}} given := map[string]*delivery.Placement{} diff --git a/internal/compose/frombus/frombus.go b/internal/compose/frombus/frombus.go index 4f682dc..5acf056 100644 --- a/internal/compose/frombus/frombus.go +++ b/internal/compose/frombus/frombus.go @@ -35,6 +35,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" engcomp "github.com/novox/mesh-host/internal/compositions" "maps" "net/netip" @@ -57,8 +58,8 @@ type Reader interface { // Placements is every node's placement, by node (delivery.PlacementBucket). Placements(ctx context.Context) (map[string]delivery.Placement, error) // Records is every node's record, by node: HaveNode.Record of each node's own have key, `.node` of - // delivery.HaveBucket (ADR 0555 §3). - Records(ctx context.Context) (map[string]delivery.NodeRecord, error) + // buckets.Have (ADR 0555 §3). + Records(ctx context.Context) (map[string]nodeengine.NodeRecord, error) // Modules is every module the indexer holds, by module (delivery.ModulesBucket). Modules(ctx context.Context) (map[string]delivery.Module, error) // Manifest is the raw manifest of a digest (delivery.ManifestsBucket). @@ -252,7 +253,7 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module type base struct { in compose.Input placements map[string]delivery.Placement - records map[string]delivery.NodeRecord + records map[string]nodeengine.NodeRecord modules map[string]delivery.Module // ownRefused is each module placed on the composing node whose manifest the indexer refused, with why, in the order // read: every scope holds it. @@ -987,15 +988,24 @@ func declaresOwnSecrets(in compose.Input, node string) bool { return false } +// attachedOf is a record's attached networks as the composer holds them (the same two fields). +func attachedOf(in []nodeengine.AttachedNetwork) []catalogue.AttachedNetwork { + var out []catalogue.AttachedNetwork + for _, n := range in { + out = append(out, catalogue.AttachedNetwork{Link: n.Link, Network: n.Network}) + } + return out +} + // nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person // decided of it, and where its modules run). -func nodeOf(rec delivery.NodeRecord, p delivery.Placement) compose.Node { +func nodeOf(rec nodeengine.NodeRecord, p delivery.Placement) compose.Node { n := compose.Node{ Name: rec.Node, Place: compose.Place{Key: rec.TunnelKey}, SealingKey: rec.SealingKey, - OutwardLinks: rec.OutwardLinks, Networks: rec.Networks, NetworksSaid: rec.NetworksSaid, - ServingKey: rec.ServingKey, Certificate: rec.Certificate, Contract: rec.Contract, Stated: rec.Contract > 0, + OutwardLinks: rec.OutwardLinks, Networks: attachedOf(rec.Networks), NetworksSaid: rec.NetworksSaid, + ServingKey: rec.ServingKey, Certificate: rec.Certificate, Contract: int(rec.Contract), Stated: rec.Contract > 0, } placeNode(&n, p) if len(rec.Capabilities) > 0 { @@ -1092,7 +1102,7 @@ func tunnelOf(t delivery.RecordTunnel) compose.Tunnel { // carriedPeers is every peer of the hub's adopted tunnel, by address, each with the node that enrolled under its key // where one has (a node whose tunnel key is the peer's) and the name a person gave it (inv.CarriedPeers). -func carriedPeers(t compose.Tunnel, names map[string]string, records map[string]delivery.NodeRecord) []compose.CarriedPeer { +func carriedPeers(t compose.Tunnel, names map[string]string, records map[string]nodeengine.NodeRecord) []compose.CarriedPeer { byKey := map[string]string{} for node, rec := range records { if rec.TunnelKey != "" { @@ -1116,7 +1126,7 @@ func carriedPeers(t compose.Tunnel, names map[string]string, records map[string] } // nodesOf is every node named by a placement or a record, by name: the order the controller reads them in. -func nodesOf(placements map[string]delivery.Placement, records map[string]delivery.NodeRecord) []string { +func nodesOf(placements map[string]delivery.Placement, records map[string]nodeengine.NodeRecord) []string { seen := map[string]bool{} for n := range placements { seen[n] = true diff --git a/internal/compose/frombus/frombus_test.go b/internal/compose/frombus/frombus_test.go index 78f1a46..efd0e77 100644 --- a/internal/compose/frombus/frombus_test.go +++ b/internal/compose/frombus/frombus_test.go @@ -11,6 +11,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "github.com/novox/mesh-host/internal/compositions" "os" @@ -31,7 +32,7 @@ type bus struct { vaultAuthority string handed map[string]bool placements map[string]delivery.Placement - records map[string]delivery.NodeRecord + records map[string]nodeengine.NodeRecord modules map[string]delivery.Module manifests map[string][]byte values map[string]map[string]any // by node.module @@ -81,10 +82,12 @@ func (b *bus) Served(_ context.Context, node, module string, keys []string) (map func (b *bus) Placements(context.Context) (map[string]delivery.Placement, error) { return b.placements, nil } -func (b *bus) VaultAuthority(context.Context) string { return b.vaultAuthority } -func (b *bus) Handed(context.Context, string) map[string]bool { return b.handed } -func (b *bus) Records(context.Context) (map[string]delivery.NodeRecord, error) { return b.records, nil } -func (b *bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.modules, nil } +func (b *bus) VaultAuthority(context.Context) string { return b.vaultAuthority } +func (b *bus) Handed(context.Context, string) map[string]bool { return b.handed } +func (b *bus) Records(context.Context) (map[string]nodeengine.NodeRecord, error) { + return b.records, nil +} +func (b *bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.modules, nil } func (b *bus) Manifest(_ context.Context, digest string) ([]byte, error) { raw, ok := b.manifests[digest] if !ok { @@ -180,7 +183,7 @@ func TestInput(t *testing.T) { {Module: "media", HasMesh: true, MeshValues: map[string]any{"name": "mesh"}, HasLayer: true, Layer: map[string]any{"places": map[string]any{"data": "/srv/data"}}, Values: map[string]any{"lines": float64(1000)}}}}, }, - records: map[string]delivery.NodeRecord{ + records: map[string]nodeengine.NodeRecord{ "anchor": {Node: "anchor", TunnelKey: keys["anchor"], Capabilities: caps, Contract: 2}, "laptop": {Node: "laptop", TunnelKey: keys["laptop"], Capabilities: caps, Authority: "the mesh's certificate"}, }, @@ -331,7 +334,7 @@ func TestInput(t *testing.T) { p.Modules = append(p.Modules, delivery.PlacedModule{Module: m}) } return &bus{placements: map[string]delivery.Placement{"n1": p}, - records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: keys["anchor"], SealingKey: "sk", Capabilities: []string{"systemd"}}}, + records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: keys["anchor"], SealingKey: "sk", Capabilities: []string{"systemd"}}}, modules: map[string]delivery.Module{"keeper": {Manifest: delivery.ManifestDigest(keeper)}, "lemurs": {Manifest: delivery.ManifestDigest(lemurs)}, "objects": {Manifest: delivery.ManifestDigest(objects)}, "files": {Manifest: delivery.ManifestDigest(files)}}, manifests: map[string][]byte{delivery.ManifestDigest(keeper): keeper, delivery.ManifestDigest(lemurs): lemurs, @@ -490,11 +493,7 @@ func TestInput(t *testing.T) { if a.Verdict != c.verdict || !c.check(a) { t.Errorf("%s: %+v", name, a) } - 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 { + if raw, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Candidates: map[string]nodeengine.CandidateAnswer{"g": a}}); err != nil { t.Errorf("%s: the answer is not one a have holds: %v (%s)", name, err, raw) } } @@ -844,9 +843,9 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates { } records, _ := b.Records(ctx) for node, r := range records { - k, _ := delivery.HaveNodeKey(node) + k, _ := nodeengineclient.HaveNodeKey(node) rec := r - raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Record: &rec}) + raw, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &rec}) put(HaveRead, k, raw, err) } modules, _ := b.Modules(ctx) diff --git a/internal/compose/frombus/frombustest/frombustest.go b/internal/compose/frombus/frombustest/frombustest.go index 5b00ed6..432e05a 100644 --- a/internal/compose/frombus/frombustest/frombustest.go +++ b/internal/compose/frombus/frombustest/frombustest.go @@ -12,6 +12,7 @@ import ( "embed" "encoding/json" "errors" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "strings" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -25,7 +26,7 @@ var testdata embed.FS // Bus is a frombus.Reader over values held in memory. A credential asked is answered held when Held is set. type Bus struct { Placement map[string]delivery.Placement - Record map[string]delivery.NodeRecord + Record map[string]nodeengine.NodeRecord Module map[string]delivery.Module Manifests map[string][]byte Value map[string]map[string]any // by node.module @@ -41,9 +42,11 @@ var _ frombus.Reader = (*Bus)(nil) func (b *Bus) Placements(context.Context) (map[string]delivery.Placement, error) { return b.Placement, nil } -func (b *Bus) Records(context.Context) (map[string]delivery.NodeRecord, error) { return b.Record, nil } -func (b *Bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.Module, nil } -func (b *Bus) VaultAuthority(context.Context) string { return "" } +func (b *Bus) Records(context.Context) (map[string]nodeengine.NodeRecord, error) { + return b.Record, nil +} +func (b *Bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.Module, nil } +func (b *Bus) VaultAuthority(context.Context) string { return "" } func (b *Bus) Manifest(_ context.Context, digest string) ([]byte, error) { raw, ok := b.Manifests[digest] @@ -152,7 +155,7 @@ func Provisioned() (*Bus, error) { Placement: map[string]delivery.Placement{Node: {Node: Node, Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1", Account: "operator", AccountHome: "/home/operator"}, Modules: []delivery.PlacedModule{{Module: "store"}, {Module: "postgres"}, {Module: "mesh-provisioner-client"}, {Module: "restic"}}}}, - Record: map[string]delivery.NodeRecord{Node: {Node: Node, TunnelKey: "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=", + Record: map[string]nodeengine.NodeRecord{Node: {Node: Node, TunnelKey: "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=", SealingKey: "sk", Contract: compose.HandedContract, Capabilities: []string{"systemd", "package-manager"}}}, Module: map[string]delivery.Module{}, Manifests: map[string][]byte{}, Own: []compose.OwnSecretRow{{Node: Node, Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}, diff --git a/internal/compose/frombus/frombustest/frombustest_test.go b/internal/compose/frombus/frombustest/frombustest_test.go index 9598824..8769825 100644 --- a/internal/compose/frombus/frombustest/frombustest_test.go +++ b/internal/compose/frombus/frombustest/frombustest_test.go @@ -3,6 +3,8 @@ package frombustest import ( "context" "encoding/json" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" engcomp "github.com/novox/mesh-host/internal/compositions" "reflect" "slices" @@ -313,7 +315,7 @@ func statesOf(t *testing.T, b *Bus) frombus.States { put(frombus.PlacementRead, "placement."+node, raw, err) } for node, rec := range b.Record { - raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Record: &rec}) + raw, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &rec}) put(frombus.HaveRead, node+".node", raw, err) } for module, m := range b.Module { diff --git a/internal/compose/frombus/held_rows_test.go b/internal/compose/frombus/held_rows_test.go index 0aa2fe2..8a083b0 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" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" engcomp "github.com/novox/mesh-host/internal/compositions" "strings" "testing" @@ -18,7 +19,7 @@ import ( func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) { t.Helper() p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}} - b := &bus{records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd"}}}, + b := &bus{records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd"}}}, modules: map[string]delivery.Module{}, manifests: map[string][]byte{}, builds: map[string]*delivery.BuilderBuild{}} fps := map[string]string{} for i, raw := range manifests { diff --git a/internal/compose/frombus/states.go b/internal/compose/frombus/states.go index ca932f3..0c2a163 100644 --- a/internal/compose/frombus/states.go +++ b/internal/compose/frombus/states.go @@ -12,6 +12,8 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "strings" "time" @@ -84,11 +86,11 @@ func (r statesReader) Placements(context.Context) (map[string]delivery.Placement return keys.All(), err } -func (r statesReader) Records(context.Context) (map[string]delivery.NodeRecord, error) { - out := map[string]delivery.NodeRecord{} +func (r statesReader) Records(context.Context) (map[string]nodeengine.NodeRecord, error) { + out := map[string]nodeengine.NodeRecord{} err := r.each(HaveRead, func(k string, v []byte) { - if node, _, isNode, err := delivery.ParseHaveKey(k); err == nil && isNode { - if h, err := delivery.DecodeHaveNode(v); err == nil && h.Record != nil { + if node, _, isNode, err := nodeengineclient.ParseHaveKey(k); err == nil && isNode { + if h, err := nodeengineclient.DecodeHaveNode(v); err == nil && h.Record != nil { out[node] = *h.Record } } diff --git a/internal/judge/aliases.go b/internal/judge/aliases.go index bf0f30e..2ad8d2e 100644 --- a/internal/judge/aliases.go +++ b/internal/judge/aliases.go @@ -5,24 +5,24 @@ package judge import ( + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - "git.novox.be/novox/mesh-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 = engcomp.Composed - Judgement = delivery.Judgement + Judgement = string CandidateAnswer = nodeengine.CandidateAnswer ModuleDiffers = nodeengine.CandidateDiffers ResourceBodies = nodeengine.CandidateBodies ) const ( - JudgedRecreates = delivery.JudgedRecreates - JudgedMovesData = delivery.JudgedMovesData - JudgedNeither = delivery.JudgedNeither - JudgedCannot = delivery.JudgedCannot - JudgedUnchanged = delivery.JudgedUnchanged + JudgedRecreates = nodeengineclient.JudgedRecreates + JudgedMovesData = nodeengineclient.JudgedMovesData + JudgedNeither = "neither" + JudgedCannot = nodeengineclient.JudgedCannot + JudgedUnchanged = "unchanged" ) diff --git a/internal/link/bus.go b/internal/link/bus.go index 0670097..6fff2ab 100644 --- a/internal/link/bus.go +++ b/internal/link/bus.go @@ -4,10 +4,9 @@ import ( "context" "errors" "fmt" - "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" + "github.com/novox/mesh-host/internal/buckets" + "time" ) // Bus is the link open now as the session holds it. What it can do is asked of it by what it implements (HaveBus, @@ -87,9 +86,9 @@ func (b OverNATS) HaveBind(ctx context.Context) (HaveBucket, error) { } js = bounded } - kv, err := js.KeyValue(delivery.HaveBucket) + kv, err := js.KeyValue(buckets.Have) if err != nil { - return nil, fmt.Errorf("the have bucket %s: %w", delivery.HaveBucket, err) + return nil, fmt.Errorf("the have bucket %s: %w", buckets.Have, err) } return natsHaveBucket{kv}, nil } diff --git a/internal/link/enginedata.go b/internal/link/enginedata.go index 1880b4a..4a155d7 100644 --- a/internal/link/enginedata.go +++ b/internal/link/enginedata.go @@ -4,17 +4,16 @@ import ( "context" "errors" "fmt" - - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" + "github.com/novox/mesh-host/internal/buckets" ) // ---- the data items of the modules placed on this node, in the bus's own storage (novox/hq ADR 0577 clause 1) ------ -// DataBucket is the node-engine's declared state data (delivery.EngineDataBucket), created by the grants part from the +// DataBucket is the node-engine's declared state data (buckets.Data), created by the grants part from the // engine's manifest as its states acts and health are: one key `.` per module placed on the node that // declares data items, written by that node's engine alone, read by that node's disk-load. -const DataBucket = delivery.EngineDataBucket +const DataBucket = buckets.Data // dataStore binds DataBucket over the link open now: bound, never created. func (l *natsLink) dataStore() (nats.KeyValue, error) { diff --git a/internal/link/have.go b/internal/link/have.go index 656de22..0047e6f 100644 --- a/internal/link/have.go +++ b/internal/link/have.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" 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" @@ -12,8 +13,6 @@ import ( "strings" "sync" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // ErrHaveAbsent is what HaveGet returns for a key the bucket does not hold. @@ -40,14 +39,14 @@ type HaveBucket interface { type haveState struct { mu sync.Mutex modules map[string]ModuleFact - health map[string]delivery.Health + health map[string]string // words is, by module, what the last look found of the resource that made its health failed or waits: the have says // it, so a failed health is never without words (issue 1496: "the node gave no words" hid a directory used as found). words map[string]string // looked says a look's health was taken: a running module it judges no resource of then has nothing that could be // down, and is ok (issue 1945). looked bool - node *delivery.HaveNode + node *nodeengine.HaveByNodeNodeValue // nodeComposed is the digest of the node's own composition it runs (ADR 0539 §9), kept beside the node's facts. nodeComposed string // nodeSealed is, on a node composing its own, each value its own composition carries sealed to its key (ADR 0547 §3, @@ -65,7 +64,7 @@ type haveState struct { removed map[string]string // userList is what the bus node's engine last wrote of the bus's user list, by the module whose have says it (novox/hq // ADR 0547 §8); kept apart from the module's fact, which every apply replaces. - userList map[string]delivery.UserListWritten + userList map[string]nodeengine.UserListWritten // userListNoKey is each module whose user list was said to have no have key to ride on, so it is said once. userListNoKey map[string]bool // unjudged is each module whose composition moved (an apply began, or the fact names another build) and that no look @@ -76,8 +75,8 @@ type haveState struct { // inMotion says a module's phase is one of a change still under way: what a look finds of it then judges nothing of the // composition being applied (a container stopped to be replaced reads unhealthy), so its health is not yet judged. func inMotion(phase string) bool { - switch delivery.Phase(phase) { - case delivery.PhaseBackingUp, delivery.PhaseApplying, delivery.PhaseChecking: + switch string(phase) { + case nodeengineclient.PhaseBackingUp, nodeengineclient.PhaseApplying, nodeengineclient.PhaseChecking: return true } return false @@ -85,15 +84,15 @@ func inMotion(phase string) bool { // healthOf is the health a module's have carries: unknown (not yet judged) while its change is under way or until a look // after it, the last look's otherwise. The caller holds the lock. -func (s *haveState) healthOf(module string, f ModuleFact) delivery.Health { +func (s *haveState) healthOf(module string, f ModuleFact) string { if inMotion(f.Phase) { - return delivery.HealthUnknown + return nodeengineclient.HealthUnknown } if got, known := s.health[module]; known { return got } if s.unjudged[module] { - return delivery.HealthUnknown + return nodeengineclient.HealthUnknown } return nothingJudged(f, s.looked) } @@ -141,8 +140,8 @@ func (s *haveState) takeLook(h *Health) { // wordsOf is what a have says of its health when the apply said nothing: the words of the resource that made it failed // or waits, while it is so. The caller holds the lock. -func (s *haveState) wordsOf(module string, health delivery.Health) string { - if health != delivery.HealthFailed && health != delivery.HealthWaits { +func (s *haveState) wordsOf(module string, health string) string { + if health != nodeengineclient.HealthFailed && health != nodeengineclient.HealthWaits { return "" } return s.words[module] @@ -154,8 +153,8 @@ func (s *haveState) wordsOf(module string, health delivery.Health) string { // module's health: it says nothing of how the module runs, waits for the operator's hand-over and keeps its own // condition, raised from the same statement (issue 1496: on ace mesh-build-node's cache directory, there before the // mesh, made its have failed while its check answered healthy, and the assigner held its version on every node). -func moduleHealths(h *Health) (map[string]delivery.Health, map[string]string) { - out, words := map[string]delivery.Health{}, map[string]string{} +func moduleHealths(h *Health) (map[string]string, map[string]string) { + out, words := map[string]string{}, map[string]string{} if h == nil { return out, words } @@ -183,7 +182,7 @@ func moduleHealths(h *Health) (map[string]delivery.Health, map[string]string) { } } for module, r := range rank { - out[module] = [...]delivery.Health{delivery.HealthOK, delivery.HealthUnknown, delivery.HealthWaits, delivery.HealthFailed}[r] + out[module] = [...]string{nodeengineclient.HealthOK, nodeengineclient.HealthUnknown, nodeengineclient.HealthWaits, nodeengineclient.HealthFailed}[r] if r < 2 { delete(words, module) } @@ -206,21 +205,28 @@ func resourceWords(r ResourceHealth) string { } // nodeFacts is the node's own facts of a report, each as the JSON the report carries it in. -func nodeFacts(r Report) delivery.HaveNode { - n := delivery.HaveNode{V: sdkkeys.Version} - carry := func(into *json.RawMessage, empty bool, v any) { +func nodeFacts(r Report) nodeengine.HaveByNodeNodeValue { + n := nodeengine.HaveByNodeNodeValue{V: sdkkeys.Version} + carry := func(into *[]map[string]any, empty bool, v any) { if empty { return } - if raw, err := json.Marshal(v); err == nil { - *into = raw + raw, err := json.Marshal(v) + if err != nil { + return + } + var list []map[string]any + if json.Unmarshal(raw, &list) == nil { + *into = list } } carry(&n.Windows, len(r.Windows) == 0, r.Windows) carry(&n.Busy, len(r.Busy) == 0, r.Busy) carry(&n.Strays, len(r.Strays) == 0, r.Strays) carry(&n.Filters, len(r.Filters) == 0, r.Filters) - carry(&n.Profile, len(r.Profile) == 0, r.Profile) + if len(r.Profile) > 0 { + n.Profile = r.Profile + } return n } @@ -296,7 +302,7 @@ func (q *Session) noteApply(r Report) bool { // nodeFactsWithRecord is the node's own facts of a report with its record (ADR 0555 §3): a report that says nothing of // the machine leaves the record last made standing, and one without a health statement keeps the contract last stated. -func (q *Session) nodeFactsWithRecord(r Report, before *delivery.HaveNode) *delivery.HaveNode { +func (q *Session) nodeFactsWithRecord(r Report, before *nodeengine.HaveByNodeNodeValue) *nodeengine.HaveByNodeNodeValue { n := nodeFacts(r) if q.Keys != nil { keys := q.Keys() @@ -377,20 +383,20 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) { sort.Strings(modules) for _, module := range modules { f := s.modules[module] - h := delivery.Have{V: sdkkeys.Version, FP: f.FP, Composed: f.Composed, Build: f.Build, - Phase: delivery.PhaseRunning, Health: delivery.HealthUnknown, Detail: f.Detail} + h := nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Fp: f.FP, Composed: f.Composed, Build: f.Build, + Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthUnknown, Detail: f.Detail} h.Kept = f.Kept - if delivery.Phase(f.Phase) == delivery.PhaseWaitingYes { - h.Judgement, h.Waits, h.BuildAlone = delivery.Judgement(f.Judgement), f.Waits, f.BuildAlone + if string(f.Phase) == nodeengineclient.PhaseWaitingYes { + h.Judgement, h.Waits, h.BuildAlone = string(f.Judgement), f.Waits, f.BuildAlone } switch { case f.Phase != "": - h.Phase = delivery.Phase(f.Phase) + h.Phase = string(f.Phase) case f.Failed: - h.Phase = delivery.PhaseFailed + h.Phase = nodeengineclient.PhaseFailed } h.SealedAt = f.Sealed - if h.Phase == delivery.PhaseFailed { + if h.Phase == nodeengineclient.PhaseFailed { h.PullWait = f.PullWait } h.Health = s.healthOf(module, f) @@ -400,19 +406,19 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) { if w, ok := s.userList[module]; ok { h.UserList = &w } - key, err := delivery.HaveKey(node, module) + key, err := nodeengineclient.HaveKey(node, module) if err != nil { q.sayHave(fmt.Sprintf("module %q has no key in the have bucket: %v", module, err)) continue } - raw, err := delivery.EncodeHave(h) + raw, err := nodeengineclient.EncodeHave(h) if err != nil { q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err)) continue } if !q.putHave(write, bucket, key, raw, func(current []byte) bool { - c, err := delivery.DecodeHave(current) - return err == nil && delivery.SameHave(c, h) + c, err := nodeengineclient.DecodeHave(current) + return err == nil && nodeengineclient.SameHave(c, h) }) { return } @@ -428,19 +434,19 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) { return } if s.node != nil { - key, err := delivery.HaveNodeKey(node) + key, err := nodeengineclient.HaveNodeKey(node) if err == nil { var raw []byte facts := *s.node facts.Composed = s.nodeComposed facts.SealedAt = s.nodeSealed - facts.Candidates = interimAnswers(s.nodeCandidates) + facts.Candidates = s.nodeCandidates facts.Composes, facts.ComposesWhy = s.nodeComposes, s.nodeComposesWhy - if raw, err = delivery.EncodeHaveNode(facts); err == nil { + if raw, err = nodeengineclient.EncodeHaveNode(facts); err == nil { want := facts if !q.putHave(write, bucket, key, raw, func(current []byte) bool { - c, err := delivery.DecodeHaveNode(current) - return err == nil && delivery.SameHaveNode(c, want) + c, err := nodeengineclient.DecodeHaveNode(current) + return err == nil && nodeengineclient.SameHaveNode(c, want) }) { return } @@ -503,15 +509,15 @@ func (q *Session) writeRemoved(ctx context.Context, b HaveBucket, node string) b } sort.Strings(gone) for _, module := range gone { - key, err := delivery.HaveKey(node, module) + key, err := nodeengineclient.HaveKey(node, module) if err != nil { q.sayHave(fmt.Sprintf("module %q has no key in the have bucket: %v", module, err)) delete(s.removed, module) continue } - h := delivery.Have{V: sdkkeys.Version, Build: s.removed[module], Phase: delivery.PhaseRemoved, - Health: delivery.HealthUnknown, Detail: "the applied declaration no longer holds the module"} - raw, err := delivery.EncodeHave(h) + h := nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: s.removed[module], Phase: nodeengineclient.PhaseRemoved, + Health: nodeengineclient.HealthUnknown, Detail: "the applied declaration no longer holds the module"} + raw, err := nodeengineclient.EncodeHave(h) if err != nil { q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err)) delete(s.removed, module) @@ -531,7 +537,7 @@ func (q *Session) writeRemoved(ctx context.Context, b HaveBucket, node string) b q.haveFailed(err) return false } - if c, err := delivery.DecodeHave(current); err == nil && c.Phase == delivery.PhaseRemoved { + if c, err := nodeengineclient.DecodeHave(current); err == nil && c.Phase == nodeengineclient.PhaseRemoved { delete(s.removed, module) continue } @@ -565,7 +571,7 @@ func (q *Session) writeUserListsAlone(ctx context.Context, b HaveBucket, node st } sort.Strings(alone) for _, module := range alone { - key, err := delivery.HaveKey(node, module) + key, err := nodeengineclient.HaveKey(node, module) if err != nil { q.sayHave(fmt.Sprintf("module %q has no key in the have bucket: %v", module, err)) continue @@ -592,7 +598,7 @@ func (q *Session) writeUserListsAlone(ctx context.Context, b HaveBucket, node st return false } delete(s.userListNoKey, module) - h, err := delivery.DecodeHave(current) + h, err := nodeengineclient.DecodeHave(current) if err != nil { q.sayHave(fmt.Sprintf("the bus's user list is not written in %s: its value cannot be read: %v", key, err)) continue @@ -600,10 +606,10 @@ func (q *Session) writeUserListsAlone(ctx context.Context, b HaveBucket, node st w := s.userList[module] next := h next.UserList = &w - if delivery.SameHave(h, next) { + if nodeengineclient.SameHave(h, next) { continue } - raw, err := delivery.EncodeHave(next) + raw, err := nodeengineclient.EncodeHave(next) if err != nil { q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err)) continue @@ -685,12 +691,12 @@ func (q *Session) NoteModule(ctx context.Context, f ModuleFact) { // NoteUserList takes what the bus node's engine wrote of the bus's user list, and writes it in the bus module's have // where it differs from the bucket's (novox/hq ADR 0547 §8). A module this engine has said no have of yet carries it // from its first have on. -func (q *Session) NoteUserList(ctx context.Context, module string, w delivery.UserListWritten) { +func (q *Session) NoteUserList(ctx context.Context, module string, w nodeengine.UserListWritten) { q.init() s := &q.haves s.mu.Lock() if s.userList == nil { - s.userList = map[string]delivery.UserListWritten{} + s.userList = map[string]nodeengine.UserListWritten{} } s.userList[module] = w s.mu.Unlock() @@ -732,7 +738,7 @@ func (q *Session) NoteNode(ctx context.Context, composed string, sealed composit s.nodeComposed = composed s.nodeSealed = sealed if s.node == nil { - n := delivery.HaveNode{V: sdkkeys.Version} + n := nodeengine.HaveByNodeNodeValue{V: sdkkeys.Version} s.node = &n } s.mu.Unlock() @@ -765,7 +771,7 @@ func (q *Session) NoteCandidates(ctx context.Context, answers map[string]nodeeng q.writeHaves(ctx, bus) } -// NoteComposes says, in the node's own key, whose compositions the node applies (delivery.ComposesOwn) and why: written +// NoteComposes says, in the node's own key, whose compositions the node applies (nodeengineclient.ComposesOwn) and why: written // at each start of its agent, where every have is read (issue 1126). func (q *Session) NoteComposes(ctx context.Context, composes, why string) { q.init() @@ -786,49 +792,49 @@ func (q *Session) NoteComposes(ctx context.Context, composes, why string) { // ReadNodeHave is the node's own key as the bucket holds it, and whether it holds one: what the node's own composition // it last ran is and carries sealed, read after a restart (ADR 0547 §3). -func (q *Session) ReadNodeHave(ctx context.Context) (delivery.HaveNode, bool) { +func (q *Session) ReadNodeHave(ctx context.Context) (nodeengine.HaveByNodeNodeValue, bool) { n, ok, _ := q.ReadNodeHaveErr(ctx) return n, ok } // ReadNodeHaveErr is ReadNodeHave telling the key absent (false, nil) from one that could not be read (false, the // error): a bus that did not answer is never kept as a node that ran nothing (issue 1126, 2026-10-28). -func (q *Session) ReadNodeHaveErr(ctx context.Context) (delivery.HaveNode, bool, error) { +func (q *Session) ReadNodeHaveErr(ctx context.Context) (nodeengine.HaveByNodeNodeValue, bool, error) { q.init() q.mu.Lock() bus := q.bus q.mu.Unlock() hb, ok := bus.(HaveBus) if !ok || bus == nil { - return delivery.HaveNode{}, false, ErrNotLinkedHaves + return nodeengine.HaveByNodeNodeValue{}, false, ErrNotLinkedHaves } - key, err := delivery.HaveNodeKey(q.Membership.Node) + key, err := nodeengineclient.HaveNodeKey(q.Membership.Node) if err != nil { - return delivery.HaveNode{}, false, err + return nodeengine.HaveByNodeNodeValue{}, false, err } read, cancel := context.WithTimeout(ctx, q.timeout()) defer cancel() bucket, err := hb.HaveBind(read) if err != nil { - return delivery.HaveNode{}, false, err + return nodeengine.HaveByNodeNodeValue{}, false, err } value, _, err := bucket.Get(read, key) if errors.Is(err, ErrHaveAbsent) { - return delivery.HaveNode{}, false, nil + return nodeengine.HaveByNodeNodeValue{}, false, nil } if err != nil { - return delivery.HaveNode{}, false, err + return nodeengine.HaveByNodeNodeValue{}, false, err } - n, err := delivery.DecodeHaveNode(value) + n, err := nodeengineclient.DecodeHaveNode(value) if err != nil { - return delivery.HaveNode{}, false, nil // a key no engine can read names nothing it runs + return nodeengine.HaveByNodeNodeValue{}, false, nil // a key no engine can read names nothing it runs } return n, true, nil } // ReadHave is the have this node-engine last wrote for a module, and whether the bucket holds one. A bucket that cannot // be read answers as none: the agent then compares with the declaration it kept, and applies only what differs. -func (q *Session) ReadHave(ctx context.Context, module string) (delivery.Have, bool) { +func (q *Session) ReadHave(ctx context.Context, module string) (nodeengine.HaveByNodeModuleValue, bool) { h, ok, _ := q.ReadHaveErr(ctx, module) return h, ok } @@ -838,62 +844,44 @@ var ErrNotLinkedHaves = errors.New("no link to the bus that binds the have bucke // ReadHaveErr is ReadHave telling a key absent (false, nil) from one that could not be read (false, the error): a bus // that does not answer for a moment is not a module that never ran (issue 1126). -func (q *Session) ReadHaveErr(ctx context.Context, module string) (delivery.Have, bool, error) { +func (q *Session) ReadHaveErr(ctx context.Context, module string) (nodeengine.HaveByNodeModuleValue, bool, error) { q.init() q.mu.Lock() bus := q.bus q.mu.Unlock() hb, ok := bus.(HaveBus) if !ok || bus == nil { - return delivery.Have{}, false, ErrNotLinkedHaves + return nodeengine.HaveByNodeModuleValue{}, false, ErrNotLinkedHaves } - key, err := delivery.HaveKey(q.Membership.Node, module) + key, err := nodeengineclient.HaveKey(q.Membership.Node, module) if err != nil { - return delivery.Have{}, false, err + return nodeengine.HaveByNodeModuleValue{}, false, err } read, cancel := context.WithTimeout(ctx, q.timeout()) defer cancel() bucket, err := hb.HaveBind(read) if err != nil { - return delivery.Have{}, false, err + return nodeengine.HaveByNodeModuleValue{}, false, err } value, _, err := bucket.Get(read, key) if errors.Is(err, ErrHaveAbsent) { - return delivery.Have{}, false, nil + return nodeengine.HaveByNodeModuleValue{}, false, nil } if err != nil { - return delivery.Have{}, false, err + return nodeengine.HaveByNodeModuleValue{}, false, err } - h, err := delivery.DecodeHave(value) + h, err := nodeengineclient.DecodeHave(value) if err != nil { - return delivery.Have{}, false, nil // a have no engine can read names nothing it runs + return nodeengine.HaveByNodeModuleValue{}, false, nil // a have no engine can read names nothing it runs } return h, true, nil } // nothingJudged is the health of a module a look judged no resource of: ok when it was looked at and the module applied // and runs (nothing of it could be down), unknown otherwise. -func nothingJudged(f ModuleFact, looked bool) delivery.Health { - if looked && !f.Failed && (f.Phase == "" || f.Phase == string(delivery.PhaseRunning)) { - return delivery.HealthOK +func nothingJudged(f ModuleFact, looked bool) string { + if looked && !f.Failed && (f.Phase == "" || f.Phase == string(nodeengineclient.PhaseRunning)) { + return nodeengineclient.HealthOK } - 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 + return nodeengineclient.HealthUnknown } diff --git a/internal/link/have_nats_integration_test.go b/internal/link/have_nats_integration_test.go index eaa862c..eb862c8 100644 --- a/internal/link/have_nats_integration_test.go +++ b/internal/link/have_nats_integration_test.go @@ -4,12 +4,12 @@ package link import ( "context" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "github.com/nats-io/nats.go" + "github.com/novox/mesh-host/internal/buckets" "sync/atomic" "testing" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" - "github.com/nats-io/nats.go" ) // 1132/AC8: against a real bus with the bucket node-engine_have, a node-engine that applies a declaration of two @@ -21,9 +21,9 @@ import ( func TestNatsHavesAreWrittenOnceAndOnlyOnChange(t *testing.T) { conn, js := aBus(t) const node = "haveok" - kv, err := js.KeyValue(delivery.HaveBucket) + kv, err := js.KeyValue(buckets.Have) if err != nil { - if kv, err = js.CreateKeyValue(&nats.KeyValueConfig{Bucket: delivery.HaveBucket, History: delivery.HaveHistory}); err != nil { + if kv, err = js.CreateKeyValue(&nats.KeyValueConfig{Bucket: buckets.Have, History: buckets.HaveHistory}); err != nil { t.Fatal(err) } } @@ -66,17 +66,17 @@ func TestNatsHavesAreWrittenOnceAndOnlyOnChange(t *testing.T) { first := revisions() a, _ := kv.Get(node + ".a") - haveA, err := delivery.DecodeHave(a.Value()) - if err != nil || haveA.Phase != delivery.PhaseRunning || haveA.Build != "b-1" || haveA.Health != delivery.HealthOK { + haveA, err := nodeengineclient.DecodeHave(a.Value()) + if err != nil || haveA.Phase != nodeengineclient.PhaseRunning || haveA.Build != "b-1" || haveA.Health != nodeengineclient.HealthOK { t.Fatalf("a holds %+v (%v)", haveA, err) } b, _ := kv.Get(node + ".b") - haveB, err := delivery.DecodeHave(b.Value()) - if err != nil || haveB.Phase != delivery.PhaseFailed || haveB.Detail == "" { + haveB, err := nodeengineclient.DecodeHave(b.Value()) + if err != nil || haveB.Phase != nodeengineclient.PhaseFailed || haveB.Detail == "" { t.Fatalf("b holds %+v (%v)", haveB, err) } n, _ := kv.Get(node + ".node") - if hn, err := delivery.DecodeHaveNode(n.Value()); err != nil || len(hn.Windows) == 0 { + if hn, err := nodeengineclient.DecodeHaveNode(n.Value()); err != nil || len(hn.Windows) == 0 { t.Fatalf("the node key holds %+v (%v)", hn, err) } diff --git a/internal/link/have_test.go b/internal/link/have_test.go index af02cab..79a8b94 100644 --- a/internal/link/have_test.go +++ b/internal/link/have_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" 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" @@ -15,7 +16,6 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" ) @@ -104,11 +104,11 @@ func (l *haveLink) Set(_ context.Context, key string, value []byte, revision uin return nil } -func (l *haveLink) have(t *testing.T, key string) delivery.Have { +func (l *haveLink) have(t *testing.T, key string) nodeengine.HaveByNodeModuleValue { t.Helper() l.hmu.Lock() defer l.hmu.Unlock() - h, err := delivery.DecodeHave(l.kv[key].value) + h, err := nodeengineclient.DecodeHave(l.kv[key].value) if err != nil { t.Fatalf("%s holds no have: %v", key, err) } @@ -121,9 +121,9 @@ func (l *haveLink) setCount() int { return len(l.sets) } -func (l *haveLink) put(t *testing.T, key string, h delivery.Have) { +func (l *haveLink) put(t *testing.T, key string, h nodeengine.HaveByNodeModuleValue) { t.Helper() - raw, err := delivery.EncodeHave(h) + raw, err := nodeengineclient.EncodeHave(h) if err != nil { t.Fatal(err) } @@ -165,11 +165,11 @@ func TestAnAppliedModuleIsWrittenRunningAndAFailedOneFailed(t *testing.T) { q, _ := aHaveQueue(l) q.SayFacts(context.Background(), appliedModules()) a, b := l.have(t, "n1.a"), l.have(t, "n1.b") - if a.Phase != delivery.PhaseRunning || a.Build != "b-1" || a.Health != delivery.HealthOK { + if a.Phase != nodeengineclient.PhaseRunning || a.Build != "b-1" || a.Health != nodeengineclient.HealthOK { t.Errorf("a: %+v", a) } - if b.Phase != delivery.PhaseFailed || b.Build != "b-2" || !strings.Contains(b.Detail, "no space") || - b.Health != delivery.HealthFailed { + if b.Phase != nodeengineclient.PhaseFailed || b.Build != "b-2" || !strings.Contains(b.Detail, "no space") || + b.Health != nodeengineclient.HealthFailed { t.Errorf("b: %+v", b) } if l.kv["n1.a"].rev != 1 { @@ -180,14 +180,14 @@ func TestAnAppliedModuleIsWrittenRunningAndAFailedOneFailed(t *testing.T) { // 1132/AC5: a have equal to the bucket's value is not written; a different one is written over the last revision. func TestAHaveIsWrittenOnlyWhenItDiffers(t *testing.T) { l := newHaveLink() - l.put(t, "n1.a", delivery.Have{V: sdkkeys.Version, Build: "b-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) - l.put(t, "n1.b", delivery.Have{V: sdkkeys.Version, Build: "b-0", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) + l.put(t, "n1.a", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: "b-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) + l.put(t, "n1.b", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: "b-0", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) q, _ := aHaveQueue(l) q.SayFacts(context.Background(), appliedModules()) if l.kv["n1.a"].rev != 1 { t.Errorf("a equal to the bucket was written again, to revision %d", l.kv["n1.a"].rev) } - if l.kv["n1.b"].rev != 2 || l.have(t, "n1.b").Phase != delivery.PhaseFailed { + if l.kv["n1.b"].rev != 2 || l.have(t, "n1.b").Phase != nodeengineclient.PhaseFailed { t.Errorf("b was not updated over its last revision: %+v at %d", l.have(t, "n1.b"), l.kv["n1.b"].rev) } before := l.setCount() @@ -205,7 +205,7 @@ func TestAModulesHealthFollowsTheLook(t *testing.T) { {Module: "c", Build: "b"}, {Module: "d", Build: "b"}, {Module: "e", Build: "b"}}} q.SayFacts(context.Background(), rep) for _, m := range []string{"a", "b", "c", "d", "e"} { - if h := l.have(t, "n1."+m).Health; h != delivery.HealthUnknown { + if h := l.have(t, "n1."+m).Health; h != nodeengineclient.HealthUnknown { t.Errorf("%s with no look is %s, want unknown", m, h) } } @@ -220,8 +220,8 @@ func TestAModulesHealthFollowsTheLook(t *testing.T) { if !q.SayHealth(context.Background(), *h) { t.Fatal("not said") } - for m, want := range map[string]delivery.Health{"a": delivery.HealthWaits, "b": delivery.HealthFailed, - "c": delivery.HealthWaits, "d": delivery.HealthUnknown, "e": delivery.HealthOK} { + for m, want := range map[string]string{"a": nodeengineclient.HealthWaits, "b": nodeengineclient.HealthFailed, + "c": nodeengineclient.HealthWaits, "d": nodeengineclient.HealthUnknown, "e": nodeengineclient.HealthOK} { if got := l.have(t, "n1."+m).Health; got != want { t.Errorf("%s is %s, want %s", m, got, want) } @@ -239,7 +239,7 @@ func TestAModulesHealthFollowsTheLook(t *testing.T) { } h2.Resources = []ResourceHealth{{Module: "a", Resource: "a.server", State: StateHealthy}} q.SayHealth(context.Background(), h2) - if got := l.have(t, "n1.a"); got.Health != delivery.HealthOK || got.Detail != "" { + if got := l.have(t, "n1.a"); got.Health != nodeengineclient.HealthOK || got.Detail != "" { t.Errorf("a after a healthy look is %s, saying %q", got.Health, got.Detail) } } @@ -255,14 +255,19 @@ func TestTheNodeKeyCarriesTheNodesFacts(t *testing.T) { rep.Filters = []Filter{{Where: "inet mesh", Owner: "mesh"}} rep.Profile = map[string]any{"cores": 8} q.SayFacts(context.Background(), rep) - got, err := delivery.DecodeHaveNode(l.kv["n1.node"].value) + got, err := nodeengineclient.DecodeHaveNode(l.kv["n1.node"].value) if err != nil { t.Fatal(err) } - enc := func(v any) json.RawMessage { b, _ := json.Marshal(v); return b } - want := delivery.HaveNode{V: sdkkeys.Version, Windows: enc(rep.Windows), Busy: enc(rep.Busy), Strays: enc(rep.Strays), - Filters: enc(rep.Filters), Profile: enc(rep.Profile)} - if !delivery.SameHaveNode(got, want) { + enc := func(v any) []map[string]any { + var out []map[string]any + b, _ := json.Marshal(v) + _ = json.Unmarshal(b, &out) + return out + } + want := nodeengine.HaveByNodeNodeValue{V: sdkkeys.Version, Windows: enc(rep.Windows), Busy: enc(rep.Busy), Strays: enc(rep.Strays), + Filters: enc(rep.Filters), Profile: rep.Profile} + if !nodeengineclient.SameHaveNode(got, want) { t.Errorf("the node key holds %+v, want %+v", got, want) } @@ -279,14 +284,14 @@ func TestTheNodeKeyCarriesTheNodesFacts(t *testing.T) { rep.Outward = []string{"eth0"} rep.Health = &Health{Contract: NodeKeyContract} q.SayFacts(context.Background(), rep) - if got, err = delivery.DecodeHaveNode(l.kv["n1.node"].value); err != nil || got.Record == nil { + if got, err = nodeengineclient.DecodeHaveNode(l.kv["n1.node"].value); err != nil || got.Record == nil { t.Fatalf("the node key holds no record: %+v, %v", got, err) } - wantRecord := delivery.NodeRecord{V: sdkkeys.Version, Node: "n1", TunnelKey: "tk", SealingKey: "sk", ServingKey: "pk", + wantRecord := nodeengine.NodeRecord{V: sdkkeys.Version, Node: "n1", TunnelKey: "tk", SealingKey: "sk", ServingKey: "pk", Certificate: "cert", Trusts: keys.Trusts, Capabilities: []string{"apt", "bus-state-calls", "ledger-calls", "systemd"}, OutwardLinks: []string{"eth0"}, NetworksSaid: true, - Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}}, Contract: NodeKeyContract, + Networks: []nodeengine.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}}, Contract: NodeKeyContract, ManifestReads: catalogue.ManifestReads()} // what it reads of a manifest: its contract's (ADR 0614 §4) - if !delivery.SameNodeRecord(*got.Record, wantRecord) { + if !nodeengineclient.SameNodeRecord(*got.Record, wantRecord) { t.Errorf("record = %+v, want %+v", *got.Record, wantRecord) } // Among them, that it fills a seat's reach from the seat holder's listen (ADR 0632 §1). @@ -307,7 +312,7 @@ func TestTheNodeKeyCarriesTheNodesFacts(t *testing.T) { } l.statedErr, l.stated = nil, map[string]string{} q.SayFacts(context.Background(), rep) - if got, _ = delivery.DecodeHaveNode(l.kv["n1.node"].value); l.kv["n1.node"].rev != at+1 || !slices.Equal(got.Record.Capabilities, []string{"apt", "systemd"}) { + if got, _ = nodeengineclient.DecodeHaveNode(l.kv["n1.node"].value); l.kv["n1.node"].rev != at+1 || !slices.Equal(got.Record.Capabilities, []string{"apt", "systemd"}) { t.Errorf("a runner stating no calls: revision %d (was %d), capabilities %v", l.kv["n1.node"].rev, at, got.Record.Capabilities) } at++ @@ -316,13 +321,13 @@ func TestTheNodeKeyCarriesTheNodesFacts(t *testing.T) { at++ keys.Certificate = "cert-2" q.SayFacts(context.Background(), rep) - if got, _ = delivery.DecodeHaveNode(l.kv["n1.node"].value); l.kv["n1.node"].rev != at+1 || got.Record.Certificate != "cert-2" { + if got, _ = nodeengineclient.DecodeHaveNode(l.kv["n1.node"].value); l.kv["n1.node"].rev != at+1 || got.Record.Certificate != "cert-2" { t.Errorf("a new certificate: revision %d (was %d), record %+v", l.kv["n1.node"].rev, at, got.Record) } bare := rep bare.Health = nil q.SayFacts(context.Background(), bare) - if got, _ = delivery.DecodeHaveNode(l.kv["n1.node"].value); got.Record.Contract != NodeKeyContract { + if got, _ = nodeengineclient.DecodeHaveNode(l.kv["n1.node"].value); got.Record.Contract != NodeKeyContract { t.Errorf("a report without a health statement dropped the contract: %+v", got.Record) } } @@ -342,7 +347,7 @@ func TestAFailedHaveWriteIsSaidAndWrittenAgain(t *testing.T) { l.failing = false l.hmu.Unlock() q.SayFacts(context.Background(), appliedModules()) - if l.have(t, "n1.a").Phase != delivery.PhaseRunning { + if l.have(t, "n1.a").Phase != nodeengineclient.PhaseRunning { t.Error("the next apply did not write again") } } @@ -407,7 +412,7 @@ func TestAFailedBindOrWriteEndsThePassAndReturnsInTime(t *testing.T) { if l.setCount() == 0 { t.Fatal("a bind that met no responders was not written again") } - if l.have(t, "n1.a").Phase != delivery.PhaseRunning { + if l.have(t, "n1.a").Phase != nodeengineclient.PhaseRunning { t.Error("the have written again does not say running") } } @@ -426,13 +431,13 @@ func TestAModuleLeavingTheDeclarationIsWrittenRemoved(t *testing.T) { revA, revB := l.kv["n1.a"].rev, l.kv["n1.b"].rev q.SayFacts(context.Background(), onlyA()) b := l.have(t, "n1.b") - if b.Phase != delivery.PhaseRemoved || b.Build != "b-2" { + if b.Phase != nodeengineclient.PhaseRemoved || b.Build != "b-2" { t.Errorf("b: %+v", b) } if l.kv["n1.b"].rev != revB+1 { t.Errorf("b was written at revision %d, want one over %d", l.kv["n1.b"].rev, revB) } - if l.kv["n1.a"].rev != revA || l.have(t, "n1.a").Phase != delivery.PhaseRunning { + if l.kv["n1.a"].rev != revA || l.have(t, "n1.a").Phase != nodeengineclient.PhaseRunning { t.Errorf("a changed: %+v at %d", l.have(t, "n1.a"), l.kv["n1.a"].rev) } before := l.setCount() @@ -449,11 +454,11 @@ func TestARemovedModuleAddedBackIsWrittenWithItsNewPhase(t *testing.T) { q.SayFacts(context.Background(), appliedModules()) q.SayFacts(context.Background(), onlyA()) q.SayFacts(context.Background(), appliedModules()) - if b := l.have(t, "n1.b"); b.Phase != delivery.PhaseFailed || b.Build != "b-2" { + if b := l.have(t, "n1.b"); b.Phase != nodeengineclient.PhaseFailed || b.Build != "b-2" { t.Errorf("b after it came back: %+v", b) } q.SayFacts(context.Background(), onlyA()) - if b := l.have(t, "n1.b"); b.Phase != delivery.PhaseRemoved { + if b := l.have(t, "n1.b"); b.Phase != nodeengineclient.PhaseRemoved { t.Errorf("b removed again: %+v", b) } } @@ -462,11 +467,11 @@ func TestARemovedModuleAddedBackIsWrittenWithItsNewPhase(t *testing.T) { // bucket is not created again. func TestAModuleNeverWrittenIsNotWrittenRemoved(t *testing.T) { l := newHaveLink() - l.put(t, "n1.c", delivery.Have{V: sdkkeys.Version, Build: "b-9", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) + l.put(t, "n1.c", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: "b-9", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) q, _ := aHaveQueue(l) q.SayFacts(context.Background(), appliedModules()) q.SayFacts(context.Background(), onlyA()) - if c := l.have(t, "n1.c"); c.Phase != delivery.PhaseRunning || l.kv["n1.c"].rev != 1 { + if c := l.have(t, "n1.c"); c.Phase != nodeengineclient.PhaseRunning || l.kv["n1.c"].rev != 1 { t.Errorf("c, which this node-engine never wrote, changed: %+v at %d", c, l.kv["n1.c"].rev) } // a key that vanished from the bucket is not created by the removal @@ -495,7 +500,7 @@ func TestARefusedRemovalIsWrittenByTheNextPass(t *testing.T) { l.failing = false l.hmu.Unlock() q.SayFacts(context.Background(), onlyA()) - if b := l.have(t, "n1.b"); b.Phase != delivery.PhaseRemoved { + if b := l.have(t, "n1.b"); b.Phase != nodeengineclient.PhaseRemoved { t.Errorf("b: %+v", b) } } @@ -504,13 +509,13 @@ func TestARefusedRemovalIsWrittenByTheNextPass(t *testing.T) { // before: one of them the new declaration lacks is written removed, one that is not in the bucket is not created. func TestARestartedNodeEngineWritesTheRemovalFromTheReportsBefore(t *testing.T) { l := newHaveLink() - l.put(t, "n1.a", delivery.Have{V: sdkkeys.Version, Build: "b-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) - l.put(t, "n1.b", delivery.Have{V: sdkkeys.Version, Build: "b-2", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) + l.put(t, "n1.a", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: "b-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) + l.put(t, "n1.b", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: "b-2", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) q, _ := aHaveQueue(l) // fresh: nothing written by this process r := onlyA() r.Before = []string{"a", "b", "c"} q.SayFacts(context.Background(), r) - if b := l.have(t, "n1.b"); b.Phase != delivery.PhaseRemoved { + if b := l.have(t, "n1.b"); b.Phase != nodeengineclient.PhaseRemoved { t.Errorf("b: %+v", b) } if _, there := l.kv["n1.c"]; there { @@ -563,24 +568,24 @@ func TestAnAppliedModulesHaveNamesTheFingerprintAndTheDigestAndFollowsItsHealth( dig := "sha256:" + strings.Repeat("b", 64) q.NoteModule(context.Background(), ModuleFact{Module: "web", FP: fp, Composed: dig}) h := l.have(t, "n1.web") - if h.FP != fp || h.Composed != dig || h.Phase != delivery.PhaseRunning || h.Health != delivery.HealthUnknown { + if h.Fp != fp || h.Composed != dig || h.Phase != nodeengineclient.PhaseRunning || h.Health != nodeengineclient.HealthUnknown { t.Fatalf("the have after the apply: %+v", h) } // Starting: it waits. Past its start window the check fails: failed. q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateStarting})) - if got := l.have(t, "n1.web"); got.Health != delivery.HealthWaits || got.FP != fp || got.Composed != dig { + if got := l.have(t, "n1.web"); got.Health != nodeengineclient.HealthWaits || got.Fp != fp || got.Composed != dig { t.Fatalf("while it starts: %+v", got) } q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateUnhealthy})) - if got := l.have(t, "n1.web"); got.Health != delivery.HealthFailed || got.FP != fp || got.Composed != dig { + if got := l.have(t, "n1.web"); got.Health != nodeengineclient.HealthFailed || got.Fp != fp || got.Composed != dig { t.Fatalf("a failing check after the start window: %+v", got) } // The bus's user list written alone rides on the module's have, and an apply after it keeps it (ADR 0547 §8). - w := delivery.UserListWritten{Source: "n1.web." + fp, Revision: 4, Wanted: dig, Digest: dig, Users: 3, + w := nodeengine.UserListWritten{Source: "n1.web." + fp, Revision: 4, Wanted: dig, Digest: dig, Users: 3, At: time.Date(2026, 10, 25, 21, 0, 0, 0, time.UTC)} q.NoteUserList(context.Background(), "web", w) - if got := l.have(t, "n1.web"); got.UserList == nil || got.UserList.Revision != 4 || got.FP != fp { + if got := l.have(t, "n1.web"); got.UserList == nil || got.UserList.Revision != 4 || got.Fp != fp { t.Fatalf("the user list is not in the have: %+v", got) } q.NoteModule(context.Background(), ModuleFact{Module: "web", FP: fp, Composed: dig}) @@ -589,24 +594,24 @@ func TestAnAppliedModulesHaveNamesTheFingerprintAndTheDigestAndFollowsItsHealth( } // A failed apply waiting on a store's holder carries the pull it waits on in its have (ENG-9, read by node_haves). - wait := &delivery.PullWait{Image: "reg/x@sha256:1", Seat: "container-image-registry", Holder: "distribution", + wait := &nodeengine.PullWait{Image: "reg/x@sha256:1", Seat: "container-image-registry", Holder: "distribution", Began: time.Date(2026, 11, 4, 14, 0, 0, 0, time.UTC), Retried: "distribution settled"} q.NoteModule(context.Background(), ModuleFact{Module: "web", FP: fp, Composed: dig, Failed: true, Detail: "the pull failed", PullWait: wait}) - if got := l.have(t, "n1.web"); got.Phase != delivery.PhaseFailed || got.PullWait == nil || got.PullWait.Holder != "distribution" { + if got := l.have(t, "n1.web"); got.Phase != nodeengineclient.PhaseFailed || got.PullWait == nil || got.PullWait.Holder != "distribution" { t.Fatalf("the failed have does not say the pull it waits on: %+v", got) } // A module held for a failed backup says so and what it keeps running; removed is written once, over its key. - q.NoteModule(context.Background(), ModuleFact{Module: "web", FP: fp, Composed: dig, Phase: string(delivery.PhaseHeld), Detail: "the repository is locked"}) - if got := l.have(t, "n1.web"); got.Phase != delivery.PhaseHeld || got.Detail == "" { + q.NoteModule(context.Background(), ModuleFact{Module: "web", FP: fp, Composed: dig, Phase: string(nodeengineclient.PhaseHeld), Detail: "the repository is locked"}) + if got := l.have(t, "n1.web"); got.Phase != nodeengineclient.PhaseHeld || got.Detail == "" { t.Fatalf("held: %+v", got) } q.NoteRemoved(context.Background(), "web", "") - if got := l.have(t, "n1.web"); got.Phase != delivery.PhaseRemoved { + if got := l.have(t, "n1.web"); got.Phase != nodeengineclient.PhaseRemoved { t.Fatalf("removed: %+v", got) } q.NoteNode(context.Background(), dig, compositions.Seals{"core-box#sealed": "core"}) - if n, err := delivery.DecodeHaveNode(l.kv["n1.node"].value); err != nil || n.Composed != dig || len(n.SealedAt) != 1 || n.SealedAt["core-box#sealed"] != "core" { + if n, err := nodeengineclient.DecodeHaveNode(l.kv["n1.node"].value); err != nil || n.Composed != dig || len(n.SealedAt) != 1 || n.SealedAt["core-box#sealed"] != "core" { t.Fatalf("the node key, with its own composition's sealed values by slot: %+v %v", n, err) } if n, ok := q.ReadNodeHave(context.Background()); !ok || n.Composed != dig || len(n.SealedAt) != 1 { @@ -615,11 +620,11 @@ func TestAnAppliedModulesHaveNamesTheFingerprintAndTheDigestAndFollowsItsHealth( // 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]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 { + if n, err := nodeengineclient.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) } q.NoteCandidates(context.Background(), nil) - if n, _ := delivery.DecodeHaveNode(l.kv["n1.node"].value); n.Candidates != nil { + if n, _ := nodeengineclient.DecodeHaveNode(l.kv["n1.node"].value); n.Candidates != nil { t.Fatalf("an answer left after its group went: %+v", n.Candidates) } } @@ -633,16 +638,16 @@ func TestAModuleWithNothingToJudgeIsOkOnceLookedAt(t *testing.T) { q, _ := aHaveQueue(l) q.SayFacts(context.Background(), Report{Modules: []ModuleFact{{Module: "mesh-host", Build: "b"}, {Module: "web", Build: "b"}, {Module: "broken", Build: "b", Failed: true, Detail: "x"}}}) - if h := l.have(t, "n1.mesh-host").Health; h != delivery.HealthUnknown { + if h := l.have(t, "n1.mesh-host").Health; h != nodeengineclient.HealthUnknown { t.Fatalf("before any look: %s", h) } if !q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateHealthy})) { t.Fatal("not said") } - if h := l.have(t, "n1.mesh-host").Health; h != delivery.HealthOK { + if h := l.have(t, "n1.mesh-host").Health; h != nodeengineclient.HealthOK { t.Fatalf("a running module with nothing to judge after a look: %s", h) } - if h := l.have(t, "n1.broken").Health; h == delivery.HealthOK { + if h := l.have(t, "n1.broken").Health; h == nodeengineclient.HealthOK { t.Fatalf("a failed module was said ok") } } @@ -654,17 +659,17 @@ func TestAModuleSaidAgainOverAnOlderEnginesUnknownIsWrittenWithThisEnginesHealth l := newHaveLink() fp := "src2:" + strings.Repeat("a", 64) dig := "sha256:" + strings.Repeat("b", 64) - l.put(t, "n1.xorg", delivery.Have{V: sdkkeys.Version, FP: fp, Composed: dig, Phase: delivery.PhaseRunning, - Health: delivery.HealthUnknown}) + l.put(t, "n1.xorg", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Fp: fp, Composed: dig, Phase: nodeengineclient.PhaseRunning, + Health: nodeengineclient.HealthUnknown}) q, _ := aHaveQueue(l) if !q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateHealthy})) { t.Fatal("not said") } - if h := l.have(t, "n1.xorg"); h.Health != delivery.HealthUnknown { + if h := l.have(t, "n1.xorg"); h.Health != nodeengineclient.HealthUnknown { t.Fatalf("a module the session does not hold was written: %+v", h) } q.NoteModule(context.Background(), ModuleFact{Module: "xorg", FP: fp, Composed: dig}) - if h := l.have(t, "n1.xorg"); h.Health != delivery.HealthOK || h.FP != fp || h.Composed != dig || h.Phase != delivery.PhaseRunning { + if h := l.have(t, "n1.xorg"); h.Health != nodeengineclient.HealthOK || h.Fp != fp || h.Composed != dig || h.Phase != nodeengineclient.PhaseRunning { t.Fatalf("said again after a look: %+v", h) } } @@ -679,26 +684,26 @@ func TestAHealthIsNotJudgedWhileAModuleIsApplied(t *testing.T) { old := ModuleFact{Module: "ctl", FP: "src2:" + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("1", 64)} q.NoteModule(ctx, old) q.SayHealth(ctx, *healthOf(map[string]string{"ctl": StateUnhealthy})) - if h := l.have(t, "n1.ctl"); h.Health != delivery.HealthFailed { + if h := l.have(t, "n1.ctl"); h.Health != nodeengineclient.HealthFailed { t.Fatalf("the running module's failed look was not said: %+v", h) } next := ModuleFact{Module: "ctl", FP: "src2:" + strings.Repeat("b", 64), Composed: "sha256:" + strings.Repeat("2", 64)} applying := next - applying.Phase = string(delivery.PhaseApplying) + applying.Phase = string(nodeengineclient.PhaseApplying) q.NoteModule(ctx, applying) - if h := l.have(t, "n1.ctl"); h.Phase != delivery.PhaseApplying || h.Health != delivery.HealthUnknown { + if h := l.have(t, "n1.ctl"); h.Phase != nodeengineclient.PhaseApplying || h.Health != nodeengineclient.HealthUnknown { t.Fatalf("applying carried the last look's health: %+v", h) } q.SayHealth(ctx, *healthOf(map[string]string{"ctl": StateUnhealthy})) - if h := l.have(t, "n1.ctl"); h.Health != delivery.HealthUnknown { + if h := l.have(t, "n1.ctl"); h.Health != nodeengineclient.HealthUnknown { t.Fatalf("a look during the apply judged it: %+v", h) } q.NoteModule(ctx, next) - if h := l.have(t, "n1.ctl"); h.Phase != delivery.PhaseRunning || h.Health != delivery.HealthUnknown { + if h := l.have(t, "n1.ctl"); h.Phase != nodeengineclient.PhaseRunning || h.Health != nodeengineclient.HealthUnknown { t.Fatalf("running before a look after the apply: %+v", h) } q.SayHealth(ctx, *healthOf(map[string]string{"ctl": StateHealthy})) - if h := l.have(t, "n1.ctl"); h.Health != delivery.HealthOK { + if h := l.have(t, "n1.ctl"); h.Health != nodeengineclient.HealthOK { t.Fatalf("the look after the apply was not taken: %+v", h) } } @@ -711,7 +716,7 @@ func TestAHealthIsNotCarriedOverToAnotherComposition(t *testing.T) { q.NoteModule(ctx, ModuleFact{Module: "ctl", FP: "src2:" + strings.Repeat("a", 64)}) q.SayHealth(ctx, *healthOf(map[string]string{"ctl": StateUnhealthy})) q.NoteModule(ctx, ModuleFact{Module: "ctl", FP: "src2:" + strings.Repeat("b", 64)}) - if h := l.have(t, "n1.ctl"); h.Health != delivery.HealthUnknown { + if h := l.have(t, "n1.ctl"); h.Health != nodeengineclient.HealthUnknown { t.Fatalf("the old composition's look judged the new one: %+v", h) } } @@ -724,11 +729,11 @@ func TestADeclarationReportMovesAHealthAndARemovalForgetsIt(t *testing.T) { ctx := context.Background() q.SayFacts(ctx, Report{Applied: []string{"a.x"}, Modules: []ModuleFact{{Module: "a", Build: "b-1"}}, Health: healthOf(map[string]string{"a": StateUnhealthy})}) - if h := l.have(t, "n1.a"); h.Health != delivery.HealthFailed { + if h := l.have(t, "n1.a"); h.Health != nodeengineclient.HealthFailed { t.Fatalf("the failed look was not said: %+v", h) } q.SayFacts(ctx, Report{Applied: []string{"a.x"}, Modules: []ModuleFact{{Module: "a", Build: "b-1", FP: "src2:" + strings.Repeat("c", 64)}}}) - if h := l.have(t, "n1.a"); h.Health != delivery.HealthUnknown { + if h := l.have(t, "n1.a"); h.Health != nodeengineclient.HealthUnknown { t.Fatalf("the old composition's look judged the new one: %+v", h) } select { @@ -750,18 +755,18 @@ func TestADeclarationReportMovesAHealthAndARemovalForgetsIt(t *testing.T) { // key that does not exist is not made up and is said once; a writer that drops the list again is written over. func TestAUserListReachesTheHaveOfAModuleNoApplyHereSays(t *testing.T) { l := newHaveLink() - older := delivery.Have{V: sdkkeys.Version, Build: "build-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK} + older := nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: "build-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK} l.put(t, "n1.nats", older) q, said := aHaveQueue(l) dig := "sha256:" + strings.Repeat("c", 64) - w := delivery.UserListWritten{Source: "n1.nats", Revision: 7, Wanted: dig, Digest: dig, Users: 58, + w := nodeengine.UserListWritten{Source: "n1.nats", Revision: 7, Wanted: dig, Digest: dig, Users: 58, At: time.Date(2026, 10, 26, 12, 26, 51, 0, time.UTC)} q.NoteUserList(context.Background(), "nats", w) got := l.have(t, "n1.nats") if got.UserList == nil || got.UserList.Users != 58 || got.UserList.Digest != dig { t.Fatalf("the user list is not in the have: %+v", got) } - if got.Build != "build-1" || got.Phase != delivery.PhaseRunning || got.Health != delivery.HealthOK { + if got.Build != "build-1" || got.Phase != nodeengineclient.PhaseRunning || got.Health != nodeengineclient.HealthOK { t.Fatalf("the merge lost the key's own fields: %+v", got) } rev := l.kv["n1.nats"].rev @@ -794,7 +799,7 @@ func TestAUserListReachesTheHaveOfAModuleNoApplyHereSays(t *testing.T) { } fp := "src2:" + strings.Repeat("a", 64) q.NoteModule(context.Background(), ModuleFact{Module: "other", FP: fp, Composed: dig}) - if got := l.have(t, "n1.other"); got.UserList == nil || got.FP != fp { + if got := l.have(t, "n1.other"); got.UserList == nil || got.Fp != fp { t.Fatalf("the first have of the module lost the list: %+v", got) } } diff --git a/internal/link/health_test.go b/internal/link/health_test.go index cc17b61..8a87e3b 100644 --- a/internal/link/health_test.go +++ b/internal/link/health_test.go @@ -3,12 +3,11 @@ package link import ( "context" "encoding/json" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "slices" "strings" "testing" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // A health statement is said on the link open now, as the machine's own word on its own subject, and @@ -60,7 +59,7 @@ func TestAHealthStatementIsSaidOnTheLinkOpenNow(t *testing.T) { since := time.Date(2026, 10, 29, 20, 0, 0, 0, time.UTC) look := func(streak int, writer string) Health { return Health{Contract: 7, At: time.Now(), Network: &NetworkHealth{State: StateUnhealthy, Since: since, Parts: []NetworkPart{ - {Part: "resolv-conf", State: StateUnhealthy, Writer: writer, Owner: "dnsmasq", Since: since, Streak: streak}}}, + {Part: "resolv-conf", State: StateUnhealthy, Writer: writer, Owner: "dnsmasq", Since: since, Streak: int64(streak)}}}, Units: &UnitsHealth{State: "degraded", Failed: []FailedUnit{{Unit: "cups.service", Scope: "system", Since: since}}}, Resources: []ResourceHealth{ {Module: "brother", Resource: "brother.scanner", Kind: KindAccount, Account: "jochen", State: StateUnhealthy, @@ -121,12 +120,12 @@ func TestAHealthStatementIsSaidOnTheLinkOpenNow(t *testing.T) { t.Fatalf("step %d: wrote %v (want %v), %v", i, wrote, step.wrote, err) } } - got, err := delivery.DecodeNodeHealth(store.values["anchor"]) + got, err := nodeengineclient.DecodeHealth(store.values["anchor"]) if err != nil || got.Node != "anchor" || got.Network.Parts[0].Writer != "openvpn" || !got.Peers[0].Seen || got.At.IsZero() { t.Fatalf("the key holds %+v, %v", got, err) } // The key's bytes are mesh-sdk's layout: decoded and encoded again by it, the same bytes. - if again, err := delivery.EncodeNodeHealth(got); err != nil || string(again) != string(store.values["anchor"]) { + if again, err := nodeengineclient.EncodeHealth(got); err != nil || string(again) != string(store.values["anchor"]) { t.Fatalf("the key's bytes are not the sdk's layout: %s\n%s (%v)", store.values["anchor"], again, err) } if store.puts != 3 { diff --git a/internal/link/messages.go b/internal/link/messages.go index 96d0475..16c32eb 100644 --- a/internal/link/messages.go +++ b/internal/link/messages.go @@ -1,11 +1,10 @@ package link import ( + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/health" "github.com/novox/mesh-host/internal/compositions" "time" - - "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-sdk/go/health" ) // The wire formats shared with the control plane, which defines them separately because this @@ -304,10 +303,10 @@ const ReasonUsedAsFound = "used as found:" // The machine's networking and its service managers in a statement are the node's health report's parts (mesh-sdk // delivery, issue 774): one layout for the message, the report and the watchman that reads it. type ( - NetworkHealth = delivery.NetworkHealth - NetworkPart = delivery.NetworkPart - UnitsHealth = delivery.UnitsHealth - FailedUnit = delivery.FailedUnit + NetworkHealth = nodeengine.NetworkHealth + NetworkPart = nodeengine.NetworkPart + UnitsHealth = nodeengine.UnitsHealth + FailedUnit = nodeengine.FailedUnit ) // The states a long-running resource is said in (ADR 0240 §4). @@ -640,12 +639,12 @@ type ModuleFact struct { // Judgement and Waits are, in phase waiting-yes, the judgement of the move and the composition waiting (ADR 0547 §3). Judgement string Waits string - // BuildAlone says, in phase waiting-yes, the move changes the build alone (ADR 0601 §1, delivery.Have.BuildAlone). + // BuildAlone says, in phase waiting-yes, the move changes the build alone (ADR 0601 §1, nodeengine.HaveByNodeModuleValue.BuildAlone). BuildAlone bool - // Kept is what the node keeps of the module as it runs, for a manifest that does not say it yet (delivery.Have.Kept). + // Kept is what the node keeps of the module as it runs, for a manifest that does not say it yet (nodeengine.HaveByNodeModuleValue.Kept). Kept []string - // Sealed is the values the composition it names carries sealed to this node's key, by slot (delivery.Have.SealedAt). + // Sealed is the values the composition it names carries sealed to this node's key, by slot (nodeengine.HaveByNodeModuleValue.SealedAt). Sealed compositions.Seals - // PullWait is, on a failed fact, the pull from a store its apply waits on (delivery.Have.PullWait, issue 1966 ENG-9). - PullWait *delivery.PullWait + // PullWait is, on a failed fact, the pull from a store its apply waits on (nodeengine.HaveByNodeModuleValue.PullWait, issue 1966 ENG-9). + PullWait *nodeengine.PullWait } diff --git a/internal/link/nodehealth.go b/internal/link/nodehealth.go index 5aa2d87..1d6f293 100644 --- a/internal/link/nodehealth.go +++ b/internal/link/nodehealth.go @@ -4,13 +4,16 @@ import ( "context" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/keys" + "github.com/novox/mesh-host/internal/buckets" "slices" "sort" "strings" "time" "git.novox.be/novox/mesh-sdk/go/declaration" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/nats-io/nats.go" ) @@ -30,17 +33,17 @@ import ( // - node-account-fallback: Fallback, the engine's fall back to its old login during a rotation (ADR 0583 §6). // -// The report's layout is mesh-sdk's (delivery.NodeHealth): the bucket, its version and its parts. +// The report's layout is mesh-sdk's (nodeengine.HealthByNodeValue): the bucket, its version and its parts. const ( - HealthBucket = delivery.NodeHealthBucket - NodeHealthVersion = delivery.NodeHealthVersion + HealthBucket = buckets.Health + NodeHealthVersion = keys.Version ) type ( - NodeHealth = delivery.NodeHealth - PeerSeen = delivery.PeerSeen - NodeAccount = delivery.NodeAccount - NodeDirectory = delivery.NodeDirectory + NodeHealth = nodeengine.HealthByNodeValue + PeerSeen = nodeengine.PeerSeen + NodeAccount = nodeengine.NodeAccount + NodeDirectory = nodeengine.NodeDirectory ) // ReasonReloginNeeded starts an account's reason when its session needs a new login (accounts.ReasonRelogin). @@ -83,7 +86,7 @@ const TowardHub = "hub" // streaks, each unhealthy part naming the machines it points at (machines), its machine units, and each account and // directory of its resources that the six alerts read. func NodeHealthOf(node string, h Health, peers []PeerSeen, machines Machines) NodeHealth { - out := NodeHealth{V: NodeHealthVersion, Node: node, Units: h.Units, Contract: h.Contract} + out := NodeHealth{V: NodeHealthVersion, Node: node, Units: h.Units, Contract: int64(h.Contract)} if len(peers) > 0 { out.Peers = append([]PeerSeen(nil), peers...) sort.Slice(out.Peers, func(i, k int) bool { return out.Peers[i].Key < out.Peers[k].Key }) @@ -108,7 +111,7 @@ func NodeHealthOf(node string, h Health, peers []PeerSeen, machines Machines) No } if r.Kind == KindAccount && r.Root == declaration.RootNever { // The account declared never to become root without a person, judged (ADR 0266): what DA and D-root read. - out.Root = append(out.Root, delivery.RootVerdict{Account: r.Account, Module: r.Module, Resource: r.Resource, + out.Root = append(out.Root, nodeengine.RootAccountVerdict{Account: r.Account, Module: r.Module, Resource: r.Resource, State: r.State, Reason: r.Reason, Since: r.Since}) } if r.Kind == KindDirectory && strings.HasPrefix(r.Reason, ReasonUsedAsFound) { @@ -191,16 +194,16 @@ func writeNodeHealth(ctx context.Context, store HealthStore, last **NodeHealth, return false, fmt.Errorf("the health report of %s cannot be read: %w", h.Node, err) } if raw != nil { - if was, err := delivery.DecodeNodeHealth(raw); err == nil { + if was, err := nodeengineclient.DecodeHealth(raw); err == nil { *last = &was } } } - if *last != nil && delivery.SameNodeHealth(**last, h) { + if *last != nil && nodeengineclient.SameHealth(**last, h) { return false, nil } h.At = now().UTC() - raw, err := delivery.EncodeNodeHealth(h) + raw, err := nodeengineclient.EncodeHealth(h) if err != nil { return false, err } diff --git a/internal/link/record.go b/internal/link/record.go index cd6a30d..b228959 100644 --- a/internal/link/record.go +++ b/internal/link/record.go @@ -2,6 +2,7 @@ package link import ( "encoding/json" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" sdkkeys "git.novox.be/novox/mesh-sdk/go/keys" "slices" "sort" @@ -9,11 +10,10 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // The node's record (novox/hq ADR 0555 §3, issue 1126): what the machine observes of itself, written in its own have key -// `.node` (delivery.HaveNode.Record) with its facts, so every node and the parts that compose read it from the bus +// `.node` (nodeengine.HaveByNodeNodeValue.Record) with its facts, so every node and the parts that compose read it from the bus // and hear its change as the key's. Each value is the one the controller took from this node's enrolment and reports // until now (its profile's present capabilities and networks, its outward links, its health statement's contract, and // the public halves of the keys it enrolled with), read here from the same sources, never from the controller. @@ -44,7 +44,7 @@ var readsStatedAt = time.Now().UTC() // recordOf is the node's record from one facts report and its keys; nil for a report that carries no profile (one that // says nothing of the machine, such as a rekey), so the record last written stands. -func recordOf(node string, r Report, keys RecordKeys) *delivery.NodeRecord { +func recordOf(node string, r Report, keys RecordKeys) *nodeengine.NodeRecord { if r.Profile == nil { return nil } @@ -61,7 +61,7 @@ func recordOf(node string, r Report, keys RecordKeys) *delivery.NodeRecord { if err != nil || json.Unmarshal(raw, &p) != nil { return nil } - rec := &delivery.NodeRecord{V: sdkkeys.Version, Node: node, TunnelKey: keys.TunnelKey, SealingKey: keys.SealingKey, + rec := &nodeengine.NodeRecord{V: sdkkeys.Version, Node: node, TunnelKey: keys.TunnelKey, SealingKey: keys.SealingKey, ServingKey: keys.ServingKey, Certificate: keys.Certificate, Authority: keys.Authority, Trusts: keys.Trusts, ManifestReads: manifestReads()} for _, c := range p.Capabilities { @@ -77,13 +77,15 @@ func recordOf(node string, r Report, keys RecordKeys) *delivery.NodeRecord { sort.Strings(rec.Capabilities) if p.Machine != nil { rec.NetworksSaid = true - rec.Networks = p.Machine.Networks + for _, n := range p.Machine.Networks { + rec.Networks = append(rec.Networks, nodeengine.AttachedNetwork{Link: n.Link, Network: n.Network}) + } } if len(r.Outward) > 0 { rec.OutwardLinks = append([]string(nil), r.Outward...) } if r.Health != nil { - rec.Contract = r.Health.Contract + rec.Contract = int64(r.Health.Contract) } return rec } diff --git a/internal/link/rotate.go b/internal/link/rotate.go index 12d06bb..e0474e9 100644 --- a/internal/link/rotate.go +++ b/internal/link/rotate.go @@ -3,6 +3,7 @@ package link import ( "context" "fmt" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "sync" "time" @@ -35,9 +36,9 @@ type Rotator struct { failures int // fellBack is the key's `since` a fallback was made for: the same rotation is not tried again. fellBackSince string - // fallen is the fall back the node's health report carries (delivery.LoginFallback), from which the watchman raises + // fallen is the fall back the node's health report carries (nodeengine.LoginFallback), from which the watchman raises // node-account-fallback (ADR 0583 §6), until the vault's key of this node says anything new. - fallen *delivery.LoginFallback + fallen *nodeengine.LoginFallback switchedSince string } @@ -162,13 +163,13 @@ func (r *Rotator) AuthFailed(login string) { if r.Now != nil { now = r.Now } - r.fallen = &delivery.LoginFallback{Node: r.Logins.node, From: login, Why: text, At: now().UTC()} + r.fallen = &nodeengine.LoginFallback{Node: r.Logins.node, From: login, Why: text, At: now().UTC()} r.old, r.failures = "", 0 r.fellBackSince = r.switchedSince } // Fallback is the fall back the node's health report carries now, nil when there is none. -func (r *Rotator) Fallback() *delivery.LoginFallback { +func (r *Rotator) Fallback() *nodeengine.LoginFallback { if r == nil { return nil } diff --git a/internal/link/rotate_test.go b/internal/link/rotate_test.go index 57a8e32..0b3c1fc 100644 --- a/internal/link/rotate_test.go +++ b/internal/link/rotate_test.go @@ -3,6 +3,8 @@ package link import ( "context" "errors" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "slices" "strings" "testing" @@ -140,7 +142,7 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t * t.Fatalf("after the bound: %s, fall back %+v", cur, fb) } // The health report carries it, from which the watchman raises node-account-fallback. - if _, err := delivery.EncodeNodeHealth(delivery.NodeHealth{Node: "ace", Fallback: fb}); err != nil { + if _, err := nodeengineclient.EncodeHealth(nodeengine.HealthByNodeValue{Node: "ace", Fallback: fb}); err != nil { t.Errorf("the fall back is no health report's: %v", err) } steps := len(f.steps) diff --git a/internal/link/session.go b/internal/link/session.go index 9af5818..a05b5f3 100644 --- a/internal/link/session.go +++ b/internal/link/session.go @@ -12,7 +12,6 @@ import ( "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/health" ) @@ -23,7 +22,7 @@ import ( // receives, orders or reconciles a whole declaration. type Session struct { // Fallback is the engine's fall back to its old login its health report carries now (Rotator.Fallback); nil says none. - Fallback func() *delivery.LoginFallback + Fallback func() *nodeengine.LoginFallback // linkedUser is the bus user the link open now authenticated as, guarded by mu (WhenLinkedAs). linkedUser string // Membership is the node and the mesh's key a controller's ask is verified with. diff --git a/internal/network/drill_test.go b/internal/network/drill_test.go index fa22fba..0d39b82 100644 --- a/internal/network/drill_test.go +++ b/internal/network/drill_test.go @@ -46,7 +46,7 @@ func TestDrillAResolverFileRewrittenInAThrowawayMachine(t *testing.T) { Network: &link.NetworkHealth{State: st.State, Since: st.Since.UTC(), Parts: []link.NetworkPart{}}} for _, p := range st.Parts { h.Network.Parts = append(h.Network.Parts, link.NetworkPart{Part: p.Part, State: p.State, Reason: p.Reason, - Said: p.Said, Writer: p.Writer, Owner: p.Owner, Toward: p.Toward, Since: p.Since.UTC(), Streak: p.Streak}) + Said: p.Said, Writer: p.Writer, Owner: p.Owner, Toward: p.Toward, Since: p.Since.UTC(), Streak: int64(p.Streak)}) } said = append(said, h) raw, _ := json.Marshal(st) diff --git a/internal/selfcompose/candidate.go b/internal/selfcompose/candidate.go index aaf30a3..5ca69a1 100644 --- a/internal/selfcompose/candidate.go +++ b/internal/selfcompose/candidate.go @@ -227,7 +227,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi case laidErr != nil && baseErr != nil: // The node composes none of its own without the candidate either: the candidate changes nothing of that. a.Verdict, a.Modules = nec.Composed, []nodeengine.Module{} - a.Own = nodeengine.Module{Module: delivery.HaveNodeWord, State: nec.ModuleHeld, Words: laidErr.Error()} + a.Own = nodeengine.Module{Module: nec.NodeWord, State: nec.ModuleHeld, Words: laidErr.Error()} a.Took = c.now().Sub(began).Milliseconds() return a case laidErr != nil: @@ -244,7 +244,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi one := func(module string) (nodeengine.Module, bool) { m := nodeengine.Module{Module: module} if module == "" { - m.Module = delivery.HaveNodeWord + m.Module = nec.NodeWord } if why, held := laidHeld[module]; held { m.State, m.Words = nec.ModuleRefused, why @@ -295,7 +295,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi if own, ok := one(""); ok { a.Own = own } else { - a.Own = nodeengine.Module{Module: delivery.HaveNodeWord, State: nec.ModuleHeld, + a.Own = nodeengine.Module{Module: nec.NodeWord, State: nec.ModuleHeld, Words: "the node composes no composition of its own"} } refused = refused || a.Own.State == nec.ModuleRefused diff --git a/internal/selfcompose/candidate_test.go b/internal/selfcompose/candidate_test.go index 18433af..6b1adcf 100644 --- a/internal/selfcompose/candidate_test.go +++ b/internal/selfcompose/candidate_test.go @@ -3,6 +3,7 @@ package selfcompose import ( "context" "encoding/json" + "github.com/novox/mesh-host/internal/buckets" "strings" "sync/atomic" "testing" @@ -83,7 +84,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { } place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []delivery.PlacedModule{{Module: "lemurs"}}}) - have, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) + have, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}) deadline := time.Now().Add(5 * time.Second) watching := func() int { b.mu.Lock(); defer b.mu.Unlock(); return len(b.watches) } @@ -91,9 +92,9 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { time.Sleep(5 * time.Millisecond) } b.put(t, delivery.PlacementBucket, "placement.n1", place) - b.put(t, delivery.HaveBucket, "n1.node", have) + b.put(t, buckets.Have, "n1.node", have) b.put(t, delivery.ModulesBucket, "lemurs", mod) - for _, bucket := range []string{delivery.PlacementCandidatesBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, ResolvedBucket} { + for _, bucket := range []string{delivery.PlacementCandidatesBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, ResolvedBucket} { b.send(bucket, agent.Update{Caught: true}) } for !s.ready.Load() && time.Now().Before(deadline) { @@ -106,7 +107,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { } if a.Verdict != nec.Composed || len(a.Modules) != 1 || a.Modules[0].Module != "lemurs" || a.Modules[0].State != nec.ModuleComposed || a.Modules[0].Fp != fpNew || !a.Modules[0].Changed || - a.Own.Module != delivery.HaveNodeWord || a.Took > nec.AnswerBound.Milliseconds() { + a.Own.Module != nec.NodeWord || a.Took > nec.AnswerBound.Milliseconds() { t.Errorf("an accepted candidate answered %+v", a) } // Issue 774: the input it sends the sandbox says how it composed the node (the builds), so the sandbox composes it @@ -181,7 +182,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) { s := NewShadow(env, nil) s.c.placements["n1"] = delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []delivery.PlacedModule{{Module: "lemurs"}}} - s.c.records["n1"] = delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}} + s.c.records["n1"] = nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}} s.c.modules["lemurs"] = delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"} s.c.modules["other"] = delivery.Module{FP2: fp, Manifest: digestOther, Repository: "novox/x", Commit: "c"} s.ready.Store(true) diff --git a/internal/selfcompose/composedstate_test.go b/internal/selfcompose/composedstate_test.go index 48d3211..86ac8a7 100644 --- a/internal/selfcompose/composedstate_test.go +++ b/internal/selfcompose/composedstate_test.go @@ -2,6 +2,9 @@ package selfcompose import ( "context" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/buckets" engcomp "github.com/novox/mesh-host/internal/compositions" "slices" "strings" @@ -85,18 +88,18 @@ func TestTheNodePublishesItsCompositions(t *testing.T) { } place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}}) - have, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) + have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}) omod, _ := delivery.EncodeModule(delivery.Module{FP2: ofp, Manifest: odigest, Repository: "novox/o", Commit: "c"}) want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true}) nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true}) b.put(t, delivery.PlacementBucket, "placement.n1", place) - b.put(t, delivery.HaveBucket, "n1.node", have) + b.put(t, buckets.Have, "n1.node", have) b.put(t, delivery.ModulesBucket, "lemurs", mod) b.put(t, delivery.ModulesBucket, "oldie", omod) b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want) b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant) - for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { b.send(bucket, agent.Update{Caught: true}) } for u := next(t, out); !u.Caught; u = next(t, out) { diff --git a/internal/selfcompose/data.go b/internal/selfcompose/data.go index fb57685..9fe8fc5 100644 --- a/internal/selfcompose/data.go +++ b/internal/selfcompose/data.go @@ -4,13 +4,15 @@ import ( "bytes" "context" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/buckets" engcomp "github.com/novox/mesh-host/internal/compositions" "sort" "strings" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/frombus" ) @@ -62,7 +64,7 @@ func (c *Composer) publishData(ctx context.Context) { items, unplaced, held, err := dataItemsOf(ctx, itemsReader{reader{c: c}}, c.env.Mesh, scope) if err != nil { c.sayData("the data items of this node's modules cannot be worked out, and what the engine's state " + - delivery.EngineDataBucket + " holds stands; " + c.oweData() + ": " + err.Error()) + buckets.Data + " holds stands; " + c.oweData() + ": " + err.Error()) return } // A module held for a refused manifest declares nothing this pass can read: it is named, its other modules' items @@ -83,7 +85,7 @@ func (c *Composer) publishData(ctx context.Context) { c.mu.Unlock() wrote, removed, err := c.writeData(ctx, items, digests, held) if err != nil { - c.sayData("the data items of this node's modules are not all kept in " + delivery.EngineDataBucket + + c.sayData("the data items of this node's modules are not all kept in " + buckets.Data + "; " + c.oweData() + ": " + err.Error()) return } @@ -93,7 +95,7 @@ func (c *Composer) publishData(ctx context.Context) { c.sayData("") if wrote+removed > 0 { c.env.Say(fmt.Sprintf("the data items of this node's modules: %d key(s) of %s written, %d deleted", - wrote, delivery.EngineDataBucket, removed)) + wrote, buckets.Data, removed)) } } @@ -145,7 +147,7 @@ func (c *Composer) digestsByModule() map[string]string { func (c *Composer) writeData(ctx context.Context, items map[string][]catalogue.PlacedDataItem, digests map[string]string, held map[string]string) (wrote, removed int, err error) { store := c.env.Data if c.dataKept == nil { - pattern, err := delivery.EngineDataNodeKeys(c.env.Node) + pattern, err := nodeengineclient.DataNodeKeys(c.env.Node) if err != nil { return 0, 0, err } @@ -176,7 +178,7 @@ func (c *Composer) writeData(ctx context.Context, items map[string][]catalogue.P if len(items[module]) == 0 { continue } - key, err := delivery.EngineDataKey(c.env.Node, module) + key, err := nodeengineclient.DataKey(c.env.Node, module) if err != nil { failed = append(failed, err.Error()) continue @@ -185,7 +187,7 @@ func (c *Composer) writeData(ctx context.Context, items map[string][]catalogue.P if !ok { digest = digests[""] // run inside the node's own composition } - raw, err := delivery.EncodeEngineData(delivery.EngineData{Node: c.env.Node, Module: module, Digest: digest, Items: items[module]}) + raw, err := nodeengineclient.EncodeData(nodeengine.DataByNodeModuleValue{Node: c.env.Node, Module: module, Digest: digest, Items: placedItems(items[module])}) if err != nil { failed = append(failed, err.Error()) continue @@ -232,9 +234,19 @@ func (c *Composer) writeData(ctx context.Context, items map[string][]catalogue.P // heldKey is whether key is the key of a module held for a refused manifest, which stands as it was. func heldKey(node, key string, held map[string]string) bool { for module := range held { - if k, err := delivery.EngineDataKey(node, module); err == nil && k == key { + if k, err := nodeengineclient.DataKey(node, module); err == nil && k == key { return true } } return false } + +// placedItems are a module's data items as the state data carries them. +func placedItems(in []catalogue.PlacedDataItem) []nodeengine.PlacedDataItem { + out := make([]nodeengine.PlacedDataItem, 0, len(in)) + for _, i := range in { + out = append(out, nodeengine.PlacedDataItem{Module: i.Module, Item: i.ID, Class: i.Class, Path: i.Path, + CoveredBy: i.CoveredBy, Protection: i.Protection, Measure: i.Measure}) + } + return out +} diff --git a/internal/selfcompose/data_test.go b/internal/selfcompose/data_test.go index c92e25b..dc32133 100644 --- a/internal/selfcompose/data_test.go +++ b/internal/selfcompose/data_test.go @@ -3,6 +3,9 @@ package selfcompose import ( "context" "errors" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/buckets" engcomp "github.com/novox/mesh-host/internal/compositions" "slices" "strings" @@ -86,7 +89,7 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { var said []string env := Env{Node: "n1", Data: store, Say: func(s string) { said = append(said, s) }} // The engine's grant lets it read its own node's keys, as the listing at a run's first pass does, and no other's. - if !grantAllows(t, "n1", delivery.EngineDataBucket, "n1.*") || grantAllows(t, "n1", delivery.EngineDataBucket, "n2.*") { + if !grantAllows(t, "n1", buckets.Data, "n1.*") || grantAllows(t, "n1", buckets.Data, "n2.*") { t.Error("the node's grant reads other keys of the engine's state data than its own") } c := New(env) @@ -105,15 +108,15 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { deletes []string asked int // want is each key's items and digest after the step. - want map[string]delivery.EngineData + want map[string]nodeengine.DataByNodeModuleValue }{ {name: "the run's first pass writes each module's items and deletes what left", due: true, items: map[string][]catalogue.PlacedDataItem{"web": {item("web", "db", "/srv/web/db")}, "oldie": {item("oldie", "x", "/srv/oldie")}, "bare": nil}, digests: map[string]string{webKey: "sha256:w1", nodeKey: "sha256:n1"}, puts: []string{"n1.oldie", "n1.web"}, deletes: []string{"n1.gone"}, asked: 1, - want: map[string]delivery.EngineData{ - "n1.web": {Module: "web", Digest: "sha256:w1", Items: []catalogue.PlacedDataItem{item("web", "db", "/srv/web/db")}}, - "n1.oldie": {Module: "oldie", Digest: "sha256:n1", Items: []catalogue.PlacedDataItem{item("oldie", "x", "/srv/oldie")}}}}, + want: map[string]nodeengine.DataByNodeModuleValue{ + "n1.web": {Module: "web", Digest: "sha256:w1", Items: placedItems([]catalogue.PlacedDataItem{item("web", "db", "/srv/web/db")})}, + "n1.oldie": {Module: "oldie", Digest: "sha256:n1", Items: placedItems([]catalogue.PlacedDataItem{item("oldie", "x", "/srv/oldie")})}}}, {name: "a pass that handed nothing new reads nothing", due: false, asked: 1}, {name: "a pass that changes no module's items writes nothing", due: true, items: map[string][]catalogue.PlacedDataItem{"web": {item("web", "db", "/srv/web/db")}, "oldie": {item("oldie", "x", "/srv/oldie")}}, @@ -126,8 +129,8 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { {name: "a module whose composition changed its items is written again", due: false, items: map[string][]catalogue.PlacedDataItem{"web": {item("web", "db", "/srv/web/db2")}, "oldie": {item("oldie", "x", "/srv/oldie")}}, digests: map[string]string{webKey: "sha256:w2", nodeKey: "sha256:n1"}, puts: []string{"n1.web"}, asked: 5, - want: map[string]delivery.EngineData{ - "n1.web": {Module: "web", Digest: "sha256:w2", Items: []catalogue.PlacedDataItem{item("web", "db", "/srv/web/db2")}}}}, + want: map[string]nodeengine.DataByNodeModuleValue{ + "n1.web": {Module: "web", Digest: "sha256:w2", Items: placedItems([]catalogue.PlacedDataItem{item("web", "db", "/srv/web/db2")})}}}, {name: "a module that left the node is deleted", due: true, items: map[string][]catalogue.PlacedDataItem{"oldie": {item("oldie", "x", "/srv/oldie")}}, digests: map[string]string{nodeKey: "sha256:n1"}, deletes: []string{"n1.web"}, asked: 6}, @@ -136,8 +139,8 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { items: map[string][]catalogue.PlacedDataItem{"web": {item("web", "db", "/srv/web/db")}}, held: map[string]string{"oldie": "the manifest of oldie is refused: a bar item is no icon"}, digests: map[string]string{webKey: "sha256:w3", nodeKey: "sha256:n1"}, puts: []string{"n1.web"}, asked: 7, - want: map[string]delivery.EngineData{ - "n1.web": {Module: "web", Digest: "sha256:w3", Items: []catalogue.PlacedDataItem{item("web", "db", "/srv/web/db")}}}}, + want: map[string]nodeengine.DataByNodeModuleValue{ + "n1.web": {Module: "web", Digest: "sha256:w3", Items: placedItems([]catalogue.PlacedDataItem{item("web", "db", "/srv/web/db")})}}}, } { t.Run(step.name, func(t *testing.T) { store.mu.Lock() @@ -159,7 +162,7 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) { t.Errorf("owed a pass: %v after %s; want after %s", c.owed.Load(), wait, step.owed) } for key, want := range step.want { - got, err := delivery.DecodeEngineData(store.kv[key]) + got, err := nodeengineclient.DecodeData(store.kv[key]) if err != nil || got.Node != "n1" || got.Module != want.Module || got.Digest != want.Digest || !slices.Equal(got.Items, want.Items) { t.Errorf("%s holds %+v (%v), want %+v", key, got, err, want) diff --git a/internal/selfcompose/packages.go b/internal/selfcompose/packages.go index 45b4801..169538b 100644 --- a/internal/selfcompose/packages.go +++ b/internal/selfcompose/packages.go @@ -10,7 +10,6 @@ import ( engcomp "github.com/novox/mesh-host/internal/compositions" "sort" "time" - ) // **The packages this node's compositions declare, kept by its engine** (novox/hq ADR 0565 §6, §15; issue 1953). The diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index 4b115df..81dff9c 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -19,6 +19,9 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/compositions" engcomp "github.com/novox/mesh-host/internal/compositions" "maps" @@ -76,7 +79,7 @@ type Env struct { 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 - // (delivery.Have.SealedAt); legacy is the list a have written before slots holds (delivery.Have.Sealed), reused + // (nodeengine.HaveByNodeModuleValue.SealedAt); legacy is the list a have written before slots holds (nodeengine.HaveByNodeModuleValue.Sealed), reused // only within its own module. // An error is a have the bus did not answer for, never one absent: the pass is then tried again (BusFailed). Held func(module string) (slots compositions.Seals, legacy []string, err error) @@ -131,7 +134,7 @@ type Composer struct { // placementKeys is the seat's state placement key by key in either form, each node's placement assembled from it // (delivery.PlacementKeys; research 072 row 37): a write of the holder's under way is never composed half. placementKeys *delivery.PlacementKeys - records map[string]delivery.NodeRecord + records map[string]nodeengine.NodeRecord // inOwn is, by module, whether this node's have of it names no fingerprint of the new form (mesh-wireguard, a module // at a build of the old form): the node runs it inside its own composition, and composes it there again (issue 1126). inOwn map[string]bool @@ -298,7 +301,7 @@ func New(env Env) *Composer { env.Say = func(string) {} } 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{}, + records: map[string]nodeengine.NodeRecord{}, inOwn: map[string]bool{}, modules: map[string]delivery.Module{}, values: map[string]map[string]any{}, manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]delivery.BuilderBuild{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{}, pkgs: map[string]compositions.Composed{}, asked: make(chan chan Pass, 8)} } @@ -357,8 +360,8 @@ func (c *Composer) inputs() []input { // Every node's record, and this node's own haves (what it runs inside its own composition): two watches, each // within the node's grant (buscompose: readGrants of the have bucket at `*.node`, NodeKeysGrants at `.>`). // A watch of every key is refused by the bus (issue 1126, 2026-10-27 09:21: the fold's `>` stopped every agent). - {delivery.HaveBucket, "*." + delivery.HaveNodeWord, nil, (*Composer).takeRecordChanged}, - {delivery.HaveBucket, node + ".>", nil, (*Composer).takeModuleHave}, + {buckets.Have, "*." + nodeengineclient.NodeWord, nil, (*Composer).takeRecordChanged}, + {buckets.Have, node + ".>", nil, (*Composer).takeModuleHave}, {delivery.ModulesBucket, ">", (*Composer).takeModule, nil}, {ResolvedBucket, node + ".>", (*Composer).takeValue, nil}, // The builder's builds: a pass that held a module for a build not yet done is made again when it lands (issue @@ -690,13 +693,13 @@ func (c *Composer) takePlacement(u agent.Update) { } func (c *Composer) takeRecord(u agent.Update) { - node, _, isNode, err := delivery.ParseHaveKey(u.Key) + node, _, isNode, err := nodeengineclient.ParseHaveKey(u.Key) if err != nil || !isNode { return } c.mu.Lock() defer c.mu.Unlock() - h, err := delivery.DecodeHaveNode(u.Value) + h, err := nodeengineclient.DecodeHaveNode(u.Value) if u.Deleted || err != nil || h.Record == nil { delete(c.records, node) return @@ -715,13 +718,13 @@ func (c *Composer) takeRecordChanged(u agent.Update) bool { // id; mesh-wireguard: none). Read from the haves, which outlast a restart, never from what the agent has applied // this run: a fresh agent composes before it applies anything (review of the fold, round 1). func (c *Composer) takeModuleHave(u agent.Update) bool { - node, module, isNode, err := delivery.ParseHaveKey(u.Key) + node, module, isNode, err := nodeengineclient.ParseHaveKey(u.Key) if err != nil || isNode || node != c.env.Node { return false } in := false if !u.Deleted { - if h, err := delivery.DecodeHave(u.Value); err == nil { + if h, err := nodeengineclient.DecodeHave(u.Value); err == nil { // A have that names nothing it runs, failed (the node could not compose a module it never ran: NotComposed), is // no module run inside the node's own composition: taken as one, it was composed from its manifest past its // build (issue 1126, review round 1 of the builds). @@ -729,9 +732,9 @@ func (c *Composer) takeModuleHave(u agent.Update) bool { // mesh-host #189, novox's postgres 2026-10-31): taken as one, it was composed from its manifest and refused // for its secret reader before the start backed it up again. Any other have naming nothing keeps the rule // (waiting for a yes inside the node's own stays in). - interrupted := h.Phase == delivery.PhaseHeld && h.Detail == agent.BackupInterrupted - runsNothing := (h.Phase == delivery.PhaseFailed || interrupted) && h.FP == "" && h.Composed == "" - in = h.Phase != delivery.PhaseRemoved && !runsNothing && !strings.HasPrefix(h.FP, delivery.SourcePrefix2) + interrupted := h.Phase == nodeengineclient.PhaseHeld && h.Detail == agent.BackupInterrupted + runsNothing := (h.Phase == nodeengineclient.PhaseFailed || interrupted) && h.Fp == "" && h.Composed == "" + in = h.Phase != nodeengineclient.PhaseRemoved && !runsNothing && !strings.HasPrefix(h.Fp, delivery.SourcePrefix2) } } c.mu.Lock() @@ -749,7 +752,7 @@ func (c *Composer) takeModuleHave(u agent.Update) bool { // inOwnOf is, of the modules p places, those the node runs inside its own composition (c.inOwn), sorted; c.mu held. // Machines is, from the placements this composer holds, each node by its address on the private network, and the hub's -// node: what names the machine a failing network part points at (delivery.NetworkPart.Targets, issue 774). Empty +// node: what names the machine a failing network part points at (nodeengine.NetworkPart.Targets, issue 774). Empty // before any placement is read. func (c *Composer) Machines() (byAddress map[string]string, hub string) { c.mu.Lock() @@ -1079,7 +1082,7 @@ func (c *Composer) refused(why string, held map[string]string) { c.refusal = said c.held = heldModules(held) if why != "" { - c.held = append(c.held, delivery.HaveNodeWord) + c.held = append(c.held, nodeengineclient.NodeWord) } c.mu.Unlock() if !same && c.env.Refused != nil { @@ -1374,10 +1377,10 @@ func (r reader) Placements(context.Context) (map[string]delivery.Placement, erro return out, nil } -func (r reader) Records(context.Context) (map[string]delivery.NodeRecord, error) { +func (r reader) Records(context.Context) (map[string]nodeengine.NodeRecord, error) { r.c.mu.Lock() defer r.c.mu.Unlock() - out := make(map[string]delivery.NodeRecord, len(r.c.records)) + out := make(map[string]nodeengine.NodeRecord, len(r.c.records)) for k, v := range r.c.records { out[k] = v } diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 7ae5ad3..9cada98 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -8,7 +8,9 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/buckets" "github.com/novox/mesh-host/internal/compositions" "os" "reflect" @@ -189,19 +191,19 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { omod, _ := delivery.EncodeModule(delivery.Module{FP2: delivery.SourcePrefix2 + strings.Repeat("c", 64), Manifest: odigest, Repository: "novox/o", Commit: "c"}) place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}}) - have, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) + have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}) want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true}) nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true}) b.put(t, delivery.PlacementBucket, "placement.n1", place) - b.put(t, delivery.HaveBucket, "n1.node", have) - oldHave, _ := delivery.EncodeHave(delivery.Have{Build: "build-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) - b.put(t, delivery.HaveBucket, "n1.oldie", oldHave) + b.put(t, buckets.Have, "n1.node", have) + oldHave, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: "build-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) + b.put(t, buckets.Have, "n1.oldie", oldHave) b.put(t, delivery.ModulesBucket, "lemurs", mod) b.put(t, delivery.ModulesBucket, "oldie", omod) b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want) b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant) - for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { b.send(bucket, agent.Update{Caught: true}) } got := map[string]bool{} @@ -696,10 +698,10 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { bkey, _ := delivery.BuilderBuildKey("tool", next2) tb.kv[delivery.BuilderBuildsBucket+"/"+bkey] = building tb.put(t, delivery.PlacementBucket, "placement.n1", tplace) - tb.put(t, delivery.HaveBucket, "n1.node", have) + tb.put(t, buckets.Have, "n1.node", have) tb.put(t, delivery.ModulesBucket, "tool", tmod) tb.put(t, delivery.AssignerWantsBucket, "want.n1.tool", twant) - for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, + for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { tb.send(bucket, agent.Update{Caught: true}) } @@ -1049,7 +1051,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } // What n1 runs: lemurs and its own composition, as its composer composes them from the trunk. running, _, _, err := frombus.Compose(ctx, fixedReader{placements: map[string]delivery.Placement{"n1": trunk}, - records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}, + records: map[string]nodeengine.NodeRecord{"n1": {Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}, modules: mods, manifests: map[string][]byte{digest: []byte(lemurs), kdigest: []byte(keeper)}}, frombus.Mesh{}, frombus.Scope{Node: "n1"}, map[string]string{"lemurs": fp}) if err != nil { t.Fatal(err) @@ -1085,10 +1087,10 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { time.Sleep(30 * time.Millisecond) sb.missing(delivery.PlacementCandidatesBucket, false) sb.made(6) - rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) + rec, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) tp, _ := delivery.EncodePlacement(trunk) sb.put(delivery.PlacementBucket, "placement.n1", tp, 1) - sb.put(delivery.HaveBucket, "n1.node", rec, 1) + sb.put(buckets.Have, "n1.node", rec, 1) for name, m := range mods { raw, _ := delivery.EncodeModule(m) sb.put(delivery.ModulesBucket, name, raw, 1) @@ -1201,7 +1203,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } } // The node's own key changing (its answers written) composes nothing again; a group's key deleted drops its answer. - sb.put(delivery.HaveBucket, "n1.node", rec, 2) + sb.put(buckets.Have, "n1.node", rec, 2) select { case a := <-answered: t.Errorf("answers handed again for nothing changed: %+v", a) @@ -1232,13 +1234,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { } } lemursHave := func(detail string, rev uint64) { - raw, err := delivery.EncodeHave(delivery.Have{FP: fp, Composed: lemursDigest, Phase: delivery.PhaseRunning, - Health: delivery.HealthFailed, Detail: detail}) + raw, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Composed: lemursDigest, Phase: nodeengineclient.PhaseRunning, + Health: nodeengineclient.HealthFailed, Detail: detail}) if err != nil { t.Fatal(err) } - hk, _ := delivery.HaveKey("n1", "lemurs") - sb.put(delivery.HaveBucket, hk, raw, rev) + hk, _ := nodeengineclient.HaveKey("n1", "lemurs") + sb.put(buckets.Have, hk, raw, rev) } lemursHave("its tool lemurs_health: no link to the bus is open", 10) drain() @@ -1273,9 +1275,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Errorf("a new revision's answer said %q", got) } settled = answering() - hk, _ := delivery.HaveKey("n1", "lemurs") - moved, _ := delivery.EncodeHave(delivery.Have{FP: fp, Composed: "sha256:" + strings.Repeat("e", 64), Phase: delivery.PhaseRunning}) - sb.put(delivery.HaveBucket, hk, moved, 13) + hk, _ := nodeengineclient.HaveKey("n1", "lemurs") + moved, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Composed: "sha256:" + strings.Repeat("e", 64), Phase: nodeengineclient.PhaseRunning}) + sb.put(buckets.Have, hk, moved, 13) drain() if got := answering(); got != settled+len(answers) { t.Errorf("what runs moved, and %d answer(s) were composed again for %d group(s)", got-settled, len(answers)) @@ -1350,7 +1352,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { go func() { _ = nsh.Run(nctx) }() nb.made(6) nb.put(delivery.PlacementBucket, "placement.n1", tp, 1) - nb.put(delivery.HaveBucket, "n1.node", rec, 1) + nb.put(buckets.Have, "n1.node", rec, 1) for name, m := range mods { raw, _ := delivery.EncodeModule(m) nb.put(delivery.ModulesBucket, name, raw, 1) @@ -1362,7 +1364,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { oldRaw, _ := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "older", Placement: &trunk}) oldKey, _ := delivery.PlacementCandidateKey("older", "n1") nb.put(delivery.PlacementCandidatesBucket, oldKey, oldRaw, 2) - for _, bucket := range []string{delivery.PlacementCandidatesBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{delivery.PlacementCandidatesBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, delivery.BuilderBuildsBucket} { nb.send(bucket, agent.Update{Caught: true}) } select { @@ -1508,7 +1510,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { cc.manifests[digest] = []byte(lemurs) cc.manifests[cdigest] = []byte(withCert) cc.modules = map[string]delivery.Module{"lemurs": {Manifest: digest}, "web": {Manifest: cdigest}} - cc.records["n1"] = delivery.NodeRecord{Node: "n1", ServingKey: base64.StdEncoding.EncodeToString(make([]byte, 32))} + cc.records["n1"] = nodeengine.NodeRecord{Node: "n1", ServingKey: base64.StdEncoding.EncodeToString(make([]byte, 32))} onlyLemurs := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "lemurs"}}} cc.certifyIfWanted(context.Background(), reader{c: cc}, onlyLemurs) if certAsks != 0 { @@ -1656,7 +1658,7 @@ func (b *patternBus) caught() { // fixedReader is frombus's Reader over values held, for composing what a node runs. type fixedReader struct { placements map[string]delivery.Placement - records map[string]delivery.NodeRecord + records map[string]nodeengine.NodeRecord modules map[string]delivery.Module manifests map[string][]byte } @@ -1664,7 +1666,7 @@ type fixedReader struct { func (r fixedReader) Placements(context.Context) (map[string]delivery.Placement, error) { return r.placements, nil } -func (r fixedReader) Records(context.Context) (map[string]delivery.NodeRecord, error) { +func (r fixedReader) Records(context.Context) (map[string]nodeengine.NodeRecord, error) { return r.records, nil } func (r fixedReader) Modules(context.Context) (map[string]delivery.Module, error) { @@ -1700,34 +1702,34 @@ func (r fixedReader) Served(context.Context, string, string, []string) (map[stri func TestAHaveSaysWhetherItsModuleRunsInsideTheNodesOwn(t *testing.T) { for _, row := range []struct { name string - have delivery.Have + have nodeengine.HaveByNodeModuleValue in bool // heldToday says the shadow counts the module held today (issue 1126: a seed holding it too holds it as today). heldToday bool }{ - {"failed, naming nothing it runs", delivery.Have{Phase: delivery.PhaseFailed, Health: delivery.HealthFailed, + {"failed, naming nothing it runs", nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseFailed, Health: nodeengineclient.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, false, true}, - {"failed at a build fingerprint", delivery.Have{FP: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseFailed, - Health: delivery.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, true, true}, + {"failed at a build fingerprint", nodeengine.HaveByNodeModuleValue{Fp: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: nodeengineclient.PhaseFailed, + Health: nodeengineclient.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, true, true}, // The live have of novox's de-spiegel (2026-10-28): a build of the old form, named by `build`, and no `fp`. - {"running a build of the old form", delivery.Have{Build: "build-1790368817085312877", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, true}, - {"running a build of the new form", delivery.Have{FP: delivery.SourcePrefix2 + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("c", 64), - Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, false, false}, - {"running with no fingerprint (mesh-wireguard)", delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, false}, - {"held today", delivery.Have{FP: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseHeld, - Health: delivery.HealthWaits, Detail: "held: a required config value is unset"}, true, true}, + {"running a build of the old form", nodeengine.HaveByNodeModuleValue{Build: "build-1790368817085312877", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, true, true}, + {"running a build of the new form", nodeengine.HaveByNodeModuleValue{Fp: delivery.SourcePrefix2 + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("c", 64), + Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, false, false}, + {"running with no fingerprint (mesh-wireguard)", nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, true, false}, + {"held today", nodeengine.HaveByNodeModuleValue{Fp: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: nodeengineclient.PhaseHeld, + Health: nodeengineclient.HealthWaits, Detail: "held: a required config value is unset"}, true, true}, // novox's postgres (2026-10-31), its have written before mesh-host #189: held by a backup the node agent's stop // interrupted and naming nothing; counted in, it was composed from its manifest and refused for its secret reader. - {"held by an interrupted backup, naming nothing", delivery.Have{Phase: delivery.PhaseHeld, Health: delivery.HealthWaits, + {"held by an interrupted backup, naming nothing", nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseHeld, Health: nodeengineclient.HealthWaits, Detail: agent.BackupInterrupted}, false, true}, } { t.Run(row.name, func(t *testing.T) { c := New(Env{Node: "n1"}) - key, err := delivery.HaveKey("n1", "web") + key, err := nodeengineclient.HaveKey("n1", "web") if err != nil { t.Fatal(err) } - value, err := delivery.EncodeHave(row.have) + value, err := nodeengineclient.EncodeHave(row.have) if err != nil { t.Fatal(err) } @@ -1783,16 +1785,16 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) { } place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []delivery.PlacedModule{{Module: "lemurs"}}}) - have, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) + have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}) want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true}) nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true}) b.put(t, delivery.PlacementBucket, "placement.n1", place) - b.put(t, delivery.HaveBucket, "n1.node", have) + b.put(t, buckets.Have, "n1.node", have) b.put(t, delivery.ModulesBucket, "lemurs", mod) b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want) b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant) - for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { + for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, buckets.Have, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} { b.send(bucket, agent.Update{Caught: true}) } handed := map[string]bool{} diff --git a/internal/selfcompose/shadow.go b/internal/selfcompose/shadow.go index 85a539d..c2d03b1 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -5,7 +5,9 @@ import ( "context" "encoding/json" "fmt" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/novox/mesh-host/internal/buckets" engcomp "github.com/novox/mesh-host/internal/compositions" "sort" "strings" @@ -160,11 +162,11 @@ func (s *Shadow) Run(ctx context.Context) error { ins := []input{ {delivery.PlacementCandidatesBucket, ">", nil, nil}, {delivery.PlacementBucket, ">", (*Composer).takePlacement, nil}, - {delivery.HaveBucket, "*." + delivery.HaveNodeWord, nil, nil}, + {buckets.Have, "*." + nodeengineclient.NodeWord, nil, nil}, {delivery.ModulesBucket, ">", (*Composer).takeModule, nil}, {ResolvedBucket, node + ".>", (*Composer).takeValue, nil}, // What this node runs moved: an apply ends in its module's have. - {delivery.HaveBucket, node + ".>", nil, nil}, + {buckets.Have, node + ".>", nil, nil}, } type tagged struct { in int @@ -315,7 +317,7 @@ func (s *Shadow) takeCandidate(u agent.Update) bool { // takeRecord takes a node's record, and says whether it changed: the node's own key changes with each answer written, // which moves nothing composed. func (s *Shadow) takeRecord(u agent.Update) bool { - node, _, isNode, err := delivery.ParseHaveKey(u.Key) + node, _, isNode, err := nodeengineclient.ParseHaveKey(u.Key) if err != nil || !isNode { return false } @@ -327,7 +329,7 @@ func (s *Shadow) takeRecord(u agent.Update) bool { c.mu.Lock() after, has := c.records[node] c.mu.Unlock() - return had != has || (has && !delivery.SameNodeRecord(before, after)) + return had != has || (has && !nodeengineclient.SameNodeRecord(before, after)) } // How long the shadow waits before it says it is not ready, and how long a proposal's answer runs before it is said slow. @@ -600,14 +602,14 @@ func (s *Shadow) heldToday() map[string]string { // takeRan keeps the digest one of this node's module haves says runs, and says whether it changed: a have whose health // or words alone moved runs the same. func (s *Shadow) takeRan(u agent.Update) bool { - node, module, isNode, err := delivery.ParseHaveKey(u.Key) + node, module, isNode, err := nodeengineclient.ParseHaveKey(u.Key) if err != nil || isNode || node != s.c.env.Node { return false } s.mu.Lock() defer s.mu.Unlock() before, had := s.ran[module] - h, err := delivery.DecodeHave(u.Value) + h, err := nodeengineclient.DecodeHave(u.Value) if s.heldNow == nil { s.heldNow = map[string]string{} } @@ -615,18 +617,18 @@ func (s *Shadow) takeRan(u agent.Update) bool { s.heldNow[module] = "" if !u.Deleted && err == nil { switch { - case h.Phase == delivery.PhaseHeld: + case h.Phase == nodeengineclient.PhaseHeld: s.heldNow[module] = "held" - case h.Phase == delivery.PhaseFailed: + case h.Phase == nodeengineclient.PhaseFailed: s.heldNow[module] = "failed" - case h.Phase != delivery.PhaseRemoved && h.FP == "" && h.Build != "": + case h.Phase != nodeengineclient.PhaseRemoved && h.Fp == "" && h.Build != "": // A build of the old form: its have names a `build` and no `fp`, which no registered build of the trunk // names, so the controller keeps it as it runs (issue 1126: novox's de-spiegel at build-…); a seed keeps it so. s.heldNow[module] = "runs a build of the old form" } } holdMoved := wasHeld != s.heldNow[module] // a hold's start or end changes the answer too - if u.Deleted || err != nil || h.Composed == "" || h.Phase == delivery.PhaseRemoved { + if u.Deleted || err != nil || h.Composed == "" || h.Phase == nodeengineclient.PhaseRemoved { delete(s.ran, module) return had || holdMoved } diff --git a/internal/userlist/userlist.go b/internal/userlist/userlist.go index b072aac..de4daa9 100644 --- a/internal/userlist/userlist.go +++ b/internal/userlist/userlist.go @@ -31,6 +31,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" engcomp "github.com/novox/mesh-host/internal/compositions" "regexp" "sort" @@ -49,7 +50,7 @@ const ResourceID = "bus-users" // Written is what the follower last did: the source it took the list from, the list's digest there, the file's digest // on the machine after the write, how many users the list holds, when, and why a write failed. -type Written = delivery.UserListWritten +type Written = nodeengine.UserListWritten // Env is everything the follower needs of the node. type Env struct { @@ -336,13 +337,13 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) { } moved := file.module != f.cur.module || pathOf(file.resource) != pathOf(f.cur.resource) f.cur = file - if !again && !moved && f.state.Source != "" && f.state.Revision == rev && f.written { + if !again && !moved && f.state.Source != "" && f.state.Revision == int64(rev) && f.written { f.mu.Unlock() return } f.mu.Unlock() - w := Written{Source: delivery.GrantsUserListSource, Revision: rev, Wanted: g.Digest, At: f.env.Now().UTC(), + w := Written{Source: delivery.GrantsUserListSource, Revision: int64(rev), Wanted: g.Digest, At: f.env.Now().UTC(), Follows: delivery.FollowGrants, Key: f.key(), Unwatched: f.unwatchedNow()} var err error var changed bool @@ -370,7 +371,7 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) { } } if err == nil { - w.Users = Users(body) + w.Users = int64(Users(body)) if err = f.guard(resource, body); err != nil { if disk, derr := f.env.OnDisk(resource); derr == nil && disk != nil { w.Digest = Digest(disk) diff --git a/internal/userlist/userlist_test.go b/internal/userlist/userlist_test.go index 5ca5b44..7aad01e 100644 --- a/internal/userlist/userlist_test.go +++ b/internal/userlist/userlist_test.go @@ -109,7 +109,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) { failWrite bool onDisk string wantWrites []string // contents written, in order - wantUsers int + wantUsers int64 wantErr string wantRefused bool grants []gput