diff --git a/cmd/mesh-host/nodeagent_integration_test.go b/cmd/mesh-host/nodeagent_integration_test.go index 933bbaac..be40a99a 100644 --- a/cmd/mesh-host/nodeagent_integration_test.go +++ b/cmd/mesh-host/nodeagent_integration_test.go @@ -490,7 +490,7 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { } return kv } - assignment, modules, manifests := kvOf(buckets.Assignments), kvOf(buckets.Modules), kvOf(buckets.Manifests) + assignment, manifests := kvOf(buckets.Assignments), kvOf(buckets.Manifests) resolved := kvOf(selfcompose.ResolvedBucket) candidates := kvOf(buckets.ProposalCandidates) packages := kvOf(link.PackagesBucket) @@ -502,7 +502,6 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { mraw, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "web", Manifest: man}) mkey, _ := meshcatalogclient.ManifestsKey(digest) fp := fpIt("a") - modv, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/web", Commit: strings.Repeat("1", 40)}) place, _ := meshmoduleassignerclient.EncodeAssignments(meshmoduleassigner.Assignments{Node: itNode, Modules: []meshmoduleassigner.PlacedModule{{Module: "web"}}}) rec, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: itNode, Capabilities: []string{"service-manager", "package-manager"}}}) // it1 in one key per module and node (research 072 row 37), it2 in the form before: a node reads both. @@ -516,7 +515,6 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { mdigest := meshcatalogclient.ManifestDigest([]byte(mailer)) mlraw, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "mailer", Manifest: mailer}) mlkey, _ := meshcatalogclient.ManifestsKey(mdigest) - mlmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fpIt("b"), Manifest: mdigest, Repository: "novox/mailer", Commit: strings.Repeat("2", 40)}) places2, _ := meshmoduleassignerclient.AssignmentsValues(meshmoduleassigner.Assignments{Node: "it2", Modules: []meshmoduleassigner.PlacedModule{{Module: "mailer"}}}) rec2, _ := nec.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "it2", Capabilities: []string{"service-manager"}}}) pkey2, _ := meshmoduleassignerclient.NodeKey("it2") @@ -525,9 +523,9 @@ func h8(t *testing.T, b *itBed, checkBuild bool) (func(), h8state) { kv nats.KeyValue key string value []byte - }{{manifests, mkey, mraw}, {modules, "web", modv}, {b.haves, hkey, rec}, + }{{manifests, mkey, mraw}, {b.haves, hkey, rec}, {resolved, itNode + ".web.greeting", []byte(`{"value":"bonjour","source":"module"}`)}, - {manifests, mlkey, mlraw}, {modules, "mailer", mlmod}, {b.haves, hkey2, rec2}, {assignment, pkey2, places2[pkey2]}, {assignment, "it2.mailer", places2["it2.mailer"]}, + {manifests, mlkey, mlraw}, {b.haves, hkey2, rec2}, {assignment, pkey2, places2[pkey2]}, {assignment, "it2.mailer", places2["it2.mailer"]}, {resolved, "it2.mailer.domain", []byte(`{"value":"example.org","source":"module"}`)}} { if _, err := w.kv.Put(w.key, w.value); err != nil { t.Fatal(err) diff --git a/internal/selfcompose/composedstate.go b/internal/selfcompose/composedstate.go index da0eec31..61180a01 100644 --- a/internal/selfcompose/composedstate.go +++ b/internal/selfcompose/composedstate.go @@ -89,47 +89,20 @@ func (c *Composer) publishComposed(ctx context.Context) { // is left out. A module whose newest build the builder holds failed is also returned in failed, by module, with the // indexer's fingerprint and the build's error: it is published as a refusal, not composed. func (c *Composer) newestFPs(ctx context.Context, at *published) (out map[string]string, failed map[string][2]string) { - if c.byEntries() { - // The newest build is the indexer's (and the builder's done key says it is built): neither is read, and the node - // follows no branch (design 47 G §421, §628), so the state holds the wanted builds alone. A placed module's newest - // build that ended is the last trunk end the builder said of it (NoteBuildEnded): a failed one is refused (A-293). - failed = map[string][2]string{} - c.mu.Lock() - if c.endsRead { - for _, module := range at.placed { - if e, ok := c.ends[module]; ok && e.Outcome == builds.BuildFailed { - failed[module] = [2]string{e.Fingerprint, e.Error} - } - } - } - c.mu.Unlock() - return maps.Clone(at.fps), failed - } - out = map[string]string{} + // The newest build is the indexer's (and the builder's done key says it is built): neither is read, and the node + // follows no branch (design 47 G §421, §628), so the state holds the wanted builds alone. A placed module's newest + // build that ended is the last trunk end the builder said of it (NoteBuildEnded): a failed one is refused (A-293). failed = map[string][2]string{} - r := reader{c: c} - for _, module := range at.placed { - c.mu.Lock() - fp2 := c.modules[module].Fp2 - c.mu.Unlock() - if fp2 != "" { - if b, err := r.Build(ctx, module, fp2); err == nil && b != nil { - switch b.State { - case builds.BuildDone: - out[module] = fp2 - continue - case builds.BuildFailed: - failed[module] = [2]string{fp2, b.Error} - } + c.mu.Lock() + if c.endsRead { + for _, module := range at.placed { + if e, ok := c.ends[module]; ok && e.Outcome == builds.BuildFailed { + failed[module] = [2]string{e.Fingerprint, e.Error} } } - if fp, ok := at.fps[module]; ok { - out[module] = fp - } else if fp, ok := at.ran[module]; ok { - out[module] = fp - } } - return out, failed + c.mu.Unlock() + return maps.Clone(at.fps), failed } // writeComposed works out the state's keys from at and writes those that differ, then deletes what is past keeping. @@ -291,7 +264,7 @@ func (c *Composer) composedValues(ctx context.Context, at *published) (map[strin func (c *Composer) endedRefusals(values map[string][]byte) []string { c.mu.Lock() defer c.mu.Unlock() - if !c.byEntries() || !c.endsRead { + if !c.endsRead { return nil } var out []string diff --git a/internal/selfcompose/entries.go b/internal/selfcompose/entries.go index bcc6e610..0af82b00 100644 --- a/internal/selfcompose/entries.go +++ b/internal/selfcompose/entries.go @@ -66,10 +66,6 @@ type WantRow struct { KeptSince time.Time `json:"kept-since"` } -// byEntries says the composer reads the catalog half by fingerprint (the node's own composer), as the shadow, which keeps -// its reads of the indexer until it goes with the checker (§628), does not. -func (c *Composer) byEntries() bool { return c.entries != nil } - // newEntries makes the composer's cache of entries over env's call of `entry`. func (c *Composer) newEntries() { if c.env.Entry == nil { @@ -96,7 +92,7 @@ func wantStateKey(module string) string { return keptWantPrefix + module } // half (§683). A want read stands only until the live read of the wants answers (endKeptWants). A state that cannot be read // is said, and what it would have kept is asked of `entry` and read from the wants as they come. func (c *Composer) loadKept(ctx context.Context) { - if !c.byEntries() || c.env.ComposedState == nil { + if c.env.ComposedState == nil { return } kept, err := c.env.ComposedState.Entries(ctx) @@ -136,9 +132,6 @@ func (c *Composer) loadKept(ctx context.Context) { // says are the ones the node composes from, never a want older than its stream's (§683). A kept want the read did not // announce is gone. func (c *Composer) endKeptWants() { - if !c.byEntries() { - return - } c.mu.Lock() defer c.mu.Unlock() for module := range c.keptWants { @@ -252,9 +245,6 @@ func (c *Composer) wantedBuilds() map[string]bool { // composes from, and lays the picture frombus reads (c.modules, c.manifests) from the entries it holds. It runs at the start of // each pass. func (c *Composer) syncEntries(ctx context.Context) { - if !c.byEntries() { - return - } need := c.wantedBuilds() for fp := range need { c.entryOf(fp) @@ -393,9 +383,6 @@ func entryManifest(e meshcatalog.CatalogEntry) (digest string, raw []byte, err e // entryHolds is, of each module this node's want names, why its entry is not held: asked and not answered, or answered not // found. It is the phase held with a detail opening entry-asked: or entry-missing: and naming the fingerprint (§421, §614). func (c *Composer) entryHolds() map[string]string { - if !c.byEntries() { - return nil - } c.mu.Lock() wants := map[string]meshwants.WantsByWantNodeModuleValue{} for module, w := range c.wants { @@ -476,9 +463,6 @@ func (c *Composer) buildOf(module, fp string) *builds.BuilderBuild { // follows no branch), the entries it keeps and the wants it composes from. func (c *Composer) FirstViews() (views []map[string]any, entries []EntryRow, wants []WantRow) { views, entries, wants = []map[string]any{}, []EntryRow{}, []WantRow{} - if !c.byEntries() { - return - } seen := map[string]bool{} for _, r := range c.entries.Rows() { seen[r.Fingerprint] = true @@ -508,9 +492,6 @@ func (c *Composer) EntryAgain(ctx context.Context, fp string) error { if c.AlsoEntryAgain != nil { c.AlsoEntryAgain(ctx, fp) } - if !c.byEntries() { - return fmt.Errorf("this engine reads no entry by fingerprint") - } c.mu.Lock() _, kept := c.keptEnt[fp] delete(c.keptEnt, fp) @@ -554,9 +535,6 @@ const noEntryWords = "no entry of fingerprint" // to, each read from the entry the composer holds of its fingerprint (design 47 G §296, §297). An entry not held is an error: the // node agent then says it is not known and the move goes on. func (c *Composer) FormChanges(_ context.Context, module, runningFp, appliedFp string) ([]catalogue.FormChange, error) { - if !c.byEntries() { - return nil, fmt.Errorf("this engine reads no entry by fingerprint") - } read := func(fp string) (catalogue.Manifest, error) { e, state := c.entryOf(fp) if state != meshcatalogclient.EntryAnswered { @@ -583,9 +561,6 @@ func (c *Composer) FormChanges(_ context.Context, module, runningFp, appliedFp s // c.manifests from the entries held, and says whether an entry asked is not yet answered: the shadow's picture of the catalog, // where it once watched the indexer's modules (design 47 G §421, A-308). func (c *Composer) syncShadowEntries(ctx context.Context) (pending bool) { - if !c.byEntries() { - return false - } c.syncEntries(ctx) c.mu.Lock() defer c.mu.Unlock() @@ -651,9 +626,6 @@ func (c *Composer) addedModule(ctx context.Context, module string) (fp string, e // and says the first not yet answered: the shadow composes a candidate from entries alone (§421, §628), and an answer wakes it. The // fingerprints are kept beside the candidates' added ones while a proposal stands (addedFps). func (c *Composer) askBuildsEntries(fps map[string]string) (pending string) { - if !c.byEntries() { - return "" - } for _, module := range slices.Sorted(maps.Keys(fps)) { fp := fps[module] if !strings.HasPrefix(fp, meshcatalogclient.SourcePrefix2) { diff --git a/internal/selfcompose/entries_test.go b/internal/selfcompose/entries_test.go index 24d32128..6045d43f 100644 --- a/internal/selfcompose/entries_test.go +++ b/internal/selfcompose/entries_test.go @@ -212,23 +212,6 @@ func composeFromEntries(t *testing.T) { } } -// takeModule lays one module of the catalog half's picture as the composer's own lay of an entry does (layModules): the tests -// that compose from a picture they set read no indexer. -func (c *Composer) takeModule(u agent.Update) { - name, err := meshcatalogclient.ParseModulesKey(u.Key) - if err != nil { - return - } - c.mu.Lock() - defer c.mu.Unlock() - m, err := meshcatalogclient.DecodeModule(u.Value) - if u.Deleted || err != nil { - delete(c.modules, name) - return - } - c.modules[name] = m -} - // catalogHalf is the catalog half's entries a test composes from, by fingerprint, answered through the seat mesh-catalog's verbs // entry and module (design 47 G §421, A-312): the node-engine reads no indexer. type catalogHalf map[string]meshcatalog.CatalogEntry diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index ed533851..d8a65be1 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -134,7 +134,7 @@ type Env struct { SeatListChanged <-chan struct{} // Entry asks the seat mesh-catalog's verb `entry` of a fingerprint (meshcatalogclient.EntryVerb), in the engine's own name. // Set, the composer reads the catalog half by the fingerprints its wants and the runnings it heard name and nothing else of - // the indexer's or the builder's states (design 47 G §421, §628); nil reads the indexer's states, as the shadow does. + // the indexer's or the builder's states (design 47 G §421, §628); a composer is made with it. Entry meshcatalogclient.Call } @@ -187,7 +187,7 @@ type Composer struct { notTaken map[string]string // unmadeSaid is, by module, the stable part of the reason its last journal line said (without where the picture stands). unmadeSaid map[string]string - // entries is the cache of entries asked by fingerprint, nil where the composer reads the indexer's states; keptEnt the entries + // entries is the cache of entries asked by fingerprint; keptEnt the entries // held from the composition state or an answer, keptWantVal the wants it keeps there and keptWants the modules whose want // stands from that state alone until the live read of the wants answers (entries.go). Under mu; keptWrite orders the writes. entries *meshcatalogclient.Entries @@ -303,7 +303,7 @@ type Pass struct { // composed at (frombus.Scope.Holds), for LastHeld's words. holds map[string]compose.Hold // Unmade is each module wanted that the pass composed no composition of at its want's fp, with why: the want not - // taken, the module not assigned here, no manifest in the composer's picture of the indexer's modules (and where that + // taken, the module not assigned here, no entry of its build in the composer's picture of the catalog half (and where that // picture stands), or what the composition left it out for. A module held alone is in Held, not here. Said once per // change as a journal line, so a want that waits for the composer says what for. Unmade map[string]string `json:"unmade,omitempty"` @@ -1557,7 +1557,7 @@ func (r reader) Manifest(ctx context.Context, digest string) ([]byte, error) { if ok { return raw, nil } - if r.c.byEntries() && !r.c.forCandidates { + if !r.c.forCandidates { return nil, fmt.Errorf("the catalog half's entries hold no manifest %s", digest) } key, err := meshcatalogclient.ManifestsKey(digest) @@ -2195,7 +2195,7 @@ func (c *Composer) unmadeOf(fps map[string]string, comps map[string]engcomp.Comp continue } if made[module] == fp { - // Composed, but from the indexer's manifest and not a build: said, since what it names no artifacts of is no module + // Composed, but from the entry's manifest and not a build: said, since what it names no artifacts of is no module // that runs. if w, ok := fromManifest[module]; ok { out[module], keys[module] = w, fp+" "+w @@ -2211,18 +2211,18 @@ func (c *Composer) unmadeOf(fps map[string]string, comps map[string]engcomp.Comp why = fmt.Sprintf("the seat's assignment places no %s on this node (%d modules placed)", module, len(own.Modules)) key = "not placed" case !in: - why = fmt.Sprintf("no manifest in the composer's picture of the indexer's modules (%d modules)", + why = fmt.Sprintf("no entry of its build in the composer's picture of the catalog half (%d modules)", len(c.modules)) key = "no manifest" case slices.Contains(skipped, module): - why = "the composition skipped it: the declaration gives it no fingerprint (indexer's picture: " + m.Fp2 + ")" + why = "the composition skipped it: the declaration gives it no fingerprint (the composer's picture: " + m.Fp2 + ")" key = why default: if at := made[module]; at != "" { - why = fmt.Sprintf("the pass composed it at %s and the want names %s (indexer's picture: %s)", at, fp, m.Fp2) + why = fmt.Sprintf("the pass composed it at %s and the want names %s (the composer's picture: %s)", at, fp, m.Fp2) key = "composed at " + at } else { - why = fmt.Sprintf("the composition made nothing of it at %s (indexer's picture: %s)", fp, m.Fp2) + why = fmt.Sprintf("the composition made nothing of it at %s (the composer's picture: %s)", fp, m.Fp2) key = "made nothing at " + m.Fp2 } } @@ -2444,7 +2444,7 @@ func (c *Composer) EngineModule() string { // stands says the want of a module stands for its placement in the early pass of a start: its want is a kept one the live read has // not answered, and the entry of its build is kept (design 47 G A-117). func (c *Composer) stands(module string, w meshwants.WantsByWantNodeModuleValue) bool { - if !c.byEntries() || !c.keptWants[module] { + if !c.keptWants[module] { return false } k, ok := c.keptEnt[w.Fp] diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 0620df94..78830f34 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -53,6 +53,7 @@ type fakeBus struct { kv map[string][]byte // bucket/key said []string reads [][2]string // bucket, pattern or key: every watch and get + half catalogHalf // the catalog half the bus's composers ask the entry of } func newFakeBus() *fakeBus { @@ -60,7 +61,7 @@ func newFakeBus() *fakeBus { } func (b *fakeBus) env() Env { - return Env{Node: "n1", + return Env{Node: "n1", Entry: b.half.call(), // the catalog half answers what the test's bus holds of it, nothing until a test sets it Watch: func(_ context.Context, bucket, pattern string) (<-chan agent.Update, error) { b.mu.Lock() defer b.mu.Unlock() @@ -163,10 +164,24 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: lemurs}) key, _ := meshcatalogclient.ManifestsKey(digest) b.kv[buckets.Manifests+"/"+key] = man + // The catalog half the node composes from, by fingerprint: the node's own composer asks it and reads nothing of the indexer. + half := catalogHalf{} + half.add(t, "lemurs", fp, lemurs) + oldieFp := meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64) + toolFp := meshcatalogclient.SourcePrefix2 + strings.Repeat("7", 64) + keeperFp := meshcatalogclient.SourcePrefix2 + strings.Repeat("9", 64) + toolBody := `{"module":"tool","version":"1","capabilities":["systemd"],"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` + + `"resources":[{"id":"service","type":"service","unit":"tool.service","state":"running","boot":"enabled"}]}` + half.add(t, "tool", toolFp, toolBody) + b.half = half + keeperBody := `{"module":"keeper","version":"1","own-secrets":{"password":"/var/lib/mesh/keeper/password"},` + + `"resources":[{"id":"state","type":"directory","path":"/var/lib/mesh/keeper","mode":"0700"}]}` + half.add(t, "keeper", keeperFp, keeperBody) ctx, cancel := context.WithCancel(context.Background()) defer cancel() env := b.env() + env.Entry = half.call() // The vault's own: keeper's password sealed to n1's key, sealed again (other bytes) at every ask. var asks, generation int sealedAt := map[string]int{} // each box handed, by the generation it opens to @@ -195,26 +210,22 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { // oldie is placed and no want names it (a module at a build of the old form, nats on novox): the node runs it inside // its own composition, and composes it there again, even before the agent has applied anything (Running empty). oldie := `{"module":"oldie","version":"1","capabilities":["systemd"],"resources":[{"id":"service","type":"service","unit":"oldie.service","state":"running","boot":"enabled"}]}` - odigest := meshcatalogclient.ManifestDigest([]byte(oldie)) - oman, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "oldie", Manifest: oldie}) - okey, _ := meshcatalogclient.ManifestsKey(odigest) - b.kv[buckets.Manifests+"/"+okey] = oman - omod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64), Manifest: odigest, Repository: "novox/o", Commit: "c"}) + half.add(t, "oldie", oldieFp, oldie) place := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}}}) have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}}) - mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"}) want, _ := bucketstest.Want(meshwants.WantsByWantNodeModuleValue{Fp: fp, Self: true}) nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true}) b.putAssigned(t, place) b.put(t, buckets.Have, "n1.node", have) oldHave, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: "build-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) b.put(t, buckets.Have, "n1.oldie", oldHave) - // The picture of the catalog half is set directly (no indexer is watched): lemurs's entry, which its want names, and oldie, - // which the node runs inside its own composition and no want names. - c.takeModule(agent.Update{Key: "lemurs", Value: mod}) - c.takeModule(agent.Update{Key: "oldie", Value: omod}) - c.holdEntry(t, "lemurs", fp, lemurs) + // The catalog half answers lemurs's entry, which its want names, and oldie's, which the node runs inside its own composition and + // no want names. + c.NoteRunning(nodeengine.Running{Node: "n1", Module: "oldie", Fingerprint: oldieFp}) + c.RunningRead() + c.holdEntry(t, "lemurs", fp, lemurs) // held at the start, as the composition state keeps it, so the first pass composes + c.holdEntry(t, "oldie", oldieFp, oldie) b.put(t, buckets.Wants, "want.n1.lemurs", want) b.put(t, buckets.Wants, "want.n1.node", nodeWant) for _, bucket := range []string{buckets.Wants, buckets.Assignments, buckets.Have, ResolvedBucket} { @@ -289,18 +300,9 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { // Issue 1966 (novox, 2026-11-09): every pass names each wanted module it composed nothing of, or composed from the indexer's // manifest for want of a done build, with why, and a want it did not take; said once; the agent's word that it waits // hands a composition the node believes handed again. - tfp := meshcatalogclient.SourcePrefix2 + strings.Repeat("7", 64) - tool := `{"module":"tool","version":"1","capabilities":["systemd"],"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` + - `"resources":[{"id":"service","type":"service","unit":"tool.service","state":"running","boot":"enabled"}]}` - tdigest := meshcatalogclient.ManifestDigest([]byte(tool)) - tman, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "tool", Manifest: tool}) - tkey, _ := meshcatalogclient.ManifestsKey(tdigest) - b.mu.Lock() - b.kv[buckets.Manifests+"/"+tkey] = tman - b.mu.Unlock() - tmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: tfp, Manifest: tdigest, Repository: "novox/t", Commit: "c"}) - c.takeModule(agent.Update{Key: "tool", Value: tmod, Revision: 9}) - for module, w := range map[string]meshwants.WantsByWantNodeModuleValue{"tool": {Fp: tfp, Self: true}, "ghost": {Fp: fp, Self: true}, "nofp": {Self: true}} { + tfp, ghostFp := toolFp, meshcatalogclient.SourcePrefix2+strings.Repeat("8", 64) + c.holdEntry(t, "tool", tfp, toolBody) + for module, w := range map[string]meshwants.WantsByWantNodeModuleValue{"tool": {Fp: tfp, Self: true}, "ghost": {Fp: ghostFp, Self: true}, "nofp": {Self: true}} { raw, _ := bucketstest.Want(w) c.takeWant(agent.Update{Key: "want.n1." + module, Value: raw}) } @@ -320,22 +322,20 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { return n } c.compose(ctx) - why := map[string]string{"tool": c.WhyUnmade("tool"), "ghost": c.WhyUnmade("ghost"), "nofp": c.WhyUnmade("nofp"), "lemurs": c.WhyUnmade("lemurs")} - if !strings.Contains(why["tool"], "the builder holds no done build of it at "+tfp) || !strings.Contains(why["ghost"], "places no ghost") || + saidOfNofp := said("the node composes nothing of nofp") + why := map[string]string{"tool": c.LastHeld()["tool"], "ghost": c.LastHeld()["ghost"], "nofp": c.WhyUnmade("nofp"), "lemurs": c.WhyUnmade("lemurs")} + if !strings.Contains(why["tool"], "tool at "+tfp) || !strings.Contains(why["ghost"], "entry-asked: the entry of the build "+ghostFp) || !strings.Contains(why["nofp"], "want not taken: its want names no fingerprint") || why["lemurs"] != "" { t.Errorf("the pass says it composed nothing of %q", why) } - // An update of another module moves the picture's revision: the same reasons are not said again. - c.takeModule(agent.Update{Key: "lemurs", Value: mod, Revision: 10}) c.compose(ctx) - if n := said("the node composes nothing of ghost"); n != 1 { - t.Errorf("the journal says why ghost is not composed %d times, want once", n) + if n := said("the node composes nothing of nofp"); n != saidOfNofp || n == 0 { + t.Errorf("the journal says why nofp is not composed %d times after the second pass, %d after the first: want the same, not none", n, saidOfNofp) } // The state of this step is taken away again, and the pass that takes back what it handed is made here. for _, module := range []string{"tool", "ghost", "nofp"} { c.takeWant(agent.Update{Key: "want.n1." + module, Deleted: true}) } - c.takeModule(agent.Update{Key: "tool", Deleted: true}) c.mu.Lock() placed.Modules = placed.Modules[:len(placed.Modules)-1] c.assignments["n1"] = placed @@ -365,18 +365,13 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Errorf("a build that cannot be composed was composed today: %s", dig) } chgJSON := strings.Replace(lemurs, `"state":"running"`, `"state":"stopped"`, 1) - chgDigest := meshcatalogclient.ManifestDigest([]byte(chgJSON)) - chgMan, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "lemurs", Manifest: chgJSON}) - chgKey, _ := meshcatalogclient.ManifestsKey(chgDigest) - b.mu.Lock() - b.kv[buckets.Manifests+"/"+chgKey] = chgMan - b.mu.Unlock() - chgMod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: fp, Manifest: chgDigest, Repository: "novox/x", Commit: "c"}) - c.takeModule(agent.Update{Key: "lemurs", Value: chgMod}) + c.holdEntry(t, "lemurs", fp, chgJSON) + c.layModules() 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}) + c.holdEntry(t, "lemurs", fp, lemurs) + c.layModules() // 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. @@ -405,6 +400,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { for k, v := range c.modules { c2.modules[k] = v } + maps.Copy(c2.keptEnt, c.keptEnt) c.mu.Unlock() return c2, &refusals } @@ -431,7 +427,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { b.mu.Unlock() failed := 0 pctx, pcancel := context.WithCancel(ctx) - c2, refusals := fresh(func(gctx context.Context, bucket, key string) ([]byte, error) { + get := func(gctx context.Context, bucket, key string) ([]byte, error) { if failed < row.failures { failed++ if row.cancel { @@ -447,7 +443,20 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { b.mu.Lock() defer b.mu.Unlock() return b.kv[bucket+"/"+key], nil - }) + } + c2, refusals := fresh(get) + // What fails is the vault's own, asked for keeper's secret through the same read. + c2.holdEntry(t, "keeper", keeperFp, keeperBody) + c2.wants["keeper"] = meshwants.WantsByWantNodeModuleValue{Fp: keeperFp, Self: true} + pk := c2.assignments["n1"] + pk.Modules = append(slices.Clone(pk.Modules), meshmoduleassigner.PlacedModule{Module: "keeper"}) + c2.assignments["n1"] = pk + c2.env.Own = func(octx context.Context, _ map[string]any) (json.RawMessage, error) { + if _, err := get(octx, buckets.Wants, "own"); err != nil { + return nil, fmt.Errorf("the vault's own could not be asked: %w", err) + } + return json.RawMessage(`{"node":"n1","key":"sk","own":[{"module":"keeper","name":"password","origin":"made","generation":1,"sealed":{"a":1}}],"handed":{"keeper":false}}`), nil + } handed := c2.compose(pctx) pcancel() tries := 1 + len(composeAgain) // a bus paused for a moment is tried again at once; a timeout or a refusal is not @@ -503,15 +512,16 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { `{"id":"pkg","type":"package","package":"haproxy"}]}` c3, told := fresh(env.Get) for m, raw := range map[string]string{"de-spiegel": string(spiegel), "proxy": proxy} { - dig := meshcatalogclient.ManifestDigest([]byte(raw)) - enc, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: m, Manifest: raw}) - k, _ := meshcatalogclient.ManifestsKey(dig) - b.mu.Lock() - b.kv[buckets.Manifests+"/"+k] = enc - b.mu.Unlock() mfp := meshcatalogclient.SourcePrefix2 + strings.Repeat(map[string]string{"de-spiegel": "d", "proxy": "e"}[m], 64) - c3.modules[m] = meshcatalog.ModulesByModuleValue{Fp2: mfp, Manifest: dig, Repository: "novox/" + m, Commit: "c"} - c3.wants[m] = meshwants.WantsByWantNodeModuleValue{Fp: mfp, Self: true} + c3.holdEntry(t, m, mfp, raw) + w := meshwants.WantsByWantNodeModuleValue{Fp: mfp, Self: true} + if m == "de-spiegel" { // builds an image: the entry names it, the want the digest its part has + k := c3.keptEnt[mfp] + k.entry.Artifacts = []meshcatalog.Artifact{{Kind: "image", Ref: "de-spiegel:site"}} + c3.keptEnt[mfp] = k + w.Parts = map[string]string{"de-spiegel:site": "sha256:" + strings.Repeat("6", 64)} + } + c3.wants[m] = w } p3 := c3.assignments["n1"] p3.Modules = append(append([]meshmoduleassigner.PlacedModule{}, p3.Modules...), meshmoduleassigner.PlacedModule{Module: "proxy"}, meshmoduleassigner.PlacedModule{Module: "de-spiegel"}) @@ -556,16 +566,16 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Fatal(err) } c4, told4 := fresh(env.Get) - powerDig := meshcatalogclient.ManifestDigest(power) - powerEnc, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "power", Manifest: string(power)}) - powerKey, _ := meshcatalogclient.ManifestsKey(powerDig) - b.mu.Lock() - b.kv[buckets.Manifests+"/"+powerKey] = powerEnc - b.mu.Unlock() powerFP := meshcatalogclient.SourcePrefix2 + strings.Repeat("c", 64) - c4.modules["power"] = meshcatalog.ModulesByModuleValue{Fp2: powerFP, Manifest: powerDig, Repository: "novox/mesh-catalog", Commit: "ca8d6f33", - // The indexer's verdict on the captured manifest (ADR 0578): the engine judges nothing, it takes this. - Verdict: meshcatalogclient.VerdictRefused, Problems: []string{`bar item "shows" is none of them`}} + powerHalf := catalogHalf{} + powerHalf.add(t, "power", powerFP, string(power)) + powerEntry := powerHalf[powerFP] + // The verdict the catalog half's entry carries on the captured manifest (ADR 0578): the engine judges nothing, it takes this. + powerEntry.Repository, powerEntry.Commit, powerEntry.Verdict = "novox/mesh-catalog", "ca8d6f33", meshcatalogclient.VerdictRefused + powerEntry.Inputs = &meshcatalog.EntryInputs{Problems: []string{`bar item "shows" is none of them`}} + c4.mu.Lock() + c4.keptEnt[powerFP] = keptEntry{entry: powerEntry} + c4.mu.Unlock() c4.wants["power"] = meshwants.WantsByWantNodeModuleValue{Fp: powerFP, Self: true} p4 := c4.assignments["n1"] p4.Modules = append(append([]meshmoduleassigner.PlacedModule{}, p4.Modules...), meshmoduleassigner.PlacedModule{Module: "power"}) @@ -735,7 +745,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { b.mu.Lock() n := 0 for _, s := range b.said { - if strings.Contains(s, "lemurs is wanted at "+gone) { + if strings.Contains(s, "lemurs: entry-missing: the catalog half holds no entry of the build "+gone) { n++ } } @@ -750,20 +760,16 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { if u := next(t, out); u.Key != lemursKey { t.Fatalf("lemurs wanted back handed %s", u.Key) } - keeper := `{"module":"keeper","version":"1","own-secrets":{"password":"/var/lib/mesh/keeper/password"},` + - `"resources":[{"id":"state","type":"directory","path":"/var/lib/mesh/keeper","mode":"0700"}]}` + keeper := keeperBody kdigest := meshcatalogclient.ManifestDigest([]byte(keeper)) kman, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "keeper", Manifest: keeper}) kkey, _ := meshcatalogclient.ManifestsKey(kdigest) b.mu.Lock() b.kv[buckets.Manifests+"/"+kkey] = kman b.mu.Unlock() - kfp9 := meshcatalogclient.SourcePrefix2 + strings.Repeat("9", 64) - kmod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: kfp9, Manifest: kdigest, Repository: "novox/k", Commit: "c"}) + kfp9 := keeperFp both := assignedValues(t, meshmoduleassigner.Assignments{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []meshmoduleassigner.PlacedModule{{Module: "lemurs"}, {Module: "oldie"}, {Module: "keeper"}}}) - c.takeModule(agent.Update{Key: "keeper", Value: kmod}) - c.holdEntry(t, "keeper", kfp9, keeper) // n1 is held in keys per module and node, which the writes below move. bothPlaced, _ := meshmoduleassignerclient.AssignmentsOf(both) bothNode := bothPlaced["n1"] @@ -884,6 +890,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { for k, v := range c.modules { again.modules[k] = v } + maps.Copy(again.keptEnt, c.keptEnt) c.mu.Unlock() var composed string for _, u := range again.compose(ctx) { @@ -1930,7 +1937,19 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) { maps.Copy(c2.assignments, c.assignments) maps.Copy(c2.records, c.records) maps.Copy(c2.modules, c.modules) + maps.Copy(c2.keptEnt, c.keptEnt) c.mu.Unlock() + // What fails is the vault's own, asked for the secret of a module the pass composes (the modules' manifests come from entries). + kfp := meshcatalogclient.SourcePrefix2 + strings.Repeat("9", 64) + c2.holdEntry(t, "keeper", kfp, `{"module":"keeper","version":"1","own-secrets":{"password":"/var/lib/mesh/keeper/password"},`+ + `"resources":[{"id":"state","type":"directory","path":"/var/lib/mesh/keeper","mode":"0700"}]}`) + c2.wants["keeper"] = meshwants.WantsByWantNodeModuleValue{Fp: kfp, Self: true} + pk := c2.assignments["n1"] + pk.Modules = append(slices.Clone(pk.Modules), meshmoduleassigner.PlacedModule{Module: "keeper"}) + c2.assignments["n1"] = pk + c2.env.Own = func(context.Context, map[string]any) (json.RawMessage, error) { + return nil, errors.New("nats says ${dir:mesh-state}, and nats declares no directory") + } c2.compose(ctx) if line := fmt.Sprintf(WaitsForTheSeatListHeld, 2, "mesh-catalog, node-engine") + "; "; !strings.HasPrefix(refusal, line) { t.Errorf("a pass refused while the list is unread says %q, want it to begin %q", refusal, line) @@ -2090,6 +2109,8 @@ func TestTheHolderOfTheCatalogIsStartedFromTheOwnCompositionBeforeTheListIsReadA `"resources":[{"id":"service","type":"service","unit":"other.service","state":"running","boot":"enabled"},` + `{"id":"extra","type":"service","unit":"extra.service","state":"running","boot":"enabled"}]}` b := newFakeBus() + half := catalogHalf{} + b.half = half ctx, cancel := context.WithCancel(context.Background()) defer cancel() env := b.env() @@ -2107,12 +2128,11 @@ func TestTheHolderOfTheCatalogIsStartedFromTheOwnCompositionBeforeTheListIsReadA mods := map[string]string{"catalog": catalog, "other": other} var placed []meshmoduleassigner.PlacedModule for _, m := range []string{"catalog", "other"} { - digest := meshcatalogclient.ManifestDigest([]byte(mods[m])) - man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: m, Manifest: mods[m]}) - mk, _ := meshcatalogclient.ManifestsKey(digest) - b.kv[buckets.Manifests+"/"+mk] = man - rec, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64), Manifest: digest, Repository: "novox/x", Commit: "c"}) - c.takeModule(agent.Update{Key: m, Value: rec}) // a module run inside the node's own composition: no want names it, so no entry is asked + mfp := meshcatalogclient.SourcePrefix2 + strings.Repeat(map[string]string{"catalog": "a", "other": "b"}[m], 64) + half.add(t, m, mfp, mods[m]) + // A module run inside the node's own composition: no want names it, the running heard does, so its entry is asked. + c.NoteRunning(nodeengine.Running{Node: "n1", Module: m, Fingerprint: mfp}) + c.holdEntry(t, m, mfp, mods[m]) hk, _ := nodeengineclient.HaveKey("n1", m) hv, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: "build-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) b.put(t, buckets.Have, hk, hv)