Merge branch 'b6/registry' into feat/registry-milestone
This commit is contained in:
@@ -3,15 +3,15 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"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"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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.
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *owedBus) have(key string) (delivery.Have, bool) {
|
func (b *owedBus) have(key string) (nodeengine.HaveByNodeModuleValue, bool) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
h, err := delivery.DecodeHave(b.kv[key])
|
h, err := nodeengineclient.DecodeHave(b.kv[key])
|
||||||
return h, err == nil
|
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)}
|
fact := link.ModuleFact{Module: "xorg", FP: "src2:" + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("b", 64)}
|
||||||
applying := fact
|
applying := fact
|
||||||
applying.Phase = string(delivery.PhaseApplying)
|
applying.Phase = string(nodeengineclient.PhaseApplying)
|
||||||
queue.NoteModule(ctx, applying)
|
queue.NoteModule(ctx, applying)
|
||||||
go judgeWhatRuns(ctx, j, queue, func(string) {}, nil, nil)
|
go judgeWhatRuns(ctx, j, queue, func(string) {}, nil, nil)
|
||||||
// The facts look inside the apply, while the module is still applying.
|
// 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()}})
|
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)
|
t.Fatalf("judged while applying: %+v", h)
|
||||||
}
|
}
|
||||||
queue.NoteModule(ctx, fact)
|
queue.NoteModule(ctx, fact)
|
||||||
deadline := time.Now().Add(2 * time.Second) // well within one look (liveness.LookEvery, 15 s)
|
deadline := time.Now().Add(2 * time.Second) // well within one look (liveness.LookEvery, 15 s)
|
||||||
for {
|
for {
|
||||||
h, _ := bus.have("n1.xorg")
|
h, _ := bus.have("n1.xorg")
|
||||||
if h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK {
|
if h.Phase == nodeengineclient.PhaseRunning && h.Health == nodeengineclient.HealthOK {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
|
|||||||
@@ -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{}}
|
h.Network = &link.NetworkHealth{State: ns.State, Since: ns.Since.UTC(), Parts: []link.NetworkPart{}}
|
||||||
for _, p := range ns.Parts {
|
for _, p := range ns.Parts {
|
||||||
h.Network.Parts = append(h.Network.Parts, link.NetworkPart{Part: p.Part, State: p.State, Reason: p.Reason,
|
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
|
return h
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
"github.com/novox/mesh-host/internal/compositions"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
"os"
|
"os"
|
||||||
@@ -86,7 +87,7 @@ func (h sessionHaves) Read(ctx context.Context, module string) (agent.Have, bool
|
|||||||
if !ok {
|
if !ok {
|
||||||
return agent.Have{}, false
|
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
|
Sealed: got.SealedAt}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,24 +105,24 @@ func (h sessionHaves) ReadErr(ctx context.Context, module string) (agent.Have, b
|
|||||||
if !ok || err != nil {
|
if !ok || err != nil {
|
||||||
return agent.Have{}, false, err
|
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
|
Sealed: got.SealedAt}, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h sessionHaves) Write(ctx context.Context, x agent.Have) {
|
func (h sessionHaves) Write(ctx context.Context, x agent.Have) {
|
||||||
if x.Module == agent.NodeModule {
|
if x.Module == agent.NodeModule {
|
||||||
// The node's own key holds its facts and the digest of the composition it runs.
|
// 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)
|
h.session.NoteNode(ctx, x.Composed, x.Sealed)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
phase := x.Phase
|
phase := x.Phase
|
||||||
if phase == string(delivery.PhaseRunning) {
|
if phase == string(nodeengineclient.PhaseRunning) {
|
||||||
phase = ""
|
phase = ""
|
||||||
}
|
}
|
||||||
h.session.NoteModule(ctx, link.ModuleFact{Module: x.Module, FP: x.FP, Composed: x.Composed, Phase: 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})
|
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
|
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.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
|
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)
|
ag = agent.New(env)
|
||||||
own.set(ag)
|
own.set(ag)
|
||||||
current.set(ag)
|
current.set(ag)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
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"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -136,7 +137,7 @@ func newItBedAs(t *testing.T, perms nodegrant.Permissions) *itBed {
|
|||||||
into *nats.KeyValue
|
into *nats.KeyValue
|
||||||
history uint8
|
history uint8
|
||||||
}{{delivery.AssignerWantsBucket, &b.wants, 8}, {composedBucket, &b.composed, engcomp.History},
|
}{{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})
|
kv, err := b.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: c.name, History: c.history})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -200,13 +201,13 @@ func (b *itBed) file(module string) string {
|
|||||||
return string(got)
|
return string(got)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *itBed) have(module string) delivery.Have {
|
func (b *itBed) have(module string) nodeengine.HaveByNodeModuleValue {
|
||||||
key, _ := delivery.HaveKey(itNode, module)
|
key, _ := nec.HaveKey(itNode, module)
|
||||||
e, err := b.haves.Get(key)
|
e, err := b.haves.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return delivery.Have{}
|
return nodeengine.HaveByNodeModuleValue{}
|
||||||
}
|
}
|
||||||
h, _ := delivery.DecodeHave(e.Value())
|
h, _ := nec.DecodeHave(e.Value())
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,11 +294,11 @@ func h8once(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, place []
|
|||||||
if settled == 0 {
|
if settled == 0 {
|
||||||
t.Fatal("feat-x was never answered")
|
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"} {
|
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 := b.have("web")
|
||||||
h.Health, h.Detail = delivery.HealthFailed, detail
|
h.Health, h.Detail = nec.HealthFailed, detail
|
||||||
raw, err := delivery.EncodeHave(h)
|
raw, err := nec.EncodeHave(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -334,15 +335,15 @@ func h8once(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, place []
|
|||||||
// composes its own.
|
// composes its own.
|
||||||
func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValue, hkey string) {
|
func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValue, hkey string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
node := func() delivery.HaveNode {
|
node := func() nodeengine.HaveByNodeNodeValue {
|
||||||
e, err := b.haves.Get(hkey)
|
e, err := b.haves.Get(hkey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return delivery.HaveNode{}
|
return nodeengine.HaveByNodeNodeValue{}
|
||||||
}
|
}
|
||||||
n, _ := delivery.DecodeHaveNode(e.Value())
|
n, _ := nec.DecodeHaveNode(e.Value())
|
||||||
return n
|
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)
|
t.Errorf("a node composing its own says it composes %q", n.Composes)
|
||||||
}
|
}
|
||||||
craw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "revert-x"})
|
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()
|
b.mu.Lock()
|
||||||
said := strings.Join(b.said, "\n")
|
said := strings.Join(b.said, "\n")
|
||||||
b.mu.Unlock()
|
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)
|
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"))
|
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")
|
fp := fpIt("a")
|
||||||
modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)})
|
modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)})
|
||||||
place, _ := delivery.EncodePlacement(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
|
place, _ := 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.
|
// 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)
|
pkey, _ := delivery.PlacementNodeKey(itNode)
|
||||||
places, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
|
places, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "web"}}})
|
||||||
place = places[pkey]
|
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.
|
// 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}},` +
|
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}"}}}`
|
`"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)
|
mlkey, _ := delivery.ManifestsKey(mdigest)
|
||||||
mlmod, _ := delivery.EncodeModule(delivery.Module{FP2: fpIt("b"), Manifest: mdigest, Repository: "novox/mailer", Commit: strings.Repeat("2", 40)})
|
mlmod, _ := delivery.EncodeModule(delivery.Module{FP2: fpIt("b"), Manifest: mdigest, Repository: "novox/mailer", Commit: strings.Repeat("2", 40)})
|
||||||
place2, _ := delivery.EncodePlacement(delivery.Placement{Node: "it2", Modules: []delivery.PlacedModule{{Module: "mailer"}}})
|
place2, _ := 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")
|
pkey2, _ := delivery.LegacyPlacementKey("it2")
|
||||||
hkey2, _ := delivery.HaveNodeKey("it2")
|
hkey2, _ := nec.HaveNodeKey("it2")
|
||||||
buildKey, _ := delivery.BuilderBuildKey("web", fp)
|
buildKey, _ := delivery.BuilderBuildKey("web", fp)
|
||||||
done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest,
|
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")}}})
|
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()
|
t.Helper()
|
||||||
within(t, 2*time.Second, "the have does not name the build", func() bool {
|
within(t, 2*time.Second, "the have does not name the build", func() bool {
|
||||||
h := b.have("web")
|
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
|
// 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.
|
// 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, 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 {
|
within(t, 2*time.Second, "the have does not name the revision", func() bool {
|
||||||
h := b.have("web")
|
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
|
// 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 {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
n, err := delivery.DecodeHaveNode(e.Value())
|
n, err := nec.DecodeHaveNode(e.Value())
|
||||||
wire, _ := json.Marshal(n.Candidates["feat-x"])
|
wire, _ := json.Marshal(n.Candidates["feat-x"])
|
||||||
_ = json.Unmarshal(wire, &answer)
|
_ = json.Unmarshal(wire, &answer)
|
||||||
return err == nil && answer.Proposal == "feat-x" && n.Record != nil
|
return err == nil && answer.Proposal == "feat-x" && n.Record != nil
|
||||||
@@ -787,13 +788,13 @@ func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *test
|
|||||||
t.Fatal(err)
|
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"}}})
|
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"}}})
|
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.
|
// The have the engine wrote of the folded bus: the old-form build, no fingerprint.
|
||||||
natsHave, _ := delivery.HaveKey(itNode, "nats")
|
natsHave, _ := nec.HaveKey(itNode, "nats")
|
||||||
oldHave, _ := delivery.EncodeHave(delivery.Have{Build: "build-1792674595610176744", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
|
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} {
|
for key, value := range map[string][]byte{hkey: rec, natsHave: oldHave} {
|
||||||
if _, err := b.haves.Put(key, value); err != nil {
|
if _, err := b.haves.Put(key, value); err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
within(t, 10*time.Second, "the bus module's first want was not applied", func() bool {
|
||||||
h := b.have("nats")
|
h := b.have("nats")
|
||||||
got, _ := os.ReadFile(conf)
|
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 {
|
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)
|
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})
|
busWant(delivery.Want{FP: next, Self: true})
|
||||||
within(t, 10*time.Second, "the build that changes the server was not applied", func() bool {
|
within(t, 10*time.Second, "the build that changes the server was not applied", func() bool {
|
||||||
h := b.have("nats")
|
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})
|
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
|
var runs int
|
||||||
for _, a := range runtime.moved(server) {
|
for _, a := range runtime.moved(server) {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -193,7 +195,7 @@ func placedAllHave(ctx context.Context, session *link.Session, node string) (boo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err.Error()
|
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)
|
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
|
// 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
|
// 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).
|
// 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 {
|
if len(p.Modules) == 0 {
|
||||||
return false, "the seat mesh-module-assigner places no module here"
|
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
|
// 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.
|
// 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"
|
return false, m.Module + " runs at a build of the old form (" + h.Build + "), which only the kept declaration records"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"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"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -120,24 +122,24 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
|
|||||||
placed := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: "lemurs"}}}
|
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
|
// 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.
|
// 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}
|
newForm := nodeengine.HaveByNodeModuleValue{Fp: delivery.SourcePrefix2 + strings.Repeat("a", 64), Phase: nodeengineclient.PhaseRunning}
|
||||||
oldForm := delivery.Have{Build: "build-1791404191702719760", Phase: delivery.PhaseRunning}
|
oldForm := nodeengine.HaveByNodeModuleValue{Build: "build-1791404191702719760", Phase: nodeengineclient.PhaseRunning}
|
||||||
for _, row := range []struct {
|
for _, row := range []struct {
|
||||||
name string
|
name string
|
||||||
has map[string]delivery.Have
|
has map[string]nodeengine.HaveByNodeModuleValue
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{"every placed module has a have of the new form", map[string]delivery.Have{"zsh": newForm, "lemurs": newForm}, true},
|
{"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]delivery.Have{"zsh": newForm}, false},
|
{"one has none yet", map[string]nodeengine.HaveByNodeModuleValue{"zsh": newForm}, false},
|
||||||
{"one runs at a build of the old form", map[string]delivery.Have{"zsh": newForm, "lemurs": oldForm}, 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)
|
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).
|
// The module the mesh provides never has a fingerprint, and keeps no read (review of issue 774).
|
||||||
withWireguard := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: overlay.Name}}}
|
withWireguard := 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 {
|
if m == overlay.Name {
|
||||||
return oldForm, true, nil
|
return oldForm, true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-58
@@ -17,6 +17,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"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"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -61,18 +63,18 @@ type Have struct {
|
|||||||
// waits for a person's yes (ADR 0547 §3).
|
// waits for a person's yes (ADR 0547 §3).
|
||||||
Judgement string
|
Judgement string
|
||||||
Waits 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
|
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
|
// 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
|
Kept []string
|
||||||
// Sealed is, on a node composing its own, each value the composition it names carries sealed to the node's key, by
|
// 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).
|
// its own slot alone (issue 1126, C1).
|
||||||
Sealed compositions.Seals
|
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
|
// 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.
|
// ENG-9, nodeengine.HaveByNodeModuleValue.PullWait): read through the have, as node_haves shows it.
|
||||||
PullWait *delivery.PullWait
|
PullWait *nodeengine.PullWait
|
||||||
}
|
}
|
||||||
|
|
||||||
// Haves is the node's have bucket as the agent uses it.
|
// Haves is the node's have bucket as the agent uses it.
|
||||||
@@ -220,7 +222,7 @@ type Agent struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
wants map[string]wantNow // by module
|
wants map[string]wantNow // by module
|
||||||
comps map[string]compNow // by composed key
|
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
|
// 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.
|
// not the key's latest value, and what it holds is still held out of the others' applies.
|
||||||
applied map[string]compNow
|
applied map[string]compNow
|
||||||
@@ -303,7 +305,7 @@ func New(env Env) *Agent {
|
|||||||
if env.Say == nil {
|
if env.Say == nil {
|
||||||
env.Say = func(string) {}
|
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{},
|
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{},
|
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),
|
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.
|
// composerMark says h is a mark NotComposed wrote: failed, its detail opening with NotComposedDetail.
|
||||||
func composerMark(h Have) bool {
|
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
|
// 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()
|
a.mu.Unlock()
|
||||||
back := mark.prior
|
back := mark.prior
|
||||||
if ran {
|
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 != "" {
|
if r.Phase != "" {
|
||||||
back.Phase = string(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 !marked {
|
||||||
if ran {
|
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 == "" {
|
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 {
|
} 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
|
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
|
// 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
|
// composer's mark over it would hide that cause, and baseline would take the mark for what runs (review of the
|
||||||
// switch, round 1).
|
// 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.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",
|
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))
|
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.notComposed[module] = mark
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
a.writeHave(ctx, Have{Module: module, FP: mark.prior.FP, Composed: mark.prior.Composed, Sealed: mark.prior.Sealed,
|
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()
|
a.mu.Unlock()
|
||||||
slices.Sort(changed)
|
slices.Sort(changed)
|
||||||
for _, m := range 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
|
h.Module = m
|
||||||
a.writeHave(ctx, h)
|
a.writeHave(ctx, h)
|
||||||
}
|
}
|
||||||
@@ -733,10 +735,10 @@ func (a *Agent) Running() []compositions.Composed {
|
|||||||
var out []compositions.Composed
|
var out []compositions.Composed
|
||||||
for _, m := range mods {
|
for _, m := range mods {
|
||||||
h := a.running[m]
|
h := a.running[m]
|
||||||
if h.Phase == delivery.PhaseRemoved {
|
if h.Phase == nodeengineclient.PhaseRemoved {
|
||||||
continue
|
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)
|
out = append(out, c.C)
|
||||||
} else if c, ok := a.applied[m]; ok && c.Dig == h.Composed {
|
} else if c, ok := a.applied[m]; ok && c.Dig == h.Composed {
|
||||||
out = append(out, c.C)
|
out = append(out, c.C)
|
||||||
@@ -890,10 +892,10 @@ func (a *Agent) others(except string) []compositions.Composed {
|
|||||||
sort.Strings(mods)
|
sort.Strings(mods)
|
||||||
for _, m := range mods {
|
for _, m := range mods {
|
||||||
h := a.running[m]
|
h := a.running[m]
|
||||||
if h.Phase == delivery.PhaseRemoved {
|
if h.Phase == nodeengineclient.PhaseRemoved {
|
||||||
continue
|
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)
|
out = append(out, c.C)
|
||||||
} else if c, ok := a.applied[m]; ok && c.Dig == h.Composed {
|
} else if c, ok := a.applied[m]; ok && c.Dig == h.Composed {
|
||||||
out = append(out, c.C)
|
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
|
// 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).
|
// composition not applied, or not yet (issue 924, review of 031e106).
|
||||||
func haveRuns(h Have) bool {
|
func haveRuns(h Have) bool {
|
||||||
switch delivery.Phase(h.Phase) {
|
switch string(h.Phase) {
|
||||||
case "", delivery.PhaseRunning, delivery.PhaseHeld:
|
case "", nodeengineclient.PhaseRunning, nodeengineclient.PhaseHeld:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return composerMark(h)
|
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] {
|
if err != nil || isNode || c.Err != nil || m == except || held[m] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if h, ok := a.running[m]; ok && h.Phase != delivery.PhaseRemoved {
|
if h, ok := a.running[m]; ok && h.Phase != nodeengineclient.PhaseRemoved {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if w := a.wants[m]; w.Present && w.Want.Removed {
|
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)
|
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:
|
case tried && att.Held:
|
||||||
return fmt.Errorf("%s is held for a failed backup: the operator's answer releases it", module)
|
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)
|
return fmt.Errorf("%s runs what its want names, nothing to retry", module)
|
||||||
case !tried:
|
case !tried:
|
||||||
return fmt.Errorf("%s has no failed apply to try again: it waits for its composition, or settles on its own", module)
|
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()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
h, ok := a.running[NodeModule]
|
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) {
|
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()
|
a.mu.Lock()
|
||||||
r, runs := a.running[module]
|
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]
|
att, tried := a.attempt[module]
|
||||||
same := tried && att.FP == w.Want.FP && att.Composed == w.Want.Composed
|
same := tried && att.FP == w.Want.FP && att.Composed == w.Want.Composed
|
||||||
if same && att.Held {
|
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.mu.Lock()
|
||||||
a.prior[module] = true
|
a.prior[module] = true
|
||||||
a.mu.Unlock()
|
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
|
// 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
|
// 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.
|
// 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()
|
a.mu.Unlock()
|
||||||
return false
|
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
|
// 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
|
// 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:
|
// 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()
|
a.mu.Unlock()
|
||||||
return false
|
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
|
// 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
|
// 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).
|
// 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,
|
a.attempt[module] = attempt{FP: w.Want.FP, Composed: w.Want.Composed, WantRev: w.Rev, CompRev: comp.Rev, Held: true,
|
||||||
AnsweredRev: w.Rev}
|
AnsweredRev: w.Rev}
|
||||||
if h.Composed != "" {
|
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.mu.Unlock()
|
||||||
a.runsChanged(module + " was taken as held")
|
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 = ""
|
h.Detail = ""
|
||||||
}
|
}
|
||||||
if h.FP == w.Want.FP && h.Composed == w.Want.Composed &&
|
if h.FP == w.Want.FP && h.Composed == w.Want.Composed &&
|
||||||
(h.Phase == "" || h.Phase == string(delivery.PhaseRunning) || marked) {
|
(h.Phase == "" || h.Phase == string(nodeengineclient.PhaseRunning) || marked) {
|
||||||
if a.inactiveAtStart(ctx, module, comp) {
|
if a.inactiveAtStart(ctx, module, comp) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
a.mu.Lock()
|
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.mu.Unlock()
|
||||||
a.runsChanged(module + " was taken from its have")
|
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,
|
// 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).
|
// 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,
|
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 true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -1529,10 +1531,10 @@ func (a *Agent) baseline(ctx context.Context, module string, w wantNow, comp com
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
a.mu.Lock()
|
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.mu.Unlock()
|
||||||
a.runsChanged(module + " was taken from the kept declaration")
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1567,10 +1569,10 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
|
|||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
if !runs {
|
if !runs {
|
||||||
if h, found, _ := a.readHave(ctx, module); found {
|
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 {
|
if got := a.env.Backup(ctx, module); !got.Done {
|
||||||
words := got.Words
|
words := got.Words
|
||||||
if got.Interrupted || ctx.Err() != nil {
|
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.attempt[module] = attempt{FP: fp, Composed: dig, WantRev: w.Rev, CompRev: comp.Rev, Held: true, AnsweredRev: w.Rev}
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
// It keeps running what it has: the have says what it holds and why it waits.
|
// 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})
|
Detail: words})
|
||||||
return
|
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)
|
res := a.env.Apply(ctx, comp.C, others)
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
if !res.Failed || res.Transient {
|
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
|
gen := a.gen
|
||||||
a.attempt[module] = attempt{FP: fp, Composed: dig, WantRev: w.Rev, CompRev: comp.Rev, Transient: true, Gen: gen}
|
a.attempt[module] = attempt{FP: fp, Composed: dig, WantRev: w.Rev, CompRev: comp.Rev, Transient: true, Gen: gen}
|
||||||
a.mu.Unlock()
|
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 {
|
if a.env.Backoff != nil {
|
||||||
wait := a.env.Backoff(try)
|
wait := a.env.Backoff(try)
|
||||||
when = "in " + wait.String()
|
when = "in " + wait.String()
|
||||||
@@ -1622,7 +1624,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if try > transientFor {
|
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.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),
|
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)
|
wait := a.pullWaitOf(module)
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
a.env.Say(fmt.Sprintf("applying %s failed: %s", what, res.Detail))
|
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})
|
PullWait: wait})
|
||||||
if again != "" {
|
if again != "" {
|
||||||
a.askPullAgain(module, 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.pulls, module)
|
||||||
delete(a.attempt, module)
|
delete(a.attempt, module)
|
||||||
delete(a.notComposed, module) // the apply says what it runs now
|
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.applied[module] = comp
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
a.keepApplied(module, comp)
|
a.keepApplied(module, comp)
|
||||||
if a.env.Settled != nil {
|
if a.env.Settled != nil {
|
||||||
a.env.Settled(module)
|
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) {
|
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()
|
a.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ran && r.Phase == delivery.PhaseRemoved {
|
if ran && r.Phase == nodeengineclient.PhaseRemoved {
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1685,7 +1687,7 @@ func (a *Agent) remove(ctx context.Context, module string, w wantNow) {
|
|||||||
delete(a.attempt, module)
|
delete(a.attempt, module)
|
||||||
delete(a.pulls, module)
|
delete(a.pulls, module)
|
||||||
delete(a.had, module)
|
delete(a.had, module)
|
||||||
a.running[module] = delivery.Have{Phase: delivery.PhaseRemoved}
|
a.running[module] = nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseRemoved}
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
if a.env.Applied != nil {
|
if a.env.Applied != nil {
|
||||||
if err := a.env.Applied.Drop(module); err != 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.readKeptData(ctx, module) // the manifest may read now though it did not at the start
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
r, ran := a.running[module]
|
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()
|
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
|
// 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).
|
// 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).
|
// Its sealed values only when they are of what runs; else writeHave reads them (sealedOf).
|
||||||
var sealed compositions.Seals
|
var sealed compositions.Seals
|
||||||
if h.Composed == r.Composed {
|
if h.Composed == r.Composed {
|
||||||
sealed = h.Sealed
|
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
|
return false
|
||||||
}
|
}
|
||||||
@@ -1799,12 +1801,12 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
|
|||||||
case known:
|
case known:
|
||||||
j = judge.JudgeMove(&from, comp.C)
|
j = judge.JudgeMove(&from, comp.C)
|
||||||
case prior:
|
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:
|
default:
|
||||||
j = judge.JudgeMove(nil, comp.C)
|
j = judge.JudgeMove(nil, comp.C)
|
||||||
}
|
}
|
||||||
switch j.Judgement {
|
switch j.Judgement {
|
||||||
case delivery.JudgedRecreates, delivery.JudgedMovesData, delivery.JudgedCannot:
|
case nodeengineclient.JudgedRecreates, nodeengineclient.JudgedMovesData, nodeengineclient.JudgedCannot:
|
||||||
default:
|
default:
|
||||||
return false
|
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))
|
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]
|
prev := a.running[module]
|
||||||
a.mu.Unlock()
|
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)})
|
Detail: j.Words, Judgement: string(j.Judgement), Waits: comp.Dig, BuildAlone: a.buildAlone(ctx, module, prev, w.Want.FP)})
|
||||||
return true
|
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
|
// 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
|
// 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.
|
// 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 {
|
func (a *Agent) buildAlone(ctx context.Context, module string, prev nodeengine.HaveByNodeModuleValue, want string) bool {
|
||||||
if prev.FP == "" && prev.Composed == "" {
|
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.
|
// 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 {
|
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
|
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
|
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
|
// 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.
|
// 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]
|
pw := a.pulls[module]
|
||||||
if pw == nil {
|
if pw == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &delivery.PullWait{Image: pw.Image, Seat: pw.Seat, Holder: pw.Holder, Began: pw.Started, Retried: pw.last,
|
out := &nodeengine.PullWait{Image: pw.Image, Seat: pw.Seat, Holder: pw.Holder, Began: pw.Started, Retried: pw.last}
|
||||||
RetriedAt: pw.lastAt}
|
if !pw.lastAt.IsZero() {
|
||||||
|
at := pw.lastAt
|
||||||
|
out.RetriedAt = &at
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"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"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"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 module was not applied", func() bool { return len(b.appliedList()) == 1 })
|
||||||
eventually(t, "the have does not name both", func() bool {
|
eventually(t, "the have does not name both", func() bool {
|
||||||
h := b.haves.last("web")
|
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;
|
// 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).
|
// ace, 2026-10-25 21:17).
|
||||||
@@ -337,7 +339,7 @@ func TestStartingOnWhatTheKeptDeclarationHoldsAppliesNothing(t *testing.T) {
|
|||||||
stop := b.run()
|
stop := b.run()
|
||||||
defer stop()
|
defer stop()
|
||||||
eventually(t, "the have was not written", func() bool { return b.haves.last("web").Composed == dig })
|
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)
|
t.Fatalf("the have: %+v", h)
|
||||||
}
|
}
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
@@ -357,7 +359,7 @@ func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) {
|
|||||||
fp := fpOf("a")
|
fp := fpOf("a")
|
||||||
ck, craw, dig := composition(t, "avahi", fp, false, file("avahi", "x", "1"))
|
ck, craw, dig := composition(t, "avahi", fp, false, file("avahi", "x", "1"))
|
||||||
wk, wraw := wantFor(t, "avahi", fp, dig, "")
|
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"}}
|
Detail: "apply: unit failed to start"}}
|
||||||
b.comps.before(ck, craw)
|
b.comps.before(ck, craw)
|
||||||
b.wants.before(wk, wraw)
|
b.wants.before(wk, wraw)
|
||||||
@@ -372,7 +374,7 @@ func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) {
|
|||||||
fp := fpOf("a")
|
fp := fpOf("a")
|
||||||
ck, craw, dig := composition(t, "avahi", fp, false, file("avahi", "x", "1"))
|
ck, craw, dig := composition(t, "avahi", fp, false, file("avahi", "x", "1"))
|
||||||
wk, wraw := wantFor(t, "avahi", fp, dig, "")
|
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"}}
|
Detail: NotComposedDetail + "frombus: the credentials of ace: context canceled"}}
|
||||||
b.comps.before(ck, craw)
|
b.comps.before(ck, craw)
|
||||||
b.wants.before(wk, wraw)
|
b.wants.before(wk, wraw)
|
||||||
@@ -380,7 +382,7 @@ func TestStartingOnItsOwnHaveAppliesNothing(t *testing.T) {
|
|||||||
defer stop()
|
defer stop()
|
||||||
eventually(t, "the mark was not cleared", func() bool {
|
eventually(t, "the mark was not cleared", func() bool {
|
||||||
h := b.haves.last("avahi")
|
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 {
|
if got := b.appliedList(); len(got) != 0 {
|
||||||
t.Fatalf("a module whose mark names what the want names was applied: %v", got)
|
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 {
|
if len(log) != 1 {
|
||||||
t.Fatalf("the have was said %d times, want once: %+v", len(log), log)
|
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)
|
t.Fatalf("the have said again: %+v", h)
|
||||||
}
|
}
|
||||||
if got := b.appliedList(); len(got) != 0 {
|
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, "not applied", func() bool { return len(b.appliedList()) == 1 })
|
||||||
eventually(t, "the have does not say failed", func() bool {
|
eventually(t, "the have does not say failed", func() bool {
|
||||||
h := b.haves.last("web")
|
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.
|
// An unrelated key changes: not a retry.
|
||||||
ck9, craw9, _ := composition(t, "web", fpOf("9"), false, file("web", "x", "9"))
|
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)
|
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, "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"} {
|
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) {
|
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)
|
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, "not applied", func() bool { return len(b.appliedList()) == 1 })
|
||||||
eventually(t, "the have does not say whom it waits on", func() bool {
|
eventually(t, "the have does not say whom it waits on", func() bool {
|
||||||
h := b.haves.last("web")
|
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()
|
b.idle()
|
||||||
if n := len(b.appliedList()); n != 1 {
|
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, "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 {
|
eventually(t, "the have does not say what triggered the retry", func() bool {
|
||||||
h := b.haves.last("web")
|
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()
|
b.idle()
|
||||||
if n := len(b.appliedList()); n != 2 {
|
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
|
// 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.
|
// 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" ||
|
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)
|
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.
|
// 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()
|
tick()
|
||||||
b.ag.Settled("distribution")
|
b.ag.Settled("distribution")
|
||||||
eventually(t, "not tried again on the next settled", func() bool { return len(b.appliedList()) == 3 })
|
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 {
|
if w := b.haves.last("web").PullWait; w != nil {
|
||||||
t.Fatalf("a running module's have still waits on %+v", w)
|
t.Fatalf("a running module's have still waits on %+v", w)
|
||||||
}
|
}
|
||||||
@@ -620,11 +622,11 @@ func TestAFailedApplyIsRetriedOnlyByAChangeOfEitherKey(t *testing.T) {
|
|||||||
b.ag.RetryTransient()
|
b.ag.RetryTransient()
|
||||||
}
|
}
|
||||||
eventually(t, "not applied again without a new revision", func() bool { return len(b.appliedList()) == 2 })
|
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()
|
b.haves.mu.Lock()
|
||||||
defer b.haves.mu.Unlock()
|
defer b.haves.mu.Unlock()
|
||||||
for _, h := range b.haves.log {
|
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)
|
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 {
|
eventually(t, "the have does not say held", func() bool {
|
||||||
h := b.haves.last("db")
|
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 {
|
if got := b.appliedList(); len(got) != 0 {
|
||||||
t.Fatalf("something was applied behind a failed backup: %v", got)
|
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 {
|
eventually(t, "not running at the new fingerprint", func() bool {
|
||||||
h := b.haves.last("db")
|
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
|
// 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)
|
ib.comps.before(ck2, craw2)
|
||||||
iwk, iwraw := wantFor(t, "db", fp2, dig2, delivery.BackupAgain)
|
iwk, iwraw := wantFor(t, "db", fp2, dig2, delivery.BackupAgain)
|
||||||
ib.wants.before(iwk, iwraw)
|
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{})
|
began := make(chan struct{})
|
||||||
ib.env.Backup = func(ctx context.Context, _ string) Backup {
|
ib.env.Backup = func(ctx context.Context, _ string) Backup {
|
||||||
close(began)
|
close(began)
|
||||||
@@ -723,7 +725,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
|||||||
stopI := ib.run()
|
stopI := ib.run()
|
||||||
<-began
|
<-began
|
||||||
stopI()
|
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)
|
t.Errorf("held after an interrupted backup: %+v", h)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,24 +735,24 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
|||||||
for _, row := range []struct {
|
for _, row := range []struct {
|
||||||
detail, backup string
|
detail, backup string
|
||||||
backups, applied int
|
backups, applied int
|
||||||
phase delivery.Phase
|
phase string
|
||||||
namesNothing bool // the have names no fp and no composition
|
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.
|
// 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,
|
// 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.
|
// 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.BackupAgain, 1, 1, nodeengineclient.PhaseRunning, false},
|
||||||
{"restic: the repository is locked", delivery.BackupSkip, 0, 1, delivery.PhaseRunning, false},
|
{"restic: the repository is locked", delivery.BackupSkip, 0, 1, nodeengineclient.PhaseRunning, false},
|
||||||
{"restic: the repository is locked", "", 0, 0, delivery.PhaseHeld, false},
|
{"restic: the repository is locked", "", 0, 0, nodeengineclient.PhaseHeld, false},
|
||||||
} {
|
} {
|
||||||
r := newBed(t)
|
r := newBed(t)
|
||||||
r.comps.before(ck1, craw1)
|
r.comps.before(ck1, craw1)
|
||||||
r.comps.before(ck2, craw2)
|
r.comps.before(ck2, craw2)
|
||||||
wk, wraw := wantFor(t, "db", fp2, dig2, row.backup)
|
wk, wraw := wantFor(t, "db", fp2, dig2, row.backup)
|
||||||
r.wants.before(wk, wraw)
|
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 {
|
if row.namesNothing {
|
||||||
held.FP, held.Composed = "", ""
|
held.FP, held.Composed = "", ""
|
||||||
}
|
}
|
||||||
@@ -800,7 +802,7 @@ func TestADataModuleIsBackedUpFirstAndHeldWhenItFails(t *testing.T) {
|
|||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
r.wants.before(wk, selfWant(row.atStart))
|
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"}}
|
Detail: "the backup failed: sh: exit status 1"}}
|
||||||
stopR := r.run()
|
stopR := r.run()
|
||||||
defer stopR()
|
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, "the answer was not acted on", func() bool { return len(r.appliedList()) == 1 })
|
||||||
eventually(t, "not running after the answer", func() bool {
|
eventually(t, "not running after the answer", func() bool {
|
||||||
h := r.haves.last("nats")
|
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()
|
r.mu.Lock()
|
||||||
n := len(r.backups)
|
n := len(r.backups)
|
||||||
@@ -959,9 +961,9 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
|
|||||||
if got := a.Everything(); len(got) != 2 {
|
if got := a.Everything(); len(got) != 2 {
|
||||||
t.Fatalf("before the agent holds anything: %d compositions, want the 2 kept", len(got))
|
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"))
|
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})
|
a.takeComposition(Update{Key: ck, Value: craw, Revision: 1})
|
||||||
got := a.Everything()
|
got := a.Everything()
|
||||||
if len(got) != 2 {
|
if len(got) != 2 {
|
||||||
@@ -998,7 +1000,7 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
|
|||||||
webDig, _ := compositions.DigestOf(web)
|
webDig, _ := compositions.DigestOf(web)
|
||||||
store.kept[NodeModule], store.kept["web"] = ownRaw, webRaw
|
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: 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.
|
// The composer's first pass reads what runs when its watch begins, before the agent reads any have.
|
||||||
firstCh := make(chan []compositions.Composed, 1)
|
firstCh := make(chan []compositions.Composed, 1)
|
||||||
comps := b.comps
|
comps := b.comps
|
||||||
@@ -1143,7 +1145,7 @@ func TestAFailedApplyIsAppliedAgainAfterARestartWhateverIsKept(t *testing.T) {
|
|||||||
b.comps.before(ck, craw)
|
b.comps.before(ck, craw)
|
||||||
b.wants.before(wk, wraw)
|
b.wants.before(wk, wraw)
|
||||||
b.kept = map[string][]json.RawMessage{"web": {file("web", "x", "1")}}
|
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()
|
stop := b.run()
|
||||||
defer stop()
|
defer stop()
|
||||||
eventually(t, "a failed apply was marked running from the kept declaration", func() bool { return len(b.appliedList()) == 1 })
|
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},
|
{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},
|
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,
|
{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,
|
{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,
|
told: "the haves were read at the start", data: true,
|
||||||
holdsData: func(context.Context, string) (bool, bool) { return true, 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
|
// 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.
|
// 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)
|
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"}`}})
|
Sealed: compositions.Seals{"idle.key#sealed": `{"box":"idle"}`}})
|
||||||
want("idle", idleFP, "")
|
want("idle", idleFP, "")
|
||||||
eventually(t, "idle's want was not taken", func() bool {
|
eventually(t, "idle's want was not taken", func() bool {
|
||||||
@@ -1599,7 +1601,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
|||||||
module, fp, composed, slot, sealed string
|
module, fp, composed, slot, sealed string
|
||||||
}{{"web", fp, dig, "web.pw#sealed", "1"}, {"idle", idleFP, idleDig, "idle.key#sealed", `{"box":"idle"}`}} {
|
}{{"web", fp, dig, "web.pw#sealed", "1"}, {"idle", idleFP, idleDig, "idle.key#sealed", `{"box":"idle"}`}} {
|
||||||
h := b.haves.last(row.module)
|
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") {
|
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)
|
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
|
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.
|
// 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)
|
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"})
|
Detail: "apply: unit failed to start"})
|
||||||
want("broken", brokenFP, "")
|
want("broken", brokenFP, "")
|
||||||
eventually(t, "unread's want was not taken", func() bool {
|
eventually(t, "unread's want was not taken", func() bool {
|
||||||
@@ -1626,7 +1628,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
|||||||
if h := b.haves.last("unread"); h.Module != "" {
|
if h := b.haves.last("unread"); h.Module != "" {
|
||||||
t.Errorf("a module whose have could not be read was marked: %+v", h)
|
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)
|
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 {
|
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(), "")
|
b.ag.NotComposed(context.Background(), "")
|
||||||
for _, row := range []struct{ module, composed string }{{"web", dig}, {"idle", idleDig}} {
|
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)
|
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
|
// novox) is marked alone, naming its own words; web, which composes, keeps running, unmarked. Composed again, idle's
|
||||||
// have is what it runs.
|
// have is what it runs.
|
||||||
b.ag.NotComposedEach(context.Background(), "", map[string]string{"idle": "idle says ${config:name}, and nothing sets it"})
|
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)
|
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)
|
t.Errorf("web, which composes, is marked beside idle: %+v", h)
|
||||||
}
|
}
|
||||||
b.ag.NotComposedEach(context.Background(), "", nil)
|
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)
|
t.Errorf("idle's have once it composes again: %+v", h)
|
||||||
}
|
}
|
||||||
// A revision at the same build (a config value changed) is applied.
|
// A revision at the same build (a config value changed) is applied.
|
||||||
@@ -1682,7 +1684,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
|||||||
b.comps.put(dk, drawMoved)
|
b.comps.put(dk, drawMoved)
|
||||||
eventually(t, "db's revision did not wait for a yes", func() bool {
|
eventually(t, "db's revision did not wait for a yes", func() bool {
|
||||||
h := b.haves.last("db")
|
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 {
|
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)
|
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, draw)
|
||||||
keep(dk, drawMoved)
|
keep(dk, drawMoved)
|
||||||
view, err := b.ag.Composition(context.Background(), "db", "", true)
|
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 ||
|
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(delivery.JudgedRecreates) ||
|
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" ||
|
!view.Judged.WaitsForYes || len(view.Diff) != 1 || view.Diff[0].ID != "db.server" || view.Diff[0].Change != "changed" ||
|
||||||
len(view.Revisions) != 2 {
|
len(view.Revisions) != 2 {
|
||||||
t.Errorf("the composition of db waiting for a yes reads %+v (%v)", view, err)
|
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)
|
b.comps.put(dk, draw)
|
||||||
eventually(t, "db's have stays waiting-yes after its composition went back to what runs", func() bool {
|
eventually(t, "db's have stays waiting-yes after its composition went back to what runs", func() bool {
|
||||||
h := b.haves.last("db")
|
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 {
|
if got := len(b.appliedList()); got != 4 {
|
||||||
t.Fatalf("a composition equal to what runs was applied again: %d applies", got)
|
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
|
// 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).
|
// what runs (fingerprint, digest, sealed values) rather than naming nothing (review of L2, round 3).
|
||||||
recFP, recDig := fpOf("d"), "sha256:"+strings.Repeat("8", 64)
|
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"}`}})
|
Sealed: compositions.Seals{"rec.key#sealed": `{"box":"rec"}`}})
|
||||||
rk, rraw, rdig := unsigned("rec", fpB, container("rec@sha256:"+strings.Repeat("3", 64)))
|
rk, rraw, rdig := unsigned("rec", fpB, container("rec@sha256:"+strings.Repeat("3", 64)))
|
||||||
b.comps.put(dk2, draw2)
|
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}} {
|
}{{"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 {
|
eventually(t, row.module+"'s move did not wait for a yes", func() bool {
|
||||||
h := b.haves.last(row.module)
|
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)
|
h := b.haves.last(row.module)
|
||||||
if h.FP != row.fp || (row.composed != "" && h.Composed != row.composed) || h.Composed == "" ||
|
if h.FP != row.fp || (row.composed != "" && h.Composed != row.composed) || h.Composed == "" ||
|
||||||
@@ -1775,8 +1777,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
|||||||
want("rec", fpB, rdig)
|
want("rec", fpB, rdig)
|
||||||
eventually(t, "the yes did not apply the move", func() bool {
|
eventually(t, "the yes did not apply the move", func() bool {
|
||||||
h, r := b.haves.last("db"), b.haves.last("rec")
|
h, r := b.haves.last("db"), b.haves.last("rec")
|
||||||
return len(b.appliedList()) == applied+2 && h.Phase == string(delivery.PhaseRunning) && h.FP == fpB &&
|
return len(b.appliedList()) == applied+2 && h.Phase == string(nodeengineclient.PhaseRunning) && h.FP == fpB &&
|
||||||
r.Phase == string(delivery.PhaseRunning) && r.FP == fpB && r.Composed == rdig
|
r.Phase == string(nodeengineclient.PhaseRunning) && r.FP == fpB && r.Composed == rdig
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1816,7 +1818,7 @@ func testAnEngineStartReconcilesAModuleFromTheMachinesFacts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if c.held {
|
if c.held {
|
||||||
b.haves.stored = map[string]Have{"web": {Module: "web", FP: fpOf("0"), Composed: "sha256:" + strings.Repeat("0", 64),
|
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 {
|
if c.kept {
|
||||||
b.kept = map[string][]json.RawMessage{"web": rs}
|
b.kept = map[string][]json.RawMessage{"web": rs}
|
||||||
@@ -1867,7 +1869,7 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone(
|
|||||||
stop := b.run()
|
stop := b.run()
|
||||||
defer stop()
|
defer stop()
|
||||||
eventually(t, "web was not settled", func() bool { return b.haves.last("web").Composed == digW })
|
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 {
|
if got := b.appliedList(); len(got) != 0 {
|
||||||
t.Fatalf("applied at the start: %v", got)
|
t.Fatalf("applied at the start: %v", got)
|
||||||
}
|
}
|
||||||
@@ -1882,13 +1884,13 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone(
|
|||||||
if got := b.appliedList(); len(got) != 0 {
|
if got := b.appliedList(); len(got) != 0 {
|
||||||
t.Fatalf("a held module and an unwanted one were applied: %v", got)
|
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)
|
t.Fatalf("the held module's have changed: %+v", h)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.ag.Reconcile("web")
|
b.ag.Reconcile("web")
|
||||||
eventually(t, "web was not applied again", func() bool { return len(b.appliedList()) == 1 })
|
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.mu.Lock()
|
||||||
b.failWith = "systemctl start web.service: exit status 1"
|
b.failWith = "systemctl start web.service: exit status 1"
|
||||||
@@ -1896,7 +1898,7 @@ func testReconcileAppliesARunningModuleAgainAndLeavesTheHeldAndTheUnwantedAlone(
|
|||||||
b.ag.Reconcile("web")
|
b.ag.Reconcile("web")
|
||||||
eventually(t, "the failed re-apply did not write the have failed with its words", func() bool {
|
eventually(t, "the failed re-apply did not write the have failed with its words", func() bool {
|
||||||
h := b.haves.last("web")
|
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)
|
time.Sleep(50 * time.Millisecond)
|
||||||
if got := b.appliedList(); len(got) != 2 {
|
if got := b.appliedList(); len(got) != 2 {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
"github.com/novox/mesh-host/internal/judge"
|
"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
|
have = &h
|
||||||
phase := h.Phase
|
phase := h.Phase
|
||||||
if 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}
|
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
|
running = &from
|
||||||
rv := &RunsView{From: source, Resources: len(from.Resources), FP: from.FP}
|
rv := &RunsView{From: source, Resources: len(from.Resources), FP: from.FP}
|
||||||
if ran {
|
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
|
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}
|
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) {
|
if a.env.SelfComposed && a.env.Policy != nil && module != NodeModule && (!w.Present || w.Want.Yes != read.Dig) {
|
||||||
switch j.Judgement {
|
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
|
view.Judged.WaitsForYes = a.env.Policy(ctx, module) == catalogue.PolicyRecord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, `<node>.<module>`, and the node's own facts, `<node>.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
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package buckets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The engine's own states are named <module>_<state> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
"github.com/novox/mesh-host/internal/compositions"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
"maps"
|
"maps"
|
||||||
@@ -13,7 +14,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"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"
|
||||||
"github.com/novox/mesh-host/internal/compose/resolution"
|
"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 {
|
if len(failed) == 0 {
|
||||||
return refuse(err)
|
return refuse(err)
|
||||||
}
|
}
|
||||||
failed[delivery.HaveNodeWord] = err.Error()
|
failed[nodeengineclient.NodeWord] = err.Error()
|
||||||
composed, skipped, stood = nil, nil, nil
|
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
|
// 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.
|
// Nothing composed to compare: every refusal is said, and nothing else.
|
||||||
for _, m := range sortedKeys(failed) {
|
for _, m := range sortedKeys(failed) {
|
||||||
a.Refusals = append(a.Refusals, nodeengine.CandidateRefusal{Module: m, Why: failed[m]})
|
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
|
} // another node's addition: the module here that carries it differs, which the assigner judges
|
||||||
case module == "" || node != s.Node:
|
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.
|
// 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:
|
default:
|
||||||
refusals[module] = append(refusals[module], x)
|
refusals[module] = append(refusals[module], x)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"os"
|
"os"
|
||||||
@@ -42,7 +43,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
|||||||
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
|
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
|
||||||
}
|
}
|
||||||
b := &bus{placements: map[string]delivery.Placement{"n1": p},
|
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)}},
|
modules: map[string]delivery.Module{"lemurs": {Manifest: delivery.ManifestDigest(lemurs)}},
|
||||||
manifests: map[string][]byte{delivery.ManifestDigest(lemurs): lemurs},
|
manifests: map[string][]byte{delivery.ManifestDigest(lemurs): lemurs},
|
||||||
builds: map[string]*delivery.BuilderBuild{}}
|
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)
|
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 raw, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Candidates: map[string]nodeengine.CandidateAnswer{"g": a}}); err != nil {
|
||||||
if wire, err := json.Marshal(a); err != nil || json.Unmarshal(wire, &wireAnswer) != nil {
|
|
||||||
t.Fatalf("the answer does not read back: %v", err)
|
|
||||||
}
|
|
||||||
if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": wireAnswer}}); err != nil {
|
|
||||||
t.Errorf("the answer is not one a have holds: %v (%s)", err, raw)
|
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 {
|
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{
|
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}}},
|
"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}}}},
|
"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)}},
|
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}}
|
manifests: map[string][]byte{delivery.ManifestDigest(prov): prov, delivery.ManifestDigest(cons): cons}}
|
||||||
given := map[string]*delivery.Placement{}
|
given := map[string]*delivery.Placement{}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"maps"
|
"maps"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -57,8 +58,8 @@ type Reader interface {
|
|||||||
// Placements is every node's placement, by node (delivery.PlacementBucket).
|
// Placements is every node's placement, by node (delivery.PlacementBucket).
|
||||||
Placements(ctx context.Context) (map[string]delivery.Placement, error)
|
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>.node` of
|
// Records is every node's record, by node: HaveNode.Record of each node's own have key, `<node>.node` of
|
||||||
// delivery.HaveBucket (ADR 0555 §3).
|
// buckets.Have (ADR 0555 §3).
|
||||||
Records(ctx context.Context) (map[string]delivery.NodeRecord, error)
|
Records(ctx context.Context) (map[string]nodeengine.NodeRecord, error)
|
||||||
// Modules is every module the indexer holds, by module (delivery.ModulesBucket).
|
// Modules is every module the indexer holds, by module (delivery.ModulesBucket).
|
||||||
Modules(ctx context.Context) (map[string]delivery.Module, error)
|
Modules(ctx context.Context) (map[string]delivery.Module, error)
|
||||||
// Manifest is the raw manifest of a digest (delivery.ManifestsBucket).
|
// 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 {
|
type base struct {
|
||||||
in compose.Input
|
in compose.Input
|
||||||
placements map[string]delivery.Placement
|
placements map[string]delivery.Placement
|
||||||
records map[string]delivery.NodeRecord
|
records map[string]nodeengine.NodeRecord
|
||||||
modules map[string]delivery.Module
|
modules map[string]delivery.Module
|
||||||
// ownRefused is each module placed on the composing node whose manifest the indexer refused, with why, in the order
|
// ownRefused is each module placed on the composing node whose manifest the indexer refused, with why, in the order
|
||||||
// read: every scope holds it.
|
// read: every scope holds it.
|
||||||
@@ -987,15 +988,24 @@ func declaresOwnSecrets(in compose.Input, node string) bool {
|
|||||||
return false
|
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
|
// 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).
|
// 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{
|
n := compose.Node{
|
||||||
Name: rec.Node,
|
Name: rec.Node,
|
||||||
Place: compose.Place{Key: rec.TunnelKey},
|
Place: compose.Place{Key: rec.TunnelKey},
|
||||||
SealingKey: rec.SealingKey,
|
SealingKey: rec.SealingKey,
|
||||||
OutwardLinks: rec.OutwardLinks, Networks: rec.Networks, NetworksSaid: rec.NetworksSaid,
|
OutwardLinks: rec.OutwardLinks, Networks: attachedOf(rec.Networks), NetworksSaid: rec.NetworksSaid,
|
||||||
ServingKey: rec.ServingKey, Certificate: rec.Certificate, Contract: rec.Contract, Stated: rec.Contract > 0,
|
ServingKey: rec.ServingKey, Certificate: rec.Certificate, Contract: int(rec.Contract), Stated: rec.Contract > 0,
|
||||||
}
|
}
|
||||||
placeNode(&n, p)
|
placeNode(&n, p)
|
||||||
if len(rec.Capabilities) > 0 {
|
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
|
// 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).
|
// 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{}
|
byKey := map[string]string{}
|
||||||
for node, rec := range records {
|
for node, rec := range records {
|
||||||
if rec.TunnelKey != "" {
|
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.
|
// 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{}
|
seen := map[string]bool{}
|
||||||
for n := range placements {
|
for n := range placements {
|
||||||
seen[n] = true
|
seen[n] = true
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
"github.com/novox/mesh-host/internal/compositions"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
"os"
|
"os"
|
||||||
@@ -31,7 +32,7 @@ type bus struct {
|
|||||||
vaultAuthority string
|
vaultAuthority string
|
||||||
handed map[string]bool
|
handed map[string]bool
|
||||||
placements map[string]delivery.Placement
|
placements map[string]delivery.Placement
|
||||||
records map[string]delivery.NodeRecord
|
records map[string]nodeengine.NodeRecord
|
||||||
modules map[string]delivery.Module
|
modules map[string]delivery.Module
|
||||||
manifests map[string][]byte
|
manifests map[string][]byte
|
||||||
values map[string]map[string]any // by node.module
|
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) {
|
func (b *bus) Placements(context.Context) (map[string]delivery.Placement, error) {
|
||||||
return b.placements, nil
|
return b.placements, nil
|
||||||
}
|
}
|
||||||
func (b *bus) VaultAuthority(context.Context) string { return b.vaultAuthority }
|
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) 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) Records(context.Context) (map[string]nodeengine.NodeRecord, error) {
|
||||||
func (b *bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.modules, nil }
|
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) {
|
func (b *bus) Manifest(_ context.Context, digest string) ([]byte, error) {
|
||||||
raw, ok := b.manifests[digest]
|
raw, ok := b.manifests[digest]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -180,7 +183,7 @@ func TestInput(t *testing.T) {
|
|||||||
{Module: "media", HasMesh: true, MeshValues: map[string]any{"name": "mesh"}, HasLayer: true,
|
{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)}}}},
|
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},
|
"anchor": {Node: "anchor", TunnelKey: keys["anchor"], Capabilities: caps, Contract: 2},
|
||||||
"laptop": {Node: "laptop", TunnelKey: keys["laptop"], Capabilities: caps, Authority: "the mesh's certificate"},
|
"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})
|
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
|
||||||
}
|
}
|
||||||
return &bus{placements: map[string]delivery.Placement{"n1": p},
|
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)},
|
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)}},
|
"objects": {Manifest: delivery.ManifestDigest(objects)}, "files": {Manifest: delivery.ManifestDigest(files)}},
|
||||||
manifests: map[string][]byte{delivery.ManifestDigest(keeper): keeper, delivery.ManifestDigest(lemurs): lemurs,
|
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) {
|
if a.Verdict != c.verdict || !c.check(a) {
|
||||||
t.Errorf("%s: %+v", name, 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 raw, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Candidates: map[string]nodeengine.CandidateAnswer{"g": a}}); err != nil {
|
||||||
if wire, err := json.Marshal(a); err != nil || json.Unmarshal(wire, &wireAnswer) != nil {
|
|
||||||
t.Fatalf("%s: the answer does not read back: %v", name, err)
|
|
||||||
}
|
|
||||||
if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": wireAnswer}}); err != nil {
|
|
||||||
t.Errorf("%s: the answer is not one a have holds: %v (%s)", name, err, raw)
|
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)
|
records, _ := b.Records(ctx)
|
||||||
for node, r := range records {
|
for node, r := range records {
|
||||||
k, _ := delivery.HaveNodeKey(node)
|
k, _ := nodeengineclient.HaveNodeKey(node)
|
||||||
rec := r
|
rec := r
|
||||||
raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Record: &rec})
|
raw, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &rec})
|
||||||
put(HaveRead, k, raw, err)
|
put(HaveRead, k, raw, err)
|
||||||
}
|
}
|
||||||
modules, _ := b.Modules(ctx)
|
modules, _ := b.Modules(ctx)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"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.
|
// Bus is a frombus.Reader over values held in memory. A credential asked is answered held when Held is set.
|
||||||
type Bus struct {
|
type Bus struct {
|
||||||
Placement map[string]delivery.Placement
|
Placement map[string]delivery.Placement
|
||||||
Record map[string]delivery.NodeRecord
|
Record map[string]nodeengine.NodeRecord
|
||||||
Module map[string]delivery.Module
|
Module map[string]delivery.Module
|
||||||
Manifests map[string][]byte
|
Manifests map[string][]byte
|
||||||
Value map[string]map[string]any // by node.module
|
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) {
|
func (b *Bus) Placements(context.Context) (map[string]delivery.Placement, error) {
|
||||||
return b.Placement, nil
|
return b.Placement, nil
|
||||||
}
|
}
|
||||||
func (b *Bus) Records(context.Context) (map[string]delivery.NodeRecord, error) { return b.Record, nil }
|
func (b *Bus) Records(context.Context) (map[string]nodeengine.NodeRecord, error) {
|
||||||
func (b *Bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.Module, nil }
|
return b.Record, nil
|
||||||
func (b *Bus) VaultAuthority(context.Context) string { return "" }
|
}
|
||||||
|
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) {
|
func (b *Bus) Manifest(_ context.Context, digest string) ([]byte, error) {
|
||||||
raw, ok := b.Manifests[digest]
|
raw, ok := b.Manifests[digest]
|
||||||
@@ -152,7 +155,7 @@ func Provisioned() (*Bus, error) {
|
|||||||
Placement: map[string]delivery.Placement{Node: {Node: Node,
|
Placement: map[string]delivery.Placement{Node: {Node: Node,
|
||||||
Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1", Account: "operator", AccountHome: "/home/operator"},
|
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"}}}},
|
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"}}},
|
SealingKey: "sk", Contract: compose.HandedContract, Capabilities: []string{"systemd", "package-manager"}}},
|
||||||
Module: map[string]delivery.Module{}, Manifests: map[string][]byte{},
|
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"`}},
|
Own: []compose.OwnSecretRow{{Node: Node, Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}},
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package frombustest
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"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"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -313,7 +315,7 @@ func statesOf(t *testing.T, b *Bus) frombus.States {
|
|||||||
put(frombus.PlacementRead, "placement."+node, raw, err)
|
put(frombus.PlacementRead, "placement."+node, raw, err)
|
||||||
}
|
}
|
||||||
for node, rec := range b.Record {
|
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)
|
put(frombus.HaveRead, node+".node", raw, err)
|
||||||
}
|
}
|
||||||
for module, m := range b.Module {
|
for module, m := range b.Module {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -18,7 +19,7 @@ import (
|
|||||||
func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) {
|
func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
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{}}
|
modules: map[string]delivery.Module{}, manifests: map[string][]byte{}, builds: map[string]*delivery.BuilderBuild{}}
|
||||||
fps := map[string]string{}
|
fps := map[string]string{}
|
||||||
for i, raw := range manifests {
|
for i, raw := range manifests {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -84,11 +86,11 @@ func (r statesReader) Placements(context.Context) (map[string]delivery.Placement
|
|||||||
return keys.All(), err
|
return keys.All(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r statesReader) Records(context.Context) (map[string]delivery.NodeRecord, error) {
|
func (r statesReader) Records(context.Context) (map[string]nodeengine.NodeRecord, error) {
|
||||||
out := map[string]delivery.NodeRecord{}
|
out := map[string]nodeengine.NodeRecord{}
|
||||||
err := r.each(HaveRead, func(k string, v []byte) {
|
err := r.each(HaveRead, func(k string, v []byte) {
|
||||||
if node, _, isNode, err := delivery.ParseHaveKey(k); err == nil && isNode {
|
if node, _, isNode, err := nodeengineclient.ParseHaveKey(k); err == nil && isNode {
|
||||||
if h, err := delivery.DecodeHaveNode(v); err == nil && h.Record != nil {
|
if h, err := nodeengineclient.DecodeHaveNode(v); err == nil && h.Record != nil {
|
||||||
out[node] = *h.Record
|
out[node] = *h.Record
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,24 @@
|
|||||||
package judge
|
package judge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
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"
|
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.
|
// The shapes of mesh-sdk's delivery package this package judges, by the names its code was written with.
|
||||||
type (
|
type (
|
||||||
Composed = engcomp.Composed
|
Composed = engcomp.Composed
|
||||||
Judgement = delivery.Judgement
|
Judgement = string
|
||||||
CandidateAnswer = nodeengine.CandidateAnswer
|
CandidateAnswer = nodeengine.CandidateAnswer
|
||||||
ModuleDiffers = nodeengine.CandidateDiffers
|
ModuleDiffers = nodeengine.CandidateDiffers
|
||||||
ResourceBodies = nodeengine.CandidateBodies
|
ResourceBodies = nodeengine.CandidateBodies
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
JudgedRecreates = delivery.JudgedRecreates
|
JudgedRecreates = nodeengineclient.JudgedRecreates
|
||||||
JudgedMovesData = delivery.JudgedMovesData
|
JudgedMovesData = nodeengineclient.JudgedMovesData
|
||||||
JudgedNeither = delivery.JudgedNeither
|
JudgedNeither = "neither"
|
||||||
JudgedCannot = delivery.JudgedCannot
|
JudgedCannot = nodeengineclient.JudgedCannot
|
||||||
JudgedUnchanged = delivery.JudgedUnchanged
|
JudgedUnchanged = "unchanged"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
"github.com/nats-io/nats.go"
|
"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,
|
// 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
|
js = bounded
|
||||||
}
|
}
|
||||||
kv, err := js.KeyValue(delivery.HaveBucket)
|
kv, err := js.KeyValue(buckets.Have)
|
||||||
if err != nil {
|
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
|
return natsHaveBucket{kv}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
"github.com/nats-io/nats.go"
|
"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) ------
|
// ---- 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 `<node>.<module>` per module placed on the node that
|
// engine's manifest as its states acts and health are: one key `<node>.<module>` per module placed on the node that
|
||||||
// declares data items, written by that node's engine alone, read by that node's disk-load.
|
// 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.
|
// dataStore binds DataBucket over the link open now: bound, never created.
|
||||||
func (l *natsLink) dataStore() (nats.KeyValue, error) {
|
func (l *natsLink) dataStore() (nats.KeyValue, error) {
|
||||||
|
|||||||
+81
-93
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
|
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
|
||||||
"github.com/novox/mesh-host/internal/compositions"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
@@ -12,8 +13,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrHaveAbsent is what HaveGet returns for a key the bucket does not hold.
|
// ErrHaveAbsent is what HaveGet returns for a key the bucket does not hold.
|
||||||
@@ -40,14 +39,14 @@ type HaveBucket interface {
|
|||||||
type haveState struct {
|
type haveState struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
modules map[string]ModuleFact
|
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
|
// 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).
|
// 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
|
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
|
// 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).
|
// down, and is ok (issue 1945).
|
||||||
looked bool
|
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 is the digest of the node's own composition it runs (ADR 0539 §9), kept beside the node's facts.
|
||||||
nodeComposed string
|
nodeComposed string
|
||||||
// nodeSealed is, on a node composing its own, each value its own composition carries sealed to its key (ADR 0547 §3,
|
// 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
|
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
|
// 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.
|
// 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 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
|
userListNoKey map[string]bool
|
||||||
// unjudged is each module whose composition moved (an apply began, or the fact names another build) and that no look
|
// 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
|
// 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.
|
// composition being applied (a container stopped to be replaced reads unhealthy), so its health is not yet judged.
|
||||||
func inMotion(phase string) bool {
|
func inMotion(phase string) bool {
|
||||||
switch delivery.Phase(phase) {
|
switch string(phase) {
|
||||||
case delivery.PhaseBackingUp, delivery.PhaseApplying, delivery.PhaseChecking:
|
case nodeengineclient.PhaseBackingUp, nodeengineclient.PhaseApplying, nodeengineclient.PhaseChecking:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
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
|
// 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.
|
// 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) {
|
if inMotion(f.Phase) {
|
||||||
return delivery.HealthUnknown
|
return nodeengineclient.HealthUnknown
|
||||||
}
|
}
|
||||||
if got, known := s.health[module]; known {
|
if got, known := s.health[module]; known {
|
||||||
return got
|
return got
|
||||||
}
|
}
|
||||||
if s.unjudged[module] {
|
if s.unjudged[module] {
|
||||||
return delivery.HealthUnknown
|
return nodeengineclient.HealthUnknown
|
||||||
}
|
}
|
||||||
return nothingJudged(f, s.looked)
|
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
|
// 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.
|
// or waits, while it is so. The caller holds the lock.
|
||||||
func (s *haveState) wordsOf(module string, health delivery.Health) string {
|
func (s *haveState) wordsOf(module string, health string) string {
|
||||||
if health != delivery.HealthFailed && health != delivery.HealthWaits {
|
if health != nodeengineclient.HealthFailed && health != nodeengineclient.HealthWaits {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return s.words[module]
|
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
|
// 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
|
// 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).
|
// 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) {
|
func moduleHealths(h *Health) (map[string]string, map[string]string) {
|
||||||
out, words := map[string]delivery.Health{}, map[string]string{}
|
out, words := map[string]string{}, map[string]string{}
|
||||||
if h == nil {
|
if h == nil {
|
||||||
return out, words
|
return out, words
|
||||||
}
|
}
|
||||||
@@ -183,7 +182,7 @@ func moduleHealths(h *Health) (map[string]delivery.Health, map[string]string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for module, r := range rank {
|
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 {
|
if r < 2 {
|
||||||
delete(words, module)
|
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.
|
// 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 {
|
func nodeFacts(r Report) nodeengine.HaveByNodeNodeValue {
|
||||||
n := delivery.HaveNode{V: sdkkeys.Version}
|
n := nodeengine.HaveByNodeNodeValue{V: sdkkeys.Version}
|
||||||
carry := func(into *json.RawMessage, empty bool, v any) {
|
carry := func(into *[]map[string]any, empty bool, v any) {
|
||||||
if empty {
|
if empty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if raw, err := json.Marshal(v); err == nil {
|
raw, err := json.Marshal(v)
|
||||||
*into = raw
|
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.Windows, len(r.Windows) == 0, r.Windows)
|
||||||
carry(&n.Busy, len(r.Busy) == 0, r.Busy)
|
carry(&n.Busy, len(r.Busy) == 0, r.Busy)
|
||||||
carry(&n.Strays, len(r.Strays) == 0, r.Strays)
|
carry(&n.Strays, len(r.Strays) == 0, r.Strays)
|
||||||
carry(&n.Filters, len(r.Filters) == 0, r.Filters)
|
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
|
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
|
// 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.
|
// 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)
|
n := nodeFacts(r)
|
||||||
if q.Keys != nil {
|
if q.Keys != nil {
|
||||||
keys := q.Keys()
|
keys := q.Keys()
|
||||||
@@ -377,20 +383,20 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) {
|
|||||||
sort.Strings(modules)
|
sort.Strings(modules)
|
||||||
for _, module := range modules {
|
for _, module := range modules {
|
||||||
f := s.modules[module]
|
f := s.modules[module]
|
||||||
h := delivery.Have{V: sdkkeys.Version, FP: f.FP, Composed: f.Composed, Build: f.Build,
|
h := nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Fp: f.FP, Composed: f.Composed, Build: f.Build,
|
||||||
Phase: delivery.PhaseRunning, Health: delivery.HealthUnknown, Detail: f.Detail}
|
Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthUnknown, Detail: f.Detail}
|
||||||
h.Kept = f.Kept
|
h.Kept = f.Kept
|
||||||
if delivery.Phase(f.Phase) == delivery.PhaseWaitingYes {
|
if string(f.Phase) == nodeengineclient.PhaseWaitingYes {
|
||||||
h.Judgement, h.Waits, h.BuildAlone = delivery.Judgement(f.Judgement), f.Waits, f.BuildAlone
|
h.Judgement, h.Waits, h.BuildAlone = string(f.Judgement), f.Waits, f.BuildAlone
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case f.Phase != "":
|
case f.Phase != "":
|
||||||
h.Phase = delivery.Phase(f.Phase)
|
h.Phase = string(f.Phase)
|
||||||
case f.Failed:
|
case f.Failed:
|
||||||
h.Phase = delivery.PhaseFailed
|
h.Phase = nodeengineclient.PhaseFailed
|
||||||
}
|
}
|
||||||
h.SealedAt = f.Sealed
|
h.SealedAt = f.Sealed
|
||||||
if h.Phase == delivery.PhaseFailed {
|
if h.Phase == nodeengineclient.PhaseFailed {
|
||||||
h.PullWait = f.PullWait
|
h.PullWait = f.PullWait
|
||||||
}
|
}
|
||||||
h.Health = s.healthOf(module, f)
|
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 {
|
if w, ok := s.userList[module]; ok {
|
||||||
h.UserList = &w
|
h.UserList = &w
|
||||||
}
|
}
|
||||||
key, err := delivery.HaveKey(node, module)
|
key, err := nodeengineclient.HaveKey(node, module)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
q.sayHave(fmt.Sprintf("module %q has no key in the have bucket: %v", module, err))
|
q.sayHave(fmt.Sprintf("module %q has no key in the have bucket: %v", module, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
raw, err := delivery.EncodeHave(h)
|
raw, err := nodeengineclient.EncodeHave(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err))
|
q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !q.putHave(write, bucket, key, raw, func(current []byte) bool {
|
if !q.putHave(write, bucket, key, raw, func(current []byte) bool {
|
||||||
c, err := delivery.DecodeHave(current)
|
c, err := nodeengineclient.DecodeHave(current)
|
||||||
return err == nil && delivery.SameHave(c, h)
|
return err == nil && nodeengineclient.SameHave(c, h)
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -428,19 +434,19 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.node != nil {
|
if s.node != nil {
|
||||||
key, err := delivery.HaveNodeKey(node)
|
key, err := nodeengineclient.HaveNodeKey(node)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var raw []byte
|
var raw []byte
|
||||||
facts := *s.node
|
facts := *s.node
|
||||||
facts.Composed = s.nodeComposed
|
facts.Composed = s.nodeComposed
|
||||||
facts.SealedAt = s.nodeSealed
|
facts.SealedAt = s.nodeSealed
|
||||||
facts.Candidates = interimAnswers(s.nodeCandidates)
|
facts.Candidates = s.nodeCandidates
|
||||||
facts.Composes, facts.ComposesWhy = s.nodeComposes, s.nodeComposesWhy
|
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
|
want := facts
|
||||||
if !q.putHave(write, bucket, key, raw, func(current []byte) bool {
|
if !q.putHave(write, bucket, key, raw, func(current []byte) bool {
|
||||||
c, err := delivery.DecodeHaveNode(current)
|
c, err := nodeengineclient.DecodeHaveNode(current)
|
||||||
return err == nil && delivery.SameHaveNode(c, want)
|
return err == nil && nodeengineclient.SameHaveNode(c, want)
|
||||||
}) {
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -503,15 +509,15 @@ func (q *Session) writeRemoved(ctx context.Context, b HaveBucket, node string) b
|
|||||||
}
|
}
|
||||||
sort.Strings(gone)
|
sort.Strings(gone)
|
||||||
for _, module := range gone {
|
for _, module := range gone {
|
||||||
key, err := delivery.HaveKey(node, module)
|
key, err := nodeengineclient.HaveKey(node, module)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
q.sayHave(fmt.Sprintf("module %q has no key in the have bucket: %v", module, err))
|
q.sayHave(fmt.Sprintf("module %q has no key in the have bucket: %v", module, err))
|
||||||
delete(s.removed, module)
|
delete(s.removed, module)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
h := delivery.Have{V: sdkkeys.Version, Build: s.removed[module], Phase: delivery.PhaseRemoved,
|
h := nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: s.removed[module], Phase: nodeengineclient.PhaseRemoved,
|
||||||
Health: delivery.HealthUnknown, Detail: "the applied declaration no longer holds the module"}
|
Health: nodeengineclient.HealthUnknown, Detail: "the applied declaration no longer holds the module"}
|
||||||
raw, err := delivery.EncodeHave(h)
|
raw, err := nodeengineclient.EncodeHave(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err))
|
q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err))
|
||||||
delete(s.removed, module)
|
delete(s.removed, module)
|
||||||
@@ -531,7 +537,7 @@ func (q *Session) writeRemoved(ctx context.Context, b HaveBucket, node string) b
|
|||||||
q.haveFailed(err)
|
q.haveFailed(err)
|
||||||
return false
|
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)
|
delete(s.removed, module)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -565,7 +571,7 @@ func (q *Session) writeUserListsAlone(ctx context.Context, b HaveBucket, node st
|
|||||||
}
|
}
|
||||||
sort.Strings(alone)
|
sort.Strings(alone)
|
||||||
for _, module := range alone {
|
for _, module := range alone {
|
||||||
key, err := delivery.HaveKey(node, module)
|
key, err := nodeengineclient.HaveKey(node, module)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
q.sayHave(fmt.Sprintf("module %q has no key in the have bucket: %v", module, err))
|
q.sayHave(fmt.Sprintf("module %q has no key in the have bucket: %v", module, err))
|
||||||
continue
|
continue
|
||||||
@@ -592,7 +598,7 @@ func (q *Session) writeUserListsAlone(ctx context.Context, b HaveBucket, node st
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
delete(s.userListNoKey, module)
|
delete(s.userListNoKey, module)
|
||||||
h, err := delivery.DecodeHave(current)
|
h, err := nodeengineclient.DecodeHave(current)
|
||||||
if err != nil {
|
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))
|
q.sayHave(fmt.Sprintf("the bus's user list is not written in %s: its value cannot be read: %v", key, err))
|
||||||
continue
|
continue
|
||||||
@@ -600,10 +606,10 @@ func (q *Session) writeUserListsAlone(ctx context.Context, b HaveBucket, node st
|
|||||||
w := s.userList[module]
|
w := s.userList[module]
|
||||||
next := h
|
next := h
|
||||||
next.UserList = &w
|
next.UserList = &w
|
||||||
if delivery.SameHave(h, next) {
|
if nodeengineclient.SameHave(h, next) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
raw, err := delivery.EncodeHave(next)
|
raw, err := nodeengineclient.EncodeHave(next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err))
|
q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err))
|
||||||
continue
|
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
|
// 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
|
// 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.
|
// 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()
|
q.init()
|
||||||
s := &q.haves
|
s := &q.haves
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if s.userList == nil {
|
if s.userList == nil {
|
||||||
s.userList = map[string]delivery.UserListWritten{}
|
s.userList = map[string]nodeengine.UserListWritten{}
|
||||||
}
|
}
|
||||||
s.userList[module] = w
|
s.userList[module] = w
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
@@ -732,7 +738,7 @@ func (q *Session) NoteNode(ctx context.Context, composed string, sealed composit
|
|||||||
s.nodeComposed = composed
|
s.nodeComposed = composed
|
||||||
s.nodeSealed = sealed
|
s.nodeSealed = sealed
|
||||||
if s.node == nil {
|
if s.node == nil {
|
||||||
n := delivery.HaveNode{V: sdkkeys.Version}
|
n := nodeengine.HaveByNodeNodeValue{V: sdkkeys.Version}
|
||||||
s.node = &n
|
s.node = &n
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
@@ -765,7 +771,7 @@ func (q *Session) NoteCandidates(ctx context.Context, answers map[string]nodeeng
|
|||||||
q.writeHaves(ctx, bus)
|
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).
|
// at each start of its agent, where every have is read (issue 1126).
|
||||||
func (q *Session) NoteComposes(ctx context.Context, composes, why string) {
|
func (q *Session) NoteComposes(ctx context.Context, composes, why string) {
|
||||||
q.init()
|
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
|
// 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).
|
// 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)
|
n, ok, _ := q.ReadNodeHaveErr(ctx)
|
||||||
return n, ok
|
return n, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadNodeHaveErr is ReadNodeHave telling the key absent (false, nil) from one that could not be read (false, the
|
// 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).
|
// 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.init()
|
||||||
q.mu.Lock()
|
q.mu.Lock()
|
||||||
bus := q.bus
|
bus := q.bus
|
||||||
q.mu.Unlock()
|
q.mu.Unlock()
|
||||||
hb, ok := bus.(HaveBus)
|
hb, ok := bus.(HaveBus)
|
||||||
if !ok || bus == nil {
|
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 {
|
if err != nil {
|
||||||
return delivery.HaveNode{}, false, err
|
return nodeengine.HaveByNodeNodeValue{}, false, err
|
||||||
}
|
}
|
||||||
read, cancel := context.WithTimeout(ctx, q.timeout())
|
read, cancel := context.WithTimeout(ctx, q.timeout())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
bucket, err := hb.HaveBind(read)
|
bucket, err := hb.HaveBind(read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return delivery.HaveNode{}, false, err
|
return nodeengine.HaveByNodeNodeValue{}, false, err
|
||||||
}
|
}
|
||||||
value, _, err := bucket.Get(read, key)
|
value, _, err := bucket.Get(read, key)
|
||||||
if errors.Is(err, ErrHaveAbsent) {
|
if errors.Is(err, ErrHaveAbsent) {
|
||||||
return delivery.HaveNode{}, false, nil
|
return nodeengine.HaveByNodeNodeValue{}, false, nil
|
||||||
}
|
}
|
||||||
if err != 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 {
|
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
|
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
|
// 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.
|
// 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)
|
h, ok, _ := q.ReadHaveErr(ctx, module)
|
||||||
return h, ok
|
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
|
// 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).
|
// 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.init()
|
||||||
q.mu.Lock()
|
q.mu.Lock()
|
||||||
bus := q.bus
|
bus := q.bus
|
||||||
q.mu.Unlock()
|
q.mu.Unlock()
|
||||||
hb, ok := bus.(HaveBus)
|
hb, ok := bus.(HaveBus)
|
||||||
if !ok || bus == nil {
|
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 {
|
if err != nil {
|
||||||
return delivery.Have{}, false, err
|
return nodeengine.HaveByNodeModuleValue{}, false, err
|
||||||
}
|
}
|
||||||
read, cancel := context.WithTimeout(ctx, q.timeout())
|
read, cancel := context.WithTimeout(ctx, q.timeout())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
bucket, err := hb.HaveBind(read)
|
bucket, err := hb.HaveBind(read)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return delivery.Have{}, false, err
|
return nodeengine.HaveByNodeModuleValue{}, false, err
|
||||||
}
|
}
|
||||||
value, _, err := bucket.Get(read, key)
|
value, _, err := bucket.Get(read, key)
|
||||||
if errors.Is(err, ErrHaveAbsent) {
|
if errors.Is(err, ErrHaveAbsent) {
|
||||||
return delivery.Have{}, false, nil
|
return nodeengine.HaveByNodeModuleValue{}, false, nil
|
||||||
}
|
}
|
||||||
if err != 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 {
|
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
|
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
|
// 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.
|
// and runs (nothing of it could be down), unknown otherwise.
|
||||||
func nothingJudged(f ModuleFact, looked bool) delivery.Health {
|
func nothingJudged(f ModuleFact, looked bool) string {
|
||||||
if looked && !f.Failed && (f.Phase == "" || f.Phase == string(delivery.PhaseRunning)) {
|
if looked && !f.Failed && (f.Phase == "" || f.Phase == string(nodeengineclient.PhaseRunning)) {
|
||||||
return delivery.HealthOK
|
return nodeengineclient.HealthOK
|
||||||
}
|
}
|
||||||
return delivery.HealthUnknown
|
return nodeengineclient.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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ package link
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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
|
// 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) {
|
func TestNatsHavesAreWrittenOnceAndOnlyOnChange(t *testing.T) {
|
||||||
conn, js := aBus(t)
|
conn, js := aBus(t)
|
||||||
const node = "haveok"
|
const node = "haveok"
|
||||||
kv, err := js.KeyValue(delivery.HaveBucket)
|
kv, err := js.KeyValue(buckets.Have)
|
||||||
if err != nil {
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,17 +66,17 @@ func TestNatsHavesAreWrittenOnceAndOnlyOnChange(t *testing.T) {
|
|||||||
first := revisions()
|
first := revisions()
|
||||||
|
|
||||||
a, _ := kv.Get(node + ".a")
|
a, _ := kv.Get(node + ".a")
|
||||||
haveA, err := delivery.DecodeHave(a.Value())
|
haveA, err := nodeengineclient.DecodeHave(a.Value())
|
||||||
if err != nil || haveA.Phase != delivery.PhaseRunning || haveA.Build != "b-1" || haveA.Health != delivery.HealthOK {
|
if err != nil || haveA.Phase != nodeengineclient.PhaseRunning || haveA.Build != "b-1" || haveA.Health != nodeengineclient.HealthOK {
|
||||||
t.Fatalf("a holds %+v (%v)", haveA, err)
|
t.Fatalf("a holds %+v (%v)", haveA, err)
|
||||||
}
|
}
|
||||||
b, _ := kv.Get(node + ".b")
|
b, _ := kv.Get(node + ".b")
|
||||||
haveB, err := delivery.DecodeHave(b.Value())
|
haveB, err := nodeengineclient.DecodeHave(b.Value())
|
||||||
if err != nil || haveB.Phase != delivery.PhaseFailed || haveB.Detail == "" {
|
if err != nil || haveB.Phase != nodeengineclient.PhaseFailed || haveB.Detail == "" {
|
||||||
t.Fatalf("b holds %+v (%v)", haveB, err)
|
t.Fatalf("b holds %+v (%v)", haveB, err)
|
||||||
}
|
}
|
||||||
n, _ := kv.Get(node + ".node")
|
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)
|
t.Fatalf("the node key holds %+v (%v)", hn, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+77
-72
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
|
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
|
||||||
"github.com/novox/mesh-host/internal/compositions"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
@@ -15,7 +16,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,11 +104,11 @@ func (l *haveLink) Set(_ context.Context, key string, value []byte, revision uin
|
|||||||
return nil
|
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()
|
t.Helper()
|
||||||
l.hmu.Lock()
|
l.hmu.Lock()
|
||||||
defer l.hmu.Unlock()
|
defer l.hmu.Unlock()
|
||||||
h, err := delivery.DecodeHave(l.kv[key].value)
|
h, err := nodeengineclient.DecodeHave(l.kv[key].value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%s holds no have: %v", key, err)
|
t.Fatalf("%s holds no have: %v", key, err)
|
||||||
}
|
}
|
||||||
@@ -121,9 +121,9 @@ func (l *haveLink) setCount() int {
|
|||||||
return len(l.sets)
|
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()
|
t.Helper()
|
||||||
raw, err := delivery.EncodeHave(h)
|
raw, err := nodeengineclient.EncodeHave(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -165,11 +165,11 @@ func TestAnAppliedModuleIsWrittenRunningAndAFailedOneFailed(t *testing.T) {
|
|||||||
q, _ := aHaveQueue(l)
|
q, _ := aHaveQueue(l)
|
||||||
q.SayFacts(context.Background(), appliedModules())
|
q.SayFacts(context.Background(), appliedModules())
|
||||||
a, b := l.have(t, "n1.a"), l.have(t, "n1.b")
|
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)
|
t.Errorf("a: %+v", a)
|
||||||
}
|
}
|
||||||
if b.Phase != delivery.PhaseFailed || b.Build != "b-2" || !strings.Contains(b.Detail, "no space") ||
|
if b.Phase != nodeengineclient.PhaseFailed || b.Build != "b-2" || !strings.Contains(b.Detail, "no space") ||
|
||||||
b.Health != delivery.HealthFailed {
|
b.Health != nodeengineclient.HealthFailed {
|
||||||
t.Errorf("b: %+v", b)
|
t.Errorf("b: %+v", b)
|
||||||
}
|
}
|
||||||
if l.kv["n1.a"].rev != 1 {
|
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.
|
// 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) {
|
func TestAHaveIsWrittenOnlyWhenItDiffers(t *testing.T) {
|
||||||
l := newHaveLink()
|
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.a", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: "b-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})
|
||||||
l.put(t, "n1.b", delivery.Have{V: sdkkeys.Version, Build: "b-0", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
|
l.put(t, "n1.b", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: "b-0", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})
|
||||||
q, _ := aHaveQueue(l)
|
q, _ := aHaveQueue(l)
|
||||||
q.SayFacts(context.Background(), appliedModules())
|
q.SayFacts(context.Background(), appliedModules())
|
||||||
if l.kv["n1.a"].rev != 1 {
|
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)
|
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)
|
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()
|
before := l.setCount()
|
||||||
@@ -205,7 +205,7 @@ func TestAModulesHealthFollowsTheLook(t *testing.T) {
|
|||||||
{Module: "c", Build: "b"}, {Module: "d", Build: "b"}, {Module: "e", Build: "b"}}}
|
{Module: "c", Build: "b"}, {Module: "d", Build: "b"}, {Module: "e", Build: "b"}}}
|
||||||
q.SayFacts(context.Background(), rep)
|
q.SayFacts(context.Background(), rep)
|
||||||
for _, m := range []string{"a", "b", "c", "d", "e"} {
|
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)
|
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) {
|
if !q.SayHealth(context.Background(), *h) {
|
||||||
t.Fatal("not said")
|
t.Fatal("not said")
|
||||||
}
|
}
|
||||||
for m, want := range map[string]delivery.Health{"a": delivery.HealthWaits, "b": delivery.HealthFailed,
|
for m, want := range map[string]string{"a": nodeengineclient.HealthWaits, "b": nodeengineclient.HealthFailed,
|
||||||
"c": delivery.HealthWaits, "d": delivery.HealthUnknown, "e": delivery.HealthOK} {
|
"c": nodeengineclient.HealthWaits, "d": nodeengineclient.HealthUnknown, "e": nodeengineclient.HealthOK} {
|
||||||
if got := l.have(t, "n1."+m).Health; got != want {
|
if got := l.have(t, "n1."+m).Health; got != want {
|
||||||
t.Errorf("%s is %s, want %s", m, 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}}
|
h2.Resources = []ResourceHealth{{Module: "a", Resource: "a.server", State: StateHealthy}}
|
||||||
q.SayHealth(context.Background(), h2)
|
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)
|
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.Filters = []Filter{{Where: "inet mesh", Owner: "mesh"}}
|
||||||
rep.Profile = map[string]any{"cores": 8}
|
rep.Profile = map[string]any{"cores": 8}
|
||||||
q.SayFacts(context.Background(), rep)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
enc := func(v any) json.RawMessage { b, _ := json.Marshal(v); return b }
|
enc := func(v any) []map[string]any {
|
||||||
want := delivery.HaveNode{V: sdkkeys.Version, Windows: enc(rep.Windows), Busy: enc(rep.Busy), Strays: enc(rep.Strays),
|
var out []map[string]any
|
||||||
Filters: enc(rep.Filters), Profile: enc(rep.Profile)}
|
b, _ := json.Marshal(v)
|
||||||
if !delivery.SameHaveNode(got, want) {
|
_ = 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)
|
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.Outward = []string{"eth0"}
|
||||||
rep.Health = &Health{Contract: NodeKeyContract}
|
rep.Health = &Health{Contract: NodeKeyContract}
|
||||||
q.SayFacts(context.Background(), rep)
|
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)
|
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,
|
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)
|
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)
|
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).
|
// 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{}
|
l.statedErr, l.stated = nil, map[string]string{}
|
||||||
q.SayFacts(context.Background(), rep)
|
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)
|
t.Errorf("a runner stating no calls: revision %d (was %d), capabilities %v", l.kv["n1.node"].rev, at, got.Record.Capabilities)
|
||||||
}
|
}
|
||||||
at++
|
at++
|
||||||
@@ -316,13 +321,13 @@ func TestTheNodeKeyCarriesTheNodesFacts(t *testing.T) {
|
|||||||
at++
|
at++
|
||||||
keys.Certificate = "cert-2"
|
keys.Certificate = "cert-2"
|
||||||
q.SayFacts(context.Background(), rep)
|
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)
|
t.Errorf("a new certificate: revision %d (was %d), record %+v", l.kv["n1.node"].rev, at, got.Record)
|
||||||
}
|
}
|
||||||
bare := rep
|
bare := rep
|
||||||
bare.Health = nil
|
bare.Health = nil
|
||||||
q.SayFacts(context.Background(), bare)
|
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)
|
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.failing = false
|
||||||
l.hmu.Unlock()
|
l.hmu.Unlock()
|
||||||
q.SayFacts(context.Background(), appliedModules())
|
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")
|
t.Error("the next apply did not write again")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,7 +412,7 @@ func TestAFailedBindOrWriteEndsThePassAndReturnsInTime(t *testing.T) {
|
|||||||
if l.setCount() == 0 {
|
if l.setCount() == 0 {
|
||||||
t.Fatal("a bind that met no responders was not written again")
|
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")
|
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
|
revA, revB := l.kv["n1.a"].rev, l.kv["n1.b"].rev
|
||||||
q.SayFacts(context.Background(), onlyA())
|
q.SayFacts(context.Background(), onlyA())
|
||||||
b := l.have(t, "n1.b")
|
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)
|
t.Errorf("b: %+v", b)
|
||||||
}
|
}
|
||||||
if l.kv["n1.b"].rev != revB+1 {
|
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)
|
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)
|
t.Errorf("a changed: %+v at %d", l.have(t, "n1.a"), l.kv["n1.a"].rev)
|
||||||
}
|
}
|
||||||
before := l.setCount()
|
before := l.setCount()
|
||||||
@@ -449,11 +454,11 @@ func TestARemovedModuleAddedBackIsWrittenWithItsNewPhase(t *testing.T) {
|
|||||||
q.SayFacts(context.Background(), appliedModules())
|
q.SayFacts(context.Background(), appliedModules())
|
||||||
q.SayFacts(context.Background(), onlyA())
|
q.SayFacts(context.Background(), onlyA())
|
||||||
q.SayFacts(context.Background(), appliedModules())
|
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)
|
t.Errorf("b after it came back: %+v", b)
|
||||||
}
|
}
|
||||||
q.SayFacts(context.Background(), onlyA())
|
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)
|
t.Errorf("b removed again: %+v", b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -462,11 +467,11 @@ func TestARemovedModuleAddedBackIsWrittenWithItsNewPhase(t *testing.T) {
|
|||||||
// bucket is not created again.
|
// bucket is not created again.
|
||||||
func TestAModuleNeverWrittenIsNotWrittenRemoved(t *testing.T) {
|
func TestAModuleNeverWrittenIsNotWrittenRemoved(t *testing.T) {
|
||||||
l := newHaveLink()
|
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, _ := aHaveQueue(l)
|
||||||
q.SayFacts(context.Background(), appliedModules())
|
q.SayFacts(context.Background(), appliedModules())
|
||||||
q.SayFacts(context.Background(), onlyA())
|
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)
|
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
|
// 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.failing = false
|
||||||
l.hmu.Unlock()
|
l.hmu.Unlock()
|
||||||
q.SayFacts(context.Background(), onlyA())
|
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)
|
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.
|
// 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) {
|
func TestARestartedNodeEngineWritesTheRemovalFromTheReportsBefore(t *testing.T) {
|
||||||
l := newHaveLink()
|
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.a", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Build: "b-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})
|
||||||
l.put(t, "n1.b", delivery.Have{V: sdkkeys.Version, Build: "b-2", Phase: delivery.PhaseRunning, Health: delivery.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
|
q, _ := aHaveQueue(l) // fresh: nothing written by this process
|
||||||
r := onlyA()
|
r := onlyA()
|
||||||
r.Before = []string{"a", "b", "c"}
|
r.Before = []string{"a", "b", "c"}
|
||||||
q.SayFacts(context.Background(), r)
|
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)
|
t.Errorf("b: %+v", b)
|
||||||
}
|
}
|
||||||
if _, there := l.kv["n1.c"]; there {
|
if _, there := l.kv["n1.c"]; there {
|
||||||
@@ -563,24 +568,24 @@ func TestAnAppliedModulesHaveNamesTheFingerprintAndTheDigestAndFollowsItsHealth(
|
|||||||
dig := "sha256:" + strings.Repeat("b", 64)
|
dig := "sha256:" + strings.Repeat("b", 64)
|
||||||
q.NoteModule(context.Background(), ModuleFact{Module: "web", FP: fp, Composed: dig})
|
q.NoteModule(context.Background(), ModuleFact{Module: "web", FP: fp, Composed: dig})
|
||||||
h := l.have(t, "n1.web")
|
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)
|
t.Fatalf("the have after the apply: %+v", h)
|
||||||
}
|
}
|
||||||
// Starting: it waits. Past its start window the check fails: failed.
|
// Starting: it waits. Past its start window the check fails: failed.
|
||||||
q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateStarting}))
|
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)
|
t.Fatalf("while it starts: %+v", got)
|
||||||
}
|
}
|
||||||
q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateUnhealthy}))
|
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)
|
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).
|
// 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)}
|
At: time.Date(2026, 10, 25, 21, 0, 0, 0, time.UTC)}
|
||||||
q.NoteUserList(context.Background(), "web", w)
|
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)
|
t.Fatalf("the user list is not in the have: %+v", got)
|
||||||
}
|
}
|
||||||
q.NoteModule(context.Background(), ModuleFact{Module: "web", FP: fp, Composed: dig})
|
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).
|
// 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"}
|
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})
|
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)
|
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.
|
// 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"})
|
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 != delivery.PhaseHeld || got.Detail == "" {
|
if got := l.have(t, "n1.web"); got.Phase != nodeengineclient.PhaseHeld || got.Detail == "" {
|
||||||
t.Fatalf("held: %+v", got)
|
t.Fatalf("held: %+v", got)
|
||||||
}
|
}
|
||||||
q.NoteRemoved(context.Background(), "web", "")
|
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)
|
t.Fatalf("removed: %+v", got)
|
||||||
}
|
}
|
||||||
q.NoteNode(context.Background(), dig, compositions.Seals{"core-box#sealed": "core"})
|
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)
|
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 {
|
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.
|
// 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"}}}
|
answers := map[string]nodeengine.CandidateAnswer{"p-1": {Proposal: "p-1", Revision: 1, Verdict: nodeengine.CandidateAnswerVerdictSame, Same: []string{"web"}}}
|
||||||
q.NoteCandidates(context.Background(), answers)
|
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)
|
t.Fatalf("the node key with its answers: %+v %v", n, err)
|
||||||
}
|
}
|
||||||
q.NoteCandidates(context.Background(), nil)
|
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)
|
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, _ := aHaveQueue(l)
|
||||||
q.SayFacts(context.Background(), Report{Modules: []ModuleFact{{Module: "mesh-host", Build: "b"}, {Module: "web", Build: "b"},
|
q.SayFacts(context.Background(), Report{Modules: []ModuleFact{{Module: "mesh-host", Build: "b"}, {Module: "web", Build: "b"},
|
||||||
{Module: "broken", Build: "b", Failed: true, Detail: "x"}}})
|
{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)
|
t.Fatalf("before any look: %s", h)
|
||||||
}
|
}
|
||||||
if !q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateHealthy})) {
|
if !q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateHealthy})) {
|
||||||
t.Fatal("not said")
|
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)
|
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")
|
t.Fatalf("a failed module was said ok")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -654,17 +659,17 @@ func TestAModuleSaidAgainOverAnOlderEnginesUnknownIsWrittenWithThisEnginesHealth
|
|||||||
l := newHaveLink()
|
l := newHaveLink()
|
||||||
fp := "src2:" + strings.Repeat("a", 64)
|
fp := "src2:" + strings.Repeat("a", 64)
|
||||||
dig := "sha256:" + strings.Repeat("b", 64)
|
dig := "sha256:" + strings.Repeat("b", 64)
|
||||||
l.put(t, "n1.xorg", delivery.Have{V: sdkkeys.Version, FP: fp, Composed: dig, Phase: delivery.PhaseRunning,
|
l.put(t, "n1.xorg", nodeengine.HaveByNodeModuleValue{V: sdkkeys.Version, Fp: fp, Composed: dig, Phase: nodeengineclient.PhaseRunning,
|
||||||
Health: delivery.HealthUnknown})
|
Health: nodeengineclient.HealthUnknown})
|
||||||
q, _ := aHaveQueue(l)
|
q, _ := aHaveQueue(l)
|
||||||
if !q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateHealthy})) {
|
if !q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateHealthy})) {
|
||||||
t.Fatal("not said")
|
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)
|
t.Fatalf("a module the session does not hold was written: %+v", h)
|
||||||
}
|
}
|
||||||
q.NoteModule(context.Background(), ModuleFact{Module: "xorg", FP: fp, Composed: dig})
|
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)
|
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)}
|
old := ModuleFact{Module: "ctl", FP: "src2:" + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("1", 64)}
|
||||||
q.NoteModule(ctx, old)
|
q.NoteModule(ctx, old)
|
||||||
q.SayHealth(ctx, *healthOf(map[string]string{"ctl": StateUnhealthy}))
|
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)
|
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)}
|
next := ModuleFact{Module: "ctl", FP: "src2:" + strings.Repeat("b", 64), Composed: "sha256:" + strings.Repeat("2", 64)}
|
||||||
applying := next
|
applying := next
|
||||||
applying.Phase = string(delivery.PhaseApplying)
|
applying.Phase = string(nodeengineclient.PhaseApplying)
|
||||||
q.NoteModule(ctx, applying)
|
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)
|
t.Fatalf("applying carried the last look's health: %+v", h)
|
||||||
}
|
}
|
||||||
q.SayHealth(ctx, *healthOf(map[string]string{"ctl": StateUnhealthy}))
|
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)
|
t.Fatalf("a look during the apply judged it: %+v", h)
|
||||||
}
|
}
|
||||||
q.NoteModule(ctx, next)
|
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)
|
t.Fatalf("running before a look after the apply: %+v", h)
|
||||||
}
|
}
|
||||||
q.SayHealth(ctx, *healthOf(map[string]string{"ctl": StateHealthy}))
|
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)
|
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.NoteModule(ctx, ModuleFact{Module: "ctl", FP: "src2:" + strings.Repeat("a", 64)})
|
||||||
q.SayHealth(ctx, *healthOf(map[string]string{"ctl": StateUnhealthy}))
|
q.SayHealth(ctx, *healthOf(map[string]string{"ctl": StateUnhealthy}))
|
||||||
q.NoteModule(ctx, ModuleFact{Module: "ctl", FP: "src2:" + strings.Repeat("b", 64)})
|
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)
|
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()
|
ctx := context.Background()
|
||||||
q.SayFacts(ctx, Report{Applied: []string{"a.x"}, Modules: []ModuleFact{{Module: "a", Build: "b-1"}},
|
q.SayFacts(ctx, Report{Applied: []string{"a.x"}, Modules: []ModuleFact{{Module: "a", Build: "b-1"}},
|
||||||
Health: healthOf(map[string]string{"a": StateUnhealthy})})
|
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)
|
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)}}})
|
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)
|
t.Fatalf("the old composition's look judged the new one: %+v", h)
|
||||||
}
|
}
|
||||||
select {
|
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.
|
// 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) {
|
func TestAUserListReachesTheHaveOfAModuleNoApplyHereSays(t *testing.T) {
|
||||||
l := newHaveLink()
|
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)
|
l.put(t, "n1.nats", older)
|
||||||
q, said := aHaveQueue(l)
|
q, said := aHaveQueue(l)
|
||||||
dig := "sha256:" + strings.Repeat("c", 64)
|
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)}
|
At: time.Date(2026, 10, 26, 12, 26, 51, 0, time.UTC)}
|
||||||
q.NoteUserList(context.Background(), "nats", w)
|
q.NoteUserList(context.Background(), "nats", w)
|
||||||
got := l.have(t, "n1.nats")
|
got := l.have(t, "n1.nats")
|
||||||
if got.UserList == nil || got.UserList.Users != 58 || got.UserList.Digest != dig {
|
if got.UserList == nil || got.UserList.Users != 58 || got.UserList.Digest != dig {
|
||||||
t.Fatalf("the user list is not in the have: %+v", got)
|
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)
|
t.Fatalf("the merge lost the key's own fields: %+v", got)
|
||||||
}
|
}
|
||||||
rev := l.kv["n1.nats"].rev
|
rev := l.kv["n1.nats"].rev
|
||||||
@@ -794,7 +799,7 @@ func TestAUserListReachesTheHaveOfAModuleNoApplyHereSays(t *testing.T) {
|
|||||||
}
|
}
|
||||||
fp := "src2:" + strings.Repeat("a", 64)
|
fp := "src2:" + strings.Repeat("a", 64)
|
||||||
q.NoteModule(context.Background(), ModuleFact{Module: "other", FP: fp, Composed: dig})
|
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)
|
t.Fatalf("the first have of the module lost the list: %+v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ package link
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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
|
// 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)
|
since := time.Date(2026, 10, 29, 20, 0, 0, 0, time.UTC)
|
||||||
look := func(streak int, writer string) Health {
|
look := func(streak int, writer string) Health {
|
||||||
return Health{Contract: 7, At: time.Now(), Network: &NetworkHealth{State: StateUnhealthy, Since: since, Parts: []NetworkPart{
|
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}}},
|
Units: &UnitsHealth{State: "degraded", Failed: []FailedUnit{{Unit: "cups.service", Scope: "system", Since: since}}},
|
||||||
Resources: []ResourceHealth{
|
Resources: []ResourceHealth{
|
||||||
{Module: "brother", Resource: "brother.scanner", Kind: KindAccount, Account: "jochen", State: StateUnhealthy,
|
{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)
|
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() {
|
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)
|
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.
|
// 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)
|
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 {
|
if store.puts != 3 {
|
||||||
|
|||||||
+11
-12
@@ -1,11 +1,10 @@
|
|||||||
package link
|
package link
|
||||||
|
|
||||||
import (
|
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"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
"time"
|
"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
|
// 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
|
// 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.
|
// delivery, issue 774): one layout for the message, the report and the watchman that reads it.
|
||||||
type (
|
type (
|
||||||
NetworkHealth = delivery.NetworkHealth
|
NetworkHealth = nodeengine.NetworkHealth
|
||||||
NetworkPart = delivery.NetworkPart
|
NetworkPart = nodeengine.NetworkPart
|
||||||
UnitsHealth = delivery.UnitsHealth
|
UnitsHealth = nodeengine.UnitsHealth
|
||||||
FailedUnit = delivery.FailedUnit
|
FailedUnit = nodeengine.FailedUnit
|
||||||
)
|
)
|
||||||
|
|
||||||
// The states a long-running resource is said in (ADR 0240 §4).
|
// 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 and Waits are, in phase waiting-yes, the judgement of the move and the composition waiting (ADR 0547 §3).
|
||||||
Judgement string
|
Judgement string
|
||||||
Waits 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
|
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
|
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
|
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 is, on a failed fact, the pull from a store its apply waits on (nodeengine.HaveByNodeModuleValue.PullWait, issue 1966 ENG-9).
|
||||||
PullWait *delivery.PullWait
|
PullWait *nodeengine.PullWait
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-13
@@ -4,13 +4,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"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"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
"github.com/nats-io/nats.go"
|
"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).
|
// - 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 (
|
const (
|
||||||
HealthBucket = delivery.NodeHealthBucket
|
HealthBucket = buckets.Health
|
||||||
NodeHealthVersion = delivery.NodeHealthVersion
|
NodeHealthVersion = keys.Version
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
NodeHealth = delivery.NodeHealth
|
NodeHealth = nodeengine.HealthByNodeValue
|
||||||
PeerSeen = delivery.PeerSeen
|
PeerSeen = nodeengine.PeerSeen
|
||||||
NodeAccount = delivery.NodeAccount
|
NodeAccount = nodeengine.NodeAccount
|
||||||
NodeDirectory = delivery.NodeDirectory
|
NodeDirectory = nodeengine.NodeDirectory
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReasonReloginNeeded starts an account's reason when its session needs a new login (accounts.ReasonRelogin).
|
// 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
|
// 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.
|
// directory of its resources that the six alerts read.
|
||||||
func NodeHealthOf(node string, h Health, peers []PeerSeen, machines Machines) NodeHealth {
|
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 {
|
if len(peers) > 0 {
|
||||||
out.Peers = append([]PeerSeen(nil), peers...)
|
out.Peers = append([]PeerSeen(nil), peers...)
|
||||||
sort.Slice(out.Peers, func(i, k int) bool { return out.Peers[i].Key < out.Peers[k].Key })
|
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 {
|
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.
|
// 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})
|
State: r.State, Reason: r.Reason, Since: r.Since})
|
||||||
}
|
}
|
||||||
if r.Kind == KindDirectory && strings.HasPrefix(r.Reason, ReasonUsedAsFound) {
|
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)
|
return false, fmt.Errorf("the health report of %s cannot be read: %w", h.Node, err)
|
||||||
}
|
}
|
||||||
if raw != nil {
|
if raw != nil {
|
||||||
if was, err := delivery.DecodeNodeHealth(raw); err == nil {
|
if was, err := nodeengineclient.DecodeHealth(raw); err == nil {
|
||||||
*last = &was
|
*last = &was
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if *last != nil && delivery.SameNodeHealth(**last, h) {
|
if *last != nil && nodeengineclient.SameHealth(**last, h) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
h.At = now().UTC()
|
h.At = now().UTC()
|
||||||
raw, err := delivery.EncodeNodeHealth(h)
|
raw, err := nodeengineclient.EncodeHealth(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package link
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
|
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -9,11 +10,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"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
|
// The node's record (novox/hq ADR 0555 §3, issue 1126): what the machine observes of itself, written in its own have key
|
||||||
// `<node>.node` (delivery.HaveNode.Record) with its facts, so every node and the parts that compose read it from the bus
|
// `<node>.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
|
// 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
|
// 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.
|
// 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
|
// 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.
|
// 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 {
|
if r.Profile == nil {
|
||||||
return 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 {
|
if err != nil || json.Unmarshal(raw, &p) != nil {
|
||||||
return 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,
|
ServingKey: keys.ServingKey, Certificate: keys.Certificate, Authority: keys.Authority, Trusts: keys.Trusts,
|
||||||
ManifestReads: manifestReads()}
|
ManifestReads: manifestReads()}
|
||||||
for _, c := range p.Capabilities {
|
for _, c := range p.Capabilities {
|
||||||
@@ -77,13 +77,15 @@ func recordOf(node string, r Report, keys RecordKeys) *delivery.NodeRecord {
|
|||||||
sort.Strings(rec.Capabilities)
|
sort.Strings(rec.Capabilities)
|
||||||
if p.Machine != nil {
|
if p.Machine != nil {
|
||||||
rec.NetworksSaid = true
|
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 {
|
if len(r.Outward) > 0 {
|
||||||
rec.OutwardLinks = append([]string(nil), r.Outward...)
|
rec.OutwardLinks = append([]string(nil), r.Outward...)
|
||||||
}
|
}
|
||||||
if r.Health != nil {
|
if r.Health != nil {
|
||||||
rec.Contract = r.Health.Contract
|
rec.Contract = int64(r.Health.Contract)
|
||||||
}
|
}
|
||||||
return rec
|
return rec
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package link
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -35,9 +36,9 @@ type Rotator struct {
|
|||||||
failures int
|
failures int
|
||||||
// fellBack is the key's `since` a fallback was made for: the same rotation is not tried again.
|
// fellBack is the key's `since` a fallback was made for: the same rotation is not tried again.
|
||||||
fellBackSince string
|
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.
|
// 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
|
switchedSince string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,13 +163,13 @@ func (r *Rotator) AuthFailed(login string) {
|
|||||||
if r.Now != nil {
|
if r.Now != nil {
|
||||||
now = r.Now
|
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.old, r.failures = "", 0
|
||||||
r.fellBackSince = r.switchedSince
|
r.fellBackSince = r.switchedSince
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback is the fall back the node's health report carries now, nil when there is none.
|
// 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 {
|
if r == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package link
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -140,7 +142,7 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t *
|
|||||||
t.Fatalf("after the bound: %s, fall back %+v", cur, fb)
|
t.Fatalf("after the bound: %s, fall back %+v", cur, fb)
|
||||||
}
|
}
|
||||||
// The health report carries it, from which the watchman raises node-account-fallback.
|
// 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)
|
t.Errorf("the fall back is no health report's: %v", err)
|
||||||
}
|
}
|
||||||
steps := len(f.steps)
|
steps := len(f.steps)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/health"
|
"git.novox.be/novox/mesh-sdk/go/health"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ import (
|
|||||||
// receives, orders or reconciles a whole declaration.
|
// receives, orders or reconciles a whole declaration.
|
||||||
type Session struct {
|
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 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 is the bus user the link open now authenticated as, guarded by mu (WhenLinkedAs).
|
||||||
linkedUser string
|
linkedUser string
|
||||||
// Membership is the node and the mesh's key a controller's ask is verified with.
|
// Membership is the node and the mesh's key a controller's ask is verified with.
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func TestDrillAResolverFileRewrittenInAThrowawayMachine(t *testing.T) {
|
|||||||
Network: &link.NetworkHealth{State: st.State, Since: st.Since.UTC(), Parts: []link.NetworkPart{}}}
|
Network: &link.NetworkHealth{State: st.State, Since: st.Since.UTC(), Parts: []link.NetworkPart{}}}
|
||||||
for _, p := range st.Parts {
|
for _, p := range st.Parts {
|
||||||
h.Network.Parts = append(h.Network.Parts, link.NetworkPart{Part: p.Part, State: p.State, Reason: p.Reason,
|
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)
|
said = append(said, h)
|
||||||
raw, _ := json.Marshal(st)
|
raw, _ := json.Marshal(st)
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi
|
|||||||
case laidErr != nil && baseErr != nil:
|
case laidErr != nil && baseErr != nil:
|
||||||
// The node composes none of its own without the candidate either: the candidate changes nothing of that.
|
// 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.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()
|
a.Took = c.now().Sub(began).Milliseconds()
|
||||||
return a
|
return a
|
||||||
case laidErr != nil:
|
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) {
|
one := func(module string) (nodeengine.Module, bool) {
|
||||||
m := nodeengine.Module{Module: module}
|
m := nodeengine.Module{Module: module}
|
||||||
if module == "" {
|
if module == "" {
|
||||||
m.Module = delivery.HaveNodeWord
|
m.Module = nec.NodeWord
|
||||||
}
|
}
|
||||||
if why, held := laidHeld[module]; held {
|
if why, held := laidHeld[module]; held {
|
||||||
m.State, m.Words = nec.ModuleRefused, why
|
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 {
|
if own, ok := one(""); ok {
|
||||||
a.Own = own
|
a.Own = own
|
||||||
} else {
|
} 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"}
|
Words: "the node composes no composition of its own"}
|
||||||
}
|
}
|
||||||
refused = refused || a.Own.State == nec.ModuleRefused
|
refused = refused || a.Own.State == nec.ModuleRefused
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package selfcompose
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/novox/mesh-host/internal/buckets"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"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"},
|
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||||
Modules: []delivery.PlacedModule{{Module: "lemurs"}}})
|
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"})
|
mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||||
deadline := time.Now().Add(5 * time.Second)
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
watching := func() int { b.mu.Lock(); defer b.mu.Unlock(); return len(b.watches) }
|
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)
|
time.Sleep(5 * time.Millisecond)
|
||||||
}
|
}
|
||||||
b.put(t, delivery.PlacementBucket, "placement.n1", place)
|
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, "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})
|
b.send(bucket, agent.Update{Caught: true})
|
||||||
}
|
}
|
||||||
for !s.ready.Load() && time.Now().Before(deadline) {
|
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" ||
|
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.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)
|
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
|
// 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 := NewShadow(env, nil)
|
||||||
s.c.placements["n1"] = delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
s.c.placements["n1"] = delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||||
Modules: []delivery.PlacedModule{{Module: "lemurs"}}}
|
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["lemurs"] = delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}
|
||||||
s.c.modules["other"] = delivery.Module{FP2: fp, Manifest: digestOther, Repository: "novox/x", Commit: "c"}
|
s.c.modules["other"] = delivery.Module{FP2: fp, Manifest: digestOther, Repository: "novox/x", Commit: "c"}
|
||||||
s.ready.Store(true)
|
s.ready.Store(true)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package selfcompose
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"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"},
|
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||||
Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}})
|
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"})
|
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"})
|
omod, _ := delivery.EncodeModule(delivery.Module{FP2: ofp, Manifest: odigest, Repository: "novox/o", Commit: "c"})
|
||||||
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
|
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
|
||||||
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
|
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
|
||||||
b.put(t, delivery.PlacementBucket, "placement.n1", place)
|
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, "lemurs", mod)
|
||||||
b.put(t, delivery.ModulesBucket, "oldie", omod)
|
b.put(t, delivery.ModulesBucket, "oldie", omod)
|
||||||
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
|
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
|
||||||
b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant)
|
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})
|
b.send(bucket, agent.Update{Caught: true})
|
||||||
}
|
}
|
||||||
for u := next(t, out); !u.Caught; u = next(t, out) {
|
for u := next(t, out); !u.Caught; u = next(t, out) {
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"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"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"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"
|
||||||
"github.com/novox/mesh-host/internal/compose/frombus"
|
"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)
|
items, unplaced, held, err := dataItemsOf(ctx, itemsReader{reader{c: c}}, c.env.Mesh, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.sayData("the data items of this node's modules cannot be worked out, and what the engine's state " +
|
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
|
return
|
||||||
}
|
}
|
||||||
// A module held for a refused manifest declares nothing this pass can read: it is named, its other modules' items
|
// 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()
|
c.mu.Unlock()
|
||||||
wrote, removed, err := c.writeData(ctx, items, digests, held)
|
wrote, removed, err := c.writeData(ctx, items, digests, held)
|
||||||
if err != nil {
|
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())
|
"; " + c.oweData() + ": " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -93,7 +95,7 @@ func (c *Composer) publishData(ctx context.Context) {
|
|||||||
c.sayData("")
|
c.sayData("")
|
||||||
if wrote+removed > 0 {
|
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",
|
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) {
|
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
|
store := c.env.Data
|
||||||
if c.dataKept == nil {
|
if c.dataKept == nil {
|
||||||
pattern, err := delivery.EngineDataNodeKeys(c.env.Node)
|
pattern, err := nodeengineclient.DataNodeKeys(c.env.Node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
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 {
|
if len(items[module]) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key, err := delivery.EngineDataKey(c.env.Node, module)
|
key, err := nodeengineclient.DataKey(c.env.Node, module)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failed = append(failed, err.Error())
|
failed = append(failed, err.Error())
|
||||||
continue
|
continue
|
||||||
@@ -185,7 +187,7 @@ func (c *Composer) writeData(ctx context.Context, items map[string][]catalogue.P
|
|||||||
if !ok {
|
if !ok {
|
||||||
digest = digests[""] // run inside the node's own composition
|
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 {
|
if err != nil {
|
||||||
failed = append(failed, err.Error())
|
failed = append(failed, err.Error())
|
||||||
continue
|
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.
|
// 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 {
|
func heldKey(node, key string, held map[string]string) bool {
|
||||||
for module := range held {
|
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 true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package selfcompose
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"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"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -86,7 +89,7 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) {
|
|||||||
var said []string
|
var said []string
|
||||||
env := Env{Node: "n1", Data: store, Say: func(s string) { said = append(said, s) }}
|
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.
|
// 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")
|
t.Error("the node's grant reads other keys of the engine's state data than its own")
|
||||||
}
|
}
|
||||||
c := New(env)
|
c := New(env)
|
||||||
@@ -105,15 +108,15 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) {
|
|||||||
deletes []string
|
deletes []string
|
||||||
asked int
|
asked int
|
||||||
// want is each key's items and digest after the step.
|
// 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,
|
{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},
|
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"},
|
digests: map[string]string{webKey: "sha256:w1", nodeKey: "sha256:n1"},
|
||||||
puts: []string{"n1.oldie", "n1.web"}, deletes: []string{"n1.gone"}, asked: 1,
|
puts: []string{"n1.oldie", "n1.web"}, deletes: []string{"n1.gone"}, asked: 1,
|
||||||
want: map[string]delivery.EngineData{
|
want: map[string]nodeengine.DataByNodeModuleValue{
|
||||||
"n1.web": {Module: "web", Digest: "sha256:w1", Items: []catalogue.PlacedDataItem{item("web", "db", "/srv/web/db")}},
|
"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: []catalogue.PlacedDataItem{item("oldie", "x", "/srv/oldie")}}}},
|
"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 handed nothing new reads nothing", due: false, asked: 1},
|
||||||
{name: "a pass that changes no module's items writes nothing", due: true,
|
{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")}},
|
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,
|
{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")}},
|
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,
|
digests: map[string]string{webKey: "sha256:w2", nodeKey: "sha256:n1"}, puts: []string{"n1.web"}, asked: 5,
|
||||||
want: map[string]delivery.EngineData{
|
want: map[string]nodeengine.DataByNodeModuleValue{
|
||||||
"n1.web": {Module: "web", Digest: "sha256:w2", Items: []catalogue.PlacedDataItem{item("web", "db", "/srv/web/db2")}}}},
|
"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,
|
{name: "a module that left the node is deleted", due: true,
|
||||||
items: map[string][]catalogue.PlacedDataItem{"oldie": {item("oldie", "x", "/srv/oldie")}},
|
items: map[string][]catalogue.PlacedDataItem{"oldie": {item("oldie", "x", "/srv/oldie")}},
|
||||||
digests: map[string]string{nodeKey: "sha256:n1"}, deletes: []string{"n1.web"}, asked: 6},
|
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")}},
|
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"},
|
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,
|
digests: map[string]string{webKey: "sha256:w3", nodeKey: "sha256:n1"}, puts: []string{"n1.web"}, asked: 7,
|
||||||
want: map[string]delivery.EngineData{
|
want: map[string]nodeengine.DataByNodeModuleValue{
|
||||||
"n1.web": {Module: "web", Digest: "sha256:w3", Items: []catalogue.PlacedDataItem{item("web", "db", "/srv/web/db")}}}},
|
"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) {
|
t.Run(step.name, func(t *testing.T) {
|
||||||
store.mu.Lock()
|
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)
|
t.Errorf("owed a pass: %v after %s; want after %s", c.owed.Load(), wait, step.owed)
|
||||||
}
|
}
|
||||||
for key, want := range step.want {
|
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 ||
|
if err != nil || got.Node != "n1" || got.Module != want.Module || got.Digest != want.Digest ||
|
||||||
!slices.Equal(got.Items, want.Items) {
|
!slices.Equal(got.Items, want.Items) {
|
||||||
t.Errorf("%s holds %+v (%v), want %+v", key, got, err, want)
|
t.Errorf("%s holds %+v (%v), want %+v", key, got, err, want)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// **The packages this node's compositions declare, kept by its engine** (novox/hq ADR 0565 §6, §15; issue 1953). The
|
// **The packages this node's compositions declare, kept by its engine** (novox/hq ADR 0565 §6, §15; issue 1953). The
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"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"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"maps"
|
"maps"
|
||||||
@@ -76,7 +79,7 @@ type Env struct {
|
|||||||
Unseal func(sealed string) ([]byte, error)
|
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,
|
// 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
|
// 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.
|
// 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).
|
// 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)
|
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
|
// 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.
|
// (delivery.PlacementKeys; research 072 row 37): a write of the holder's under way is never composed half.
|
||||||
placementKeys *delivery.PlacementKeys
|
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
|
// 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).
|
// 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
|
inOwn map[string]bool
|
||||||
@@ -298,7 +301,7 @@ func New(env Env) *Composer {
|
|||||||
env.Say = func(string) {}
|
env.Say = func(string) {}
|
||||||
}
|
}
|
||||||
return &Composer{env: env, wants: map[string]delivery.Want{}, placements: map[string]delivery.Placement{}, placementKeys: delivery.NewPlacementKeys(),
|
return &Composer{env: env, wants: map[string]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{},
|
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)}
|
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
|
// Every node's record, and this node's own haves (what it runs inside its own composition): two watches, each
|
||||||
// within the node's grant (buscompose: readGrants of the have bucket at `*.node`, NodeKeysGrants at `<node>.>`).
|
// within the node's grant (buscompose: readGrants of the have bucket at `*.node`, NodeKeysGrants at `<node>.>`).
|
||||||
// A watch of every key is refused by the bus (issue 1126, 2026-10-27 09:21: the fold's `>` stopped every agent).
|
// 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},
|
{buckets.Have, "*." + nodeengineclient.NodeWord, nil, (*Composer).takeRecordChanged},
|
||||||
{delivery.HaveBucket, node + ".>", nil, (*Composer).takeModuleHave},
|
{buckets.Have, node + ".>", nil, (*Composer).takeModuleHave},
|
||||||
{delivery.ModulesBucket, ">", (*Composer).takeModule, nil},
|
{delivery.ModulesBucket, ">", (*Composer).takeModule, nil},
|
||||||
{ResolvedBucket, node + ".>", (*Composer).takeValue, 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
|
// 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) {
|
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 {
|
if err != nil || !isNode {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
h, err := delivery.DecodeHaveNode(u.Value)
|
h, err := nodeengineclient.DecodeHaveNode(u.Value)
|
||||||
if u.Deleted || err != nil || h.Record == nil {
|
if u.Deleted || err != nil || h.Record == nil {
|
||||||
delete(c.records, node)
|
delete(c.records, node)
|
||||||
return
|
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
|
// 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).
|
// this run: a fresh agent composes before it applies anything (review of the fold, round 1).
|
||||||
func (c *Composer) takeModuleHave(u agent.Update) bool {
|
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 {
|
if err != nil || isNode || node != c.env.Node {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
in := false
|
in := false
|
||||||
if !u.Deleted {
|
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
|
// 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
|
// 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).
|
// 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
|
// 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
|
// 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).
|
// (waiting for a yes inside the node's own stays in).
|
||||||
interrupted := h.Phase == delivery.PhaseHeld && h.Detail == agent.BackupInterrupted
|
interrupted := h.Phase == nodeengineclient.PhaseHeld && h.Detail == agent.BackupInterrupted
|
||||||
runsNothing := (h.Phase == delivery.PhaseFailed || interrupted) && h.FP == "" && h.Composed == ""
|
runsNothing := (h.Phase == nodeengineclient.PhaseFailed || interrupted) && h.Fp == "" && h.Composed == ""
|
||||||
in = h.Phase != delivery.PhaseRemoved && !runsNothing && !strings.HasPrefix(h.FP, delivery.SourcePrefix2)
|
in = h.Phase != nodeengineclient.PhaseRemoved && !runsNothing && !strings.HasPrefix(h.Fp, delivery.SourcePrefix2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
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.
|
// 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
|
// 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.
|
// before any placement is read.
|
||||||
func (c *Composer) Machines() (byAddress map[string]string, hub string) {
|
func (c *Composer) Machines() (byAddress map[string]string, hub string) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@@ -1079,7 +1082,7 @@ func (c *Composer) refused(why string, held map[string]string) {
|
|||||||
c.refusal = said
|
c.refusal = said
|
||||||
c.held = heldModules(held)
|
c.held = heldModules(held)
|
||||||
if why != "" {
|
if why != "" {
|
||||||
c.held = append(c.held, delivery.HaveNodeWord)
|
c.held = append(c.held, nodeengineclient.NodeWord)
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
if !same && c.env.Refused != nil {
|
if !same && c.env.Refused != nil {
|
||||||
@@ -1374,10 +1377,10 @@ func (r reader) Placements(context.Context) (map[string]delivery.Placement, erro
|
|||||||
return out, nil
|
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()
|
r.c.mu.Lock()
|
||||||
defer r.c.mu.Unlock()
|
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 {
|
for k, v := range r.c.records {
|
||||||
out[k] = v
|
out[k] = v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
|
"github.com/novox/mesh-host/internal/buckets"
|
||||||
"github.com/novox/mesh-host/internal/compositions"
|
"github.com/novox/mesh-host/internal/compositions"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"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"})
|
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"},
|
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||||
Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}})
|
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"})
|
mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||||
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
|
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
|
||||||
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
|
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
|
||||||
b.put(t, delivery.PlacementBucket, "placement.n1", place)
|
b.put(t, delivery.PlacementBucket, "placement.n1", place)
|
||||||
b.put(t, delivery.HaveBucket, "n1.node", have)
|
b.put(t, buckets.Have, "n1.node", have)
|
||||||
oldHave, _ := delivery.EncodeHave(delivery.Have{Build: "build-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
|
oldHave, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: "build-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})
|
||||||
b.put(t, delivery.HaveBucket, "n1.oldie", oldHave)
|
b.put(t, buckets.Have, "n1.oldie", oldHave)
|
||||||
b.put(t, delivery.ModulesBucket, "lemurs", mod)
|
b.put(t, delivery.ModulesBucket, "lemurs", mod)
|
||||||
b.put(t, delivery.ModulesBucket, "oldie", omod)
|
b.put(t, delivery.ModulesBucket, "oldie", omod)
|
||||||
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
|
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
|
||||||
b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant)
|
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})
|
b.send(bucket, agent.Update{Caught: true})
|
||||||
}
|
}
|
||||||
got := map[string]bool{}
|
got := map[string]bool{}
|
||||||
@@ -696,10 +698,10 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
|||||||
bkey, _ := delivery.BuilderBuildKey("tool", next2)
|
bkey, _ := delivery.BuilderBuildKey("tool", next2)
|
||||||
tb.kv[delivery.BuilderBuildsBucket+"/"+bkey] = building
|
tb.kv[delivery.BuilderBuildsBucket+"/"+bkey] = building
|
||||||
tb.put(t, delivery.PlacementBucket, "placement.n1", tplace)
|
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.ModulesBucket, "tool", tmod)
|
||||||
tb.put(t, delivery.AssignerWantsBucket, "want.n1.tool", twant)
|
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} {
|
ResolvedBucket, delivery.BuilderBuildsBucket} {
|
||||||
tb.send(bucket, agent.Update{Caught: true})
|
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.
|
// What n1 runs: lemurs and its own composition, as its composer composes them from the trunk.
|
||||||
running, _, _, err := frombus.Compose(ctx, fixedReader{placements: map[string]delivery.Placement{"n1": trunk},
|
running, _, _, err := frombus.Compose(ctx, fixedReader{placements: map[string]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})
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -1085,10 +1087,10 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
|||||||
time.Sleep(30 * time.Millisecond)
|
time.Sleep(30 * time.Millisecond)
|
||||||
sb.missing(delivery.PlacementCandidatesBucket, false)
|
sb.missing(delivery.PlacementCandidatesBucket, false)
|
||||||
sb.made(6)
|
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)
|
tp, _ := delivery.EncodePlacement(trunk)
|
||||||
sb.put(delivery.PlacementBucket, "placement.n1", tp, 1)
|
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 {
|
for name, m := range mods {
|
||||||
raw, _ := delivery.EncodeModule(m)
|
raw, _ := delivery.EncodeModule(m)
|
||||||
sb.put(delivery.ModulesBucket, name, raw, 1)
|
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.
|
// 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 {
|
select {
|
||||||
case a := <-answered:
|
case a := <-answered:
|
||||||
t.Errorf("answers handed again for nothing changed: %+v", a)
|
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) {
|
lemursHave := func(detail string, rev uint64) {
|
||||||
raw, err := delivery.EncodeHave(delivery.Have{FP: fp, Composed: lemursDigest, Phase: delivery.PhaseRunning,
|
raw, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Composed: lemursDigest, Phase: nodeengineclient.PhaseRunning,
|
||||||
Health: delivery.HealthFailed, Detail: detail})
|
Health: nodeengineclient.HealthFailed, Detail: detail})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
hk, _ := delivery.HaveKey("n1", "lemurs")
|
hk, _ := nodeengineclient.HaveKey("n1", "lemurs")
|
||||||
sb.put(delivery.HaveBucket, hk, raw, rev)
|
sb.put(buckets.Have, hk, raw, rev)
|
||||||
}
|
}
|
||||||
lemursHave("its tool lemurs_health: no link to the bus is open", 10)
|
lemursHave("its tool lemurs_health: no link to the bus is open", 10)
|
||||||
drain()
|
drain()
|
||||||
@@ -1273,9 +1275,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
|||||||
t.Errorf("a new revision's answer said %q", got)
|
t.Errorf("a new revision's answer said %q", got)
|
||||||
}
|
}
|
||||||
settled = answering()
|
settled = answering()
|
||||||
hk, _ := delivery.HaveKey("n1", "lemurs")
|
hk, _ := nodeengineclient.HaveKey("n1", "lemurs")
|
||||||
moved, _ := delivery.EncodeHave(delivery.Have{FP: fp, Composed: "sha256:" + strings.Repeat("e", 64), Phase: delivery.PhaseRunning})
|
moved, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Fp: fp, Composed: "sha256:" + strings.Repeat("e", 64), Phase: nodeengineclient.PhaseRunning})
|
||||||
sb.put(delivery.HaveBucket, hk, moved, 13)
|
sb.put(buckets.Have, hk, moved, 13)
|
||||||
drain()
|
drain()
|
||||||
if got := answering(); got != settled+len(answers) {
|
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))
|
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) }()
|
go func() { _ = nsh.Run(nctx) }()
|
||||||
nb.made(6)
|
nb.made(6)
|
||||||
nb.put(delivery.PlacementBucket, "placement.n1", tp, 1)
|
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 {
|
for name, m := range mods {
|
||||||
raw, _ := delivery.EncodeModule(m)
|
raw, _ := delivery.EncodeModule(m)
|
||||||
nb.put(delivery.ModulesBucket, name, raw, 1)
|
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})
|
oldRaw, _ := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Proposal: "older", Placement: &trunk})
|
||||||
oldKey, _ := delivery.PlacementCandidateKey("older", "n1")
|
oldKey, _ := delivery.PlacementCandidateKey("older", "n1")
|
||||||
nb.put(delivery.PlacementCandidatesBucket, oldKey, oldRaw, 2)
|
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})
|
nb.send(bucket, agent.Update{Caught: true})
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
@@ -1508,7 +1510,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
|||||||
cc.manifests[digest] = []byte(lemurs)
|
cc.manifests[digest] = []byte(lemurs)
|
||||||
cc.manifests[cdigest] = []byte(withCert)
|
cc.manifests[cdigest] = []byte(withCert)
|
||||||
cc.modules = map[string]delivery.Module{"lemurs": {Manifest: digest}, "web": {Manifest: cdigest}}
|
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"}}}
|
onlyLemurs := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "lemurs"}}}
|
||||||
cc.certifyIfWanted(context.Background(), reader{c: cc}, onlyLemurs)
|
cc.certifyIfWanted(context.Background(), reader{c: cc}, onlyLemurs)
|
||||||
if certAsks != 0 {
|
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.
|
// fixedReader is frombus's Reader over values held, for composing what a node runs.
|
||||||
type fixedReader struct {
|
type fixedReader struct {
|
||||||
placements map[string]delivery.Placement
|
placements map[string]delivery.Placement
|
||||||
records map[string]delivery.NodeRecord
|
records map[string]nodeengine.NodeRecord
|
||||||
modules map[string]delivery.Module
|
modules map[string]delivery.Module
|
||||||
manifests map[string][]byte
|
manifests map[string][]byte
|
||||||
}
|
}
|
||||||
@@ -1664,7 +1666,7 @@ type fixedReader struct {
|
|||||||
func (r fixedReader) Placements(context.Context) (map[string]delivery.Placement, error) {
|
func (r fixedReader) Placements(context.Context) (map[string]delivery.Placement, error) {
|
||||||
return r.placements, nil
|
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
|
return r.records, nil
|
||||||
}
|
}
|
||||||
func (r fixedReader) Modules(context.Context) (map[string]delivery.Module, error) {
|
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) {
|
func TestAHaveSaysWhetherItsModuleRunsInsideTheNodesOwn(t *testing.T) {
|
||||||
for _, row := range []struct {
|
for _, row := range []struct {
|
||||||
name string
|
name string
|
||||||
have delivery.Have
|
have nodeengine.HaveByNodeModuleValue
|
||||||
in bool
|
in bool
|
||||||
// heldToday says the shadow counts the module held today (issue 1126: a seed holding it too holds it as today).
|
// heldToday says the shadow counts the module held today (issue 1126: a seed holding it too holds it as today).
|
||||||
heldToday bool
|
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},
|
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,
|
{"failed at a build fingerprint", nodeengine.HaveByNodeModuleValue{Fp: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: nodeengineclient.PhaseFailed,
|
||||||
Health: delivery.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, true, true},
|
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`.
|
// 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 old form", nodeengine.HaveByNodeModuleValue{Build: "build-1790368817085312877", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.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),
|
{"running a build of the new form", nodeengine.HaveByNodeModuleValue{Fp: delivery.SourcePrefix2 + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("c", 64),
|
||||||
Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, false, false},
|
Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, false, false},
|
||||||
{"running with no fingerprint (mesh-wireguard)", delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, false},
|
{"running with no fingerprint (mesh-wireguard)", nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}, true, false},
|
||||||
{"held today", delivery.Have{FP: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseHeld,
|
{"held today", nodeengine.HaveByNodeModuleValue{Fp: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: nodeengineclient.PhaseHeld,
|
||||||
Health: delivery.HealthWaits, Detail: "held: a required config value is unset"}, true, true},
|
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
|
// 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.
|
// 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},
|
Detail: agent.BackupInterrupted}, false, true},
|
||||||
} {
|
} {
|
||||||
t.Run(row.name, func(t *testing.T) {
|
t.Run(row.name, func(t *testing.T) {
|
||||||
c := New(Env{Node: "n1"})
|
c := New(Env{Node: "n1"})
|
||||||
key, err := delivery.HaveKey("n1", "web")
|
key, err := nodeengineclient.HaveKey("n1", "web")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
value, err := delivery.EncodeHave(row.have)
|
value, err := nodeengineclient.EncodeHave(row.have)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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"},
|
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
|
||||||
Modules: []delivery.PlacedModule{{Module: "lemurs"}}})
|
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"})
|
mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
|
||||||
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
|
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
|
||||||
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
|
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
|
||||||
b.put(t, delivery.PlacementBucket, "placement.n1", place)
|
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, "lemurs", mod)
|
||||||
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
|
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
|
||||||
b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant)
|
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})
|
b.send(bucket, agent.Update{Caught: true})
|
||||||
}
|
}
|
||||||
handed := map[string]bool{}
|
handed := map[string]bool{}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
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"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -160,11 +162,11 @@ func (s *Shadow) Run(ctx context.Context) error {
|
|||||||
ins := []input{
|
ins := []input{
|
||||||
{delivery.PlacementCandidatesBucket, ">", nil, nil},
|
{delivery.PlacementCandidatesBucket, ">", nil, nil},
|
||||||
{delivery.PlacementBucket, ">", (*Composer).takePlacement, nil},
|
{delivery.PlacementBucket, ">", (*Composer).takePlacement, nil},
|
||||||
{delivery.HaveBucket, "*." + delivery.HaveNodeWord, nil, nil},
|
{buckets.Have, "*." + nodeengineclient.NodeWord, nil, nil},
|
||||||
{delivery.ModulesBucket, ">", (*Composer).takeModule, nil},
|
{delivery.ModulesBucket, ">", (*Composer).takeModule, nil},
|
||||||
{ResolvedBucket, node + ".>", (*Composer).takeValue, nil},
|
{ResolvedBucket, node + ".>", (*Composer).takeValue, nil},
|
||||||
// What this node runs moved: an apply ends in its module's have.
|
// 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 {
|
type tagged struct {
|
||||||
in int
|
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,
|
// 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.
|
// which moves nothing composed.
|
||||||
func (s *Shadow) takeRecord(u agent.Update) bool {
|
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 {
|
if err != nil || !isNode {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -327,7 +329,7 @@ func (s *Shadow) takeRecord(u agent.Update) bool {
|
|||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
after, has := c.records[node]
|
after, has := c.records[node]
|
||||||
c.mu.Unlock()
|
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.
|
// 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
|
// 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.
|
// or words alone moved runs the same.
|
||||||
func (s *Shadow) takeRan(u agent.Update) bool {
|
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 {
|
if err != nil || isNode || node != s.c.env.Node {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
before, had := s.ran[module]
|
before, had := s.ran[module]
|
||||||
h, err := delivery.DecodeHave(u.Value)
|
h, err := nodeengineclient.DecodeHave(u.Value)
|
||||||
if s.heldNow == nil {
|
if s.heldNow == nil {
|
||||||
s.heldNow = map[string]string{}
|
s.heldNow = map[string]string{}
|
||||||
}
|
}
|
||||||
@@ -615,18 +617,18 @@ func (s *Shadow) takeRan(u agent.Update) bool {
|
|||||||
s.heldNow[module] = ""
|
s.heldNow[module] = ""
|
||||||
if !u.Deleted && err == nil {
|
if !u.Deleted && err == nil {
|
||||||
switch {
|
switch {
|
||||||
case h.Phase == delivery.PhaseHeld:
|
case h.Phase == nodeengineclient.PhaseHeld:
|
||||||
s.heldNow[module] = "held"
|
s.heldNow[module] = "held"
|
||||||
case h.Phase == delivery.PhaseFailed:
|
case h.Phase == nodeengineclient.PhaseFailed:
|
||||||
s.heldNow[module] = "failed"
|
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
|
// 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.
|
// 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"
|
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
|
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)
|
delete(s.ran, module)
|
||||||
return had || holdMoved
|
return had || holdMoved
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"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
|
// 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.
|
// 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.
|
// Env is everything the follower needs of the node.
|
||||||
type Env struct {
|
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)
|
moved := file.module != f.cur.module || pathOf(file.resource) != pathOf(f.cur.resource)
|
||||||
f.cur = file
|
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()
|
f.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.mu.Unlock()
|
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()}
|
Follows: delivery.FollowGrants, Key: f.key(), Unwatched: f.unwatchedNow()}
|
||||||
var err error
|
var err error
|
||||||
var changed bool
|
var changed bool
|
||||||
@@ -370,7 +371,7 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
w.Users = Users(body)
|
w.Users = int64(Users(body))
|
||||||
if err = f.guard(resource, body); err != nil {
|
if err = f.guard(resource, body); err != nil {
|
||||||
if disk, derr := f.env.OnDisk(resource); derr == nil && disk != nil {
|
if disk, derr := f.env.OnDisk(resource); derr == nil && disk != nil {
|
||||||
w.Digest = Digest(disk)
|
w.Digest = Digest(disk)
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
|
|||||||
failWrite bool
|
failWrite bool
|
||||||
onDisk string
|
onDisk string
|
||||||
wantWrites []string // contents written, in order
|
wantWrites []string // contents written, in order
|
||||||
wantUsers int
|
wantUsers int64
|
||||||
wantErr string
|
wantErr string
|
||||||
wantRefused bool
|
wantRefused bool
|
||||||
grants []gput
|
grants []gput
|
||||||
|
|||||||
Reference in New Issue
Block a user