Merge branch 'b6/registry' into feat/registry-milestone

This commit is contained in:
2026-11-05 15:12:02 +01:00
58 changed files with 986 additions and 516 deletions
+30 -30
View File
@@ -126,9 +126,9 @@ type Env struct {
// Apply applies one module's composition (scope NodeModule for the node's own), given the compositions the node's
// other modules run so a path, unit or group they hold is not removed or taken. It is called with one module at a
// time.
Apply func(ctx context.Context, c delivery.Composed, others []delivery.Composed) Result
Apply func(ctx context.Context, c compositions.Composed, others []compositions.Composed) Result
// Remove removes the module's own resources and nothing of another's.
Remove func(ctx context.Context, module string, others []delivery.Composed) Result
Remove func(ctx context.Context, module string, others []compositions.Composed) Result
// Backup backs a module up before a new composition of it is applied; nil backs up nothing.
Backup func(ctx context.Context, module string) Backup
// Kept is the resources the node last applied, by module, from the declaration it kept before the switch (ADR 0539
@@ -143,12 +143,12 @@ type Env struct {
KeptFilled func() map[string][]string
// Recorded says the engine holds a record of every resource of the composition, so it was applied and not removed
// since; nil says yes. A composition the kept declaration or a have says runs is applied again when it says no.
Recorded func(ctx context.Context, c delivery.Composed) bool
Recorded func(ctx context.Context, c compositions.Composed) bool
// Inactive says a resource the composition declares running is not active on the machine now (a unit stopped, a
// container not running), read from the machine and not from any record (ADR 0568 §1). A have or a kept declaration
// that says the module runs is believed at the agent's start only when this says no; nil says no, and an unreadable
// machine says no (unknown is not inactive).
Inactive func(ctx context.Context, c delivery.Composed) bool
Inactive func(ctx context.Context, c compositions.Composed) bool
// Revisions are a composition key's revisions the bucket keeps, oldest first; nil reads none. The composition a want
// names is taken from them when a newer one replaced it under the same key (ADR 0539 clause 1).
Revisions func(ctx context.Context, key string) ([]Update, error)
@@ -282,7 +282,7 @@ type wantNow struct {
type compNow struct {
Raw []byte
C delivery.Composed
C compositions.Composed
Dig string
Rev uint64
Err error
@@ -722,7 +722,7 @@ func (a *Agent) sealedOf(ctx context.Context, module, fp, composed string) compo
// Running is the compositions the node runs now: each module's that the agent applied or found already running, and
// the node's own. What the node judges its health by and says of itself is built from them.
func (a *Agent) Running() []delivery.Composed {
func (a *Agent) Running() []compositions.Composed {
a.mu.Lock()
defer a.mu.Unlock()
mods := make([]string, 0, len(a.running))
@@ -730,7 +730,7 @@ func (a *Agent) Running() []delivery.Composed {
mods = append(mods, m)
}
sort.Strings(mods)
var out []delivery.Composed
var out []compositions.Composed
for _, m := range mods {
h := a.running[m]
if h.Phase == delivery.PhaseRemoved {
@@ -781,7 +781,7 @@ func (a *Agent) takeWant(u Update) string {
// takeComposition takes a change of a composition key and returns the module it concerns.
func (a *Agent) takeComposition(u Update) string {
node, module, _, isNode, err := delivery.ParseComposedKey(u.Key)
node, module, _, isNode, err := compositions.ParseKey(u.Key)
if err != nil || node != a.env.Node {
return ""
}
@@ -795,14 +795,14 @@ func (a *Agent) takeComposition(u Update) string {
return module
}
now := compNow{Raw: u.Value, Rev: u.Revision}
c, err := delivery.DecodeComposed(u.Value)
c, err := compositions.Decode(u.Value)
if err == nil {
// The node composed it itself: it is applied as composed, never verified against a signer, since no composition
// read from the bus is applied (novox/hq ADR 0547 §2, ADR 0593 §6).
now.C = c
now.Dig, err = delivery.ComposedDigestOf(c)
now.Dig, err = compositions.DigestOf(c)
if err != nil {
now.C = delivery.Composed{}
now.C = compositions.Composed{}
}
} else {
a.sayOnce("decode:"+u.Key, fmt.Sprintf("the composition %s cannot be read, and nothing of it is applied: %v", u.Key, err))
@@ -844,9 +844,9 @@ func (a *Agent) compositionFor(module, fp string) (compNow, bool) {
var key string
var err error
if module == NodeModule {
key, err = delivery.ComposedNodeKey(a.env.Node)
key, err = compositions.NodeKey(a.env.Node)
} else {
key, err = delivery.ComposedKey(a.env.Node, module, fp)
key, err = compositions.Key(a.env.Node, module, fp)
}
if err != nil {
return compNow{}, false
@@ -879,8 +879,8 @@ func (a *Agent) revisionNamed(ctx context.Context, module, fp, digest string) (c
// others are what the node's other modules run now, so what they hold is not taken: the compositions the agent runs and,
// for every module the agent does not hold yet, the resources the kept declaration has for it (ADR 0429 §4).
func (a *Agent) others(except string) []delivery.Composed {
var out []delivery.Composed
func (a *Agent) others(except string) []compositions.Composed {
var out []compositions.Composed
mods := make([]string, 0, len(a.running))
for m := range a.running {
if m != except {
@@ -1011,7 +1011,7 @@ func (a *Agent) loadApplied(ctx context.Context) {
if rs, ok := a.env.Kept()[NodeModule]; ok && len(rs) > 0 {
c, _ := a.keptComposed(NodeModule, rs)
c.Module = ""
raw, eerr := delivery.EncodeComposed(c)
raw, eerr := compositions.Encode(c)
if eerr == nil {
eerr = a.env.Applied.Keep(NodeModule, raw)
}
@@ -1035,10 +1035,10 @@ func (a *Agent) loadApplied(ctx context.Context) {
if ctx.Err() != nil {
return
}
c, err := delivery.DecodeComposed(kept[m])
c, err := compositions.Decode(kept[m])
var dig string
if err == nil {
dig, err = delivery.ComposedDigestOf(c)
dig, err = compositions.DigestOf(c)
}
if err != nil {
a.env.Say(fmt.Sprintf("the composition this engine kept of %s cannot be read, so what runs of it is read from its have: %v", m, err))
@@ -1077,7 +1077,7 @@ func (a *Agent) keepApplied(module string, comp compNow) {
raw := comp.Raw
if len(raw) == 0 {
var err error
if raw, err = delivery.EncodeComposed(comp.C); err != nil {
if raw, err = compositions.Encode(comp.C); err != nil {
a.env.Say(fmt.Sprintf("the composition %s was applied from cannot be kept: %v", module, err))
return
}
@@ -1126,7 +1126,7 @@ func (a *Agent) unreadableModules() []string {
}
current := map[string]pick{}
for key, c := range a.comps {
_, m, fp, isNode, err := delivery.ParseComposedKey(key)
_, m, fp, isNode, err := compositions.ParseKey(key)
if err != nil || isNode {
continue
}
@@ -1159,7 +1159,7 @@ func (a *Agent) unreadableModules() []string {
// until that one is applied, so the node's own apply never removes them in between (issue 1945, live on 2026-10-25:
// shanks lost systemd-resolved's guard while its composition waited for a credential). The composition its want names,
// else its newest. Not part of Everything: what has not been applied is not what runs.
func (a *Agent) heldOut(except string) []delivery.Composed {
func (a *Agent) heldOut(except string) []compositions.Composed {
out := a.others(except)
held := map[string]bool{}
for _, c := range out {
@@ -1167,7 +1167,7 @@ func (a *Agent) heldOut(except string) []delivery.Composed {
}
waiting := map[string]compNow{}
for key, c := range a.comps {
_, m, fp, isNode, err := delivery.ParseComposedKey(key)
_, m, fp, isNode, err := compositions.ParseKey(key)
if err != nil || isNode || c.Err != nil || m == except || held[m] {
continue
}
@@ -1203,7 +1203,7 @@ func (a *Agent) heldOut(except string) []delivery.Composed {
if len(a.heldAlone) == 0 {
return out
}
var own *delivery.Composed
var own *compositions.Composed
for _, c := range a.others("\x00") {
if c.Module == "" {
own = &c
@@ -1237,7 +1237,7 @@ func (a *Agent) heldOut(except string) []delivery.Composed {
}
if len(rs) > 0 {
// Held out, never judged: its Data from its manifest where known, as every other composition says it.
out = append(out, delivery.Composed{Node: a.env.Node, Module: m, Resources: rs, Data: a.keptData[m]})
out = append(out, compositions.Composed{Node: a.env.Node, Module: m, Resources: rs, Data: a.keptData[m]})
}
}
return out
@@ -1246,7 +1246,7 @@ func (a *Agent) heldOut(except string) []delivery.Composed {
// Everything is what the node runs: the compositions the agent runs, and the kept declaration's resources for each module
// the agent does not hold yet. What the node judges its health by, and what the filling, the take act and the judgement
// of directories read, is built from it.
func (a *Agent) Everything() []delivery.Composed {
func (a *Agent) Everything() []compositions.Composed {
a.mu.Lock()
defer a.mu.Unlock()
return a.others("\x00")
@@ -1550,7 +1550,7 @@ func (a *Agent) inactiveAtStart(ctx context.Context, module string, comp compNow
return true
}
func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNow, others []delivery.Composed) {
func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNow, others []compositions.Composed) {
fp, dig := w.Want.FP, w.Want.Composed
a.mu.Lock()
_, ranBefore := a.running[module]
@@ -1840,7 +1840,7 @@ func (a *Agent) buildAlone(ctx context.Context, module string, prev delivery.Hav
// The composition its have named at the start comes before the kept declaration (issue 924). From the kept declaration,
// what runs carries Data as its manifest declares it; a module whose manifest could not be read cannot be judged against
// it (false), and that is said, rather than taking it to hold no data and judging a false move of its data.
func (a *Agent) runningComposition(module string) (delivery.Composed, bool) {
func (a *Agent) runningComposition(module string) (compositions.Composed, bool) {
if c, ok := a.applied[module]; ok && c.Err == nil {
return c.C, true
}
@@ -1855,14 +1855,14 @@ func (a *Agent) runningComposition(module string) (delivery.Composed, bool) {
}
return c, known
}
return delivery.Composed{}, false
return compositions.Composed{}, false
}
// keptComposed is what runs of a module from the kept declaration: its resources there, the paths it names as filled,
// and Data as the module's manifest declares it; false when the manifest could not be read, so Data is not known. Called
// with a.mu held.
func (a *Agent) keptComposed(module string, rs []json.RawMessage) (delivery.Composed, bool) {
c := delivery.Composed{Node: a.env.Node, Module: module, Resources: rs}
func (a *Agent) keptComposed(module string, rs []json.RawMessage) (compositions.Composed, bool) {
c := compositions.Composed{Node: a.env.Node, Module: module, Resources: rs}
if a.env.KeptFilled != nil {
c.Filled = a.env.KeptFilled()[module]
}
+41 -41
View File
@@ -123,7 +123,7 @@ type bed struct {
during func()
backup func(module string) Backup
kept map[string][]json.RawMessage
others [][]delivery.Composed
others [][]compositions.Composed
said []string
// settled is each module Settled was told, and settledRan whether what runs held it then.
settled []string
@@ -135,7 +135,7 @@ type bed struct {
func newBed(t *testing.T) *bed {
b := &bed{t: t, wants: newFeed(), comps: newFeed(), haves: &haves{}}
b.env = Env{Node: node, Wants: b.wants, Composed: b.comps, Haves: b.haves,
Apply: func(_ context.Context, c delivery.Composed, others []delivery.Composed) Result {
Apply: func(_ context.Context, c compositions.Composed, others []compositions.Composed) Result {
if b.during != nil {
b.during()
}
@@ -156,7 +156,7 @@ func newBed(t *testing.T) *bed {
}
return Result{}
},
Remove: func(_ context.Context, m string, _ []delivery.Composed) Result {
Remove: func(_ context.Context, m string, _ []compositions.Composed) Result {
b.mu.Lock()
defer b.mu.Unlock()
b.removed = append(b.removed, m)
@@ -222,13 +222,13 @@ func file(module, id, content string) json.RawMessage {
// composition is a module's composition at a fingerprint: its key, its bytes and its digest, from the writer's code.
func composition(t *testing.T, module, fp string, data bool, rs ...json.RawMessage) (key string, raw []byte, digest string) {
t.Helper()
c := delivery.Composed{Node: node, Module: module, FP: fp, Data: data, Resources: rs}
c := compositions.Composed{Node: node, Module: module, FP: fp, Data: data, Resources: rs}
var err error
if module == NodeModule {
c.Module, c.FP = "", ""
key, err = delivery.ComposedNodeKey(node)
key, err = compositions.NodeKey(node)
} else {
key, err = delivery.ComposedKey(node, module, fp)
key, err = compositions.Key(node, module, fp)
}
if err != nil {
t.Fatal(err)
@@ -237,13 +237,13 @@ func composition(t *testing.T, module, fp string, data bool, rs ...json.RawMessa
}
// encodedAs is c as the node's composer hands it: unsigned (ADR 0593 §6), its key, its bytes and its digest.
func encodedAs(t *testing.T, c delivery.Composed, key string) (string, []byte, string) {
func encodedAs(t *testing.T, c compositions.Composed, key string) (string, []byte, string) {
t.Helper()
raw, err := delivery.EncodeComposed(c)
raw, err := compositions.Encode(c)
if err != nil {
t.Fatal(err)
}
digest, err := delivery.ComposedDigestOf(c)
digest, err := compositions.DigestOf(c)
if err != nil {
t.Fatal(err)
}
@@ -987,20 +987,20 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
b.env.Applied = store
// A folded module's resource in the node's own composition: its id names the module, as the controller folds it.
natsConf, _ := json.Marshal(map[string]any{"id": "nats.conf", "type": "file", "path": "/tmp/r6/nats/conf", "content": "listen 4222"})
own := delivery.Composed{Node: node, Resources: []json.RawMessage{natsConf}}
ownRaw, err := delivery.EncodeComposed(own)
own := compositions.Composed{Node: node, Resources: []json.RawMessage{natsConf}}
ownRaw, err := compositions.Encode(own)
if err != nil {
t.Fatal(err)
}
ownDig, _ := delivery.ComposedDigestOf(own)
web := delivery.Composed{Node: node, Module: "web", FP: fpOf("a"), Resources: []json.RawMessage{file("web", "x", "1")}}
webRaw, _ := delivery.EncodeComposed(web)
webDig, _ := delivery.ComposedDigestOf(web)
ownDig, _ := compositions.DigestOf(own)
web := compositions.Composed{Node: node, Module: "web", FP: fpOf("a"), Resources: []json.RawMessage{file("web", "x", "1")}}
webRaw, _ := compositions.Encode(web)
webDig, _ := compositions.DigestOf(web)
store.kept[NodeModule], store.kept["web"] = ownRaw, webRaw
b.haves.Write(context.Background(), Have{Module: NodeModule, Composed: ownDig})
b.haves.Write(context.Background(), Have{Module: "web", FP: fpOf("a"), Composed: webDig, Phase: string(delivery.PhaseRunning)})
// The composer's first pass reads what runs when its watch begins, before the agent reads any have.
firstCh := make(chan []delivery.Composed, 1)
firstCh := make(chan []compositions.Composed, 1)
comps := b.comps
var ag *Agent
b.env.Composed = watchFunc(func(ctx context.Context, p string) (<-chan Update, error) {
@@ -1013,7 +1013,7 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
done := make(chan struct{})
go func() { defer close(done); _ = ag.Run(ctx) }()
defer func() { cancel(); <-done }()
var first []delivery.Composed
var first []compositions.Composed
select {
case first = <-firstCh:
case <-time.After(5 * time.Second):
@@ -1029,9 +1029,9 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
ranOwn, ranWeb, len(first))
}
// An apply keeps the composition it applied; a removal forgets it.
web2 := delivery.Composed{Node: node, Module: "web", FP: fpOf("b"), Resources: []json.RawMessage{file("web", "x", "2")}}
ck, _ := delivery.ComposedKey(node, "web", fpOf("b"))
raw2, _ := delivery.EncodeComposed(web2)
web2 := compositions.Composed{Node: node, Module: "web", FP: fpOf("b"), Resources: []json.RawMessage{file("web", "x", "2")}}
ck, _ := compositions.Key(node, "web", fpOf("b"))
raw2, _ := compositions.Encode(web2)
comps.put(ck, raw2)
wk, _ := delivery.WantKey(node, "web")
wraw, _ := delivery.EncodeWant(delivery.Want{FP: fpOf("b"), Self: true})
@@ -1050,10 +1050,10 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
store := &memApplied{kept: map[string][]byte{}}
h := &haves{}
h.Write(context.Background(), Have{Module: NodeModule, Composed: "sha256:" + strings.Repeat("c", 64)})
firstPass := func(kept map[string][]json.RawMessage) []delivery.Composed {
firstPass := func(kept map[string][]json.RawMessage) []compositions.Composed {
b := newBed(t)
b.env.SelfComposed, b.kept, b.env.Applied, b.env.Haves = true, kept, store, h
firstCh := make(chan []delivery.Composed, 1)
firstCh := make(chan []compositions.Composed, 1)
comps := b.comps
var ag *Agent
b.env.Composed = watchFunc(func(ctx context.Context, p string) (<-chan Update, error) {
@@ -1073,7 +1073,7 @@ func TestEverythingIsWhatTheAgentRunsAndWhatIsKeptForTheRest(t *testing.T) {
}
return nil
}
runsNats := func(cs []delivery.Composed) bool {
runsNats := func(cs []compositions.Composed) bool {
for _, c := range cs {
if c.Module == "" && len(c.Resources) == 1 && strings.Contains(string(c.Resources[0]), "4222") {
return true
@@ -1281,7 +1281,7 @@ func testARemovedModulesCompositionHoldsNothingBack(t *testing.T) {
// it (issue 1945, the review's gap 2). It is said, naming the composition, and applied once that one reads.
func TestTheNodesOwnWaitsWhileAModulesCompositionCannotBeRead(t *testing.T) {
b := newBed(t)
bad, err := delivery.ComposedKey(node, "broken", fpOf("c"))
bad, err := compositions.Key(node, "broken", fpOf("c"))
if err != nil {
t.Fatal(err)
}
@@ -1317,7 +1317,7 @@ func TestTheNodesOwnWaitsWhileAModulesCompositionCannotBeRead(t *testing.T) {
// compositions written before the filled paths, which the SDK now refuses, and nobody applies them).
func TestAStaleRefusedCompositionDoesNotHoldTheNodesOwnBack(t *testing.T) {
b := newBed(t)
stale, err := delivery.ComposedKey(node, "power", fpOf("a"))
stale, err := compositions.Key(node, "power", fpOf("a"))
if err != nil {
t.Fatal(err)
}
@@ -1353,7 +1353,7 @@ func TestACompositionWhoseResourcesWereRemovedIsAppliedAgain(t *testing.T) {
} else {
b.haves.stored = map[string]Have{"ca-trust": {Module: "ca-trust", FP: fp, Composed: dig, Phase: "running"}}
}
b.env.Recorded = func(context.Context, delivery.Composed) bool { return false }
b.env.Recorded = func(context.Context, compositions.Composed) bool { return false }
stop := b.run()
defer stop()
eventually(t, "not applied", func() bool { return len(b.appliedList()) == 1 })
@@ -1402,8 +1402,8 @@ func TestWhatRunsBeforeTheAgentTakesAModuleNamesItsFilledPaths(t *testing.T) {
`","mode":"0644","owner-at-runtime":"owned"}`)
}
fp := fpOf("a")
ck, _ := delivery.ComposedKey(node, "power", fp)
applied := delivery.Composed{Node: node, Module: "power", FP: fp, Filled: []string{old, added}, Data: true,
ck, _ := compositions.Key(node, "power", fp)
applied := compositions.Composed{Node: node, Module: "power", FP: fp, Filled: []string{old, added}, Data: true,
Resources: []json.RawMessage{filledFile("settings"), filledFile("bound")}}
_, appliedRaw, appliedDig := encodedAs(t, applied, ck)
newer := applied
@@ -1527,13 +1527,13 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
stop := b.run()
defer stop()
unsigned := func(module, fp string, rs ...json.RawMessage) (string, []byte, string) {
c := delivery.Composed{Node: node, Module: module, FP: fp, Resources: rs}
key, _ := delivery.ComposedKey(node, module, fp)
raw, err := delivery.EncodeComposed(c)
c := compositions.Composed{Node: node, Module: module, FP: fp, Resources: rs}
key, _ := compositions.Key(node, module, fp)
raw, err := compositions.Encode(c)
if err != nil {
t.Fatal(err)
}
dig, _ := delivery.ComposedDigestOf(c)
dig, _ := compositions.DigestOf(c)
return key, raw, dig
}
want := func(module, fp, yes string) {
@@ -1567,10 +1567,10 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
// The node's own composition carries its sealed values in its key, as a module's have does, so a restart reuses its
// core boxes and applies nothing again (review of L2, round 3, finding 1).
box, _ := json.Marshal(map[string]any{"id": "core-box", "type": "file", "path": "/tmp/r6/core-box", "sealed": "core"})
own := delivery.Composed{Node: node, Resources: []json.RawMessage{box}}
nk, _ := delivery.ComposedNodeKey(node)
nraw, _ := delivery.EncodeComposed(own)
ndig, _ := delivery.ComposedDigestOf(own)
own := compositions.Composed{Node: node, Resources: []json.RawMessage{box}}
nk, _ := compositions.NodeKey(node)
nraw, _ := compositions.Encode(own)
ndig, _ := compositions.DigestOf(own)
b.comps.put(nk, nraw)
nwk, _ := delivery.WantNodeKey(node)
nwraw, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
@@ -1707,8 +1707,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
}
// Issue 774 (ace, 2026-11-04 01:29): the node's own revision shows its own fields, the provisioner client's account
// and the adoption among them, and what the composer's last pass held.
ownKey, ownRaw, ownDig := encodedAs(t, delivery.Composed{Node: node, Own: delivery.NodeOwn{Provisioner: "mesh-provisioner-client",
Adoption: &declaration.Adoption{Taken: []string{"web"}}}}, func() string { k, _ := delivery.ComposedNodeKey(node); return k }())
ownKey, ownRaw, ownDig := encodedAs(t, compositions.Composed{Node: node, Own: compositions.NodeOwn{Provisioner: "mesh-provisioner-client",
Adoption: &declaration.Adoption{Taken: []string{"web"}}}}, func() string { k, _ := compositions.NodeKey(node); return k }())
keep(ownKey, ownRaw)
if view, err := b.ag.Composition(context.Background(), NodeModule, ownDig, false); err != nil || view.Own == nil ||
view.Own.Provisioner != "mesh-provisioner-client" || view.Own.Adoption == nil || view.Own.Adoption.Taken[0] != "web" ||
@@ -1717,9 +1717,9 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
}
// novox/hq ADR 0593 §6: the engine publishes each composition with its plan in its composition state; the verb reads
// that revision by the digest its have and want name, its resources' (the plan left out of the digest).
planned, _ := delivery.DecodeComposed(draw)
planned, _ := compositions.Decode(draw)
planned.Plan = &catalogue.Plan{}
praw, err := delivery.EncodeComposed(planned)
praw, err := compositions.Encode(planned)
if err != nil {
t.Fatal(err)
}
@@ -1823,7 +1823,7 @@ func testAnEngineStartReconcilesAModuleFromTheMachinesFacts(t *testing.T) {
}
var mu sync.Mutex
asked := false
b.env.Inactive = func(context.Context, delivery.Composed) bool {
b.env.Inactive = func(context.Context, compositions.Composed) bool {
mu.Lock()
defer mu.Unlock()
asked = true
+7 -6
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration"
@@ -45,7 +46,7 @@ type CompositionView struct {
Own *OwnView `json:"own,omitempty"`
}
// OwnView is the node's own fields of a composition (delivery.NodeOwn) beside what runs of them, and what the node's
// OwnView is the node's own fields of a composition (engcomp.NodeOwn) beside what runs of them, and what the node's
// composer held in its last pass.
type OwnView struct {
// Provisioner is the account the provisioner's client runs as, as the revision read names it; RunsProvisioner as
@@ -218,7 +219,7 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo
view.Composition.Refused = read.Err.Error()
return view, nil
}
var running *delivery.Composed
var running *engcomp.Composed
if known {
running = &from
rv := &RunsView{From: source, Resources: len(from.Resources), FP: from.FP}
@@ -255,9 +256,9 @@ func (a *Agent) Composition(ctx context.Context, module, digest string, diff boo
// compositionKey is a module's composition key at a build, or the node's own.
func (a *Agent) compositionKey(module, fp string) (string, error) {
if module == NodeModule {
return delivery.ComposedNodeKey(a.env.Node)
return engcomp.NodeKey(a.env.Node)
}
return delivery.ComposedKey(a.env.Node, module, fp)
return engcomp.Key(a.env.Node, module, fp)
}
// revisionsOf is every revision of a composition key the state keeps, oldest first, each with its digest (unsigned, ADR
@@ -276,12 +277,12 @@ func (a *Agent) revisionsOf(ctx context.Context, key string) ([]compNow, error)
if u.Deleted || u.Key != key {
continue
}
c, err := delivery.DecodeComposed(u.Value)
c, err := engcomp.Decode(u.Value)
if err != nil {
out = append(out, compNow{Raw: u.Value, Rev: u.Revision, Err: err})
continue
}
dig, err := delivery.ComposedDigestOf(c) // unsigned: a record, never verified (ADR 0593 §6)
dig, err := engcomp.DigestOf(c) // unsigned: a record, never verified (ADR 0593 §6)
out = append(out, compNow{Raw: u.Value, C: c, Dig: dig, Rev: u.Revision, Err: err})
}
return out, nil
+15 -14
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"slices"
"sort"
"strings"
@@ -17,14 +18,14 @@ import (
// Compositions is one node's compositions (novox/hq ADR 0539 clauses 1 and 2): its declaration composed over in, as
// the controller composes it, split into each module's composition at the fingerprint fps gives it and the node's own
// (`<node>.node`), by key (delivery.ComposedKey). Each resource says its module, whatever the node reported it reads.
// (`<node>.node`), by key (engcomp.Key). Each resource says its module, whatever the node reported it reads.
// A module fps gives no fingerprint for is not composed and is named in skipped, so a caller composing one module
// gets that one; but for a module the node runs inside its own composition (Node.InOwn, and the
// private network's always), which fps names at no fingerprint of the new form: its resources go into the node's own,
// their module taken off and their ids still naming it, and its filled paths with them, as the controller's composed
// writer folds them (issue 1126). Each module's Data says whether its manifest declares data. The compositions are unsigned
// (ADR 0593 §6) and not yet by digest (Composed.ByDigest).
func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]delivery.Composed, skipped []string, err error) {
func Compositions(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped []string, err error) {
out, skipped, _, err = compositions(in, node, fps, mode, false)
return out, skipped, err
}
@@ -41,7 +42,7 @@ const BusUsersStandIn = "the bus's user list"
// 1126): a declaration that stands in for anything (Declared.StoodIn, Foreseen, ForeseenPorts) is refused with
// ErrStoodIn, naming each, so a stand-in is never applied as if it were the value; but for the bus's user list
// (BusUsersStandIn), which the bus node holds out of every apply of a composition it made itself.
func CompositionsApplied(in Input, node string, fps map[string]string) (map[string]delivery.Composed, []string, error) {
func CompositionsApplied(in Input, node string, fps map[string]string) (map[string]engcomp.Composed, []string, error) {
out, skipped, _, err := compositions(in, node, fps, Reading, true)
return out, skipped, err
}
@@ -52,14 +53,14 @@ func CompositionsApplied(in Input, node string, fps map[string]string) (map[stri
// the bus's user list left out) rather than refused, so the caller judges each stand-in: a value not yet issued for a
// module the candidate adds, or one missing for a module the node runs. A module whose every value is held composes
// as CompositionsApplied composes it: the modes differ only in an own secret never issued.
func CompositionsShadow(in Input, node string, fps map[string]string) (out map[string]delivery.Composed, skipped, stood []string, err error) {
func CompositionsShadow(in Input, node string, fps map[string]string) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
return compositions(in, node, fps, Foreseeing, false)
}
// CompositionsIn is the node's compositions in mode with what they stand in for answered (stood, sorted, the bus's user
// list left out) rather than refused: CompositionsShadow in Foreseeing, and in Reading what CompositionsApplied refuses
// before it is refused, so a caller that applies refuses each stand-in to the module it is for alone (issue 1126).
func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (out map[string]delivery.Composed, skipped, stood []string, err error) {
func CompositionsIn(in Input, node string, fps map[string]string, mode Mode) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
return compositions(in, node, fps, mode, false)
}
@@ -72,7 +73,7 @@ func Folds(inOwn bool, fp string) bool {
return inOwn && !strings.HasPrefix(fp, delivery.SourcePrefix2)
}
func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]delivery.Composed, skipped, stood []string, err error) {
func compositions(in Input, node string, fps map[string]string, mode Mode, applied bool) (out map[string]engcomp.Composed, skipped, stood []string, err error) {
reading := in
reading.Nodes = append([]Node(nil), in.Nodes...)
found := false
@@ -102,7 +103,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
// (Carried), so what it provides and requires still shapes the rest and what the others read of it (its published
// ports, the paths it owns) is what runs, and its resources are then the ones the node runs.
carried := map[string]bool{}
var runsOwn *delivery.Composed
var runsOwn *engcomp.Composed
for i := range reading.Nodes {
n := &reading.Nodes[i]
if n.Name != node || n.RunsOwn == nil {
@@ -264,13 +265,13 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
filled = append(filled, p)
}
}
out, err = delivery.SplitComposed(node, kept, fps, filled,
delivery.NodeOwn{Adoption: envelope.Adoption, Provisioner: envelope.Provisioner})
out, err = engcomp.Split(node, kept, fps, filled,
engcomp.NodeOwn{Adoption: envelope.Adoption, Provisioner: envelope.Provisioner})
if err != nil {
return nil, nil, nil, err
}
if len(ownFilled) > 0 {
own, err := delivery.ComposedNodeKey(node)
own, err := engcomp.NodeKey(node)
if err != nil {
return nil, nil, nil, err
}
@@ -296,7 +297,7 @@ func compositions(in Input, node string, fps map[string]string, mode Mode, appli
}
// RunsAnyOf says runs (a composition as it runs) holds a resource of module, by its module or the module its id names.
func RunsAnyOf(runs *delivery.Composed, module string) bool {
func RunsAnyOf(runs *engcomp.Composed, module string) bool {
if runs == nil {
return false
}
@@ -324,7 +325,7 @@ func RunsAnyOf(runs *delivery.Composed, module string) bool {
// (catalogue.RuntimeToolModules, catalogue.RuntimeToolEnv, the mesh's own words left out). A bundle the runtime loads
// nothing of has no Loads: the runtime dropped it, and the composition reads its loads again from the module's build
// (issue 1126, review: shanks had already applied the drop of systemd-resolved). Nil when running holds no runtime process.
func ToolsRunning(running []delivery.Composed) map[string][]catalogue.Bundle {
func ToolsRunning(running []engcomp.Composed) map[string][]catalogue.Bundle {
type resource struct {
ID string `json:"id"`
Module string `json:"module"`
@@ -476,7 +477,7 @@ func Stub(m catalogue.Manifest) catalogue.Manifest {
// is composed into what is applied either way (compositions leaves a carried module's out, and carries the ones that
// run). What it provides, requires, listens on and grants is its manifest's. A module whose resources runs holds none
// of is its Stub, as before.
func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catalogue.Manifest {
func Carried(m catalogue.Manifest, module string, runs *engcomp.Composed) catalogue.Manifest {
if runs == nil {
return Stub(m)
}
@@ -521,7 +522,7 @@ func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catal
// carry puts into the node's resources (kept, in the declaration's order) the resources of the node's own composition as
// it runs (runs) that belong to a carried module, each after the resource it runs after, or first when that one is
// not among them; a file among them is a folded path.
func carry(kept []json.RawMessage, foldedPaths map[string]bool, runs *delivery.Composed, carried func(module, id string) bool) ([]json.RawMessage, map[string]bool, error) {
func carry(kept []json.RawMessage, foldedPaths map[string]bool, runs *engcomp.Composed, carried func(module, id string) bool) ([]json.RawMessage, map[string]bool, error) {
idOf := func(raw json.RawMessage) (string, string, string, string, error) {
var head struct {
ID string `json:"id"`
+15 -14
View File
@@ -2,6 +2,7 @@ package compose
import (
"encoding/json"
engcomp "github.com/novox/mesh-host/internal/compositions"
"sort"
"strings"
"testing"
@@ -36,14 +37,14 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
}
var got []string
for key, c := range out {
node, module, fp, isNode, err := delivery.ParseComposedKey(key)
node, module, fp, isNode, err := engcomp.ParseKey(key)
if err != nil || node != "laptop" || module != c.Module || fp != c.FP || (isNode != (module == "")) {
t.Errorf("key %s holds %s at %s (%v)", key, c.Module, c.FP, err)
}
if !isNode && fp != fps[module] {
t.Errorf("%s is composed at %s, given %s", module, fp, fps[module])
}
if _, err := delivery.EncodeComposed(c); err != nil {
if _, err := engcomp.Encode(c); err != nil {
t.Errorf("%s: %v", key, err)
}
for _, raw := range c.Resources {
@@ -81,7 +82,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
}
}
all, skipped, err := Compositions(folding, "laptop", nil, Foreseeing)
nodeKey, _ := delivery.ComposedNodeKey("laptop")
nodeKey, _ := engcomp.NodeKey("laptop")
if err != nil || len(skipped) != 0 || len(all) != 1 || len(all[nodeKey].Resources) != len(want) {
t.Fatalf("folded: %v, skipped %v, %d compositions, %d of %d resources in the node's own", err, skipped, len(all), len(all[nodeKey].Resources), len(want))
}
@@ -92,7 +93,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
t.Errorf("folded resource %d is %v, the declaration's %v", i, r["id"], d.Resources[i]["id"])
}
}
if _, err := delivery.EncodeComposed(all[nodeKey]); err != nil {
if _, err := engcomp.Encode(all[nodeKey]); err != nil {
t.Errorf("the node's own with the folded modules: %v", err)
}
if _, _, err := Compositions(in, "nowhere", fps, Foreseeing); err == nil {
@@ -121,35 +122,35 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
t.Fatal(err)
}
served := "lamp=" + resolution.BundlePath("lamp", "tools") + "/lamp-tools"
runtimeRuns := func(dropped bool) delivery.Composed {
runtimeRuns := func(dropped bool) engcomp.Composed {
env := `"MESH_TOOL_MODULES":"` + served + `","MESH_TOOL_ENV":"{\"lamp\":{\"LAMP_CONF\":\"/var/lib/lamp/conf\"}}"`
if dropped {
env = `"MESH_TOOL_MODULES":""` // the drop already applied (shanks, 2026-10-29)
}
return delivery.Composed{Node: "anchor", Module: "node-tools", Resources: []json.RawMessage{json.RawMessage(
return engcomp.Composed{Node: "anchor", Module: "node-tools", Resources: []json.RawMessage{json.RawMessage(
`{"id":"node-tools.runtime","module":"node-tools","type":"process","name":"node-tools","env":{` + env + `}}`)}}
}
lampRuns := `{"id":"lamp.bundle-tools","type":"archive","source":"` + catalogue.ArtifactStoreScheme + `lamp/tools/blobs/` + dg('b') +
`","digest":"` + dg('b') + `","path":"` + resolution.BundlePath("lamp", "tools") + `"}`
for _, row := range []struct {
name string
set func(n *Node) delivery.Composed // and what runs of lamp
set func(n *Node) engcomp.Composed // and what runs of lamp
fps map[string]string
dropped bool
}{
{"folded at a build of the old form", func(n *Node) delivery.Composed {
{"folded at a build of the old form", func(n *Node) engcomp.Composed {
n.InOwn = map[string]bool{"lamp": true}
n.RunsOwn = &delivery.Composed{Node: "anchor", Resources: []json.RawMessage{json.RawMessage(lampRuns)}}
n.RunsOwn = &engcomp.Composed{Node: "anchor", Resources: []json.RawMessage{json.RawMessage(lampRuns)}}
return *n.RunsOwn
}, map[string]string{}, false},
{"folded, the runtime already dropped it", func(n *Node) delivery.Composed {
{"folded, the runtime already dropped it", func(n *Node) engcomp.Composed {
n.InOwn = map[string]bool{"lamp": true}
n.RunsOwn = &delivery.Composed{Node: "anchor", Resources: []json.RawMessage{json.RawMessage(lampRuns)}}
n.RunsOwn = &engcomp.Composed{Node: "anchor", Resources: []json.RawMessage{json.RawMessage(lampRuns)}}
return *n.RunsOwn
}, map[string]string{}, true},
{"held, its new build not delivered", func(n *Node) delivery.Composed {
{"held, its new build not delivered", func(n *Node) engcomp.Composed {
n.Held = map[string]bool{"lamp": true}
lr := delivery.Composed{Node: "anchor", Module: "lamp", Resources: []json.RawMessage{json.RawMessage(
lr := engcomp.Composed{Node: "anchor", Module: "lamp", Resources: []json.RawMessage{json.RawMessage(
strings.Replace(lampRuns, `"type"`, `"module":"lamp","type"`, 1))}}
n.Kept = map[string]catalogue.Manifest{"lamp": Carried(newLamp, "lamp", &lr)}
return lr
@@ -161,7 +162,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
anchor.Assigned = append(anchor.Assigned, "artifacts", "node-tools", "lamp")
tools.BusMinted = []string{"anchor." + "node-tools"}
anchor.SealingKey = keyOf(t, "anchor")
anchor.RunsTools = ToolsRunning([]delivery.Composed{runtimeRuns(row.dropped), row.set(anchor)})
anchor.RunsTools = ToolsRunning([]engcomp.Composed{runtimeRuns(row.dropped), row.set(anchor)})
row.fps[overlay.Name], row.fps["artifacts"], row.fps["node-tools"] = fpFor('a'), fpFor('5'), fpFor('e')
out, _, _, err := CompositionsIn(tools, "anchor", row.fps, Foreseeing)
if err != nil {
+22 -21
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"github.com/novox/mesh-host/internal/compositions"
"maps"
"regexp"
@@ -27,11 +28,11 @@ import (
// composition stands in for is judged: a value of a module the proposal adds (on this node or another: its credential, own
// secrets and boxes are made only once it is placed, ADR 0549 §5) is not yet issued, which is no refusal; any other
// stand-in is a value missing for a module the node runs, a refusal naming it ("node" when it is the node's own).
func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string, running []delivery.Composed, proposal string, revision uint64) delivery.CandidateAnswer {
a := delivery.CandidateAnswer{Proposal: proposal, Revision: revision}
refuse := func(err error) delivery.CandidateAnswer {
func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string, running []compositions.Composed, proposal string, revision uint64) nodeengine.CandidateAnswer {
a := nodeengine.CandidateAnswer{Proposal: proposal, Revision: int64(revision)}
refuse := func(err error) nodeengine.CandidateAnswer {
a.Refused = err.Error()
a.Verdict = delivery.CandidateRefused
a.Verdict = nodeengine.CandidateAnswerVerdictRefused
return a
}
trunk, err := r.Placements(ctx)
@@ -99,16 +100,16 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
if _, whole := failed[delivery.HaveNodeWord]; whole {
// Nothing composed to compare: every refusal is said, and nothing else.
for _, m := range sortedKeys(failed) {
a.Refusals = append(a.Refusals, delivery.ModuleRefused{Module: m, Why: failed[m]})
a.Refusals = append(a.Refusals, nodeengine.CandidateRefusal{Module: m, Why: failed[m]})
}
heldAsToday(&a, s.HeldToday)
a.Verdict = delivery.VerdictOf(a)
a.Verdict = compositions.VerdictOf(a)
return a
}
// A module the composition folds inside the node's own is judged there: the agent also says it runs by module, from
// the declaration it kept before the switch (agent.Everything), which is no removal (issue 1126, seed #531).
if len(s.InOwn) > 0 {
var judged []delivery.Composed
var judged []compositions.Composed
for _, c := range running {
if c.Module == "" || !slices.Contains(s.InOwn, c.Module) {
judged = append(judged, c)
@@ -126,7 +127,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
budget -= n
kept = append(kept, b)
} else {
kept = append(kept, delivery.ResourceBodies{ID: b.ID, Why: b.Why})
kept = append(kept, nodeengine.CandidateBodies{ID: b.ID, Why: b.Why})
a.Note = "some differing resources are named without their bodies: the answer holds at most " +
fmt.Sprint(MaxShown) + " bytes of them"
}
@@ -137,8 +138,8 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
notFailed := func(m string) bool { _, ok := failed[m]; return !ok }
a.Same = slices.DeleteFunc(cmp.Same, func(m string) bool { return !notFailed(m) })
a.Removed = slices.DeleteFunc(cmp.Removed, func(m string) bool { return !notFailed(m) })
a.Differs = slices.DeleteFunc(cmp.Differs, func(d delivery.ModuleDiffers) bool { return !notFailed(d.Module) })
cmp.Added = slices.DeleteFunc(cmp.Added, func(d delivery.ModuleAdded) bool { return !notFailed(d.Module) })
a.Differs = slices.DeleteFunc(cmp.Differs, func(d nodeengine.CandidateDiffers) bool { return !notFailed(d.Module) })
cmp.Added = slices.DeleteFunc(cmp.Added, func(d nodeengine.CandidateAdded) bool { return !notFailed(d.Module) })
skipped = slices.DeleteFunc(skipped, func(m string) bool { return !notFailed(m) })
// What the proposal adds, by node and module: placed by a candidate and not by the trunk on that node.
@@ -177,12 +178,12 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
refusals[module] = append(refusals[module], x)
}
}
addedHere := map[string]delivery.ModuleAdded{}
addedHere := map[string]nodeengine.CandidateAdded{}
for _, m := range cmp.Added {
addedHere[m.Module] = m
}
for _, m := range skipped { // placed, and no build given to compose it at
addedHere[m] = delivery.ModuleAdded{Module: m, Unissued: []string{"no build of " + m + " is known to compose it at"}}
addedHere[m] = nodeengine.CandidateAdded{Module: m, Unissued: []string{"no build of " + m + " is known to compose it at"}}
}
for m, xs := range unissued {
got := addedHere[m]
@@ -200,14 +201,14 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
if _, isFailed := failed[m]; isFailed {
continue // said below, as why it could not be composed
}
a.Refusals = append(a.Refusals, delivery.ModuleRefused{Module: m, Why: "stands in for " + strings.Join(refusals[m], ", ")})
a.Refusals = append(a.Refusals, nodeengine.CandidateRefusal{Module: m, Why: "stands in for " + strings.Join(refusals[m], ", ")})
}
for _, m := range sortedKeys(failed) {
a.Refusals = append(a.Refusals, delivery.ModuleRefused{Module: m, Why: failed[m]})
a.Refusals = append(a.Refusals, nodeengine.CandidateRefusal{Module: m, Why: failed[m]})
}
sort.Slice(a.Refusals, func(i, j int) bool { return a.Refusals[i].Module < a.Refusals[j].Module })
heldAsToday(&a, s.HeldToday)
a.Verdict = delivery.VerdictOf(a)
a.Verdict = compositions.VerdictOf(a)
return a
}
@@ -215,7 +216,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
// held, failed, running a build of the old form): the candidate keeps it as the node does, which is no change (issue
// 1126: a seed reproduces today, and today keeps them so). Each says how it stands today (ModuleRefused.Today). No
// module is held behind another (issue 1960): a module that reads from a held one composes with what it last offered.
func heldAsToday(a *delivery.CandidateAnswer, today map[string]string) {
func heldAsToday(a *nodeengine.CandidateAnswer, today map[string]string) {
if len(today) == 0 {
return
}
@@ -237,7 +238,7 @@ func heldAsToday(a *delivery.CandidateAnswer, today map[string]string) {
// maxCulprits bounds the modules one refusal is tried on, each a whole composition.
const maxCulprits = 8
// MaxShown bounds the bytes of shown resource bodies in one answer (delivery.ModuleDiffers.Bodies).
// MaxShown bounds the bytes of shown resource bodies in one answer (nodeengine.CandidateDiffers.Bodies).
const MaxShown = 256 << 10
var (
@@ -291,13 +292,13 @@ func standsInFor(x, node string) (string, string) {
// else the manifest it is composed from. Only the part of the offer that cannot compose is left out, by provision
// (culprit): de-spiegel's routes, its names unset, leave route-proxy without them while mailu keeps its smtp account,
// and the hold says so.
func composeEach(in compose.Input, node string, fps map[string]string, applied bool, running []delivery.Composed, failed map[string]string,
ran func(module string) (catalogue.Manifest, bool)) (out map[string]delivery.Composed, skipped, stood []string, err error) {
func composeEach(in compose.Input, node string, fps map[string]string, applied bool, running []compositions.Composed, failed map[string]string,
ran func(module string) (catalogue.Manifest, bool)) (out map[string]compositions.Composed, skipped, stood []string, err error) {
mode := compose.Foreseeing
if applied {
mode = compose.Reading
}
runs := map[string]*delivery.Composed{}
runs := map[string]*compositions.Composed{}
for i := range running {
if m := running[i].Module; m != "" {
runs[m] = &running[i]
@@ -543,7 +544,7 @@ func stubbed(in compose.Input, node, module, leave string) compose.Input {
// its offer: a provision's contributions, with leaveOwn its own secrets alone, or with leaveAll everything it contributes
// and its own secrets, none of a held module's own composition being applied, so what it has not been issued (restic's
// repository on a node it never ran on) is no refusal of the rest.
func heldAs(in compose.Input, node, module string, man catalogue.Manifest, left map[string]bool, runs *delivery.Composed) compose.Input {
func heldAs(in compose.Input, node, module string, man catalogue.Manifest, left map[string]bool, runs *compositions.Composed) compose.Input {
out := in
out.Nodes = append([]compose.Node(nil), in.Nodes...)
for i := range out.Nodes {
+31 -25
View File
@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
engcomp "github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
"slices"
@@ -76,7 +78,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
for _, r := range a.Refusals {
refused = append(refused, r.Module)
}
if a.Verdict != delivery.CandidateRefused || a.Refused != "" || !reflect.DeepEqual(refused, []string{"api", "spiegel", "web"}) {
if a.Verdict != nodeengine.CandidateAnswerVerdictRefused || a.Refused != "" || !reflect.DeepEqual(refused, []string{"api", "spiegel", "web"}) {
t.Errorf("every refusal: %+v, want api, spiegel and web refused, each named", a)
}
for _, r := range a.Refusals {
@@ -86,27 +88,31 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
t.Errorf("%s refused as %q, want %q", r.Module, r.Why, want)
}
}
if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}}); err != nil {
var wireAnswer delivery.CandidateAnswer // the form mesh-sdk's have still holds, until the Have group moves
if wire, err := json.Marshal(a); err != nil || json.Unmarshal(wire, &wireAnswer) != nil {
t.Fatalf("the answer does not read back: %v", err)
}
if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": wireAnswer}}); err != nil {
t.Errorf("the answer is not one a have holds: %v (%s)", err, raw)
}
if lemursOK := slices.ContainsFunc(a.Added, func(m delivery.ModuleAdded) bool { return m.Module == "lemurs" }); !lemursOK {
if lemursOK := slices.ContainsFunc(a.Added, func(m nodeengine.CandidateAdded) bool { return m.Module == "lemurs" }); !lemursOK {
t.Errorf("lemurs, which composes, is not answered beside the refusals: %+v", a)
}
// Issue 1126: a module the node holds today that the candidate holds too is held as today, no refusal: with every
// refused module held today the verdict is no refusal, and each is named under Held.
today := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: map[string]string{"api": "held", "spiegel": "held", "web": "held"}},
map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3)
if today.Verdict == delivery.CandidateRefused || len(today.Refusals) != 0 || len(today.Held) != 3 || today.Held[0].Why == "" {
if today.Verdict == nodeengine.CandidateAnswerVerdictRefused || len(today.Refusals) != 0 || len(today.Held) != 3 || today.Held[0].Why == "" {
t.Errorf("held as today: %+v", today)
}
// A refused module the node does not hold today stays a refusal: holding only api today leaves spiegel and web refused.
part := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: map[string]string{"api": "held"}},
map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3)
if part.Verdict != delivery.CandidateRefused || len(part.Refusals) != 2 || len(part.Held) != 1 || part.Held[0].Module != "api" {
if part.Verdict != nodeengine.CandidateAnswerVerdictRefused || len(part.Refusals) != 2 || len(part.Held) != 1 || part.Held[0].Module != "api" {
t.Errorf("held as today in part: %+v", part)
}
// No module is held behind one kept today (issue 1960): mailu's own refusal stays a refusal beside de-spiegel's hold.
if a := (delivery.CandidateAnswer{Refusals: []delivery.ModuleRefused{{Module: "de-spiegel", Why: "a value is unset"},
if a := (nodeengine.CandidateAnswer{Refusals: []nodeengine.CandidateRefusal{{Module: "de-spiegel", Why: "a value is unset"},
{Module: "mailu", Why: "no build"}}}); true {
heldAsToday(&a, map[string]string{"de-spiegel": "runs a build of the old form"})
if len(a.Refusals) != 1 || a.Refusals[0].Module != "mailu" || len(a.Held) != 1 {
@@ -136,14 +142,14 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
if m, _ := culprit(in, "n1", fps, compose.Foreseeing, map[string]string{}, fmt.Errorf("nothing in this mesh provides \"artifact-store\"")); m != "" {
t.Errorf("a refusal naming no placed module was laid on %s", m)
}
ownKey, _ := delivery.ComposedNodeKey("n1")
ownKey, _ := engcomp.NodeKey("n1")
own := base[ownKey]
carriedRes := []json.RawMessage{
json.RawMessage(`{"id":"spiegel.state","type":"directory","path":"/var/lib/spiegel","mode":"0700"}`),
json.RawMessage(`{"id":"spiegel.server","type":"container","name":"spiegel","image":"registry.example/spiegel@sha256:` + strings.Repeat("c", 64) + `"}`)}
// They run after the node's first own resource.
own.Resources = append(append(append([]json.RawMessage{}, own.Resources[:1]...), carriedRes...), own.Resources[1:]...)
var running []delivery.Composed
var running []engcomp.Composed
for k, c := range base {
if k == ownKey {
c = own
@@ -164,26 +170,26 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
t.Errorf("the node's own composition is\n%s\nwant what runs\n%s", gotOwn, wantOwn)
}
a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, running, "p-h1", 3)
if a.Verdict != delivery.CandidateSame {
if a.Verdict != nodeengine.CandidateAnswerVerdictSame {
t.Errorf("the fold's answer: %+v, want the same", a)
}
// (A) The agent also says spiegel runs by module, from the declaration kept before the switch (agent.Everything): a
// module the composition folds is judged inside the node's own, not as removed (issue 1126, seed #531's answer).
keptToo := append(append([]delivery.Composed{}, running...), delivery.Composed{Node: "n1", Module: "spiegel", Resources: carriedRes})
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, keptToo, "p-h1", 3); a.Verdict != delivery.CandidateSame || len(a.Removed) != 0 {
keptToo := append(append([]engcomp.Composed{}, running...), engcomp.Composed{Node: "n1", Module: "spiegel", Resources: carriedRes})
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, keptToo, "p-h1", 3); a.Verdict != nodeengine.CandidateAnswerVerdictSame || len(a.Removed) != 0 {
t.Errorf("a folded module running from the kept declaration is answered %+v, want the same", a)
}
// (B) The node's own composition carries its bus membership as it runs it (the controller's, from its store): composed
// the same, not 'in what runs, not composed' (seed #531, novox).
membership := json.RawMessage(`{"id":"bus-membership","type":"file","path":"` + catalogue.BusMembershipPath + `","sealed":"{\"box\":\"m\"}","mode":"0600"}`)
withMembership := make([]delivery.Composed, 0, len(running))
withMembership := make([]engcomp.Composed, 0, len(running))
for _, c := range running {
if c.Module == "" {
c.Resources = append(append([]json.RawMessage{}, c.Resources...), membership)
}
withMembership = append(withMembership, c)
}
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, withMembership, "p-h1", 3); a.Verdict != delivery.CandidateSame {
if a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"spiegel"}}, fps, withMembership, "p-h1", 3); a.Verdict != nodeengine.CandidateAnswerVerdictSame {
t.Errorf("the node's own with its bus membership is answered %+v, want the same", a)
}
// Without what runs, the fold composes spiegel from its manifest, as before, and the composition refuses it.
@@ -309,7 +315,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
if err != nil {
t.Fatal(err)
}
f1runs := delivery.Composed{Node: "ace", Resources: []json.RawMessage{
f1runs := engcomp.Composed{Node: "ace", Resources: []json.RawMessage{
json.RawMessage(`{"id":"f1-circuits.state","module":"f1-circuits","type":"directory","path":"/var/lib/f1-circuits","mode":"0700"}`),
json.RawMessage(`{"id":"f1-circuits.cache","module":"f1-circuits","type":"directory","path":"/var/lib/f1-circuits/cache","mode":"0700"}`),
json.RawMessage(`{"id":"f1-circuits.site-env","module":"f1-circuits","type":"file","path":"/var/lib/f1-circuits/site.env","mode":"0644","content":"F1_CIRCUITS_BASE_URL=https://f1.example\n"}`),
@@ -385,7 +391,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
t.Errorf("a module's own refusal: %+v, want de-spiegel refused alone, the node, mailu and proxy not", a)
}
for _, m := range []string{"lemurs", "mailu", "proxy"} {
if !slices.ContainsFunc(a.Added, func(x delivery.ModuleAdded) bool { return x.Module == m }) {
if !slices.ContainsFunc(a.Added, func(x nodeengine.CandidateAdded) bool { return x.Module == m }) {
t.Errorf("%s, which composes, is not judged beside de-spiegel's refusal: %+v", m, a)
}
}
@@ -425,11 +431,11 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
t.Errorf("held %v, want de-spiegel alone, in its config form's words, composed without its routes", held)
}
for _, m := range []string{"lemurs", "mailu", "proxy"} {
if key, _ := delivery.ComposedKey("n1", m, fps[m]); got[key].Module != m {
if key, _ := engcomp.Key("n1", m, fps[m]); got[key].Module != m {
t.Errorf("%s is not composed beside de-spiegel's hold: %v", m, sortedKeys(got))
}
}
carries := func(c delivery.Composed, what string) {
carries := func(c engcomp.Composed, what string) {
t.Helper()
ownRes, _ := json.Marshal(c.Resources)
for _, raw := range spiegelRuns {
@@ -450,7 +456,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
if err != nil || !strings.Contains(held["de-spiegel"], "${config:praktijk-name}") {
t.Fatalf("de-spiegel leaving the fold: %v, held %v", err, held)
}
spiegelKey, _ := delivery.ComposedKey("n1", "de-spiegel", fps["de-spiegel"])
spiegelKey, _ := engcomp.Key("n1", "de-spiegel", fps["de-spiegel"])
if _, ok := got[spiegelKey]; ok {
t.Errorf("held de-spiegel's own composition is handed")
}
@@ -481,11 +487,11 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
rec.OutwardLinks = []string{"eth0"}
b.records["n1"] = rec
fps = map[string]string{"lemurs": fp("e"), "nft": fp("f"), "site": fp("8")}
siteRuns := delivery.Composed{Node: "n1", Module: "site", FP: fps["site"], Resources: []json.RawMessage{
siteRuns := engcomp.Composed{Node: "n1", Module: "site", FP: fps["site"], Resources: []json.RawMessage{
json.RawMessage(`{"id":"site.state","module":"site","type":"directory","path":"/var/lib/site","mode":"0700"}`),
json.RawMessage(`{"id":"site.server","module":"site","type":"container","name":"site","image":"registry.example/site@sha256:` + strings.Repeat("c", 64) + `","ports":["10.77.0.1:20005:8080"]}`)}}
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps)
nftKey, _ := delivery.ComposedKey("n1", "nft", fps["nft"])
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []engcomp.Composed{siteRuns}}, fps)
nftKey, _ := engcomp.Key("n1", "nft", fps["nft"])
if err != nil || held["site"] == "" || held["nft"] != "" || got[nftKey].Module != "nft" {
t.Fatalf("site, its own secret never issued: %v, held %v; want site held alone, and the filter composed", err, held)
}
@@ -521,8 +527,8 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
b2.records["n1"] = rec
b2.held = true
fps["app"] = fp("9")
got, _, held, err = Compose(ctx, b2, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps)
appKey, _ := delivery.ComposedKey("n1", "app", fps["app"])
got, _, held, err = Compose(ctx, b2, Mesh{}, Scope{Node: "n1", Running: []engcomp.Composed{siteRuns}}, fps)
appKey, _ := engcomp.Key("n1", "app", fps["app"])
if _, composed := got[appKey]; err != nil || held["site"] == "" || held["app"] != "" || !composed {
t.Errorf("a consumer of held site: %v, held %v; want site held, and app composed against what runs of it", err, held)
}
@@ -560,7 +566,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
siteRuns.FP = fp("7")
b.manifests[delivery.ManifestDigest(site)] = site
b.builds["site "+fp("7")] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: delivery.ManifestDigest(site)}
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []delivery.Composed{siteRuns}}, fps)
got, _, held, err = Compose(ctx, b, Mesh{}, Scope{Node: "n1", Running: []engcomp.Composed{siteRuns}}, fps)
if err != nil || held["site"] == "" || held["nft"] != "" {
t.Errorf("site moved to 9090 in its new manifest: %v, held %v; want site held alone", err, held)
}
@@ -597,7 +603,7 @@ func otherNodeNotResolvedIsNamed(t *testing.T) {
given[n] = &p
}
a := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given}, map[string]string{"cons": "src2:" + strings.Repeat("1", 64)}, nil, "p-h1", 1)
if a.Verdict != delivery.CandidateRefused || !strings.Contains(a.Refused, "prov on n2 lacks container-runtime") {
if a.Verdict != nodeengine.CandidateAnswerVerdictRefused || !strings.Contains(a.Refused, "prov on n2 lacks container-runtime") {
t.Errorf("a refusal for want of n2's provider = %+v", a)
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ package frombus
import (
"fmt"
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
"sort"
"strconv"
@@ -27,7 +28,7 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
}
// At the version a placement key is read at, with a list of modules even when none is placed: the export is the
// seed's value, and the reader refuses a value of version 0 or without its modules.
p := delivery.Placement{V: delivery.Version, Node: node, Modules: []delivery.PlacedModule{}, Decided: delivery.NodeDecided{
p := delivery.Placement{V: sdkkeys.Version, Node: node, Modules: []delivery.PlacedModule{}, Decided: delivery.NodeDecided{
Endpoint: n.Place.Endpoint, Site: n.Place.Site, Hub: n.Place.Hub, Address: n.Place.Address,
PublicDomain: n.PublicDomain, Account: n.Account, AccountHome: n.AccountHome, AgentAccount: n.AgentAccount,
AgentHome: n.AgentAccountHome, Adopted: n.Adopted, Taken: n.Taken}}
+9 -8
View File
@@ -35,6 +35,7 @@ import (
"encoding/json"
"errors"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"maps"
"net/netip"
"slices"
@@ -146,7 +147,7 @@ type Scope struct {
KeptAt map[string]string
Placements map[string]*delivery.Placement
InOwn []string
Running []delivery.Composed
Running []engcomp.Composed
// HeldToday is, in an answer, the modules the node keeps today as they run ("node" for its own composition), each with
// how ("held", "failed", "runs a build of the old form"): a candidate that cannot compose one, or holds one only
// behind one of them, keeps it as today, which is said under CandidateAnswer.Held and is no refusal (issue 1126).
@@ -165,7 +166,7 @@ type Scope struct {
// controller's composed writer folded there because no fingerprint of the new form names them (mesh-wireguard, a module
// run at a build of the old form; issue 1126). A node composing itself passes it as Scope.InOwn, so its own
// composition keeps them as it runs them until a want names a build of the new form for one.
func InOwn(running []delivery.Composed, p delivery.Placement) []string {
func InOwn(running []engcomp.Composed, p delivery.Placement) []string {
placed := map[string]bool{}
for _, m := range p.Modules {
placed[m.Module] = true
@@ -554,7 +555,7 @@ func (b base) finish(ctx context.Context, r Reader, build func(ctx context.Conte
// why, and none of its own composition is in out, so the node keeps what it last applied of it and applies the rest; a
// node never applies a stand-in. Only a refusal that is no single module's (the node's own resources, a value served
// elsewhere, a stand-in for the node's own) refuses the node, err.
func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]delivery.Composed, skipped []string, held map[string]string, err error) {
func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]engcomp.Composed, skipped []string, held map[string]string, err error) {
held = map[string]string{}
// A node refused whole says what it held on the way, since the refusal can be of a held module's stub (issue 774,
// 2026-10-30 19:30: node-tools held, then "its build produced 0 bundle(s)" with its own refusal unsaid).
@@ -608,7 +609,7 @@ func ComposingInput(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[s
// ComposeSent composes node from an input ComposingInput read, through the path Compose takes (composeEach, the read's holds
// first): what a check's sandbox runs. False when the input says no way of composing node (an input from a node-engine
// before ComposedHow): the caller cannot compose it as the node does.
func ComposeSent(in compose.Input, node string) (map[string]delivery.Composed, bool, error) {
func ComposeSent(in compose.Input, node string) (map[string]engcomp.Composed, bool, error) {
c := in.ComposedHow
if c == nil || c.Node != node {
return nil, false, nil
@@ -763,7 +764,7 @@ func parser(Mesh) func([]byte) (catalogue.Manifest, error) {
// the builder holds it done. False where nothing is known of it (it runs no composition of its own, it runs the build
// it is composed at, or its build is unread): the manifest it is composed from is its offer then. Only what it says to
// others is taken of it, which needs no take-in of its artifacts.
func ranOf(ctx context.Context, r Reader, mesh Mesh, running []delivery.Composed, fps map[string]string) func(string) (catalogue.Manifest, bool) {
func ranOf(ctx context.Context, r Reader, mesh Mesh, running []engcomp.Composed, fps map[string]string) func(string) (catalogue.Manifest, bool) {
parse := parser(mesh)
return func(module string) (catalogue.Manifest, bool) {
for _, c := range running {
@@ -1168,7 +1169,7 @@ func claimsStore(m catalogue.Manifest) bool {
}
// keptOf is the operator export's file (catalogue.KeptID) of a module's composition as it runs, or nil.
func keptOf(c delivery.Composed) map[string]any {
func keptOf(c engcomp.Composed) map[string]any {
for _, raw := range c.Resources {
var r map[string]any
if json.Unmarshal(raw, &r) == nil && r["id"] == c.Module+"."+resolution.KeptID() {
@@ -1180,7 +1181,7 @@ func keptOf(c delivery.Composed) map[string]any {
// membershipOf is the sealed value of the bus membership resource (catalogue.BusMembershipID) of a node's own
// composition, or "".
func membershipOf(own delivery.Composed) string {
func membershipOf(own engcomp.Composed) string {
for _, raw := range own.Resources {
var r struct {
ID string `json:"id"`
@@ -1244,7 +1245,7 @@ func withoutHeldData(in compose.Input, node string) compose.Input {
// runningProvisioner names, on node, the account its provisioner client runs as now (compose.Node.RunningProvisioner): the
// account its own composition as it runs names, else the user of the running process of the module that runs the client.
func runningProvisioner(in *compose.Input, node string, running []delivery.Composed) {
func runningProvisioner(in *compose.Input, node string, running []engcomp.Composed) {
account := ""
for _, c := range running {
if c.Module == "" && c.Own.Provisioner != "" {
+32 -27
View File
@@ -11,6 +11,7 @@ import (
"encoding/json"
"errors"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
@@ -282,7 +283,7 @@ func TestInput(t *testing.T) {
// Issue 1126: a candidate placement equal to the node's composes what it runs, and CompareComposed says so; one
// thing changed in the group's candidate of another node (the hub's endpoint) names exactly the module it moves
// here. Placed puts the same placement over the store's input unchanged.
running := make([]delivery.Composed, 0, len(fromBus))
running := make([]compositions.Composed, 0, len(fromBus))
for _, c := range fromBus {
running = append(running, c)
}
@@ -346,7 +347,7 @@ func TestInput(t *testing.T) {
if err != nil {
t.Fatal(err)
}
var runsNow []delivery.Composed
var runsNow []compositions.Composed
for _, c := range runs {
runsNow = append(runsNow, c)
}
@@ -357,12 +358,12 @@ func TestInput(t *testing.T) {
if err != nil {
t.Fatal(err)
}
var consumerRuns []delivery.Composed
var consumerRuns []compositions.Composed
for _, c := range objectsRun {
consumerRuns = append(consumerRuns, c)
}
withoutOwn := func(cs []delivery.Composed) []delivery.Composed {
var out []delivery.Composed
withoutOwn := func(cs []compositions.Composed) []compositions.Composed {
var out []compositions.Composed
for _, c := range cs {
if c.Module != "" {
out = append(out, c)
@@ -384,7 +385,7 @@ func TestInput(t *testing.T) {
p := tunnelled().placements["n1"]
return map[string]*delivery.Placement{"n1": &p}
}
folded := func() []delivery.Composed {
folded := func() []compositions.Composed {
b := tunnelled()
in, err := Input(ctx, b, Mesh{}, Scope{Node: "n1"})
if err != nil {
@@ -417,11 +418,11 @@ func TestInput(t *testing.T) {
raw, _ := json.Marshal(r)
resources = append(resources, raw)
}
split, err := delivery.SplitComposed("n1", resources, map[string]string{"keeper": oneFps["keeper"]}, nil, delivery.NodeOwn{})
split, err := compositions.Split("n1", resources, map[string]string{"keeper": oneFps["keeper"]}, nil, compositions.NodeOwn{})
if err != nil {
t.Fatal(err)
}
var out []delivery.Composed
var out []compositions.Composed
for _, c := range split {
out = append(out, c)
}
@@ -437,20 +438,20 @@ func TestInput(t *testing.T) {
for name, c := range map[string]struct {
b *bus
given map[string]*delivery.Placement
running []delivery.Composed
running []compositions.Composed
verdict string
check func(delivery.CandidateAnswer) bool
check func(nodeengine.CandidateAnswer) bool
}{
"the same": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), runsNow, delivery.CandidateSame,
func(a delivery.CandidateAnswer) bool {
"the same": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), runsNow, nodeengine.CandidateAnswerVerdictSame,
func(a nodeengine.CandidateAnswer) bool {
return len(a.Same) == 3 && a.Proposal == "p-h1" && a.Revision == 7
}},
"the node runs no own composition": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), withoutOwn(runsNow), delivery.CandidateSame,
func(a delivery.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }},
"one removed": {one("keeper", "lemurs"), placedAs("keeper"), runsNow, delivery.CandidateDiffers,
func(a delivery.CandidateAnswer) bool { return reflect.DeepEqual(a.Removed, []string{"lemurs"}) }},
"the node runs no own composition": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), withoutOwn(runsNow), nodeengine.CandidateAnswerVerdictSame,
func(a nodeengine.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }},
"one removed": {one("keeper", "lemurs"), placedAs("keeper"), runsNow, nodeengine.CandidateAnswerVerdictDiffers,
func(a nodeengine.CandidateAnswer) bool { return reflect.DeepEqual(a.Removed, []string{"lemurs"}) }},
"one added, its secret not yet issued": {func() *bus { b := one("lemurs"); b.own = nil; return b }(), placedAs("keeper", "lemurs"),
nil, delivery.CandidateDiffers, func(a delivery.CandidateAnswer) bool {
nil, nodeengine.CandidateAnswerVerdictDiffers, func(a nodeengine.CandidateAnswer) bool {
for _, m := range a.Added {
if m.Module == "keeper" {
return reflect.DeepEqual(m.Unissued, []string{"keeper/password"}) && len(a.Refusals) == 0
@@ -459,27 +460,27 @@ func TestInput(t *testing.T) {
return false
}},
"a value missing for a module that runs": {func() *bus { b := one("keeper", "lemurs"); b.own = nil; return b }(), placedAs("keeper", "lemurs"),
runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool {
runsNow, nodeengine.CandidateAnswerVerdictRefused, func(a nodeengine.CandidateAnswer) bool {
return len(a.Refusals) == 1 && a.Refusals[0].Module == "keeper" && strings.Contains(a.Refusals[0].Why, "keeper/password")
}},
// Review of L2b, round 1, finding 1: a consumer the candidate adds has no credential yet, and the vault refuses an
// ask for a module not on the node; it is not asked, and the module is added, its credential not yet issued.
"a consumer added, the vault not asked for it": {one("objects"), placedAs("objects", "files"), consumerRuns, delivery.CandidateDiffers,
func(a delivery.CandidateAnswer) bool {
"a consumer added, the vault not asked for it": {one("objects"), placedAs("objects", "files"), consumerRuns, nodeengine.CandidateAnswerVerdictDiffers,
func(a nodeengine.CandidateAnswer) bool {
return len(a.Added) == 1 && a.Added[0].Module == "files" && len(a.Added[0].Unissued) > 0 && len(a.Refusals) == 0
}},
// And with no trunk at all (the seed), a stand-in of a module the node runs is a refusal, not an addition.
"the seed, a value missing for a module that runs": {func() *bus { b := one(); b.own = nil; return b }(), placedAs("keeper", "lemurs"),
runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool {
runsNow, nodeengine.CandidateAnswerVerdictRefused, func(a nodeengine.CandidateAnswer) bool {
return len(a.Refusals) == 1 && a.Refusals[0].Module == "keeper" && len(a.Added) == 0
}},
"cannot compose": {one("keeper"), map[string]*delivery.Placement{"n1": {Node: "n2"}}, runsNow, delivery.CandidateRefused,
func(a delivery.CandidateAnswer) bool { return a.Refused != "" }},
"cannot compose": {one("keeper"), map[string]*delivery.Placement{"n1": {Node: "n2"}}, runsNow, nodeengine.CandidateAnswerVerdictRefused,
func(a nodeengine.CandidateAnswer) bool { return a.Refused != "" }},
// Issue 1126 (the seed's review): the private network's module and one at a build of the old form, which no want
// names and the controller folds into the node's own, are folded there again: the node's own is the same, and
// neither is added.
"mesh-wireguard and an old-form module, folded": {tunnelled(), tunnelledAs(),
folded, delivery.CandidateSame, func(a delivery.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }},
folded, nodeengine.CandidateAnswerVerdictSame, func(a nodeengine.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }},
} {
fps := oneFps
if name == "mesh-wireguard and an old-form module, folded" {
@@ -489,7 +490,11 @@ func TestInput(t *testing.T) {
if a.Verdict != c.verdict || !c.check(a) {
t.Errorf("%s: %+v", name, a)
}
if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}}); err != nil {
var wireAnswer delivery.CandidateAnswer // the form mesh-sdk's have still holds, until the Have group moves
if wire, err := json.Marshal(a); err != nil || json.Unmarshal(wire, &wireAnswer) != nil {
t.Fatalf("%s: the answer does not read back: %v", name, err)
}
if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": wireAnswer}}); err != nil {
t.Errorf("%s: the answer is not one a have holds: %v (%s)", name, err, raw)
}
}
@@ -658,7 +663,7 @@ func TestInput(t *testing.T) {
if !reflect.DeepEqual(builtBus.fetched, []string{"http://127.0.0.1:5000/v2/restic/files/blobs/" + archiveDigest}) {
t.Errorf("the templates archive was read at %v", builtBus.fetched)
}
resticKey, _ := delivery.ComposedKey("n1", "restic", movedFps["restic"])
resticKey, _ := compositions.Key("n1", "restic", movedFps["restic"])
if c := fromBuild[resticKey]; len(c.Filled) != 4 { // its three templated files and its own secret
t.Errorf("restic is not handed to the provisioner: its filled files are %v", c.Filled)
}
@@ -875,7 +880,7 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates {
// composed as what runs shows, said, by what runs and never by a name; a manifest that says it, or nothing running so, marks
// nothing.
func whatRunsIsKeptWhileAManifestDoesNotSayIt(t *testing.T) {
running := []delivery.Composed{
running := []compositions.Composed{
{Node: "a", Module: "runner-x", Resources: []json.RawMessage{json.RawMessage(`{"id":"runner-x.runtime","type":"process"}`)}},
{Node: "a", Module: "prov-y", Resources: []json.RawMessage{json.RawMessage(`{"id":"prov-y.need-broker","type":"file","vault-sealed":{"box":"b"}}`)}},
{Node: "a", Module: "app", Resources: []json.RawMessage{json.RawMessage(`{"id":"app.need-broker","type":"file"}`)}},
@@ -3,6 +3,7 @@ package frombustest
import (
"context"
"encoding/json"
engcomp "github.com/novox/mesh-host/internal/compositions"
"reflect"
"slices"
"strings"
@@ -105,7 +106,7 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
if err != nil {
t.Fatalf("%s: the node's composition: %v", row.name, err)
}
if key, _ := delivery.ComposedKey(Node, "restic", fps["restic"]); out[key].Module != "restic" {
if key, _ := engcomp.Key(Node, "restic", fps["restic"]); out[key].Module != "restic" {
t.Errorf("%s: restic is not composed (held %v)", row.name, held)
}
if why, ok := held["backup"]; (row.bad == "" && len(held) > 0) || (row.bad != "" && (!ok || !strings.Contains(why, row.why) || len(held) != 1)) {
@@ -214,14 +215,14 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
t.Fatal(err)
}
fps, ownKey := FPs(), ""
ownKey, _ = delivery.ComposedNodeKey(Node)
resticKey, _ := delivery.ComposedKey(Node, "restic", fps["restic"])
ownKey, _ = engcomp.NodeKey(Node)
resticKey, _ := engcomp.Key(Node, "restic", fps["restic"])
before, _, _, err := frombus.Compose(context.Background(), b, frombus.Mesh{}, frombus.Scope{Node: Node}, fps)
account := before[ownKey].Own.Provisioner
if err != nil || account == "" || len(before[resticKey].Filled) == 0 {
t.Fatalf("the node composes with its client: account %q, restic's filled files %v: %v", account, before[resticKey].Filled, err)
}
var running []delivery.Composed
var running []engcomp.Composed
for _, c := range before {
running = append(running, c)
}
+6 -5
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"strings"
"testing"
@@ -122,7 +123,7 @@ func aRunningModuleWaitsForItsBuild(t *testing.T) {
store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"claims":[{"name":"container-image-registry","scope":"mesh"}],"listens":[{"name":"registry","port":5100,"from":"mesh"}],` +
`"receives":{"artifact-store":"/var/lib/artifacts/received.json"}}`
old, next := "src2:"+strings.Repeat("a", 64), "src2:"+strings.Repeat("b", 64)
running := []delivery.Composed{{Node: "n1", Module: "node-tools", FP: old, Resources: []json.RawMessage{
running := []engcomp.Composed{{Node: "n1", Module: "node-tools", FP: old, Resources: []json.RawMessage{
json.RawMessage(`{"id":"node-tools.runtime","module":"node-tools","type":"process","name":"node-tools","env":{"MESH_TOOL_MODULES":""}}`),
json.RawMessage(`{"id":"node-tools.bundle-runtime","module":"node-tools","type":"archive","source":"artifact-store://node-tools/runtime/blobs/` +
dg("1") + `","digest":"` + dg("1") + `","path":"` + resolution.BundlePath("node-tools", "runtime") + `"}`)}}}
@@ -156,14 +157,14 @@ func aRunningModuleWaitsForItsBuild(t *testing.T) {
t.Errorf("%s: the node is refused: %v", row.name, err)
continue
}
if lampKey, _ := delivery.ComposedKey("n1", "lamp", fps["lamp"]); out[lampKey].Module != "lamp" {
if lampKey, _ := engcomp.Key("n1", "lamp", fps["lamp"]); out[lampKey].Module != "lamp" {
t.Errorf("%s: lamp is not composed (held %v)", row.name, held)
}
if why := held["node-tools"]; (row.held == "") != (why == "") || !strings.Contains(why, row.held) {
t.Errorf("%s: node-tools held %q, want %q", row.name, why, row.held)
}
if row.digest != "" {
key, _ := delivery.ComposedKey("n1", "node-tools", row.want)
key, _ := engcomp.Key("n1", "node-tools", row.want)
if c := out[key]; !strings.Contains(fmt.Sprint(rawStrings(c.Resources)), row.digest) {
t.Errorf("%s: node-tools is composed as %v, want its bundle %s", row.name, rawStrings(c.Resources), row.digest)
}
@@ -267,7 +268,7 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) {
// A held module keeps what it contributes (issue 774, 2026-10-31: nextcloud held for its admin secret dropped out
// of postgres's received file, and postgres's helper would have locked its login): only its own secrets are left
// out of its stand-in, never its contributions, so the provider's received file names every consumer.
storeKey, _ := delivery.ComposedKey("n1", "artifacts", fps["artifacts"])
storeKey, _ := engcomp.Key("n1", "artifacts", fps["artifacts"])
// And never a stand-in for a grant not yet issued: the provider is applied, so a held module whose grant is
// not issued is composed without that contribution, never with "foreseen: made by the next send".
for key, c := range out {
@@ -284,7 +285,7 @@ func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) {
}
}
for _, m := range []string{"node-tools", "check", "lamp", "heal"} {
key, _ := delivery.ComposedKey("n1", m, fps[m])
key, _ := engcomp.Key("n1", m, fps[m])
_, composed := out[key]
why, isHeld := held[m]
want, wantHeld := row.held[m]
+2 -2
View File
@@ -3,13 +3,13 @@ package frombus
import (
"encoding/json"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"maps"
"slices"
"sort"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/compose/resolution"
)
@@ -25,7 +25,7 @@ import (
// keepWhatRuns marks, in in, each module placed on node that what runs shows is the tool runner or carries the core's
// credentials, where its manifest does not say so; it returns, sorted by module, what it marked, in words. The tool runner is
// marked only where no placed module's manifest says it is one.
func keepWhatRuns(in *compose.Input, node string, running []delivery.Composed) []KeptAsRuns {
func keepWhatRuns(in *compose.Input, node string, running []engcomp.Composed) []KeptAsRuns {
placed := assignedOn(*in, node)
if len(placed) == 0 || len(running) == 0 {
return nil
+3 -3
View File
@@ -19,11 +19,11 @@ import (
"encoding/json"
"errors"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Input is everything the composition reads. Each field is what one read of the controller's inventory answers, named
@@ -224,7 +224,7 @@ type Node struct {
// there (InOwn) whose manifest names a build (NamesBuild) runs a build of the old form only the controller holds,
// so it is left to the node: Compositions composes it as a stub (Stub) and carries its resources as they run, each
// after the resource it runs after (issue 1126). Nil composes every folded module from its manifest, as before.
RunsOwn *delivery.Composed
RunsOwn *engcomp.Composed
// RunningProvisioner is the account the provisioner's client runs as on this node now, read from what runs (its own
// composition's provisioner account, else the user of the provisioner client's running process), empty when nothing
// says it. A client held as its stub this pass composes no process, so the node keeps this account and its handing
@@ -254,7 +254,7 @@ type ComposedHow struct {
// Held is each module the read held, with why: composed as what runs of it, and the rest without it.
Held map[string]string
// Running is what runs of each held module, its own composition, by which it is carried as it runs.
Running []delivery.Composed
Running []engcomp.Composed
// HeldBefore is each module the node holds composing without the change (the candidate's base), held by the read or
// blamed later for a refusal: the change refuses none of them (issue 774, review round 1).
HeldBefore map[string]string
+2 -1
View File
@@ -3,6 +3,7 @@ package composer
import (
"context"
"encoding/json"
engcomp "github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
"strings"
@@ -122,7 +123,7 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
t.Fatal(err)
}
node := compose.Node{Name: "a", Assigned: []string{"bus"}, InOwn: map[string]bool{"bus": true},
RunsOwn: &delivery.Composed{Node: "a"}}
RunsOwn: &engcomp.Composed{Node: "a"}}
in := compose.Input{Catalogue: map[string]catalogue.Manifest{"bus": man}, Nodes: []compose.Node{node}}
request := func(in compose.Input) Request {
return withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose,
+3 -4
View File
@@ -3,7 +3,6 @@ package compositions
import (
"bytes"
"encoding/json"
"git.novox.be/novox/mesh-sdk/go/delivery"
"sort"
"strconv"
"strings"
@@ -31,9 +30,9 @@ func DifferingPaths(composed, running json.RawMessage) []string {
var out []string
walkDiffering(a, b, "", &out)
sort.Strings(out)
if len(out) > delivery.MaxPaths {
more := len(out) - (delivery.MaxPaths - 1)
out = append(out[:delivery.MaxPaths-1], "… "+strconv.Itoa(more)+" more")
if len(out) > MaxPaths {
more := len(out) - (MaxPaths - 1)
out = append(out[:MaxPaths-1], "… "+strconv.Itoa(more)+" more")
}
return out
}
+53
View File
@@ -0,0 +1,53 @@
package compositions
import (
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
)
// The bounds of a module's shown bodies in a candidate answer, and of one answer as the node's key holds it (the node-engine
// seat's state have, key `<node>.node`, takes a value of about 1 MiB and holds every proposal's answer beside the node's
// facts, so one answer keeps to a sixth).
const (
MaxBodies = 20
MaxBody = 8192
MaxPaths = 64
MaxAnswerBytes = 160 << 10
)
// ReturnsToNothing is what a node answers a proposal that takes its placement key away.
const ReturnsToNothing = "the proposal takes this node's placement away: it composes nothing then"
// NodeWord is the last part of a node's own key, and the name of its own composition in an answer.
const NodeWord = "node"
// VerdictOf is the verdict of an answer's lists: refused on any refusal, differs on any difference, addition or
// removal, same otherwise.
func VerdictOf(a seat.CandidateAnswer) string {
switch {
case a.Refused != "" || len(a.Refusals) > 0:
return seat.CandidateAnswerVerdictRefused
case len(a.Differs)+len(a.Added)+len(a.Removed) > 0:
return seat.CandidateAnswerVerdictDiffers
}
return seat.CandidateAnswerVerdictSame
}
// WithValuesDiffer is answers with each one's ValuesDiffer counted: how many shown bodies hold a value the node opened on
// both sides to two values (the vault's value and the one that runs drifted apart; issue 1126, cause C4).
func WithValuesDiffer(answers map[string]seat.CandidateAnswer) map[string]seat.CandidateAnswer {
out := make(map[string]seat.CandidateAnswer, len(answers))
for g, a := range answers {
var shown []meshvaultclient.ModuleBodies
for _, d := range a.Differs {
m := meshvaultclient.ModuleBodies{Module: d.Module}
for _, b := range d.Bodies {
m.Bodies = append(m.Bodies, meshvaultclient.Body{ID: b.ID, Composed: b.Composed, Running: b.Running})
}
shown = append(shown, m)
}
a.ValuesDiffer = int64(meshvaultclient.ValuesDiffer(shown))
out[g] = a
}
return out
}
+19 -20
View File
@@ -4,11 +4,10 @@ import (
"bytes"
"encoding/json"
"fmt"
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// What the node-engine finds when it compares the compositions it made with the ones it runs (issue 1126): the bus's user
@@ -21,11 +20,11 @@ func TestComparingCompositions(t *testing.T) {
return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"0600","content":"` + content + `"}`)
}
fp := "src2:" + strings.Repeat("a", 64)
comp := func(rs ...json.RawMessage) delivery.Composed {
return delivery.Composed{Node: "novox", Module: "nats", FP: fp, Resources: rs}
comp := func(rs ...json.RawMessage) Composed {
return Composed{Node: "novox", Module: "nats", FP: fp, Resources: rs}
}
for name, tc := range map[string]struct {
composed, running delivery.Composed
composed, running Composed
resource, why string
}{
"user list alone": {comp(file("conf", "a"), file("nats.bus-users", "stand-in")), comp(file("conf", "a"), file("nats.bus-users", "real")), "", ""},
@@ -33,7 +32,7 @@ func TestComparingCompositions(t *testing.T) {
"one side": {comp(file("conf", "a"), file("more", "x")), comp(file("conf", "a")), "more", "composed, and not in what runs"},
"order": {comp(file("conf", "a"), file("more", "x")), comp(file("more", "x"), file("conf", "a")), "", "the same resources in another order"},
} {
c := CompareComposed(map[string]delivery.Composed{"novox.nats." + fp: tc.composed}, []delivery.Composed{tc.running})
c := CompareComposed(map[string]Composed{"novox.nats." + fp: tc.composed}, []Composed{tc.running})
if tc.why == "" {
if len(c.Differs) != 0 || len(c.Same) != 1 {
t.Errorf("%s: %+v", name, c)
@@ -54,15 +53,15 @@ func TestComparingCompositions(t *testing.T) {
sameBox := func(a, b string) (same, known bool) {
return (a == "box-2" && b == "box-3") || a == b, true
}
shown := CompareComposedWith(map[string]delivery.Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"),
shown := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"),
file("c", "x"), sealedFile("d", "box-2", "0600"))},
[]delivery.Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"), sealedFile("d", "box-4", "0600"))},
[]Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"), sealedFile("d", "box-4", "0600"))},
sameBox)
if len(shown.Differs) != 1 || len(shown.Differs[0].Bodies) != 3 {
t.Fatalf("every differing resource is not shown, or b sealed again is shown: %+v", shown.Differs)
}
if c := CompareComposedWith(map[string]delivery.Composed{"k": comp(sealedFile("b", "box-2", "0600"))},
[]delivery.Composed{comp(sealedFile("b", "box-3", "0600"))}, sameBox); len(c.Same) != 1 {
if c := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("b", "box-2", "0600"))},
[]Composed{comp(sealedFile("b", "box-3", "0600"))}, sameBox); len(c.Same) != 1 {
t.Errorf("a value sealed again at its own slot is not the same: %+v", c)
}
for _, row := range []struct {
@@ -73,7 +72,7 @@ func TestComparingCompositions(t *testing.T) {
{"c", `"content":"x"`, "", []string{"content"}},
{"d", "<masked: another value>", "box-2", []string{"sealed"}},
} {
var body delivery.ResourceBodies
var body seat.CandidateBodies
for _, b := range shown.Differs[0].Bodies {
if b.ID == row.id {
body = b
@@ -93,8 +92,8 @@ func TestComparingCompositions(t *testing.T) {
`"id":"grafana.ini","mode":"0600","module":"nats","path":"/etc/grafana/grafana.ini","secrets":{"postgres-database":"` + db +
`","secret-key":"` + key + `"},"type":"file"}`)
}
if c := CompareComposedWith(map[string]delivery.Composed{"k": comp(withSecrets("box-2", "box-1"))},
[]delivery.Composed{comp(withSecrets("box-3", "box-1"))}, sameBox); len(c.Same) != 1 {
if c := CompareComposedWith(map[string]Composed{"k": comp(withSecrets("box-2", "box-1"))},
[]Composed{comp(withSecrets("box-3", "box-1"))}, sameBox); len(c.Same) != 1 {
t.Errorf("a secret sealed again at its own entry is not the same: %+v", c)
}
if kept := KeepSeals(comp(withSecrets("box-2", "box-1")), SealsIn(comp(withSecrets("box-3", "box-1"))), sameBox); !bytes.Equal(kept.Resources[0], withSecrets("box-3", "box-1")) {
@@ -105,8 +104,8 @@ func TestComparingCompositions(t *testing.T) {
}
// Another value at one entry differs there alone, each entry masked by what it opens to.
// box-2 and box-4 open to one value, so the box that runs is kept there; box-5 and box-6 are two values.
mixed := CompareComposedWith(map[string]delivery.Composed{"k": comp(withSecrets("box-2", "box-5"))},
[]delivery.Composed{comp(withSecrets("box-4", "box-6"))}, func(a, b string) (bool, bool) {
mixed := CompareComposedWith(map[string]Composed{"k": comp(withSecrets("box-2", "box-5"))},
[]Composed{comp(withSecrets("box-4", "box-6"))}, func(a, b string) (bool, bool) {
return (a == "box-2" && b == "box-4") || a == b, true
})
if len(mixed.Differs) != 1 || len(mixed.Differs[0].Bodies) != 1 {
@@ -128,7 +127,7 @@ func TestComparingCompositions(t *testing.T) {
return json.RawMessage(`{"id":"core-box","type":"file","module":"nats","path":"/etc/core","mode":"0600","vault-sealed":{"v":1,"eph":"e-` +
ct + `","ct":"` + ct + `"}}`)
}
vault := CompareComposedWith(map[string]delivery.Composed{"k": comp(box("c1"))}, []delivery.Composed{comp(box("c2"))},
vault := CompareComposedWith(map[string]Composed{"k": comp(box("c1"))}, []Composed{comp(box("c2"))},
func(a, b string) (bool, bool) {
return false, strings.Contains(a, `"ct":"c1"`) && strings.Contains(b, `"ct":"c2"`)
})
@@ -141,14 +140,14 @@ func TestComparingCompositions(t *testing.T) {
}
// Issue 1126, T1: the paths are found on the whole bodies, before they are cut; JSON in a string is walked into, a
// key a secret could be under is named and never shown.
long := strings.Repeat("x", delivery.MaxBody)
long := strings.Repeat("x", MaxBody)
conf := func(link, password string) json.RawMessage {
inner, _ := json.Marshal(map[string]any{"pad": long, "links": []string{"a", link}, "password": password})
outer, _ := json.Marshal(map[string]any{"id": "conf", "type": "file", "module": "nats", "path": "/etc/conf",
"mode": "0600", "content": string(inner)})
return outer
}
far := CompareComposedWith(map[string]delivery.Composed{"k": comp(conf("b", "hunter2"))}, []delivery.Composed{comp(conf("c", "hunter3"))}, nil)
far := CompareComposedWith(map[string]Composed{"k": comp(conf("b", "hunter2"))}, []Composed{comp(conf("c", "hunter3"))}, nil)
if len(far.Differs) != 1 || len(far.Differs[0].Bodies) != 1 ||
!slices.Equal(far.Differs[0].Bodies[0].Paths, []string{"content.links.1", "content.password"}) ||
!strings.Contains(far.Differs[0].Bodies[0].Composed, "cut at") || strings.Contains(fmt.Sprint(far), "hunter") {
@@ -160,10 +159,10 @@ func TestComparingCompositions(t *testing.T) {
!strings.Contains(c, "<masked: the same on both sides>") {
t.Errorf("masked as %s and %s", c, r)
}
if c := CompareComposed(nil, []delivery.Composed{{Node: "novox", Resources: []json.RawMessage{}}}); len(c.Removed) != 1 || c.Removed[0] != "node" {
if c := CompareComposed(nil, []Composed{{Node: "novox", Resources: []json.RawMessage{}}}); len(c.Removed) != 1 || c.Removed[0] != "node" {
t.Errorf("the node's own composition run and not composed is removed: %+v", c)
}
if c := CompareComposed(map[string]delivery.Composed{"k": comp(file("conf", "a"))}, nil); len(c.Added) != 1 || c.Added[0].Module != "nats" || c.Added[0].Composed == "" {
if c := CompareComposed(map[string]Composed{"k": comp(file("conf", "a"))}, nil); len(c.Added) != 1 || c.Added[0].Module != "nats" || c.Added[0].Composed == "" {
t.Errorf("a composition not run is added: %+v", c)
}
}
+251
View File
@@ -0,0 +1,251 @@
package compositions
import (
"encoding/json"
"fmt"
"regexp"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/keys"
)
var bucketNode = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// NodeOwn is the node's own parts in a composition (the node-engine seat's state composed, key `<node>.node`): its layout
// is the seat's contract (nodeengine.NodeOwn); the engine holds them as the declaration package's types.
type NodeOwn struct {
Adoption *declaration.Adoption `json:"adoption,omitempty"`
Identities []declaration.Identity `json:"identities,omitempty"`
Groups []declaration.Group `json:"groups,omitempty"`
Libraries []declaration.Library `json:"libraries,omitempty"`
Provisioner string `json:"provisioner_account,omitempty"`
}
// Composed is a composition (the node-engine seat's state composed): one module's resources composed for one node at one
// fingerprint, or, with Module and FP empty, the node's own. Its codec, digest and keys are the node-engine client's
// (nodeengineclient); the engine holds the plan as the catalogue's Plan, which the client carries as JSON.
type Composed struct {
V int
Node string
Module string
FP string
Data bool
Resources []json.RawMessage
Filled []string
Own NodeOwn
Plan *catalogue.Plan
Refused string
}
// Bucket constants of a node's compositions (the state composed is one bucket per node).
const (
// BucketPrefix starts the name of a node's bucket of compositions, BucketFor(node).
BucketPrefix = "composed-"
// History is the revisions a node's composition bucket keeps per key.
History = 5
// Keep is how many of a module's newest fingerprints keep their compositions though no want names them.
Keep = 5
)
// BucketFor is the name of a node's bucket of compositions, "composed-<node>".
func BucketFor(node string) (string, error) {
if node == "" {
return "", fmt.Errorf("compositions: the node name is empty; a node's composition bucket is named for its node")
}
if !bucketNode.MatchString(node) {
return "", fmt.Errorf("compositions: the node name %q is not one a bucket accepts: letters, digits, '_' and '-' only", node)
}
return BucketPrefix + node, nil
}
func toClient(c Composed) (nodeengineclient.Composition, error) {
out := nodeengineclient.Composition{V: c.V, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data,
Resources: c.Resources, Filled: c.Filled, Refused: c.Refused}
raw, err := json.Marshal(c.Own)
if err != nil {
return out, err
}
if err := json.Unmarshal(raw, &out.Own); err != nil {
return out, fmt.Errorf("compositions: the node's own parts do not fit the seat's layout: %w", err)
}
if c.Plan != nil {
if out.Plan, err = json.Marshal(c.Plan); err != nil {
return out, err
}
}
return out, nil
}
func fromClient(c nodeengineclient.Composition) (Composed, error) {
out := Composed{V: c.V, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data, Resources: c.Resources,
Filled: c.Filled, Refused: c.Refused}
raw, err := json.Marshal(c.Own)
if err != nil {
return out, err
}
if err := json.Unmarshal(raw, &out.Own); err != nil {
return out, fmt.Errorf("compositions: the node's own parts do not fit the engine's: %w", err)
}
if len(c.Plan) > 0 {
out.Plan = &catalogue.Plan{}
if err := json.Unmarshal(c.Plan, out.Plan); err != nil {
return out, fmt.Errorf("compositions: the plan does not fit: %w", err)
}
}
return out, nil
}
// Encode is c as the client writes it.
func Encode(c Composed) ([]byte, error) {
cc, err := toClient(c)
if err != nil {
return nil, err
}
return nodeengineclient.EncodeComposition(cc)
}
// Decode reads a composition as the client reads it.
func Decode(data []byte) (Composed, error) {
cc, err := nodeengineclient.DecodeComposition(data)
if err != nil {
return Composed{}, err
}
return fromClient(cc)
}
// DigestOf is a composition's digest, "sha256:<hex>", of its encoding without its plan; a refused one has none.
func DigestOf(c Composed) (string, error) {
cc, err := toClient(c)
if err != nil {
return "", err
}
return nodeengineclient.ComposedDigest(cc)
}
// Same says whether two compositions are the same value, their plans included.
func Same(a, b Composed) bool {
ca, errA := toClient(a)
cb, errB := toClient(b)
return errA == nil && errB == nil && nodeengineclient.SameComposition(ca, cb)
}
// Key is the key of a module's composition for a node at a fingerprint: `<node>.<module>.<hex>`.
func Key(node, module, fingerprint string) (string, error) {
part, err := meshcatalogclient.KeyPart(fingerprint)
if err != nil {
return "", err
}
return nodeengineclient.ComposedKey(node, module, part)
}
// NodeKey is the key of a node's own composition: `<node>.node`.
func NodeKey(node string) (string, error) {
if err := keys.CheckName("node", node); err != nil {
return "", err
}
return node + "." + nodeengineclient.NodeWord, nil
}
// ParseKey is the inverse of Key and NodeKey: isNode is true for `<node>.node`, with module and fingerprint empty.
func ParseKey(key string) (node, module, fingerprint string, isNode bool, err error) {
node, module, part, isNode, err := nodeengineclient.ParseComposedKey(key)
if err != nil || isNode {
return node, module, "", isNode, err
}
return node, module, meshcatalogclient.SourcePrefix2 + part, false, nil
}
// ByDigest is c with every file over 64 KiB given by its digest and size, and the content of each by digest, for the
// writer to put in the node's object bucket (declaration.FileBucketFor) under declaration.ObjectName before it writes
// the composition. A file that is sealed, filled with secrets or written into another stays as it is.
func (c Composed) ByDigest() (Composed, map[string][]byte, error) {
out := c
out.Resources = make([]json.RawMessage, len(c.Resources))
objects := map[string][]byte{}
for i, raw := range c.Resources {
out.Resources[i] = raw
r, err := declaration.ParseResource(raw)
if err != nil {
return Composed{}, nil, err
}
f, ok := r.(*declaration.File)
if !ok || !f.TravelsByDigest() {
continue
}
ref, body, err := f.AsReference()
if err != nil {
return Composed{}, nil, err
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
return Composed{}, nil, err
}
delete(fields, "content")
delete(fields, "bytes")
fields["digest"], _ = json.Marshal(ref.Digest)
fields["size"], _ = json.Marshal(ref.Size)
rewritten, err := json.Marshal(fields)
if err != nil {
return Composed{}, nil, err
}
out.Resources[i] = rewritten
objects[ref.Digest] = body
}
return out, objects, nil
}
// Split splits a node's composed resources into its compositions, by key (Key, NodeKey): each resource into the
// composition of the module it says, under that module's fingerprint in fps, and a resource that says no module into the
// node's own, with own. Each filled path goes with the module whose file declares it. The order of the resources is kept
// within each composition. A resource of a module fps names no fingerprint for is refused, naming it.
func Split(node string, resources []json.RawMessage, fps map[string]string, filled []string, own NodeOwn) (map[string]Composed, error) {
nodeKey, err := NodeKey(node)
if err != nil {
return nil, err
}
out := map[string]Composed{nodeKey: {Node: node, Own: own, Resources: []json.RawMessage{}}}
pathOf := map[string]string{}
for _, raw := range resources {
var head struct {
ID string `json:"id"`
Type string `json:"type"`
Module string `json:"module"`
Path string `json:"path"`
}
if err := json.Unmarshal(raw, &head); err != nil {
return nil, fmt.Errorf("compositions: a resource of %s is not JSON: %w", node, err)
}
key := nodeKey
if head.Module != "" {
fp, ok := fps[head.Module]
if !ok {
return nil, fmt.Errorf("compositions: the resource %q is of %q, whose fingerprint on %s is not given", head.ID, head.Module, node)
}
if key, err = Key(node, head.Module, fp); err != nil {
return nil, err
}
if _, ok := out[key]; !ok {
out[key] = Composed{Node: node, Module: head.Module, FP: fp}
}
}
if head.Type == string(declaration.TypeFile) && head.Module != "" {
pathOf[head.Path] = key
}
c := out[key]
c.Resources = append(c.Resources, raw)
out[key] = c
}
for _, path := range filled {
key, ok := pathOf[path]
if !ok {
return nil, fmt.Errorf("compositions: the provisioner fills %q on %s, and no module's file declares it", path, node)
}
c := out[key]
c.Filled = append(c.Filled, path)
out[key] = c
}
return out, nil
}
+27 -27
View File
@@ -4,7 +4,7 @@ import (
"bytes"
"encoding/json"
"errors"
"git.novox.be/novox/mesh-sdk/go/delivery"
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"io"
"maps"
"slices"
@@ -18,8 +18,8 @@ import (
// now) and removed by it (run now, not composed). Each list is sorted by module.
type Comparison struct {
Same []string
Differs []delivery.ModuleDiffers
Added []delivery.ModuleAdded
Differs []seat.CandidateDiffers
Added []seat.CandidateAdded
Removed []string
}
@@ -32,25 +32,25 @@ type Comparison struct {
// difference names the first resource, by its id, whose content differs or that one side alone holds; when every
// resource is equal it says what else differs (their order, the files filled, the data, the node's own parts, a file
// carried by digest).
func CompareComposed(composed map[string]delivery.Composed, running []delivery.Composed) Comparison {
func CompareComposed(composed map[string]Composed, running []Composed) Comparison {
return CompareComposedWith(composed, running, nil)
}
// CompareComposedWith is CompareComposed showing, for each module that differs, every differing resource with both
// bodies masked (MaskedPair); sameValue, when given, says whether two sealed values open to the same value (the node
// opens them with its own key), so a value sealed again reads as such.
func CompareComposedWith(composed map[string]delivery.Composed, running []delivery.Composed, sameValue func(a, b string) (same, known bool)) Comparison {
name := func(c delivery.Composed) string {
func CompareComposedWith(composed map[string]Composed, running []Composed, sameValue func(a, b string) (same, known bool)) Comparison {
name := func(c Composed) string {
if c.Module == "" {
return delivery.HaveNodeWord
return NodeWord
}
return c.Module
}
ours := map[string]delivery.Composed{}
ours := map[string]Composed{}
for _, c := range composed {
ours[name(c)] = c
}
theirs := map[string]delivery.Composed{}
theirs := map[string]Composed{}
for _, c := range running {
theirs[name(c)] = c
}
@@ -67,8 +67,8 @@ func CompareComposedWith(composed map[string]delivery.Composed, running []delive
b, runs := theirs[m]
switch {
case !runs:
dig, _ := delivery.ComposedDigestOf(withoutBusUsers(a))
out.Added = append(out.Added, delivery.ModuleAdded{Module: m, Composed: dig})
dig, _ := DigestOf(withoutBusUsers(a))
out.Added = append(out.Added, seat.CandidateAdded{Module: m, Composed: dig})
case !composedHere:
out.Removed = append(out.Removed, m)
default:
@@ -83,21 +83,21 @@ func CompareComposedWith(composed map[string]delivery.Composed, running []delive
}
// compareOne is one module's comparison: true when the digests agree, else what differs first.
func compareOne(module string, composed, running delivery.Composed, sameValue func(a, b string) (bool, bool)) (delivery.ModuleDiffers, bool) {
func compareOne(module string, composed, running Composed, sameValue func(a, b string) (bool, bool)) (seat.CandidateDiffers, bool) {
composed, running = withoutBusUsers(composed), withoutBusUsers(running)
// A sealed value is judged by the value it opens to, slot by slot (issue 1126, C1): a value sealed again at the same
// field of the same resource is the box that runs, never a difference; and a box is never matched across resources.
composed = KeepSeals(composed, SealsIn(running), sameValue)
dc, errC := delivery.ComposedDigestOf(composed)
dr, errR := delivery.ComposedDigestOf(running)
dc, errC := DigestOf(composed)
dr, errR := DigestOf(running)
if errC != nil {
return delivery.ModuleDiffers{Module: module, Running: dr, Why: "the composition has no digest: " + errC.Error()}, false
return seat.CandidateDiffers{Module: module, Running: dr, Why: "the composition has no digest: " + errC.Error()}, false
}
if errR == nil && dc == dr {
return delivery.ModuleDiffers{}, true
return seat.CandidateDiffers{}, true
}
differs := func(resource, why string) (delivery.ModuleDiffers, bool) {
return delivery.ModuleDiffers{Module: module, Composed: dc, Running: dr, Resource: resource, Why: why}, false
differs := func(resource, why string) (seat.CandidateDiffers, bool) {
return seat.CandidateDiffers{Module: module, Composed: dc, Running: dr, Resource: resource, Why: why}, false
}
// Words for the difference: resource by resource, each side with its large files by digest, so a file carried whole
// and the same file carried by digest read as one.
@@ -114,9 +114,9 @@ func compareOne(module string, composed, running delivery.Composed, sameValue fu
for id := range rb {
ids[id] = true
}
var first delivery.ModuleDiffers
var first seat.CandidateDiffers
found := false
var bodies []delivery.ResourceBodies
var bodies []seat.CandidateBodies
for _, id := range slices.Sorted(maps.Keys(ids)) {
x, inA := ra[id]
y, inB := rb[id]
@@ -135,11 +135,11 @@ func compareOne(module string, composed, running delivery.Composed, sameValue fu
first, _ = differs(id, why)
found = true
}
if len(bodies) < delivery.MaxBodies {
if len(bodies) < MaxBodies {
// The paths are found on the whole bodies, before either is cut (issue 1126, T1: three resources over
// MaxBody could not be told apart).
c, r := MaskedPair(x, y, sameValue)
bodies = append(bodies, delivery.ResourceBodies{ID: id, Why: why, Paths: DifferingPaths(x, y),
bodies = append(bodies, seat.CandidateBodies{ID: id, Why: why, Paths: DifferingPaths(x, y),
Composed: cut(c), Running: cut(r)})
}
}
@@ -157,8 +157,8 @@ func compareOne(module string, composed, running delivery.Composed, sameValue fu
case a.FP != b.FP:
return differs("", "the fingerprint differs")
}
if dA, err := delivery.ComposedDigestOf(a); err == nil {
if dB, err := delivery.ComposedDigestOf(b); err == nil && dA == dB {
if dA, err := DigestOf(a); err == nil {
if dB, err := DigestOf(b); err == nil && dA == dB {
return differs("", "the same content, a large file carried whole on one side and by digest on the other")
}
}
@@ -166,7 +166,7 @@ func compareOne(module string, composed, running delivery.Composed, sameValue fu
}
// withoutBusUsers is c without the bus's user list resource (its id `bus-users`, or `<module>.bus-users`).
func withoutBusUsers(c delivery.Composed) delivery.Composed {
func withoutBusUsers(c Composed) Composed {
out := c
out.Resources = nil
for _, raw := range c.Resources {
@@ -204,10 +204,10 @@ func resourcesByID(resources []json.RawMessage) map[string][]byte {
// cut is s cut at MaxBody bytes, saying so.
func cut(s string) string {
if len(s) <= delivery.MaxBody {
if len(s) <= MaxBody {
return s
}
return s[:delivery.MaxBody] + "… (cut at " + strconv.Itoa(delivery.MaxBody) + " bytes)"
return s[:MaxBody] + "… (cut at " + strconv.Itoa(MaxBody) + " bytes)"
}
// secretWords are the words of a key whose value may hold a secret or a sealed one: masked in a shown body.
+2 -3
View File
@@ -3,7 +3,6 @@ package compositions
import (
"bytes"
"encoding/json"
"git.novox.be/novox/mesh-sdk/go/delivery"
"sort"
"strconv"
@@ -63,7 +62,7 @@ func sealsAt(fields map[string]json.RawMessage) []sealAt {
}
// SealsIn is every sealed value c carries, by slot; nil when it carries none.
func SealsIn(c delivery.Composed) Seals {
func SealsIn(c Composed) Seals {
var out Seals
for i, raw := range c.Resources {
id, fields, ok := sealedFieldsOf(raw, i)
@@ -86,7 +85,7 @@ func SealsIn(c delivery.Composed) Seals {
// same value (known and same): a value the vault sealed again is then the box that runs, so the composition is the
// one that runs (ADR 0547 §3, issue 1126). A slot kept holds nothing for, or whose value differs or cannot be opened,
// keeps c's. Every other byte of a resource is kept as it is; c itself is not changed.
func KeepSeals(c delivery.Composed, kept Seals, same func(a, b string) (same, known bool)) delivery.Composed {
func KeepSeals(c Composed, kept Seals, same func(a, b string) (same, known bool)) Composed {
if len(kept) == 0 || same == nil {
return c
}
+84
View File
@@ -0,0 +1,84 @@
package compositions
import (
"encoding/json"
"fmt"
"sort"
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-sdk/go/declaration"
)
// PackagesBucket is the node-engine's state packages (the seat's state of the same name), one key per node.
const PackagesBucket = "mesh-host_" + seat.StatePackages
// DeclaredPackages is a node's key of `packages` from the compositions its engine composed for the node's wants:
// each resource of type package, by its name and whether it is declared absent, with the modules that declare it so,
// in the mirror's layout and order (names sorted, absent after present, modules sorted). A resource's module is the
// one it names, else its composition's, else the guess from its id (declaration.ModuleGuess); a package of no module
// lists none.
func DeclaredPackages(node string, comps []Composed) (seat.PackagesByNodeValue, error) {
by := map[packageKey]map[string]bool{}
for _, c := range comps {
for _, raw := range c.Resources {
var r struct {
ID string `json:"id"`
Type string `json:"type"`
Package string `json:"package"`
Absent bool `json:"absent"`
Module string `json:"module"`
}
if err := json.Unmarshal(raw, &r); err != nil {
return seat.PackagesByNodeValue{}, fmt.Errorf("compositions: a resource of %s's composition %q is not readable: %w", node, c.Module, err)
}
if r.Type != "package" || r.Package == "" {
continue
}
k := packageKey{r.Package, r.Absent}
if by[k] == nil {
by[k] = map[string]bool{}
}
module := r.Module
if module == "" {
module = c.Module
}
if module == "" {
module, _ = declaration.ModuleGuess(r.ID)
}
if module != "" {
by[k][module] = true
}
}
}
v := seat.PackagesByNodeValue{Node: node, Packages: []seat.PackagesByNodeValuePackagesItem{}}
for _, k := range sortedPackageKeys(by) {
modules := make([]string, 0, len(by[k]))
for m := range by[k] {
modules = append(modules, m)
}
sort.Strings(modules)
v.Packages = append(v.Packages, seat.PackagesByNodeValuePackagesItem{Package: k.name, Modules: modules, Absent: k.absent})
}
return v, nil
}
// packageKey is a package as the layout tells it apart: its name, and whether it is declared absent.
type packageKey struct {
name string
absent bool
}
// sortedPackageKeys is the keys of m by name, absent after present.
func sortedPackageKeys[V any](m map[packageKey]V) []packageKey {
keys := make([]packageKey, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].name != keys[j].name {
return keys[i].name < keys[j].name
}
return !keys[i].absent && keys[j].absent
})
return keys
}
+9 -5
View File
@@ -4,15 +4,19 @@
// the shapes it judges (a composition, a judgement's words, a candidate's answer) stay in mesh-sdk.
package judge
import "git.novox.be/novox/mesh-sdk/go/delivery"
import (
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-sdk/go/delivery"
engcomp "github.com/novox/mesh-host/internal/compositions"
)
// The shapes of mesh-sdk's delivery package this package judges, by the names its code was written with.
type (
Composed = delivery.Composed
Composed = engcomp.Composed
Judgement = delivery.Judgement
CandidateAnswer = delivery.CandidateAnswer
ModuleDiffers = delivery.ModuleDiffers
ResourceBodies = delivery.ResourceBodies
CandidateAnswer = nodeengine.CandidateAnswer
ModuleDiffers = nodeengine.CandidateDiffers
ResourceBodies = nodeengine.CandidateBodies
)
const (
+3 -2
View File
@@ -1,6 +1,7 @@
package judge
import (
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"strings"
"testing"
@@ -11,7 +12,7 @@ import (
// given is not changed (moved from mesh-sdk's placement test with FitAnswer, novox/hq ADR 0593 §7).
func TestFitAnswer(t *testing.T) {
long := strings.Repeat("x", delivery.MaxBody)
big := CandidateAnswer{Proposal: "p-x1", Revision: 1, Verdict: delivery.CandidateDiffers, Differs: []ModuleDiffers{{
big := CandidateAnswer{Proposal: "p-x1", Revision: 1, Verdict: nodeengine.CandidateAnswerVerdictDiffers, Differs: []ModuleDiffers{{
Module: "nats", Why: "differs", Bodies: []ResourceBodies{{ID: "conf", Why: "changed",
Paths: []string{"content.links.1", "content.password"}, Composed: long, Running: long}}}}}
if same := FitAnswer(big, 1<<20); same.Note != "" || same.Differs[0].Bodies[0].Composed != long {
@@ -19,7 +20,7 @@ func TestFitAnswer(t *testing.T) {
}
fitted := FitAnswer(big, 4<<10)
if len(fitted.Differs[0].Bodies[0].Composed) != 0 || len(fitted.Differs[0].Bodies[0].Paths) != 2 ||
fitted.Verdict != delivery.CandidateDiffers || !strings.Contains(fitted.Note, "cut") || big.Differs[0].Bodies[0].Composed == "" {
fitted.Verdict != nodeengine.CandidateAnswerVerdictDiffers || !strings.Contains(fitted.Note, "cut") || big.Differs[0].Bodies[0].Composed == "" {
t.Errorf("fitted as %+v", fitted)
}
}
+3 -3
View File
@@ -4,8 +4,8 @@ import (
"context"
"errors"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-host/internal/selfcompose"
@@ -14,9 +14,9 @@ import (
// ---- this node's composition state, in the bus's own storage (novox/hq ADR 0593 §6) --------------------------------
// composedStore binds the node's composition state `composed-<node>` over the link open now: bound, never created (the
// grants part creates it, create-only, history delivery.ComposedHistory).
// grants part creates it, create-only, history engcomp.History).
func composedStore(js nats.JetStreamContext, node string) (nats.KeyValue, error) {
bucket, err := delivery.ComposedBucketFor(node)
bucket, err := engcomp.BucketFor(node)
if err != nil {
return nil, err
}
+9 -8
View File
@@ -5,8 +5,9 @@ import (
"encoding/json"
"errors"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"github.com/novox/mesh-host/internal/compositions"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go"
)
@@ -14,8 +15,8 @@ import (
// PackagesBucket is the node-engine's declared state packages, created by the grants part from the engine's manifest as
// its states health and data are: one key, the node's name, written by that node's engine alone, read by pacman as
// mesh-host.packages (delivery.EnginePackagesRead).
const PackagesBucket = "mesh-host_" + delivery.EnginePackagesState
// mesh-host.packages ("mesh-host." + nodeengine.StatePackages).
const PackagesBucket = compositions.PackagesBucket
// packagesStore binds PackagesBucket over the link open now: bound, never created.
func (l *natsLink) packagesStore() (nats.KeyValue, error) {
@@ -84,9 +85,9 @@ func (p EnginePackages) Put(_ context.Context, key string, value []byte) error {
type PackagesView struct {
Node string `json:"node"`
// Key is the node's key as the bus holds it (Source); null when the engine has written none.
Source string `json:"source"`
Key *delivery.DeclaredPackages `json:"key"`
KeyError string `json:"key_error,omitempty"`
Source string `json:"source"`
Key *nodeengine.PackagesByNodeValue `json:"key"`
KeyError string `json:"key_error,omitempty"`
// Wrote says, for write, whether the key was written again (false: it already held what the engine composed).
Wrote *bool `json:"wrote,omitempty"`
Note string `json:"note,omitempty"`
@@ -94,9 +95,9 @@ type PackagesView struct {
// PackagesAnswer is the verb's answer from the key as read (nil: no key), with its read's error.
func PackagesAnswer(node string, key []byte, keyErr error) PackagesView {
v := PackagesView{Node: node, Source: delivery.EnginePackagesRead}
v := PackagesView{Node: node, Source: "mesh-host." + nodeengine.StatePackages}
if keyErr == nil && key != nil {
var d delivery.DeclaredPackages
var d nodeengine.PackagesByNodeValue
if keyErr = json.Unmarshal(key, &d); keyErr == nil {
v.Key = &d
}
+2 -2
View File
@@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
"sort"
"strconv"
"strings"
@@ -17,7 +18,6 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The node seat node-engine's acts (novox/hq ADR 0563).
@@ -273,7 +273,7 @@ func (h *HandActs) Call(c EngineCall) (any, error) {
return nil, errors.New("this engine states no manifest reads yet: its node agent has not started")
}
reads, at := h.ManifestReads()
return nodeengine.ManifestReadsAnswer{V: delivery.Version, Node: h.Node, ManifestReads: reads,
return nodeengine.ManifestReadsAnswer{V: sdkkeys.Version, Node: h.Node, ManifestReads: reads,
StatedAt: at.UTC().Format(time.RFC3339Nano)}, nil
case "acts":
if id := arg("id"); id != "" {
+27 -7
View File
@@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
"github.com/novox/mesh-host/internal/compositions"
"sort"
"strings"
@@ -52,7 +54,7 @@ type haveState struct {
// issue 1126): kept in its key so a restart reuses them and applies nothing again.
nodeSealed compositions.Seals
// nodeCandidates is the node's answer to each placement candidate proposal (novox/hq ADR 0559 §4), written into its key.
nodeCandidates map[string]delivery.CandidateAnswer
nodeCandidates map[string]nodeengine.CandidateAnswer
// nodeComposes and nodeComposesWhy are whose compositions the node applies and why (HaveNode.Composes), written into
// its key.
nodeComposes, nodeComposesWhy string
@@ -205,7 +207,7 @@ func resourceWords(r ResourceHealth) string {
// nodeFacts is the node's own facts of a report, each as the JSON the report carries it in.
func nodeFacts(r Report) delivery.HaveNode {
n := delivery.HaveNode{V: delivery.Version}
n := delivery.HaveNode{V: sdkkeys.Version}
carry := func(into *json.RawMessage, empty bool, v any) {
if empty {
return
@@ -375,7 +377,7 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) {
sort.Strings(modules)
for _, module := range modules {
f := s.modules[module]
h := delivery.Have{V: delivery.Version, FP: f.FP, Composed: f.Composed, Build: f.Build,
h := delivery.Have{V: sdkkeys.Version, FP: f.FP, Composed: f.Composed, Build: f.Build,
Phase: delivery.PhaseRunning, Health: delivery.HealthUnknown, Detail: f.Detail}
h.Kept = f.Kept
if delivery.Phase(f.Phase) == delivery.PhaseWaitingYes {
@@ -432,7 +434,7 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) {
facts := *s.node
facts.Composed = s.nodeComposed
facts.SealedAt = s.nodeSealed
facts.Candidates = s.nodeCandidates
facts.Candidates = interimAnswers(s.nodeCandidates)
facts.Composes, facts.ComposesWhy = s.nodeComposes, s.nodeComposesWhy
if raw, err = delivery.EncodeHaveNode(facts); err == nil {
want := facts
@@ -507,7 +509,7 @@ func (q *Session) writeRemoved(ctx context.Context, b HaveBucket, node string) b
delete(s.removed, module)
continue
}
h := delivery.Have{V: delivery.Version, Build: s.removed[module], Phase: delivery.PhaseRemoved,
h := delivery.Have{V: sdkkeys.Version, Build: s.removed[module], Phase: delivery.PhaseRemoved,
Health: delivery.HealthUnknown, Detail: "the applied declaration no longer holds the module"}
raw, err := delivery.EncodeHave(h)
if err != nil {
@@ -730,7 +732,7 @@ func (q *Session) NoteNode(ctx context.Context, composed string, sealed composit
s.nodeComposed = composed
s.nodeSealed = sealed
if s.node == nil {
n := delivery.HaveNode{V: delivery.Version}
n := delivery.HaveNode{V: sdkkeys.Version}
s.node = &n
}
s.mu.Unlock()
@@ -742,7 +744,7 @@ func (q *Session) NoteNode(ctx context.Context, composed string, sealed composit
// NoteCandidates takes the node's answers to the placement candidates, by proposal (its shadow composer's, novox/hq ADR
// 0559 §4), and writes the node's key where it differs; a proposal left out is no longer answered.
func (q *Session) NoteCandidates(ctx context.Context, answers map[string]delivery.CandidateAnswer) {
func (q *Session) NoteCandidates(ctx context.Context, answers map[string]nodeengine.CandidateAnswer) {
q.init()
s := &q.haves
s.mu.Lock()
@@ -877,3 +879,21 @@ func nothingJudged(f ModuleFact, looked bool) delivery.Health {
}
return delivery.HealthUnknown
}
// interimAnswers is the node's candidate answers in the form mesh-sdk's HaveNode still holds them, until the node-engine's
// have is written through the seat's contract types (the Have group of the go/delivery rewrite): the two shapes share
// their wire names, so the JSON carries one into the other.
func interimAnswers(in map[string]nodeengine.CandidateAnswer) map[string]delivery.CandidateAnswer {
if in == nil {
return nil
}
raw, err := json.Marshal(in)
if err != nil {
return nil
}
var out map[string]delivery.CandidateAnswer
if json.Unmarshal(raw, &out) != nil {
return nil
}
return out
}
+12 -10
View File
@@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
"github.com/novox/mesh-host/internal/compositions"
"slices"
"strings"
@@ -178,8 +180,8 @@ func TestAnAppliedModuleIsWrittenRunningAndAFailedOneFailed(t *testing.T) {
// 1132/AC5: a have equal to the bucket's value is not written; a different one is written over the last revision.
func TestAHaveIsWrittenOnlyWhenItDiffers(t *testing.T) {
l := newHaveLink()
l.put(t, "n1.a", delivery.Have{V: delivery.Version, Build: "b-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
l.put(t, "n1.b", delivery.Have{V: delivery.Version, Build: "b-0", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
l.put(t, "n1.a", delivery.Have{V: sdkkeys.Version, Build: "b-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
l.put(t, "n1.b", delivery.Have{V: sdkkeys.Version, Build: "b-0", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
q, _ := aHaveQueue(l)
q.SayFacts(context.Background(), appliedModules())
if l.kv["n1.a"].rev != 1 {
@@ -258,7 +260,7 @@ func TestTheNodeKeyCarriesTheNodesFacts(t *testing.T) {
t.Fatal(err)
}
enc := func(v any) json.RawMessage { b, _ := json.Marshal(v); return b }
want := delivery.HaveNode{V: delivery.Version, Windows: enc(rep.Windows), Busy: enc(rep.Busy), Strays: enc(rep.Strays),
want := delivery.HaveNode{V: sdkkeys.Version, Windows: enc(rep.Windows), Busy: enc(rep.Busy), Strays: enc(rep.Strays),
Filters: enc(rep.Filters), Profile: enc(rep.Profile)}
if !delivery.SameHaveNode(got, want) {
t.Errorf("the node key holds %+v, want %+v", got, want)
@@ -280,7 +282,7 @@ func TestTheNodeKeyCarriesTheNodesFacts(t *testing.T) {
if got, err = delivery.DecodeHaveNode(l.kv["n1.node"].value); err != nil || got.Record == nil {
t.Fatalf("the node key holds no record: %+v, %v", got, err)
}
wantRecord := delivery.NodeRecord{V: delivery.Version, Node: "n1", TunnelKey: "tk", SealingKey: "sk", ServingKey: "pk",
wantRecord := delivery.NodeRecord{V: sdkkeys.Version, Node: "n1", TunnelKey: "tk", SealingKey: "sk", ServingKey: "pk",
Certificate: "cert", Trusts: keys.Trusts, Capabilities: []string{"apt", "bus-state-calls", "ledger-calls", "systemd"}, OutwardLinks: []string{"eth0"}, NetworksSaid: true,
Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}}, Contract: NodeKeyContract,
ManifestReads: catalogue.ManifestReads()} // what it reads of a manifest: its contract's (ADR 0614 §4)
@@ -460,7 +462,7 @@ func TestARemovedModuleAddedBackIsWrittenWithItsNewPhase(t *testing.T) {
// bucket is not created again.
func TestAModuleNeverWrittenIsNotWrittenRemoved(t *testing.T) {
l := newHaveLink()
l.put(t, "n1.c", delivery.Have{V: delivery.Version, Build: "b-9", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
l.put(t, "n1.c", delivery.Have{V: sdkkeys.Version, Build: "b-9", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
q, _ := aHaveQueue(l)
q.SayFacts(context.Background(), appliedModules())
q.SayFacts(context.Background(), onlyA())
@@ -502,8 +504,8 @@ func TestARefusedRemovalIsWrittenByTheNextPass(t *testing.T) {
// before: one of them the new declaration lacks is written removed, one that is not in the bucket is not created.
func TestARestartedNodeEngineWritesTheRemovalFromTheReportsBefore(t *testing.T) {
l := newHaveLink()
l.put(t, "n1.a", delivery.Have{V: delivery.Version, Build: "b-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
l.put(t, "n1.b", delivery.Have{V: delivery.Version, Build: "b-2", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
l.put(t, "n1.a", delivery.Have{V: sdkkeys.Version, Build: "b-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
l.put(t, "n1.b", delivery.Have{V: sdkkeys.Version, Build: "b-2", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
q, _ := aHaveQueue(l) // fresh: nothing written by this process
r := onlyA()
r.Before = []string{"a", "b", "c"}
@@ -611,7 +613,7 @@ func TestAnAppliedModulesHaveNamesTheFingerprintAndTheDigestAndFollowsItsHealth(
t.Fatalf("the node key read back: %+v %v", n, ok)
}
// The node's answers to placement candidates ride in the same key (ADR 0559 §4), and go when no group is answered.
answers := map[string]delivery.CandidateAnswer{"p-1": {Proposal: "p-1", Revision: 1, Verdict: delivery.CandidateSame, Same: []string{"web"}}}
answers := map[string]nodeengine.CandidateAnswer{"p-1": {Proposal: "p-1", Revision: 1, Verdict: nodeengine.CandidateAnswerVerdictSame, Same: []string{"web"}}}
q.NoteCandidates(context.Background(), answers)
if n, err := delivery.DecodeHaveNode(l.kv["n1.node"].value); err != nil || n.Candidates["p-1"].Proposal != "p-1" || n.Composed != dig {
t.Fatalf("the node key with its answers: %+v %v", n, err)
@@ -652,7 +654,7 @@ func TestAModuleSaidAgainOverAnOlderEnginesUnknownIsWrittenWithThisEnginesHealth
l := newHaveLink()
fp := "src2:" + strings.Repeat("a", 64)
dig := "sha256:" + strings.Repeat("b", 64)
l.put(t, "n1.xorg", delivery.Have{V: delivery.Version, FP: fp, Composed: dig, Phase: delivery.PhaseRunning,
l.put(t, "n1.xorg", delivery.Have{V: sdkkeys.Version, FP: fp, Composed: dig, Phase: delivery.PhaseRunning,
Health: delivery.HealthUnknown})
q, _ := aHaveQueue(l)
if !q.SayHealth(context.Background(), *healthOf(map[string]string{"web": StateHealthy})) {
@@ -748,7 +750,7 @@ func TestADeclarationReportMovesAHealthAndARemovalForgetsIt(t *testing.T) {
// key that does not exist is not made up and is said once; a writer that drops the list again is written over.
func TestAUserListReachesTheHaveOfAModuleNoApplyHereSays(t *testing.T) {
l := newHaveLink()
older := delivery.Have{V: delivery.Version, Build: "build-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
older := delivery.Have{V: sdkkeys.Version, Build: "build-1", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
l.put(t, "n1.nats", older)
q, said := aHaveQueue(l)
dig := "sha256:" + strings.Repeat("c", 64)
+2 -1
View File
@@ -2,6 +2,7 @@ package link
import (
"encoding/json"
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
"slices"
"sort"
"sync"
@@ -60,7 +61,7 @@ func recordOf(node string, r Report, keys RecordKeys) *delivery.NodeRecord {
if err != nil || json.Unmarshal(raw, &p) != nil {
return nil
}
rec := &delivery.NodeRecord{V: delivery.Version, Node: node, TunnelKey: keys.TunnelKey, SealingKey: keys.SealingKey,
rec := &delivery.NodeRecord{V: sdkkeys.Version, Node: node, TunnelKey: keys.TunnelKey, SealingKey: keys.SealingKey,
ServingKey: keys.ServingKey, Certificate: keys.Certificate, Authority: keys.Authority, Trusts: keys.Trusts,
ManifestReads: manifestReads()}
for _, c := range p.Capabilities {
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
sdkkeys "git.novox.be/novox/mesh-sdk/go/keys"
"strings"
"sync"
"time"
@@ -413,7 +414,7 @@ func (q *Session) SayReads() {
}
q.reads.linked = true
q.reads.Unlock()
q.SayEngine(nodeengine.EventManifestReadsChanged, nodeengine.ManifestReadsChangedEvent{V: delivery.Version,
q.SayEngine(nodeengine.EventManifestReadsChanged, nodeengine.ManifestReadsChangedEvent{V: sdkkeys.Version,
Node: q.Membership.Node, ManifestReads: manifestReads(), At: time.Now().UTC().Format(time.RFC3339Nano), Why: why})
}
+7 -6
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"maps"
"slices"
"sort"
@@ -173,7 +174,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi
// reads every manifest in the mesh one by one, which off the control-node took about 35 s each (issue 1126).
base.warm(ctx, c, cand)
var running []delivery.Composed
var running []engcomp.Composed
if c.env.Running != nil {
running = c.env.Running()
}
@@ -232,8 +233,8 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi
case laidErr != nil:
return refuse(laidErr.Error())
}
byModule := func(comps map[string]delivery.Composed) map[string]delivery.Composed {
out := map[string]delivery.Composed{}
byModule := func(comps map[string]engcomp.Composed) map[string]engcomp.Composed {
out := map[string]engcomp.Composed{}
for _, comp := range comps {
out[comp.Module] = comp
}
@@ -256,7 +257,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi
if !ok {
return m, false
}
dig, err := delivery.ComposedDigestOf(comp)
dig, err := engcomp.DigestOf(comp)
if err != nil {
m.State, m.Words = nec.ModuleRefused, err.Error()
return m, true
@@ -264,7 +265,7 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask nodeengi
m.State, m.Fp, m.Digest = nec.ModuleComposed, comp.FP, dig
if before, ok := was[module]; !ok {
m.Changed = true
} else if d, err := delivery.ComposedDigestOf(before); err != nil || d != dig {
} else if d, err := engcomp.DigestOf(before); err != nil || d != dig {
m.Changed = true
}
return m, true
@@ -426,7 +427,7 @@ func (c *Composer) layCandidate(ctx context.Context, cand delivery.Candidate) (m
// composeShadow composes this node from what c holds, as the shadow composes it (shadowBuilds): each module at the build
// it runs, one placed and not run at the indexer's, and one the candidate laid (laid) at the candidate's. input, when
// given, is handed the scope.
func (c *Composer) composeShadow(ctx context.Context, running []delivery.Composed, laid map[string]bool, input func(frombus.Scope, map[string]string)) (map[string]delivery.Composed, map[string]string, error) {
func (c *Composer) composeShadow(ctx context.Context, running []engcomp.Composed, laid map[string]bool, input func(frombus.Scope, map[string]string)) (map[string]engcomp.Composed, map[string]string, error) {
node := c.env.Node
c.mu.Lock()
placement, placed := c.placements[node]
+1 -1
View File
@@ -74,7 +74,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
var applied atomic.Int64
env.Refused = func(string, map[string]string) { applied.Add(1) }
var handed atomic.Int64
s := NewShadow(env, func(context.Context, map[string]delivery.CandidateAnswer) { handed.Add(1) })
s := NewShadow(env, func(context.Context, map[string]nodeengine.CandidateAnswer) { handed.Add(1) })
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = s.Run(ctx) }()
+16 -15
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"maps"
"sort"
"strings"
@@ -17,7 +18,7 @@ import (
)
// **The compositions this node makes, published in its composition state** (novox/hq ADR 0593 §6). The node's engine
// alone composes, and it writes every composition it makes into `composed-<node>` (delivery.ComposedBucketFor), a
// alone composes, and it writes every composition it makes into `composed-<node>` (engcomp.BucketFor), a
// record for the provisioner, the assigner and the view, never an instruction: the engine applies only what it composes
// itself, never a value read from the state, so a value carries no signature. After a pass that composed, it writes:
//
@@ -30,7 +31,7 @@ import (
// - a module refused alone at a fingerprint as that key with its refusal (Refused), and no resources.
//
// A key is written only when its value differs from what the state holds (read once per run), and a module's key is
// deleted once its fingerprint is not among the five it wrote last (delivery.ComposedKeep) and no want names it. A pass
// deleted once its fingerprint is not among the five it wrote last (engcomp.Keep) and no want names it. A pass
// refused whole, or a bus that did not answer, writes nothing: what was written stands, and each module's have says why.
// ComposedStore is this node's composition state, bound and never created (the grants part creates it).
@@ -51,7 +52,7 @@ type ComposedEntry struct {
type published struct {
scope frombus.Scope
fps map[string]string // by module, the wanted fingerprint
comps map[string]delivery.Composed
comps map[string]engcomp.Composed
held map[string]string
// placed is the modules the node's placement puts here, ran what runs of each module (by its composition's FP).
placed []string
@@ -151,7 +152,7 @@ func (c *Composer) writeComposed(ctx context.Context, at *published) (wrote, del
func (c *Composer) composedValues(ctx context.Context, at *published) (map[string][]byte, error) {
r := reader{c: c}
out := map[string][]byte{}
put := func(comp delivery.Composed, plans map[string]catalogue.Plan) error {
put := func(comp engcomp.Composed, plans map[string]catalogue.Plan) error {
if comp.Module != "" && comp.Refused == "" {
if p, ok := plans[comp.Module]; ok {
p := p
@@ -162,7 +163,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin
if err != nil {
return err
}
raw, err := delivery.EncodeComposed(comp)
raw, err := engcomp.Encode(comp)
if err != nil {
return fmt.Errorf("the composition %s cannot be written: %w", key, err)
}
@@ -172,7 +173,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin
runs := []struct {
scope frombus.Scope
fps map[string]string
comps map[string]delivery.Composed
comps map[string]engcomp.Composed
held map[string]string
}{{at.scope, at.fps, at.comps, at.held}}
// The newest builds, composed only where they differ from the wanted ones.
@@ -209,7 +210,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin
runs = append(runs, struct {
scope frombus.Scope
fps map[string]string
comps map[string]delivery.Composed
comps map[string]engcomp.Composed
held map[string]string
}{scope, newest, comps, held})
}
@@ -234,7 +235,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin
if !ok || module == agent.NodeModule {
continue
}
if err := put(delivery.Composed{Node: c.env.Node, Module: module, FP: fp, Refused: why}, nil); err != nil {
if err := put(engcomp.Composed{Node: c.env.Node, Module: module, FP: fp, Refused: why}, nil); err != nil {
return nil, err
}
}
@@ -243,15 +244,15 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin
}
// composedKeyOf is a composition's key in the state.
func composedKeyOf(comp delivery.Composed) (string, error) {
func composedKeyOf(comp engcomp.Composed) (string, error) {
if comp.Module == "" {
return delivery.ComposedNodeKey(comp.Node)
return engcomp.NodeKey(comp.Node)
}
return delivery.ComposedKey(comp.Node, comp.Module, comp.FP)
return engcomp.Key(comp.Node, comp.Module, comp.FP)
}
// pastKeeping is, of the module keys the state holds, those whose fingerprint is not among the five its module wrote
// last (delivery.ComposedKeep, by revision) and that no want names (ADR 0593 §6). Under pubWrite.
// last (engcomp.Keep, by revision) and that no want names (ADR 0593 §6). Under pubWrite.
func (c *Composer) pastKeeping() []string {
c.mu.Lock()
wanted := map[string]string{}
@@ -266,7 +267,7 @@ func (c *Composer) pastKeeping() []string {
}
byModule := map[string][]kept{}
for key, e := range c.pubKept {
_, module, fp, isNode, err := delivery.ParseComposedKey(key)
_, module, fp, isNode, err := engcomp.ParseKey(key)
if err != nil || isNode {
continue
}
@@ -276,7 +277,7 @@ func (c *Composer) pastKeeping() []string {
for module, ks := range byModule {
sort.Slice(ks, func(i, j int) bool { return ks[i].rev > ks[j].rev })
for i, k := range ks {
if i < delivery.ComposedKeep || wanted[module] == k.fp {
if i < engcomp.Keep || wanted[module] == k.fp {
continue
}
out = append(out, k.key)
@@ -309,7 +310,7 @@ func (c *Composer) sayComposed(line string) {
}
// ranOf is, of what runs, each module's fingerprint by its composition (the node's own left out).
func ranOf(running []delivery.Composed) map[string]string {
func ranOf(running []engcomp.Composed) map[string]string {
out := map[string]string{}
for _, comp := range running {
if comp.Module != "" && comp.FP != "" && !strings.EqualFold(comp.Module, agent.NodeModule) {
+5 -4
View File
@@ -2,6 +2,7 @@ package selfcompose
import (
"context"
engcomp "github.com/novox/mesh-host/internal/compositions"
"slices"
"strings"
"sync"
@@ -100,8 +101,8 @@ func TestTheNodePublishesItsCompositions(t *testing.T) {
}
for u := next(t, out); !u.Caught; u = next(t, out) {
}
lemursKey, _ := delivery.ComposedKey("n1", "lemurs", fp)
oldieKey, _ := delivery.ComposedKey("n1", "oldie", ofp)
lemursKey, _ := engcomp.Key("n1", "lemurs", fp)
oldieKey, _ := engcomp.Key("n1", "oldie", ofp)
waitFor := func(what string, ok func([]string) bool) []string {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
@@ -122,7 +123,7 @@ func TestTheNodePublishesItsCompositions(t *testing.T) {
t.Errorf("the first pass wrote %v, want the wanted module's key and the node's own", first)
}
if raw := state.kv[lemursKey].Value; raw != nil {
comp, err := delivery.DecodeComposed(raw)
comp, err := engcomp.Decode(raw)
if err != nil || comp.Module != "lemurs" || comp.FP != fp || len(comp.Resources) == 0 {
t.Errorf("the published composition of lemurs: %+v, %v", comp, err)
}
@@ -158,7 +159,7 @@ func TestTheNodePublishesItsCompositions(t *testing.T) {
keep.pubKept = map[string]ComposedEntry{}
var keys []string
for i, ch := range "123456" {
key, _ := delivery.ComposedKey("n1", "lemurs", delivery.SourcePrefix2+strings.Repeat(string(ch), 64))
key, _ := engcomp.Key("n1", "lemurs", delivery.SourcePrefix2+strings.Repeat(string(ch), 64))
keys = append(keys, key)
keep.pubKept[key] = ComposedEntry{Revision: uint64(10 + i)}
}
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"sort"
"strings"
"time"
@@ -126,7 +127,7 @@ func (c *Composer) sayData(line string) {
func (c *Composer) digestsByModule() map[string]string {
out := map[string]string{}
for key, dig := range c.handed {
_, module, _, isNode, err := delivery.ParseComposedKey(key)
_, module, _, isNode, err := engcomp.ParseKey(key)
if err != nil {
continue
}
+9 -8
View File
@@ -3,6 +3,7 @@ package selfcompose
import (
"context"
"errors"
engcomp "github.com/novox/mesh-host/internal/compositions"
"slices"
"strings"
"sync"
@@ -89,8 +90,8 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) {
t.Error("the node's grant reads other keys of the engine's state data than its own")
}
c := New(env)
webKey, _ := delivery.ComposedKey("n1", "web", delivery.SourcePrefix2+strings.Repeat("a", 64))
nodeKey, _ := delivery.ComposedNodeKey("n1")
webKey, _ := engcomp.Key("n1", "web", delivery.SourcePrefix2+strings.Repeat("a", 64))
nodeKey, _ := engcomp.NodeKey("n1")
for _, step := range []struct {
name string
due bool
@@ -183,8 +184,8 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) {
// the key again, so a key lost on the bus is written back. Another node's key is never touched.
pkgStore := &fakeData{kv: map[string][]byte{"n2": []byte(`{"node":"n2","packages":[]}`)}}
pc := New(Env{Node: "n1", Packages: pkgStore})
web := func(pkgs ...string) delivery.Composed {
comp := delivery.Composed{Node: "n1", Module: "web"}
web := func(pkgs ...string) engcomp.Composed {
comp := engcomp.Composed{Node: "n1", Module: "web"}
for _, p := range pkgs {
comp.Resources = append(comp.Resources, []byte(`{"id":"`+p+`","type":"package","package":"`+p+`"}`))
}
@@ -193,25 +194,25 @@ func TestTheEngineKeepsEachPlacedModulesDataItems(t *testing.T) {
}
for _, step := range []struct {
name string
comp delivery.Composed
comp engcomp.Composed
fail error
puts int
owed bool
holds string
}{
{name: "the run's first pass writes the key", comp: web("curl"), puts: 1,
holds: `{"node":"n1","packages":[{"package":"curl","modules":["web"],"absent":false}]}`},
holds: `{"node":"n1","packages":[{"absent":false,"modules":["web"],"package":"curl"}]}`},
{name: "a pass that changes no package writes nothing", comp: web("curl"), puts: 1},
{name: "a refused write is owed after the bound", comp: web("curl", "jq"), fail: errors.New("no grant yet"), puts: 1, owed: true},
{name: "a composition adding a package writes the key once", comp: web("curl", "jq"), puts: 2,
holds: `{"node":"n1","packages":[{"package":"curl","modules":["web"],"absent":false},{"package":"jq","modules":["web"],"absent":false}]}`},
holds: `{"node":"n1","packages":[{"absent":false,"modules":["web"],"package":"curl"},{"absent":false,"modules":["web"],"package":"jq"}]}`},
} {
pkgStore.mu.Lock()
pkgStore.fail = step.fail
pkgStore.mu.Unlock()
pc.paid()
pc.mu.Lock()
pc.pkgs, pc.pkgsComposed, pc.pkgsDue = map[string]delivery.Composed{webKey: step.comp}, true, true
pc.pkgs, pc.pkgsComposed, pc.pkgsDue = map[string]engcomp.Composed{webKey: step.comp}, true, true
pc.mu.Unlock()
pc.publishPackages(context.Background())
if len(pkgStore.puts) != step.puts || pc.owed.Load() != step.owed ||
+9 -8
View File
@@ -6,10 +6,11 @@ import (
"encoding/json"
"errors"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
engcomp "github.com/novox/mesh-host/internal/compositions"
"sort"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// **The packages this node's compositions declare, kept by its engine** (novox/hq ADR 0565 §6, §15; issue 1953). The
@@ -28,8 +29,8 @@ type PackagesStore interface {
}
// packagesOf is a composition's package resources alone, with its module: what the node's key is worked out from.
func packagesOf(comp delivery.Composed) delivery.Composed {
out := delivery.Composed{Node: comp.Node, Module: comp.Module}
func packagesOf(comp engcomp.Composed) engcomp.Composed {
out := engcomp.Composed{Node: comp.Node, Module: comp.Module}
for _, raw := range comp.Resources {
var head struct {
Type string `json:"type"`
@@ -42,24 +43,24 @@ func packagesOf(comp delivery.Composed) delivery.Composed {
}
// DeclaredPackages is the node's key as this composer would write it now, from what it handed.
func (c *Composer) DeclaredPackages() (delivery.DeclaredPackages, error) {
func (c *Composer) DeclaredPackages() (nodeengine.PackagesByNodeValue, error) {
c.mu.Lock()
keys := make([]string, 0, len(c.pkgs))
for k := range c.pkgs {
keys = append(keys, k)
}
sort.Strings(keys)
comps := make([]delivery.Composed, 0, len(keys))
comps := make([]engcomp.Composed, 0, len(keys))
for _, k := range keys {
comps = append(comps, c.pkgs[k])
}
composed := c.pkgsComposed
c.mu.Unlock()
if !composed {
return delivery.DeclaredPackages{}, errors.New("this node's composer has handed no composition yet in this run, so it " +
return nodeengine.PackagesByNodeValue{}, errors.New("this node's composer has handed no composition yet in this run, so it " +
"declares no packages yet")
}
return delivery.DeclaredPackagesOf(c.env.Node, comps)
return engcomp.DeclaredPackages(c.env.Node, comps)
}
// publishPackages writes the node's key of the state packages when a pass asked it (pkgsDue) and the value differs.
@@ -86,7 +87,7 @@ func (c *Composer) publishPackages(ctx context.Context) {
}
// enginePackagesBucket is the engine's state packages as the bus names it, for what the composer says.
const enginePackagesBucket = "mesh-host_" + delivery.EnginePackagesState
const enginePackagesBucket = engcomp.PackagesBucket
// WritePackages writes the node's key again from what the composer handed, when it differs from what the bus holds
// now (the node seat's verb packages with write, ADR 0565 §15): the key is read again rather than trusted, so a key lost
+18 -17
View File
@@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"github.com/novox/mesh-host/internal/compositions"
engcomp "github.com/novox/mesh-host/internal/compositions"
"maps"
"reflect"
"slices"
@@ -71,7 +72,7 @@ type Env struct {
// switch to composing its own nor a restart changes a composition (ADR 0547 §3); a box held at another slot is
// never taken, though it opens to the same value (issue 1126, C1: a provider's grant and its consumer's secret).
// Either nil keeps the vault's box.
Running func() []delivery.Composed
Running func() []compositions.Composed
Unseal func(sealed string) ([]byte, error)
// Held is the sealed values a module's have says it runs, kept by the agent on the bus: what runs after a restart,
// before the agent applied anything, and after the switch once the kept declaration is old. By slot
@@ -195,7 +196,7 @@ type Composer struct {
// pkgs is the package resources of each composition handed, by its composed key, and pkgsComposed whether this run
// handed any (ADR 0565 §6); pkgsDue, pkgsTries and pkgsSaid are as the data items' are. Under mu. pkgsKept is what
// the node's key of the state packages held when last read or written (pkgsRead), under pkgsWrite.
pkgs map[string]delivery.Composed
pkgs map[string]compositions.Composed
pkgsComposed bool
pkgsDue bool
pkgsTries int
@@ -299,7 +300,7 @@ func New(env Env) *Composer {
return &Composer{env: env, wants: map[string]delivery.Want{}, placements: map[string]delivery.Placement{}, placementKeys: delivery.NewPlacementKeys(),
records: map[string]delivery.NodeRecord{}, inOwn: map[string]bool{}, modules: map[string]delivery.Module{}, values: map[string]map[string]any{},
manifests: map[string][]byte{}, archives: map[string][]byte{}, builds: map[string]delivery.BuilderBuild{}, handed: map[string]string{}, sealedOwn: map[string]ownSealed{},
pkgs: map[string]delivery.Composed{}, asked: make(chan chan Pass, 8)}
pkgs: map[string]compositions.Composed{}, asked: make(chan chan Pass, 8)}
}
// ComposeNow asks the composer for a pass now (the node-engine's verb compose, issue 774): the growing bound after a seat
@@ -880,7 +881,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
}
// What it runs: a folded module of an old-form build is carried in its own composition as it runs (issue 1126).
var running []delivery.Composed
var running []compositions.Composed
if c.env.Running != nil {
running = c.env.Running()
}
@@ -999,7 +1000,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
continue
}
now[key] = true
dig, err := delivery.ComposedDigestOf(comp)
dig, err := engcomp.DigestOf(comp)
if err != nil {
c.env.Say(fmt.Sprintf("the composition %s has no digest, and is not handed: %v", key, err))
continue
@@ -1007,7 +1008,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
if c.handed[key] == dig {
continue
}
raw, err := delivery.EncodeComposed(comp)
raw, err := engcomp.Encode(comp)
if err != nil {
c.env.Say(fmt.Sprintf("the composition %s cannot be written, and is not handed: %v", key, err))
continue
@@ -1018,7 +1019,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
out = append(out, agent.Update{Key: key, Value: raw, Revision: c.rev})
}
for key := range c.handed {
if _, module, _, _, err := delivery.ParseComposedKey(key); err == nil && module != "" {
if _, module, _, _, err := engcomp.ParseKey(key); err == nil && module != "" {
if _, isHeld := held[module]; isHeld {
continue // held: what was handed of it stays, so the node keeps running it
}
@@ -1038,7 +1039,7 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
placedMods = append(placedMods, m.Module)
}
sort.Strings(placedMods)
pubComps := make(map[string]delivery.Composed, len(compositions))
pubComps := make(map[string]engcomp.Composed, len(compositions))
for key, comp := range compositions {
if comp.Module == "" && !nodeWanted {
continue
@@ -1097,7 +1098,7 @@ func (c *Composer) heldNow() []string {
// value (compositions.KeepSeals, issue 1126, C1): of what runs (running, the agent's), else of what its module's have says
// it runs (Held). A have written before slots names no slot: its boxes are reused within their own module alone, at a
// slot no box is kept for, and only one opening to the slot's value.
func (c *Composer) keepRunning(comp delivery.Composed, running []delivery.Composed) delivery.Composed {
func (c *Composer) keepRunning(comp compositions.Composed, running []compositions.Composed) compositions.Composed {
if c.env.Unseal == nil {
return comp
}
@@ -1731,7 +1732,7 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string
if !placed {
return "", false
}
var running []delivery.Composed
var running []compositions.Composed
if c.env.Running != nil {
running = c.env.Running()
}
@@ -1742,7 +1743,7 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string
}
for _, comp := range comps {
if comp.Module == module && comp.FP == fp {
dig, err := delivery.ComposedDigestOf(c.keepRunning(comp, running))
dig, err := compositions.DigestOf(c.keepRunning(comp, running))
return dig, err == nil
}
}
@@ -1750,7 +1751,7 @@ func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string
}
// ownProvisioner is the provisioner client's account the node's own composition among cs names, or "".
func ownProvisioner(cs []delivery.Composed) string {
func ownProvisioner(cs []compositions.Composed) string {
for _, c := range cs {
if c.Module == "" {
return c.Own.Provisioner
@@ -1759,8 +1760,8 @@ func ownProvisioner(cs []delivery.Composed) string {
return ""
}
func mapValues(m map[string]delivery.Composed) []delivery.Composed {
out := make([]delivery.Composed, 0, len(m))
func mapValues(m map[string]compositions.Composed) []compositions.Composed {
out := make([]compositions.Composed, 0, len(m))
for _, k := range slices.Sorted(maps.Keys(m)) {
out = append(out, m[k])
}
@@ -1778,18 +1779,18 @@ const WaitsForTheSeatListOwn = "the node has not read the seat list, so it compo
// digest) while the reader may not act on its seats, and names the module in held: it keeps running what it runs, and
// the pass after the list was read hands it. The node's own composition is held the same way, whatever it folds, under
// the scope agent.NodeModule; a composition that runs already is left as it is.
func (c *Composer) holdUntilListed(ctx context.Context, r reader, compositions map[string]delivery.Composed, held map[string]string, running []delivery.Composed) map[string]string {
func (c *Composer) holdUntilListed(ctx context.Context, r reader, compositions map[string]compositions.Composed, held map[string]string, running []compositions.Composed) map[string]string {
if c.env.SeatList == nil {
return held
}
ran := map[string]bool{}
for _, comp := range running {
if dig, err := delivery.ComposedDigestOf(comp); err == nil {
if dig, err := engcomp.DigestOf(comp); err == nil {
ran[comp.Module+"/"+dig] = true
}
}
for key, comp := range compositions {
if dig, err := delivery.ComposedDigestOf(comp); err == nil && ran[comp.Module+"/"+dig] {
if dig, err := engcomp.DigestOf(comp); err == nil && ran[comp.Module+"/"+dig] {
continue
}
module, why := comp.Module, WaitsForTheSeatList
+63 -62
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"github.com/novox/mesh-host/internal/compositions"
"os"
"reflect"
@@ -172,7 +173,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
return secretChanged, func() {}, nil
}
var composed atomic.Int64 // passes made: each reads what runs once
env.Running = func() []delivery.Composed { composed.Add(1); return nil }
env.Running = func() []compositions.Composed { composed.Add(1); return nil }
c := New(env)
out, err := c.Watch(ctx, "n1.>")
if err != nil {
@@ -204,13 +205,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
b.send(bucket, agent.Update{Caught: true})
}
got := map[string]bool{}
var own delivery.Composed
var own compositions.Composed
for {
u := next(t, out)
if u.Caught {
break
}
c, err := delivery.DecodeComposed(u.Value)
c, err := compositions.Decode(u.Value)
if err != nil {
t.Fatalf("%s: %v", u.Key, err)
}
@@ -261,8 +262,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
t.Errorf("a composer that watches nothing answered compose now: %v", err)
}
icancel()
lemursKey, _ := delivery.ComposedKey("n1", "lemurs", fp)
nodeKey, _ := delivery.ComposedNodeKey("n1")
lemursKey, _ := compositions.Key("n1", "lemurs", fp)
nodeKey, _ := compositions.NodeKey("n1")
if !got[lemursKey] || !got[nodeKey] || len(got) != 2 {
b.mu.Lock()
t.Logf("said: %v", b.said)
@@ -436,14 +437,14 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
r3 := c3.records["n1"]
r3.Capabilities = []string{"systemd", "container-runtime"}
c3.records["n1"] = r3
spiegelKey, _ := delivery.ComposedKey("n1", "de-spiegel", c3.wants["de-spiegel"].FP)
proxyKey, _ := delivery.ComposedKey("n1", "proxy", c3.wants["proxy"].FP)
spiegelKey, _ := compositions.Key("n1", "de-spiegel", c3.wants["de-spiegel"].FP)
proxyKey, _ := compositions.Key("n1", "proxy", c3.wants["proxy"].FP)
c3.handed[spiegelKey] = "sha256:" + strings.Repeat("5", 64) // what the node runs of it, handed before
handed3 := map[string]agent.Update{}
for _, u := range c3.compose(ctx) {
handed3[u.Key] = u
}
lemursKey3, _ := delivery.ComposedKey("n1", "lemurs", c3.wants["lemurs"].FP)
lemursKey3, _ := compositions.Key("n1", "lemurs", c3.wants["lemurs"].FP)
if u, ok := handed3[lemursKey3]; !ok || u.Deleted {
t.Errorf("lemurs, which composes, is not handed beside de-spiegel's refusal: %v (told %q)", handed3, *told)
}
@@ -507,7 +508,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
p5.Decided.Endpoint = "n1.example:51820"
c5.placements["n1"] = p5
c5.inOwn[overlay.Name] = true
nodeKey5, _ := delivery.ComposedNodeKey("n1")
nodeKey5, _ := compositions.NodeKey("n1")
var own5 string
for _, u := range c5.compose(ctx) {
if u.Key == nodeKey5 {
@@ -678,8 +679,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
defer hmu.Unlock()
held = append(held, why+fmt.Sprint(h))
}
tenv.Running = func() []delivery.Composed {
return []delivery.Composed{{Node: "n1", Module: "tool", FP: old, Resources: []json.RawMessage{
tenv.Running = func() []compositions.Composed {
return []compositions.Composed{{Node: "n1", Module: "tool", FP: old, Resources: []json.RawMessage{
json.RawMessage(`{"id":"service","module":"tool","type":"service","unit":"tool.service","state":"running","boot":"enabled"}`)}}}
}
tc := New(tenv)
@@ -702,7 +703,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
ResolvedBucket, delivery.BuilderBuildsBucket} {
tb.send(bucket, agent.Update{Caught: true})
}
toolKey, _ := delivery.ComposedKey("n1", "tool", next2)
toolKey, _ := compositions.Key("n1", "tool", next2)
for u := next(t, tout); !u.Caught; u = next(t, tout) {
if u.Key == toolKey {
t.Errorf("tool is handed at a build not yet done")
@@ -781,7 +782,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
}
b.put(t, delivery.AssignerWantsBucket, "want.n1.keeper", want)
keeperKey, _ := delivery.ComposedKey("n1", "keeper", fp)
keeperKey, _ := compositions.Key("n1", "keeper", fp)
var handed string
for handed == "" {
if u := next(t, out); u.Key == keeperKey {
@@ -823,7 +824,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
// or another resource of keeper holds is never taken, though it opens to the same value; a have written before slots
// (a list) is reused within its own module alone.
boxOf := func(raw string) (string, string) {
c, err := delivery.DecodeComposed([]byte(raw))
c, err := compositions.Decode([]byte(raw))
if err != nil {
t.Fatal(err)
}
@@ -852,7 +853,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
{"the bus does not answer for the have", "keeper", rotatedBox, nil, false, true},
} {
restarted := env
restarted.Running = func() []delivery.Composed { return nil }
restarted.Running = func() []compositions.Composed { return nil }
restarted.Held = func(module string) (compositions.Seals, []string, error) {
if module != row.in {
return nil, nil, nil
@@ -917,7 +918,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
// And the node's own key (review of L2, round 3, finding 1): a core box its own composition carries is held there, and
// kept after a restart as a module's is, so the node's own composition does not move either.
ownComp := delivery.Composed{Node: "n1", Resources: []json.RawMessage{json.RawMessage(`{"id":"core-box","sealed":"{\"ask\":999}"}`)}}
ownComp := compositions.Composed{Node: "n1", Resources: []json.RawMessage{json.RawMessage(`{"id":"core-box","sealed":"{\"ask\":999}"}`)}}
b.mu.Lock()
sealedAt[`{"ask":999}`] = generation + 1
b.mu.Unlock()
@@ -946,8 +947,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
sealedAt["box-again"] = sealedAt[rotatedBox]
sealedAt["box-other"] = sealedAt[rotatedBox] + 100
b.mu.Unlock()
ini := func(db, key string) delivery.Composed {
return delivery.Composed{Node: "n1", Module: "grafana", Resources: []json.RawMessage{json.RawMessage(
ini := func(db, key string) compositions.Composed {
return compositions.Composed{Node: "n1", Module: "grafana", Resources: []json.RawMessage{json.RawMessage(
`{"content":"[database]\npassword = ${secret:postgres-database}\n[security]\nsecret_key = ${secret:secret-key}\n",` +
`"id":"grafana.ini","mode":"0600","module":"grafana","path":"/etc/grafana/grafana.ini","secrets":{"postgres-database":` +
strconv.Quote(db) + `,"secret-key":` + strconv.Quote(key) + `},"type":"file"}`)}}
@@ -985,8 +986,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
// The node runs the consumer's credential already, sealed otherwise (the controller sealed it, or the vault at
// another ask): that box opens to the same value, so it is kept, and the composition does not move.
pairEnv.Running = func() []delivery.Composed {
return []delivery.Composed{{Module: "web", Resources: []json.RawMessage{json.RawMessage(`{"id":"web.secret-pg","sealed":"{\"run\":1}"}`)}}}
pairEnv.Running = func() []compositions.Composed {
return []compositions.Composed{{Module: "web", Resources: []json.RawMessage{json.RawMessage(`{"id":"web.secret-pg","sealed":"{\"run\":1}"}`)}}}
}
opened := map[string]string{`{"c":1}`: "pg-pw", `{"run":1}`: "pg-pw", `{"p":1}`: "s3-pw"}
pairEnv.Unseal = func(box string) ([]byte, error) {
@@ -1009,7 +1010,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
t.Errorf("credentials %+v issued %+v", creds, issued)
}
// What runs at the credential's slot is kept, after the composition (keepRunning, issue 1126, C1).
webComp := delivery.Composed{Node: "n1", Module: "web", Resources: []json.RawMessage{json.RawMessage(`{"id":"web.secret-pg","sealed":"{\"c\":1}"}`)}}
webComp := compositions.Composed{Node: "n1", Module: "web", Resources: []json.RawMessage{json.RawMessage(`{"id":"web.secret-pg","sealed":"{\"c\":1}"}`)}}
if got := compositions.SealsIn(pc.keepRunning(webComp, pairEnv.Running()))["web.secret-pg#sealed"]; got != `{"run":1}` {
t.Errorf("web's credential composed as %s, want the box that runs", got)
}
@@ -1053,8 +1054,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
if err != nil {
t.Fatal(err)
}
senv.Running = func() []delivery.Composed {
var out []delivery.Composed
senv.Running = func() []compositions.Composed {
var out []compositions.Composed
for _, c := range running {
out = append(out, c)
}
@@ -1062,9 +1063,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
// The controller's compositions at n1's wants, in n1's composition bucket: what a head deleting its placement
// returns it to (the seed's revert); here the ones it runs.
composedBucket, _ := delivery.ComposedBucketFor("n1")
composedBucket, _ := compositions.BucketFor("n1")
for key, c := range running {
raw, err := delivery.EncodeComposed(c)
raw, err := compositions.Encode(c)
if err != nil {
t.Fatal(err)
}
@@ -1072,8 +1073,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
readOnly.kv[composedBucket+"/"+key] = raw
readOnly.mu.Unlock()
}
answered := make(chan map[string]delivery.CandidateAnswer, 8)
sh := NewShadow(senv, func(_ context.Context, a map[string]delivery.CandidateAnswer) { answered <- a })
answered := make(chan map[string]nodeengine.CandidateAnswer, 8)
sh := NewShadow(senv, func(_ context.Context, a map[string]nodeengine.CandidateAnswer) { answered <- a })
sctx, scancel := context.WithCancel(ctx)
defer scancel()
// Review of L2b, round 1, finding 2: the candidates' state is not there when the node starts (a delivery before the
@@ -1096,7 +1097,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
var lemursDigest string
for _, c := range running {
if c.Module == "lemurs" {
lemursDigest, _ = delivery.ComposedDigestOf(c)
lemursDigest, _ = compositions.DigestOf(c)
}
}
wantRaw, err := delivery.EncodeWant(delivery.Want{FP: fp, Composed: lemursDigest})
@@ -1142,8 +1143,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
sb.put(delivery.PlacementCandidatesBucket, k, raw, 8)
}
sb.caught()
var answers map[string]delivery.CandidateAnswer
waitFor := func(why string, ok func(map[string]delivery.CandidateAnswer) bool) {
var answers map[string]nodeengine.CandidateAnswer
waitFor := func(why string, ok func(map[string]nodeengine.CandidateAnswer) bool) {
t.Helper()
deadline := time.After(10 * time.Second)
for answers == nil || !ok(answers) {
@@ -1154,7 +1155,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
}
}
waitFor("every group answered", func(a map[string]delivery.CandidateAnswer) bool { return len(a) == 6 })
waitFor("every group answered", func(a map[string]nodeengine.CandidateAnswer) bool { return len(a) == 6 })
// Every watch and get the shadow makes is one the node's grant allows (issue 1126, 2026-10-27 09:21).
sb.mu.Lock()
for _, w := range sb.watches {
@@ -1170,28 +1171,28 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
}
readOnly.mu.Unlock()
for group, want := range map[string]func(delivery.CandidateAnswer) bool{
"same": func(a delivery.CandidateAnswer) bool {
return a.Verdict == delivery.CandidateSame && a.Proposal == "same" && a.Revision == 3 && len(a.Same) == 2
for group, want := range map[string]func(nodeengine.CandidateAnswer) bool{
"same": func(a nodeengine.CandidateAnswer) bool {
return a.Verdict == nodeengine.CandidateAnswerVerdictSame && a.Proposal == "same" && a.Revision == 3 && len(a.Same) == 2
},
"adds": func(a delivery.CandidateAnswer) bool {
return a.Verdict == delivery.CandidateDiffers && len(a.Added) == 1 && a.Added[0].Module == "keeper" &&
"adds": func(a nodeengine.CandidateAnswer) bool {
return a.Verdict == nodeengine.CandidateAnswerVerdictDiffers && len(a.Added) == 1 && a.Added[0].Module == "keeper" &&
len(a.Added[0].Unissued) == 1 && len(a.Refusals) == 0
},
"drops": func(a delivery.CandidateAnswer) bool {
return a.Verdict == delivery.CandidateDiffers && len(a.Removed) == 1 && a.Removed[0] == "lemurs"
"drops": func(a nodeengine.CandidateAnswer) bool {
return a.Verdict == nodeengine.CandidateAnswerVerdictDiffers && len(a.Removed) == 1 && a.Removed[0] == "lemurs"
},
"ghost": func(a delivery.CandidateAnswer) bool {
return a.Verdict == delivery.CandidateRefused && a.Refused != ""
"ghost": func(a nodeengine.CandidateAnswer) bool {
return a.Verdict == nodeengine.CandidateAnswerVerdictRefused && a.Refused != ""
},
"reach": func(a delivery.CandidateAnswer) bool {
return a.Verdict == delivery.CandidateSame && a.Returns == "" && a.Revision == 8 && len(a.Removed) == 0 &&
"reach": func(a nodeengine.CandidateAnswer) bool {
return a.Verdict == nodeengine.CandidateAnswerVerdictSame && a.Returns == "" && a.Revision == 8 && len(a.Removed) == 0 &&
fmt.Sprint(a.Same) == "[lemurs node]"
},
// Issue 1126, A7: a candidate deleting the node's placement is refused, never answered as a return to the
// controller's compositions: no composition made elsewhere is applied, so the node would compose nothing.
"deletes": func(a delivery.CandidateAnswer) bool {
return a.Verdict == delivery.CandidateRefused && a.Returns == "" && a.Revision == 7 &&
"deletes": func(a nodeengine.CandidateAnswer) bool {
return a.Verdict == nodeengine.CandidateAnswerVerdictRefused && a.Returns == "" && a.Revision == 7 &&
strings.Contains(a.Refused, "composes nothing")
},
} {
@@ -1252,7 +1253,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
t.Errorf("answered %d more time(s) with no candidate revision and no input of the node changed", got-settled)
}
candidate("same", 8, "lemurs")
waitFor("the new revision answered", func(a map[string]delivery.CandidateAnswer) bool { return a["same"].Revision == 8 })
waitFor("the new revision answered", func(a map[string]nodeengine.CandidateAnswer) bool { return a["same"].Revision == 8 })
drain()
if got := answering(); got != settled+1 {
t.Errorf("a new revision of one group answered %d group(s), want 1", got-settled)
@@ -1286,12 +1287,12 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
setFor(t, &laterAgain, []time.Duration{20 * time.Millisecond})
ownFails.Store(true)
candidate("adds", 9, "lemurs", "keeper")
waitFor("the answer met the vault not answering", func(a map[string]delivery.CandidateAnswer) bool {
waitFor("the answer met the vault not answering", func(a map[string]nodeengine.CandidateAnswer) bool {
return a["adds"].Revision == 9 && strings.Contains(a["adds"].Refused, "no responders")
})
ownFails.Store(false)
waitFor("answered again once the bus answers, with nothing else moved", func(a map[string]delivery.CandidateAnswer) bool {
return a["adds"].Revision == 9 && a["adds"].Verdict == delivery.CandidateDiffers
waitFor("answered again once the bus answers, with nothing else moved", func(a map[string]nodeengine.CandidateAnswer) bool {
return a["adds"].Revision == 9 && a["adds"].Verdict == nodeengine.CandidateAnswerVerdictDiffers
})
drain()
if got := lastSaid("answering placement candidate adds "); got != "answering placement candidate adds (revision 9; woken by a retry after a read the bus did not answer)" {
@@ -1299,7 +1300,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
k, _ := delivery.PlacementCandidateKey("same", "n1")
sb.del(delivery.PlacementCandidatesBucket, k)
waitFor("the deleted group's answer stays", func(a map[string]delivery.CandidateAnswer) bool { _, ok := a["same"]; return !ok && len(a) == 5 })
waitFor("the deleted group's answer stays", func(a map[string]nodeengine.CandidateAnswer) bool { _, ok := a["same"]; return !ok && len(a) == 5 })
// Review of L2b, round 2: a watch ends while the link stays; a group's key is deleted before the shadow watches
// again. The new run reads afresh, so that group is no longer answered.
@@ -1309,7 +1310,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
sb.del(delivery.PlacementCandidatesBucket, dropsKey)
time.Sleep(30 * time.Millisecond)
sb.missing(delivery.PlacementCandidatesBucket, false)
waitFor("a group deleted while the shadow was down is still answered", func(a map[string]delivery.CandidateAnswer) bool {
waitFor("a group deleted while the shadow was down is still answered", func(a map[string]nodeengine.CandidateAnswer) bool {
_, ok := a["drops"]
return !ok && len(a) == 4
})
@@ -1342,8 +1343,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
var nsaid []string
var nmu sync.Mutex
nenv.Say = func(l string) { nmu.Lock(); nsaid = append(nsaid, l); nmu.Unlock() }
nanswered := make(chan map[string]delivery.CandidateAnswer, 8)
nsh := NewShadow(nenv, func(_ context.Context, a map[string]delivery.CandidateAnswer) { nanswered <- a })
nanswered := make(chan map[string]nodeengine.CandidateAnswer, 8)
nsh := NewShadow(nenv, func(_ context.Context, a map[string]nodeengine.CandidateAnswer) { nanswered <- a })
nctx, ncancel := context.WithCancel(ctx)
defer ncancel()
go func() { _ = nsh.Run(nctx) }()
@@ -1366,7 +1367,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
}
select {
case a := <-nanswered:
if g := a["same"]; g.Verdict != delivery.CandidateRefused || !strings.Contains(g.Refused, ResolvedBucket) || g.Proposal != "same" {
if g := a["same"]; g.Verdict != nodeengine.CandidateAnswerVerdictRefused || !strings.Contains(g.Refused, ResolvedBucket) || g.Proposal != "same" {
t.Errorf("a shadow waiting for its values answered %+v", g)
}
case <-time.After(5 * time.Second):
@@ -1384,14 +1385,14 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
nb.send(ResolvedBucket, agent.Update{Caught: true})
select {
case a := <-nanswered:
if g := a["same"]; g.Verdict != delivery.CandidateSame || !strings.Contains(g.Note, "this answer took over") {
if g := a["same"]; g.Verdict != nodeengine.CandidateAnswerVerdictSame || !strings.Contains(g.Note, "this answer took over") {
t.Errorf("the shadow, once ready, answered %+v", g)
}
// The newest first, handed at once: the older group still holds the answer it had.
if g := a["older"]; g.Verdict != delivery.CandidateRefused {
if g := a["older"]; g.Verdict != nodeengine.CandidateAnswerVerdictRefused {
t.Errorf("the older group was answered before the newest was handed: %+v", g)
}
if g := (<-nanswered)["older"]; g.Verdict != delivery.CandidateSame {
if g := (<-nanswered)["older"]; g.Verdict != nodeengine.CandidateAnswerVerdictSame {
t.Errorf("the older group, answered next: %+v", g)
}
// Issue 1126, T2: an answer says when it starts and when it ends (and, every minute between, what it waits on).
@@ -1419,8 +1420,8 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
// Issue 1126, probe #532 on shanks: what runs is the composition the have names, never the copy of the declaration
// kept before the switch (no fp, no digest): a module composed at the digest its have says runs is the same.
{
probe := delivery.CandidateAnswer{Proposal: "pro", Revision: 68, Verdict: delivery.CandidateDiffers, Same: []string{"adwaita"},
Differs: []delivery.ModuleDiffers{
probe := nodeengine.CandidateAnswer{Proposal: "pro", Revision: 68, Verdict: nodeengine.CandidateAnswerVerdictDiffers, Same: []string{"adwaita"},
Differs: []nodeengine.CandidateDiffers{
{Module: "xterm", Composed: "sha256:3c0a31df64283c0678b1cdbeff8ac030609cab65cc7e47faaf714da743d9340e", Why: "the fingerprint differs"},
{Module: "zsh", Composed: "sha256:6093f31056d39eaf604410b7289ec6b5fe602b19c8547283562fcfc078c4844f", Resource: "zsh.bundle-tools", Why: "the resource differs"},
{Module: "brother", Composed: "sha256:0702b13991288da72d230460624e949d3c1d7d833eac2ec2134073c82e5a61cf",
@@ -1429,11 +1430,11 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
"xterm": "sha256:3c0a31df64283c0678b1cdbeff8ac030609cab65cc7e47faaf714da743d9340e",
"zsh": "sha256:6093f31056d39eaf604410b7289ec6b5fe602b19c8547283562fcfc078c4844f",
"brother": "sha256:c11c14af2738e3fcdb04da4004bbe84728bdefaf8c986d76601ffbe502435871"})
if fmt.Sprint(got.Same) != "[adwaita xterm zsh]" || len(got.Differs) != 1 || got.Differs[0].Module != "brother" || got.Verdict != delivery.CandidateDiffers {
if fmt.Sprint(got.Same) != "[adwaita xterm zsh]" || len(got.Differs) != 1 || got.Differs[0].Module != "brother" || got.Verdict != nodeengine.CandidateAnswerVerdictDiffers {
t.Errorf("shanks' probe answered %+v", got)
}
if one := asHavesSay(delivery.CandidateAnswer{Proposal: "pro", Revision: 1, Verdict: delivery.CandidateDiffers, Differs: probe.Differs[:1]},
map[string]string{"xterm": probe.Differs[0].Composed}); one.Verdict != delivery.CandidateSame {
if one := asHavesSay(nodeengine.CandidateAnswer{Proposal: "pro", Revision: 1, Verdict: nodeengine.CandidateAnswerVerdictDiffers, Differs: probe.Differs[:1]},
map[string]string{"xterm": probe.Differs[0].Composed}); one.Verdict != nodeengine.CandidateAnswerVerdictSame {
t.Errorf("a module the same as its have says is answered %+v", one)
}
}
@@ -1441,7 +1442,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
// Issue 1126, the seed's review: a module n1 runs inside its own composition (nats, at a build of the old form) is
// composed there again, not at the trunk's build as one added; lemurs at the build it runs, keeper (added) at the
// trunk's.
inOwnRunning := []delivery.Composed{{Node: "n1", Module: "lemurs", FP: fp}}
inOwnRunning := []compositions.Composed{{Node: "n1", Module: "lemurs", FP: fp}}
other := "src2:" + strings.Repeat("9", 64)
gotFps := shadowBuilds(inOwnRunning, delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "lemurs"}, {Module: "nats"}, {Module: "keeper"}}},
map[string]delivery.Module{"lemurs": {FP2: other}, "nats": {FP2: other}, "keeper": {FP2: other}}, []string{"nats"})
+23 -20
View File
@@ -3,7 +3,10 @@ package selfcompose
import (
"bytes"
"context"
"encoding/json"
"fmt"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
engcomp "github.com/novox/mesh-host/internal/compositions"
"sort"
"strings"
"sync"
@@ -29,11 +32,11 @@ import (
// keys go.
type Shadow struct {
c *Composer
answer func(ctx context.Context, answers map[string]delivery.CandidateAnswer)
answer func(ctx context.Context, answers map[string]nodeengine.CandidateAnswer)
mu sync.Mutex
candidates map[string]map[string]candidateKey // by proposal, by node
answers map[string]delivery.CandidateAnswer
answers map[string]nodeengine.CandidateAnswer
// ran is, by module, the digest the module's have says runs: what runs, where the agent's list holds only the copy
// from the declaration kept before the switch (issue 1126, probe #532 on shanks).
ran map[string]string
@@ -99,9 +102,9 @@ type candidateKey struct {
}
// NewShadow is the shadow composer of one node; answer writes the node's answers into its own have key.
func NewShadow(env Env, answer func(ctx context.Context, answers map[string]delivery.CandidateAnswer)) *Shadow {
func NewShadow(env Env, answer func(ctx context.Context, answers map[string]nodeengine.CandidateAnswer)) *Shadow {
return &Shadow{c: New(env), answer: answer, candidates: map[string]map[string]candidateKey{},
answers: map[string]delivery.CandidateAnswer{}, ran: map[string]string{}}
answers: map[string]nodeengine.CandidateAnswer{}, ran: map[string]string{}}
}
// Keep runs the shadow until ctx ends, beginning again after backoff(try) whenever it stops: an input it cannot watch
@@ -341,13 +344,13 @@ func (s *Shadow) notReady(ctx context.Context, ins []input, caught []bool) {
why := fmt.Sprintf("the node has not read %s after %s; it answers once it has", strings.Join(waits, ", "), notReadyAfter)
s.c.env.Say("this node answers no placement candidate yet: " + why)
s.mu.Lock()
next := map[string]delivery.CandidateAnswer{}
next := map[string]nodeengine.CandidateAnswer{}
for proposal, keys := range s.candidates {
own, ok := keys[s.c.env.Node]
if !ok {
continue // no key of this node's: nothing asks it to answer
}
next[proposal] = delivery.CandidateAnswer{Proposal: proposal, Revision: own.rev, Refused: why, Verdict: delivery.CandidateRefused}
next[proposal] = nodeengine.CandidateAnswer{Proposal: proposal, Revision: int64(own.rev), Refused: why, Verdict: nodeengine.CandidateAnswerVerdictRefused}
}
s.answers = next
s.mu.Unlock()
@@ -386,7 +389,7 @@ func (s *Shadow) answerAll(ctx context.Context) {
}
}
// Each proposal's last answer stands until it is answered again; a proposal whose keys went is dropped.
next := map[string]delivery.CandidateAnswer{}
next := map[string]nodeengine.CandidateAnswer{}
for _, l := range proposals {
if a, ok := s.answers[l.proposal]; ok {
next[l.proposal] = a
@@ -408,7 +411,7 @@ func (s *Shadow) answerAll(ctx context.Context) {
same = false
}
}
out := make(map[string]delivery.CandidateAnswer, len(next))
out := make(map[string]nodeengine.CandidateAnswer, len(next))
for g, a := range next {
out[g] = a
}
@@ -417,7 +420,7 @@ func (s *Shadow) answerAll(ctx context.Context) {
}
s.mu.Unlock()
if !same && s.answer != nil {
given := make(map[string]delivery.CandidateAnswer, len(out))
given := make(map[string]nodeengine.CandidateAnswer, len(out))
for g, a := range out {
given[g] = a
}
@@ -466,7 +469,7 @@ func (s *Shadow) answerAll(ctx context.Context) {
}
}
if ok {
a = judge.FitAnswer(a, delivery.MaxAnswerBytes)
a = judge.FitAnswer(a, engcomp.MaxAnswerBytes)
next[proposal] = a
c.env.Say(fmt.Sprintf("answered placement candidate %s in %s: %s", proposal, took.Round(time.Millisecond), a.Verdict))
} else {
@@ -495,7 +498,7 @@ var progressEvery = time.Minute
// answerProposal is the node's answer to one proposal, and false when the node has nothing to answer: neither the trunk nor
// the proposal places it.
func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery.CandidateAnswer, bool) {
func (s *Shadow) answerProposal(ctx context.Context, proposal string) (nodeengine.CandidateAnswer, bool) {
c := s.c
node := c.env.Node
defer c.waiting("composing")()
@@ -520,10 +523,10 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery.
}
c.mu.Unlock()
if !mine {
return delivery.CandidateAnswer{}, false
return nodeengine.CandidateAnswer{}, false
}
var running []delivery.Composed
var running []engcomp.Composed
if c.env.Running != nil {
running = c.env.Running()
}
@@ -531,7 +534,7 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery.
// The proposal deletes this node's placement key: the node would compose nothing and keep running what it runs,
// since no composition made elsewhere is applied (issue 1126: the controller ended by its unassignment). Refused,
// so the catalogue does not take a node's placement away by a merge.
return delivery.CandidateAnswer{Proposal: proposal, Revision: rev, Verdict: delivery.CandidateRefused,
return nodeengine.CandidateAnswer{Proposal: proposal, Revision: int64(rev), Verdict: nodeengine.CandidateAnswerVerdictRefused,
Refused: "the proposal takes this node's placement away: it then composes nothing, and no composition made " +
"elsewhere is applied, so what runs here would be followed by nothing"}, true
}
@@ -635,7 +638,7 @@ func (s *Shadow) takeRan(u agent.Update) bool {
// counted the same where the composed digest is the one its have says runs: what runs is the composition at that
// digest, never the kept copy (issue 1126, probe #532: shanks' xterm, zsh, zsh-autosuggestions and
// zsh-syntax-highlighting).
func asHavesSay(a delivery.CandidateAnswer, ran map[string]string) delivery.CandidateAnswer {
func asHavesSay(a nodeengine.CandidateAnswer, ran map[string]string) nodeengine.CandidateAnswer {
if a.Refused != "" || len(a.Differs) == 0 {
return a
}
@@ -649,7 +652,7 @@ func asHavesSay(a delivery.CandidateAnswer, ran map[string]string) delivery.Cand
}
a.Differs = kept
sort.Strings(a.Same)
a.Verdict = delivery.VerdictOf(a)
a.Verdict = engcomp.VerdictOf(a)
return a
}
@@ -658,7 +661,7 @@ func asHavesSay(a delivery.CandidateAnswer, ran map[string]string) delivery.Cand
// of the old form) is given none, so it is folded there again, as the node's own composer folds it, rather than
// composed at the trunk's build as if the candidate added it (issue 1126: nats and photos have a trunk build that no
// want names).
func shadowBuilds(running []delivery.Composed, placement delivery.Placement, modules map[string]delivery.Module, inOwn []string) map[string]string {
func shadowBuilds(running []engcomp.Composed, placement delivery.Placement, modules map[string]delivery.Module, inOwn []string) map[string]string {
fps := map[string]string{}
for _, r := range running {
if r.Module != "" && r.FP != "" {
@@ -679,9 +682,9 @@ func shadowBuilds(running []delivery.Composed, placement delivery.Placement, mod
return fps
}
func sameAnswer(a, b delivery.CandidateAnswer) bool {
x, errA := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}})
y, errB := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": b}})
func sameAnswer(a, b nodeengine.CandidateAnswer) bool {
x, errA := json.Marshal(a)
y, errB := json.Marshal(b)
return errA == nil && errB == nil && string(x) == string(y)
}
+4 -3
View File
@@ -31,6 +31,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
engcomp "github.com/novox/mesh-host/internal/compositions"
"regexp"
"sort"
"strings"
@@ -55,7 +56,7 @@ type Env struct {
Node string
// Running is the compositions this node runs now, as its agent holds them (agent.Agent.Everything): where the user
// list's file is is read from these alone (issue 1126). Nil runs nothing.
Running func() []delivery.Composed
Running func() []engcomp.Composed
// OnDisk is what the resource's file holds on the machine now; nil when there is none.
OnDisk func(resource json.RawMessage) ([]byte, error)
// Write writes the user list's file resource of a module's composition alone, with body as its content, when it
@@ -233,7 +234,7 @@ func (f *Follower) busNode() bool {
// look reads what this node runs now for the bus's user list: each module whose composition carries it.
func (f *Follower) look() {
var running []delivery.Composed
var running []engcomp.Composed
if f.env.Running != nil {
running = f.env.Running()
}
@@ -410,7 +411,7 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
// A list on the machine that cannot be read is an error: left out, the apply would remove the file as an orphan and the
// bus would refuse every connection at its next reload, so the bus module's apply waits instead. A composition without
// a user list is c unchanged.
func (f *Follower) Hold(c delivery.Composed) (delivery.Composed, error) {
func (f *Follower) Hold(c engcomp.Composed) (engcomp.Composed, error) {
out := c
out.Resources = nil
for _, raw := range c.Resources {
+7 -6
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
engcomp "github.com/novox/mesh-host/internal/compositions"
"strings"
"sync"
"testing"
@@ -59,7 +60,7 @@ func list(users ...string) string {
// comp is a composition of module at fp as this node runs it, holding the user list content (none when empty); by
// digest when byDigest, as a file over 64 KiB is given; for another node than this one when asked.
func comp(module, fp, content string, p put) delivery.Composed {
func comp(module, fp, content string, p put) engcomp.Composed {
server, _ := json.Marshal(map[string]any{"id": module + ".server-conf", "type": "file", "module": module,
"path": "/var/lib/nats-module/conf/nats.conf", "content": "include accounts.conf\n"})
rs := []json.RawMessage{server}
@@ -73,7 +74,7 @@ func comp(module, fp, content string, p put) delivery.Composed {
users, _ := json.Marshal(r)
rs = append(rs, users)
}
c := delivery.Composed{Node: node, Module: module, FP: fp, Resources: rs}
c := engcomp.Composed{Node: node, Module: module, FP: fp, Resources: rs}
if p.asNode != "" {
c.Node = p.asNode
}
@@ -171,7 +172,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
} {
t.Run(c.name, func(t *testing.T) {
var mu sync.Mutex
var running []delivery.Composed
var running []engcomp.Composed
var writes []string
var reports []Written
reported := make(chan struct{}, 64)
@@ -226,10 +227,10 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
}
}
fl := New(Env{Node: node, Grants: gf,
Running: func() []delivery.Composed {
Running: func() []engcomp.Composed {
mu.Lock()
defer mu.Unlock()
return append([]delivery.Composed(nil), running...)
return append([]engcomp.Composed(nil), running...)
},
Kept: func() ([]byte, uint64) { return keptRaw, 9 },
Keep: func([]byte, uint64) {},
@@ -288,7 +289,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
}
for _, p := range c.after {
mu.Lock()
running = []delivery.Composed{comp(p.module, p.fp, p.content, p)}
running = []engcomp.Composed{comp(p.module, p.fp, p.content, p)}
mu.Unlock()
fl.RunsChanged()
}