Files
mesh-delivery/modules/mesh-delivery-indexer/internal/index/control_records.go
T

293 lines
9.9 KiB
Go

package index
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
// The indexer's two control records (novox/hq ADR 0565 §2, issue 1953 D2, D3): the catalogue's predefined list, read from the
// forge bridge's merged message, and each open pull request's config declarations, read from its pull message's manifests
// against the trunk's manifests the indexer holds. mesh-config reads both in place of the controller's mirror and its
// `pull-declares`; a key's write is its event, nothing polls.
// The two states' buckets.
var (
PredefinedBucket = delivery.IndexerModule + "_" + delivery.IndexerPredefinedState
ConfigCandidatesBucket = delivery.IndexerModule + "_" + delivery.IndexerConfigCandidatesState
)
// writePredefined keeps the predefined list a merged message carries, unless the key names a later merge: a merge older
// than the one it names never replaces it (ADR 0348 §7). The same list at the same merge writes nothing (ADR 0289 §1).
func (ix *Indexer) writePredefined(ctx context.Context, f delivery.Forge) error {
if ix.Predefined == nil || len(f.Predefined) == 0 {
return nil
}
return ix.notYet(PredefinedBucket, write(ctx, ix.Predefined, delivery.PredefinedListKey, func(value []byte, found bool) ([]byte, bool, error) {
if found {
var cur delivery.PredefinedKept
if err := json.Unmarshal(value, &cur); err == nil {
if cur.Commit == f.Merge && string(cur.List) == string(f.Predefined) {
return nil, false, nil
}
older, err := ix.older(ctx, f.Repository, f.Merge, cur.Commit)
if err != nil {
return nil, false, err
}
if older {
ix.logf("%s: the predefined list of %.12s is older than the one kept (%.12s): left", f.Repository, f.Merge, cur.Commit)
return nil, false, nil
}
}
}
next, err := json.Marshal(delivery.PredefinedKept{V: delivery.Version, Commit: f.Merge, At: ix.now(), List: f.Predefined})
return next, false, err
}))
}
// notYet says a state not on the bus yet (it comes with this delivery, bound when it appears) and lets the message go:
// what it would have written is written at the next message that carries it.
func (ix *Indexer) notYet(bucket string, err error) error {
if errors.Is(err, process.ErrNotYet) {
ix.logf("the state %s is not on the bus yet: nothing is written to it for this message", bucket)
return nil
}
return err
}
func (ix *Indexer) now() time.Time {
if ix.Now != nil {
return ix.Now()
}
return time.Now()
}
// PullName is a pull request as mesh-config names it, `owner/repository#number`.
func PullName(repository string, number int) string { return repository + "#" + strconv.Itoa(number) }
// DeclarationsOf is a manifest's config declarations by key, in the layout of the controller's declared-config mirror: the
// config form's `config`, else the settings form's `settings`, each default and enum encoded the one way.
func DeclarationsOf(raw string) (map[string]delivery.ConfigDeclaration, error) {
var m struct {
Config map[string]delivery.ConfigDeclaration `json:"config"`
Settings map[string]delivery.ConfigDeclaration `json:"settings"`
}
if err := json.Unmarshal([]byte(raw), &m); err != nil {
return nil, err
}
declared := m.Config
if declared == nil {
declared = m.Settings
}
out := map[string]delivery.ConfigDeclaration{}
for key, d := range declared {
for _, field := range []*json.RawMessage{&d.Default, &d.Enum} {
if len(*field) == 0 {
continue
}
var v any
if err := json.Unmarshal(*field, &v); err != nil {
return nil, fmt.Errorf("%s: %w", key, err)
}
canonical, err := json.Marshal(v)
if err != nil {
return nil, err
}
*field = canonical
}
out[key] = d
}
return out, nil
}
// changedDeclarations are the declarations head adds or changes against trunk.
func changedDeclarations(head, trunk map[string]delivery.ConfigDeclaration) map[string]delivery.ConfigDeclaration {
out := map[string]delivery.ConfigDeclaration{}
for key, d := range head {
if before, there := trunk[key]; !there || !reflect.DeepEqual(before, d) {
out[key] = d
}
}
return out
}
// trunkManifest is the manifest the indexer holds for a module on the trunk, "" when it holds none.
func (ix *Indexer) trunkManifest(ctx context.Context, held map[string]delivery.Module, module string) (string, error) {
m, ok := held[module]
if !ok || m.Manifest == "" || ix.Manifests == nil {
return "", nil
}
raw, _, found, err := ix.Manifests.Get(ctx, m.Manifest)
if err != nil || !found {
return "", err
}
kept, err := delivery.DecodeManifest(raw)
if err != nil {
return "", err
}
return kept.JSON, nil
}
// candidateDeclares are, per module of a pull request's head, the declarations its manifest adds or changes against the
// trunk's (an empty set for a module that adds none): heads are the head's manifests by module.
func (ix *Indexer) candidateDeclares(ctx context.Context, held map[string]delivery.Module, heads map[string]string) (map[string]map[string]delivery.ConfigDeclaration, error) {
out := map[string]map[string]delivery.ConfigDeclaration{}
for module, raw := range heads {
head, err := DeclarationsOf(raw)
if err != nil {
return nil, fmt.Errorf("%w: the manifest of %s at the head: %v", ErrUnreadable, module, err)
}
trunkRaw, err := ix.trunkManifest(ctx, held, module)
if err != nil {
return nil, err
}
trunk := map[string]delivery.ConfigDeclaration{}
if trunkRaw != "" {
if trunk, err = DeclarationsOf(trunkRaw); err != nil {
return nil, fmt.Errorf("the trunk's manifest of %s: %w", module, err)
}
}
if changed := changedDeclarations(head, trunk); len(changed) > 0 {
out[module] = changed
}
}
return out, nil
}
// writeConfigCandidate writes a pull request's key of config-candidates from its head's manifests by module: written when
// what it says differs (its time apart), never for the same head and declarations twice.
func (ix *Indexer) writeConfigCandidate(ctx context.Context, repository string, number int, head string, held map[string]delivery.Module, heads map[string]string) error {
if ix.ConfigCandidates == nil {
return nil
}
pull := PullName(repository, number)
key, err := delivery.ConfigCandidateKey(pull)
if err != nil {
return fmt.Errorf("%w: %v", ErrUnreadable, err)
}
declares, err := ix.candidateDeclares(ctx, held, heads)
if err != nil {
return err
}
return ix.notYet(ConfigCandidatesBucket, write(ctx, ix.ConfigCandidates, key, func(value []byte, found bool) ([]byte, bool, error) {
if found {
var cur delivery.ConfigCandidate
if json.Unmarshal(value, &cur) == nil && cur.Head == head && reflect.DeepEqual(cur.Declares, declares) {
return nil, false, nil
}
}
next, err := json.Marshal(delivery.ConfigCandidate{V: delivery.Version, Pull: pull, Head: head, Declares: declares, At: ix.now()})
return next, false, err
}))
}
// dropConfigCandidate deletes a pull request's key: it merged or closed.
func (ix *Indexer) dropConfigCandidate(ctx context.Context, repository string, number int) error {
if ix.ConfigCandidates == nil {
return nil
}
key, err := delivery.ConfigCandidateKey(PullName(repository, number))
if err != nil {
return nil
}
return ix.notYet(ConfigCandidatesBucket, write(ctx, ix.ConfigCandidates, key, func(_ []byte, found bool) ([]byte, bool, error) { return nil, found, nil }))
}
// configCandidateOfPull is a pull message's part in config-candidates: a head whose manifests the message carries is written,
// a closed one deleted. A head held again without its directories leaves the key as it is.
func (ix *Indexer) configCandidateOfPull(ctx context.Context, f delivery.Forge, held map[string]delivery.Module) error {
if f.Action == delivery.PullClosed {
return ix.dropConfigCandidate(ctx, f.Repository, f.Number)
}
if !f.DirsKnown {
return nil
}
heads := map[string]string{}
for dir, raw := range f.Manifests {
heads[moduleOfDir(held, f, dir)] = raw
}
return ix.writeConfigCandidate(ctx, f.Repository, f.Number, f.Head, held, heads)
}
// configCandidatesAfterMerge writes again every open pull request's key that names a module the merge changed, since what
// it adds is judged against the trunk (ADR 0565 §2), from the head's manifests its candidate keeps; and deletes the merged
// pull request's.
func (ix *Indexer) configCandidatesAfterMerge(ctx context.Context, f delivery.Forge, changed []string) error {
if ix.ConfigCandidates == nil {
return nil
}
if err := ix.dropConfigCandidate(ctx, f.Repository, f.Number); err != nil {
return err
}
if len(changed) == 0 || ix.Store == nil || ix.Manifests == nil {
return nil
}
held, err := ix.heldModules(ctx)
if err != nil {
return err
}
if _, err := ix.ConfigCandidates.Keys(ctx); errors.Is(err, process.ErrNotYet) {
return nil
}
groups, err := ix.Store.Keys(ctx)
if err != nil {
return err
}
var errs []error
for _, g := range groups {
raw, _, found, err := ix.Store.Get(ctx, g)
if err != nil || !found {
continue
}
c, err := delivery.DecodeCandidate(raw)
if err != nil {
continue
}
for _, p := range c.Pulls {
if !touchesAny(p, changed) {
continue
}
heads := map[string]string{}
for module, fp := range p.Fps {
mraw, _, found, err := ix.Manifests.Get(ctx, fp.Manifest)
if err != nil || !found {
continue
}
if kept, err := delivery.DecodeManifest(mraw); err == nil {
heads[module] = kept.JSON
}
}
if err := ix.writeConfigCandidate(ctx, p.Repository, p.Number, p.Head.Head, held, heads); err != nil {
errs = append(errs, err)
}
}
}
return errors.Join(errs...)
}
func touchesAny(p delivery.CandidatePull, modules []string) bool {
for _, m := range modules {
if _, ok := p.Fps[m]; ok {
return true
}
}
return false
}
// writtenModules are the modules a merged message wrote, from its written keys (`modules.<module>`).
func writtenModules(written []string) []string {
var out []string
for _, k := range written {
out = append(out, strings.TrimPrefix(k, "modules."))
}
return out
}