Reverts the Go-bundle package of 53caa3ba; the run's program is the directory ./cmd/mesh-judge under the go.mod the build reads. Builds: design 47 G §712, §676, §491, A-268 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
265 lines
11 KiB
Go
265 lines
11 KiB
Go
package index
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/composer"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/trunk"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
|
|
"mesh-delivery-indexer/judge"
|
|
)
|
|
|
|
// The validation before take-in (design 47 G §712, A-213, A-232): for a pending revision of a watched branch the running
|
|
// indexer asks the seat node-build-go a `run` of its own manifest validator over two cases, change and trunk, and refuses only
|
|
// what the revision causes (ADR 0579 §2). The ask rides the check queue under the indexer's name, its kind `run` in the body.
|
|
const (
|
|
// validationProgram is the manifest validator's program, by its directory under the indexer entry's Go module: the indexer's
|
|
// own code, so no repository or module name is fixed here (ADR 0666 §5).
|
|
validationProgram = "cmd/mesh-judge"
|
|
// ValidationAsker is the asker the ask is put under (buildrun.Asker.Name, A-248). A-232 names the indexer's seat name mesh-catalog; the ask code still
|
|
// names askers by their nicknames until the asker rename after the join (A-163), so this is `indexer` until then.
|
|
ValidationAsker = "indexer"
|
|
// validationCatalogSeat is the seat the indexer entry claims.
|
|
validationCatalogSeat = "mesh-catalog"
|
|
// The cases of the request, as the manifest validator names them.
|
|
validationChange = "change"
|
|
validationTrunk = judge.TrunkCase
|
|
)
|
|
|
|
// validationInputVerb is where the build seat's holder asks the input: the seat mesh-catalog's verb input (§354).
|
|
var validationInputVerb = "seat:" + meshcatalog.Seat + "." + meshcatalog.VerbInput
|
|
|
|
// validationRequest is the manifest validator's request for the ready merges of branch over the branch's current revision
|
|
// (§712): case change, every module of the revision by its entry's manifest, each one the merges carry replaced by the merged
|
|
// message's bytes (the newest merge governs) and one added for a new directory; case trunk, every module as it stands; the
|
|
// seat definitions the merges' trees carry; the packages and the predefined list as the take-in's own validator has them;
|
|
// no machines. It answers the indexer entry of the change case too, the one that claims the seat mesh-catalog.
|
|
func (ix *Indexer) validationRequest(ctx context.Context, branch string, ready []PendingEntry) (composer.Request, meshcatalog.CatalogEntry, error) {
|
|
var indexer meshcatalog.CatalogEntry
|
|
if ix.Revisions == nil {
|
|
return composer.Request{}, indexer, errors.New("the catalog graph is not kept here: no revision to validate over")
|
|
}
|
|
rev, found, err := newestOf(ctx, ix.Revisions, branch)
|
|
if err != nil {
|
|
return composer.Request{}, indexer, err
|
|
}
|
|
var entries []meshcatalog.CatalogEntry
|
|
if found {
|
|
if _, entries, err = ReadRevision(ctx, ix.Revisions, RevisionObject(rev)); err != nil {
|
|
return composer.Request{}, indexer, err
|
|
}
|
|
}
|
|
trunkCase := composer.Case{Name: validationTrunk, Manifests: map[string]composer.Manifest{}}
|
|
byPlace := map[string]string{} // <repository> <path> -> module
|
|
of := map[string]meshcatalog.CatalogEntry{}
|
|
for _, e := range entries {
|
|
raw := ix.manifestBytes(ctx, e.ManifestDigest)
|
|
if raw == "" {
|
|
b, err := json.Marshal(e.Manifest)
|
|
if err != nil {
|
|
return composer.Request{}, indexer, fmt.Errorf("the manifest of %s: %w", e.Module, err)
|
|
}
|
|
raw = string(b)
|
|
}
|
|
trunkCase.Manifests[e.Module] = composer.Manifest{Raw: json.RawMessage(raw)}
|
|
byPlace[placeKey(e.Repository, e.Path)] = e.Module
|
|
of[e.Module] = e
|
|
}
|
|
change := composer.Case{Name: validationChange, Manifests: maps.Clone(trunkCase.Manifests)}
|
|
for _, e := range ready {
|
|
if e.Covered {
|
|
continue
|
|
}
|
|
for _, dir := range slices.Sorted(maps.Keys(e.Forge.Manifests)) {
|
|
raw := e.Forge.Manifests[dir]
|
|
module, known := byPlace[placeKey(e.Forge.Repository, dir)]
|
|
if !known {
|
|
m, err := judge.ForInputs([]byte(raw))
|
|
if err != nil || m.Module == "" {
|
|
module = e.Forge.Repository + "/" + dir // a new directory whose manifest names no module: judged under its place
|
|
} else {
|
|
module = m.Module
|
|
}
|
|
byPlace[placeKey(e.Forge.Repository, dir)] = module
|
|
of[module] = meshcatalog.CatalogEntry{Module: module, Repository: e.Forge.Repository, Path: dir}
|
|
}
|
|
change.Manifests[module] = composer.Manifest{Raw: json.RawMessage(raw)}
|
|
}
|
|
}
|
|
for _, module := range slices.Sorted(maps.Keys(change.Manifests)) {
|
|
m, err := judge.ForInputs(change.Manifests[module].Raw)
|
|
if err != nil || !slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == validationCatalogSeat }) {
|
|
continue
|
|
}
|
|
indexer = of[module]
|
|
if indexer.Module == "" {
|
|
indexer.Module = module
|
|
}
|
|
break
|
|
}
|
|
seats, err := ix.revisionDefinitions(ctx, ready)
|
|
if err != nil {
|
|
return composer.Request{}, indexer, err
|
|
}
|
|
req := composer.Request{V: composer.Version, Input: json.RawMessage("{}"), Cases: []composer.Case{change, trunkCase}, Seats: seats,
|
|
Predefined: ix.predefinedList(ctx)}
|
|
if known := ix.knownPackages(ctx); known != nil {
|
|
req.Packages = slices.Sorted(maps.Keys(known))
|
|
}
|
|
return req, indexer, nil
|
|
}
|
|
|
|
func placeKey(repository, path string) string {
|
|
return strings.ToLower(trunk.RepositoryName(repository)) + " " + path
|
|
}
|
|
|
|
// revisionDefinitions are the validation's seat definitions, raw (§712 `seats`, A-241): the branch's revision's seat
|
|
// definitions with the definitions the merges' trees carry laid over them, so a revision that touches no definition is still
|
|
// judged against the whole seat list. A revision's own definitions are not kept yet (A-234), so the revision's are the
|
|
// trunk's, read from the state seat-definitions as the verb seat-definitions answers them.
|
|
func (ix *Indexer) revisionDefinitions(ctx context.Context, ready []PendingEntry) ([]json.RawMessage, error) {
|
|
defs, err := ix.trunkDefinitions(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
merged, err := ix.revisionSeatDefinitions(ctx, ready)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for seat, d := range merged {
|
|
raw, err := json.Marshal(d)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the definition of %s: %w", seat, err)
|
|
}
|
|
defs[seat] = raw
|
|
}
|
|
var out []json.RawMessage
|
|
for _, seat := range slices.Sorted(maps.Keys(defs)) {
|
|
out = append(out, defs[seat])
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// trunkDefinitions are the trunk's seat definitions of the state seat-definitions, raw, by seat: every `<seat>.trunk` entry
|
|
// that holds a definition; none where the indexer keeps no such state.
|
|
func (ix *Indexer) trunkDefinitions(ctx context.Context) (map[string]json.RawMessage, error) {
|
|
defs := map[string]json.RawMessage{}
|
|
if ix.SeatDefinitions == nil {
|
|
return defs, nil
|
|
}
|
|
keys, err := ix.SeatDefinitions.Keys(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the %s state cannot be listed: %w", SeatDefinitionsState, err)
|
|
}
|
|
for _, key := range keys {
|
|
seat, ok := strings.CutSuffix(key, "."+SourceTrunk)
|
|
if !ok || strings.HasPrefix(key, outOfStepPrefix) {
|
|
continue
|
|
}
|
|
v, _, found, err := ix.SeatDefinitions.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s.%s cannot be read: %w", SeatDefinitionsState, key, err)
|
|
}
|
|
var e SeatDefinitionEntry
|
|
if !found || json.Unmarshal(v, &e) != nil || e.Digest == "" || len(e.Definition) == 0 {
|
|
continue // an entry that holds no definition is no seat of the list
|
|
}
|
|
defs[seat] = e.Definition
|
|
}
|
|
return defs, nil
|
|
}
|
|
|
|
// validationJob is the run of the manifest validator for the request (§712): the program cmd/mesh-judge, its directory under
|
|
// the Go module the go.mod at the entry's path names (read by the build, A-268), at the entry's path, built at head, the revision's head of the entry's repository; no
|
|
// heads and no arguments; the input the request's bytes, served by the seat mesh-catalog's verb input.
|
|
func validationJob(indexer meshcatalog.CatalogEntry, head, branch string, seq uint64, input []byte) (buildstep.RunJob, error) {
|
|
switch {
|
|
case indexer.Repository == "":
|
|
return buildstep.RunJob{}, fmt.Errorf("no entry of the revision claims the seat %s", validationCatalogSeat)
|
|
case head == "":
|
|
return buildstep.RunJob{}, fmt.Errorf("the revision names no head of %s, the repository of the indexer entry %s", indexer.Repository, indexer.Module)
|
|
}
|
|
in, err := buildstep.RunInputOf(validationInputVerb, input)
|
|
if err != nil {
|
|
return buildstep.RunJob{}, err
|
|
}
|
|
dir := indexer.Path
|
|
if dir == "" {
|
|
dir = "."
|
|
}
|
|
return buildstep.RunJob{
|
|
Program: buildstep.RunProgram{Repository: indexer.Repository, Commit: head, Dir: dir, Package: "./" + validationProgram},
|
|
Input: in,
|
|
Reader: fmt.Sprintf("manifest validator of %s at %d", branchName(branch), seq),
|
|
}, nil
|
|
}
|
|
|
|
func branchName(branch string) string {
|
|
if isTrunkBranch(branch) {
|
|
return TrunkBranch
|
|
}
|
|
return branch
|
|
}
|
|
|
|
// ValidationRefusal is what a validation's verdict refuses (ADR 0579 §2, §712): of each manifest the change case refuses that
|
|
// the revision adds or changes (its bytes differ from the trunk case's), each problem the trunk case lacks; and each problem
|
|
// between manifests the trunk case lacks. A problem only in unchanged manifests refuses nothing. It is empty when the revision
|
|
// is accepted. An answer that holds not both cases is no verdict: it answers an error, so the run could not validate.
|
|
func ValidationRefusal(req composer.Request, v composer.Verdict) (map[string][]string, error) {
|
|
cases := map[string]composer.CaseVerdict{}
|
|
for _, c := range v.Cases {
|
|
cases[c.Name] = c
|
|
}
|
|
change, okChange := cases[validationChange]
|
|
trunkV, okTrunk := cases[validationTrunk]
|
|
if !okChange || !okTrunk {
|
|
return nil, fmt.Errorf("the manifest validator's answer holds not both cases %s and %s", validationChange, validationTrunk)
|
|
}
|
|
raws := map[string]map[string]string{}
|
|
for _, c := range req.Cases {
|
|
raws[c.Name] = map[string]string{}
|
|
for name, m := range c.Manifests {
|
|
raws[c.Name][name] = InputDigest(m.Raw)
|
|
}
|
|
}
|
|
refused := map[string][]string{}
|
|
for _, module := range slices.Sorted(maps.Keys(change.Manifests)) {
|
|
r := change.Manifests[module]
|
|
if r.Accepted || r.Refused == "" {
|
|
continue
|
|
}
|
|
before, had := raws[validationTrunk][module]
|
|
if module != judge.Unnamed && had && before == raws[validationChange][module] {
|
|
continue // unchanged: its problems refuse nothing, and its entry keeps its verdict (ADR 0579 §1)
|
|
}
|
|
old := map[string]bool{}
|
|
for _, p := range strings.Split(trunkV.Manifests[module].Refused, "\n") {
|
|
old[p] = true
|
|
}
|
|
for _, p := range strings.Split(r.Refused, "\n") {
|
|
if p != "" && !old[p] {
|
|
refused[module] = append(refused[module], p)
|
|
}
|
|
}
|
|
}
|
|
return refused, nil
|
|
}
|
|
|
|
// RefusedWords is a refusal as the pending value's `refused` says it: each manifest and its problems, one manifest a line.
|
|
func RefusedWords(refused map[string][]string) string {
|
|
var lines []string
|
|
for _, module := range slices.Sorted(maps.Keys(refused)) {
|
|
lines = append(lines, module+": "+strings.Join(refused[module], "; "))
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|