Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/buildread.go
T

223 lines
7.1 KiB
Go

package build
import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
// **builder_build answers a module's build keys whole** (design 47 G §589, A-87): with `fingerprint` that key whole, with
// `module` alone the module's keys newest first, at most MostBuilds, each with its fingerprint whole, its state, revision and
// since when, its step and node, its error whole, event-builds and the open ask (§568), and `newest` naming each watched branch
// whose newest catalog graph entry of the module names that fingerprint, read from the catalog half by the tools bundle.
// BuildTool is the builder's read tool of a module's build keys.
const BuildTool = "builder_build"
// MostBuilds is how many keys of a module builder_build answers by module alone.
const MostBuilds = 20
// BuildRow is one build key as builder_build answers it.
type BuildRow struct {
Key string `json:"key"`
Fingerprint string `json:"fingerprint"`
State string `json:"state"`
Revision uint64 `json:"revision,omitempty"`
Since time.Time `json:"since,omitzero"`
Step string `json:"step,omitempty"`
Node string `json:"node,omitempty"`
Error string `json:"error,omitempty"`
EventBuilds int `json:"event-builds"`
// Ask is the open ask of the operator about the key (§568).
Ask *OpenAsk `json:"ask,omitempty"`
// Newest names each watched branch whose newest catalog graph entry of the module names this fingerprint.
Newest []string `json:"newest,omitempty"`
}
// OpenAsk is the ask the operator has not answered.
type OpenAsk struct {
ID string `json:"id"`
Asked string `json:"asked"`
Expires string `json:"expires"`
}
// BuildAnswer is what builder_build answers.
type BuildAnswer struct {
Module string `json:"module"`
Builds []BuildRow `json:"builds"`
// Newest is the fingerprint of the module's newest catalog graph entry on each watched branch.
Newest map[string]string `json:"newest-entries,omitempty"`
// Note says what no key shows: that none of the keys is the newest entry's, naming its fingerprint.
Note string `json:"note,omitempty"`
}
// BuildReader reads what builder_build answers.
type BuildReader struct {
Builds StateRead
// Entry is a key's value with its revision and when it was written; nil leaves both out.
Entry func(key string) (json.RawMessage, uint64, time.Time, error)
// Asked is the builder's state asked, which holds the asks the operator was asked.
Asked StateRead
// Newest is the fingerprint of the module's newest catalog graph entry on each watched branch, read from the catalog half.
Newest func(module string) (map[string]string, error)
Steps StuckReader
index map[string]map[string]string
}
// Build answers for module, and for one fingerprint when fingerprint is not empty.
func (r *BuildReader) Build(module, fingerprint string) (BuildAnswer, error) {
out := BuildAnswer{Module: module, Builds: []BuildRow{}}
if module == "" {
return out, errors.New("name the module (module)")
}
all, err := r.Builds.Keys()
if err != nil {
return out, err
}
var rows []BuildRow
for _, key := range all {
mod, fp, err := builds.ParseBuilderBuildKey(key)
if err != nil || mod != module || (fingerprint != "" && fp != fingerprint) {
continue
}
raw, rev, at, err := r.read(key)
if err != nil {
return out, fmt.Errorf("%s cannot be read: %w", key, err)
}
if raw == nil {
continue
}
v, err := builds.DecodeBuilderBuild(raw)
if err != nil {
return out, fmt.Errorf("%s holds what is not a build: %w", key, err)
}
row := BuildRow{Key: key, Fingerprint: fp, State: v.State, Revision: rev, Since: at, Step: v.Step, Error: v.Error, EventBuilds: v.EventBuilds}
if _, _, node, _, ok := builds.TransportFaultOf(v.Error); ok && node != "no node" {
row.Node = node
}
if row.Node == "" && v.State == builds.BuildBuilding {
row.Node = r.stepNode(v)
}
rows = append(rows, row)
}
if fingerprint != "" && len(rows) == 0 {
return out, fmt.Errorf("the builder holds no build key of %s at %s", module, fingerprint)
}
sort.Slice(rows, func(i, j int) bool {
if !rows[i].Since.Equal(rows[j].Since) {
return rows[i].Since.After(rows[j].Since)
}
return rows[i].Key > rows[j].Key
})
if len(rows) > MostBuilds {
rows = rows[:MostBuilds]
}
asks := r.openAsks()
for i := range rows {
rows[i].Ask = asks[rows[i].Key]
}
if r.Newest != nil {
newest, err := r.Newest(module)
if err != nil {
return out, fmt.Errorf("the catalog half's newest entry of %s cannot be read: %w", module, err)
}
out.Newest = newest
named := false
for i := range rows {
for branch, fp := range newest {
if fp == rows[i].Fingerprint {
rows[i].Newest, named = append(rows[i].Newest, branch), true
}
}
slices.Sort(rows[i].Newest)
}
if !named && len(newest) > 0 {
var says []string
for branch, fp := range newest {
says = append(says, branch+" names "+fp)
}
slices.Sort(says)
out.Note = "no build key of " + module + " is the newest entry's: " + strings.Join(says, "; ")
}
}
out.Builds = append(out.Builds, rows...)
return out, nil
}
// stepNode is the node a building key's step runs on, read from its asks' steps in the build modules' steps; empty when none runs.
func (r *BuildReader) stepNode(v builds.BuilderBuild) string {
if len(v.Asks) == 0 || len(r.Steps.Steps) == 0 {
return ""
}
if r.index == nil {
idx, _, err := r.Steps.stepIndex()
if err != nil {
return ""
}
r.index = idx
}
languages := make([]string, 0, len(v.Asks))
for l := range v.Asks {
languages = append(languages, l)
}
slices.Sort(languages)
for _, l := range languages {
bm, _ := buildstep.BuildModuleOf(l)
st, ok := r.Steps.Steps[bm]
k, found := r.index[bm][v.Asks[l]]
if !ok || !found {
continue
}
if raw, _, err := st.Get(k); err == nil && raw != nil {
if step, err := buildstep.DecodeStep(raw); err == nil && step.Node != "" {
return step.Node
}
}
}
return ""
}
func (r *BuildReader) read(key string) (json.RawMessage, uint64, time.Time, error) {
if r.Entry != nil {
return r.Entry(key)
}
raw, at, err := r.Builds.Get(key)
return raw, 0, at, err
}
// openAsks are the asks kept in the state asked that nobody answered yet, by the key each is about.
func (r *BuildReader) openAsks() map[string]*OpenAsk {
out := map[string]*OpenAsk{}
if r.Asked.Keys == nil {
return out
}
keys, err := r.Asked.Keys()
if err != nil {
return out
}
for _, k := range keys {
raw, _, err := r.Asked.Get(k)
if err != nil || raw == nil {
continue
}
var kept occ.Kept
if json.Unmarshal(raw, &kept) != nil || kept.Ended != "" || kept.Acted || kept.Ask.About == "" {
continue
}
if t, err := occ.Time(kept.Ask.Expires); err == nil && !t.IsZero() && time.Now().After(t) {
continue
}
out[kept.Ask.About] = &OpenAsk{ID: kept.Ask.ID, Asked: kept.Asked.UTC().Format(time.RFC3339), Expires: kept.Ask.Expires}
}
return out
}