diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go index 221653b6..29740838 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go @@ -15,6 +15,7 @@ import ( "time" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" @@ -328,26 +329,12 @@ func written(s stdio.KeptState, key string, revision uint64) time.Time { return time.Time{} } -// The state the bundle follows, its owner's tool, and the seat verbs it calls (the manifest's invokes). +// The seat verbs the bundle calls (the manifest's invokes). const ( - indexerModule = "mesh-delivery-indexer" - packagesState = "packages" - packagesTool = indexerModule + ".indexer_read_packages" catalogVersionsVerb = "seat:mesh-catalog.versions" reservedVerb = "seat:mesh-builds.reserved" ) -// followedPackages is the catalog half's packages state as the bundle's picture of it holds it (versions.PackagesState). -type followedPackages struct{ r *views.Reader } - -func (f followedPackages) Keys(context.Context) ([]string, error) { - return f.r.Keys(indexerModule + "." + packagesState) -} - -func (f followedPackages) Get(_ context.Context, key string) ([]byte, bool, error) { - return f.r.Get(indexerModule+"."+packagesState, key) -} - func main() { steps := map[string]build.StateRead{} for _, bm := range build.StuckBuildModules { @@ -407,9 +394,13 @@ func main() { // The catalog half's view of the packages the bundle follows itself (design 47 G §572, A-70), as other bundles follow views. pk := &views.Reader{Module: builds.BuilderModule, Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] "+f+"\n", a...) }, Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}} - pk.Follow(indexerModule, packagesState, packagesTool) + branches, err := meshcatalogclient.FollowBranches(pk, stdio.Ask) + if err != nil { + fmt.Fprintln(os.Stderr, "mesh-delivery-builder-tools:", err) + os.Exit(1) + } pk.Start(context.Background()) - carriers := versions.Carriers(followedPackages{pk}) + carriers := versions.CarriersFrom(versions.EntriesOf(branches)) released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) { raw, err := stdio.Ask(catalogVersionsVerb, meshcatalog.VersionsInput{Module: module}) if err != nil { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go index 34654335..694d507d 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go @@ -3,13 +3,9 @@ package main import ( "context" "errors" - "fmt" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" - "git.novox.be/novox/mesh-sdk/go/catalogue" - "mesh-delivery-builder/internal/build" ) @@ -106,23 +102,3 @@ func follow(ctx context.Context, w jetstream.KeyWatcher, each func(jetstream.Key } } } - -// manifestReader reads manifests. of the indexer and parses the manifest it holds. -type manifestReader struct{ kv jetstream.KeyValue } - -func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue.Manifest, error) { - key, err := meshcatalogclient.ManifestsKey(digest) - if err != nil { - return catalogue.Manifest{}, err - } - e, err := m.kv.Get(ctx, key) - if err != nil { - return catalogue.Manifest{}, fmt.Errorf("manifests.%s: %w", key, err) - } - kept, err := meshcatalogclient.DecodeManifest(e.Value()) - if err != nil { - return catalogue.Manifest{}, err - } - // Decoded, never judged (novox/hq ADR 0578 §3): what a build needs of it is read, a field it does not know set aside. - return catalogue.Decode([]byte(kept.Manifest)) -} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go new file mode 100644 index 00000000..19515895 --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go @@ -0,0 +1,191 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "slices" + "sync" + "time" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/views" + "mesh-delivery-builder/internal/build" +) + +// **The builder reads the catalog half per watched branch** (design 47 G §348, §479, §614; A-5, A-15): the shared branch follower +// of meshcatalogclient, adopted on the builder's views reader, gives each watched branch's entries; the manifests, the packages +// and each package's name in its ecosystem are read from the entries, never the indexer's states. + +// entryManifests keeps the manifest of each entry heard, by the entry's manifest digest (the entry holds the manifest whole). +type entryManifests struct { + mu sync.Mutex + byFp map[string]catalogue.Manifest +} + +// keep takes the manifest of e in; an entry with no manifest or digest keeps none. +func (m *entryManifests) keep(e meshcatalog.CatalogEntry) error { + if e.ManifestDigest == "" || len(e.Manifest) == 0 { + return nil + } + raw, err := json.Marshal(e.Manifest) + if err != nil { + return err + } + // Decoded, never judged (novox/hq ADR 0578 §3): what a build needs of it is read, a field it does not know set aside. + mf, err := catalogue.Decode(raw) + if err != nil { + return fmt.Errorf("the manifest of the entry of %s does not decode: %w", e.Module, err) + } + m.mu.Lock() + defer m.mu.Unlock() + if m.byFp == nil { + m.byFp = map[string]catalogue.Manifest{} + } + m.byFp[e.ManifestDigest] = mf + return nil +} + +// Manifest is the manifest the entry heard with that digest holds. +func (m *entryManifests) Manifest(_ context.Context, digest string) (catalogue.Manifest, error) { + m.mu.Lock() + defer m.mu.Unlock() + mf, ok := m.byFp[digest] + if !ok { + return catalogue.Manifest{}, fmt.Errorf("no entry of the catalog half holds the manifest %s", digest) + } + return mf, nil +} + +// catalogHalf is the builder's part over the branch follower: what changes in a branch's half goes to the builder, and the +// entries of every followed half are what the package names are read from. +type catalogHalf struct { + branches *meshcatalogclient.Branches + b *build.Builder + manifests *entryManifests + logf func(string, ...any) + // named is, by module, the package names of its first entry last heard, so a name newly heard looks at its module again. + mu sync.Mutex + named map[string]string + // ready is told when every followed branch's first view is in, once; halves are the views adopted (sweepWiring). + ready func() + halves map[string]*views.Follower + once sync.Once + ctx context.Context +} + +// first says whether branch is the first watched branch of module's registration (design 47 G §371); a module with no +// registration heard is taken as watched on this branch, the first one. +func (c *catalogHalf) first(branch, module string) bool { + reg, ok := c.branches.Registrations.Registration(module) + return !ok || len(reg.Branches) == 0 || reg.Branches[0] == branch +} + +// Changed is the follower's Changed: one module's entry of a branch came, or went. +func (c *catalogHalf) Changed(ctx context.Context) func(branch, module string, entry json.RawMessage, deleted bool, at time.Time) { + return func(branch, module string, entry json.RawMessage, deleted bool, _ time.Time) { + first := c.first(branch, module) + if deleted { + c.b.EntryGone(branch, first, module) + return + } + var e meshcatalog.CatalogEntry + if err := json.Unmarshal(entry, &e); err != nil || e.Module != module { + c.logf("the catalog half of the branch %q holds what is not the entry of %s, left: %v", branch, module, err) + return + } + if err := c.manifests.keep(e); err != nil { + c.logf("%v", err) + } + c.b.Entry(ctx, branch, first, e) + c.mu.Lock() + was := c.named[module] + now := namesOf(e) + if c.named == nil { + c.named = map[string]string{} + } + c.named[module] = now + c.mu.Unlock() + if first && now != "" && now != was { + c.logf("the catalog half's entry names a package of %s: its build is looked at again", module) + c.b.PackageNamed(ctx, module) + } + } +} + +// namesOf are the package names an entry's artifacts carry, in one line. +func namesOf(e meshcatalog.CatalogEntry) string { + var names []string + for _, a := range e.Artifacts { + if a.Package != nil && a.Package.Name != "" { + names = append(names, a.Ref+"="+a.Package.Name) + } + } + slices.Sort(names) + return fmt.Sprint(names) +} + +// sweepWiring is the follower's wiring on the builder's reader that learns each catalog half view adopted, so ready is told once +// when the first view of every branch followed is in (a branch with no revision yet is ready and empty at once). +type sweepWiring struct { + meshcatalogclient.Wiring + half *catalogHalf +} + +func (w sweepWiring) Adopt(f *views.Follower, seatName, event string, hear func(context.Context, []byte) error) error { + if err := w.Wiring.Adopt(f, seatName, event, hear); err != nil { + return err + } + if seatName == meshcatalog.Seat && event == meshcatalog.EventCatalogRevision { + w.half.adopted(f) + } + return nil +} + +func (w sweepWiring) Drop(owner, state string) error { + err := w.Wiring.Drop(owner, state) + w.half.dropped(state) + return err +} + +func (c *catalogHalf) adopted(f *views.Follower) { + c.mu.Lock() + if c.halves == nil { + c.halves = map[string]*views.Follower{} + } + c.halves[f.State] = f + c.mu.Unlock() + go func() { + select { + case <-c.ctx.Done(): + case <-f.Ready(): + c.checkReady() + } + }() +} + +func (c *catalogHalf) dropped(state string) { + c.mu.Lock() + delete(c.halves, state) + c.mu.Unlock() + c.checkReady() +} + +// checkReady calls ready once every adopted half's first view is in. +func (c *catalogHalf) checkReady() { + c.mu.Lock() + all := len(c.halves) > 0 + for _, f := range c.halves { + select { + case <-f.Ready(): + default: + all = false + } + } + c.mu.Unlock() + if all && c.ready != nil { + c.once.Do(c.ready) + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog_test.go new file mode 100644 index 00000000..87768101 --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// TestTheManifestOfAnEntryIsReadByItsDigest: the builder reads a manifest from the entry that holds it, by the entry's manifest +// digest (design 47 G §348), and an entry with no manifest keeps none. +func TestTheManifestOfAnEntryIsReadByItsDigest(t *testing.T) { + m := &entryManifests{} + e := meshcatalog.CatalogEntry{Module: "netcheck", ManifestDigest: "sha256:aa", Manifest: map[string]any{"module": "netcheck", "version": "1"}} + if err := m.keep(e); err != nil { + t.Fatal(err) + } + if mf, err := m.Manifest(context.Background(), "sha256:aa"); err != nil || mf.Module != "netcheck" { + t.Errorf("Manifest(sha256:aa) = %+v, %v", mf, err) + } + if _, err := m.Manifest(context.Background(), "sha256:bb"); err == nil { + t.Error("Manifest of a digest no entry holds: want an error") + } + if err := m.keep(meshcatalog.CatalogEntry{Module: "bare"}); err != nil || len(m.byFp) != 1 { + t.Errorf("an entry with no manifest kept one: %v, %v", err, m.byFp) + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalogholder_integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalogholder_integration_test.go new file mode 100644 index 00000000..6d2950fe --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalogholder_integration_test.go @@ -0,0 +1,94 @@ +//go:build integration + +package main + +import ( + "context" + "encoding/json" + "sync" + "testing" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" +) + +// catalogHolder is the holder of the seat mesh-catalog as the bed needs it (design 47 G §348, §614): it answers `revision` for the +// branch main with the entries put so far, `registrations` with none (so the follower follows the branches the gate's `source` +// answers) and `entry`, and says `catalog-revision` at each put, as the follower and the builder hear it. +type catalogHolder struct { + ctx context.Context + js jetstream.JetStream + + mu sync.Mutex + entries map[string]meshcatalog.CatalogEntry + revision int64 +} + +func newCatalogHolder(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) *catalogHolder { + t.Helper() + h := &catalogHolder{ctx: ctx, js: js, entries: map[string]meshcatalog.CatalogEntry{}} + answer := func(subject string, with func(in []byte) any) { + s, err := nc.Subscribe(subject, func(m *nats.Msg) { + reply, _ := json.Marshal(map[string]any{"result": with(m.Data), "node": "ctl"}) + _ = m.Respond(reply) + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = s.Unsubscribe() }) + } + answer("mesh.seat."+meshcatalog.Seat+".call."+meshcatalog.VerbRevision+".>", func([]byte) any { return h.whole() }) + answer("mesh.seat."+meshcatalog.Seat+".call."+meshcatalog.VerbRegistrations+".>", func([]byte) any { + return meshcatalog.RegistrationsAnswer{Registrations: []meshcatalog.Registration{}} + }) + answer("mesh.seat."+meshcatalog.Seat+".call."+meshcatalog.VerbEntry+".>", func(in []byte) any { + var q meshcatalog.EntryInput + _ = json.Unmarshal(in, &q) + h.mu.Lock() + defer h.mu.Unlock() + for _, e := range h.entries { + if e.Fp == q.Fingerprint { + return meshcatalog.EntryAnswer{Entry: e} + } + } + return map[string]any{"error": "no entry of that fingerprint"} + }) + answer("mesh.seat."+meshdelivery.Seat+".call."+meshdelivery.VerbSource+".>", func([]byte) any { + return meshdelivery.SourceAnswer{Branches: []string{"main"}} + }) + return h +} + +// whole is the newest trunk revision of main, every entry in one page; the branch has none yet before the first put. +func (h *catalogHolder) whole() meshcatalog.RevisionAnswer { + h.mu.Lock() + defer h.mu.Unlock() + if h.revision == 0 { + return meshcatalog.RevisionAnswer{NoneYet: true} + } + var refs []meshcatalog.EntryRef + var all []meshcatalog.CatalogEntry + for m, e := range h.entries { + refs = append(refs, meshcatalog.EntryRef{Module: m, Digest: "sha256:" + e.Fp}) + all = append(all, e) + } + at := time.Now() + return meshcatalog.RevisionAnswer{Revision: &meshcatalog.CatalogRevision{Revision: h.revision, Kind: "trunk", Branch: "main", Entries: refs, Source: 1, At: &at}, Entries: all} +} + +// put sets one module's entry in a new trunk revision of main and says catalog-revision. +func (h *catalogHolder) put(t *testing.T, e meshcatalog.CatalogEntry) { + t.Helper() + h.mu.Lock() + h.revision++ + h.entries[e.Module] = e + ev, _ := json.Marshal(meshcatalog.CatalogRevisionEvent{Branch: "main", Kind: "trunk", Revision: h.revision, Source: 1, Entries: int64(len(h.entries)), Changed: []string{e.Module}, V: 1}) + h.mu.Unlock() + if _, err := h.js.Publish(h.ctx, meshcatalog.SubjectCatalogRevision("main"), ev); err != nil { + t.Fatal(err) + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go index 19619406..3d5831a7 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go @@ -219,20 +219,18 @@ func (b *bed) trunk(t *testing.T, head string) uint64 { } type bed struct { - ctx context.Context - js jetstream.JetStream - modules jetstream.KeyValue - candidates jetstream.KeyValue - source jetstream.KeyValue - builds jetstream.KeyValue - heard jetstream.KeyValue // the state toolchains-heard - instances jetstream.KeyValue - packages jetstream.KeyValue // the indexer's packages, served by its read tool and views family - out *saidLog // the builder's process log, every instance's - stop func() // stops the builder started last - fake *fakeBuildModules - srvURL string - dir string + ctx context.Context + js jetstream.JetStream + catalog *catalogHolder // the seat mesh-catalog, as its holder answers the branch follower + source jetstream.KeyValue + builds jetstream.KeyValue + heard jetstream.KeyValue // the state toolchains-heard + instances jetstream.KeyValue + out *saidLog // the builder's process log, every instance's + stop func() // stops the builder started last + fake *fakeBuildModules + srvURL string + dir string } // startBed starts the bus, the buckets the controller makes from the builder's and the build modules' manifests, the @@ -300,17 +298,12 @@ func startBedWith(t *testing.T, closed bool) *bed { } return kv } - b.modules = mk(indexerBucket(modulesState), modulesHistory) - b.candidates = mk(indexerBucket(candidatesState), candidatesHistory) b.source = mk(sourceBucket, sourceHistory) - mk(indexerBucket(manifestsState), manifestsHistory) - // The indexer and the assigner as the owners of the states the builder follows: their read tools and views families - // (ADR 0615, DLV-M1); n1 and n2 each have their durable consumer. + // The seat mesh-catalog's holder and the assigner as the owners of what the builder follows: the catalog half through the seat's + // verbs and events (design 47 G §348, §614), the assignments by their read tool and views family (ADR 0615, DLV-M1); n1 and n2 + // each have their durable consumer. + b.catalog = newCatalogHolder(t, ctx, js, ctl) readers := []string{"n1_" + builds.BuilderModule, "n2_" + builds.BuilderModule} - for _, st := range []string{modulesState, manifestsState, candidatesState} { - viewstest.Serve(t, ctx, js, ctl, indexerModule, st, indexerModule+".indexer_read_"+st, readers...) - } - b.packages = viewstest.Serve(t, ctx, js, ctl, indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages", readers...) viewstest.Serve(t, ctx, js, ctl, assignerModule, "assignments", assignerModule+".assigner_read_assignments", readers...) b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory) b.instances = mk(instancesBucket, 1) @@ -400,28 +393,34 @@ func (b *bed) moduleFP(t *testing.T, name, fixture, path string, toolchains map[ t.Fatal(err) } digest := meshcatalogclient.ManifestDigest(raw) - manifests, err := b.js.KeyValue(b.ctx, indexerBucket(manifestsState)) - if err != nil { - t.Fatal(err) - } - mv, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: name, Manifest: string(raw)}) - if err != nil { - t.Fatal(err) - } - if _, err := manifests.Put(b.ctx, digest, mv); err != nil { - t.Fatal(err) - } - data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: at, Fp: "src1:" + strings.Repeat("f", 64), Fp2: fp2, - Manifest: digest, Path: path, Toolchains: build.CatalogToolchains(toolchains), Src: int64(src)}) - if err != nil { - t.Fatal(err) - } - if _, err := b.modules.Put(b.ctx, name, data); err != nil { + var manifest map[string]any + if err := json.Unmarshal(raw, &manifest); err != nil { t.Fatal(err) } + b.catalog.put(t, b.entry(name, path, at, fp2, digest, manifest, toolchains, src)) return digest } +// manifestOf is a fixture manifest as the entry holds it. +func (b *bed) manifestOf(t *testing.T, fixture string) map[string]any { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", fixture)) + if err != nil { + t.Fatal(err) + } + var m map[string]any + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + return m +} + +// entry is the catalog graph entry of a module as the holder answers it. +func (b *bed) entry(name, path, at, fp2, digest string, manifest map[string]any, toolchains map[string]builds.ToolchainUsed, src uint64) meshcatalog.CatalogEntry { + return meshcatalog.CatalogEntry{Module: name, Repository: "novox/mesh-catalog", Commit: at, Fp: fp2, Path: path, ManifestDigest: digest, Manifest: manifest, + Inputs: &meshcatalog.EntryInputs{Toolchains: build.CatalogToolchains(toolchains), Src: int64(src)}} +} + // archive writes modules. as the real indexer does for a module of an archive only: its fingerprint names no // toolchain (mesh-delivery-indexer internal/index inputs.go languagesOf gives an archive no language), so Toolchains is empty. func (b *bed) archive(t *testing.T, name, path, at string) string { @@ -455,6 +454,12 @@ type saidLog struct { buf strings.Builder } +func (l *saidLog) String() string { + l.mu.Lock() + defer l.mu.Unlock() + return l.buf.String() +} + func (l *saidLog) Write(p []byte) (int, error) { l.mu.Lock() defer l.mu.Unlock() @@ -572,11 +577,12 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 t.Errorf("%s called %d times, want %d (on one node, ADR 0478 clause 4)", tool, got, want) } } - // Design 32, ADR 0615 §1: a package's name the indexer writes later reaches the builder by its packages' views family - // (never their bucket), and looks at the module again; the done bundles are kept, not numbered twice. - if _, err := b.packages.Put(b.ctx, "netcheck.0.1", []byte(`{"v":1,"name":"netcheck","package":"netcheck/lib","series":"0.1"}`)); err != nil { - t.Fatal(err) - } + // Design 32, design 47 G §348: a package's name the catalog half's entry of the module gives later reaches the builder by the + // branch follower's view (never the indexer's state), and looks at the module again; the done bundles are kept, not numbered twice. + named := b.entry("netcheck", "modules/netcheck", commit, fp, digest, nil, bothToolchains, 0) + named.Manifest = b.manifestOf(t, "catalogue-a67f0196-netcheck.module.json") + named.Artifacts = []meshcatalog.Artifact{{Ref: "netcheck:lib", Language: "go", Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: "netcheck"}}} + b.catalog.put(t, named) b.awaitSaid(t, "names a package of netcheck") // Design 47 G §349: version.numbered is no longer said, for a number or for a late name. if got := reg.numberedOf("netcheck/tools-go"); len(got) != 0 { @@ -639,7 +645,7 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 reg.awaitFinding(t, failed, true) b.fake.hang.Store(nil) close(release) - b.moduleFP(t, "netcheck", netcheck, "modules/netcheck", bothToolchains, commit, third, 3) // the indexer writes it again + b.moduleFP(t, "netcheck", netcheck, "modules/netcheck", bothToolchains, "89abcdef0123456789abcdef0123456789abcdef", third, 3) // the entry moves to a later commit, same fingerprint b.awaitFP(t, "netcheck", third, isState(builds.BuildDone)) b.awaitSaid(t, "kept: fingerprint "+third+" is numbered under it") if !slices.ContainsFunc(b.logged(t, "netcheck"), func(e builds.BuildLogEntry) bool { @@ -739,7 +745,7 @@ func TestAToolchainChangeDoesNotBuildTheOldFingerprint(t *testing.T) { // 1615/A map[string]builds.ToolchainUsed{"go": {Version: "go1.27.2", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}}, commit) b.await(t, "netcheck2", isState(builds.BuildDone)) if _, err := b.builds.Get(b.ctx, mustKey(t, "netcheck")); err == nil { - t.Error("the old fingerprint, naming go1.27.1, was built with go1.27.2") + t.Error("the old fingerprint, naming go1.27.1, was built with go1.27.2; the log: " + b.out.String()) } if n := b.fake.count("mesh-build-go.build_go_build"); n != 1 { t.Errorf("build_go_build was called %d times, want 1 (the new fingerprint on one node only)", n) @@ -778,77 +784,14 @@ func TestAKeyLeftBuildingForAModuleThatIsGoneIsClosedAtStart(t *testing.T) { } } -// TestBuilderBuildsACandidatesFingerprintFromTheHeadMergedOntoTheTrunk: the trunk's record of netcheck carries a fingerprint of its -// own; a candidate the indexer wrote gives netcheck the fingerprint fp at a pull request's head; the program writes -// builds.netcheck. done, its fetch given the head, the ref that carries it and the trunk head of the candidate's source -// revision, newer than the module value's commit (issue 1684; the operator 2026-10-25: a value names the commit its -// fingerprint first appeared at). -func TestBuilderBuildsACandidatesFingerprintFromTheHeadMergedOntoTheTrunk(t *testing.T) { // N7/AC1 +// TestAnImageIsBuiltOnOneNodeAndEveryFingerprintOfTheCatalogHalfPublishes: the catalog half's entry of imaged (one image +// artifact) is built on n1, where mesh-build-image runs: its key is done with the digest and the reference, its build call carrying +// publish; the entry's next fingerprint is built again with publish and ends with a reference of its own (ADR 0487 clauses 5 and +// 12; a run's candidate is no input of the builder: design 47 G A-84). +func TestAnImageIsBuiltOnOneNodeAndEveryFingerprintOfTheCatalogHalfPublishes(t *testing.T) { // 1705 b := startBed(t) other := "src2:" + strings.Repeat("e", 64) - digest := b.module(t, "netcheck", "catalogue-a67f0196-netcheck.module.json", "modules/netcheck") - data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: commit, Fp: "src1:" + strings.Repeat("f", 64), Fp2: other, - Manifest: digest, Path: "modules/netcheck", Toolchains: build.CatalogToolchains(map[string]builds.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}})}) - if err != nil { - t.Fatal(err) - } - if _, err := b.modules.Put(b.ctx, "netcheck", data); err != nil { - t.Fatal(err) - } - const head = "1111111111111111111111111111111111111111" - pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: head, Modules: []string{"netcheck"}, - Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"netcheck": {Fp2: fp, Manifest: digest}}}} - hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}}) - cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Base: int64(b.trunk(t, trunkHead)), Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}) - if err != nil { - t.Fatal(err) - } - ckey, err := keys.EscapeName("feat/x"), error(nil) - if err != nil { - t.Fatal(err) - } - if _, err := b.candidates.Put(b.ctx, ckey, cand); err != nil { - t.Fatal(err) - } - b.start(t, "n1", nil) - v := b.await(t, "netcheck", isState(builds.BuildDone)) - if len(v.Artifacts) != 2 { - t.Fatalf("build = %+v", v) - } - b.fake.mu.Lock() - defer b.fake.mu.Unlock() - found := false - for _, f := range b.fake.fetches { - if f["commit"] == head && f["ref"] == "refs/pull/7/head" && f["base"] == trunkHead { - found = true - } - } - if !found { - t.Errorf("fetches = %v, want one of the candidate's head with its ref and the trunk head it was previewed onto, not the module value's commit", b.fake.fetches) - } -} - -// TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes: modules.imaged (one image artifact) is built on n1, where -// mesh-build-image runs: a trunk record's key is done with the digest and the reference, its build call carrying publish; a -// candidate's key for the same module is built first with no publish and no reference; when the pull request is merged and the -// trunk's record names the candidate's fingerprint, the key is built again with publish and ends with a reference -// (ADR 0487 clauses 5 and 12). -func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 1705 - b := startBed(t) - other := "src2:" + strings.Repeat("e", 64) - digest := b.moduleCommit(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit) - // The trunk carries `other`; its key is built and published. - trunk := func(fp2 string) { - data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: commit, Fp: "src1:" + strings.Repeat("f", 64), Fp2: fp2, - Manifest: digest, Path: "modules/imaged"}) - if err != nil { - t.Fatal(err) - } - if _, err := b.modules.Put(b.ctx, "imaged", data); err != nil { - t.Fatal(err) - } - } - trunk(other) + digest := b.moduleFP(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit, other, 0) b.start(t, "n1", nil) okey, _ := builds.BuilderBuildKey("imaged", other) awaitKey := func(key string, pred func(builds.BuilderBuild) bool) builds.BuilderBuild { @@ -860,42 +803,23 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170 } } } - t.Fatalf("builds.%s = %+v within 20 s", key, last) + t.Fatalf("builds.%s = %+v within 20 s; the log: %s", key, last, b.out.String()) return last } v := awaitKey(okey, isState(builds.BuildDone)) if len(v.Artifacts) != 1 || v.Artifacts[0].Digest != digestOf("server") || !strings.HasSuffix(v.Artifacts[0].Reference, "@"+digestOf("server")) { - t.Fatalf("trunk build = %+v, want the digest and the reference", v) + t.Fatalf("build = %+v, want the digest and the reference", v) } - // The candidate carries fp: built, not published. - const head = "1111111111111111111111111111111111111111" - pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: head, Modules: []string{"imaged"}, - Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"imaged": {Fp2: fp, Manifest: digest}}}} - hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}}) - cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Base: int64(b.trunk(t, trunkHead)), Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}) - if err != nil { - t.Fatal(err) + // The module's next fingerprint: built once more, published. + if got := b.moduleFP(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit, fp, 0); got != digest { + t.Fatalf("manifest digest = %s, want %s", got, digest) } - ckey := keys.EscapeName("feat/x") - if _, err := b.candidates.Put(b.ctx, ckey, cand); err != nil { - t.Fatal(err) - } - v = b.await(t, "imaged", isState(builds.BuildDone)) - if len(v.Artifacts) != 1 || v.Artifacts[0].Reference != "" { - t.Fatalf("candidate build = %+v, want done with no reference", v) - } - b.fake.mu.Lock() - if len(b.fake.builds) != 2 || b.fake.builds[0]["publish"] != true || b.fake.builds[1]["publish"] != nil { - t.Errorf("builds = %v, want the trunk's with publish, then the candidate's with none", b.fake.builds) - } - b.fake.mu.Unlock() - // The pull request is merged: the trunk's record names fp, and its key ends published, built once more with publish. - trunk(fp) - v = b.await(t, "imaged", func(v builds.BuilderBuild) bool { + fkey, _ := builds.BuilderBuildKey("imaged", fp) + v = awaitKey(fkey, func(v builds.BuilderBuild) bool { return v.State == builds.BuildDone && len(v.Artifacts) == 1 && v.Artifacts[0].Reference != "" }) if v.Artifacts[0].Digest != digestOf("server") { - t.Errorf("merged build = %+v", v) + t.Errorf("second build = %+v", v) } b.fake.mu.Lock() defer b.fake.mu.Unlock() @@ -905,8 +829,8 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170 published++ } } - if len(b.fake.builds) != 3 || published != 2 { - t.Errorf("builds = %v, want three, two of them with publish (the trunk's two fingerprints)", b.fake.builds) + if len(b.fake.builds) != 2 || published != 2 { + t.Errorf("builds = %v, want two, both with publish", b.fake.builds) } } diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index adac30c3..6f2702b0 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -30,10 +30,10 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" - "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/trunk" @@ -78,10 +78,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err != nil { return err } - // The indexer's modules, manifests, candidates and packages and the assigner's assignments are their owners' states, read by the - // first view of their read tools and then their views families, never their buckets (ADR 0615 §1, DLV-M1). An owner that - // is down never holds the start: each is read on a goroutine once its first view is answered, and until then the - // unanswered first view is the builder's finding. + // The assigner's assignments are its state, read by the first view of its read tool and then its views family, never its + // bucket (ADR 0615 §1, DLV-M1); the catalog half is read through the seat mesh-catalog. An owner that is down never holds the + // start: each is read on a goroutine once its first view is answered, and until then the unanswered first view is the + // builder's finding. calls, err := process.Connect(cred, nats.Name(cred.Node+"."+cred.Module+".calls"), nats.RetryOnFailedConnect(true)) if err != nil { return err @@ -105,24 +105,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } }) reader.Findings = func() views.WireWriter { return fw } - modules, err := reader.Follow(indexerModule, modulesState, indexerModule+".indexer_read_modules") - if err != nil { - return err - } - manifests, err := reader.Follow(indexerModule, manifestsState, indexerModule+".indexer_read_manifests") - if err != nil { - return err - } - // The candidates' fingerprints are built too (issue 1684). - candidates, err := reader.Follow(indexerModule, candidatesState, indexerModule+".indexer_read_candidates") - if err != nil { - return err - } - // The indexer's packages, whose names a package is numbered under (design 32). - packages, err := reader.Follow(indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages") + // The catalog half per watched branch (design 47 G §348, §479, §614; A-5, A-15): the shared branch follower of + // meshcatalogclient on the builder's reader, so the modules, their manifests, their packages and each package's name in its + // ecosystem are read from the entries, never the indexer's states. + half := &catalogHalf{manifests: &entryManifests{}, logf: logger.Printf, ctx: ctx} + branches, err := meshcatalogclient.NewBranches(sweepWiring{meshcatalogclient.ProcessWiring(reader), half}, calls.Call) if err != nil { return err } + branches.Logf = logger.Printf + half.branches = branches assignments, err := reader.Follow(assignerModule, "assignments", assignerModule+".assigner_read_assignments") if err != nil { return err @@ -171,9 +163,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Ready(func(kv jetstream.KeyValue) { floorWaits.Keep(kvKeeper{ctx: ctx, kv: kv}, heldFloorWaits(ctx, kv, logger.Printf)) }) - // A package is numbered under its name in its ecosystem, read from the indexer's packages state (design 32). + // A package is numbered under its name in its ecosystem, read from the catalog half's entries (design 32, design 47 G §348). numberer := &versions.Numberer{Versions: versions.Versions{Store: awaitedVersions{b: versionsKV}}, Floor: floorVia(calls.Call), Waits: floorWaits, - Names: versions.NamesIn(indexerPackages{packages})} + Names: versions.NamesFrom(versions.EntriesOf(branches))} // A publish that ends badly is a finding (design 51 *Health and conditions*): a holder's refused, a version no holder // answered within its bound (a key of the state bounds with its own time to live), a seat's outcome that does not read. pubBoundsKV := process.AwaitBucket(ctx, js, conn, versions.BoundsBucket, "a publish no holder answers, and a node holding the old form seven days, are not raised", logger.Printf) @@ -187,7 +179,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Gone: heldOnTheBus{js}, CheckAfter: bound, Toolchains: tools, - Manifests: manifestReader{manifests}, + Manifests: half.manifests, Logf: logger.Printf, Stored: storedVia(calls.Call), Node: cred.Node, @@ -245,7 +237,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro trunks.start() b.Trunk = trunks.head b.MemberBounds = &memberBounds{bounds: pubBoundsKV.KV, now: time.Now, logf: logger.Printf} - b.Members = versions.ModuleOf(indexerPackages{packages}) // the build of a group's package member (design 47 G §373, §465) + b.Members = versions.ModuleFrom(versions.EntriesOf(branches)) // the build of a group's package member (design 47 G §373, §465) // The builder's log (ADR 0571 §1): each build's start and end in its module log `build-log`, read by the view, the // indexer and the assigner through their reads, and by the operator's command nox builds through the tool builder_builds_since. b.Log = func(e builds.BuildLogEntry) { @@ -380,11 +372,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } heardGone(ctx, b, m, logger.Printf) } - // The seats' events (artifacts-gone, the registry seats' words, manifest-accepted and manifest-reads-changed and the - // seat mesh-conditions' condition-cleared, which this pattern covers: a consumer takes no two overlapping filters); the - // owners' views events are the views reader's. - if err := hub.Handle("mesh.seat.*.event.>", events); err != nil { - return err + // The seats' events (artifacts-gone, the registry seats' words, the build seats' toolchain-reported, manifest-accepted and + // manifest-reads-changed and the seat mesh-conditions' condition-cleared: seatEventPatterns); the owners' views events and the + // catalog half's catalog-revision and registration-changed and the gate's source-moved are the views reader's (a message has + // one taker, so no pattern here covers them). + for _, pattern := range seatEventPatterns { + if err := hub.Handle(pattern, events); err != nil { + return err + } } hub.OnBound(reads.atBind) // what a node's engine said before the consumer was bound (ADR 0668 §4) if stopped != nil { @@ -397,10 +392,16 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return hub.Start(ctx, js, cred) }) go func() { <-ctx.Done(); hub.Stop() }() - // The modules, once their first views and the manifests' are answered: what a module builds is read from the manifests. - go func() { errs <- buildFromModules(ctx, b, modules, manifests, candidates) }() - // A package's name heard in the indexer's packages looks at its module again, so a done build is numbered then. - go func() { errs <- followPackages(ctx, packages, b, logger.Printf) }() + // The catalog half's entries as they come: a module's entry of a branch goes to the builder, and a key left building for a + // fingerprint no branch carries is closed (issue 1660) once the first view of every followed branch is in (never at the start: + // a holder of the seat that is down holds no other part of it). + half.b = b + half.ready = func() { + if err := b.Sweep(ctx); err != nil { + errs <- err + } + } + branches.Changed = half.Changed(ctx) // A person asks a key's build again with builder_ask_again, which writes the reasks state (issue 1945): watched, as // every other input. The builder asks again by itself without it. // A state not on the bus yet at this start is watched once it appears (issue 1790). @@ -482,55 +483,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // keeper raises a first view unanswered past its bound (ADR 0615 §6). var firstViewsBucket = builds.BuilderModule + "_" + views.StatusState -// The indexer's states the builder follows (indexerBucket(modulesState), ManifestsBucket, CandidatesBucket), and the owners' -// views families as their manifests' `streams` name them. -const ( - modulesState = "modules" - manifestsState = "manifests" - candidatesState = "candidates" -) - -// buildFromModules builds from the indexer's modules and candidates once the first views of the modules, the manifests and -// the candidates are answered (never at the start: an indexer that is down holds no other part of it), and follows them. -func buildFromModules(ctx context.Context, b *build.Builder, modules, manifests, candidates *views.Picture) error { - for _, p := range []*views.Picture{modules, manifests, candidates} { - select { - case <-ctx.Done(): - return nil - case <-p.Follower().Ready(): - } - } - each := func(e jetstream.KeyValueEntry) { - if opOf(e) { - b.Module(ctx, e.Key(), e.Value()) - } else { - b.Forget(e.Key()) - } - } - w, err := settle(ctx, modules, each) - if err != nil { - return err - } - // A key left building for a fingerprint its module no longer has is closed (issue 1660); the modules are all known now. - if err := b.Sweep(ctx); err != nil { - return err - } - // The fingerprints the indexer gives the candidates' modules are built too (issue 1684), once the trunk's modules are known. - eachCandidate := func(e jetstream.KeyValueEntry) { - if opOf(e) { - b.Candidate(ctx, e.Key(), e.Value()) - } else { - b.ForgetCandidate(e.Key()) - } - } - cw, err := settle(ctx, candidates, eachCandidate) - if err != nil { - return err - } - go func() { _ = follow(ctx, cw, eachCandidate) }() - return follow(ctx, w, each) -} - // sourceHeads are the trunk heads of each revision of the gate's source.main the watch delivered, the newest // sourceHistory of them: what a candidate's base revision names. type sourceHeads struct { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go index b52510ea..53c8b837 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go @@ -21,17 +21,27 @@ const ( // sourceBucket is the gate's state source (reads: mesh-delivery-gate.source), the heads a candidate's base names. sourceBucket = gateModule + "_source" - // packagesBucket is the indexer's state packages. - packagesBucket = indexerModule + "_" + meshcatalog.StatePackages - // The revisions each keeps per key (the beds of the integration test create them alike). - sourceHistory = 64 - candidatesHistory = 10 - modulesHistory = 20 - manifestsHistory = 1 + // The revisions the gate's source keeps per key (the bed of the integration test creates it alike). + sourceHistory = 64 ) -func indexerBucket(state string) string { return indexerModule + "_" + state } - // namedTreePrefix opens the tree input of a module whose every artifact names its paths. const namedTreePrefix = "named:" + +// seatEventPatterns are the seats' events the builder's own handler takes off its durable consumer (its manifest's `consumes` of +// them): the registry seats' words, artifacts-gone, the build seats' toolchain-reported, condition-cleared, the node-engine +// seat's events and, of the seat mesh-catalog, manifest-accepted, version-released and versions-filled. The seat mesh-catalog's +// catalog-revision and registration-changed and the seat mesh-delivery's source-moved are the views reader's (the branch +// follower's), so no pattern here covers them: a message has one taker. +var seatEventPatterns = func() []string { + var out []string + for _, seat := range []string{"container-image-registry", "go-package-registry", "npm-package-registry", "oci-artifact-registry", + "mesh-artifact-sweep", "mesh-conditions", "node-engine", "node-build-go", "node-build-image", "node-build-node"} { + out = append(out, "mesh.seat."+seat+".event.>") + } + for _, event := range []string{meshcatalog.EventManifestAccepted, meshcatalog.EventVersionReleased, meshcatalog.EventVersionsFilled} { + out = append(out, "mesh.seat."+meshcatalog.Seat+".event."+event, "mesh.seat."+meshcatalog.Seat+".event."+event+".>") + } + return out +}() diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go index 2c1549b5..3b3efee3 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "slices" + "strings" "testing" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" @@ -32,15 +33,20 @@ func TestTheNamesAreTheManifests(t *testing.T) { t.Errorf("the builder reads %s, its manifest's reads are %v", read, m.Reads) } } - for _, view := range []string{indexerModule + ".views.modules", indexerModule + ".views.manifests", indexerModule + ".views.candidates", - indexerModule + ".views.packages", assignerModule + ".views.assignments"} { + // The catalog half (design 47 G §348, §614) is followed through the seat, and no state of the indexer is. + for _, c := range m.Consumes { + if strings.HasPrefix(c, indexerModule+".") { + t.Errorf("the builder follows %s, an indexer state, its manifest consumes it", c) + } + } + for _, view := range []string{"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", assignerModule + ".views.assignments"} { if !slices.Contains(m.Consumes, view) { t.Errorf("the builder follows %s, its manifest consumes %v", view, m.Consumes) } } for want, got := range map[string]string{ "mesh-delivery-gate_source": sourceBucket, "mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"), - buildrun.ToolchainsHeardBucket: "mesh-delivery-builder_toolchains-heard", "mesh-delivery-indexer_packages": packagesBucket, + buildrun.ToolchainsHeardBucket: "mesh-delivery-builder_toolchains-heard", "mesh-delivery-assigner_assignments": buildrun.AssignmentsBucket, } { if want != got { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go index ed1460ab..24565e05 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go @@ -61,55 +61,6 @@ func (s awaitedVersions) Get(ctx context.Context, key string) ([]byte, uint64, b return e.Value(), e.Revision(), true, nil } -// indexerPackages is the indexer's packages state as the builder reads a package's name in its ecosystem from it (design -// 32): the picture of its read tool's first view and its views family (ADR 0615 §1), never its bucket. Until the first -// view is answered a package is built unnumbered and said (versions.Numberer.Names), and numbered once its name is heard -// (followPackages). -type indexerPackages struct{ kv jetstream.KeyValue } - -func (s indexerPackages) Keys(ctx context.Context) ([]string, error) { - keys, err := s.kv.Keys(ctx) - if errors.Is(err, jetstream.ErrNoKeysFound) { - return nil, nil - } - return keys, err -} - -func (s indexerPackages) Get(ctx context.Context, key string) ([]byte, bool, error) { - e, err := s.kv.Get(ctx, key) - if errors.Is(err, jetstream.ErrKeyNotFound) || errors.Is(err, jetstream.ErrKeyDeleted) { - return nil, false, nil - } - if err != nil { - return nil, false, err - } - return e.Value(), true, nil -} - -// followPackages watches the picture of the indexer's packages state: each series key heard that names a package's name -// (versions.NamedModule), its first view's included, looks at its module again (build.Builder.PackageNamed), so a done -// build that left the package unnumbered is numbered then with its digests. Woken by the views family, never a timer; a -// key heard again naming the name and module it named (the indexer writes a series at each publish) looks at nothing. -func followPackages(ctx context.Context, kv jetstream.KeyValue, b *build.Builder, logf func(string, ...any)) error { - w, err := kv.WatchAll(ctx) - if err != nil { - return err - } - heard := map[string]string{} - return follow(ctx, w, func(e jetstream.KeyValueEntry) { - if e.Operation() != jetstream.KeyValuePut { - delete(heard, e.Key()) - return - } - module, name, named := versions.NamedModule(e.Key(), e.Value()) - if named && heard[e.Key()] != module+" "+name { - heard[e.Key()] = module + " " + name - logf("%s.%s names a package of %s: its build is looked at again", packagesBucket, e.Key(), module) - b.PackageNamed(ctx, module) - } - }) -} - func versionsConflict(err error) error { if errors.Is(err, jetstream.ErrKeyExists) || errors.Is(err, jetstream.ErrKeyRevisionMismatch) { return versions.ErrConflict diff --git a/modules/mesh-delivery-builder/internal/build/entry_test.go b/modules/mesh-delivery-builder/internal/build/entry_test.go new file mode 100644 index 00000000..0eaa2008 --- /dev/null +++ b/modules/mesh-delivery-builder/internal/build/entry_test.go @@ -0,0 +1,28 @@ +package build + +import ( + "reflect" + "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// TestAnEntryOfTheCatalogHalfIsTheFormTheBuilderBuildsBy: a catalog graph entry gives the builder its fingerprint, manifest digest, +// source, the toolchains its fingerprint was computed with, its bases, packages, needs and the named paths of its artifacts by path +// (design 47 G §348, §614). +func TestAnEntryOfTheCatalogHalfIsTheFormTheBuilderBuildsBy(t *testing.T) { + e := meshcatalog.CatalogEntry{Module: "netcheck", Repository: "novox/mesh-catalog", Path: "modules/netcheck", Commit: "abc", Fp: "src2:f", + ManifestDigest: "sha256:m", Bases: []string{"base"}, Packages: map[string]string{"p": "1"}, Needs: []string{"n"}, + Inputs: &meshcatalog.EntryInputs{Src: 7, Toolchains: map[string]meshcatalog.ToolchainUsed{"go": {Version: "go1", Recipe: "1"}}, + Paths: map[string][]string{"lib": {"a/b", "c"}}}} + got := ModuleOf(e) + if got.Fp2 != "src2:f" || got.Manifest != "sha256:m" || got.Path != "modules/netcheck" || got.Commit != "abc" || got.Src != 7 || + got.Toolchains["go"].Version != "go1" || got.Packages["p"] != "1" || !reflect.DeepEqual(got.Needs, []string{"n"}) || + got.Inputs2 == nil || !reflect.DeepEqual(got.Inputs2.Bases, []string{"base"}) || + !reflect.DeepEqual(got.Inputs2.Paths, map[string]string{"a/b": "lib", "c": "lib"}) { + t.Errorf("ModuleOf = %+v", got) + } + if bare := ModuleOf(meshcatalog.CatalogEntry{Module: "m"}); bare.Inputs2 != nil || bare.Fp2 != "" { + t.Errorf("ModuleOf of an entry with none = %+v, want nothing set", bare) + } +} diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index 52402be3..f6f5574b 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -117,7 +117,8 @@ type Builder struct { mu sync.Mutex // fillWaits are the builds that said wait-held for the fill (module.fingerprint), so it is said once. fillWaits sync.Map - known map[string]meshcatalog.ModulesByModuleValue // by modules key + known map[string]meshcatalog.ModulesByModuleValue // by modules key: the first watched branch's + heard map[string]meshcatalog.ModulesByModuleValue // by branch and module (take): what supersedes what inflight map[string]bool // by build key again map[string]meshcatalog.ModulesByModuleValue // by build key: the newest value that arrived while the key was being built plans map[string]planned // by manifest digest @@ -169,7 +170,8 @@ func (b *Builder) init() { } b.sem = make(chan struct{}, limit) b.cands, b.via, b.trunkFP = map[string]meshcatalog.CandidatesByGroupValue{}, map[string]via{}, map[string]bool{} - b.known, b.inflight, b.again, b.plans, b.tried = map[string]meshcatalog.ModulesByModuleValue{}, map[string]bool{}, map[string]meshcatalog.ModulesByModuleValue{}, map[string]planned{}, map[string]bool{} + b.heard, b.known = map[string]meshcatalog.ModulesByModuleValue{}, map[string]meshcatalog.ModulesByModuleValue{} + b.inflight, b.again, b.plans, b.tried = map[string]bool{}, map[string]meshcatalog.ModulesByModuleValue{}, map[string]planned{}, map[string]bool{} b.published, b.held, b.waiting = map[string]bool{}, map[string]bool{}, map[string]map[string]chan string{} b.hands = map[string]*handWatch{} b.memberWaits, b.memberExpired, b.fetched = map[string]bool{}, map[string]bool{}, map[string][]sdkstep.FetchBuild{} @@ -186,8 +188,9 @@ func (b *Builder) logf(format string, args ...any) { // Wait returns when every build started so far has ended. func (b *Builder) Wait() { b.wg.Wait() } -// Module takes one value of the indexer's modules bucket in (the key is the module's name) and builds it if it is -// to be built. A value that is not a module is said and left. +// Module takes one value of a modules bucket in (the key is the module's name) and builds it if it is to be built. A value that +// is not a module is said and left. The builder takes its modules from the catalog half (Entry); this is the same take of one +// module's value, with no branch, the first one's. func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.init() m, err := meshcatalogclient.DecodeModule(value) @@ -195,9 +198,41 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.logf("modules.%s holds what is not a module, left: %v", key, err) return } + b.take(ctx, "", true, key, m) +} + +// Entry takes the catalog graph entry of one module on one watched branch in (design 47 G §348, §614) and builds its +// fingerprint if it is to be built. first says the branch is the first of the module's registration, whose entry is the module's +// value for what the builder reads by name (a version, a build again); an entry of another branch builds its fingerprint and +// supersedes only that branch's older one. An entry naming no module is said and left. +func (b *Builder) Entry(ctx context.Context, branch string, first bool, e meshcatalog.CatalogEntry) { + b.init() + if e.Module == "" { + b.logf("the catalog half of the branch %q holds an entry that names no module, left", branch) + return + } + b.take(ctx, branch, first, e.Module, ModuleOf(e)) +} + +// EntryGone takes a module's entry out of a branch's half (the module was dropped from it, or the branch is followed no more). +func (b *Builder) EntryGone(branch string, first bool, module string) { + b.init() b.mu.Lock() - old, had := b.known[key] - b.known[key] = m + delete(b.heard, branch+"\x00"+module) + b.mu.Unlock() + if first { + b.Forget(module) + } +} + +func (b *Builder) take(ctx context.Context, branch string, first bool, key string, m meshcatalog.ModulesByModuleValue) { + b.mu.Lock() + hk := branch + "\x00" + key + old, had := b.heard[hk] + b.heard[hk] = m + if first { + b.known[key] = m + } var superseded context.CancelCauseFunc if had && old.Fp2 != "" && old.Fp2 != m.Fp2 { if ok, err := builds.BuilderBuildKey(key, old.Fp2); err == nil { @@ -205,7 +240,7 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) { } } if bk, err := builds.BuilderBuildKey(key, m.Fp2); err == nil && m.Fp2 != "" { - // A fingerprint the trunk carries is built from the trunk's commit, whoever asked for it first. + // A fingerprint a watched branch carries is built from that branch's commit, whoever asked for it first. b.trunkFP[bk] = true delete(b.via, bk) } @@ -218,6 +253,32 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.consider(ctx, key, m) } +// ModuleOf is a catalog entry in the form the builder builds by: its fingerprint, manifest digest, source (repository, +// directory, commit, the source revision it was taken in at), the toolchains its fingerprint was computed with, the bases it is +// built against, its packages and needs, and the named paths of its artifacts. +func ModuleOf(e meshcatalog.CatalogEntry) meshcatalog.ModulesByModuleValue { + m := meshcatalog.ModulesByModuleValue{Repository: e.Repository, Path: e.Path, Commit: e.Commit, Fp2: e.Fp, None2: e.None, + Manifest: e.ManifestDigest, Packages: e.Packages, Needs: e.Needs} + if len(e.Bases) > 0 { + m.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{Bases: e.Bases} + } + if in := e.Inputs; in != nil { + m.Src, m.Toolchains, m.Problems = in.Src, in.Toolchains, in.Problems + if len(in.Paths) > 0 { + if m.Inputs2 == nil { + m.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{} + } + m.Inputs2.Paths = map[string]string{} + for artifact, paths := range in.Paths { + for _, p := range paths { + m.Inputs2.Paths[p] = artifact + } + } + } + } + return m +} + // NodesChanged is Reconsider after a toolchain moved, so a build that waited for the nodes to agree on one may go on. func (b *Builder) NodesChanged(ctx context.Context) { b.Reconsider(ctx) } @@ -234,10 +295,14 @@ func (b *Builder) Sweep(ctx context.Context) error { } b.mu.Lock() current := map[string]bool{} - for name, m := range b.known { - if m.Fp2 != "" { - if k, err := builds.BuilderBuildKey(name, m.Fp2); err == nil { - current[k] = true + // What every followed branch carries now is current, not only the first branch's (design 47 G §348). + for _, held := range []map[string]meshcatalog.ModulesByModuleValue{b.known, b.heard} { + for hk, m := range held { + name := hk[strings.LastIndex(hk, "\x00")+1:] + if m.Fp2 != "" { + if k, err := builds.BuilderBuildKey(name, m.Fp2); err == nil { + current[k] = true + } } } } diff --git a/modules/mesh-delivery-builder/internal/versions/entrynames.go b/modules/mesh-delivery-builder/internal/versions/entrynames.go new file mode 100644 index 00000000..a140613f --- /dev/null +++ b/modules/mesh-delivery-builder/internal/versions/entrynames.go @@ -0,0 +1,82 @@ +package versions + +import ( + "context" + "sort" + "strings" + + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// Entries are the catalog graph entries of the halves the builder follows, one per module and watched branch (design 47 G §348, +// §614): where a package's name in its ecosystem is read from, each artifact's `package` (§676), never the indexer's state. +type Entries func() []meshcatalog.CatalogEntry + +// artifactOf is the artifact name of an artifact reference `:` of module. +func artifactOf(module, ref string) (string, bool) { + m, a, ok := strings.Cut(ref, ":") + return a, ok && m == module && a != "" +} + +// NamesFrom answers a package's name in its ecosystem from the entries (design 47 G §348): the name the entry of module gives the +// artifact; known is false when no entry names one for it. +func NamesFrom(all Entries) PackageNames { + return func(_ context.Context, module, artifact string) (string, bool, error) { + for _, e := range all() { + if e.Module != module { + continue + } + for _, a := range e.Artifacts { + if name, ok := artifactOf(module, a.Ref); ok && name == artifact && a.Package != nil && a.Package.Name != "" { + return a.Package.Name, true, nil + } + } + } + return "", false, nil + } +} + +// CarriersFrom are the module artifacts a package's name in its ecosystem is the name of, as the entries hold them (design 47 G +// §572, §478), each distinct module and artifact once, sorted; a name no entry carries answers none. +func CarriersFrom(all Entries) func(ctx context.Context, name string) ([]Built, error) { + return func(_ context.Context, name string) ([]Built, error) { + seen := map[Built]bool{} + var out []Built + for _, e := range all() { + for _, a := range e.Artifacts { + artifact, ok := artifactOf(e.Module, a.Ref) + if !ok || a.Package == nil || a.Package.Name != name || seen[Built{e.Module, artifact}] { + continue + } + seen[Built{e.Module, artifact}] = true + out = append(out, Built{e.Module, artifact}) + } + } + sort.Slice(out, func(i, j int) bool { return out[i].Module+"/"+out[i].Artifact < out[j].Module+"/"+out[j].Artifact }) + return out, nil + } +} + +// ModuleFrom is the module and artifact a package's name is built from, from the entries (CarriersFrom); more than one entry +// carrying it is an error naming them. +func ModuleFrom(all Entries) func(ctx context.Context, name string) (module, artifact string, found bool, err error) { + return moduleOfCarriers(CarriersFrom(all)) +} + +// EntriesOf are the entries of every half the branch follower holds, the followed branches in the order they came. +func EntriesOf(branches *meshcatalogclient.Branches) Entries { + return func() []meshcatalog.CatalogEntry { + var out []meshcatalog.CatalogEntry + for _, br := range branches.Followed() { + if cat, ok := branches.Catalog(br); ok { + if half, ok := cat.View(); ok { + for _, e := range half.Entries { + out = append(out, e) + } + } + } + } + return out + } +} diff --git a/modules/mesh-delivery-builder/internal/versions/entrynames_test.go b/modules/mesh-delivery-builder/internal/versions/entrynames_test.go new file mode 100644 index 00000000..32097c78 --- /dev/null +++ b/modules/mesh-delivery-builder/internal/versions/entrynames_test.go @@ -0,0 +1,55 @@ +package versions + +import ( + "context" + "reflect" + "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +func named(module, artifact, name string) meshcatalog.CatalogEntry { + return meshcatalog.CatalogEntry{Module: module, Artifacts: []meshcatalog.Artifact{ + {Ref: module + ":" + artifact, Language: "go", Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: name}}}} +} + +// TestPackageNamesAreReadFromTheCatalogHalfsEntries: a package's name in its ecosystem is the name the entry of its module gives the +// artifact (design 47 G §348, §676), the module and artifact a name is built from are the entries carrying it, and a name two +// entries carry is an error naming both (§491); no state of the indexer is read. +func TestPackageNamesAreReadFromTheCatalogHalfsEntries(t *testing.T) { + entries := func() []meshcatalog.CatalogEntry { + return []meshcatalog.CatalogEntry{named("mesh-sdk", "go", "git.novox.be/novox/mesh-sdk/go"), named("netcheck", "lib", "netcheck"), + named("twin-a", "lib", "twin"), named("twin-b", "lib", "twin"), {Module: "bare"}} + } + ctx := context.Background() + for _, c := range []struct { + module, artifact string + name string + known bool + }{ + {"mesh-sdk", "go", "git.novox.be/novox/mesh-sdk/go", true}, + {"netcheck", "lib", "netcheck", true}, + {"netcheck", "other", "", false}, + {"bare", "lib", "", false}, + {"unknown", "lib", "", false}, + } { + got, known, err := NamesFrom(entries)(ctx, c.module, c.artifact) + if err != nil || got != c.name || known != c.known { + t.Errorf("NamesFrom(%s/%s) = %q, %v, %v; want %q, %v", c.module, c.artifact, got, known, err, c.name, c.known) + } + } + module, artifact, found, err := ModuleFrom(entries)(ctx, "netcheck") + if module != "netcheck" || artifact != "lib" || !found || err != nil { + t.Errorf("ModuleFrom(netcheck) = %q, %q, %v, %v", module, artifact, found, err) + } + if _, _, found, err := ModuleFrom(entries)(ctx, "nobody"); found || err != nil { + t.Errorf("ModuleFrom(nobody) = %v, %v; want none", found, err) + } + if _, _, _, err := ModuleFrom(entries)(ctx, "twin"); err == nil { + t.Error("ModuleFrom(twin): two entries carry it, want an error") + } + got, _ := CarriersFrom(entries)(ctx, "twin") + if want := []Built{{"twin-a", "lib"}, {"twin-b", "lib"}}; !reflect.DeepEqual(got, want) { + t.Errorf("CarriersFrom(twin) = %v, want %v", got, want) + } +} diff --git a/modules/mesh-delivery-builder/internal/versions/names.go b/modules/mesh-delivery-builder/internal/versions/names.go index bb1881c0..a4b7e796 100644 --- a/modules/mesh-delivery-builder/internal/versions/names.go +++ b/modules/mesh-delivery-builder/internal/versions/names.go @@ -115,7 +115,11 @@ func Carriers(s PackagesState) func(ctx context.Context, name string) ([]Built, // ModuleOf is the module and artifact a package's name is built from (Carriers), found false for a name no entry carries; more // than one entry carrying it is an error naming them. func ModuleOf(s PackagesState) func(ctx context.Context, name string) (module, artifact string, found bool, err error) { - carriers := Carriers(s) + return moduleOfCarriers(Carriers(s)) +} + +// moduleOfCarriers is ModuleOf over one way of finding the carriers. +func moduleOfCarriers(carriers func(ctx context.Context, name string) ([]Built, error)) func(ctx context.Context, name string) (module, artifact string, found bool, err error) { return func(ctx context.Context, name string) (string, string, bool, error) { got, err := carriers(ctx, name) switch { diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index 0a6d1dc5..075fe81e 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -128,9 +128,9 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 // 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no // other tool; it holds no capacity. // 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's `held` is no longer asked (issue 1955). - // DLV-M1 (ADR 0615 §1): the indexer's modules, manifests and candidates and the assigner's assignments are asked by their - // read tools and followed by their views families, never read; so are the indexer's packages, whose names a package is - // numbered under (design 32). + // DLV-M1 (ADR 0615 §1): the assigner's assignments are asked by their read tool and followed by their views family, never + // read; the catalog half (the modules, their manifests, their packages and the package names a package is numbered under) is + // read through the seat mesh-catalog's branch follower (design 47 G §348, §614), and the indexer's states not at all. for _, r := range m.Reads { // The toolchain gate follows the assigner's assignments through meshmoduleassignerclient (buildrun.AssignmentsBucket), so // it declares that one read, as the console and route-proxy do; everything else of theirs is asked by tools. @@ -140,11 +140,10 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 } // Design 51 *The floor*: the floor's three sources, the registry seats' versions, bridge_tags and indexer_requirements. // The watchdogs seat's presence says a node's machine is heard (issue 1966, ADR 0596 §2, ADR 0668 §3). - if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests", - "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions", + if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_requirements", "seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-delivery.source", "seat:container-image-registry.remove", "seat:container-image-registry.versions", "seat:git.tag", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-builds.reserved", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove", - "seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) { + "seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), sorted(want)) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // The builder hears the build seats' toolchain-reported and the node-engine seat's running (design 47 G §492, §546) and reads no @@ -157,10 +156,9 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 "go-package-registry.kept", "go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed", "mesh-artifact-sweep.artifacts-gone", "mesh-catalog.manifest-accepted", "mesh-catalog.version-released", "mesh-catalog.versions-filled", "mesh-conditions.condition-cleared", "mesh-conditions.findings-asked", "mesh-delivery-assigner.views.assignments", "mesh-delivery-forge-bridge.forge.**", - "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", - "mesh-delivery-indexer.views.packages", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.manifest-reads-changed", "node-engine.running", + "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.manifest-reads-changed", "node-engine.running", "npm-package-registry.kept", "npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed", - "oci-artifact-registry.kept", "oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed"}; !reflect.DeepEqual(sorted(m.Consumes), want) { + "oci-artifact-registry.kept", "oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed"}; !reflect.DeepEqual(sorted(m.Consumes), sorted(want)) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" { diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index 8995002a..6f0808a6 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -68,10 +68,9 @@ "oci-artifact-registry.refused", "oci-artifact-registry.removed", "mesh-delivery-assigner.views.assignments", - "mesh-delivery-indexer.views.candidates", - "mesh-delivery-indexer.views.manifests", - "mesh-delivery-indexer.views.modules", - "mesh-delivery-indexer.views.packages" + "mesh-catalog.catalog-revision", + "mesh-catalog.registration-changed", + "mesh-delivery.source-moved" ], "invokes": [ "mesh-delivery-forge-bridge.bridge_tags", @@ -95,10 +94,10 @@ "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions", "mesh-delivery-assigner.assigner_read_assignments", - "mesh-delivery-indexer.indexer_read_candidates", - "mesh-delivery-indexer.indexer_read_manifests", - "mesh-delivery-indexer.indexer_read_modules", - "mesh-delivery-indexer.indexer_read_packages" + "seat:mesh-catalog.entry", + "seat:mesh-catalog.registrations", + "seat:mesh-catalog.revision", + "seat:mesh-delivery.source" ], "state": [ { "name": "builds", "history": 5 },