20 Commits
Author SHA1 Message Date
jschoubben a24a1d72ad Pin mesh-sdk cf5812101b0b, the group's final head: the module graph is no part of mesh-sdk (ADR 0603, ADR 0604, issue 1367)
mesh/check failed (4): unit: modules/mesh-provisioner.gofmt: not as gofmt writes them (run gofmt -w): modul…; and 3 more | a24a1d72ad45 on 798d73eb11e9
mesh/delivery-group group feat/l3b-bus rejected: composed together: the checker's verdict at the members' heads (a1cb9beeb97a): unit: modules/mesh-provisioner.…
mesh/delivery closed: the pull request closed unmerged
2026-11-03 17:02:14 +01:00
jschoubben 02c7f8e052 Pin mesh-sdk 65b7adc8bdd9; the seat table here takes the SDK's mesh-catalog entry with its four verbs (ADR 0604 section 1) 2026-11-03 16:25:46 +01:00
jschoubben 53b0e8d318 Genesis installs no catalog module: the catalogue step and the take of mesh-catalog go (ADR 0604 section 7, issue 774) 2026-11-03 16:17:27 +01:00
jschoubben c777fbae8f Pin mesh-sdk e7ce86ff62b5, the group's head of feat/l3b-bus (review round 2) 2026-11-03 14:12:50 +01:00
jschoubben 98719a1bd5 Pin mesh-sdk 825483043549, the group's head of feat/l3b-bus (review round 1) 2026-11-03 14:05:07 +01:00
jschoubben 72bcf20870 The bus module's integration test publishes the backup's end at once: the node agent hears it before it asks, so no sleep (review round 1, finding 5) 2026-11-03 14:02:01 +01:00
jschoubben b47668421f The witness judges the tool runner's process by its id, <its module>.runtime, which the composition gives only the module whose manifest says it is the tool runner, never by a name (ADR 0601 §4; review round 1, finding 4) 2026-11-03 14:02:01 +01:00
jschoubben 2239fb253d Rows for the composer's composition of today: the same inputs give the running digest, a changed input another, a build it cannot compose none (ADR 0601 §1; review round 1, finding 2) 2026-11-03 14:02:01 +01:00
jschoubben 7da4deb3f1 What a node keeps of a module as it runs, for a manifest that does not yet say it, is carried on the module's have by module, so the watchman raises its condition (ADR 0601 §4; review round 1, finding 3) 2026-11-03 14:02:01 +01:00
jschoubben b461b005e9 Pin mesh-sdk 09ec82cb3551, the group's head of feat/l3b-bus (ADR 0601) 2026-11-03 13:46:47 +01:00
jschoubben 75e3a26336 The integration test of the bus module's first want shows its node backing it up through the backup seat before that apply and before the one that replaces the server, as every module that keeps data (ADR 0601 §2) 2026-11-03 13:37:42 +01:00
jschoubben 5b455cecfe A have waiting for a yes says whether the move changes the build alone: the composer composes the build the module runs with today's settings and secrets and the agent compares it with what runs, so a release's Approve answers it and a changed setting is still asked (ADR 0601 §1) 2026-11-03 13:35:12 +01:00
jschoubben faa2005538 A node agent that meets a manifest which does not yet say it is the tool runner, or that its own secrets are the core's credentials, keeps what runs of it, its tool runner process and its sealed secrets alike, known by what runs and said; never composed without it (ADR 0601 §4) 2026-11-03 13:31:44 +01:00
jschoubben 6ea23b6731 The sandbox's composer marks a node it does not judge in the contract's field (Machine.NotJudged), neither composed nor refused, never by a prefix of its reason (ADR 0593 §9; the prepare's seams finding on "not judged") 2026-11-03 13:29:21 +01:00
jschoubben db7cd273f0 The node-engine finds the tool runner by what its manifest says (tool-runner), never by its name, pinned at mesh-sdk a973bfa (issue 774, the operator 2026-11-03) 2026-11-03 13:28:44 +01:00
jschoubben 12c80d7c2e The node-engine carries a module's own secrets sealed by the vault when its manifest says they are the core's credentials, pinned at mesh-sdk 4f141ff (issue 774, the operator 2026-11-03) 2026-11-03 13:28:44 +01:00
jschoubben 649ed21d55 The sandbox refuses no module the node holds composing without the change, a module blamed for a refusal among them (issue 774, review round 1) 2026-11-03 13:28:44 +01:00
jschoubben 23cbf9294a The node agent's applies run through a runner its options name, so one integration test proves the bus module's first want takes the bus server over from the node's own composition without moving it, and a build that changes it replaces it once (ADR 0599 §4, §6) 2026-11-03 13:28:44 +01:00
jschoubben 03b7fcd7bb The sandbox fails a node whose change refuses a module the node-engine did not hold, in that module's words, instead of passing it with the rest composed (issue 774, review of the sandbox input) 2026-11-03 13:28:44 +01:00
jschoubben 7b8f5e7805 The sandbox composes a node as its node-engine does, from an input that says how, and names a node it cannot judge from an older engine's input (issue 774, ADR 0593 §9) 2026-11-03 13:28:44 +01:00
44 changed files with 1018 additions and 159 deletions
+13 -2
View File
@@ -156,6 +156,17 @@ type options struct {
file string
// out is where what a command says goes. Stdout, and a buffer under test.
out io.Writer
// runner is what the node agent's applies run commands through (issue 774, ADR 0594): nil is the machine's own,
// apply.ExecRunner; a test gives one that records what an apply asked of the container runtime.
runner apply.Runner
}
// run is the runner the node agent's applies use: the options' own, else the machine's.
func (o options) run() apply.Runner {
if o.runner != nil {
return o.runner
}
return apply.ExecRunner
}
// parseArgs takes the subcommand first, then its flags.
@@ -1637,11 +1648,11 @@ func applyModuleHeld(ctx context.Context, opts options, declared *declaration.De
func(known store.State) (apply.Report, store.State, error) {
if scope == nil {
return apply.ApplyMindingWindows(ctx, built, declared, known, store.OriginDeclared,
apply.ExecRunner, announceOr(say), sealOpener(opts.state), apply.KeepIn(filepath.Dir(opts.state)),
opts.run(), announceOr(say), sealOpener(opts.state), apply.KeepIn(filepath.Dir(opts.state)),
apply.WindowsIn(filepath.Dir(opts.state)))
}
return apply.ApplyModule(ctx, built, declared, *scope, known, store.OriginDeclared,
apply.ExecRunner, announceOr(say), sealOpener(opts.state), apply.KeepIn(filepath.Dir(opts.state)),
opts.run(), announceOr(say), sealOpener(opts.state), apply.KeepIn(filepath.Dir(opts.state)),
apply.WindowsIn(filepath.Dir(opts.state)))
}, announceOr(say))
+12 -5
View File
@@ -120,7 +120,7 @@ func (h sessionHaves) Write(ctx context.Context, x agent.Have) {
}
h.session.NoteModule(ctx, link.ModuleFact{Module: x.Module, FP: x.FP, Composed: x.Composed, Phase: phase,
Failed: phase == string(delivery.PhaseFailed), Detail: shorten(x.Detail), Judgement: x.Judgement, Waits: x.Waits,
Sealed: x.Sealed})
BuildAlone: x.BuildAlone, Kept: x.Kept, Sealed: x.Sealed})
}
func (h sessionHaves) Removed(ctx context.Context, module string) {
@@ -508,7 +508,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
// What this engine applied, kept on the machine and read at the start, so what runs is known before the node
// composes its own (issue 774).
Applied: appliedStoreFor(opts.state),
Inactive: inactiveIn(&liveness.Exec{Run: apply.ExecRunner, System: builtFor}, heldBy(apply.WindowsIn(filepath.Dir(opts.state)))),
Inactive: inactiveIn(&liveness.Exec{Run: liveness.Runner(opts.run()), System: builtFor}, heldBy(apply.WindowsIn(filepath.Dir(opts.state)))),
Revisions: func(ctx context.Context, key string) ([]agent.Update, error) {
got, err := session.KVHistory(ctx, composedBucket, key)
if err != nil {
@@ -606,6 +606,12 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
now.NotComposedEach(run, why, held) // a module held alone marks that module, never the others (issue 1126)
}
},
// And what it keeps of a module as it runs, for a manifest that does not say it yet: on the module's have (ADR 0601 §4).
kept: func(kept map[string][]string) {
if now := own.get(); now != nil && run.Err() == nil {
now.KeptNow(run, kept)
}
},
// And the vault's authority beside the controller's, and this node's certificate asked of the vault when a
// module here asks for one (ADR 0550 §2).
vaultAuthority: trust.VaultAuthority,
@@ -616,7 +622,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
// And its answer to every placement candidate, placed or not, applying nothing (novox/hq ADR 0559 §4): written into
// its own key, `<node>.node`, under candidates.
shadowHooks := hooks
shadowHooks.refused, shadowHooks.certify = nil, nil
shadowHooks.refused, shadowHooks.kept, shadowHooks.certify = nil, nil, nil
shadowEnv := composerEnv(session, node, say, shadowHooks)
shadow := selfcompose.NewShadow(shadowEnv,
func(ctx context.Context, answers map[string]delivery.CandidateAnswer) {
@@ -630,7 +636,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
composer := selfComposer(session, node, hooks, say)
machinesNow.Store(composer) // the machines a failing network part points at, by the placements (issue 774)
composerNow.Store(composer) // what the verb compose asks a pass of, this run's alone
env.Composed, env.SelfComposed, env.Policy = composer, true, composer.Policy
env.Composed, env.SelfComposed, env.Policy, env.ComposedToday = composer, true, composer.Policy, composer.ComposedToday
env.HoldsData = composer.HoldsData // what runs from the kept declaration says Data as its manifest does
session.NoteComposes(linked, delivery.ComposesOwn, "it composes its own modules, and applies no composition made elsewhere")
ag = agent.New(env)
@@ -958,6 +964,7 @@ type composerHooks struct {
running func() []delivery.Composed
unseal func(string) ([]byte, error)
refused func(why string, held map[string]string)
kept func(kept map[string][]string)
held func(module string) (delivery.Seals, []string, error)
// vaultAuthority and certify are selfcompose.Env's VaultAuthority and Certify.
vaultAuthority func() string
@@ -1011,6 +1018,6 @@ func composerEnv(session *link.Session, node string, say func(string), hooks com
Hear: session.Hear,
// And a moved module's templates archive, in the artifact store, as every archive is fetched (issue 1126).
Fetch: apply.Fetch,
Running: hooks.running, Unseal: hooks.unseal, Refused: hooks.refused, Held: hooks.held,
Running: hooks.running, Unseal: hooks.unseal, Refused: hooks.refused, Held: hooks.held, Kept: hooks.kept,
VaultAuthority: hooks.vaultAuthority, Certify: hooks.certify}
}
+255
View File
@@ -6,6 +6,7 @@ import (
"context"
"crypto/ed25519"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
@@ -14,13 +15,17 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/link"
"github.com/novox/mesh-host/internal/nodegrant"
"github.com/novox/mesh-host/internal/selfcompose"
"github.com/novox/mesh-host/internal/store"
"github.com/novox/mesh-host/internal/system"
)
// The node agent whole, against a real bus with JetStream started in the test: the wants bucket, the node's composition
@@ -619,3 +624,253 @@ func mustComposedKey(t *testing.T, module, fp string) string {
}
return key
}
// recordingRuntime is a container runtime that keeps its containers in memory and records every command an apply asks
// of it (issue 774): what the bus server's container met is read from it, never from a real runtime.
type recordingRuntime struct {
mu sync.Mutex
containers map[string]string // name: the spec label it was made with
asked []string
}
func (r *recordingRuntime) run(_ context.Context, name string, args ...string) (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.asked = append(r.asked, name+" "+strings.Join(args, " "))
if name != "docker" || len(args) == 0 {
return "", nil
}
switch args[0] {
case "info":
return "27.0\n", nil
case "container":
if len(args) > 1 && args[1] == "inspect" {
spec, ok := r.containers[args[len(args)-1]]
if !ok {
return "", errors.New("no such container")
}
return "true\t" + spec + "\n", nil
}
case "image":
if len(args) > 1 && args[1] == "inspect" {
return "2026-01-01T00:00:00Z\n", nil
}
case "rm":
delete(r.containers, args[len(args)-1])
case "rename":
if spec, ok := r.containers[args[1]]; ok {
delete(r.containers, args[1])
r.containers[args[2]] = spec
}
case "run", "create":
var made, spec string
for i, a := range args {
if a == "--name" && i+1 < len(args) {
made = args[i+1]
}
if a == "--label" && i+1 < len(args) && strings.HasPrefix(args[i+1], "mesh-host.spec=") {
spec = strings.TrimPrefix(args[i+1], "mesh-host.spec=")
}
}
r.containers[made] = spec
return "made\n", nil
}
return "", nil
}
// moved is each command that removed, made, stopped, renamed or restarted the container named.
func (r *recordingRuntime) moved(container string) []string {
r.mu.Lock()
defer r.mu.Unlock()
var out []string
for _, a := range r.asked {
for _, verb := range []string{"rm ", "run ", "create ", "stop ", "restart ", "kill ", "rename ", "start "} {
for _, prefix := range []string{"docker ", "docker container "} {
if strings.HasPrefix(a, prefix+verb) && strings.Contains(a, container) {
out = append(out, a)
}
}
}
}
return out
}
// ADR 0599 §6 and §4 (issue 774): the bus server the node's own composition made while the bus module was folded is taken
// over by the bus module's first want, the node agent started whole against a real bus: it composes the bus module, writes
// its changed file and records the server as the bus module's, and asks the container runtime to remove, make, stop,
// rename or restart nothing, since the build does not change the server. A later want whose build changes the server
// replaces it once, as any module's, once the want carries the yes (§2: the assigner gives it from the release's Approve).
// ADR 0601 §2: the bus module keeps data (its streams), so before each of those applies the node agent asks its node's
// backup seat to back it up, as for every module that keeps data, and applies only once the backup says it is done.
func TestNatsTheBusServerIsTakenOverAndReplacedOnlyWhenItsBuildChangesIt(t *testing.T) {
b := newItBed(t)
runtime := &recordingRuntime{containers: map[string]string{}}
b.opts.runner = runtime.run
const server = "mesh-broker-it"
conf := filepath.Join(b.dir, "nats.conf")
image := "registry.example/nats@sha256:" + strings.Repeat("a", 64)
// What the node's own composition made of the folded bus: the server and its file, recorded as the node's own.
built, err := system.For(builtFor)
if err != nil {
t.Fatal(err)
}
folded, err := declaration.Parse([]byte(`{"declaration":1,"resources":[` +
`{"id":"nats.server-conf","type":"file","path":"` + conf + `","mode":"0644","content":"listen: old\n"},` +
`{"id":"nats.server","type":"container","name":"` + server + `","image":"` + image + `"}]}`))
if err != nil {
t.Fatal(err)
}
_, known, err := apply.ApplyModule(context.Background(), built, folded, apply.Scope{Module: store.NodeModule}, store.State{},
store.OriginDeclared, runtime.run, nil, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
if err := store.Save(b.opts.state, known); err != nil {
t.Fatal(err)
}
if moved := runtime.moved(server); len(moved) != 1 {
t.Fatalf("the folded server was not made once: %v", moved)
}
made := runtime.containers[server]
runtime.mu.Lock()
runtime.asked = nil
runtime.mu.Unlock()
kvOf := func(bucket string) nats.KeyValue {
kv, err := b.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: bucket, History: 5})
if err != nil {
t.Fatal(err)
}
return kv
}
placement, modules, manifests := kvOf(delivery.PlacementBucket), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket)
kvOf(selfcompose.ResolvedBucket)
builds := kvOf(delivery.BuilderBuildsBucket)
kvOf(delivery.PlacementCandidatesBucket)
kvOf(link.PackagesBucket)
// The bus module at a build, done: its manifest written and the indexer's module naming it at fp.
publish := func(fp, env string) {
man := `{"module":"nats","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}],` +
`"data":{"own":[{"id":"jetstream","path":"` + b.dir + `","class":"valuable","why":"the bus's streams"}]},"resources":[` +
`{"id":"server-conf","type":"file","path":"` + conf + `","mode":"0644","content":"listen: new\n"},` +
`{"id":"server","type":"container","name":"` + server + `","image":"` + image + `"` + env + `}]}`
digest := delivery.ManifestDigest([]byte(man))
mraw, _ := delivery.EncodeManifest(delivery.Manifest{Module: "nats", JSON: man})
mkey, _ := delivery.ManifestsKey(digest)
modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/mesh-catalog", Commit: strings.Repeat("3", 40)})
if _, err := manifests.Put(mkey, mraw); err != nil {
t.Fatal(err)
}
bkey, _ := delivery.BuilderBuildKey("nats", fp)
done, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: digest})
if _, err := builds.Put(bkey, done); err != nil {
t.Fatal(err)
}
if _, err := modules.Put("nats", modv); err != nil {
t.Fatal(err)
}
}
rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: itNode,
Capabilities: []string{"service-manager", "package-manager", "container-runtime"}}})
hkey, _ := delivery.HaveNodeKey(itNode)
places, _ := delivery.PlacementValues(delivery.Placement{Node: itNode, Modules: []delivery.PlacedModule{{Module: "nats"}}})
// The have the engine wrote of the folded bus: the old-form build, no fingerprint.
natsHave, _ := delivery.HaveKey(itNode, "nats")
oldHave, _ := delivery.EncodeHave(delivery.Have{Build: "build-1792674595610176744", Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
for key, value := range map[string][]byte{hkey: rec, natsHave: oldHave} {
if _, err := b.haves.Put(key, value); err != nil {
t.Fatal(err)
}
}
busWant := func(w delivery.Want) {
raw, err := delivery.EncodeWant(w)
if err != nil {
t.Fatal(err)
}
key, _ := delivery.WantKey(itNode, "nats")
if _, err := b.wants.Put(key, raw); err != nil {
t.Fatal(err)
}
}
// The node's backup seat: its holder says the bus module has something to back up, starts a backup on now, and says
// when it ended on its event; each ask of now notes what the bus's file held then, so the order is seen.
var backups []string
var backupsMu sync.Mutex
if _, err := b.conn.Subscribe(link.SeatToolSubject("node-backup", "backed-up", itNode), func(m *nats.Msg) {
_ = m.Respond([]byte(`{"result":[{"module":"nats","runs":1}]}`))
}); err != nil {
t.Fatal(err)
}
if _, err := b.conn.Subscribe(link.SeatToolSubject("node-backup", "now", itNode), func(m *nats.Msg) {
got, _ := os.ReadFile(conf)
backupsMu.Lock()
backups = append(backups, string(got))
backupsMu.Unlock()
// The node agent hears the backup's end before it asks (backupNode), so the event follows the answer at once.
_ = m.Respond([]byte(`{"result":{"started":true}}`))
_ = b.conn.Publish(backedUpEvent, backupEvent(itNode, "nats", true, time.Now(), ""))
}); err != nil {
t.Fatal(err)
}
backedUp := func() []string {
backupsMu.Lock()
defer backupsMu.Unlock()
return append([]string(nil), backups...)
}
first := fpIt("e")
publish(first, "")
busWant(delivery.Want{FP: first, Self: true})
stop := b.start()
defer stop()
for key, value := range places {
if _, err := placement.Put(key, value); err != nil {
t.Fatal(err)
}
}
within(t, 10*time.Second, "the bus module's first want was not applied", func() bool {
h := b.have("nats")
got, _ := os.ReadFile(conf)
return h.FP == first && h.Phase == delivery.PhaseRunning && string(got) == "listen: new\n"
})
if moved := runtime.moved(server); len(moved) != 0 || runtime.containers[server] != made {
t.Fatalf("the first want moved the bus server its build does not change: %v", moved)
}
if got := backedUp(); len(got) != 1 || got[0] != "listen: old\n" {
t.Fatalf("the bus module's first apply was not backed up first: the backups saw %q", got)
}
state, err := store.Load(b.opts.state)
if err != nil {
t.Fatal(err)
}
if rec, ok := state.Find("nats.server"); !ok || rec.Module != "nats" {
t.Errorf("the server is recorded %+v, not as the bus module's", rec)
}
// A build that changes the server: replaced once, as any module's, with the yes the assigner writes from the release's
// Approve for a move the node judged to recreate it (ADR 0599 §2, §4).
next := fpIt("f")
publish(next, `,"env":{"CHANGED":"1"}`)
busWant(delivery.Want{FP: next, Self: true})
within(t, 10*time.Second, "the build that changes the server was not applied", func() bool {
h := b.have("nats")
if h.Phase == delivery.PhaseWaitingYes && h.Waits != "" {
busWant(delivery.Want{FP: next, Self: true, Yes: h.Waits})
}
return h.FP == next && h.Phase == delivery.PhaseRunning
})
var runs int
for _, a := range runtime.moved(server) {
if strings.HasPrefix(a, "docker run ") && strings.Contains(a, " --name "+server+" ") {
runs++
}
}
if got := backedUp(); len(got) != 2 || got[1] != "listen: new\n" {
t.Errorf("the replacing apply of the bus module was not backed up first: the backups saw %q", got)
}
if runs != 1 || runtime.containers[server] == made {
t.Errorf("the build that changes the server made it %d times, its spec still the folded one %v: %v", runs,
runtime.containers[server] == made, runtime.moved(server))
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/novox/mesh-host
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
golang.org/x/crypto v0.57.0
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b h1:UPkJqQj+1y6WraOxDidkrokNjA25jxmCk+zIRkujjMc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103155154-cf5812101b0b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+66 -1
View File
@@ -17,6 +17,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"sort"
"strings"
"sync"
@@ -58,6 +59,11 @@ type Have struct {
// waits for a person's yes (ADR 0547 §3).
Judgement string
Waits string
// BuildAlone says, in phase waiting-yes, the move changes the build alone (novox/hq ADR 0601 §1, delivery.Have.BuildAlone).
BuildAlone bool
// Kept says, in words, what the node keeps of the module as it runs because its manifest does not yet say it (ADR 0601
// §4, delivery.Have.Kept): the watchman raises a condition from it.
Kept []string
// Sealed is, on a node composing its own, each value the composition it names carries sealed to the node's key, by
// slot (delivery.Have.SealedAt): kept in the have so the composer reuses them after a restart (ADR 0547 §3), each at
// its own slot alone (issue 1126, C1).
@@ -153,6 +159,11 @@ type Env struct {
// move of a module of policy record that recreates a container or moves its data waits for a person's yes in its
// want (ADR 0547 §3). Nil judges no move.
Policy func(ctx context.Context, module string) string
// ComposedToday is the digest of a module composed at the build fp with today's settings and secrets, as the node would
// compose it now, and whether it could be composed: a move to another build whose composition of the build it runs is
// what runs changes the build alone (novox/hq ADR 0601 §1), and the have says so. Nil: no move is said to change the
// build alone, and each is asked of a person.
ComposedToday func(ctx context.Context, module, fp string) (string, bool)
// HoldsData says whether a module's manifest declares data (catalogue.Manifest.DataItems), and whether its manifest
// could be read: what a composition's Data is, for what runs of a module from the kept declaration, which carries no
// Data of its own. A composition says Data, so without it a data module from the kept declaration is judged to move
@@ -198,6 +209,8 @@ type Agent struct {
// keptData is, for each module of the kept declaration whose manifest could be read, whether it declares data
// (Env.HoldsData): the Data of what runs of it from the kept declaration.
keptData map[string]bool
// kept is, by module, what the composer's last pass keeps of it as it runs (KeptNow, ADR 0601 §4). Under mu.
kept map[string][]string
// attempt is the last apply of a module that failed or is held, with the revisions it was made from.
attempt map[string]attempt
checked map[string]bool
@@ -572,6 +585,11 @@ func (a *Agent) NotComposedEach(ctx context.Context, why string, held map[string
// said runs, its fingerprint, digest and sealed values, rather than a have naming nothing: what runs is not lost, and
// the boxes the composer reuses after a restart stay (review of L2, rounds 2 and 3).
func (a *Agent) writeHave(ctx context.Context, h Have) {
if h.Module != NodeModule {
a.mu.Lock()
h.Kept = a.kept[h.Module]
a.mu.Unlock()
}
if h.Composed == "" && h.Module != NodeModule {
if last, ok := a.env.Haves.Read(ctx, h.Module); ok && last.Composed != "" {
h.FP, h.Composed = last.FP, last.Composed
@@ -588,6 +606,36 @@ func (a *Agent) writeHave(ctx context.Context, h Have) {
a.env.Haves.Write(ctx, h)
}
// KeptNow takes in what the composer's last pass keeps of each module as it runs because its manifest does not yet say it
// (ADR 0601 §4), and writes again the have of each module whose words changed, so it carries them; a have waiting for a yes
// takes them at its next write, which keeps what it waits for.
func (a *Agent) KeptNow(ctx context.Context, kept map[string][]string) {
a.mu.Lock()
var changed []string
for m := range a.kept {
if _, still := kept[m]; !still {
changed = append(changed, m)
}
}
for m, words := range kept {
if !slices.Equal(a.kept[m], words) {
changed = append(changed, m)
}
}
a.kept = map[string][]string{}
for m, words := range kept {
a.kept[m] = slices.Clone(words)
}
a.mu.Unlock()
slices.Sort(changed)
for _, m := range changed {
if h, ok := a.env.Haves.Read(ctx, m); ok && m != NodeModule && h.Phase != string(delivery.PhaseWaitingYes) {
h.Module = m
a.writeHave(ctx, h)
}
}
}
func (a *Agent) sealedOf(ctx context.Context, module, fp, composed string) delivery.Seals {
a.mu.Lock()
if c, ok := a.applied[module]; ok && c.Err == nil && c.Dig == composed {
@@ -1649,10 +1697,27 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
prev := a.running[module]
a.mu.Unlock()
a.writeHave(ctx, Have{Module: module, FP: prev.FP, Composed: prev.Composed, Phase: string(delivery.PhaseWaitingYes),
Detail: j.Words, Judgement: string(j.Judgement), Waits: comp.Dig})
Detail: j.Words, Judgement: string(j.Judgement), Waits: comp.Dig, BuildAlone: a.buildAlone(ctx, module, prev, w.Want.FP)})
return true
}
// buildAlone says a move of module from what runs to the build want changes the build alone (ADR 0601 §1): another build than
// the one it runs, and the build it runs composed with today's settings and secrets is the composition it runs. Anything it
// cannot tell is no: the move is then asked of a person.
func (a *Agent) buildAlone(ctx context.Context, module string, prev delivery.Have, want string) bool {
if prev.FP == "" && prev.Composed == "" {
// Nothing of it run since this agent's start: its last have says what runs, as the have written keeps it.
if h, ok := a.env.Haves.Read(ctx, module); ok {
prev.FP, prev.Composed = h.FP, h.Composed
}
}
if a.env.ComposedToday == nil || prev.FP == "" || prev.Composed == "" || prev.FP == want {
return false
}
dig, ok := a.env.ComposedToday(ctx, module, prev.FP)
return ok && dig == prev.Composed
}
// runningComposition is what the module runs now: the composition last applied, else its resources in the declaration
// the node kept from before the switch. Called with a.mu held.
//
+28 -1
View File
@@ -1288,6 +1288,17 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
}
return catalogue.PolicyRoll
}
// ADR 0601 §1: what runs composed today. rec's build composed again is what it runs, so its move changes the build alone;
// db's is not (a setting changed too).
b.env.ComposedToday = func(_ context.Context, m, fp string) (string, bool) {
switch m {
case "rec":
return "sha256:" + strings.Repeat("8", 64), fp == fpOf("d")
case "db":
return "sha256:" + strings.Repeat("6", 64), true
}
return "", false
}
// The bucket's revisions of each key, as the verb composition reads them (issue 774).
var historyMu sync.Mutex
history := map[string][]Update{}
@@ -1329,6 +1340,15 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
want("web", fp, "")
eventually(t, "the node's own composition was not applied", func() bool { return len(b.appliedList()) == 1 })
eventually(t, "the have does not name the composed digest", func() bool { return b.haves.last("web").Composed == dig })
// ADR 0601 §4: what the composer keeps of web as it runs is carried on its have, and goes once it keeps none.
b.ag.KeptNow(context.Background(), map[string][]string{"web": {"web runs the tool runner's process here"}})
if h := b.haves.last("web"); len(h.Kept) != 1 || h.Composed != dig {
t.Errorf("web's have does not carry what is kept as it runs: %+v", h)
}
b.ag.KeptNow(context.Background(), nil)
if h := b.haves.last("web"); len(h.Kept) != 0 || h.Composed != dig {
t.Errorf("web's have still carries what was kept: %+v", h)
}
// The same composition handed again is no move.
b.comps.put(ck, craw)
time.Sleep(50 * time.Millisecond)
@@ -1452,6 +1472,9 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
h := b.haves.last("db")
return h.Phase == string(delivery.PhaseWaitingYes) && h.Waits == ddigMoved
})
if h := b.haves.last("db"); h.BuildAlone {
t.Errorf("a revision at the build it runs is said to change the build alone: %+v", h)
}
// The verb composition says why it waits: the revision that waits, what runs, the judgement, and with diff the
// container it recreates; the revision that runs, asked by its digest, is no move (issue 774).
keep(dk, draw)
@@ -1508,7 +1531,8 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
for _, row := range []struct {
module, fp, composed, waits string
sealed delivery.Seals
}{{"db", fp, "", ddig2, nil}, {"rec", recFP, recDig, rdig, delivery.Seals{"rec.key#sealed": `{"box":"rec"}`}}} {
alone bool
}{{"db", fp, "", ddig2, nil, false}, {"rec", recFP, recDig, rdig, delivery.Seals{"rec.key#sealed": `{"box":"rec"}`}, true}} {
eventually(t, row.module+"'s move did not wait for a yes", func() bool {
h := b.haves.last(row.module)
return h.Phase == string(delivery.PhaseWaitingYes) && h.Waits == row.waits
@@ -1518,6 +1542,9 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
(row.sealed != nil && !maps.Equal(h.Sealed, row.sealed)) {
t.Errorf("%s's have waiting for a yes does not keep what runs: %+v", row.module, h)
}
if h.BuildAlone != row.alone {
t.Errorf("%s's have waiting for a yes says build alone %v, want %v", row.module, h.BuildAlone, row.alone)
}
}
if got := b.appliedList(); len(got) != applied {
t.Fatalf("a move waiting for a yes was applied: %v", got)
+1 -1
View File
@@ -536,7 +536,7 @@ func witnessOf(r *declaration.Process) string {
if r.Witness != "" {
return r.Witness
}
return witness.Default(r.Name)
return witness.Default(r.ID, r.Name)
}
// digestWritten is the bundle digest a process's record says was placed: the first half of what it
+1 -1
View File
@@ -185,7 +185,7 @@ func (r Rewritten) declares(id string) bool {
// container runtime's configuration whole — and neither is anything the operator installs later.
var genesisTakes = map[string]bool{
RegistryModule: true, ControlPlaneModule: true, BuilderModule: true,
"postgres": true, BusModule: true, "mesh-vault": true, "mesh-catalog": true,
"postgres": true, BusModule: true, "mesh-vault": true,
}
// takeIfAdopted takes one of genesis's own modules on an adopted node, once it is assigned and
+1 -8
View File
@@ -64,7 +64,6 @@ const (
StepStore Step = "store"
StepBroker Step = "broker"
StepVault Step = "vault"
StepCatalogue Step = "catalogue"
StepNetwork Step = "network"
StepFilter Step = "filter"
StepExtras Step = "extras"
@@ -89,7 +88,7 @@ var Steps = []Step{
// Phase two. The twelve above make a mesh that RUNS; these make one that WORKS — able to
// build, to say what it holds, on its network, filtering. They used to be things somebody
// typed afterwards, which is how they went missing without anything complaining.
StepBase, StepStore, StepBroker, StepVault, StepCatalogue, StepNetwork, StepFilter, StepExtras, StepExport,
StepBase, StepStore, StepBroker, StepVault, StepNetwork, StepFilter, StepExtras, StepExport,
}
// Error is a failure, named by the step it happened in.
@@ -750,12 +749,6 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
return result, failed(StepVault, err)
}
// ---- 15. catalogue --------------------------------------------------------------------
say("catalogue — the module graph: what is held, what a change reaches, what to rebuild")
if err := InstallFromCatalogue(ctx, o, permanentControl, "mesh-catalog", say); err != nil {
return result, failed(StepCatalogue, err)
}
// ---- 16. network ----------------------------------------------------------------------
say("network — the private network, and this machine's name on it")
if err := PlaceOnTheNetwork(ctx, o, permanentControl, rewritten.BrokerAddress, say); err != nil {
+12 -2
View File
@@ -13,14 +13,24 @@ import (
// stopped genesis at step 17: "this mesh holds no copy of it. Build mesh-tools-go first".
// genesisModules is every catalogue module genesis installs on its own, read from the steps that
// install them: the registry, the build agent, the store, the bus, the vault, the catalogue and the
// install them: the registry, the build agent, the store, the bus, the vault and the
// filter (extras are the operator's choice and come after the base too, but are not genesis's own).
var genesisModules = []string{RegistryModule, BuilderModule, "postgres", BusModule, "mesh-vault", "mesh-catalog", "nftables"}
var genesisModules = []string{RegistryModule, BuilderModule, "postgres", BusModule, "mesh-vault", "nftables"}
// Every genesis module's bundles are compiled in a toolchain the base step builds, read from the real
// catalogue's manifests: a module that gains a language genesis does not build fails here, not at a
// genesis.
func TestTheBaseStepBuildsEveryToolchainAGenesisModuleIsCompiledIn(t *testing.T) {
// ADR 0604 section 7: the catalog is the indexer's seat now, so genesis has no catalogue step and installs or takes no
// mesh-catalog module.
for _, step := range Steps {
if step == "catalogue" {
t.Fatal("genesis still has a catalogue step")
}
}
if genesisTakes["mesh-catalog"] {
t.Fatal("an adopted genesis still takes mesh-catalog")
}
built := map[string]bool{}
for _, tc := range genesisToolchains {
built[tc.Language] = true
+2 -2
View File
@@ -316,7 +316,6 @@ func RunsAnyOf(runs *delivery.Composed, module string) bool {
// 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 {
runtimeID := catalogue.RuntimeModule + "." + resolution.RuntimeProcessID()
type resource struct {
ID string `json:"id"`
Module string `json:"module"`
@@ -339,7 +338,8 @@ func ToolsRunning(running []delivery.Composed) map[string][]catalogue.Bundle {
owner, _ = declaration.ModuleGuess(r.ID)
}
switch {
case r.Type == "process" && r.ID == runtimeID && owner == catalogue.RuntimeModule:
case r.Type == "process" && owner != "" && r.ID == owner+"."+resolution.RuntimeProcessID():
// The tool runner's process, by its id alone: `<its module>.runtime` (resolutionRuntimeProcess).
env = r.Env
case r.Type == "archive" && owner != "" && strings.HasPrefix(r.ID, owner+"."+resolution.BundleID("")):
name := strings.TrimPrefix(r.ID, owner+"."+resolution.BundleID(""))
+7 -7
View File
@@ -104,7 +104,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
// indexer's commit has no bundles (shanks's runtime lost the folded systemd-resolved, novox's the folded nats), and a
// held module's new build's bundles are not delivered.
dg := func(c byte) string { return "sha256:" + strings.Repeat(string(c), 64) }
runtime := catalogue.Manifest{Module: catalogue.RuntimeModule, Version: "1",
runtime := catalogue.Manifest{Module: "node-tools", Version: "1", ToolRunner: true,
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/node-tools/broker"}},
Bundles: []catalogue.Bundle{{Name: "runtime", Source: catalogue.ArtifactStoreScheme + "node-tools/runtime/blobs/" + dg('a'),
Digest: dg('a'), Language: "go", Binary: "node-tools"}}}
@@ -125,7 +125,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
if dropped {
env = `"MESH_TOOL_MODULES":""` // the drop already applied (shanks, 2026-10-29)
}
return delivery.Composed{Node: "anchor", Module: catalogue.RuntimeModule, Resources: []json.RawMessage{json.RawMessage(
return delivery.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') +
@@ -155,13 +155,13 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
}, map[string]string{"lamp": fpFor('1')}, false},
} {
tools := aMesh(t)
tools.Catalogue[catalogue.RuntimeModule], tools.Catalogue["artifacts"], tools.Catalogue["lamp"] = runtime, store, lamp
tools.Catalogue["node-tools"], tools.Catalogue["artifacts"], tools.Catalogue["lamp"] = runtime, store, lamp
anchor := nodeOf(t, &tools, "anchor")
anchor.Assigned = append(anchor.Assigned, "artifacts", catalogue.RuntimeModule, "lamp")
tools.BusMinted = []string{"anchor." + catalogue.RuntimeModule}
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)})
row.fps[overlay.Name], row.fps["artifacts"], row.fps[catalogue.RuntimeModule] = fpFor('a'), fpFor('5'), fpFor('e')
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 {
t.Fatalf("%s: %v", row.name, err)
@@ -188,7 +188,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) {
// 1943/AC1: the upgrade policies and sources a composition's value may name are the catalogue's own words.
func TestTheComposedPoliciesAreTheCatalogues(t *testing.T) {
for _, p := range []string{catalogue.PolicyRoll, catalogue.PolicyTogether, catalogue.PolicyRecord} {
for _, from := range []string{catalogue.FromPerson, catalogue.FromModule, catalogue.FromBus, catalogue.FromData, catalogue.FromDefault} {
for _, from := range []string{catalogue.FromPerson, catalogue.FromModule, "the bus", catalogue.FromData, catalogue.FromDefault} { // "the bus": an older reader's word, still read
v := delivery.ComposedDigest{Digest: "sha256:" + strings.Repeat("a", 64), Judgement: delivery.JudgedNeither, Policy: p, PolicyFrom: from}
if _, err := delivery.EncodeComposedDigest(v); err != nil {
t.Errorf("%s from %s: %v", p, from, err)
+2 -2
View File
@@ -386,13 +386,13 @@ type CoreBoxWanted struct {
Name string
}
// coreModulesOf is every module of the plan on the core's list whose own secrets this machine's declaration carries, with their
// coreModulesOf is every module of the plan whose manifest says its own secrets are the core's credentials, whose own secrets this machine's declaration carries, with their
// plain own secret names, sorted: one handed to the provisioner has its secrets from the vault through the client, and one issued
// outside the mesh is never the vault's to seal here.
func coreModulesOf(modules []catalogue.Manifest, handed func(catalogue.Manifest) bool) map[string][]string {
out := map[string][]string{}
for _, m := range modules {
if !resolution.CarriesVaultSealed(m.Module) || (handed != nil && handed(m)) {
if !resolution.CarriesVaultSealed(m) || (handed != nil && handed(m)) {
continue
}
var names []string
+1 -1
View File
@@ -295,7 +295,7 @@ func aValidKey(t *testing.T, name string) string { return keyOf(t, name) }
func TestACoreBoxTheInputHoldsNoneOfIsReturnedToKeepAndNothingIsWritten(t *testing.T) {
mk := func() Input {
in := aMesh(t)
in.Catalogue["nats"] = catalogue.Manifest{Module: "nats", Version: "1",
in.Catalogue["nats"] = catalogue.Manifest{Module: "nats", Version: "1", CoreCredentials: true,
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/nats/broker", Taken: catalogue.TakenAtStart}}}
laptop := nodeOf(t, &in, "laptop")
laptop.Assigned = append(laptop.Assigned, "nats")
+5 -4
View File
@@ -404,11 +404,11 @@ func firstReason(err error, failed map[string]string) error {
return fmt.Errorf("%s; its stand-in is then refused: %w", strings.Join(named, "; "), err)
}
// blames says the composition with m's stub (again) makes m the culprit of err: it composes, or fails otherwise. The
// blames says the composition with m's stub (again) makes m the culprit of err, m the tool runner (runner) or not: it composes, or fails otherwise. The
// runtime's stub is never a runtime (no build is taken in, so it carries no bundle) and always fails otherwise: it is
// the culprit only where its stub composes (issue 774, 2026-10-30 19:30: node-tools held for any error naming it).
func blames(m string, again, err error) bool {
if m == catalogue.RuntimeModule {
func blames(runner bool, again, err error) bool {
if runner {
// Or where its stub fails only for the modules of their own account it would run (issue 774): they are held
// after it, each for its own words, and the node composes the rest.
var none *resolution.NoRuntimeToRunError
@@ -470,7 +470,8 @@ func culprit(in compose.Input, node string, fps map[string]string, mode compose.
}
changes := func(m, leave string) bool {
_, _, _, again := compose.CompositionsIn(stubbed(in, node, m, leave), node, fps, mode)
return blames(m, again, err)
man, known := manifestOn(in, node, m)
return blames(known && catalogue.IsToolRunner(man), again, err)
}
for _, m := range order {
if _, done := failed[m]; done {
+65 -1
View File
@@ -154,6 +154,9 @@ type Scope struct {
// key), so a differing resource shows a value sealed again as such (delivery.CompareComposedWith). Nil shows them
// as differing.
SameValue func(a, b string) (same, known bool)
// Kept is told, in words, each module composed as what runs shows because its manifest does not yet say it (the tool
// runner, the core's credentials; ADR 0601 §4), so the node says it. Nil: said nowhere.
Kept func(module, words string)
}
// InOwn is, of the modules p places on its node, those whose resources the node's own composition it runs
@@ -229,7 +232,16 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
if err != nil {
return compose.Input{}, err
}
return b.finish(ctx, r, r.Build, mesh, s, b.in, refuse, wanted)
in, err := b.finish(ctx, r, r.Build, mesh, s, b.in, refuse, wanted)
if err == nil {
// What runs is kept while a manifest does not yet say what shared code no longer infers (ADR 0601 §4).
for _, k := range keepWhatRuns(&in, s.Node, s.Running) {
if s.Kept != nil {
s.Kept(k.Module, k.Words)
}
}
}
return in, err
}
// base is the part of a node's input that the fingerprints it composes its modules at leave alone (issue 1954): read once
@@ -571,6 +583,58 @@ func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]s
return out, skipped, held, err
}
// ComposingInput is the input the scope's node composes itself from at the builds fps, as Compose reads it (the same read,
// the refusals it holds), with how Compose composes from it (compose.Input.ComposedHow): what a node-engine sends a
// check's sandbox, so the sandbox composes the node through ComposeSent, the path Compose takes (issue 774). Read no
// credential: the sandbox composes no pair value.
func ComposingInput(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (compose.Input, error) {
held := map[string]string{}
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }, fps)
if err != nil {
return compose.Input{}, err
}
c := &compose.ComposedHow{Node: s.Node, Builds: maps.Clone(fps), Held: held}
for _, run := range s.Running {
if _, isHeld := held[run.Module]; isHeld && run.Module != "" {
c.Running = append(c.Running, run)
}
}
in.ComposedHow = c
return in, nil
}
// 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) {
c := in.ComposedHow
if c == nil || c.Node != node {
return nil, false, nil
}
held := maps.Clone(c.Held)
if held == nil {
held = map[string]string{}
}
out, _, _, err := composeEach(in, node, c.Builds, false, c.Running, held, nil)
if err != nil {
return nil, true, err
}
// A module the sandbox held that the node-engine held neither in the read nor composing without the change is the change
// refusing it: the node does not compose, said in that module's words, never passed with the rest composed (issue 774,
// review of the sandbox input).
var refused []string
for _, m := range sortedKeys(held) {
_, was := c.Held[m]
if _, before := c.HeldBefore[m]; !was && !before {
refused = append(refused, m+": "+held[m])
}
}
if len(refused) > 0 {
return nil, true, fmt.Errorf("on %s the change refuses %s", node, strings.Join(refused, "; "))
}
return out, true, nil
}
// Plans is the provisioning plan of each module handed to the provisioner on the scope's node, composed from the bus
// alone (novox/hq ADR 0570 clause 2): the controller's handedPlans over the same input (mesh-control
// cmd/mesh-controller/compose_sdk.go), its rendering in the Foreseeing mode with the mesh's generators. A plan names
+32
View File
@@ -148,6 +148,7 @@ func TestInput(t *testing.T) {
t.Run("a held module's first reason leads", aHeldModulesFirstReasonLeads)
t.Run("a held module of its own account never refuses the node", aHeldModuleOfItsOwnAccountNeverRefusesTheNode)
t.Run("another node not resolved is named", otherNodeNotResolvedIsNamed)
t.Run("what runs is kept while a manifest does not say it", whatRunsIsKeptWhileAManifestDoesNotSayIt)
ctx := context.Background()
xterm, err := os.ReadFile("testdata/catalogue-835bf22b-xterm.module.json")
if err != nil {
@@ -867,3 +868,34 @@ func statesOf(t *testing.T, ctx context.Context, b *bus) mapStates {
}
return out
}
// ADR 0601 §4: the new node agent meets the tool runner's older manifest (no tool-runner) while the node runs its runtime
// process, and the provisioner's older manifest (no core-credentials) while its own secret runs as the vault's box: each is
// 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{
{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"}`)}},
}
input := func(runnerSays bool) compose.Input {
return compose.Input{Nodes: []compose.Node{{Name: "a", Assigned: []string{"app", "prov-y", "runner-x"}}},
Catalogue: map[string]catalogue.Manifest{"runner-x": {Module: "runner-x", ToolRunner: runnerSays}, "prov-y": {Module: "prov-y"},
"app": {Module: "app"}}}
}
in := input(false)
said := keepWhatRuns(&in, "a", running)
if !in.Catalogue["runner-x"].ToolRunner || !in.Catalogue["prov-y"].CoreCredentials || in.Catalogue["app"].ToolRunner ||
in.Catalogue["app"].CoreCredentials || len(said) != 2 || said[0].Module != "prov-y" || said[1].Module != "runner-x" {
t.Errorf("what runs was not kept: %+v, said %q", in.Catalogue, said)
}
in = input(true)
if said := keepWhatRuns(&in, "a", running[:1]); len(said) != 0 {
t.Errorf("a manifest that says it is the tool runner was marked again: %q", said)
}
in = input(false)
if said := keepWhatRuns(&in, "a", nil); len(said) != 0 || in.Catalogue["runner-x"].ToolRunner {
t.Errorf("nothing running marked %q", said)
}
}
+8 -9
View File
@@ -8,7 +8,6 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/compose"
"github.com/novox/mesh-host/internal/compose/resolution"
@@ -100,12 +99,12 @@ func theRuntimeIsBlamedOnlyWhereItsStubComposes(t *testing.T) {
{"lamp", nil, true},
{"lamp", errors.New("other"), true},
{"lamp", errors.New("lamp refuses"), false},
{catalogue.RuntimeModule, nil, true},
{catalogue.RuntimeModule, errors.New("node-tools is assigned to n1 and its build produced 0 bundle(s)"), false},
{catalogue.RuntimeModule, fmt.Errorf("compose: %w", &resolution.NoRuntimeToRunError{Held: true}), true},
{catalogue.RuntimeModule, &resolution.NoRuntimeToRunError{}, false},
{"node-tools", nil, true},
{"node-tools", errors.New("node-tools is assigned to n1 and its build produced 0 bundle(s)"), false},
{"node-tools", fmt.Errorf("compose: %w", &resolution.NoRuntimeToRunError{Held: true}), true},
{"node-tools", &resolution.NoRuntimeToRunError{}, false},
} {
if got := blames(row.module, row.again, err); got != row.blamed {
if got := blames(row.module == "node-tools", row.again, err); got != row.blamed {
t.Errorf("%s with %v: blamed %v, want %v", row.module, row.again, got, row.blamed)
}
}
@@ -117,13 +116,13 @@ func theRuntimeIsBlamedOnlyWhereItsStubComposes(t *testing.T) {
// another build than the trunk's is composed from the build its want names.
func aRunningModuleWaitsForItsBuild(t *testing.T) {
dg := func(c string) string { return "sha256:" + strings.Repeat(c, 64) }
tools := `{"module":"node-tools","version":"1","own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` +
tools := `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` +
`"build":{"artifacts":[{"name":"runtime","kind":"bundle","language":"go","system":"arch","from":"cmd/node-tools"}]}}`
lamp := `{"module":"lamp","version":"1","resources":[{"id":"service","type":"service","unit":"lamp.service","state":"running","boot":"enabled"}]}`
store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"serves":{"artifact-store":{"port":5100}},` +
`"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: catalogue.RuntimeModule, FP: old, Resources: []json.RawMessage{
running := []delivery.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") + `"}`)}}}
@@ -207,7 +206,7 @@ func aHeldModulesFirstReasonLeads(t *testing.T) {
// secrets: the node composes every other module, and the held one keeps the words it was first held for.
func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) {
dg := "sha256:" + strings.Repeat("1", 64)
tools := `{"module":"node-tools","version":"1","own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` +
tools := `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` +
`"build":{"artifacts":[{"name":"runtime","kind":"bundle","language":"go","system":"arch","from":"cmd/node-tools"}]}}`
own := func(name string) string {
return `{"module":"` + name + `","version":"1","runs-as":"` + name + `","secrets-owner":"` + name + `","tools":["status"],` +
+106
View File
@@ -0,0 +1,106 @@
package frombus
import (
"encoding/json"
"fmt"
"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"
)
// **What runs is kept while a manifest does not yet say what shared code no longer infers** (novox/hq ADR 0601 §4). mesh-sdk
// knows the tool runner and the modules whose own secrets are the core's credentials only by what their manifests say
// (`tool-runner`, `core-credentials`), no longer by their names. A module placed on the composing node whose manifest (an
// older build's) does not say so yet, while what the node runs of it shows it (the tool runner's process, `<module>.runtime`;
// an own secret carried as the vault's box, the file's `vault-sealed`), is composed as what runs shows: its tool runner
// process and its sealed secrets alike are kept, never composed away, and the node says so until the manifest says it.
// Known by what runs, never by a module's name.
// 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 {
placed := assignedOn(*in, node)
if len(placed) == 0 || len(running) == 0 {
return nil
}
runner, sealed := "", map[string]bool{}
for _, run := range running {
for _, raw := range run.Resources {
var r struct {
ID string `json:"id"`
Type string `json:"type"`
}
if json.Unmarshal(raw, &r) != nil || r.ID == "" {
continue
}
owner := run.Module
if owner == "" {
owner, _ = declaration.ModuleGuess(r.ID)
}
if owner == "" || !slices.Contains(placed, owner) {
continue
}
if r.Type == "process" && r.ID == owner+"."+resolution.RuntimeProcessID() {
runner = owner
}
if r.Type == "file" {
var fields map[string]json.RawMessage
if json.Unmarshal(raw, &fields) == nil {
if _, ok := fields[catalogue.VaultSealedField]; ok {
sealed[owner] = true
}
}
}
}
}
var said []KeptAsRuns
saysRunner := false
for _, m := range placed {
if man, ok := manifestOn(*in, node, m); ok && catalogue.IsToolRunner(man) {
saysRunner = true
}
}
mark := func(module string, set func(*catalogue.Manifest), words string) {
k := KeptAsRuns{Module: module, Words: words}
for i := range in.Nodes {
if in.Nodes[i].Name != node {
continue
}
if man, ok := in.Nodes[i].Kept[module]; ok {
set(&man)
in.Nodes[i].Kept[module] = man
said = append(said, k)
return
}
}
if man, ok := in.Catalogue[module]; ok {
set(&man)
in.Catalogue[module] = man
said = append(said, k)
}
}
if runner != "" && !saysRunner {
mark(runner, func(m *catalogue.Manifest) { m.ToolRunner = true }, fmt.Sprintf("%s runs the tool runner's process here and its "+
"manifest does not say it is the tool runner (tool-runner): it is composed as the tool runner it runs as, until its manifest says so", runner))
}
for _, m := range slices.Sorted(maps.Keys(sealed)) {
if man, ok := manifestOn(*in, node, m); ok && !catalogue.CarriesCoreCredentials(man) {
mark(m, func(m *catalogue.Manifest) { m.CoreCredentials = true }, fmt.Sprintf("%s carries its own secrets sealed by the vault here "+
"and its manifest does not say they are the core's credentials (core-credentials): they stay sealed, until its manifest says so", m))
}
}
sort.SliceStable(said, func(i, j int) bool { return said[i].Module < said[j].Module })
return said
}
// KeptAsRuns is one module composed as what runs shows, and why, in words (ADR 0601 §4): its have carries the words.
type KeptAsRuns struct {
Module, Words string
}
+1
View File
@@ -66,6 +66,7 @@ func PlansHeldTwice(ctx context.Context, r Reader, mesh Mesh, s Scope) (at, newe
sc.into.Err = err
continue
}
keepWhatRuns(&in, sc.scope.Node, sc.scope.Running) // what runs is kept, as Compose keeps it (ADR 0601 §4)
sc.into.Plans, sc.into.Err = plansHeldOf(in, s.Node, held)
mark("plans at " + sc.name)
}
+23
View File
@@ -73,6 +73,13 @@ type Input struct {
// issue 1126, C6). Used only while OperatorExport is nil.
KeptRuns map[string]map[string]map[string]any
// ComposedHow is how the node-engine that read this input composes its node from it (frombus.Compose): the builds
// it composes each module at, the modules it holds and what runs of each held one. A node-engine answers it with the
// input it sends a check's sandbox (composer.SentInput), so the sandbox composes the node through the same path, a
// module folded in the node's own composition kept as it runs rather than resolved from its manifest (issue 774, the
// check of the bus want: novox's nats). Nil from every other reader and from a node-engine before it.
ComposedHow *ComposedHow
// Issued is the credentials made for the consumers of each provider (inv.SecretsFrom): one row for each pair credential
// that exists, by the provider node it is for. The grants a provider must create are read from these, so a credential not
// made yet has no grant until the send that makes it. An assigner fills it from the facts snapshot's Grants of each
@@ -231,6 +238,22 @@ type Node struct {
RunsTools map[string][]catalogue.Bundle
}
// ComposedHow is how a node-engine composes its node from an Input (Input.ComposedHow).
type ComposedHow struct {
// Node is the node composed.
Node string
// Builds is the build each module is composed at, by module: a module the node runs folded in its own composition
// at a build of the old form has none, and is composed there as it runs (Node.InOwn, Node.RunsOwn).
Builds map[string]string
// 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
// 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
}
// Place is where a node is, as its placement says (inv.Overlay).
type Place struct {
Key string
+1 -1
View File
@@ -168,7 +168,7 @@ func TestWhatASendWouldMakeIsNamedAsForeseenAndNeverInvented(t *testing.T) {
in.Issued[0].ForProvider = ""
in.Catalogue["assistant"] = anAssistant()
in.Catalogue["web"] = aWebModule()
in.Catalogue["nats"] = catalogue.Manifest{Module: "nats", Version: "1",
in.Catalogue["nats"] = catalogue.Manifest{Module: "nats", Version: "1", CoreCredentials: true,
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/nats/broker", Taken: catalogue.TakenAtStart}}}
anchor := nodeOf(t, &in, "anchor")
anchor.Assigned = append(anchor.Assigned, "assistant", "web", "nats")
@@ -2,14 +2,14 @@ package resolution
import (
"encoding/json"
"slices"
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
)
// CarriesVaultSealed says whether module's own secrets are the core's credentials.
func CarriesVaultSealed(module string) bool {
return slices.Contains(mcat.CoreCredentialModules, module)
// CarriesVaultSealed says whether a module's own secrets are the core's credentials, as its manifest says
// (core-credentials): no list of modules is kept here.
func CarriesVaultSealed(m mcat.Manifest) bool {
return mcat.CarriesCoreCredentials(m)
}
// vaultSealedOf is the vault's box for one of module's own secrets, if the rendering was given one.
@@ -1,24 +1,24 @@
package resolution
import (
"reflect"
"testing"
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The core's own credentials are what a module's manifest says (core-credentials, issue 774): no module is on a list.
func TestTheCoresOwnCredentialsAreTheBusTheProvisionerAndItsClient(t *testing.T) {
if want := []string{"mesh-provisioner", "mesh-provisioner-client", "nats"}; !reflect.DeepEqual(mcat.CoreCredentialModules, want) {
t.Fatalf("the core's modules are %v, want %v (mesh-vault's CoreModules is the same list)", mcat.CoreCredentialModules, want)
}
for _, m := range []string{"nats", "mesh-provisioner", "mesh-provisioner-client"} {
if !CarriesVaultSealed(m) {
t.Errorf("%s is not on the list", m)
for raw, core := range map[string]bool{
`{"module":"nats","version":"1","core-credentials":true}`: true,
`{"module":"nats","version":"1"}`: false,
`{"module":"postgres","version":"1"}`: false,
} {
m, err := mcat.Decode([]byte(raw))
if err != nil {
t.Fatal(err)
}
}
for _, m := range []string{"postgres", "mesh-vault", "mesh-controller", ""} {
if CarriesVaultSealed(m) {
t.Errorf("%s is on the list", m)
if CarriesVaultSealed(m) != core {
t.Errorf("%s: carries the core's credentials %v", raw, !core)
}
}
}
+2 -2
View File
@@ -1046,7 +1046,7 @@ func resolutionCompose(r mcat.Resolution, with mcat.Rendering, owner map[string]
return nil, err
}
if process != nil { // none for a held runtime's stand-in: what runs of it is carried
owner[fmt.Sprint(process["id"])] = mcat.RuntimeModule
owner[fmt.Sprint(process["id"])] = fmt.Sprint(process["name"]) // the tool runner's module (resolutionRuntimeProcess)
out = append(out, process)
}
// And a runtime of its own for each module of its own account, after it (novox/hq ADR 0259 §8).
@@ -1308,7 +1308,7 @@ func resolutionOwnSecretOwner(r mcat.Resolution, m mcat.Manifest, name string) s
if o := m.OwnSecrets[name].Owner; o != "" {
return o
}
if r.Account != "" && (m.Module == mcat.RuntimeModule || ToolsBusSecret(m, name)) {
if r.Account != "" && (mcat.IsToolRunner(m) || ToolsBusSecret(m, name)) {
return r.Account
}
return m.SecretsOwner
@@ -113,7 +113,7 @@ func TestTheForgesOwnAddressFollowsThePortTheNodeGaveIt(t *testing.T) {
// The number this node was given for the forge — the one the machine it is about to run on
// already publishes.
out, err := Declaration(r, mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-broker"}},
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-broker"}},
Given: map[string]map[int]int{"gitea": {3000: 2999}},
})
if err != nil {
@@ -129,7 +129,7 @@ func TestTheForgesOwnAddressFollowsThePortTheNodeGaveIt(t *testing.T) {
t.Fatalf("the forge is not published on the port this node gave it: %v", server["ports"])
}
// The forge's own code runs in the node's runtime (novox/hq ADR 0198), given its words there.
runtime := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID())
runtime := fileNamed(out, "node-tools"+"."+RuntimeProcessID())
if runtime == nil {
t.Fatalf("the node's runtime is not in the declaration: %v", ids(out))
}
@@ -168,7 +168,7 @@ func declaredGiteaSsh(t *testing.T, given map[int]int) map[string]any {
givenPorts[k] = v
}
out, err := Declaration(r, mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-broker"}},
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-broker"}},
Ports: map[string]map[int]int{"gitea": givenPorts},
Given: map[string]map[int]int{"gitea": given},
})
+2 -2
View File
@@ -46,9 +46,9 @@ func twoMergeableFiles(app, sidecar []any) mcat.Manifest {
// The indexer's judge's proofs, items 1 to 13 of its list.
func TestTheJudgesProofsOfAComposition(t *testing.T) {
runtimeWith := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "dash": {"token": "sealed-token"}}}
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "dash": {"token": "sealed-token"}}}
processOf := func(out []map[string]any) map[string]any {
return fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID())
return fileNamed(out, "node-tools"+"."+RuntimeProcessID())
}
t.Run("1 a bundle's words resolve per machine into the runtime's tool env", func(t *testing.T) {
+5 -5
View File
@@ -13,13 +13,13 @@ import (
// place the operator-sealed secrets are kept — and no other module is told.
func TestTheHolderOfNodeReadsIsToldWhereItsNodeKeepsASecret(t *testing.T) {
with := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "reads": {"broker": "own"}, "desk": {"broker": "own"}}}
goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1",
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}},
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "reads": {"broker": "own"}, "desk": {"broker": "own"}}}
goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true,
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}},
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go",
System: "arch", From: "cmd/node-tools"}}}}
goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle,
Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
@@ -59,7 +59,7 @@ func TestTheHolderOfNodeReadsIsToldWhereItsNodeKeepsASecret(t *testing.T) {
t.Fatalf("the holder is not told its node's secret places: %v (%v)", err, given)
}
want := []string{"/etc/app/db.secret", "/home/ops/.config/app", "/var/lib/desk", "/var/lib/mesh-reads",
"/var/lib/mesh-vault", "/var/lib/mesh/" + mcat.RuntimeModule}
"/var/lib/mesh-vault", "/var/lib/mesh/" + "node-tools"}
if strings.Join(places, " ") != strings.Join(want, " ") {
t.Errorf("secret places:\n got %v\nwant %v", places, want)
}
+16 -20
View File
@@ -21,12 +21,8 @@ func BundlePath(module, bundle string) string { return mcat.BundleRoot + "/" + m
// a node is sent exactly what it was sent before, bundles included, because a bundle nothing loads
// is bytes nobody reads.
func resolutionRuntimeHere(r mcat.Resolution) bool {
for _, m := range r.Modules {
if m.Module == mcat.RuntimeModule {
return true
}
}
return false
_, here := mcat.ToolRunnerOf(r.Modules)
return here
}
// bundleArchives is one archive per tools bundle of a module — a bundle the runtime LOADS something
@@ -59,13 +55,13 @@ func RuntimeProcessID() string { return "runtime" }
// interpreterFor is how a bundle in a language is run: the program the host's unit starts, with the
// bundle's entrypoint after it. The one thing the composer takes from a language, and said here
// rather than in a manifest because the runtime's process is the mesh's to compose (to-be 38 WP3).
func interpreterFor(language string) (string, error) {
func interpreterFor(module, language string) (string, error) {
switch language {
case "typescript":
return "node", nil
}
return "", fmt.Errorf(
"%s is written in %q, and the mesh knows no interpreter to run a %q bundle with", mcat.RuntimeModule, language, language)
"%s is written in %q, and the mesh knows no interpreter to run a %q bundle with", module, language, language)
}
// runtimeProcess is the one process a machine runs the node's tool runtime as (novox/hq ADR 0175,
@@ -80,7 +76,7 @@ func interpreterFor(language string) (string, error) {
func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[string]any, error) {
var runtime *mcat.Manifest
for i := range r.Modules {
if r.Modules[i].Module == mcat.RuntimeModule {
if mcat.IsToolRunner(r.Modules[i]) {
runtime = &r.Modules[i]
}
}
@@ -93,7 +89,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin
if len(runtime.Bundles) != 1 {
return nil, fmt.Errorf(
"%s is assigned to %s and its build produced %d bundle(s); the runtime is one bundle "+
"the mesh runs, so the module declares exactly one (novox/hq to-be 38)", mcat.RuntimeModule, r.Node, len(runtime.Bundles))
"the mesh runs, so the module declares exactly one (novox/hq to-be 38)", runtime.Module, r.Node, len(runtime.Bundles))
}
bundle := runtime.Bundles[0]
// What runs it (novox/hq ADR 0193): a runtime compiled to a binary runs itself, from its own
@@ -105,9 +101,9 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin
if len(bundle.Entrypoints) != 1 {
return nil, fmt.Errorf(
"%s's bundle %q names %d entrypoint(s); the runtime is run from one, so the module "+
"declares exactly one (novox/hq to-be 38)", mcat.RuntimeModule, bundle.Name, len(bundle.Entrypoints))
"declares exactly one (novox/hq to-be 38)", runtime.Module, bundle.Name, len(bundle.Entrypoints))
}
interpreter, err := interpreterFor(bundle.Language)
interpreter, err := interpreterFor(runtime.Module, bundle.Language)
if err != nil {
return nil, err
}
@@ -117,7 +113,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin
if !declared {
return nil, fmt.Errorf(
"%s declares no own secret named broker, and the node's credential is delivered there: "+
"a module that speaks on the bus declares \"own-secrets\": {\"broker\": <path>}", mcat.RuntimeModule,
"a module that speaks on the bus declares \"own-secrets\": {\"broker\": <path>}", runtime.Module,
)
}
@@ -163,7 +159,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin
}
}
sort.Strings(served)
restartOn = append(restartOn, mcat.RuntimeModule+"."+mcat.NeedID("broker"))
restartOn = append(restartOn, runtime.Module+"."+mcat.NeedID("broker"))
sort.Strings(restartOn)
env := map[string]string{mcat.RuntimeToolModules: strings.Join(served, ","), mcat.RuntimeBrokerFile: credential.Path}
@@ -176,7 +172,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin
env[mcat.RuntimeToolEnv] = string(body)
}
process := map[string]any{
"id": mcat.RuntimeModule + "." + RuntimeProcessID(), "type": "process", "name": mcat.RuntimeModule,
"id": runtime.Module + "." + RuntimeProcessID(), "type": "process", "name": runtime.Module,
"source": bundle.Source, "digest": bundle.Digest,
"run": run,
"env": env,
@@ -193,7 +189,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin
}
// Routed through the artifact store as this network reaches it now, like everything the mesh
// built; refused with the same words when there is no store to route through.
if err := artifactsInto(process, mcat.RuntimeModule, with); err != nil {
if err := artifactsInto(process, runtime.Module, with); err != nil {
return nil, err
}
return process, nil
@@ -229,14 +225,14 @@ func resolutionOwnRuntimes(r mcat.Resolution, with mcat.Rendering) ([]map[string
}
var runtime *mcat.Manifest
for i := range r.Modules {
if r.Modules[i].Module == mcat.RuntimeModule {
if mcat.IsToolRunner(r.Modules[i]) {
runtime = &r.Modules[i]
}
}
if runtime == nil || len(runtime.Bundles) != 1 || runtime.Bundles[0].Binary == "" {
return nil, &NoRuntimeToRunError{Held: runtime != nil && runtime.Stubbed, said: fmt.Sprintf("%s runs as its own "+
"account in a runtime of its own, and %s is not here to run it from: assign %s to %s first (novox/hq ADR 0259)",
own[0].Module, mcat.RuntimeModule, mcat.RuntimeModule, r.Node)}
"account in a runtime of its own, and the tool runner is not here to run it from: assign the tool runner to %s first "+
"(novox/hq ADR 0259)", own[0].Module, r.Node)}
}
program := runtime.Bundles[0]
var out []map[string]any
@@ -314,7 +310,7 @@ func resolutionOwnRuntimes(r mcat.Resolution, with mcat.Rendering) ([]map[string
if yielding != nil {
process["yields"] = yielding
}
if err := artifactsInto(process, mcat.RuntimeModule, with); err != nil {
if err := artifactsInto(process, runtime.Module, with); err != nil {
return nil, err
}
out = append(out, process)
@@ -9,12 +9,12 @@ import (
// The runtime-health is composed onto <module>.own-runtime, in the words the node-engine takes; to an engine
// that does not read health it is not sent, as for every resource.
func TestARuntimeHealthIsComposedOntoTheModulesOwnRuntime(t *testing.T) {
goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1",
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}},
goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true,
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}},
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go",
System: "arch", From: "cmd/node-tools"}}}}
goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle,
Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
@@ -26,7 +26,7 @@ func TestARuntimeHealthIsComposedOntoTheModulesOwnRuntime(t *testing.T) {
"looks": 2.0}
for _, reads := range []bool{true, false} {
with := mcat.Rendering{ArtifactStore: "anchor.internal:5101", ReadsHealth: reads,
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed"}, "keeper": {"broker": "own"}}}
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed"}, "keeper": {"broker": "own"}}}
out, err := Declaration(mcat.Resolution{Node: "anchor", Account: "ops", Modules: []mcat.Manifest{keeper, goRuntime}}, with)
if err != nil {
t.Fatal(err)
@@ -46,7 +46,7 @@ func TestARuntimeHealthIsComposedOntoTheModulesOwnRuntime(t *testing.T) {
h["timeout"] != "10s" || h["looks"] != 2 || h["grace"] != "1m0s" {
t.Errorf("its own runtime's health: %v", own[mcat.HealthField])
}
if machine := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID()); machine[mcat.HealthField] != nil {
if machine := fileNamed(out, "node-tools"+"."+RuntimeProcessID()); machine[mcat.HealthField] != nil {
t.Errorf("the machine's runtime was given the module's health: %v", machine[mcat.HealthField])
}
}
+48 -36
View File
@@ -31,12 +31,12 @@ func aToolsModule(t *testing.T, name string, entrypoints ...string) mcat.Manifes
// loaded, and its broker secret to receive the node's credential in.
func theRuntime(t *testing.T) mcat.Manifest {
t.Helper()
m := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1",
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}},
m := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true,
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}},
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "typescript",
Entrypoints: []string{"src/main.js"}}}}}
resolved, err := m.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle,
Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
@@ -45,7 +45,7 @@ func theRuntime(t *testing.T) mcat.Manifest {
func TestEveryToolsBundleIsDeliveredWhereTheRuntimeRuns(t *testing.T) {
store := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}}}
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}}}
nftables := aToolsModule(t, "nftables", "tools/index.js")
zsh := aToolsModule(t, "zsh", "tools/index.js", "tools/more.js")
@@ -70,7 +70,7 @@ func TestEveryToolsBundleIsDeliveredWhereTheRuntimeRuns(t *testing.T) {
t.Errorf("zsh's tools bundle was not delivered: %v", ids(out))
}
// The runtime's own bundle is run, not loaded: its process delivers it, not an archive.
if fileNamed(out, mcat.RuntimeModule+"."+BundleID("runtime")) != nil {
if fileNamed(out, "node-tools"+"."+BundleID("runtime")) != nil {
t.Error("the runtime's own bundle was delivered as an archive beside its process")
}
})
@@ -86,6 +86,18 @@ func TestEveryToolsBundleIsDeliveredWhereTheRuntimeRuns(t *testing.T) {
t.Errorf("%s was delivered to a machine running no runtime to load it", id)
}
}
// The tool runner is what says it is one, never a name (issue 774): node-tools saying nothing is no runner.
silent := theRuntime(t)
silent.ToolRunner = false
out, err = Declaration(mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{nftables, zsh, silent}}, store)
if err != nil {
t.Fatal(err)
}
for _, id := range ids(out) {
if strings.Contains(id, BundleID("")) || strings.HasSuffix(id, "."+RuntimeProcessID()) {
t.Errorf("%s was composed for a module that does not say it is the tool runner", id)
}
}
})
}
@@ -101,7 +113,7 @@ func ids(out []map[string]any) []string {
// where its credential is, and who the operator is — restarted when any of that changes.
func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) {
with := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}}}
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}}}
nftables := aToolsModule(t, "nftables", "tools/index.js")
// A bundle carrying a daemon beside its tools says which files the runtime loads.
showcase := mcat.Manifest{Module: "showcase", Version: "1", Tools: []string{"greet"},
@@ -118,12 +130,12 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) {
if err != nil {
t.Fatal(err)
}
process := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID())
process := fileNamed(out, "node-tools"+"."+RuntimeProcessID())
if process == nil {
t.Fatalf("no runtime process was composed: %v", ids(out))
}
if process["type"] != "process" || process["name"] != mcat.RuntimeModule || process["digest"] != bundleDigest ||
process["source"] != "http://anchor.internal:5101/v2/"+mcat.RuntimeModule+"/runtime/blobs/"+bundleDigest {
if process["type"] != "process" || process["name"] != "node-tools" || process["digest"] != bundleDigest ||
process["source"] != "http://anchor.internal:5101/v2/"+"node-tools"+"/runtime/blobs/"+bundleDigest {
t.Errorf("the runtime's process is %v", process)
}
if fmt.Sprint(process["run"]) != "[node src/main.js]" {
@@ -134,7 +146,7 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) {
"showcase="+mcat.BundleRoot+"/showcase/code/tools/index.js" {
t.Errorf("the runtime is told to serve %q: every loaded file, by module, and nothing a bundle runs", env[mcat.RuntimeToolModules])
}
if env[mcat.RuntimeBrokerFile] != "/var/lib/mesh/"+mcat.RuntimeModule+"/broker" {
if env[mcat.RuntimeBrokerFile] != "/var/lib/mesh/"+"node-tools"+"/broker" {
t.Errorf("the runtime reads its credential at %q, not where the module's own secret is placed", env[mcat.RuntimeBrokerFile])
}
if env[mcat.RuntimeOperatorAccount] != "ops" || env[mcat.RuntimeOperatorHome] != "/home/ops" || process["user"] != "ops" {
@@ -142,18 +154,18 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) {
}
// The credential the process reads belongs to the account it runs as, or it could not read it
// (to-be 38 WP3); other modules' secrets are left as their manifests say.
if credential := fileNamed(out, mcat.RuntimeModule+"."+mcat.NeedID("broker")); credential == nil || credential["owner"] != "ops" {
if credential := fileNamed(out, "node-tools"+"."+mcat.NeedID("broker")); credential == nil || credential["owner"] != "ops" {
t.Errorf("the runtime's credential is not the account's to read: %v", credential)
}
restarts := fmt.Sprint(process["restart-on"])
for _, want := range []string{"nftables." + BundleID("tools"), "showcase." + BundleID("code"), mcat.RuntimeModule + "." + mcat.NeedID("broker")} {
for _, want := range []string{"nftables." + BundleID("tools"), "showcase." + BundleID("code"), "node-tools" + "." + mcat.NeedID("broker")} {
if !strings.Contains(restarts, want) {
t.Errorf("the runtime is not restarted when %s changes: %s", want, restarts)
}
}
// After every bundle and the credential, so both exist before it starts.
names := ids(out)
if names[len(names)-1] != mcat.RuntimeModule+"."+RuntimeProcessID() {
if names[len(names)-1] != "node-tools"+"."+RuntimeProcessID() {
t.Errorf("the runtime's process is not last: %v", names)
}
@@ -162,7 +174,7 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) {
if err != nil {
t.Fatal(err)
}
process := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID())
process := fileNamed(out, "node-tools"+"."+RuntimeProcessID())
env := process["env"].(map[string]string)
if _, set := env[mcat.RuntimeOperatorAccount]; set {
t.Error("an operator account was named on a machine that has none")
@@ -170,13 +182,13 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) {
if _, set := process["user"]; set {
t.Error("a user was set on a machine with no account")
}
if credential := fileNamed(out, mcat.RuntimeModule+"."+mcat.NeedID("broker")); credential == nil || credential["owner"] != nil {
if credential := fileNamed(out, "node-tools"+"."+mcat.NeedID("broker")); credential == nil || credential["owner"] != nil {
t.Errorf("the runtime's credential was given an owner on a machine with no account: %v", credential)
}
})
t.Run("a runtime module built wrong is refused by name", func(t *testing.T) {
two := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1", OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/b"}},
two := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/b"}},
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "typescript",
Entrypoints: []string{"a.js", "b.js"}}}}}
resolved, err := two.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle,
@@ -195,7 +207,7 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) {
// build from before launchers — so the move needs no flag day.
func TestTheRuntimeStartsTheLauncherWhereTheBuildWroteOne(t *testing.T) {
with := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}}}
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}}}
launched := mcat.Manifest{Module: "dash", Version: "1", Tools: []string{"status"},
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "tools", Kind: mcat.ArtifactBundle, Language: "typescript",
Entrypoints: []string{"tools/index.js"}}}}}
@@ -210,7 +222,7 @@ func TestTheRuntimeStartsTheLauncherWhereTheBuildWroteOne(t *testing.T) {
if err != nil {
t.Fatal(err)
}
env := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID())["env"].(map[string]string)
env := fileNamed(out, "node-tools"+"."+RuntimeProcessID())["env"].(map[string]string)
want := "dash=" + mcat.BundleRoot + "/dash/tools/tools/index.serve.mjs,nftables=" + mcat.BundleRoot + "/nftables/tools/tools/index.js"
if env[mcat.RuntimeToolModules] != want {
t.Errorf("the runtime is told %q, want %q", env[mcat.RuntimeToolModules], want)
@@ -220,13 +232,13 @@ func TestTheRuntimeStartsTheLauncherWhereTheBuildWroteOne(t *testing.T) {
// novox/hq ADR 0193: a runtime compiled to a binary runs itself from its own unpacked bundle.
func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) {
with := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}}}
goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1",
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}},
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}}}
goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true,
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}},
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go",
System: "arch", From: "cmd/node-tools"}}}}
goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle,
Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
@@ -234,7 +246,7 @@ func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) {
if err != nil {
t.Fatal(err)
}
process := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID())
process := fileNamed(out, "node-tools"+"."+RuntimeProcessID())
if fmt.Sprint(process["run"]) != "[./node-tools]" {
t.Errorf("a Go runtime is run as %v, want its own binary", process["run"])
}
@@ -249,13 +261,13 @@ func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) {
// which runs as the operator's account and is given none of its words.
func TestAModuleOfItsOwnAccountIsServedByARuntimeOfItsOwn(t *testing.T) {
with := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "telegram": {"broker": "own"}}}
goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1",
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}},
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "telegram": {"broker": "own"}}}
goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true,
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}},
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go",
System: "arch", From: "cmd/node-tools"}}}}
goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle,
Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
@@ -267,7 +279,7 @@ func TestAModuleOfItsOwnAccountIsServedByARuntimeOfItsOwn(t *testing.T) {
if err != nil {
t.Fatal(err)
}
machine := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID())
machine := fileNamed(out, "node-tools"+"."+RuntimeProcessID())
if served := machine["env"].(map[string]string)[mcat.RuntimeToolModules]; strings.Contains(served, "telegram") || !strings.Contains(served, "nftables") {
t.Errorf("the machine's runtime serves %q", served)
}
@@ -342,13 +354,13 @@ func TestAModuleOfItsOwnAccountIsServedByARuntimeOfItsOwn(t *testing.T) {
// as the account agents run as there — either would hand what it holds back to the accounts it is kept from.
func TestAModuleOfItsOwnAccountIsRefusedTheOperatorsAndTheAgentsAccount(t *testing.T) {
with := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "telegram": {"broker": "own"}}}
goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1",
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}},
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "telegram": {"broker": "own"}}}
goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true,
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}},
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go",
System: "arch", From: "cmd/node-tools"}}}}
goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle,
Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
@@ -368,16 +380,16 @@ func TestAModuleOfItsOwnAccountIsRefusedTheOperatorsAndTheAgentsAccount(t *testi
// files it holds for its consumers, and what its words name — belongs to that account, never the operator's.
func TestWhatAModuleOfItsOwnAccountReadsIsItsAccounts(t *testing.T) {
with := mcat.Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "vault": {"broker": "own"}},
Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "vault": {"broker": "own"}},
Kept: &mcat.KeptExport{Export: 1, OperatorKey: "k"},
Grants: []mcat.Grant{{Provision: "secret", Consumer: "anchor", From: "telegram", Sealed: "sealed-to-anchor",
Values: map[string]any{"name": "telegram"}}}}
goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1",
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}},
goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true,
OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}},
Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go",
System: "arch", From: "cmd/node-tools"}}}}
goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle,
Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
+11 -1
View File
@@ -193,7 +193,8 @@ var defaultSeats = append([]mcat.Seat{
// images and archives, by digest — which is why the provision is the artifact store and not an
// image registry.
{Name: "mesh-artifact-store", Scope: mcat.ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"},
{Name: "mesh-catalog", Scope: mcat.ScopeMesh, Decision: "novox/hq ADR 0121"},
// The catalog (novox/hq ADR 0604): the SDK's entry with its four verbs, so the copies of the table agree.
sdkSeat(mcat.CatalogSeat),
// The delivery's owner (novox/hq ADR 0239): one commit's journey from its pull request's head to every
// machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act.
// What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it.
@@ -649,3 +650,12 @@ func vaultIntake(first map[string]string) map[string]string {
}
return out
}
// sdkSeat is the SDK's entry for a seat, taken whole, so this copy of the table cannot disagree with it.
func sdkSeat(name string) mcat.Seat {
seat, ok := mcat.SeatNamed(name)
if !ok {
panic("the SDK's seat table holds no seat " + name)
}
return seat
}
+20
View File
@@ -1,6 +1,7 @@
package resolution
import (
"reflect"
"strings"
"testing"
@@ -69,4 +70,23 @@ func TestTheHolderIsTheModuleNotTheMachine(t *testing.T) {
if !held || holder.Module != "gitea" {
t.Fatalf("the holder was not told apart from a neighbour: %+v", holder)
}
// ADR 0604 section 1, F3: the seat table written here answers as the SDK's does for the catalog seat, its four
// verbs included, so no two copies of the table disagree.
want, ok := mcat.SeatNamed(mcat.CatalogSeat)
if !ok {
t.Fatal("the SDK holds no catalog seat")
}
var have *mcat.Seat
for i := range defaultSeats {
if defaultSeats[i].Name == mcat.CatalogSeat {
have = &defaultSeats[i]
}
}
if have == nil {
t.Fatal("the table here holds no catalog seat")
}
if !reflect.DeepEqual(*have, want) {
t.Errorf("the table here has %+v, the SDK's is %+v", *have, want)
}
}
+45
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"slices"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
sdk "git.novox.be/novox/mesh-sdk/go/composer"
@@ -205,8 +206,25 @@ func machine(in compose.Input, node, depth string, declarations bool) Machine {
}
return Machine{Composes: true}
}
// Composed as the node-engine composes it, when its input says how (compose.Input.ComposedHow, issue 774): the same
// path, at the same builds, a module folded in its own composition kept as it runs.
if !declarations {
if _, how, err := frombus.ComposeSent(in, node); how {
if err != nil {
return Machine{Reason: err.Error()}
}
return Machine{Composes: true}
}
}
d, problems, err := compose.Compose(in, node)
if err != nil {
// An input from a node-engine before ComposedHow says nothing of what runs of the modules folded in the node's
// own composition: the sandbox cannot compose them as the node does, so it does not judge the node, and names
// it (ADR 0593 §9), never a failure of the change and never a silent pass.
if folded := foldedUnsaid(in, node); len(folded) > 0 && in.ComposedHow == nil {
return Machine{NotJudged: NotJudged + strings.Join(folded, ", ") +
" (its own composition carries them as they run, and the sandbox cannot): " + err.Error()}
}
return Machine{Reason: err.Error()}
}
m := Machine{Composes: len(problems) == 0, Problems: problems, Refusals: d.Refusals()}
@@ -219,6 +237,33 @@ func machine(in compose.Input, node, depth string, declarations bool) Machine {
return m
}
// NotJudged opens the words of a machine the sandbox does not judge (Machine.NotJudged, the contract's field, ADR 0593 §9):
// its node-engine sent an input that does not say how it composes (compose.Input.ComposedHow), and the node runs modules
// folded in its own composition. The machine is then neither composed nor refused; a reader decides by the field.
const NotJudged = "not judged: its node-engine sends no running state for "
// foldedUnsaid is each module node runs folded in its own composition (Node.InOwn) whose manifest names a build: what
// the node composes as it runs, and an input without ComposedHow cannot.
func foldedUnsaid(in compose.Input, node string) []string {
var out []string
for _, n := range in.Nodes {
if n.Name != node {
continue
}
for m, ok := range n.InOwn {
man, known := n.Kept[m]
if !known {
man, known = in.Catalogue[m]
}
if ok && known && compose.NamesBuild(man) {
out = append(out, m)
}
}
}
sort.Strings(out)
return out
}
// pathsOf is every resource of a declaration that names a path, with its owner, group and mode as declared, sorted by
// path.
func pathsOf(d compose.Declared) []Path {
+71
View File
@@ -108,6 +108,77 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
t.Errorf("the sandbox composed n1 %+v; the bus reader declared %+v", got, want)
}
})
// Issue 774 (the check of the bus want, the third of a kind: the sandbox's input differs from what the bus reader
// composes): a node like the bus node, its bus module folded in its own composition at a build of the old form and no
// build published for the module's server artifact, composes in the sandbox as its node-engine composes it, from the
// input that says how (compose.Input.ComposedHow); from an older engine's input, which does not, it is named not
// judged, never failed and never passed in silence.
t.Run("a module folded in the node's own composition", func(t *testing.T) {
bus := `{"module": "bus", "version": "1", "claims": [{"name": "node-files"}], ` +
`"resources": [{"id": "server", "type": "container", "name": "bus-server", "artifact": "server"}], ` +
`"build": {"artifacts": [{"name": "server", "kind": "upstream", "from": "nats:2.11@sha256:` + strings.Repeat("a", 64) + `"}]}}`
man, err := catalogue.Decode([]byte(bus))
if err != nil {
t.Fatal(err)
}
node := compose.Node{Name: "a", Assigned: []string{"bus"}, InOwn: map[string]bool{"bus": true},
RunsOwn: &delivery.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,
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{}}}}, in)
}
older, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := older.Cases[0].Machines["a"]; m.Composes || !strings.HasPrefix(m.NotJudged, NotJudged+"bus") || m.Reason != "" {
t.Errorf("an older engine's input of a node with a folded module: %+v", m)
}
in.ComposedHow = &compose.ComposedHow{Node: "a", Builds: map[string]string{}}
sent, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := sent.Cases[0].Machines["a"]; !m.Composes || m.Reason != "" {
t.Errorf("the input that says how the node composes, its bus folded: %+v", m)
}
// A module the change refuses beside the folded one fails the node in its words, never passed with the rest
// composed (review of the sandbox input); held already where the node-engine composed, it stays held as there.
app, err := catalogue.Decode([]byte(`{"module": "app", "version": "1", ` +
`"resources": [{"id": "env", "type": "file", "path": "/var/lib/app/env", "mode": "0600", "content": "N=${secret:repository}\n"}]}`))
if err != nil {
t.Fatal(err)
}
in.Catalogue["app"] = app
in.Nodes[0].Assigned = []string{"bus", "app"}
refused, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := refused.Cases[0].Machines["a"]; m.Composes || !strings.Contains(m.Reason, "app: ") || !strings.Contains(m.Reason, "repository") {
t.Errorf("a module the change refuses on a node composed as its node-engine does: %+v", m)
}
// Held composing without the change, blamed for a refusal beyond the read (issue 1960's unset names): no refusal of
// the change either (review round 1).
in.ComposedHow.HeldBefore = map[string]string{"app": "its route names are unset"}
before, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := before.Cases[0].Machines["a"]; !m.Composes {
t.Errorf("a module the node holds composing without the change: %+v", m)
}
in.ComposedHow.HeldBefore = nil
in.ComposedHow.Held = map[string]string{"app": "held on the node"}
held, err := Run(request(in))
if err != nil {
t.Fatal(err)
}
if m := held.Cases[0].Machines["a"]; !m.Composes {
t.Errorf("a module the node-engine holds too: %+v", m)
}
})
v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}})
if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" {
t.Fatalf("a request with no machines: %+v, %v", v, err)
+2 -1
View File
@@ -374,8 +374,9 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) {
f := s.modules[module]
h := delivery.Have{V: delivery.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 {
h.Judgement, h.Waits = delivery.Judgement(f.Judgement), f.Waits
h.Judgement, h.Waits, h.BuildAlone = delivery.Judgement(f.Judgement), f.Waits, f.BuildAlone
}
switch {
case f.Phase != "":
+4
View File
@@ -639,6 +639,10 @@ type ModuleFact struct {
// Judgement and Waits are, in phase waiting-yes, the judgement of the move and the composition waiting (ADR 0547 §3).
Judgement string
Waits string
// BuildAlone says, in phase waiting-yes, the move changes the build alone (ADR 0601 §1, delivery.Have.BuildAlone).
BuildAlone bool
// Kept is what the node keeps of the module as it runs, for a manifest that does not say it yet (delivery.Have.Kept).
Kept []string
// Sealed is the values the composition it names carries sealed to this node's key, by slot (delivery.Have.SealedAt).
Sealed delivery.Seals
}
+9 -4
View File
@@ -181,14 +181,19 @@ func (s *Shadow) composeCandidate(ctx context.Context, c *Composer, ask delivery
if err != nil {
return unready(err.Error())
}
laidComps, laidHeld, laidErr := laid.composeShadow(ctx, running, touched, func(scope frombus.Scope) {
laidComps, laidHeld, laidErr := laid.composeShadow(ctx, running, touched, func(scope frombus.Scope, fps map[string]string) {
if !ask.Input {
return
}
in, err := frombus.Input(ctx, reader{c: laid}, laid.env.Mesh, scope)
// Read as the composition reads it, at the builds it composes at, with how it composes (compose.Input.ComposedHow),
// so the sandbox composes this node through the same path (issue 774).
in, err := frombus.ComposingInput(ctx, reader{c: laid}, laid.env.Mesh, scope, fps)
if err != nil {
return
}
// What the node holds composing without the candidate, a module blamed for a refusal among them, is no refusal of
// the change (review round 1).
in.ComposedHow.HeldBefore = maps.Clone(baseHeld)
// As the sandbox reads it (composer.SentInput, the one shape): this node's own modules as taken in from their
// builds, every other module's manifest left to the cases.
if body, err := json.Marshal(composer.SentInput(in, laid.env.Node)); err == nil {
@@ -411,7 +416,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]delivery.Composed, map[string]string, error) {
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) {
node := c.env.Node
c.mu.Lock()
placement, placed := c.placements[node]
@@ -444,7 +449,7 @@ func (c *Composer) composeShadow(ctx context.Context, running []delivery.Compose
c.mu.Unlock()
scope := frombus.Scope{Node: node, KeptAt: kept, InOwn: inOwn, Running: running, SameValue: c.sameValue}
if input != nil {
input(scope)
input(scope, fps)
}
comps, _, held, err := frombus.Compose(ctx, reader{c: c}, c.env.Mesh, scope, fps)
if err != nil {
+10
View File
@@ -2,6 +2,7 @@ package selfcompose
import (
"context"
"encoding/json"
"strings"
"sync/atomic"
"testing"
@@ -10,6 +11,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/compose"
)
// novox/hq ADR 0593 §7: the verb compose-candidate composes a check's candidate on this node in shadow. A candidate that
@@ -105,6 +107,14 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
a.Own.Module != delivery.HaveNodeWord || a.Took > delivery.ComposeAnswerBound.Milliseconds() {
t.Errorf("an accepted candidate answered %+v", a)
}
// Issue 774: the input it sends the sandbox says how it composed the node (the builds), so the sandbox composes it
// through the same path.
sent := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: accepted, Input: true})
var in compose.Input
if err := json.Unmarshal(sent.Input, &in); err != nil || in.ComposedHow == nil || in.ComposedHow.Node != "n1" ||
in.ComposedHow.Builds["lemurs"] != fpNew {
t.Errorf("the input sent for the sandbox does not say how the node composes: %+v, %v", in.ComposedHow, err)
}
r := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: refusedKey})
if r.Verdict != delivery.ComposeCandidateRefused || len(r.Modules) != 1 || r.Modules[0].State != delivery.ModuleRefusedState ||
!strings.Contains(r.Modules[0].Words, "a field the judge refuses") {
+65 -1
View File
@@ -78,6 +78,9 @@ type Env struct {
// only within its own module.
// An error is a have the bus did not answer for, never one absent: the pass is then tried again (BusFailed).
Held func(module string) (slots delivery.Seals, legacy []string, err error)
// Kept is told, after each pass that composed, what the node keeps of each module as it runs because its manifest does
// not yet say it (ADR 0601 §4), by module, so each module's have carries it (agent.KeptNow). Nil tells nobody.
Kept func(kept map[string][]string)
// Fetch reads an archive at a URL in the artifact store: a moved module's templates archive, whose texts its built
// manifest carries (issue 1126). Read once per digest. Nil reads none, and a moved module does not compose.
Fetch func(ctx context.Context, url string) ([]byte, error)
@@ -844,7 +847,18 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
}
// Its own certificate, asked of the vault when a module here asks for one and the record holds none it trusts.
c.certifyIfWanted(ctx, r, own)
scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running}
// What it keeps of a module as it runs, for a manifest that does not say it yet (ADR 0601 §4): said, and carried on the
// module's have, from which the watchman raises its condition.
keptRuns := map[string][]string{}
var keptMu sync.Mutex
scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running, Kept: func(module, words string) {
keptMu.Lock()
defer keptMu.Unlock()
if !slices.Contains(keptRuns[module], words) {
keptRuns[module] = append(keptRuns[module], words)
}
c.sayOnceEach(words)
}}
c.mu.Lock()
c.dataScope = frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn} // the data items are worked out as composed
c.mu.Unlock()
@@ -906,6 +920,11 @@ func (c *Composer) compose(ctx context.Context) []agent.Update {
done := c.waiting("the agent marking each held module's have")
c.refused("", held)
done()
if c.env.Kept != nil {
keptMu.Lock()
c.env.Kept(keptRuns)
keptMu.Unlock()
}
if len(held) == 0 {
c.sayOnce("")
} else {
@@ -1635,3 +1654,48 @@ func Transient(err error) bool {
}
return false
}
// ComposedToday is the digest of module composed at the build fp with today's settings and secrets, as this node would
// compose it now beside its other modules at their wanted builds, the boxes it runs kept as compose keeps them, and whether
// it composed (novox/hq ADR 0601 §1): the node agent compares it with what runs, to say whether a move to another build
// changes the build alone. A module held, a node refused, or no placement: false, and the move is asked of a person.
func (c *Composer) ComposedToday(ctx context.Context, module, fp string) (string, bool) {
c.mu.Lock()
fps, kept := map[string]string{}, map[string]string{}
for m, w := range c.wants {
if m == agent.NodeModule {
continue
}
fps[m] = w.FP
if mm, ok := c.modules[m]; ok && mm.FP2 != w.FP {
kept[m] = w.FP
}
}
fps[module] = fp
delete(kept, module)
if mm, ok := c.modules[module]; !ok || mm.FP2 != fp {
kept[module] = fp
}
own, placed := c.placements[c.env.Node]
inOwn := c.inOwnOf(own)
c.mu.Unlock()
if !placed {
return "", false
}
var running []delivery.Composed
if c.env.Running != nil {
running = c.env.Running()
}
scope := frombus.Scope{Node: c.env.Node, KeptAt: kept, InOwn: inOwn, Running: running}
comps, _, held, err := frombus.Compose(ctx, reader{c: c}, c.env.Mesh, scope, fps)
if err != nil || held[module] != "" {
return "", false
}
for _, comp := range comps {
if comp.Module == module && comp.FP == fp {
dig, err := delivery.ComposedDigestOf(c.keepRunning(comp, running))
return dig, err == nil
}
}
return "", false
}
+25
View File
@@ -268,6 +268,31 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
b.mu.Unlock()
t.Fatalf("handed %v, want %s and %s", got, lemursKey, nodeKey)
}
// ADR 0601 §1: the module composed today at the build it runs, as the agent compares it with what runs. The same inputs give
// the digest handed; a changed input (here its manifest's service, as a changed setting would change a file) another; a
// build that cannot be composed (no done build or manifest of it: held) gives none.
c.mu.Lock()
handedDig := c.handed[lemursKey]
c.mu.Unlock()
if dig, ok := c.ComposedToday(ctx, "lemurs", fp); !ok || dig != handedDig {
t.Errorf("composed today at the build it runs: %s %v, want %s", dig, ok, handedDig)
}
if dig, ok := c.ComposedToday(ctx, "lemurs", delivery.SourcePrefix2+strings.Repeat("9", 64)); ok {
t.Errorf("a build that cannot be composed was composed today: %s", dig)
}
chgJSON := strings.Replace(lemurs, `"state":"running"`, `"state":"stopped"`, 1)
chgDigest := delivery.ManifestDigest([]byte(chgJSON))
chgMan, _ := delivery.EncodeManifest(delivery.Manifest{Module: "lemurs", JSON: chgJSON})
chgKey, _ := delivery.ManifestsKey(chgDigest)
b.mu.Lock()
b.kv[delivery.ManifestsBucket+"/"+chgKey] = chgMan
b.mu.Unlock()
chgMod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: chgDigest, Repository: "novox/x", Commit: "c"})
c.takeModule(agent.Update{Key: "lemurs", Value: chgMod})
if dig, ok := c.ComposedToday(ctx, "lemurs", fp); !ok || dig == handedDig {
t.Errorf("composed today with a changed input: %s %v, want another digest than %s", dig, ok, handedDig)
}
c.takeModule(agent.Update{Key: "lemurs", Value: mod})
// Issue 1126, the switch made safe: a bus not answering for a moment (a reload of its accounts) is tried again and
// refuses nothing; a pass whose run ended refuses nothing either.
+8 -9
View File
@@ -9,6 +9,7 @@ package witness
import (
"time"
"github.com/novox/mesh-host/internal/compose/resolution"
"github.com/novox/mesh-host/internal/link"
)
@@ -33,15 +34,13 @@ const (
GiveUp = 30 * time.Minute
)
// NodeToolsService is the runtime's name on the services protocol, and its instance is this
// machine's node name: what `$SRV.PING.<service>.<node>` asks, so only this machine's runtime can
// answer (mesh-tools node-tools internal/runtime, announce.Service{Name: module, ID: node}).
const NodeToolsService = "node-tools"
// Default is the witness a process is judged by when it names none: the core process the mesh composes on every
// machine, by the name it composes it under, and nothing else.
func Default(process string) string {
if process == NodeToolsService {
// Default is the witness a process is judged by when it names none: the tool runner's process, the core process the mesh
// composes on every machine for the module whose manifest says it is the tool runner (catalogue.IsToolRunner), with the id
// `<its module>.runtime` and named as its module (resolution's runtime process), and nothing else. It is pinged by that
// name on this machine's instance, `$SRV.PING.<name>.<node>`, so only this machine's runtime answers; never known here by
// a module's name (ADR 0601 §4).
func Default(id, process string) string {
if process != "" && id == process+"."+resolution.RuntimeProcessID() {
return ByPing
}
return ByNone
+7 -4
View File
@@ -13,7 +13,7 @@ import (
// The runtime is asked by its own name and this machine's: only this machine's runtime can answer.
func TestTheRuntimeIsAskedByItsNameAndThisMachines(t *testing.T) {
if got := link.PingSubject(NodeToolsService, "anchor"); got != "$SRV.PING.node-tools.anchor" {
if got := link.PingSubject("runner-x", "anchor"); got != "$SRV.PING.runner-x.anchor" {
t.Fatalf("the host asks %s; the runtime answers $SRV.PING.node-tools.<node>", got)
}
}
@@ -21,9 +21,12 @@ func TestTheRuntimeIsAskedByItsNameAndThisMachines(t *testing.T) {
// What is judged by default is the mesh's core process on every machine, by the name it is composed under, and
// nothing else: the controller ended (issue 1126), so its name is judged as any other.
func TestOnlyTheCoreProcessIsJudgedByDefault(t *testing.T) {
for name, want := range map[string]string{"mesh-controller": ByNone, "node-tools": ByPing, "greeter": ByNone} {
if got := Default(name); got != want {
t.Errorf("%s is judged by %q, want %q", name, got, want)
// The tool runner's process by its id and name, whatever its module is called (ADR 0601 §4); a process named so by
// another id, or another process, is not.
for id, want := range map[string]string{"runner-x.runtime": ByPing, "node-tools.runtime": ByNone, "runner-x.server": ByNone,
"mesh-controller.runtime": ByNone} {
if got := Default(id, "runner-x"); got != want {
t.Errorf("%s is judged by %q, want %q", id, got, want)
}
}
if NodeToolsWithin != 60*time.Second || PingWithin != 5*time.Second {