R3-3: the node's own composer has no path without an entry call; its tests compose from the catalog half
Builds: design 47 G §628, §421, A-312, A-317 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user