Indexer: the seat mesh-catalog serves seat-definitions and says seat-definition-changed and -removed for trunks; a definition names its domain and a seat of the mesh's own contracts is taken only from its domain's module, a move there a handover in either order; seats listed from the module's seats/ directory, no index (D-29 verb, D-I3, D-I5, ADR 0636 §4, §5; issue 1966)
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
@@ -12,8 +13,9 @@ import (
|
||||
)
|
||||
|
||||
// catalogRead reads the indexer's modules, manifests and graph states for the seat's verbs.
|
||||
func catalogRead(modules, manifests, nodes stdio.KeptState) index.CatalogRead {
|
||||
func catalogRead(modules, manifests, nodes stdio.KeptState, seatDefinitions, marks views.Source) index.CatalogRead {
|
||||
return index.CatalogRead{
|
||||
SeatDefinitions: seatDefinitions, Marks: marks,
|
||||
Modules: func() (map[string]delivery.Module, error) {
|
||||
keys, err := modules.Keys()
|
||||
if err != nil {
|
||||
@@ -74,5 +76,6 @@ func catalogRead(modules, manifests, nodes stdio.KeptState) index.CatalogRead {
|
||||
|
||||
// catalogTools are the seat's verbs over the indexer's own states, served beside the indexer's own tools, since the indexer's manifest claims the seat (ADR 0604 §2).
|
||||
func catalogTools() []stdio.Tool {
|
||||
return index.CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(graph.State)))
|
||||
return index.CatalogTools(catalogRead(stdio.State("modules"), stdio.State("manifests"), stdio.State(graph.State),
|
||||
keptSource(index.SeatDefinitionsState), keptSource(views.MarksState)))
|
||||
}
|
||||
|
||||
@@ -411,7 +411,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
Named: namedVia(calls.Call), Trunk: trunkVia(calls.Call), Contains: containsVia(calls.Call), Read: readVia(calls.Call),
|
||||
ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call),
|
||||
OwnBuild: ownBuildVia(calls.Call, cred.Node), Graph: awaitedStore{graph},
|
||||
SeatDefinitions: awaitedStore{seatDefs}, ReadFile: readFileVia(calls.Call), SaySeatDefinitionChanged: saySeatDefinitionChanged(js, cred),
|
||||
SeatDefinitions: awaitedStore{seatDefs}, ReadFile: readFileVia(calls.Call), SaySeatDefinition: saySeatDefinition(js, cred),
|
||||
Message: messageVia(calls.Call),
|
||||
// The take-in step (ADR 0633): its state pending and its stream take-in, both this delivery's, bound when they appear;
|
||||
// until then every merge is taken in at once, as before.
|
||||
|
||||
+89
-11
@@ -17,6 +17,7 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
|
||||
|
||||
@@ -80,8 +81,8 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
if seatDefs, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.SeatDefinitionsBucket, History: 5}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "INDEXER_SEAT_DEFINITIONS",
|
||||
Subjects: []string{"mesh.stream.mesh-delivery-indexer.event." + index.SeatDefinitionChangedEvent}}); err != nil {
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "SEAT_MESH_CATALOG",
|
||||
Subjects: []string{"mesh.seat." + index.CatalogSeat + ".event.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// DLV-I19, I20: the indexer's findings and takeins states, and the checker's stream asks with the indexer's durable
|
||||
@@ -212,9 +213,15 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
// request's head whose definition does not parse is kept as its entry naming why while the trunk's stays; it closes
|
||||
// and its entry goes.
|
||||
const contractsMerge, badHead = "5555555555555555555555555555555555555555", "6666666666666666666666666666666666666666"
|
||||
git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","delivers":"git"}`
|
||||
seatFiles[contractsMerge] = map[string]string{"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": git}
|
||||
seatFiles[badHead] = map[string]string{"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": `{"format":1,"seat":"git"}`}
|
||||
git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","domain":"Core","delivers":"git"}`
|
||||
// No index names the seats: the indexer lists the module's `seats/` directory (ADR 0635 §6, D-I5).
|
||||
seatFiles[contractsMerge] = map[string]string{"seats/git/seat.json": git}
|
||||
seatFiles[badHead] = map[string]string{"seats/git/seat.json": `{"format":1,"seat":"git"}`}
|
||||
// The indexer starts with no definition kept (only the bootstrapped seats are the readers'): the verb seat-definitions
|
||||
// answers an empty list at once (ADR 0636 §4, §8).
|
||||
if got, err := index.SeatDefinitionsOf(jsSource{ctx, seatDefs}, nil, ""); err != nil || func() string { raw, _ := json.Marshal(got); return string(raw) }() != `{"revision":0,"seats":[]}` {
|
||||
t.Errorf("seat-definitions of an indexer that kept none = %+v, %v", got, err)
|
||||
}
|
||||
contracts := `{"module":"mesh-contracts","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
|
||||
merged("novox/mesh-contracts", 80, contractsMerge, forgemsg.Forge{Root: "dddddddddddddddddddddddddddddddddddddddd",
|
||||
Trees: map[string]string{"": "dddddddddddddddddddddddddddddddddddddddd"}, Manifests: map[string]string{"": contracts}})
|
||||
@@ -255,13 +262,17 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
t.Fatalf("%s is kept 20 s after its pull request closed", pk)
|
||||
}
|
||||
}
|
||||
info, err := js.Stream(ctx, "INDEXER_SEAT_DEFINITIONS")
|
||||
info, err := js.Stream(ctx, "SEAT_MESH_CATALOG")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Said: the trunk's git, and the closed pull request's removal; the unparsed head says nothing.
|
||||
if n := info.CachedInfo().State.Msgs; n != 2 {
|
||||
t.Errorf("seat-definition-changed said %d times, want 2", n)
|
||||
// Said on the seat's subject: the trunk's git alone; a pull request's head (unparsed, then closed) says nothing (ADR 0636 §4).
|
||||
if n := info.CachedInfo().State.Msgs; n != 1 {
|
||||
t.Errorf("seat-definition-changed said %d times, want 1", n)
|
||||
}
|
||||
if m, err := info.GetLastMsgForSubject(ctx, "mesh.seat.mesh-catalog.event.seat-definition-changed"); err != nil ||
|
||||
!strings.Contains(string(m.Data), `"seat":"git"`) || !strings.Contains(string(m.Data), `"source":"trunk"`) || !strings.Contains(string(m.Data), `"digest":"`+trunk.Digest+`"`) {
|
||||
t.Errorf("seat-definition-changed = %v, %v; want {seat, source, digest} of the trunk's git", m, err)
|
||||
}
|
||||
|
||||
// DLV-I20 [i]: a pull request's check verdict naming out-of-step files keeps them; a trunk build end of the builder's log
|
||||
@@ -311,7 +322,7 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
// DLV-I19 [i]: indexer_seat_definitions_again for a pull request, asked through the takeins state as the tool asks it,
|
||||
// reads the head with its own manifest; indexer_seat_definition then answers the pull request's entry beside the trunk's.
|
||||
const goodHead = "7777777777777777777777777777777777777777"
|
||||
seatFiles[goodHead] = map[string]string{"module.json": contracts, "seats/index.json": `{"format":1,"seats":["git"]}`,
|
||||
seatFiles[goodHead] = map[string]string{"module.json": contracts,
|
||||
"seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0200", 1)}
|
||||
key, value, err := index.NewSeatDefinitionsAgain("mesh-contracts", 83, goodHead, "the forge did not answer", "person.op", time.Now())
|
||||
if err != nil {
|
||||
@@ -326,6 +337,65 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
a.Entry.Commit != goodHead || a.Trunk == nil || a.Trunk.Digest != trunk.Digest {
|
||||
t.Errorf("indexer_seat_definition of the pull request = %+v, %v", a, err)
|
||||
}
|
||||
|
||||
// ADR 0636 §5, D-I3, D-I5: a seat of the mesh's own contracts is taken only from its domain's contracts module, and a move
|
||||
// there is a handover in either merge order of the two trunks. `wants` (Placement) and `vault` (Identity and access) are
|
||||
// held by mesh-contracts as an older indexer kept them.
|
||||
seat := func(name, domain string) string {
|
||||
return `{"format":1,"seat":"` + name + `","scope":"mesh","grain":"seat-list","decision":"d","domain":"` + domain + `"}`
|
||||
}
|
||||
for name, domain := range map[string]string{"wants": "Placement", "vault": "Identity and access"} {
|
||||
var d seatdef.Definition
|
||||
if err := json.Unmarshal([]byte(seat(name, domain)), &d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(d)
|
||||
held, _ := json.Marshal(index.SeatDefinitionEntry{Seat: name, Source: index.SourceTrunk, Repository: "novox/mesh-contracts", Module: "mesh-contracts",
|
||||
Directory: ".", Commit: "old", Digest: "old-" + name, Definition: raw})
|
||||
if _, err := seatDefs.Put(ctx, name+".trunk", held); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
const dropped, placement, identity = "8888888888888888888888888888888888888888", "9999999999999999999999999999999999999999", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
seatFiles[dropped] = map[string]string{"seats/git/seat.json": git} // mesh-contracts without wants and vault
|
||||
seatFiles[placement] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
|
||||
seatFiles[identity] = map[string]string{"seats/vault/seat.json": seat("vault", "Identity and access")}
|
||||
domainModule := func(name string) string {
|
||||
return strings.Replace(contracts, `"mesh-contracts"`, `"`+name+`"`, 1)
|
||||
}
|
||||
// Order 1: the domain's module merges first (its own repository) and takes wants over.
|
||||
merged("novox/mesh-placement", 90, placement, forgemsg.Forge{Root: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||
Trees: map[string]string{"": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}, Manifests: map[string]string{"": domainModule("mesh-placement-contracts")}})
|
||||
entry("wants.trunk", func(e index.SeatDefinitionEntry) bool {
|
||||
return e.Module == "mesh-placement-contracts" && e.Commit == placement
|
||||
})
|
||||
// mesh-contracts then drops both: wants stays the domain module's; vault, whose module has not merged yet, is kept.
|
||||
merged("novox/mesh-contracts", 91, dropped, forgemsg.Forge{Root: "dddddddddddddddddddddddddddddddddddddddd",
|
||||
Trees: map[string]string{"": "dddddddddddddddddddddddddddddddddddddddd"}, Manifests: map[string]string{"": contracts}})
|
||||
entry("git.trunk", func(e index.SeatDefinitionEntry) bool { return e.Commit == contractsMerge })
|
||||
for _, k := range []string{"wants.trunk", "vault.trunk"} {
|
||||
if _, err := seatDefs.Get(ctx, k); err != nil {
|
||||
t.Fatalf("%s is gone after mesh-contracts dropped it: %v", k, err)
|
||||
}
|
||||
}
|
||||
// Order 2 completed: the identity module merges afterwards and takes vault over.
|
||||
merged("novox/mesh-identity", 92, identity, forgemsg.Forge{Root: "ffffffffffffffffffffffffffffffffffffffff",
|
||||
Trees: map[string]string{"": "ffffffffffffffffffffffffffffffffffffffff"}, Manifests: map[string]string{"": domainModule("mesh-identity-and-access-contracts")}})
|
||||
entry("vault.trunk", func(e index.SeatDefinitionEntry) bool {
|
||||
return e.Module == "mesh-identity-and-access-contracts" && e.Commit == identity
|
||||
})
|
||||
// The seat list the verb answers: every trunk seat with its digest, the handed-over ones included, never a pull request's.
|
||||
list, err := index.SeatDefinitionsOf(jsSource{ctx, seatDefs}, nil, "")
|
||||
raw, _ := json.Marshal(list)
|
||||
for _, want := range []string{`"seat":"git"`, `"seat":"vault"`, `"seat":"wants"`} {
|
||||
if err != nil || !strings.Contains(string(raw), want) || strings.Contains(string(raw), `"source":"pull"`) {
|
||||
t.Errorf("seat-definitions = %s, %v; want %s", raw, err, want)
|
||||
}
|
||||
}
|
||||
// Said: git, wants and vault changed once each beyond the first count; no removal for the handed-over seats.
|
||||
if m, err := info.GetLastMsgForSubject(ctx, "mesh.seat.mesh-catalog.event.seat-definition-removed"); err == nil {
|
||||
t.Errorf("a handover said seat-definition-removed: %s", m.Data)
|
||||
}
|
||||
}
|
||||
|
||||
// seatFiles are mesh-contracts' files by commit, as gitea_get_file answers them.
|
||||
@@ -354,6 +424,14 @@ func serveForge(t *testing.T, nc *nats.Conn, listing map[string]map[string]strin
|
||||
if a.Repo == "mesh-sdk" && d == "go" && listing[a.Sha] != nil {
|
||||
files[d] = listing[a.Sha]
|
||||
}
|
||||
if fs, ok := seatFiles[a.Sha]; ok && strings.HasPrefix(d, "seats") { // a contracts module's seats/ directory (D-I5)
|
||||
files[d] = map[string]string{}
|
||||
for p := range fs {
|
||||
if rel, ok := strings.CutPrefix(p, d+"/"); ok {
|
||||
files[d][rel] = "blob"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out["files"] = files
|
||||
}
|
||||
@@ -365,7 +443,7 @@ func serveForge(t *testing.T, nc *nats.Conn, listing map[string]map[string]strin
|
||||
file, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_get_file", func(m *nats.Msg) {
|
||||
var a struct{ Repo, Path, Ref string }
|
||||
_ = json.Unmarshal(m.Data, &a)
|
||||
if a.Repo == "mesh-contracts" { // the seat definitions (DLV-I17): a path the commit does not hold is a 404
|
||||
if _, held := seatFiles[a.Ref]; held { // the seat definitions (DLV-I17): a path the commit does not hold is a 404
|
||||
if v, ok := seatFiles[a.Ref][a.Path]; ok {
|
||||
reply(m, map[string]any{"file": map[string]any{"content": v}})
|
||||
} else {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// The seat definitions' wiring (DLV-I17-I20; design 26): the forge's get-file tool reads them, seat-definition-changed
|
||||
// is said on the indexer's stream indexer-seat-definitions (kept seven days), and the checker's verdict event, heard on
|
||||
// is said on the seat mesh-catalog's own subject (kept on the shared events stream, seven days), and the checker's verdict event, heard on
|
||||
// the indexer's durable consumer of the checker's stream asks, carries a pull request's out-of-step files.
|
||||
|
||||
// checkerAsksStream is the checker's stream asks as the bus names it (its manifest's streams; StreamBusName).
|
||||
@@ -28,16 +28,27 @@ func readFileVia(call toolCaller) func(ctx context.Context, repository, sha, pat
|
||||
return goFileVia(call, "")
|
||||
}
|
||||
|
||||
// saySeatDefinitionChanged publishes seat-definition-changed on the indexer's own stream, its duplicate id the seat,
|
||||
// source and digest, so a message delivered again says it once.
|
||||
func saySeatDefinitionChanged(js jetstream.JetStream, cred process.Account) func(context.Context, index.SeatDefinitionChanged) error {
|
||||
// seatEventSubject is where the seat mesh-catalog, which the indexer holds, says an event: mesh.seat.<seat>.event.<name>
|
||||
// (ADR 0636 §4). The events are the seat's, not the indexer's own, so they are not among the manifest's `emits`.
|
||||
func seatEventSubject(event string) string {
|
||||
return "mesh.seat." + index.CatalogSeat + ".event." + event
|
||||
}
|
||||
|
||||
// saySeatDefinition publishes a trunk's definition moving as seat-definition-changed ({seat, source, digest}), or going as
|
||||
// seat-definition-removed ({seat}), on the seat's subject; its duplicate id is the seat, event and digest, so a message
|
||||
// delivered again says it once. Only a trunk's is said (the index package decides); a reader takes no pull request's.
|
||||
func saySeatDefinition(js jetstream.JetStream, cred process.Account) func(context.Context, index.SeatDefinitionChanged) error {
|
||||
return func(ctx context.Context, c index.SeatDefinitionChanged) error {
|
||||
data, err := json.Marshal(c)
|
||||
event, body := index.SeatDefinitionChangedEvent, any(map[string]string{"seat": c.Seat, "source": c.Source, "digest": c.Digest})
|
||||
if c.Digest == "" {
|
||||
event, body = index.SeatDefinitionRemovedEvent, map[string]string{"seat": c.Seat}
|
||||
}
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := index.SeatDefinitionKey(c.Seat, c.Source, c.Repository, c.Pull) + "." + c.Digest
|
||||
msg := nats.NewMsg(cred.StreamSubject(index.SeatDefinitionChangedEvent))
|
||||
id := c.Seat + "." + event + "." + c.Digest
|
||||
msg := nats.NewMsg(seatEventSubject(event))
|
||||
msg.Header.Set("x-event-id", id)
|
||||
msg.Header.Set("x-source", cred.Module)
|
||||
msg.Header.Set("x-node", cred.Node)
|
||||
|
||||
@@ -83,8 +83,8 @@ type Indexer struct {
|
||||
// ReadFile reads one file of a repository at a commit (gitea.gitea_get_file); a file the commit does not hold is
|
||||
// nil with no error. Nil reads no seat definition.
|
||||
ReadFile func(ctx context.Context, repository, sha, path string) ([]byte, error)
|
||||
// SaySeatDefinitionChanged says seat-definition-changed (seatdefs.go); nil says nothing.
|
||||
SaySeatDefinitionChanged func(ctx context.Context, c SeatDefinitionChanged) error
|
||||
// SaySeatDefinition says seat-definition-changed (seatdefs.go); nil says nothing.
|
||||
SaySeatDefinition func(ctx context.Context, c SeatDefinitionChanged) error
|
||||
// Graph is the module graph's state (graph.Bucket, graph.go; novox/hq ADR 0603 §2); nil writes no graph.
|
||||
Graph Store
|
||||
// Files asks the forge for every blob under each directory at a commit, by directory then path relative to it, each
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
)
|
||||
@@ -28,6 +29,9 @@ type CatalogRead struct {
|
||||
Manifest func(digest string) (string, error)
|
||||
// Graph is the module graph, by module.
|
||||
Graph func() (graph.Graph, error)
|
||||
// SeatDefinitions and Marks are the indexer's state of the seat definitions and of the revisions its views said it
|
||||
// up to, which the verb seat-definitions answers from (seatdefs.go); nil answers the verb nothing.
|
||||
SeatDefinitions, Marks views.Source
|
||||
}
|
||||
|
||||
func entryOf(name string, m delivery.Module) catalogue.CatalogEntry {
|
||||
@@ -153,10 +157,7 @@ func (c CatalogRead) graphAndModules() (graph.Graph, map[string]delivery.Module,
|
||||
// seat's contract gives them (novox/hq ADR 0604 §1, §2), answered from the indexer's own states. They are served once the
|
||||
// indexer's manifest claims the seat, in the release after L3's (the lead's decision): until then main serves them not.
|
||||
func CatalogTools(c CatalogRead) []stdio.Tool {
|
||||
seat, ok := catalogue.SeatNamed(catalogue.CatalogSeat)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
seat, _ := catalogue.SeatNamed(CatalogSeat)
|
||||
str := func(args map[string]any, k string) string { s, _ := args[k].(string); return s }
|
||||
runs := map[string]func(map[string]any) (any, error){
|
||||
"modules": func(map[string]any) (any, error) { return c.CatalogModulesAnswer() },
|
||||
@@ -167,8 +168,24 @@ func CatalogTools(c CatalogRead) []stdio.Tool {
|
||||
var out []stdio.Tool
|
||||
for _, v := range seat.Serves {
|
||||
if run, ok := runs[v.Name]; ok {
|
||||
out = append(out, stdio.Tool{Name: catalogue.CatalogSeat + "." + v.Name, Description: v.Description, Input: v.Input, Run: run})
|
||||
out = append(out, stdio.Tool{Name: CatalogSeat + "." + v.Name, Description: v.Description, Input: v.Input, Run: run})
|
||||
}
|
||||
}
|
||||
if c.SeatDefinitions != nil {
|
||||
out = append(out, stdio.Tool{Name: CatalogSeat + "." + SeatDefinitionsVerb, Description: SeatDefinitionsDescription, Input: SeatDefinitionsInput,
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return SeatDefinitionsOf(c.SeatDefinitions, c.Marks, str(a, "seat"))
|
||||
}})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SeatDefinitionsDescription and SeatDefinitionsInput are the verb seat-definitions of the seat mesh-catalog (ADR 0636 §4).
|
||||
const SeatDefinitionsDescription = "The seat list: the definitions of the seats the trunks hold. With no seat, every trunk seat with the digest of " +
|
||||
"its definition and the revision of the state it is read from; with a seat, that seat's trunk definition, resolved. Only the " +
|
||||
"merged definitions, never a pull request's (indexer_seat_definition answers those). Every reader of the seat list reads it " +
|
||||
"here, and follows seat-definition-changed and seat-definition-removed."
|
||||
|
||||
// SeatDefinitionsInput is the verb's input.
|
||||
var SeatDefinitionsInput = map[string]any{"type": "object", "properties": map[string]any{
|
||||
"seat": map[string]any{"type": "string", "description": "one seat's name; absent for the list of every trunk seat"}}}
|
||||
|
||||
@@ -76,6 +76,11 @@ func catalogVerbsAnswerFromTheStates(t *testing.T) {
|
||||
}
|
||||
return g, nil
|
||||
},
|
||||
// The verb seat-definitions answers from the seat definitions state: the trunk's git, a pull request's git beside it.
|
||||
SeatDefinitions: memSource{
|
||||
"git.trunk": {Value: []byte(`{"seat":"git","source":"trunk","digest":"d1","definition":{"seat":"git"}}`)},
|
||||
SeatDefinitionKey("git", SourcePull, "r", 1): {Value: []byte(`{"seat":"git","source":"pull","digest":"d2"}`)},
|
||||
},
|
||||
}
|
||||
answer := func(v any, err error) []byte {
|
||||
t.Helper()
|
||||
@@ -97,13 +102,21 @@ func catalogVerbsAnswerFromTheStates(t *testing.T) {
|
||||
var names []string
|
||||
for _, tool := range CatalogTools(c) {
|
||||
names = append(names, tool.Name)
|
||||
if tool.Name == "mesh-catalog.seat-definitions" {
|
||||
if got, err := tool.Run(nil); err != nil || string(answer(got, nil)) != `{"revision":0,"seats":[{"seat":"git","source":"trunk","digest":"d1"}]}` {
|
||||
t.Errorf("mesh-catalog.seat-definitions answered %s, %v", answer(got, nil), err)
|
||||
}
|
||||
if got, err := tool.Run(map[string]any{"seat": "git"}); err != nil || !strings.Contains(string(answer(got, nil)), `"found":true`) {
|
||||
t.Errorf("mesh-catalog.seat-definitions of git answered %s, %v", answer(got, nil), err)
|
||||
}
|
||||
}
|
||||
if tool.Name == "mesh-catalog.modules" {
|
||||
if got, err := tool.Run(nil); err != nil || !strings.Contains(string(answer(got, nil)), `"module":"avahi"`) {
|
||||
t.Errorf("mesh-catalog.modules answered %v, %v", got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !slices.Equal(names, []string{"mesh-catalog.modules", "mesh-catalog.module", "mesh-catalog.provides", "mesh-catalog.dependents"}) {
|
||||
if !slices.Equal(names, []string{"mesh-catalog.modules", "mesh-catalog.module", "mesh-catalog.provides", "mesh-catalog.dependents", "mesh-catalog.seat-definitions"}) {
|
||||
t.Errorf("the tools bundle serves %v", names)
|
||||
}
|
||||
raw := answer(c.CatalogModulesAnswer())
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
@@ -122,23 +123,36 @@ func TestTheControlRecords(t *testing.T) {
|
||||
t.Run("seat definitions", func(t *testing.T) {
|
||||
const repo, m1, m2, m3, head = "novox/mesh-contracts", "m1", "m2", "m3", "h1"
|
||||
manifest := `{"module":"mesh-contracts","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"series":"0.1","generated-from":"."}]}}`
|
||||
git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","delivers":"git"}`
|
||||
git := `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"novox/hq ADR 0111","domain":"Core","delivers":"git"}`
|
||||
files := map[string]map[string]string{
|
||||
m1: {"seats/index.json": `{"format":1,"seats":["git","gone"]}`, "seats/git/seat.json": git, "seats/gone/seat.json": strings.Replace(git, `"git"`, `"gone"`, 1)},
|
||||
m2: {"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"x","oops":1}`},
|
||||
m3: {"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0112", 1)},
|
||||
head: {"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0200", 1)},
|
||||
m1: {"seats/git/seat.json": git, "seats/gone/seat.json": strings.Replace(git, `"git"`, `"gone"`, 1)},
|
||||
m2: {"seats/git/seat.json": `{"format":1,"seat":"git","scope":"mesh","grain":"seat-list","decision":"x","domain":"Core","oops":1}`},
|
||||
m3: {"seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0112", 1)},
|
||||
head: {"seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0200", 1)},
|
||||
}
|
||||
defs, findings := newMem(), newMem()
|
||||
var said []SeatDefinitionChanged
|
||||
ix := &Indexer{SeatDefinitions: defs, Findings: findings, Logf: t.Logf,
|
||||
// The forge's listing of a contracts module's `seats/` directory: every blob under it at the commit (ADR 0635 §6).
|
||||
Files: func(_ context.Context, _, sha string, dirs []string) (map[string]map[string]string, error) {
|
||||
out := map[string]map[string]string{}
|
||||
for _, d := range dirs {
|
||||
out[d] = map[string]string{}
|
||||
for p := range files[sha] {
|
||||
if rel, ok := strings.CutPrefix(p, d+"/"); ok {
|
||||
out[d][rel] = "blob"
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
ReadFile: func(_ context.Context, r, sha, p string) ([]byte, error) {
|
||||
if v, ok := files[sha][p]; ok && r == repo {
|
||||
return []byte(v), nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
SaySeatDefinitionChanged: func(_ context.Context, c SeatDefinitionChanged) error { said = append(said, c); return nil }}
|
||||
SaySeatDefinition: func(_ context.Context, c SeatDefinitionChanged) error { said = append(said, c); return nil }}
|
||||
ctx := context.Background()
|
||||
merged := func(merge string) {
|
||||
t.Helper()
|
||||
@@ -186,8 +200,8 @@ func TestTheControlRecords(t *testing.T) {
|
||||
// Ruling (lead, on A2i's finding 4): another repository's trunk defining git is refused, the trunk's entry stands,
|
||||
// nothing is said, and its finding names both repositories; its next merge without git clears the finding.
|
||||
const other = "novox/other-contracts"
|
||||
files["o1"] = map[string]string{"seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0300", 1)}
|
||||
files["o2"] = map[string]string{"seats/index.json": `{"format":1,"seats":[]}`}
|
||||
files["o1"] = map[string]string{"seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0300", 1)}
|
||||
files["o2"] = map[string]string{}
|
||||
read := ix.ReadFile
|
||||
ix.ReadFile = func(ctx context.Context, r, sha, p string) ([]byte, error) {
|
||||
if r == other {
|
||||
@@ -271,7 +285,7 @@ func TestTheControlRecords(t *testing.T) {
|
||||
ix.Manifests.(*memStore).vals[mkey] = kept
|
||||
mod, _ := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: delivery.ManifestDigest([]byte(plain)), Repository: repo, Commit: m3, Path: delivery.RootPath})
|
||||
ix.Modules.(*memStore).vals["mesh-contracts"] = mod
|
||||
files["h2"] = map[string]string{"module.json": manifest, "seats/index.json": `{"format":1,"seats":["git"]}`, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0400", 1)}
|
||||
files["h2"] = map[string]string{"module.json": manifest, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0400", 1)}
|
||||
if r := ix.SeatDefinitionsAgain(ctx, "mesh-contracts", 9, "h2"); !r.Stored {
|
||||
t.Errorf("again for the pull request's head = %+v, want it read with the head's generated-from", r)
|
||||
}
|
||||
@@ -291,8 +305,107 @@ func TestTheControlRecords(t *testing.T) {
|
||||
if _, ok := defs.vals[OutOfStepKey(repo, 9)]; ok {
|
||||
t.Error("a closed pull request's out-of-step files are kept")
|
||||
}
|
||||
if last := said[len(said)-1]; last != (SeatDefinitionChanged{Seat: "git", Source: SourcePull, Repository: repo, Pull: 9}) {
|
||||
t.Errorf("last said %+v, want the pull request's removal", last)
|
||||
for _, c := range said {
|
||||
if c.Source != SourceTrunk {
|
||||
t.Errorf("said %+v: only a trunk's definition is said (ADR 0636 §4)", c)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0636 §5, D-I3, D-I5: a seat of the mesh's own contracts is taken only from its domain's contracts module; a
|
||||
// move there is a handover in either order of the two trunks; a second definer is refused with a finding naming both.
|
||||
// The seats `wants` (Placement) and `vault` (Identity and access) are held by mesh-contracts as an older indexer
|
||||
// kept them; their definitions are listed from the module's `seats/` directory with no index.
|
||||
seat := func(name, domain string) string {
|
||||
return `{"format":1,"seat":"` + name + `","scope":"mesh","grain":"seat-list","decision":"d","domain":"` + domain + `"}`
|
||||
}
|
||||
for _, name := range []string{"wants", "vault"} {
|
||||
dom := map[string]string{"wants": "Placement", "vault": "Identity and access"}[name]
|
||||
var d seatdef.Definition
|
||||
if err := json.Unmarshal([]byte(seat(name, dom)), &d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, digest, _ := digestOf(d)
|
||||
data, _ := json.Marshal(SeatDefinitionEntry{Seat: name, Source: SourceTrunk, Repository: repo, Module: "mesh-contracts", Directory: ".",
|
||||
Commit: "old", Digest: digest, Definition: raw})
|
||||
defs.vals[name+".trunk"] = data
|
||||
}
|
||||
files["c1"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement"), "seats/vault/seat.json": seat("vault", "Identity and access"),
|
||||
"seats/lone/seat.json": seat("lone", "Core")}
|
||||
files["c2"] = map[string]string{"seats/lone/seat.json": seat("lone", "Core")}
|
||||
files["p1"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
|
||||
files["i1"] = map[string]string{"seats/vault/seat.json": seat("vault", "Identity and access")}
|
||||
domainManifest := func(module string) map[string]string {
|
||||
return map[string]string{delivery.RootPath: strings.Replace(manifest, `"mesh-contracts"`, `"`+module+`"`, 1)}
|
||||
}
|
||||
mergeAt := func(repository, merge string, manifests map[string]string) {
|
||||
t.Helper()
|
||||
ix.ReadFile = func(_ context.Context, _, sha, p string) ([]byte, error) { return []byte(files[sha][p]), nil }
|
||||
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repository, Number: 3, Merge: merge, Manifests: manifests}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
outside := health.FindingKey(health.ScopeModule, findingID("seat-outside-its-domain", "mesh-contracts"), SeatOutsideItsDomainKind)
|
||||
before = len(said)
|
||||
// mesh-contracts defines wants and vault, seats of other domains: refused with a finding naming both modules, the
|
||||
// held entries stand, and lone (Core) is taken.
|
||||
mergeAt(repo, "c1", map[string]string{delivery.RootPath: manifest})
|
||||
if v, ok := findings.vals[outside]; !ok || !strings.Contains(string(v), "mesh-placement-contracts") || !strings.Contains(string(v), "mesh-identity-and-access-contracts") ||
|
||||
!strings.Contains(string(v), "mesh-contracts defines it") {
|
||||
t.Errorf("finding %s = %s, want it naming both modules", outside, v)
|
||||
}
|
||||
if w, _ := entry("wants.trunk"); w.Commit != "old" {
|
||||
t.Errorf("a refused definition replaced the held one: %+v", w)
|
||||
}
|
||||
if l, ok := entry("lone.trunk"); !ok || l.Commit != "c1" {
|
||||
t.Errorf("a Core seat from mesh-contracts is not taken: %+v (%v)", l, ok)
|
||||
}
|
||||
// Order 1: the domain's module merges first (another repository) and takes wants over; mesh-contracts then drops it and
|
||||
// the entry stays the domain module's, nothing removed.
|
||||
mergeAt("novox/mesh-placement", "p1", domainManifest("mesh-placement-contracts"))
|
||||
if w, _ := entry("wants.trunk"); w.Module != "mesh-placement-contracts" || w.Commit != "p1" {
|
||||
t.Errorf("the domain's module did not take wants over: %+v", w)
|
||||
}
|
||||
mergeAt(repo, "c2", map[string]string{delivery.RootPath: manifest})
|
||||
if w, _ := entry("wants.trunk"); w.Module != "mesh-placement-contracts" {
|
||||
t.Errorf("mesh-contracts dropping wants removed or replaced the domain module's entry: %+v", w)
|
||||
}
|
||||
if _, ok := findings.vals[outside]; ok {
|
||||
t.Error("the outside-its-domain finding stands once mesh-contracts defines only its own domain's seats")
|
||||
}
|
||||
// Order 2: mesh-contracts drops vault first, and the last trunk definition is kept (no removal said) until the
|
||||
// domain's module defines it.
|
||||
if v, _ := entry("vault.trunk"); v.Module != "mesh-contracts" {
|
||||
t.Fatalf("vault was not kept by mesh-contracts after it dropped it: %+v", v)
|
||||
}
|
||||
mergeAt("novox/mesh-identity", "i1", domainManifest("mesh-identity-and-access-contracts"))
|
||||
if v, _ := entry("vault.trunk"); v.Module != "mesh-identity-and-access-contracts" || v.Commit != "i1" {
|
||||
t.Errorf("the domain's module did not take vault over: %+v", v)
|
||||
}
|
||||
for _, c := range said[before:] {
|
||||
if c.Digest == "" && (c.Seat == "wants" || c.Seat == "vault") {
|
||||
t.Errorf("said %+v: a handover removes nothing", c)
|
||||
}
|
||||
}
|
||||
// A pull request of mesh-contracts defining a Placement seat fails its check as a definition that does not parse.
|
||||
files["h2"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
|
||||
ix.ReadFile = func(_ context.Context, _, sha, p string) ([]byte, error) { return []byte(files[sha][p]), nil }
|
||||
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgePull, Action: forgemsg.PullUpdated, Repository: repo, Number: 11, Head: "h2",
|
||||
Dirs: []string{delivery.RootPath}, Manifests: map[string]string{delivery.RootPath: manifest}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e, _ := entry(SeatDefinitionKey("wants", SourcePull, repo, 11)); !strings.Contains(e.Unparsed, "mesh-placement-contracts") || e.Definition != nil {
|
||||
t.Errorf("a pull request's wants from mesh-contracts = %+v, want it refused naming the domain's module", e)
|
||||
}
|
||||
// The verb seat-definitions: no seat lists every trunk seat with its digest (never a pull request's); a seat answers it whole.
|
||||
listed, err := SeatDefinitionsOf(src(), nil, "")
|
||||
raw, _ := json.Marshal(listed)
|
||||
if err != nil || !strings.Contains(string(raw), `"seat":"wants","source":"trunk","digest":"`) || strings.Contains(string(raw), "pull") || strings.Contains(string(raw), "_out-of-step") {
|
||||
t.Errorf("the verb with no seat = %s, %v", raw, err)
|
||||
}
|
||||
one, err := SeatDefinitionsOf(src(), nil, "vault")
|
||||
raw, _ = json.Marshal(one)
|
||||
if err != nil || !strings.Contains(string(raw), `"found":true`) || !strings.Contains(string(raw), `"definition"`) {
|
||||
t.Errorf("the verb with a seat = %s, %v", raw, err)
|
||||
}
|
||||
})
|
||||
t.Run("packages per series", func(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package index
|
||||
|
||||
// Where a seat of the mesh's own contracts may be defined (novox/hq ADR 0636 §5, ADR 0634 §7): a definition names its
|
||||
// glossary domain; the indexer accepts a seat of the mesh's own contracts only from that domain's contracts module
|
||||
// (mesh-<domain>-contracts), or from mesh-contracts for a domain with none. A second definer is refused with a finding
|
||||
// naming both. A definition moving from mesh-contracts to its domain's contracts module is a handover: the new
|
||||
// definer takes the seat over whichever trunk merges first, and a removal from the module that is not the seat's
|
||||
// domain's keeps the last trunk definition until the domain's module defines it.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// fallbackContracts is the mesh's contracts module for a domain with no contracts module of its own.
|
||||
const fallbackContracts = "mesh-contracts"
|
||||
|
||||
// domainsWithContracts are the domains whose seats have a contracts module of their own (ADR 0634 §7): the seven
|
||||
// the milestone creates. Every other domain's seats are defined in fallbackContracts.
|
||||
var domainsWithContracts = []string{"Delivery", "Placement", "Provisioning", "Identity and access", "Health and repair",
|
||||
"Connectivity", "Operator and conversation"}
|
||||
|
||||
// domainContracts is the module that defines a domain's seats: mesh-<domain>-contracts, the heading lowercased with
|
||||
// its spaces as dashes, or fallbackContracts for a domain with none.
|
||||
func domainContracts(domain string) string {
|
||||
if !slices.Contains(domainsWithContracts, domain) {
|
||||
return fallbackContracts
|
||||
}
|
||||
return "mesh-" + strings.ReplaceAll(strings.ToLower(domain), " ", "-") + "-contracts"
|
||||
}
|
||||
|
||||
// ownContracts says a contracts module is one of the mesh's own: fallbackContracts or a domain's.
|
||||
func ownContracts(module string) bool {
|
||||
return module == fallbackContracts || slices.ContainsFunc(seatdef.Domains, func(d string) bool {
|
||||
return module == domainContracts(d) && module != fallbackContracts
|
||||
})
|
||||
}
|
||||
|
||||
// domainRefusal is, for a definition a contracts module of the mesh's own holds, why it may not: the words naming the
|
||||
// module and the domain's. Empty when the module may define the seat (a module outside the mesh's own contracts defines
|
||||
// what it likes, ADR 0625 §7).
|
||||
func domainRefusal(module string, d seatdef.Definition) string {
|
||||
if !ownContracts(module) {
|
||||
return ""
|
||||
}
|
||||
if want := domainContracts(d.Domain); want != module {
|
||||
return fmt.Sprintf("it is a seat of the domain %q, whose seats %s defines, and %s defines it", d.Domain, want, module)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// domainOf is the domain a kept definition names ("" when it is none or does not decode).
|
||||
func domainOf(raw json.RawMessage) string {
|
||||
var d struct {
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &d)
|
||||
return d.Domain
|
||||
}
|
||||
|
||||
// handsOver says a kept trunk entry is handed over to a definer: the entry is held by a module of the mesh's own
|
||||
// that is not the seat's domain's, and the definer is the seat's domain's contracts module.
|
||||
func handsOver(cur SeatDefinitionEntry, module string, d seatdef.Definition) bool {
|
||||
return cur.Module != module && ownContracts(cur.Module) && domainContracts(d.Domain) == module
|
||||
}
|
||||
|
||||
// handoverPending says a trunk entry its holder no longer defines waits for its domain's module: the holder is a
|
||||
// module of the mesh's own and not the seat's domain's, so the last trunk definition stays until that module defines it.
|
||||
func handoverPending(cur SeatDefinitionEntry) bool {
|
||||
return ownContracts(cur.Module) && domainContracts(domainOf(cur.Definition)) != cur.Module
|
||||
}
|
||||
@@ -692,7 +692,7 @@ func (ix *Indexer) dry() *dryRun {
|
||||
// The registry milestone's fields: what a take-in would write on the bus stays out of a dry run, so the states of the
|
||||
// requirements, findings, seat definitions, holds and their bounds are not overlaid, nothing is said, and no hold is
|
||||
// weighed (Holds and Counted nil hold nothing); ReadFile only reads.
|
||||
Requirements: nil, Findings: nil, SeatDefinitions: nil, ReadFile: ix.ReadFile, SaySeatDefinitionChanged: nil,
|
||||
Requirements: nil, Findings: nil, SeatDefinitions: nil, ReadFile: ix.ReadFile, SaySeatDefinition: nil,
|
||||
Holds: nil, Counted: nil, AskEngines: nil, HoldBounds: nil}
|
||||
x.Logf = func(format string, args ...any) { ix.logf("take-in, computed and not written: "+format, args...) }
|
||||
x.Store, d.candidates = over(ix.Store)
|
||||
|
||||
@@ -521,7 +521,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
|
||||
for _, f := range []string{"Store", "Modules", "Manifests", "Predefined", "ConfigCandidates", "Taken", "Verdicts", "OldSections",
|
||||
"JudgeID", "SayRefused", "SayAccepted", "Packages", "Graph", "Files", "GoSum", "GoMod", "Now", "Builds", "Named", "Trunk",
|
||||
"Read", "Contains", "Logf", "ReadOpen", "OwnBuild", "Message", "Pending", "SayTakeIn",
|
||||
"Requirements", "Findings", "SeatDefinitions", "ReadFile", "SaySeatDefinitionChanged", "Holds", "Counted", "AskEngines", "HoldBounds"} {
|
||||
"Requirements", "Findings", "SeatDefinitions", "ReadFile", "SaySeatDefinition", "Holds", "Counted", "AskEngines", "HoldBounds"} {
|
||||
named[f] = true
|
||||
}
|
||||
ty := reflect.TypeOf(Indexer{})
|
||||
|
||||
@@ -7,7 +7,8 @@ package index
|
||||
// and source, as its only writer. Only the trunk's entry is the seat's contract; a pull request's entries go when it
|
||||
// closes or merges. A trunk definition that does not parse keeps the trunk's entry and writes the finding
|
||||
// seat-definition-unparsed; a trunk defining a seat another repository's trunk already defines is refused with the
|
||||
// finding seat-defined-twice naming both (a seat has one definition); generated types out of step on a trunk, said by
|
||||
// finding seat-defined-twice naming both (a seat has one definition); a seat of the mesh's own contracts is taken only
|
||||
// from its domain's contracts module, and a move there is a handover (ADR 0636 §5, domain.go); generated types out of step on a trunk, said by
|
||||
// the builder's build end, write the finding generated-types-out-of-step, cleared only by a clean build end of the same
|
||||
// contracts module.
|
||||
|
||||
@@ -36,15 +37,20 @@ const (
|
||||
// SeatDefinitionsState is the indexer's state of the seat definitions (design 26), one key per seat and source.
|
||||
SeatDefinitionsState = "seat-definitions"
|
||||
SeatDefinitionsBucket = delivery.IndexerModule + "_" + SeatDefinitionsState
|
||||
// SeatDefinitionChangedEvent is said, kept seven days on the indexer's stream SeatDefinitionsStream, only when a
|
||||
// definition's digest changed (ADR 0608 §3).
|
||||
// The seat mesh-catalog's verb and events (ADR 0636 §4): the indexer, which holds the seat, serves the verb and says
|
||||
// the events on the seat's own subject, for the trunks' definitions only, changed when a digest moved and removed when
|
||||
// a trunk's definition went.
|
||||
CatalogSeat = "mesh-catalog"
|
||||
SeatDefinitionsVerb = "seat-definitions"
|
||||
SeatDefinitionChangedEvent = "seat-definition-changed"
|
||||
SeatDefinitionsStream = "indexer-seat-definitions"
|
||||
SeatDefinitionRemovedEvent = "seat-definition-removed"
|
||||
// The findings (ADR 0608 §4, ADR 0564: the indexer writes findings, the condition keeper raises them).
|
||||
SeatDefinitionUnparsedKind = "seat-definition-unparsed"
|
||||
OutOfStepKind = "generated-types-out-of-step"
|
||||
// SeatDefinedTwiceKind: a contracts module's trunk defines a seat another repository's trunk already defines.
|
||||
SeatDefinedTwiceKind = "seat-defined-twice"
|
||||
// SeatOutsideItsDomainKind: a contracts module of the mesh's own defines a seat of another domain (ADR 0636 §5).
|
||||
SeatOutsideItsDomainKind = "seat-outside-its-domain"
|
||||
// The three tools (design 26).
|
||||
SeatDefinitionTool = "indexer_seat_definition"
|
||||
SeatDefinitionsAgainTool = "indexer_seat_definitions_again"
|
||||
@@ -75,8 +81,8 @@ type SeatDefinitionEntry struct {
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// SeatDefinitionChanged is the event's body: the seat, its source and the new digest ("" when the source holds the
|
||||
// seat no more).
|
||||
// SeatDefinitionChanged is what the indexer says of a definition: the seat, its source and the new digest ("" when the
|
||||
// source holds the seat no more, which is the event seat-definition-removed). Only a trunk's is said.
|
||||
type SeatDefinitionChanged struct {
|
||||
Seat string `json:"seat"`
|
||||
Source string `json:"source"`
|
||||
@@ -143,9 +149,33 @@ func contractsModules(manifests map[string]string) []contractsModule {
|
||||
return out
|
||||
}
|
||||
|
||||
// readDefinitions reads a contracts module's definitions at a commit: its seats' index, each seat's definition and the
|
||||
// parts it includes, each resolved. A seat that does not parse is answered by its words; a definitions directory
|
||||
// with no index, or an index that does not parse, is an error naming it.
|
||||
// listSeats names the seats a contracts module defines at a commit: every directory of its definitions' `seats/` that
|
||||
// holds a `seat.json`, whatever the seat's grain (ADR 0635 §2, §6), from the forge's one listing of that directory. No
|
||||
// index file names them. A listing the forge did not answer is a read it did not answer (the message is delivered again).
|
||||
func (ix *Indexer) listSeats(ctx context.Context, repository, commit string, c contractsModule) ([]string, error) {
|
||||
if ix.Files == nil {
|
||||
return nil, fmt.Errorf("%w: the indexer has no listing of %s's seats", errForgeRead, c.module)
|
||||
}
|
||||
dir := path.Join(c.defs, "seats")
|
||||
if c.defs == "." || c.defs == "" {
|
||||
dir = "seats"
|
||||
}
|
||||
got, err := ix.Files(ctx, repository, commit, []string{dir})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: the seats of %s at %.12s: %w", errForgeRead, c.module, commit, err)
|
||||
}
|
||||
var seats []string
|
||||
for p := range got[dir] {
|
||||
if seat, ok := strings.CutSuffix(p, "/seat.json"); ok && seat != "" && !strings.Contains(seat, "/") {
|
||||
seats = append(seats, seat)
|
||||
}
|
||||
}
|
||||
slices.Sort(seats)
|
||||
return seats, nil
|
||||
}
|
||||
|
||||
// readDefinitions reads a contracts module's definitions at a commit: each seat's definition (listSeats) and the
|
||||
// parts it includes, each resolved. A seat that does not parse is answered by its words.
|
||||
func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit string, c contractsModule) (map[string]seatdef.Definition, map[string]string, error) {
|
||||
file := func(p string) ([]byte, error) {
|
||||
if c.defs != "." && c.defs != "" {
|
||||
@@ -160,17 +190,13 @@ func (ix *Indexer) readDefinitions(ctx context.Context, repository, commit strin
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
raw, err := file("seats/index.json")
|
||||
seats, err := ix.listSeats(ctx, repository, commit, c)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
idx, err := seatdef.ParseIndex(raw)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("seats/index.json does not parse: %w", err)
|
||||
}
|
||||
defs, unparsed := map[string]seatdef.Definition{}, map[string]string{}
|
||||
parts := map[string]seatdef.Part{}
|
||||
for _, seat := range idx.Seats {
|
||||
for _, seat := range seats {
|
||||
raw, err := file("seats/" + seat + "/seat.json")
|
||||
if err != nil {
|
||||
if errors.Is(err, errForgeRead) {
|
||||
@@ -372,8 +398,13 @@ func (ix *Indexer) keepTrunk(ctx context.Context, repository, commit string, c c
|
||||
}
|
||||
ix.unparsedFinding(ctx, c.module, unparsed)
|
||||
var errs []error
|
||||
twice := map[string]string{} // seat -> the repository and module whose trunk holds it
|
||||
twice := map[string]string{} // seat -> the repository and module whose trunk holds it
|
||||
outside := map[string]string{} // seat -> why its module may not define it (ADR 0636 §5)
|
||||
for seat, d := range defs {
|
||||
if why := domainRefusal(c.module, d); why != "" {
|
||||
outside[seat] = why
|
||||
continue
|
||||
}
|
||||
holder, err := ix.putDefinition(ctx, SeatDefinitionEntry{Seat: seat, Source: SourceTrunk, Repository: repository,
|
||||
Module: c.module, Directory: c.dir, Commit: commit}, d)
|
||||
if holder != "" {
|
||||
@@ -382,7 +413,9 @@ func (ix *Indexer) keepTrunk(ctx context.Context, repository, commit string, c c
|
||||
errs = append(errs, err)
|
||||
}
|
||||
ix.twiceFinding(ctx, repository, c.module, twice)
|
||||
// A seat the module's trunk no longer defines leaves the state.
|
||||
ix.outsideFinding(ctx, c.module, outside)
|
||||
// A seat the module's trunk no longer defines leaves the state, unless it is handed over: a seat that is not its
|
||||
// holder's domain's waits for the domain's module (domain.go).
|
||||
keys, err := ix.SeatDefinitions.Keys(ctx)
|
||||
if err != nil {
|
||||
return errors.Join(append(errs, err)...)
|
||||
@@ -395,7 +428,7 @@ func (ix *Indexer) keepTrunk(ctx context.Context, repository, commit string, c c
|
||||
gone := false
|
||||
err := write(ctx, ix.SeatDefinitions, key, func(value []byte, found bool) ([]byte, bool, error) {
|
||||
var e SeatDefinitionEntry
|
||||
gone = found && json.Unmarshal(value, &e) == nil && e.Repository == repository && e.Module == c.module
|
||||
gone = found && json.Unmarshal(value, &e) == nil && e.Repository == repository && e.Module == c.module && !handoverPending(e)
|
||||
return nil, gone, nil
|
||||
})
|
||||
errs = append(errs, err)
|
||||
@@ -419,6 +452,10 @@ func (ix *Indexer) keepPull(ctx context.Context, repository string, pull int, he
|
||||
base := SeatDefinitionEntry{Source: SourcePull, Repository: repository, Pull: pull, Module: c.module, Directory: c.dir, Commit: head}
|
||||
var errs []error
|
||||
for seat, d := range defs {
|
||||
if why := domainRefusal(c.module, d); why != "" {
|
||||
unparsed[seat] = why // its check fails on it, as on a definition that does not parse
|
||||
continue
|
||||
}
|
||||
e := base
|
||||
e.Seat = seat
|
||||
_, err := ix.putDefinition(ctx, e, d)
|
||||
@@ -442,8 +479,9 @@ func (ix *Indexer) keepPull(ctx context.Context, repository string, pull int, he
|
||||
}
|
||||
|
||||
// putDefinition writes one parsed definition at its key, said when its digest moved. A trunk's definition of a seat
|
||||
// another repository's (or module's) trunk already defines is refused, written nowhere: a seat has one definition. The
|
||||
// refused one answers who holds it ("<repository> (<module>)").
|
||||
// another repository's (or module's) trunk already defines is refused, written nowhere: a seat has one definition, unless
|
||||
// the definer is the seat's domain's module taking it over from another module of the mesh's own (a handover, domain.go).
|
||||
// The refused one answers who holds it ("<repository> (<module>)").
|
||||
func (ix *Indexer) putDefinition(ctx context.Context, e SeatDefinitionEntry, d seatdef.Definition) (string, error) {
|
||||
raw, digest, err := digestOf(d)
|
||||
if err != nil {
|
||||
@@ -455,11 +493,11 @@ func (ix *Indexer) putDefinition(ctx context.Context, e SeatDefinitionEntry, d s
|
||||
var cur SeatDefinitionEntry
|
||||
known := found && json.Unmarshal(value, &cur) == nil
|
||||
moved, holder = false, ""
|
||||
if known && e.Source == SourceTrunk && (cur.Repository != e.Repository || cur.Module != e.Module) {
|
||||
if known && e.Source == SourceTrunk && (cur.Repository != e.Repository || cur.Module != e.Module) && !handsOver(cur, e.Module, d) {
|
||||
holder = cur.Repository + " (" + cur.Module + ")"
|
||||
return nil, false, nil
|
||||
}
|
||||
if known && cur.Digest == digest && cur.Unparsed == "" {
|
||||
if known && cur.Digest == digest && cur.Unparsed == "" && cur.Repository == e.Repository && cur.Module == e.Module {
|
||||
return nil, false, nil
|
||||
}
|
||||
moved = true
|
||||
@@ -488,6 +526,22 @@ func (ix *Indexer) twiceFinding(ctx context.Context, repository, module string,
|
||||
"No seat on "+module+"'s trunk is defined by another repository.")
|
||||
}
|
||||
|
||||
// outsideFinding writes the contracts module's seat-outside-its-domain finding for the seats it defines that belong to
|
||||
// another domain's module, naming both, or clears it when there are none.
|
||||
func (ix *Indexer) outsideFinding(ctx context.Context, module string, outside map[string]string) {
|
||||
var lines []string
|
||||
for seat, why := range outside {
|
||||
lines = append(lines, seat+": "+why)
|
||||
}
|
||||
slices.Sort(lines)
|
||||
ix.finding(ctx, findingID("seat-outside-its-domain", module), SeatOutsideItsDomainKind, len(lines) > 0,
|
||||
fmt.Sprintf("%s's trunk defines seats of another domain, refused: %s", module, strings.Join(lines, "; ")),
|
||||
"A seat on "+module+"'s trunk belongs to another domain's contracts module",
|
||||
"A seat of the mesh's own contracts is taken only from its domain's contracts module (mesh-contracts for a domain with none). "+
|
||||
"Move the definition there; the next merge of "+module+" without it clears this.",
|
||||
"Every seat on "+module+"'s trunk is of its domain.")
|
||||
}
|
||||
|
||||
// dropPull removes a pull request's entries and its out-of-step files.
|
||||
func (ix *Indexer) dropPull(ctx context.Context, repository string, pull int) error {
|
||||
keys, err := ix.SeatDefinitions.Keys(ctx)
|
||||
@@ -509,10 +563,10 @@ func (ix *Indexer) dropPull(ctx context.Context, repository string, pull int) er
|
||||
}
|
||||
|
||||
func (ix *Indexer) sayChanged(ctx context.Context, c SeatDefinitionChanged) {
|
||||
if ix.SaySeatDefinitionChanged == nil {
|
||||
if ix.SaySeatDefinition == nil || c.Source != SourceTrunk {
|
||||
return
|
||||
}
|
||||
if err := ix.SaySeatDefinitionChanged(ctx, c); err != nil {
|
||||
if err := ix.SaySeatDefinition(ctx, c); err != nil {
|
||||
ix.logf("seat definitions: %s of %s (%s) was not said: %v", SeatDefinitionChangedEvent, c.Seat, c.Source, err)
|
||||
}
|
||||
}
|
||||
@@ -731,3 +785,60 @@ func OutOfStepOf(defs, findings views.Source) ([]OutOfStepRow, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SeatDefinitionList is the verb seat-definitions with no seat (ADR 0636 §4): every trunk seat with the digest of its
|
||||
// definition, and the revision the views mark of the state names.
|
||||
type SeatDefinitionList struct {
|
||||
Revision uint64 `json:"revision"`
|
||||
Seats []SeatDefinition `json:"seats"`
|
||||
}
|
||||
|
||||
// SeatDefinition is one seat of the list: the seat, its source (always the trunk) and its digest.
|
||||
type SeatDefinition struct {
|
||||
Seat string `json:"seat"`
|
||||
Source string `json:"source"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
// SeatDefinitionsOf answers the verb seat-definitions of the seat mesh-catalog (ADR 0636 §4) from the state: with no
|
||||
// seat, every trunk seat with its digest, by seat; with a seat, that seat's trunk definition (found false when the trunk
|
||||
// holds none). Never a pull request's: a running part acts on what is merged.
|
||||
func SeatDefinitionsOf(defs, marks views.Source, seat string) (any, error) {
|
||||
if seat != "" {
|
||||
a, err := SeatDefinitionOf(defs, marks, seat, "", 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return struct {
|
||||
Seat string `json:"seat"`
|
||||
Found bool `json:"found"`
|
||||
Entry *SeatDefinitionEntry `json:"entry,omitempty"`
|
||||
Revision uint64 `json:"revision"`
|
||||
}{a.Seat, a.Found, a.Entry, a.Revision}, nil
|
||||
}
|
||||
keys, err := defs.Keys()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the %s state cannot be listed: %w", SeatDefinitionsState, err)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
out := SeatDefinitionList{Revision: markOf(marks), Seats: []SeatDefinition{}}
|
||||
for _, key := range keys {
|
||||
name, ok := strings.CutSuffix(key, "."+SourceTrunk)
|
||||
if !ok || strings.HasPrefix(key, outOfStepPrefix) {
|
||||
continue
|
||||
}
|
||||
v, ok, err := defs.Get(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s.%s cannot be read: %w", SeatDefinitionsState, key, err)
|
||||
}
|
||||
if !ok || v == nil {
|
||||
continue
|
||||
}
|
||||
var e SeatDefinitionEntry
|
||||
if err := json.Unmarshal(v.Value, &e); err != nil || e.Digest == "" {
|
||||
continue // an entry that holds no definition is no seat of the list
|
||||
}
|
||||
out.Seats = append(out.Seats, SeatDefinition{Seat: name, Source: SourceTrunk, Digest: e.Digest})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -214,16 +214,21 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
}
|
||||
}
|
||||
// ADR 0615 §5, ADR 0619 §2: its views family on its old streams section, 7 days and 256 MiB, dropping its oldest.
|
||||
// Design 26: seat-definition-changed is kept seven days (DLV-I17).
|
||||
if want := []stream{{Name: index.ViewsStream, Events: []string{views.EventPattern}, MaxAgeDays: views.StreamDays, MaxMiB: views.StreamMiB},
|
||||
{Name: index.SeatDefinitionsStream, Events: []string{index.SeatDefinitionChangedEvent}, MaxAgeDays: 7}}; !reflect.DeepEqual(m.Streams, want) {
|
||||
// ADR 0636 §4: seat-definition-changed and -removed are the seat mesh-catalog's events, said on the seat's subject and kept
|
||||
// on the shared events stream: no stream of the indexer's, and not among its own emits.
|
||||
if want := []stream{{Name: index.ViewsStream, Events: []string{views.EventPattern}, MaxAgeDays: views.StreamDays, MaxMiB: views.StreamMiB}}; !reflect.DeepEqual(m.Streams, want) {
|
||||
t.Errorf("streams = %+v, want %+v", m.Streams, want)
|
||||
}
|
||||
// ADR 0604 §2: it holds the seat mesh-catalog, serving its four verbs from its tools bundle (which stays without a
|
||||
// request subject).
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != catalogue.CatalogSeat || m.Claims[0].Scope != catalogue.ScopeMesh ||
|
||||
!slices.Equal(m.Claims[0].Serves, []string{"modules", "module", "provides", "dependents"}) {
|
||||
t.Errorf("claims = %+v, want mesh-catalog with its four verbs", m.Claims)
|
||||
// ADR 0604 §2, ADR 0636 §4: it holds the seat mesh-catalog, serving its four verbs and seat-definitions from its tools
|
||||
// bundle (which stays without a request subject).
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != index.CatalogSeat || m.Claims[0].Scope != catalogue.ScopeMesh ||
|
||||
!slices.Equal(m.Claims[0].Serves, []string{"modules", "module", "provides", "dependents", index.SeatDefinitionsVerb}) {
|
||||
t.Errorf("claims = %+v, want mesh-catalog with its five verbs", m.Claims)
|
||||
}
|
||||
for _, e := range []string{index.SeatDefinitionChangedEvent, index.SeatDefinitionRemovedEvent} {
|
||||
if slices.Contains(m.Emits, e) {
|
||||
t.Errorf("emits lists %s, which the seat says (the validator refuses a seat's event among a holder's emits)", e)
|
||||
}
|
||||
}
|
||||
// 1585/T11 (ADR 0633 §1, §5): the take-in events are its ordinary events, with no stream of its own.
|
||||
if !slices.Contains(m.Emits, takein.TakeInPendingEvent) || !slices.Contains(m.Emits, takein.TakeInTakenEvent) {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"modules",
|
||||
"module",
|
||||
"provides",
|
||||
"dependents"
|
||||
"dependents",
|
||||
"seat-definitions"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -88,8 +89,7 @@
|
||||
{ "name": "pending", "history": 1 }
|
||||
],
|
||||
"streams": [
|
||||
{ "name": "indexer-views", "events": ["views.**"], "max-age-days": 7, "max-mib": 256 },
|
||||
{ "name": "indexer-seat-definitions", "events": ["seat-definition-changed"], "max-age-days": 7 }
|
||||
{ "name": "indexer-views", "events": ["views.**"], "max-age-days": 7, "max-mib": 256 }
|
||||
],
|
||||
"emits": [
|
||||
"manifest-refused",
|
||||
|
||||
Reference in New Issue
Block a user