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
46 changed files with 880 additions and 935 deletions
+26 -22
View File
@@ -85,7 +85,7 @@ func (h sessionHaves) Read(ctx context.Context, module string) (agent.Have, bool
return agent.Have{}, false
}
return agent.Have{Module: module, FP: got.FP, Composed: got.Composed, Phase: string(got.Phase), Detail: got.Detail,
Sealed: got.SealedAt, Server: got.Server, Build: got.Build}, true
Sealed: got.SealedAt}, true
}
// ReadErr is Read telling a have absent from one the bus could not read (agent.HavesReadErr).
@@ -103,7 +103,7 @@ func (h sessionHaves) ReadErr(ctx context.Context, module string) (agent.Have, b
return agent.Have{}, false, err
}
return agent.Have{Module: module, FP: got.FP, Composed: got.Composed, Phase: string(got.Phase), Detail: got.Detail,
Sealed: got.SealedAt, Server: got.Server, Build: got.Build}, true, nil
Sealed: got.SealedAt}, true, nil
}
func (h sessionHaves) Write(ctx context.Context, x agent.Have) {
@@ -120,11 +120,11 @@ 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, Server: x.Server})
BuildAlone: x.BuildAlone, Kept: x.Kept, Sealed: x.Sealed})
}
func (h sessionHaves) Removed(ctx context.Context, module string, words ...string) {
h.session.NoteRemoved(ctx, module, "", words...)
func (h sessionHaves) Removed(ctx context.Context, module string) {
h.session.NoteRemoved(ctx, module, "")
}
// shorten keeps a have's detail within what a have value holds: its head, which says what failed, and its tail, which is
@@ -538,7 +538,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
Say: say,
// A failure of the bus not answering for a moment is tried again: 2 s, 5 s, 15 s, 60 s, then every 5 min.
Backoff: link.Backoff,
Apply: func(ctx context.Context, c delivery.Composed, others []delivery.Composed, keep map[string]bool) agent.Result {
Apply: func(ctx context.Context, c delivery.Composed, others []delivery.Composed) agent.Result {
actx, done := applyContext(session.WithLink(ctx))
// While the grants part's list is followed, the controller's list in the bus module's composition is not
// written: the apply keeps the list on the machine (ADR 0547 clause 8, never both).
@@ -547,7 +547,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
if herr != nil {
res = failedWith(herr.Error())
} else {
res = applyComposition(actx, opts, held, others, ag.Placed(scopeName(c)), ag.RemovedByWant(), keep, sched, say)
res = applyComposition(actx, opts, held, others, ag.Placed(scopeName(c)), ag.RemovedByWant(), sched, say)
}
done()
// The mesh's own certificate it carries is kept for this machine's record (ADR 0555 §3).
@@ -564,9 +564,9 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
sayFacts(ctx, ag)
return res
},
Remove: func(ctx context.Context, module string, others []delivery.Composed, keep map[string]bool) agent.Result {
Remove: func(ctx context.Context, module string, others []delivery.Composed) agent.Result {
actx, done := applyContext(session.WithLink(ctx))
res := removeComposition(actx, opts, module, others, ag.Placed(module), keep, sched, say)
res := removeComposition(actx, opts, module, others, ag.Placed(module), sched, say)
done()
afterApply()
sayFacts(ctx, ag)
@@ -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,12 +636,9 @@ 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
env.BusServer = composer.BusServer // the bus server an unwarranted want leaves as it runs (ADR 0594 §2)
// And its agent keeps the bus server as it runs unless a want carries the warrant's mark (ADR 0594 §8).
session.NoteComposes(linked, delivery.ComposesOwn, "it composes its own modules, and applies no composition made elsewhere",
delivery.KeepsBusServer)
session.NoteComposes(linked, delivery.ComposesOwn, "it composes its own modules, and applies no composition made elsewhere")
ag = agent.New(env)
own.set(ag)
current.set(ag)
@@ -695,7 +698,7 @@ func (c *agentNow) get() *agent.Agent {
//
// removed is the modules whose want says removed: in the node's own apply, a resource of any other module goes only when
// its module is among them (issue 774).
func applyComposition(ctx context.Context, opts options, c delivery.Composed, others []delivery.Composed, placed, removed, keep map[string]bool,
func applyComposition(ctx context.Context, opts options, c delivery.Composed, others []delivery.Composed, placed, removed map[string]bool,
sched *apply.Scheduler, say link.Announce) agent.Result {
name := scopeName(c)
if c.Module == "" {
@@ -711,7 +714,7 @@ func applyComposition(ctx context.Context, opts options, c delivery.Composed, ot
if err := refuseEmptyWrites(d); err != nil {
return failedWith(err.Error())
}
scope := apply.Scope{Module: name, Others: resourcesOf(others), Placed: placed, Keep: keep}
scope := apply.Scope{Module: name, Others: resourcesOf(others), Placed: placed}
if name == agent.NodeModule {
scope.RemovedByWant = removed
if scope.RemovedByWant == nil {
@@ -732,13 +735,13 @@ func applyComposition(ctx context.Context, opts options, c delivery.Composed, ot
if got.Probe != nil {
return failedWith(got.Probe.partial().Error())
}
return agent.Result{Kept: got.Outcome.Kept}
return agent.Result{}
}
// removeComposition removes one module's own resources and nothing of another's.
func removeComposition(ctx context.Context, opts options, module string, others []delivery.Composed, placed, keep map[string]bool,
func removeComposition(ctx context.Context, opts options, module string, others []delivery.Composed, placed map[string]bool,
sched *apply.Scheduler, say link.Announce) agent.Result {
scope := apply.Scope{Module: module, Others: resourcesOf(others), Placed: placed, Keep: keep}
scope := apply.Scope{Module: module, Others: resourcesOf(others), Placed: placed}
got := applyModuleHeld(ctx, opts, &declaration.Declaration{Version: declaration.Version}, &scope, sched, say)
switch {
case got.Refused != "":
@@ -746,7 +749,7 @@ func removeComposition(ctx context.Context, opts options, module string, others
case got.Err != nil:
return agent.Result{Failed: true, Detail: shorten(got.Err.Error())}
}
return agent.Result{Kept: got.Outcome.Kept}
return agent.Result{}
}
// busyModules are the modules whose last apply left a container running because it said busy.
@@ -961,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
@@ -1014,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}
}
+101 -62
View File
@@ -657,6 +657,11 @@ func (r *recordingRuntime) run(_ context.Context, name string, args ...string) (
}
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 {
@@ -673,31 +678,31 @@ func (r *recordingRuntime) run(_ context.Context, name string, args ...string) (
return "", nil
}
// moved is each command that removed, made, stopped or restarted the container named.
// 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{"docker rm ", "docker run ", "docker create ", "docker stop ", "docker restart ", "docker kill ",
"docker rename ", "docker start ", "docker container rm ", "docker container rename ", "docker container stop ",
"docker container restart ", "docker container start ", "docker container kill "} {
if strings.HasPrefix(a, verb) && strings.Contains(a, container) {
out = append(out, a)
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
}
// Issue 774, ADR 0594 §2-§4: 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, at a new-form fingerprint and without the warrant's mark, and left as it runs:
// the node agent started whole against a real bus composes the bus module, writes its file and records the server as
// the bus module's, asks the container runtime to remove, make, stop or restart nothing, and its have names the
// fingerprint applied beside the old-form build the server still runs; its own key says it keeps the bus server. The
// operator's warranted want at the same fingerprint then replaces the server once, and the have says it runs that
// fingerprint.
func TestNatsTheBusServerIsKeptUntilAWarrantedWantReplacesIt(t *testing.T) {
// 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
@@ -711,8 +716,8 @@ func TestNatsTheBusServerIsKeptUntilAWarrantedWantReplacesIt(t *testing.T) {
t.Fatal(err)
}
folded, err := declaration.Parse([]byte(`{"declaration":1,"resources":[` +
`{"id":"nats.server-conf","type":"file","path":"` + conf + `","content":"listen: old\n"},` +
`{"id":"nats.server","type":"container","name":"` + server + `","image":"` + image + `","env":{"FOLDED":"1"}}]}`))
`{"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)
}
@@ -728,7 +733,9 @@ func TestNatsTheBusServerIsKeptUntilAWarrantedWantReplacesIt(t *testing.T) {
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})
@@ -739,36 +746,45 @@ func TestNatsTheBusServerIsKeptUntilAWarrantedWantReplacesIt(t *testing.T) {
}
placement, modules, manifests := kvOf(delivery.PlacementBucket), kvOf(delivery.ModulesBucket), kvOf(delivery.ManifestsBucket)
kvOf(selfcompose.ResolvedBucket)
kvOf(delivery.BuilderBuildsBucket)
builds := kvOf(delivery.BuilderBuildsBucket)
kvOf(delivery.PlacementCandidatesBucket)
kvOf(link.PackagesBucket)
man := `{"module":"nats","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}],"resources":[` +
`{"id":"server-conf","type":"file","path":"` + conf + `","mode":"0644","content":"listen: new\n"},` +
`{"id":"server","type":"container","name":"` + server + `","image":"` + image + `"}]}`
digest := delivery.ManifestDigest([]byte(man))
mraw, _ := delivery.EncodeManifest(delivery.Manifest{Module: "nats", JSON: man})
mkey, _ := delivery.ManifestsKey(digest)
fp := fpIt("e")
modv, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/mesh-catalog", Commit: strings.Repeat("3", 40)})
// 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 before the keeper wrote of the folded bus: the old-form build, no fingerprint.
const took = "build-1792674595610176744"
// 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: took, Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
for _, w := range []struct {
kv nats.KeyValue
key string
value []byte
}{{manifests, mkey, mraw}, {modules, "nats", modv}, {b.haves, hkey, rec}, {b.haves, natsHave, oldHave}} {
if _, err := w.kv.Put(w.key, w.value); err != nil {
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(warrant string) {
raw, err := delivery.EncodeWant(delivery.Want{FP: fp, Self: true, Warrant: warrant})
busWant := func(w delivery.Want) {
raw, err := delivery.EncodeWant(w)
if err != nil {
t.Fatal(err)
}
@@ -777,7 +793,34 @@ func TestNatsTheBusServerIsKeptUntilAWarrantedWantReplacesIt(t *testing.T) {
t.Fatal(err)
}
}
busWant("")
// 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 {
@@ -786,16 +829,16 @@ func TestNatsTheBusServerIsKeptUntilAWarrantedWantReplacesIt(t *testing.T) {
}
}
within(t, 10*time.Second, "the bus module's want was not applied", func() bool {
within(t, 10*time.Second, "the bus module's first want was not applied", func() bool {
h := b.have("nats")
got, _ := os.ReadFile(conf)
return h.FP == fp && h.Phase == delivery.PhaseRunning && string(got) == "listen: new\n"
return h.FP == first && h.Phase == delivery.PhaseRunning && string(got) == "listen: new\n"
})
if h := b.have("nats"); h.Server != took {
t.Errorf("the bus's have says its server runs %q, not the build it took over", h.Server)
}
if moved := runtime.moved(server); len(moved) != 0 || runtime.containers[server] != made {
t.Fatalf("an unwarranted want moved the bus server: %v", moved)
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 {
@@ -804,22 +847,18 @@ func TestNatsTheBusServerIsKeptUntilAWarrantedWantReplacesIt(t *testing.T) {
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)
}
// Said with the node's facts, as every start of its agent says them.
b.session.SayFacts(context.Background(), link.Report{Profile: map[string]any{"capabilities": []any{
map[string]any{"name": "container-runtime", "present": true}}}})
within(t, 5*time.Second, "the node's key does not say it keeps the bus server", func() bool {
e, err := b.haves.Get(hkey)
if err != nil {
return false
}
n, err := delivery.DecodeHaveNode(e.Value())
return err == nil && len(n.Keeps) == 1 && n.Keeps[0] == delivery.KeepsBusServer
})
// The operator's warrant at the fingerprint applied: the server is replaced once.
busWant(delivery.WarrantOperator)
within(t, 10*time.Second, "the warranted want did not replace the bus server", func() bool {
return b.have("nats").Server == fp
// 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) {
@@ -827,11 +866,11 @@ func TestNatsTheBusServerIsKeptUntilAWarrantedWantReplacesIt(t *testing.T) {
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 warranted want made the server %d times, its spec still the folded one %v: %v", runs,
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))
}
if h := b.have("nats"); h.FP != fp || h.Phase != delivery.PhaseRunning {
t.Errorf("the bus's have after the warrant: %+v", h)
}
}
+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.20261103075323-959b1575a8c1
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.20261103075323-959b1575a8c1 h1:lW1NzV8w3pLjH9ACAASYgtYVF3XdCQqIvK/BU81K34Y=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103075323-959b1575a8c1/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=
+62 -184
View File
@@ -17,6 +17,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"sort"
"strings"
"sync"
@@ -58,17 +59,15 @@ 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).
Sealed delivery.Seals
// Server is, in the bus module's have, what the bus server runs (delivery.Have.Server, novox/hq ADR 0594 §4): the
// fingerprint whose server this node agent last made, or the old-form build of the server it took over. Empty on
// every other have.
Server string
// Build is the old-form build a have from before the fingerprints names (delivery.Have.Build): read, never written
// by the agent; the server the bus module's first want takes over runs it.
Build string
}
// Haves is the node's have bucket as the agent uses it.
@@ -77,9 +76,8 @@ type Haves interface {
Read(ctx context.Context, module string) (Have, bool)
// Write writes a module's have where it differs from the bucket's.
Write(ctx context.Context, h Have)
// Removed writes the module's have with the phase removed, once, if a key of it exists; words, when given, are its
// detail.
Removed(ctx context.Context, module string, words ...string)
// Removed writes the module's have with the phase removed, once, if a key of it exists.
Removed(ctx context.Context, module string)
}
// Result is how one apply or removal came out.
@@ -89,8 +87,6 @@ type Result struct {
// JetStream API error while the bus reloads), not the module's: the apply is tried again without a new revision.
Transient bool
Detail string
// Kept is the resources of the keep set the apply left as they ran, by id (apply.Report.Kept, novox/hq ADR 0594 §4).
Kept []string
}
// Backup is the answer of the backup asked before a module moves.
@@ -113,16 +109,10 @@ type Env struct {
Haves Haves
// Apply applies one module's composition (scope NodeModule for the node's own), given the compositions the node's
// other modules run so a path, unit or group they hold is not removed or taken. It is called with one module at a
// time. keep is the resources, by id, the apply leaves as they run when they exist (apply.Scope.Keep): the bus
// server's, on a want of the bus module without the warrant's mark (novox/hq ADR 0594 §2); nil keeps nothing.
Apply func(ctx context.Context, c delivery.Composed, others []delivery.Composed, keep map[string]bool) Result
// BusServer is, for a module that provides the mesh's bus, the ids its manifest declares of the bus server's
// resources (catalogue.BusServerOf, novox/hq ADR 0594 §7), empty for any other module; known says whether the
// module's manifest could be read. Nil names no bus module.
BusServer func(ctx context.Context, module string) (ids []string, known bool)
// Remove removes the module's own resources and nothing of another's, and leaves those of keep as they run (the bus
// server's, ADR 0594 §2).
Remove func(ctx context.Context, module string, others []delivery.Composed, keep map[string]bool) Result
// time.
Apply func(ctx context.Context, c delivery.Composed, others []delivery.Composed) Result
// Remove removes the module's own resources and nothing of another's.
Remove func(ctx context.Context, module string, others []delivery.Composed) Result
// Backup backs a module up before a new composition of it is applied; nil backs up nothing.
Backup func(ctx context.Context, module string) Backup
// Kept is the resources the node last applied, by module, from the declaration it kept before the switch (ADR 0539
@@ -169,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
@@ -214,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
@@ -237,9 +234,6 @@ type Agent struct {
again chan againAsk
transient chan struct{}
stopped chan struct{}
// server is, by bus module, what its have says the bus server runs (Have.Server): read once from the have, then
// kept as this agent writes it, so every have written of the module carries it (novox/hq ADR 0594 §4).
server map[string]string
}
type againAsk struct {
@@ -280,7 +274,7 @@ func New(env Env) *Agent {
return &Agent{env: env, wants: map[string]wantNow{}, comps: map[string]compNow{}, running: map[string]delivery.Have{}, applied: map[string]compNow{}, had: map[string]compNow{}, keptData: map[string]bool{},
attempt: map[string]attempt{}, checked: map[string]bool{}, prior: map[string]bool{}, said: map[string]string{}, notComposed: map[string]notComposedMark{},
retry: make(chan []string, 8), reconcile: make(chan []string, 64), failedAgain: make(chan string, 8), tries: map[string]int{}, again: make(chan againAsk, 64),
transient: make(chan struct{}, 1), stopped: make(chan struct{}), server: map[string]string{}}
transient: make(chan struct{}, 1), stopped: make(chan struct{})}
}
// Run follows the wants until ctx ends. The keys there when it starts are taken first, the node's own composition
@@ -591,11 +585,9 @@ 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.Server == "" && h.Module != NodeModule {
h.Server = a.serverOf(ctx, h.Module)
} else if h.Server != "" {
if h.Module != NodeModule {
a.mu.Lock()
a.server[h.Module] = h.Server
h.Kept = a.kept[h.Module]
a.mu.Unlock()
}
if h.Composed == "" && h.Module != NodeModule {
@@ -614,67 +606,34 @@ func (a *Agent) writeHave(ctx context.Context, h Have) {
a.env.Haves.Write(ctx, h)
}
// serverOf is what the module's have says the bus server runs (Have.Server): kept since this agent read or wrote it,
// else read from the have once. Empty for every module but the bus.
//
// A have written before this node agent kept the server names none: the server then runs what that have says the module
// runs, its fingerprint, else its old-form build (the take-over of a server the node's own composition made). Read so on
// the first read, before this agent writes the have, so every have it writes carries it.
func (a *Agent) serverOf(ctx context.Context, module string) string {
// 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()
s, known := a.server[module]
a.mu.Unlock()
if known {
return s
}
if server, _ := a.busKeep(ctx, module); len(server) == 0 {
return "" // not the bus, or not known to be yet: nothing kept, so a later read asks again
}
if h, ok := a.env.Haves.Read(ctx, module); ok {
s = h.Server
if s == "" {
s = h.FP
}
if s == "" {
s = h.Build
var changed []string
for m := range a.kept {
if _, still := kept[m]; !still {
changed = append(changed, m)
}
}
a.mu.Lock()
if _, written := a.server[module]; !written {
a.server[module] = s
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)
}
s = a.server[module]
a.mu.Unlock()
return s
}
// busKeep is, for a module that provides the mesh's bus, the ids of its server's resources (novox/hq ADR 0594 §7) as
// its composition names them (`<module>.<id>`), and true; nil and true for any other module. known is false when the
// module's manifest could not be read: whether it is the bus cannot be told, so nothing of it is applied.
func (a *Agent) busKeep(ctx context.Context, module string) (server map[string]bool, known bool) {
if module == NodeModule || a.env.BusServer == nil {
return nil, true
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)
}
}
ids, known := a.env.BusServer(ctx, module)
if !known || len(ids) == 0 {
return nil, known
}
server = map[string]bool{}
for _, id := range ids {
server[module+"."+id] = true
}
return server, true
}
// serverOwed says a want of the bus module carries the warrant's mark at a fingerprint whose server does not run yet
// (novox/hq ADR 0594 §4): applied once more though its fingerprint and composition are what runs, so the server is
// replaced at the fingerprint already applied.
func (a *Agent) serverOwed(ctx context.Context, module string, w wantNow) bool {
if w.Want.Warrant == "" {
return false
}
server, _ := a.busKeep(ctx, module)
return len(server) > 0 && a.serverOf(ctx, module) != w.Want.FP
}
func (a *Agent) sealedOf(ctx context.Context, module, fp, composed string) delivery.Seals {
@@ -1302,16 +1261,6 @@ func (a *Agent) settleOne(ctx context.Context, module string) {
a.remove(ctx, module, w)
return
}
if _, known := a.busKeep(ctx, module); !known {
// Whether it is the bus, whose server an unwarranted want never replaces (novox/hq ADR 0594 §2), cannot be told:
// nothing of it is applied until its manifest reads.
a.mu.Lock()
a.sayOnce("bus-unknown:"+module, fmt.Sprintf("%s at %s is not applied: its manifest cannot be read, so whether it "+
"is the bus, whose server only a warranted want replaces, cannot be told", module, w.Want.FP))
a.mu.Unlock()
return
}
owed := a.serverOwed(ctx, module, w)
a.mu.Lock()
comp, have := a.compositionFor(module, w.Want.FP)
a.mu.Unlock()
@@ -1346,15 +1295,14 @@ func (a *Agent) settleOne(ctx context.Context, module string) {
if !a.checked[module] {
a.checked[module] = true
a.mu.Unlock()
if !owed && a.baseline(ctx, module, w, comp) {
if a.baseline(ctx, module, w, comp) {
return
}
} else {
a.mu.Unlock()
}
a.mu.Lock()
// What runs is what the want names, and no warranted want owes the bus server's replacement (ADR 0594 §4).
if r, ok := a.running[module]; ok && !owed && r.FP == w.Want.FP && r.Composed == w.Want.Composed && r.Phase != delivery.PhaseRemoved {
if r, ok := a.running[module]; ok && r.FP == w.Want.FP && r.Composed == w.Want.Composed && r.Phase != delivery.PhaseRemoved {
a.mu.Unlock()
return
}
@@ -1505,27 +1453,6 @@ func (a *Agent) inactiveAtStart(ctx context.Context, module string, comp compNow
func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNow, others []delivery.Composed) {
fp, dig := w.Want.FP, w.Want.Composed
// The bus module's server is kept as it runs unless the want carries the warrant's mark (novox/hq ADR 0594 §2, §6):
// the mark alone decides. Its have says what the server runs (§4): before the apply what it ran, the fingerprint
// applied when the apply kept it a fingerprint or a build ago, and the want's fingerprint once a warranted apply
// replaced it.
keep, _ := a.busKeep(ctx, module)
ranServer, madeServer := "", ""
if len(keep) > 0 {
ranServer = a.serverOf(ctx, module)
if ranServer == "" {
ranServer = fp // a bus module this node never ran: its server is made at this fingerprint
}
madeServer = ranServer
a.mu.Lock()
a.server[module] = ranServer
a.mu.Unlock()
if w.Want.Warrant != "" {
keep, madeServer = nil, fp
a.env.Say(fmt.Sprintf("%s at %s carries the warrant %q: its server, running %s, is replaced", module, fp,
w.Want.Warrant, ranServer))
}
}
a.mu.Lock()
_, ranBefore := a.running[module]
ranBefore = ranBefore || a.prior[module]
@@ -1565,7 +1492,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
}
}
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseApplying)})
res := a.env.Apply(ctx, comp.C, others, keep)
res := a.env.Apply(ctx, comp.C, others)
a.mu.Lock()
what := fmt.Sprintf("%s at %s", module, fp)
if module == NodeModule {
@@ -1615,12 +1542,7 @@ func (a *Agent) apply(ctx context.Context, module string, w wantNow, comp compNo
if a.env.Settled != nil {
a.env.Settled(module)
}
if len(keep) > 0 && len(res.Kept) == 0 {
// Nothing of the server was there to keep: the apply made it at this fingerprint (ADR 0594 §4).
madeServer = fp
}
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseRunning), Detail: res.Detail,
Server: madeServer})
a.writeHave(ctx, Have{Module: module, FP: fp, Composed: dig, Phase: string(delivery.PhaseRunning), Detail: res.Detail})
}
func (a *Agent) remove(ctx context.Context, module string, w wantNow) {
@@ -1637,16 +1559,7 @@ func (a *Agent) remove(ctx context.Context, module string, w wantNow) {
}
others := a.heldOut(module)
a.mu.Unlock()
// A removed want of the bus module never removes its server (novox/hq ADR 0594 §2): a removal carries no warrant, and
// removing the server is not built. One whose manifest cannot be read keeps what would be the server's, said.
keep, known := a.busKeep(ctx, module)
if !known {
keep = map[string]bool{}
for _, id := range catalogue.BusServer {
keep[module+"."+id] = true
}
}
res := a.env.Remove(ctx, module, others, keep)
res := a.env.Remove(ctx, module, others)
a.mu.Lock()
if res.Failed {
a.attempt[module] = attempt{FP: "removed", WantRev: w.Rev}
@@ -1666,16 +1579,6 @@ func (a *Agent) remove(ctx context.Context, module string, w wantNow) {
if a.env.Settled != nil {
a.env.Settled(module)
}
if len(res.Kept) > 0 {
words := fmt.Sprintf("removed, but for %s: the bus server stays as it runs until a want carrying the operator's "+
"warrant removes it (novox/hq ADR 0594 §2)", strings.Join(res.Kept, ", "))
if !known {
words += "; its manifest could not be read, so what would be its server was left"
}
a.env.Say(module + " is " + words)
a.env.Haves.Removed(ctx, module, words)
return
}
a.env.Haves.Removed(ctx, module)
}
@@ -1744,13 +1647,6 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
if module == NodeModule || a.env.Policy == nil || a.env.Policy(ctx, module) != catalogue.PolicyRecord || w.Want.Yes == comp.Dig {
return false
}
// For the bus module the judgement covers its server alone (novox/hq ADR 0594 §3): a want with the warrant's mark is
// the operator's warrant, the put's ask after the streams' snapshot or the hand write at the terminal, and stands as
// its yes (ADR 0546 §5); one without it is judged with the server's resources as they run, which it leaves.
server, _ := a.busKeep(ctx, module)
if len(server) > 0 && w.Want.Warrant != "" {
return false
}
a.readKeptData(ctx, module) // the manifest may read now though it did not at the start
a.mu.Lock()
r, ran := a.running[module]
@@ -1784,11 +1680,6 @@ func (a *Agent) waitsForYes(ctx context.Context, module string, w wantNow, comp
}
var j judge.Judged
switch {
case len(server) > 0 && !known:
// What the bus runs cannot be read, and its server is left as it runs whatever it is: nothing to hold.
return false
case len(server) > 0:
j = judge.JudgeMove(&from, withServerAsItRuns(from, comp.C, server))
case known:
j = judge.JudgeMove(&from, comp.C)
case prior:
@@ -1806,38 +1697,25 @@ 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
}
// withServerAsItRuns is c with each resource of server taken as from runs it (novox/hq ADR 0594 §3): what an apply that
// keeps the bus server makes of the machine, so its judgement covers the rest of the module alone. A server resource
// from does not run stays as c declares it: an apply makes one that does not exist.
func withServerAsItRuns(from, c delivery.Composed, server map[string]bool) delivery.Composed {
running := map[string]json.RawMessage{}
for _, raw := range from.Resources {
if id := resourceID(raw); server[id] {
running[id] = raw
// 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
}
}
out := c
out.Resources = make([]json.RawMessage, 0, len(c.Resources))
for _, raw := range c.Resources {
if was, ok := running[resourceID(raw)]; ok {
raw = was
}
out.Resources = append(out.Resources, raw)
if a.env.ComposedToday == nil || prev.FP == "" || prev.Composed == "" || prev.FP == want {
return false
}
return out
}
// resourceID is a composed resource's id, empty when it names none.
func resourceID(raw json.RawMessage) string {
var r struct {
ID string `json:"id"`
}
_ = json.Unmarshal(raw, &r)
return r.ID
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
+38 -186
View File
@@ -17,15 +17,6 @@ import (
const node = "n1"
func mustComposedKey(t *testing.T, module, fp string) string {
t.Helper()
key, err := delivery.ComposedKey(node, module, fp)
if err != nil {
t.Fatal(err)
}
return key
}
func fpOf(c string) string { return delivery.SourcePrefix2 + strings.Repeat(c, 64) }
// A watcher the test writes to; it says Caught once the keys written before the watch began are out.
@@ -65,11 +56,10 @@ func (f *feed) put(key string, value []byte) {
}
type haves struct {
mu sync.Mutex
stored map[string]Have
log []Have
removed []string
removedWords []string
mu sync.Mutex
stored map[string]Have
log []Have
removed []string
// unreadable is each module whose have the bus cannot read now (ReadErr).
unreadable map[string]bool
}
@@ -100,11 +90,10 @@ func (h *haves) Write(_ context.Context, x Have) {
h.stored[x.Module] = x
h.log = append(h.log, x)
}
func (h *haves) Removed(_ context.Context, m string, words ...string) {
func (h *haves) Removed(_ context.Context, m string) {
h.mu.Lock()
defer h.mu.Unlock()
h.removed = append(h.removed, m)
h.removedWords = append(h.removedWords, strings.Join(words, ""))
}
func (h *haves) last(module string) Have {
h.mu.Lock()
@@ -128,10 +117,7 @@ type bed struct {
backup func(module string) Backup
kept map[string][]json.RawMessage
others [][]delivery.Composed
keeps []map[string]bool // what each apply was told to keep as it runs (ADR 0594 §2)
// serverGone is a bus server not on the machine: an apply keeping it keeps nothing and makes it.
serverGone bool
said []string
said []string
// settled is each module Settled was told, and settledRan whether what runs held it then.
settled []string
settledRan []bool
@@ -142,18 +128,10 @@ type bed struct {
func newBed(t *testing.T) *bed {
b := &bed{t: t, wants: newFeed(), comps: newFeed(), haves: &haves{}}
b.env = Env{Node: node, Wants: b.wants, Composed: b.comps, Haves: b.haves,
Apply: func(_ context.Context, c delivery.Composed, others []delivery.Composed, keep map[string]bool) Result {
Apply: func(_ context.Context, c delivery.Composed, others []delivery.Composed) Result {
b.mu.Lock()
defer b.mu.Unlock()
b.others = append(b.others, others)
b.keeps = append(b.keeps, keep)
var kept []string // what of keep was there to keep: all of it, unless the test took the server away
for id := range keep {
if !b.serverGone {
kept = append(kept, id)
}
}
slices.Sort(kept)
name := c.Module
if name == "" {
name = NodeModule
@@ -162,19 +140,13 @@ func newBed(t *testing.T) *bed {
if b.failWith != "" && (b.failTimes == 0 || len(b.applied) <= b.failTimes) {
return Result{Failed: true, Transient: b.transient, Detail: b.failWith}
}
return Result{Kept: kept}
return Result{}
},
Remove: func(_ context.Context, m string, _ []delivery.Composed, keep map[string]bool) Result {
Remove: func(_ context.Context, m string, _ []delivery.Composed) Result {
b.mu.Lock()
defer b.mu.Unlock()
b.removed = append(b.removed, m)
b.keeps = append(b.keeps, keep)
var kept []string
for id := range keep {
kept = append(kept, id)
}
slices.Sort(kept)
return Result{Kept: kept}
return Result{}
},
Backup: func(_ context.Context, m string) Backup {
b.mu.Lock()
@@ -1316,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{}
@@ -1357,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)
@@ -1480,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)
@@ -1536,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
@@ -1546,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)
@@ -1557,153 +1556,6 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
return len(b.appliedList()) == applied+2 && h.Phase == string(delivery.PhaseRunning) && h.FP == fpB &&
r.Phase == string(delivery.PhaseRunning) && r.FP == fpB && r.Composed == rdig
})
// Issue 774, ADR 0594 §2-§4: the bus module's server is kept as it runs on every want without the warrant's mark (a
// new fingerprint whose server recreates, a revision of its user list), never held for a yes, and its have names the
// fingerprint applied and, beside it, what the server runs: at the take-over the old-form build. A warranted want at
// the fingerprint already applied replaces the server once (the warrant is its yes), and the same want again applies
// nothing. A module that is not the bus keeps nothing, marked or not.
t.Run("the bus module", func(t *testing.T) {
b := newBed(t)
b.env.SelfComposed = true
b.env.Policy = func(context.Context, string) string { return catalogue.PolicyRecord }
b.env.BusServer = func(_ context.Context, m string) ([]string, bool) {
if m == "nats" {
return []string{"program-entrypoint", "program-snapshot", "server"}, true
}
return nil, true
}
const took = "build-1792674595610176744"
b.haves.Write(context.Background(), Have{Module: "nats", Build: took, Phase: string(delivery.PhaseRunning)})
stop := b.run()
defer stop()
bus := func(fp, image, users string) (string, []byte, string) {
server, _ := json.Marshal(map[string]any{"id": "nats.server", "type": "container", "module": "nats",
"name": "mesh-broker-nats", "image": "nats@sha256:" + strings.Repeat(image, 64)})
c := delivery.Composed{Node: node, Module: "nats", FP: fp, Resources: []json.RawMessage{server,
file("nats", "server-conf", "listen"), file("nats", "bus-users", users)}}
key, _ := delivery.ComposedKey(node, "nats", fp)
return encodedAs(t, c, key)
}
want := func(module, fp, warrant string) {
key, _ := delivery.WantKey(node, module)
raw, err := delivery.EncodeWant(delivery.Want{FP: fp, Self: true, Warrant: warrant})
if err != nil {
t.Fatal(err)
}
b.wants.put(key, raw)
}
server := map[string]bool{"nats.server": true, "nats.program-snapshot": true, "nats.program-entrypoint": true}
lastKeep := func() map[string]bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.keeps[len(b.keeps)-1]
}
fpA, fpB := fpOf("a"), fpOf("b")
// A have written before the first apply (the node cannot compose yet) keeps the old build's name as what the
// server runs: the writer names no build, so it is read before the first write.
want("nats", fpA, "")
eventually(t, "the bus's want was not taken", func() bool {
b.ag.mu.Lock()
defer b.ag.mu.Unlock()
return b.ag.wants["nats"].Present
})
b.ag.NotComposed(context.Background(), "a stand-in for the bus's secret")
if h := b.haves.last("nats"); h.Build != "" || h.Server != took {
t.Fatalf("a have written before the first apply: %+v", h)
}
b.ag.NotComposed(context.Background(), "")
for i, row := range []struct {
name, fp, image, users, warrant string
gone bool // the server is not on the machine: made, at the want's fingerprint
applies bool
keep map[string]bool
server string
}{
{name: "the take-over of the folded server", fp: fpA, image: "1", users: "u1", applies: true, keep: server, server: took},
{name: "a new fingerprint whose server recreates", fp: fpB, image: "2", users: "u1", applies: true, keep: server, server: took},
{name: "a revision of the user list", fp: fpB, image: "2", users: "u2", applies: true, keep: server, server: took},
{name: "the operator's warrant at the fingerprint applied", fp: fpB, image: "2", users: "u2", warrant: delivery.WarrantOperator,
applies: true, server: fpB},
{name: "the same warrant again", fp: fpB, image: "2", users: "u2", warrant: delivery.WarrantOperator, server: fpB},
{name: "a build after the warrant", fp: fpA, image: "3", users: "u2", applies: true, keep: server, server: fpB},
{name: "a build whose server is not there", fp: fpOf("e"), image: "4", users: "u2", gone: true, applies: true,
keep: server, server: fpOf("e")},
} {
b.mu.Lock()
b.serverGone = row.gone
b.mu.Unlock()
before := len(b.appliedList())
key, raw, dig := bus(row.fp, row.image, row.users)
b.comps.put(key, raw)
want("nats", row.fp, row.warrant)
if row.applies {
eventually(t, row.name+": not applied", func() bool {
h := b.haves.last("nats")
return len(b.appliedList()) == before+1 && h.Composed == dig && h.Phase == string(delivery.PhaseRunning)
})
if got := lastKeep(); !maps.Equal(got, row.keep) {
t.Errorf("%s: the apply kept %v, want %v", row.name, got, row.keep)
}
} else {
time.Sleep(100 * time.Millisecond)
if got := b.appliedList(); len(got) != before {
t.Errorf("%s: applied again: %v", row.name, got)
}
}
if h := b.haves.last("nats"); h.FP != row.fp || h.Server != row.server || h.Waits != "" {
t.Errorf("row %d, %s: the bus's have is %+v, its server %s", i, row.name, h, row.server)
}
}
// Every have written of the bus says what its server runs, applying included: one that did not would lose it.
b.haves.mu.Lock()
for _, h := range b.haves.log[1:] {
if h.Module == "nats" && h.Server == "" {
t.Errorf("a have of the bus names no server: %+v", h)
}
}
b.haves.mu.Unlock()
for _, warrant := range []string{"", delivery.WarrantOperator} {
before := len(b.appliedList())
fp := fpOf("c")
if warrant != "" {
fp = fpOf("d")
}
key, raw, _ := encodedAs(t, delivery.Composed{Node: node, Module: "web", FP: fp,
Resources: []json.RawMessage{file("web", "x", warrant)}}, mustComposedKey(t, "web", fp))
b.comps.put(key, raw)
want("web", fp, warrant)
eventually(t, "web was not applied", func() bool { return len(b.appliedList()) == before+1 })
if got := lastKeep(); got != nil {
t.Errorf("a module that is not the bus kept %v", got)
}
if h := b.haves.last("web"); h.Server != "" {
t.Errorf("web's have names a server: %+v", h)
}
}
time.Sleep(100 * time.Millisecond)
if got := b.appliedList(); len(got) != 8 {
t.Errorf("applied %v, not once for each change", got)
}
// A removed want removes the rest of the bus and never its server: it is kept, said in the journal and in the
// removed have's words.
rkey, _ := delivery.WantKey(node, "nats")
rraw, _ := delivery.EncodeWant(delivery.Want{Removed: true})
b.wants.put(rkey, rraw)
eventually(t, "the bus's removal was not asked", func() bool {
b.haves.mu.Lock()
defer b.haves.mu.Unlock()
return slices.Contains(b.haves.removed, "nats")
})
if got := lastKeep(); !maps.Equal(got, server) {
t.Errorf("the bus's removal kept %v, not its server %v", got, server)
}
b.haves.mu.Lock()
words := b.haves.removedWords[len(b.haves.removedWords)-1]
b.haves.mu.Unlock()
if !strings.Contains(words, "the bus server stays") || !strings.Contains(words, "nats.server") {
t.Errorf("the removed have does not say the server stays: %q", words)
}
})
}
// 1957/AC1 and AC4 (unit; ADR 0568 §1, §4): an engine start takes a module from what the machine shows, not only from the
+4 -112
View File
@@ -120,9 +120,6 @@ type Report struct {
MovedOut []MovedOut `json:"moved-out,omitempty"`
// Left is every socket, pipe or device a move-out directory holds and did not move (ADR 0422 §4).
Left []LeftInPlace `json:"left,omitempty"`
// Kept is every resource the scope keeps (Scope.Keep) that this apply left as it runs, by id (novox/hq ADR 0594
// §2, §4): what says whether the bus server still runs what it ran, or was made by this apply.
Kept []string `json:"kept,omitempty"`
}
// Waiting says whether a container of this apply waits to be replaced because it is busy: what makes the
@@ -146,9 +143,8 @@ func (r Report) Changed() bool {
// (novox/hq issue 224).
// waits busy is a container left running because it said busy (novox/hq ADR 0335), which nothing
// moved either.
// Kept as it runs is a bus server a want without the warrant's mark left alone (novox/hq ADR 0594 §2).
if o.Action != "unchanged" && o.Action != "held" && o.Action != "waiting" && o.Action != heldStill &&
o.Action != waitsBusy && o.Action != keptAsItRuns {
o.Action != waitsBusy {
return true
}
}
@@ -325,11 +321,6 @@ type Scope struct {
// in its own composition has no want, so Placed does not name it (issue 774, review of the re-land, round 1: nats on
// the bus node has none, and its nats.conf was guessed as nats's and removed). Nil guards by Placed alone.
RemovedByWant map[string]bool
// Keep are the resources, by id, the apply leaves as they run when they exist (novox/hq ADR 0594 §2): the bus
// server's container and what restarts it, on a want of the bus module without the warrant's mark. A kept resource
// that exists is neither recreated nor rewritten and is recorded as the module's with what it was last recorded
// reading; one that does not exist is made. Nil keeps nothing.
Keep map[string]bool
}
// placedOwner is the module a want still places on the node that the record belongs to, other than the scope's own:
@@ -419,11 +410,6 @@ func applyScoped(
if err := refuseRootIdentities(d); err != nil {
return report, known, &Error{Resource: "the declaration's identities", Err: err, Done: report}
}
// What of the bus server is kept as it runs, asked of the runtime before anything moves (ADR 0594 §2).
keepNow, err := keptServer(ctx, d, scope, run)
if err != nil {
return report, known, &Error{Resource: "the bus server", Err: err, Done: report}
}
declared := map[string]bool{}
for _, r := range d.Resources {
@@ -552,17 +538,6 @@ func applyScoped(
removeOrphan := func(orphan store.Applied) error {
var action, detail string
var err error
if keepNow[orphan.ID] {
// A removed want of the bus module removes the rest of it and never its server (ADR 0594 §2): only a
// warranted want replaces or removes the server. Its record stays, the module's.
detail := "no longer declared; the bus server is left as it runs: only a want carrying the operator's " +
"warrant removes it (novox/hq ADR 0594 §2)"
report.Outcomes = append(report.Outcomes, Outcome{ID: orphan.ID, Type: orphan.Type, Target: orphan.Target,
Action: keptAsItRuns, Detail: detail})
report.Kept = append(report.Kept, orphan.ID)
log(fmt.Sprintf(" %s %s (%s): %s", keptAsItRuns, orphan.ID, orphan.Target, detail))
return nil
}
if module, held := heldByOthers[orphan.ID]; held && !declared[orphan.ID] {
if module == "" {
module = "another module"
@@ -1090,12 +1065,7 @@ func applyScoped(
if from, handed := handedFiles[resource.Identity()]; handed && was.ID == "" {
previous.Wrote = from.Wrote
}
if o, kept := keepAsItRuns(keepNow, resource, previous); kept {
outcome = o
report.Kept = append(report.Kept, o.ID)
} else {
outcome, err = applyOne(ctx, sys, resource, run, changed, in, previous, unseal, keepFound)
}
outcome, err = applyOne(ctx, sys, resource, run, changed, in, previous, unseal, keepFound)
}
if err != nil {
// **What was found is kept whatever the apply then did** (novox/hq ADR 0118). The
@@ -1302,7 +1272,7 @@ func applyScoped(
if outcome.busy != nil {
report.Busy = append(report.Busy, *outcome.busy)
}
if outcome.Action == heldStill || outcome.Action == waitsBusy || outcome.Action == keptAsItRuns {
if outcome.Action == heldStill || outcome.Action == waitsBusy {
// Not changed: nothing about it moved, so nothing that restarts on it restarts.
log(fmt.Sprintf(" %s %s (%s): %s", outcome.Action, outcome.ID, outcome.Target, outcome.Detail))
} else if moved(outcome) {
@@ -1459,8 +1429,7 @@ func recordApplied(known *store.State, origin string, resource declaration.Resou
// moved reports whether applying a resource moved it, so what restarts or reloads on it does: anything but
// unchanged, and not a container held still or waiting for its busy one to say idle.
func moved(outcome Outcome) bool {
return outcome.Action != "unchanged" && outcome.Action != heldStill && outcome.Action != waitsBusy &&
outcome.Action != keptAsItRuns
return outcome.Action != "unchanged" && outcome.Action != heldStill && outcome.Action != waitsBusy
}
// afterWhatTheyRead is the resources in declared order, except that **a service comes after every
@@ -3050,83 +3019,6 @@ func applyNetwork(ctx context.Context, r *declaration.Network, run Runner) (Outc
return out, nil
}
// keptAsItRuns is the action of a resource the scope keeps (Scope.Keep, novox/hq ADR 0594 §2), found on the machine
// and left as it runs: the bus server on a want of the bus module without the warrant's mark. Its own word, beside
// heldStill: nothing about it moved, so nothing that restarts on it restarts, and a person reading the apply sees why
// a changed server was not replaced.
const keptAsItRuns = "kept-as-it-runs"
// keepAsItRuns is the outcome of a resource the scope keeps that exists on the machine, true; false for any other,
// which is applied as always. A container exists when the runtime knows it by its name; an archive when it was
// unpacked at its path before. The outcome carries what the resource was last recorded reading and holding, so its
// record is the module's from here on (taken over from the node's own composition at the bus module's first want) and
// a later warranted apply still sees what changed under it.
func keepAsItRuns(keep map[string]bool, r declaration.Resource, previous store.Applied) (Outcome, bool) {
if !keep[r.Identity()] {
return Outcome{}, false
}
out := begin(r)
out.Action = keptAsItRuns
why := "the bus server is left as it runs: only a want carrying the operator's warrant replaces it (novox/hq ADR 0594 §2)"
switch res := r.(type) {
case *declaration.Container:
if res.Schedule != "" || res.RunOnce {
return Outcome{}, false
}
// Found running by keptServer before the loop: it is left whatever its declaration says now.
out.reads = previous.Reads
out.busySince = previous.BusySince
out.Detail = why
return out, true
case *declaration.Archive:
if previous.Wrote == "" || previous.Target != res.Path {
return Outcome{}, false
}
if _, err := os.Stat(res.Path); err != nil {
return Outcome{}, false
}
out.wrote = previous.Wrote
out.unpacked = previous.Unpacked
if previous.Wrote != res.Digest {
out.Detail = why + "; the declared " + res.Digest + " waits for the warrant"
} else {
out.Detail = why
}
return out, true
}
return Outcome{}, false
}
// keptServer is what of the scope's Keep this apply keeps (novox/hq ADR 0594 §2), decided once before anything is
// applied, as the archives that restart the server come before it: every kept container the declaration names is
// asked of the runtime once. All found: the Keep stands. One the runtime says is not there: nothing is kept, so the
// server is made at what the declaration says now and its archives are applied with it. A runtime that does not
// answer, or answers anything else, is an error, never taken as an absent server: replacing a running bus server
// because one inspect failed is the harm the Keep exists to prevent.
func keptServer(ctx context.Context, d *declaration.Declaration, scope *Scope, run Runner) (map[string]bool, error) {
if scope == nil || len(scope.Keep) == 0 {
return nil, nil
}
for _, r := range d.Resources {
c, ok := r.(*declaration.Container)
if !ok || !scope.Keep[r.Identity()] || c.Schedule != "" || c.RunOnce {
continue
}
cri, err := containerRuntime(ctx, run)
if err != nil {
return nil, fmt.Errorf("%w, so whether the bus server %s runs cannot be told; nothing was applied", err, c.Name)
}
if _, err := run(ctx, cri, "container", "inspect", "--format", "{{.State.Running}}", c.Name); err != nil {
if absent(err) {
return nil, nil // made, at what the declaration says now
}
return nil, fmt.Errorf("the container runtime did not say whether the bus server %s runs (%v); nothing was "+
"applied, so it is not replaced on an unanswered look", c.Name, err)
}
}
return scope.Keep, nil
}
// heldStill is the action of a container left alone because a maintenance window holds it stopped
// (novox/hq issue 224). Its own word rather than "unchanged": a person reading a machine that looks
// half-stopped needs to see why, and "unchanged" beside a stopped server says the opposite.
-146
View File
@@ -2,10 +2,7 @@ package apply
import (
"context"
"errors"
"os"
"path/filepath"
"slices"
"strings"
"testing"
@@ -222,149 +219,6 @@ func TestAResourceThatMovedToItsModulesCompositionIsLeftByTheNodesOwnApply(t *te
}
})
}
// Issue 774, ADR 0594 §2: the bus module's first own apply takes over the server the node's own composition made
// while the bus was folded. Kept (a want without the warrant's mark), the server's container and its program archive
// are left as they run, whatever the build changed in them, and become nats's; its files and the tools' account file
// are written. Not kept, the same apply replaces the container.
// Review of 774, round 1: the keep is decided once before anything moves, by the runtime's answer for the server. A
// server not there is made and its archives applied with it, none said kept; a runtime that does not answer fails the
// apply, nothing written and nothing replaced; a removal of the bus removes the rest and leaves the server.
for _, tc := range []struct {
name string
keep bool
gone bool // the server is not on the machine
mute bool // the runtime does not answer the server's inspect
remove bool // the bus module's want is removed
}{
{name: "kept", keep: true},
{name: "not kept"},
{name: "kept, the server not there", keep: true, gone: true},
{name: "kept, the runtime does not answer", keep: true, mute: true},
{name: "kept, the bus removed", keep: true, remove: true},
} {
t.Run("the bus server "+tc.name, func(t *testing.T) {
keep := tc.keep && !tc.gone
dir := t.TempDir()
conf, account, program := filepath.Join(dir, "nats.conf"), filepath.Join(dir, "tools", "account"), filepath.Join(dir, "program")
bus := func(env, confBody, programBody, module string) *declaration.Declaration {
archive, _ := archiveDecl(t, "nats.program-snapshot", program, map[string]string{"snapshot": programBody}, "")
if module != "" {
archive = strings.Replace(archive, `"id":"nats.program-snapshot"`, `"id":"nats.program-snapshot","module":"nats"`, 1)
module = `,"module":"nats"`
}
return declare(t, archive+`,
{"id":"nats.server-conf","type":"file","path":"`+conf+`","content":"`+confBody+`"`+module+`},
{"id":"nats.server","type":"container","name":"mesh-broker-nats","image":"`+pinned+`","env":{"V":"`+env+
`"},"restart-on":["nats.program-snapshot","nats.server-conf"]`+module+`}`)
}
m := &machine{containers: map[string]*fakeContainer{}}
_, state, err := ApplyModule(context.Background(), archHost(t), bus("1", "a", "one", ""), Scope{Module: store.NodeModule},
state0(), store.OriginDeclared, m.run, nil, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
made := m.containers["mesh-broker-nats"]
if made == nil {
t.Fatalf("the node's own made no server: %v", m.asked)
}
if tc.gone {
delete(m.containers, "mesh-broker-nats")
}
m.asked = nil
run := m.run
if tc.mute {
run = func(ctx context.Context, name string, args ...string) (string, error) {
if name == "docker" && len(args) > 1 && args[0] == "container" && args[1] == "inspect" {
m.asked = append(m.asked, "docker "+strings.Join(args, " "))
return "", errors.New("Cannot connect to the Docker daemon at unix:///var/run/docker.sock")
}
return m.run(ctx, name, args...)
}
}
next := bus("2", "b", "two", "nats")
next.Resources = append(next.Resources, declare(t, `{"id":"nats.tools-account","type":"file","path":"`+account+
`","content":"tools","module":"nats"}`).Resources...)
scope := Scope{Module: "nats", Placed: map[string]bool{"nats": true}}
if tc.keep {
scope.Keep = map[string]bool{"nats.server": true, "nats.program-snapshot": true}
}
moved := func() []string {
var out []string
for _, a := range m.asked {
for _, verb := range []string{"docker rm", "docker run", "docker create", "docker stop", "docker restart", "docker rename"} {
if strings.HasPrefix(a, verb+" ") {
out = append(out, a)
}
}
}
return out
}
report, after, err := ApplyModule(context.Background(), archHost(t), next, scope, state,
store.OriginDeclared, run, nil, nil, nil, nil)
if tc.mute {
if err == nil || !strings.Contains(err.Error(), "did not say whether the bus server") {
t.Fatalf("an unanswered inspect did not fail the apply: %v", err)
}
if got, _ := os.ReadFile(conf); string(got) != "a" || len(moved()) > 0 {
t.Errorf("an apply that could not tell whether the server runs wrote %q and moved %v", got, moved())
}
return
}
if err != nil {
t.Fatal(err)
}
if keep == (len(moved()) > 0) {
t.Fatalf("kept %v, and the server was moved by %v", keep, moved())
}
if got, _ := os.ReadFile(conf); string(got) != "b" {
t.Errorf("the server's file holds %q", got)
}
if got, _ := os.ReadFile(account); string(got) != "tools" {
t.Errorf("the tools' account file holds %q", got)
}
if got, _ := os.ReadFile(filepath.Join(program, "snapshot")); keep != (string(got) == "one") {
t.Errorf("kept %v, and the server's program is %q", keep, got)
}
var kept []string
for _, id := range []string{"nats.program-snapshot", "nats.server"} {
rec, ok := after.Find(id)
if !ok || rec.Module != "nats" {
t.Errorf("%s is recorded %+v, not as nats's", id, rec)
}
if o := outcomeOf(report, id); keep != (o.Action == keptAsItRuns) {
t.Errorf("kept %v, and %s was %s: %s", keep, id, o.Action, o.Detail)
}
if keep {
kept = append(kept, id)
}
}
if !slices.Equal(report.Kept, kept) && !(len(kept) == 0 && len(report.Kept) == 0) {
t.Errorf("the report says it kept %v, want %v", report.Kept, kept)
}
if tc.keep && report.Changed() == false {
t.Error("an apply that wrote the server's files says nothing changed")
}
if !tc.remove {
return
}
m.asked = nil
gone, last, err := ApplyModule(context.Background(), archHost(t), &declaration.Declaration{}, scope, after,
store.OriginDeclared, run, nil, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
if len(moved()) > 0 || m.containers["mesh-broker-nats"] == nil || !exists(filepath.Join(program, "snapshot")) {
t.Errorf("the bus's removal removed its server: %v", moved())
}
if exists(conf) || exists(account) {
t.Errorf("the bus's removal left the rest of it: conf %v, account %v", exists(conf), exists(account))
}
if _, ok := last.Find("nats.server"); !ok || !slices.Equal(gone.Kept, []string{"nats.program-snapshot", "nats.server"}) &&
!slices.Equal(gone.Kept, []string{"nats.server", "nats.program-snapshot"}) {
t.Errorf("the removal kept %v, the server recorded %v", gone.Kept, ok)
}
})
}
}
func state0() store.State { return store.State{} }
+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)
+7 -30
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"sync"
@@ -56,11 +55,6 @@ type haveState struct {
// nodeComposes and nodeComposesWhy are whose compositions the node applies and why (HaveNode.Composes), written into
// its key.
nodeComposes, nodeComposesWhy string
// nodeKeeps is what the node agent keeps as it runs on a want without the warrant's mark (HaveNode.Keeps, novox/hq
// ADR 0594 §8), written into its key.
nodeKeeps []string
// removedWords is, by module in removed, what its removed have says when it is more than that it was removed.
removedWords map[string]string
// written is the build of each module this node-engine wrote a have key for and that the declaration it applied
// still holds; removed is each such module the latest declaration no longer holds, with the build its key last
// said, until the key is written once with the phase removed (issue 1369, novox/hq ADR 0408 §4: never deleted).
@@ -380,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 != "":
@@ -390,7 +385,6 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) {
h.Phase = delivery.PhaseFailed
}
h.SealedAt = f.Sealed
h.Server = f.Server
h.Health = s.healthOf(module, f)
if h.Detail == "" {
h.Detail = s.wordsOf(module, h.Health)
@@ -434,7 +428,6 @@ func (q *Session) writeHaves(ctx context.Context, bus Bus) {
facts.SealedAt = s.nodeSealed
facts.Candidates = s.nodeCandidates
facts.Composes, facts.ComposesWhy = s.nodeComposes, s.nodeComposesWhy
facts.Keeps = s.nodeKeeps
if raw, err = delivery.EncodeHaveNode(facts); err == nil {
want := facts
if !q.putHave(write, bucket, key, raw, func(current []byte) bool {
@@ -510,9 +503,6 @@ func (q *Session) writeRemoved(ctx context.Context, b HaveBucket, node string) b
}
h := delivery.Have{V: delivery.Version, Build: s.removed[module], Phase: delivery.PhaseRemoved,
Health: delivery.HealthUnknown, Detail: "the applied declaration no longer holds the module"}
if words := s.removedWords[module]; words != "" {
h.Detail = words
}
raw, err := delivery.EncodeHave(h)
if err != nil {
q.sayHave(fmt.Sprintf("cannot encode %s: %v", key, err))
@@ -704,23 +694,13 @@ func (q *Session) NoteUserList(ctx context.Context, module string, w delivery.Us
// NoteRemoved takes that a module's own resources were removed, and writes its have with the phase removed once (issue
// 1369): the key is never deleted.
//
// words, when given, are the removed have's detail (the bus server left as it runs, novox/hq ADR 0594 §2).
func (q *Session) NoteRemoved(ctx context.Context, module, build string, words ...string) {
func (q *Session) NoteRemoved(ctx context.Context, module, build string) {
q.init()
s := &q.haves
s.mu.Lock()
if s.removed == nil {
s.removed = map[string]string{}
}
if said := strings.Join(words, ""); said != "" {
if s.removedWords == nil {
s.removedWords = map[string]string{}
}
s.removedWords[module] = said
} else {
delete(s.removedWords, module)
}
if f, ok := s.modules[module]; ok && build == "" {
build = f.Build
}
@@ -777,17 +757,14 @@ func (q *Session) NoteCandidates(ctx context.Context, answers map[string]deliver
q.writeHaves(ctx, bus)
}
// NoteComposes says, in the node's own key, whose compositions the node applies (delivery.ComposesOwn) and why, and what
// its agent keeps as it runs on a want without the warrant's mark (delivery.KeepsBusServer, novox/hq ADR 0594 §8): written
// at each start of its agent, where every have is read (issue 1126). The assigner writes the bus module's want only to
// a node whose key says it keeps the bus server.
func (q *Session) NoteComposes(ctx context.Context, composes, why string, keeps ...string) {
// NoteComposes says, in the node's own key, whose compositions the node applies (delivery.ComposesOwn) and why: written
// at each start of its agent, where every have is read (issue 1126).
func (q *Session) NoteComposes(ctx context.Context, composes, why string) {
q.init()
s := &q.haves
s.mu.Lock()
changed := s.nodeComposes != composes || s.nodeComposesWhy != why || !slices.Equal(s.nodeKeeps, keeps)
changed := s.nodeComposes != composes || s.nodeComposesWhy != why
s.nodeComposes, s.nodeComposesWhy = composes, why
s.nodeKeeps = slices.Clone(keeps)
known := s.node != nil
s.mu.Unlock()
if !known || !changed {
-18
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"sync"
"testing"
@@ -580,23 +579,6 @@ func TestAnAppliedModulesHaveNamesTheFingerprintAndTheDigestAndFollowsItsHealth(
if n, _ := delivery.DecodeHaveNode(l.kv["n1.node"].value); n.Candidates != nil {
t.Fatalf("an answer left after its group went: %+v", n.Candidates)
}
// Issue 774, ADR 0594 §4, §8: the bus module's have names what its server runs beside the fingerprint applied, and the
// node's key says its agent keeps the bus server.
q.NoteModule(context.Background(), ModuleFact{Module: "nats", FP: fp, Composed: dig, Server: "build-1792674595610176744"})
if got := l.have(t, "n1.nats"); got.FP != fp || got.Server != "build-1792674595610176744" {
t.Fatalf("the bus's have: %+v", got)
}
// Its removal leaves the server, and its removed have says so (review of 774, round 1).
q.NoteRemoved(context.Background(), "nats", "", "removed, but for nats.server: the bus server stays as it runs")
if got := l.have(t, "n1.nats"); got.Phase != delivery.PhaseRemoved || !strings.Contains(got.Detail, "the bus server stays") {
t.Fatalf("the bus's removed have: %+v", got)
}
q.NoteModule(context.Background(), ModuleFact{Module: "nats", FP: fp, Composed: dig, Server: "build-1792674595610176744"})
q.NoteComposes(context.Background(), delivery.ComposesOwn, "it composes its own", delivery.KeepsBusServer)
if n, err := delivery.DecodeHaveNode(l.kv["n1.node"].value); err != nil || !slices.Equal(n.Keeps, []string{delivery.KeepsBusServer}) ||
n.Composes != delivery.ComposesOwn {
t.Fatalf("the node key does not say it keeps the bus server: %+v %v", n, err)
}
}
// A module applied and running that has nothing long-running to judge is ok once the engine has looked: there is nothing
+4 -2
View File
@@ -639,8 +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
// Server is, for the bus module, what its server runs (delivery.Have.Server, novox/hq ADR 0594 §4).
Server string
}
+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 -25
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 {
@@ -1163,30 +1182,6 @@ func (c *Composer) Policy(ctx context.Context, module string) string {
return policy
}
// BusServer is, for a module whose manifest provides the mesh's bus, the ids it declares of the bus server's resources
// (catalogue.BusServerOf, novox/hq ADR 0594 §7), none for any other; known says whether the manifest could be read.
func (c *Composer) BusServer(ctx context.Context, module string) (ids []string, known bool) {
c.mu.Lock()
m, ok := c.modules[module]
c.mu.Unlock()
if !ok || m.Manifest == "" {
return nil, false
}
raw, err := reader{c: c}.Manifest(ctx, m.Manifest)
if err != nil {
return nil, false
}
parsed, held, err := c.taken(module, m, raw)
if err != nil || held != "" {
return nil, false
}
for id := range catalogue.BusServerOf(parsed) {
ids = append(ids, id)
}
sort.Strings(ids)
return ids, true
}
// HoldsData says whether a module's manifest declares data (catalogue.Manifest.DataItems), as a composition's Data says
// it, and whether the manifest could be read: the agent's Data for what runs of a module from the kept declaration
// (agent.Env.HoldsData, issue 924).
@@ -1659,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 {