diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go index 0b1bc2fa..bb068040 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go @@ -15,8 +15,8 @@ import ( nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/buildstep" - "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" ) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go index e0fb6fad..02dcae26 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -395,6 +395,13 @@ func startBed(t *testing.T) *bed { if err != nil { t.Fatal(err) } + // The seat mesh-catalog's holder answers the seat list with no trunk seat named: the checker's reader has then heard the seat + // and holds the bootstrapped definitions, which the judge is handed (ADR 0636 §1). + for _, subject := range []string{"mesh.seat.mesh-catalog.call.seat-definitions", "mesh.seat.mesh-catalog.call.seat-definitions.>"} { + if _, err := ctl.Subscribe(subject, func(m *nats.Msg) { _ = m.Respond([]byte(`{"result":{"revision":1,"seats":[]},"node":"ctl"}`)) }); err != nil { + t.Fatal(err) + } + } ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) t.Cleanup(cancel) b := &bed{ctx: ctx, js: js, buckets: map[string]jetstream.KeyValue{}, tools: map[string]jetstream.KeyValue{}, srvURL: srv.ClientURL()} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go index fd0d494b..9e6a2075 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -28,6 +28,8 @@ type judgeDeps struct { asks check.Asks renames check.Renames seatDefs check.SeatDefinitions + // seats are the seat definitions the manifests are judged against (seatlist.go). + seats check.SeatList tools *buildrun.ToolchainSet trunks *check.Trunks kept check.KV @@ -220,7 +222,7 @@ func newJudge(d judgeDeps) check.Judge { nodes = d.nodes } ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools, - Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, KnownPackages: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, SeatDefinitions: d.seatDefs, Logf: d.logf} + Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, KnownPackages: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, SeatDefinitions: d.seatDefs, SeatList: d.seats, Logf: d.logf} if d.nodes != nil && d.builds != nil && d.repos != nil { ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf} } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index 55cc4c02..55a908d7 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -26,6 +26,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/announce" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/seatlist" "git.novox.be/novox/mesh-sdk/go/views" "mesh-delivery-checker/internal/check" @@ -146,6 +147,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // published, started once both are registered. hub := process.NewEventHub(logger.Printf) reader := &views.ProcessReader{Module: delivery.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub} + seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf)} findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf) reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findings.KV) } // Each by the owner's read tool and views family as the owner's manifest names them. @@ -263,12 +265,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // not there yet a group's check asks and waits in vain until its bound, said in its verdict. tags := tagAsks{js: js, node: cred.Node, heard: &check.PublishedHeard{}, logf: logger.Printf, now: time.Now} ch.Says = tags // a pull request's verdict event, on the same stream asks (design 26 *Out of step*) - if err := hub.Handle("mesh.seat.*.event.>", func(m jetstream.Msg) { - if !tags.heardPublished(m) { - _ = m.Ack() // nothing else the checker consumes rides the shared stream + for subject := range publishedSeats { + if err := hub.Handle(subject, func(m jetstream.Msg) { + if !tags.heardPublished(m) { + _ = m.Ack() // nothing else the checker consumes rides the shared stream + } + }); err != nil { + return err } - }); err != nil { - return err } ch.Judge = newJudge(judgeDeps{ asks: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}, tools: tools, trunks: trunks, @@ -279,6 +283,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro predefined: keptPredefined{predefined}, renames: seatRenames{call: caller.Call}, seatDefs: seatDefinitionsOf(seatDefs), + seats: seats, }) ch.Announce = announce tryer.Judge = ch.Judge diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist.go new file mode 100644 index 00000000..6da7f71e --- /dev/null +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/seatlist.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "fmt" + "maps" + "slices" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/seatlist" +) + +// The checker reads the seat list as every part does (novox/hq ADR 0635 §1, ADR 0636): through the seat mesh-catalog's verb +// seat-definitions, its two events heard as handlers of the module's event hub, from the bootstrapped definitions until the seat +// has answered. What the reader holds is given to catalogue at every change, which the module graph is worked out with, and to the +// indexer's judge as a request's Seats, which the manifests of a change are judged against. + +// followSeatList starts the checker's reader of the seat list over the bus; the hub is started by the caller once every handler is registered. +func followSeatList(bus seatlist.Bus, logf func(string, ...any)) *seatlist.Reader { + reader, err := seatlist.Start(seatlist.Config{Bus: bus, Bootstrap: seatlist.Bootstrapped(), + OnChange: func(seats []catalogue.Seat) { catalogue.UseSeats(seats) }}) + if err != nil { + logf("the checker cannot follow its seat list, so no manifest is judged until it starts again: %v", err) + } + if reader != nil { + catalogue.UseSeats(reader.Seats()) + } + return reader +} + +// seatList is the reader as the check's SeatList: the held definitions raw, and an error while the seat has not answered. +type seatList struct{ reader *seatlist.Reader } + +func (s seatList) Definitions() ([]json.RawMessage, error) { + if s.reader == nil { + return nil, fmt.Errorf("the checker follows no seat list") + } + if st := s.reader.Status(); !st.Heard { + return nil, fmt.Errorf("%s", st.Health()) + } + defs := s.reader.Definitions() + var out []json.RawMessage + for _, seat := range slices.Sorted(maps.Keys(defs)) { + raw, err := json.Marshal(defs[seat]) + if err != nil { + return nil, fmt.Errorf("the definition of %s cannot be encoded: %w", seat, err) + } + out = append(out, raw) + } + return out, nil +} diff --git a/modules/mesh-delivery-checker/internal/check/engines.go b/modules/mesh-delivery-checker/internal/check/engines.go index e6d73e9f..a5a4529e 100644 --- a/modules/mesh-delivery-checker/internal/check/engines.go +++ b/modules/mesh-delivery-checker/internal/check/engines.go @@ -11,7 +11,7 @@ import ( nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - "git.novox.be/novox/mesh-sdk/go/composer" + "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -205,10 +205,14 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s } } } - verdicts := map[string]delivery.ManifestVerdict{} + verdicts := map[string]json.RawMessage{} if e.Verdicts != nil { for name, v := range e.Verdicts() { - verdicts[name] = v + raw, err := json.Marshal(v) + if err != nil { + return a, fmt.Errorf("the verdict of the manifest of %s cannot be encoded: %w", name, err) + } + verdicts[name] = raw } } for _, p := range c.Pulls { diff --git a/modules/mesh-delivery-checker/internal/check/engines_test.go b/modules/mesh-delivery-checker/internal/check/engines_test.go index 5489310d..b5072813 100644 --- a/modules/mesh-delivery-checker/internal/check/engines_test.go +++ b/modules/mesh-delivery-checker/internal/check/engines_test.go @@ -11,7 +11,7 @@ import ( nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - "git.novox.be/novox/mesh-sdk/go/composer" + "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/delivery" ) diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index 5ac80658..b5d20493 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -21,8 +21,8 @@ import ( gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" "git.novox.be/novox/mesh-contracts/go/seats/meshconfig" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -1054,7 +1054,13 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { // trunk's, in a second ask (ADR 0633 clause 6, 1585/T22, T23) indexer string ownJudge bool + // seats is the seat list the checker holds: "read" gives the judge its two definitions as the request's Seats, "unread" + // is a reader the seat has not answered, and the manifests are then judged by nothing (ADR 0636 §1). + seats string }{ + {name: "the seat list read is the request's Seats", seats: "read"}, + {name: "the seat list not read judges nothing", seats: "unread", + fails: "the change's manifests cannot be judged: the seat list is not read (the seat list is held from the bootstrap"}, {name: "a seat definition that does not parse", unparsed: "seats/web-api/seat.json: unexpected end of JSON input", fails: "novox/mesh-delivery#7: the seat definition of web-api (module web-contracts, go) does not parse: seats/web-api/seat.json: unexpected end of JSON input"}, {name: "a rename brought, the verb not served", renames: "bring", seat: "not served", @@ -1162,6 +1168,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web"}, web: {Module: "web", Claims: []catalogue.Claim{{Name: CatalogSeat}}}} } + if row.seats != "" { + b.ex.SeatList = fakeSeatList{read: row.seats == "read"} + } var judged []delivery.ComposeJob answer := b.asks.Answer b.asks.Answer = func(a delivery.StepAsk) delivery.Step { @@ -1207,6 +1216,12 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { }) { t.Errorf("%s: the case says %v, want the new indexer's manifest said accepted by the trunk's manifest validator", row.name, cs.notes) } + if row.seats == "unread" { + if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) || len(judged) != 0 { + t.Errorf("%s: %+v, judged %d, want a fail saying %q before any judge", row.name, v.Failed, len(judged), row.fails) + } + continue + } if row.ownJudge && (len(judged) != 2 || judged[0].Fetch["commit"] != head1 || judged[1].Fetch["commit"] != trunk) { t.Fatalf("%s: judged %+v, want the head's judge and then the trunk's", row.name, judged) } @@ -1224,6 +1239,12 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { req.Cases[0].Manifests["mail"].Digest != strings.Repeat("a", 64) { t.Fatalf("%s: the request %+v: %v", row.name, req, err) } + if row.seats == "read" && (len(req.Seats) != 2 || string(req.Seats[0]) != `{"seat":"a"}` || string(req.Seats[1]) != `{"seat":"b"}`) { + t.Errorf("%s: the request's seats %s, want the two definitions the reader holds", row.name, req.Seats) + } + if row.seats == "" && len(req.Seats) != 0 { + t.Errorf("%s: the request's seats %s, want none from a checker that follows no list", row.name, req.Seats) + } if !reflect.DeepEqual(req.Packages, row.known) || len(req.Input) != 0 && string(req.Input) != "null" { t.Errorf("%s: the judge's known packages %v and input %s, want the known packages %v and no input", row.name, req.Packages, req.Input, row.known) @@ -1242,6 +1263,16 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { } } +// fakeSeatList is the seat list reader as a test gives it: two definitions once read, else the reader's words. +type fakeSeatList struct{ read bool } + +func (f fakeSeatList) Definitions() ([]json.RawMessage, error) { + if !f.read { + return nil, errors.New("the seat list is held from the bootstrap (2 seats) and nothing new is acted on") + } + return []json.RawMessage{json.RawMessage(`{"seat":"a"}`), json.RawMessage(`{"seat":"b"}`)}, nil +} + // knownNames are the packages state's names as a test gives them, each with the / it is built from. type knownNames map[string]string diff --git a/modules/mesh-delivery-checker/internal/check/graph_cycle.go b/modules/mesh-delivery-checker/internal/check/graph_cycle.go index 56766476..444f0b1e 100644 --- a/modules/mesh-delivery-checker/internal/check/graph_cycle.go +++ b/modules/mesh-delivery-checker/internal/check/graph_cycle.go @@ -7,8 +7,8 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/sdk/graph" diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index a7f0b8d4..e119b636 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -137,6 +137,9 @@ type Examiner struct { Manifests TrunkManifests // SeatDefinitions reads the indexer's seat definitions of a pull request (seatdefs.go, design 26); nil reads none. SeatDefinitions SeatDefinitions + // SeatList gives the seat definitions the change's manifests are judged against, as the seat list reader holds them + // (manifests_judge.go, novox/hq ADR 0636 §1); nil gives the judge none. + SeatList SeatList // Renames asks the seat mesh-config's verb renames for a change that brings or drops a `renamed-from` (renames.go, // design 32); nil fails such a change, saying it cannot ask. Renames Renames diff --git a/modules/mesh-delivery-checker/internal/check/manifests_judge.go b/modules/mesh-delivery-checker/internal/check/manifests_judge.go index 7be5f955..41b6b21a 100644 --- a/modules/mesh-delivery-checker/internal/check/manifests_judge.go +++ b/modules/mesh-delivery-checker/internal/check/manifests_judge.go @@ -9,9 +9,9 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/sdk/packagename" @@ -63,6 +63,13 @@ type TrunkManifests interface { ManifestDigests() map[string]string } +// SeatList gives the seat definitions the manifests are judged against, whole, as raw definitions (a request's Seats): the +// list the seat list reader holds. It is an error when the list has not been read from the seat yet, since a manifest judged +// against the bootstrapped seats alone would be refused for a seat it names that the mesh defines (novox/hq ADR 0636 §1). +type SeatList interface { + Definitions() ([]json.RawMessage, error) +} + // PredefinedList is the catalogue's list of predefined keys the indexer keeps (its state predefined, key list), the one a // config value falling back to a key is judged against (ADR 0348 §7); nil when it keeps none. type PredefinedList interface { @@ -124,6 +131,13 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { st.c.note("the manifests are judged against the predefined keys of " + from) } req.Packages = st.knownPackages(ctx) + if st.j.SeatList != nil { + seats, err := st.j.SeatList.Definitions() + if err != nil { + return fail(goModule, "the change's manifests cannot be judged: the seat list is not read ("+err.Error()+")") + } + req.Seats = seats + } cv, trunkCase, v := st.runJudge(ctx, req, touched, commit, at) if v != nil { return v diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index 93b4e6a9..6fe40f36 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -124,7 +124,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 "mesh-delivery-assigner.assigner_read_placement", "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_read_predefined", "mesh-delivery-indexer.indexer_read_seat_definitions", - "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) { + "seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) { t.Errorf("invokes = %v, want the forge's reads and compose-candidate", m.Invokes) } if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) { @@ -133,7 +133,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 // The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts. // And a group's check by the package registry seats' published of its tagged versions (design 51 *Tagged versions*). if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", - "mesh-delivery-assigner.views.placement", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", + "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.placement", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index e1ccb9d1..8d47c3c4 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -15,6 +15,8 @@ ], "consumes": [ "go-package-registry.published", + "mesh-catalog.seat-definition-changed", + "mesh-catalog.seat-definition-removed", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-delivery-assigner.views.placement", @@ -81,6 +83,7 @@ "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_seat_definitions", + "seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"