Merge branch 'b6/registry' into feat/registry-milestone
This commit is contained in:
@@ -280,7 +280,7 @@ func ownPackages(t *testing.T) map[string][]string {
|
||||
}
|
||||
|
||||
// 1621/AC2 and AC4: the command imports, through every package of this repository it reaches, only the standard
|
||||
// library, mesh-sdk, the private network's overlay package (mesh-wireguard's own pure package: names, graph and
|
||||
// library, mesh-sdk, the contracts modules' pure packages (seats' types, clients' codecs), the private network's overlay package (mesh-wireguard's own pure package: names, graph and
|
||||
// generator, no bus) and this repository's own packages, and none that reaches a bus, a store, a network service or
|
||||
// another process.
|
||||
func TestTheCommandImportsNoBusStoreNetworkOrProcess(t *testing.T) {
|
||||
@@ -292,7 +292,7 @@ func TestTheCommandImportsNoBusStoreNetworkOrProcess(t *testing.T) {
|
||||
for path, imports := range own {
|
||||
for _, imp := range imports {
|
||||
std := !strings.Contains(strings.SplitN(imp, "/", 2)[0], ".")
|
||||
if !std && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-sdk/go") && !strings.HasPrefix(imp, "github.com/novox/mesh-host/") {
|
||||
if !std && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-sdk/go") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/contracts/") && !strings.HasPrefix(imp, "github.com/novox/mesh-host/") {
|
||||
t.Errorf("%s imports %s, which is neither the standard library, mesh-sdk nor this repository", path, imp)
|
||||
}
|
||||
for _, f := range forbidden {
|
||||
|
||||
@@ -2,12 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// authorityPath is where this engine keeps the mesh's own certificate as it last applied it (public): beside the
|
||||
@@ -17,7 +17,7 @@ func authorityPath(state string) string { return filepath.Join(filepath.Dir(stat
|
||||
// keepAuthority keeps the mesh's own certificate a composition carries (the resource a module's certificate.authority
|
||||
// asks for, catalogue.AuthorityID), so the record says the authority this machine holds. A composition carrying none
|
||||
// changes nothing.
|
||||
func keepAuthority(state string, c delivery.Composed) error {
|
||||
func keepAuthority(state string, c engcomp.Composed) error {
|
||||
for _, raw := range c.Resources {
|
||||
var r struct {
|
||||
ID string `json:"id"`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -29,13 +30,13 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
|
||||
raw, _ := json.Marshal(map[string]any{"id": id, "type": "file", "path": "/etc/x", "content": content})
|
||||
return raw
|
||||
}
|
||||
if err := keepAuthority(state, delivery.Composed{Resources: []json.RawMessage{res("web.conf", "x")}}); err != nil {
|
||||
if err := keepAuthority(state, engcomp.Composed{Resources: []json.RawMessage{res("web.conf", "x")}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(authorityPath(state)); !os.IsNotExist(err) {
|
||||
t.Fatalf("a composition with no authority kept one: %v", err)
|
||||
}
|
||||
if err := keepAuthority(state, delivery.Composed{Resources: []json.RawMessage{res("ca-trust.certificate-authority", "CA-1")}}); err != nil {
|
||||
if err := keepAuthority(state, engcomp.Composed{Resources: []json.RawMessage{res("ca-trust.certificate-authority", "CA-1")}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := recordKeys(state, nil)(); got.Authority != "CA-1" {
|
||||
|
||||
@@ -2,10 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// A composition that names a file the provisioner fills without saying it is filled would have this node-engine write
|
||||
@@ -13,7 +12,7 @@ import (
|
||||
// when ace, g14 and shanks wrote claude-code's, node-env's and power's files empty).
|
||||
func TestAFileItCannotProduceIsNeverWrittenEmpty(t *testing.T) {
|
||||
unit := json.RawMessage(`{"id":"power.after-boot-unit","type":"file","path":"/etc/systemd/system/mesh-power-after-boot.service","mode":"0644","owner-at-runtime":"owned"}`)
|
||||
c := delivery.Composed{Node: "ace", Resources: []json.RawMessage{unit}}
|
||||
c := engcomp.Composed{Node: "ace", Resources: []json.RawMessage{unit}}
|
||||
d, err := declarationOf(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -25,7 +24,7 @@ func TestAFileItCannotProduceIsNeverWrittenEmpty(t *testing.T) {
|
||||
if d, _ = declarationOf(c); refuseEmptyWrites(d) != nil {
|
||||
t.Fatalf("a filled file was refused: %v", refuseEmptyWrites(d))
|
||||
}
|
||||
plain := delivery.Composed{Node: "ace", Resources: []json.RawMessage{json.RawMessage(
|
||||
plain := engcomp.Composed{Node: "ace", Resources: []json.RawMessage{json.RawMessage(
|
||||
`{"id":"x.f","type":"file","path":"/etc/x","mode":"0644","content":"x"}`)}}
|
||||
if d, _ = declarationOf(plain); refuseEmptyWrites(d) != nil {
|
||||
t.Fatalf("a file with its content was refused: %v", refuseEmptyWrites(d))
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
)
|
||||
@@ -140,7 +140,7 @@ func TestADeclarationWithAFileByDigestAppliesFromTheObject(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
runs := runsAsWritten([]delivery.Composed{{Node: "n", Module: "m", Resources: []json.RawMessage{stale}}}, opts.state)
|
||||
runs := runsAsWritten([]engcomp.Composed{{Node: "n", Module: "m", Resources: []json.RawMessage{stale}}}, opts.state)
|
||||
var got struct {
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/novox/mesh-host/internal/apply"
|
||||
"github.com/novox/mesh-host/internal/liveness"
|
||||
)
|
||||
@@ -26,8 +26,8 @@ func heldBy(windows *apply.Windows) func(time.Time) map[string]bool {
|
||||
// long-running resource the composition declares running not active, read from the service manager and the container
|
||||
// runtime and from no record. A resource held by an open maintenance window, or waiting for a library, is not counted;
|
||||
// one that cannot be read is not counted either (unknown is not inactive), and neither is one the manager is restarting.
|
||||
func inactiveIn(rt liveness.Runtime, held func(time.Time) map[string]bool) func(context.Context, delivery.Composed) bool {
|
||||
return func(ctx context.Context, c delivery.Composed) bool {
|
||||
func inactiveIn(rt liveness.Runtime, held func(time.Time) map[string]bool) func(context.Context, engcomp.Composed) bool {
|
||||
return func(ctx context.Context, c engcomp.Composed) bool {
|
||||
d, err := declarationOf(c)
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/novox/mesh-host/internal/liveness"
|
||||
)
|
||||
|
||||
@@ -77,7 +77,7 @@ func testTheMachineShowsAResourceNotActive(t *testing.T) {
|
||||
{"unread", fakeRuntime{err: errors.New("the service manager could not be read")}, nil, false},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
d, err := declarationOf(delivery.Composed{Node: "n1", Module: "dnsmasq", Resources: []json.RawMessage{svc, ctr}})
|
||||
d, err := declarationOf(engcomp.Composed{Node: "n1", Module: "dnsmasq", Resources: []json.RawMessage{svc, ctr}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+21
-20
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"github.com/novox/mesh-host/internal/compositions"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -256,16 +257,16 @@ func awaitBackup(ctx context.Context, events <-chan []byte, node, module string,
|
||||
// the controller put modules it knew no fingerprint of the new form for into the node's own composition while their
|
||||
// wants named their own, and the declaration joined of both declared each such file twice, which the filling refuses
|
||||
// ("2 resources declare X") on every node. The node's own copy is left out, and which were left out is said once.
|
||||
func declarationOf(cs ...delivery.Composed) (*declaration.Declaration, error) {
|
||||
func declarationOf(cs ...compositions.Composed) (*declaration.Declaration, error) {
|
||||
d := &declaration.Declaration{Version: declaration.Version}
|
||||
var modules []delivery.Composed
|
||||
var modules []compositions.Composed
|
||||
for _, c := range cs {
|
||||
if c.Module != "" {
|
||||
modules = append(modules, c)
|
||||
}
|
||||
}
|
||||
if len(modules) > 0 {
|
||||
joined := make([]delivery.Composed, 0, len(cs))
|
||||
joined := make([]compositions.Composed, 0, len(cs))
|
||||
for _, c := range cs {
|
||||
if c.Module == "" {
|
||||
var dropped []string
|
||||
@@ -300,7 +301,7 @@ func declarationOf(cs ...delivery.Composed) (*declaration.Declaration, error) {
|
||||
// the same kind and id. A resource that shares only its target (a unit or container of the same name, another id) is
|
||||
// not the module's, and stays: dropped, its record would be an orphan the apply removes, stopping the module's unit. It
|
||||
// answers what it left out, as "<id> (<module>)".
|
||||
func ownWithout(own delivery.Composed, modules []delivery.Composed) (delivery.Composed, []string) {
|
||||
func ownWithout(own compositions.Composed, modules []compositions.Composed) (compositions.Composed, []string) {
|
||||
byID := map[string]string{}
|
||||
for _, c := range modules {
|
||||
if c.Module == "" {
|
||||
@@ -356,14 +357,14 @@ func sayDroppedOnce(dropped []string) {
|
||||
"declares, and they are left out of it: %s (issue 1945)\n", len(dropped), key)
|
||||
}
|
||||
|
||||
func scopeName(c delivery.Composed) string {
|
||||
func scopeName(c compositions.Composed) string {
|
||||
if c.Module == "" {
|
||||
return agent.NodeModule
|
||||
}
|
||||
return c.Module
|
||||
}
|
||||
|
||||
func resourcesOf(cs []delivery.Composed) []declaration.Resource {
|
||||
func resourcesOf(cs []compositions.Composed) []declaration.Resource {
|
||||
var out []declaration.Resource
|
||||
for _, c := range cs {
|
||||
for _, raw := range c.Resources {
|
||||
@@ -445,7 +446,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
}
|
||||
began := time.Now()
|
||||
node := session.Membership.Node
|
||||
composedBucket, err := delivery.ComposedBucketFor(node)
|
||||
composedBucket, err := compositions.BucketFor(node)
|
||||
if err != nil {
|
||||
cancel()
|
||||
say("the node agent cannot name this node's composition bucket: " + err.Error())
|
||||
@@ -469,7 +470,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
// composes its own (ADR 0555 §3): without it a module that asks for a certificate would be stood in for.
|
||||
if byModule, _ := kept(); byModule != nil {
|
||||
for module, resources := range byModule {
|
||||
if err := keepAuthority(opts.state, delivery.Composed{Module: module, Resources: resources}); err != nil {
|
||||
if err := keepAuthority(opts.state, compositions.Composed{Module: module, Resources: resources}); err != nil {
|
||||
say("the mesh's certificate the kept declaration carries could not be kept for this machine's record: " + err.Error())
|
||||
}
|
||||
}
|
||||
@@ -478,7 +479,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
// bus module this node runs declares, as its agent holds what runs (its own composition, or the declaration kept
|
||||
// before the switch): never read from the node's composition bucket, which the controller wrote. On every node;
|
||||
// a node that runs no list writes nothing. Looked at again whenever what runs changed (RunsChanged, Settled).
|
||||
users := userListFollower(opts, session, func() []delivery.Composed {
|
||||
users := userListFollower(opts, session, func() []compositions.Composed {
|
||||
if now := current.get(); now != nil {
|
||||
return now.Everything()
|
||||
}
|
||||
@@ -545,7 +546,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
Say: say,
|
||||
// A failure of the bus not answering for a moment is tried again: 2 s, 5 s, 15 s, 60 s, then every 5 min.
|
||||
Backoff: link.Backoff,
|
||||
Apply: func(ctx context.Context, c delivery.Composed, others []delivery.Composed) agent.Result {
|
||||
Apply: func(ctx context.Context, c compositions.Composed, others []compositions.Composed) agent.Result {
|
||||
actx, done := applyContext(session.WithLink(ctx))
|
||||
// While the grants part's list is followed, the controller's list in the bus module's composition is not
|
||||
// written: the apply keeps the list on the machine (ADR 0547 clause 8, never both).
|
||||
@@ -571,7 +572,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
sayFacts(ctx, ag)
|
||||
return res
|
||||
},
|
||||
Remove: func(ctx context.Context, module string, others []delivery.Composed) agent.Result {
|
||||
Remove: func(ctx context.Context, module string, others []compositions.Composed) agent.Result {
|
||||
actx, done := applyContext(session.WithLink(ctx))
|
||||
res := removeComposition(actx, opts, module, others, ag.Placed(module), sched, say)
|
||||
done()
|
||||
@@ -595,7 +596,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
}
|
||||
hooks := composerHooks{
|
||||
seatList: seats,
|
||||
running: func() []delivery.Composed {
|
||||
running: func() []compositions.Composed {
|
||||
if now := own.get(); now != nil {
|
||||
return runsAsWritten(now.Everything(), opts.state)
|
||||
}
|
||||
@@ -639,8 +640,8 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
shadowHooks.refused, shadowHooks.kept, shadowHooks.certify, shadowHooks.seatList = nil, nil, nil, nil
|
||||
shadowEnv := composerEnv(session, node, say, shadowHooks)
|
||||
shadow := selfcompose.NewShadow(shadowEnv,
|
||||
func(ctx context.Context, answers map[string]delivery.CandidateAnswer) {
|
||||
session.NoteCandidates(ctx, delivery.WithValuesDiffer(answers))
|
||||
func(ctx context.Context, answers map[string]nodeengine.CandidateAnswer) {
|
||||
session.NoteCandidates(ctx, compositions.WithValuesDiffer(answers))
|
||||
})
|
||||
go shadow.Keep(run, link.Backoff) // this run's alone: a switch begins a run, and its shadow, again
|
||||
shadowNow.Store(shadow) // what the verb compose-candidate composes a check's candidate with
|
||||
@@ -712,7 +713,7 @@ func (c *agentNow) get() *agent.Agent {
|
||||
//
|
||||
// removed is the modules whose want says removed: in the node's own apply, a resource of any other module goes only when
|
||||
// its module is among them (issue 774).
|
||||
func applyComposition(ctx context.Context, opts options, c delivery.Composed, others []delivery.Composed, placed, removed map[string]bool,
|
||||
func applyComposition(ctx context.Context, opts options, c compositions.Composed, others []compositions.Composed, placed, removed map[string]bool,
|
||||
sched *apply.Scheduler, say link.Announce) agent.Result {
|
||||
name := scopeName(c)
|
||||
if c.Module == "" {
|
||||
@@ -760,7 +761,7 @@ func applyComposition(ctx context.Context, opts options, c delivery.Composed, ot
|
||||
}
|
||||
|
||||
// removeComposition removes one module's own resources and nothing of another's.
|
||||
func removeComposition(ctx context.Context, opts options, module string, others []delivery.Composed, placed map[string]bool,
|
||||
func removeComposition(ctx context.Context, opts options, module string, others []compositions.Composed, placed map[string]bool,
|
||||
sched *apply.Scheduler, say link.Announce) agent.Result {
|
||||
scope := apply.Scope{Module: module, Others: resourcesOf(others), Placed: placed}
|
||||
got := applyModuleHeld(ctx, opts, &declaration.Declaration{Version: declaration.Version}, &scope, sched, say)
|
||||
@@ -833,7 +834,7 @@ func keptDeclaration(statePath string, signer ed25519.PublicKey) (*declaration.D
|
||||
// (adoption, identities, groups, libraries) from the node's composition once the agent holds it and from the kept
|
||||
// declaration until then. What the node judges its health by, and what the filling, the take act and the judgement of
|
||||
// directories read.
|
||||
func whatRuns(everything []delivery.Composed, holdsNode bool, kept func() (*declaration.Declaration, error)) (*declaration.Declaration, error) {
|
||||
func whatRuns(everything []compositions.Composed, holdsNode bool, kept func() (*declaration.Declaration, error)) (*declaration.Declaration, error) {
|
||||
d, err := declarationOf(everything...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -919,8 +920,8 @@ func refuseEmptyWrites(d *declaration.Declaration) error {
|
||||
// recordedIn says the engine's applied state holds a record of every resource of a composition: what the agent believes
|
||||
// of a have or the kept declaration only when it does, so a resource removed since is made again (issue 1945). A state
|
||||
// that cannot be read says yes: the agent then believes what it was told, as before.
|
||||
func recordedIn(statePath string, say func(string)) func(ctx context.Context, c delivery.Composed) bool {
|
||||
return func(_ context.Context, c delivery.Composed) bool {
|
||||
func recordedIn(statePath string, say func(string)) func(ctx context.Context, c compositions.Composed) bool {
|
||||
return func(_ context.Context, c compositions.Composed) bool {
|
||||
known, err := store.Load(statePath)
|
||||
if err != nil {
|
||||
if say != nil {
|
||||
@@ -982,7 +983,7 @@ func applyContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
|
||||
// composerHooks are what the composer reads of the agent and the machine (selfcompose.Env Running, Unseal, Refused).
|
||||
type composerHooks struct {
|
||||
running func() []delivery.Composed
|
||||
running func() []compositions.Composed
|
||||
unseal func(string) ([]byte, error)
|
||||
refused func(why string, held map[string]string)
|
||||
kept func(kept map[string][]string)
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -60,7 +62,9 @@ type itBed struct {
|
||||
mu sync.Mutex
|
||||
// seatSilent makes the stand-in of the seat mesh-catalog refuse the seat list (it is served from the start otherwise).
|
||||
seatSilent atomic.Bool
|
||||
findings nats.KeyValue
|
||||
// seatAsked counts the node's asks of the stand-in seat, answered or refused: the node reached the bus and asked.
|
||||
seatAsked atomic.Int32
|
||||
findings nats.KeyValue
|
||||
}
|
||||
|
||||
// aServer is an embedded bus with JetStream and two users: the test's, unrestricted, and the node's, granted exactly what
|
||||
@@ -126,12 +130,12 @@ func newItBedAs(t *testing.T, perms nodegrant.Permissions) *itBed {
|
||||
if b.nodeJS, err = b.nodeConn.JetStream(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
composedBucket, _ := delivery.ComposedBucketFor(itNode)
|
||||
composedBucket, _ := engcomp.BucketFor(itNode)
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
into *nats.KeyValue
|
||||
history uint8
|
||||
}{{delivery.AssignerWantsBucket, &b.wants, 8}, {composedBucket, &b.composed, delivery.ComposedHistory},
|
||||
}{{delivery.AssignerWantsBucket, &b.wants, 8}, {composedBucket, &b.composed, engcomp.History},
|
||||
{delivery.HaveBucket, &b.haves, delivery.HaveHistory}} {
|
||||
kv, err := b.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: c.name, History: c.history})
|
||||
if err != nil {
|
||||
@@ -253,7 +257,7 @@ func TestNatsANodeThePlacementNamesComposesAndAppliesItsOwn(t *testing.T) {
|
||||
within(t, 5*time.Second, "the node did not compose and apply its own module once placed", func() bool { return b.file("web") == "greeting=bonjour\n" })
|
||||
// ADR 0565 §6 (1953): the node's engine writes its own key of the state packages, under the grant the grants part
|
||||
// composes for it, with each package its compositions declare and the module declaring it; once, while nothing changes.
|
||||
wantPkgs := `{"node":"` + itNode + `","packages":[{"package":"web-tools","modules":["web"],"absent":false}]}`
|
||||
wantPkgs := `{"node":"` + itNode + `","packages":[{"absent":false,"modules":["web"],"package":"web-tools"}]}`
|
||||
within(t, 5*time.Second, "the node did not write its key of the state packages", func() bool {
|
||||
e, err := st.packages.Get(itNode)
|
||||
return err == nil && string(e.Value()) == wantPkgs
|
||||
@@ -351,7 +355,7 @@ func h8back(t *testing.T, b *itBed, placement, resolved, candidates nats.KeyValu
|
||||
}
|
||||
within(t, 10*time.Second, "the node did not refuse a candidate taking its placement away", func() bool {
|
||||
a := node().Candidates["revert-x"]
|
||||
return a.Proposal == "revert-x" && a.Returns == "" && a.Verdict == delivery.CandidateRefused &&
|
||||
return a.Proposal == "revert-x" && a.Returns == "" && a.Verdict == nodeengine.CandidateAnswerVerdictRefused &&
|
||||
strings.Contains(a.Refused, "composes nothing")
|
||||
})
|
||||
ran := b.have("web").Composed
|
||||
@@ -504,17 +508,17 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str
|
||||
// ADR 0593 §6: the node publishes what it composed in its composition state, under its own grant: web at the build
|
||||
// its want names, with the digest its have names (the plan left out of it), and the node's own.
|
||||
first := b.have("web").Composed
|
||||
webKey, _ := delivery.ComposedKey(itNode, "web", fp)
|
||||
webKey, _ := engcomp.Key(itNode, "web", fp)
|
||||
within(t, 5*time.Second, "the node did not publish web's composition in its composition state", func() bool {
|
||||
e, err := b.composed.Get(webKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
c, err := delivery.DecodeComposed(e.Value())
|
||||
c, err := engcomp.Decode(e.Value())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
d, err := delivery.ComposedDigestOf(c)
|
||||
d, err := engcomp.DigestOf(c)
|
||||
return err == nil && d == first
|
||||
})
|
||||
if _, err := resolved.Put(itNode+".web.greeting", []byte(`{"value":"salut","source":"module"}`)); err != nil {
|
||||
@@ -541,17 +545,18 @@ func h8rest(t *testing.T, b *itBed, resolved, candidates nats.KeyValue, hkey str
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var answer delivery.CandidateAnswer
|
||||
var answer nodeengine.CandidateAnswer
|
||||
within(t, 10*time.Second, "the node did not answer its candidate", func() bool {
|
||||
e, err := b.haves.Get(hkey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
n, err := delivery.DecodeHaveNode(e.Value())
|
||||
answer = n.Candidates["feat-x"]
|
||||
wire, _ := json.Marshal(n.Candidates["feat-x"])
|
||||
_ = json.Unmarshal(wire, &answer)
|
||||
return err == nil && answer.Proposal == "feat-x" && n.Record != nil
|
||||
})
|
||||
if answer.Verdict != delivery.CandidateSame || answer.Revision != crev || len(answer.Same) != 1 || answer.Same[0] != "web" {
|
||||
if answer.Verdict != nodeengine.CandidateAnswerVerdictSame || answer.Revision != int64(crev) || len(answer.Same) != 1 || answer.Same[0] != "web" {
|
||||
t.Errorf("the answer to a candidate equal to the placement: %+v", answer)
|
||||
}
|
||||
if h := b.have("web"); h.Composed != second || b.file("web") != "greeting=salut\n" {
|
||||
@@ -629,7 +634,7 @@ func h8candidate(t *testing.T, b *itBed, fp, running string) {
|
||||
|
||||
func mustComposedKey(t *testing.T, module, fp string) string {
|
||||
t.Helper()
|
||||
key, err := delivery.ComposedKey(itNode, module, fp)
|
||||
key, err := engcomp.Key(itNode, module, fp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -904,6 +909,7 @@ func (b *itBed) standInSeat() {
|
||||
Seat string `json:"seat"`
|
||||
}
|
||||
_ = json.Unmarshal(m.Data, &q)
|
||||
b.seatAsked.Add(1)
|
||||
var result any
|
||||
switch {
|
||||
case b.seatSilent.Load():
|
||||
@@ -979,6 +985,17 @@ func TestNatsANodeThatCannotReadTheSeatListActsOnNothingNew(t *testing.T) {
|
||||
defer clock.mu.Unlock()
|
||||
return len(clock.fire) > 0
|
||||
})
|
||||
// A fresh node, nothing applied, reaches the bus and asks the seat while its own composition is held: the read needs
|
||||
// the bus the node composes from and the grant it has from its start, nothing of what the hold keeps back.
|
||||
if b.seatAsked.Load() == 0 {
|
||||
t.Fatal("the fresh node did not ask the seat for its list while its own composition was held")
|
||||
}
|
||||
within(t, 5*time.Second, "the fresh node did not say it holds web and its own composition for the unread list", func() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
said := strings.Join(b.said, "\n")
|
||||
return strings.Contains(said, selfcompose.WaitsForTheSeatListOwn) && strings.Contains(said, selfcompose.WaitsForTheSeatList)
|
||||
})
|
||||
if b.file("web") != "" {
|
||||
t.Fatalf("the node applied web before it read the seat list: %q", b.file("web"))
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"github.com/novox/mesh-host/internal/liveness"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
@@ -21,7 +20,7 @@ func TestACompositionIsRecordedOnlyWhenEveryResourceIs(t *testing.T) {
|
||||
if err := store.Save(path, s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := delivery.Composed{Node: "ace", Module: "ca-trust", Resources: []json.RawMessage{
|
||||
c := engcomp.Composed{Node: "ace", Module: "ca-trust", Resources: []json.RawMessage{
|
||||
json.RawMessage(`{"id":"ca-trust.trust","type":"file","module":"ca-trust","path":"/etc/ca","mode":"0644","content":"x"}`)}}
|
||||
recorded := recordedIn(path, func(string) {})
|
||||
if !recorded(context.Background(), c) {
|
||||
@@ -87,7 +86,7 @@ func TestAFilledFileDoesNotMakeItsCompositionUnrecorded(t *testing.T) {
|
||||
if err := store.Save(path, s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := delivery.Composed{Node: "ace", Module: "ca-trust", Filled: []string{"/etc/ca-filled"}, Resources: []json.RawMessage{
|
||||
c := engcomp.Composed{Node: "ace", Module: "ca-trust", Filled: []string{"/etc/ca-filled"}, Resources: []json.RawMessage{
|
||||
json.RawMessage(`{"id":"ca-trust.trust","type":"file","module":"ca-trust","path":"/etc/ca","mode":"0644","content":"x"}`),
|
||||
json.RawMessage(`{"id":"ca-trust.filled","type":"file","module":"ca-trust","path":"/etc/ca-filled","mode":"0644","owner-at-runtime":"owned"}`)}}
|
||||
if !recordedIn(path, func(string) {})(context.Background(), c) {
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"os"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// names what is on disk: **only when the node's record says it wrote exactly that content there** (store Applied.Wrote,
|
||||
// Target), never a file someone else changed. A file that cannot be read, or that the record does not vouch for, is
|
||||
// left as the composition names it.
|
||||
func runsAsWritten(running []delivery.Composed, statePath string) []delivery.Composed {
|
||||
func runsAsWritten(running []engcomp.Composed, statePath string) []engcomp.Composed {
|
||||
if len(running) == 0 || statePath == "" {
|
||||
return running
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func runsAsWritten(running []delivery.Composed, statePath string) []delivery.Com
|
||||
if err != nil {
|
||||
return running
|
||||
}
|
||||
out := make([]delivery.Composed, len(running))
|
||||
out := make([]engcomp.Composed, len(running))
|
||||
for i, c := range running {
|
||||
out[i] = c
|
||||
var changed []json.RawMessage
|
||||
|
||||
@@ -110,7 +110,7 @@ func (w findingsOfTheNode) encode(f views.Finding) (string, []byte, error) {
|
||||
key := conditions.Key(conditions.ScopeMachine, f.ID, f.Kind)
|
||||
v := condseat.FindingsByKeyValue{V: 1, Key: key, Kind: f.Kind, Observer: f.Observer, Severity: conditions.Warning,
|
||||
Subject: condseat.Subject{Scope: conditions.ScopeMachine, ID: f.ID, Machine: f.ID}, Summary: f.Summary,
|
||||
Headline: f.Headline, Explanation: f.Explanation, Source: "seat-list-unread", At: f.At.UTC().Format(time.RFC3339)}
|
||||
Headline: f.Headline, Explanation: f.Explanation, Source: "seat-list-unread", At: f.At.UTC()}
|
||||
if err := conditions.CheckFinding(v); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -25,7 +26,7 @@ import (
|
||||
|
||||
// userListFollower is the follower of the bus's user list for one link: the node's composition bucket, the keys the
|
||||
// agent trusts, the write alone, and the have.
|
||||
func userListFollower(opts options, session *link.Session, running func() []delivery.Composed,
|
||||
func userListFollower(opts options, session *link.Session, running func() []engcomp.Composed,
|
||||
say link.Announce, wrote func()) *userlist.Follower {
|
||||
return userlist.New(userlist.Env{
|
||||
Node: session.Membership.Node,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -31,7 +32,7 @@ func aFileOf(module, id string) json.RawMessage {
|
||||
func TestWhatRunsIsTheAgentsCompositionsAndTheKeptRest(t *testing.T) {
|
||||
kept := &declaration.Declaration{Identities: []declaration.Identity{{Module: "db", Name: "mesh-db"}},
|
||||
Libraries: []declaration.Library{{Library: "media"}}}
|
||||
everything := []delivery.Composed{
|
||||
everything := []engcomp.Composed{
|
||||
{Node: "n1", Module: "web", Resources: []json.RawMessage{aFileOf("web", "a")}},
|
||||
{Node: "n1", Module: "db", Resources: []json.RawMessage{aFileOf("db", "a")}}, // still the kept declaration's
|
||||
}
|
||||
@@ -45,7 +46,7 @@ func TestWhatRunsIsTheAgentsCompositionsAndTheKeptRest(t *testing.T) {
|
||||
}
|
||||
|
||||
// Once the agent holds the node's composition, its parts are the node's.
|
||||
node := delivery.Composed{Node: "n1", Own: delivery.NodeOwn{Identities: []declaration.Identity{{Module: "web", Name: "mesh-web"}}}}
|
||||
node := engcomp.Composed{Node: "n1", Own: engcomp.NodeOwn{Identities: []declaration.Identity{{Module: "web", Name: "mesh-web"}}}}
|
||||
d, err = whatRuns(append(everything, node), true, func() (*declaration.Declaration, error) { return nil, errors.New("not read") })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -88,7 +89,7 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
|
||||
for _, name := range row.runs {
|
||||
raws = append(raws, process(name))
|
||||
}
|
||||
runs, err := whatRuns([]delivery.Composed{{Node: "n1", Module: "restic", Resources: raws}}, false,
|
||||
runs, err := whatRuns([]engcomp.Composed{{Node: "n1", Module: "restic", Resources: raws}}, false,
|
||||
func() (*declaration.Declaration, error) { return nil, errors.New("none kept") })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -161,9 +162,9 @@ func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
|
||||
// the module's: the declaration joined of both names each file once, so the filling writes it; the node's own resources
|
||||
// no module declares stay. (Live from 2026-10-25 12:43: "2 resources declare X" on every node.)
|
||||
func TestTheNodesOwnCopyOfAModulesResourceIsLeftOut(t *testing.T) {
|
||||
own := delivery.Composed{Node: "n1", Resources: []json.RawMessage{aFileOf("zsh", "rc"), aFileOf("mesh-wireguard", "conf")},
|
||||
own := engcomp.Composed{Node: "n1", Resources: []json.RawMessage{aFileOf("zsh", "rc"), aFileOf("mesh-wireguard", "conf")},
|
||||
Filled: []string{"/tmp/r6/zsh/rc"}}
|
||||
zsh := delivery.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")},
|
||||
zsh := engcomp.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")},
|
||||
Filled: []string{"/tmp/r6/zsh/rc"}}
|
||||
d, err := declarationOf(own, zsh)
|
||||
if err != nil {
|
||||
@@ -181,7 +182,7 @@ func TestTheNodesOwnCopyOfAModulesResourceIsLeftOut(t *testing.T) {
|
||||
}
|
||||
|
||||
// The same at the node's own apply: what the others declare is theirs.
|
||||
trimmed, dropped := ownWithout(own, []delivery.Composed{zsh})
|
||||
trimmed, dropped := ownWithout(own, []engcomp.Composed{zsh})
|
||||
if len(trimmed.Resources) != 1 || len(dropped) != 1 || dropped[0] != "zsh.rc (zsh)" {
|
||||
t.Errorf("the node's own without zsh's: %d resources, dropped %v", len(trimmed.Resources), dropped)
|
||||
}
|
||||
@@ -211,9 +212,9 @@ func TestTheKeptDeclarationsFilledPathsGoToTheirModules(t *testing.T) {
|
||||
strings.Join(filled[agent.NodeModule], ",") != "/etc/motd" || len(filled["zsh"]) != 0 {
|
||||
t.Fatalf("filled by module: %v", filled)
|
||||
}
|
||||
cc := delivery.Composed{Node: "n1", Module: "claude-code", Resources: by["claude-code"], Filled: filled["claude-code"]}
|
||||
node := delivery.Composed{Node: "n1"}
|
||||
d, err := whatRuns([]delivery.Composed{cc, node}, true, func() (*declaration.Declaration, error) { return nil, errors.New("not read") })
|
||||
cc := engcomp.Composed{Node: "n1", Module: "claude-code", Resources: by["claude-code"], Filled: filled["claude-code"]}
|
||||
node := engcomp.Composed{Node: "n1"}
|
||||
d, err := whatRuns([]engcomp.Composed{cc, node}, true, func() (*declaration.Declaration, error) { return nil, errors.New("not read") })
|
||||
if err != nil || !d.Fills("/etc/cc.json") {
|
||||
t.Fatalf("what runs does not name claude-code's file filled: %v (%v)", d, err)
|
||||
}
|
||||
@@ -226,9 +227,9 @@ func TestTheKeptDeclarationsFilledPathsGoToTheirModules(t *testing.T) {
|
||||
// unit or a container of the same name) is not the module's, and is neither dropped nor left to be removed.
|
||||
func TestASharedTargetWithAnotherIDIsNotDropped(t *testing.T) {
|
||||
ownFile, _ := json.Marshal(map[string]any{"id": "host.zshrc", "type": "file", "path": "/tmp/r6/zsh/rc", "content": "x"})
|
||||
own := delivery.Composed{Node: "n1", Resources: []json.RawMessage{ownFile}}
|
||||
zsh := delivery.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")}}
|
||||
trimmed, dropped := ownWithout(own, []delivery.Composed{zsh})
|
||||
own := engcomp.Composed{Node: "n1", Resources: []json.RawMessage{ownFile}}
|
||||
zsh := engcomp.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")}}
|
||||
trimmed, dropped := ownWithout(own, []engcomp.Composed{zsh})
|
||||
if len(dropped) != 0 || len(trimmed.Resources) != 1 {
|
||||
t.Errorf("a resource sharing only its target was dropped: %v", dropped)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ module github.com/novox/mesh-host
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-20261105000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-20261105000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-20261105000000-000000000000
|
||||
git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go v0.0.0-20261105000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
|
||||
+30
-30
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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}}
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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 {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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) }()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user