Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/renames.go
T

129 lines
5.2 KiB
Go

package check
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// A config value renamed (design 32 *A config value renamed*, DLV-C5): the checker asks the seat mesh-config's verb
// renames, never the module, for every change that brings `renamed-from` or drops one. A holder that does not serve the
// verb refuses `renamed-from`, naming that; while a rename's window is open, dropping its `renamed-from` is refused,
// naming the nodes holding it open; a check that cannot ask fails, saying so.
// RenamesSeat is the verb the checker asks, as its manifest's `invokes` names it.
var RenamesSeat = "seat:" + meshconfig.Seat + "." + meshconfig.VerbRenames
// ErrRenamesNotServed is the answer of Renames when the seat's holder serves no verb renames.
var ErrRenamesNotServed = errors.New("the holder of the seat " + meshconfig.Seat + " does not serve its verb " + meshconfig.VerbRenames)
// Renames asks the seat mesh-config's verb renames for a module's open renames.
type Renames interface {
Renames(ctx context.Context, module string) (meshconfig.RenamesAnswer, error)
}
// renameChange is what a pull request's head does to a module's `renamed-from` against the trunk: the declarations it
// brings (key from old name) and the ones it drops (key, old name).
type renameChange struct {
brought, dropped map[string]string
}
// renamesOf compares a module's config declarations at the trunk and at the head.
func renamesOf(was, now catalogue.Manifest) renameChange {
rc := renameChange{brought: map[string]string{}, dropped: map[string]string{}}
for key, d := range now.Settings {
if d.RenamedFrom != "" && was.Settings[key].RenamedFrom != d.RenamedFrom {
rc.brought[key] = d.RenamedFrom
}
}
for key, d := range was.Settings {
if d.RenamedFrom != "" && now.Settings[key].RenamedFrom != d.RenamedFrom {
rc.dropped[key] = d.RenamedFrom
}
}
return rc
}
// judgeRenames fails the check for a rename the seat refuses, a window a change would drop, or an ask that cannot be made.
func (st *state) judgeRenames(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue {
if st.c.Try != nil {
return nil
}
var out *meshdeliverychecker.VerdictsByGroupHeadsValue
failed := func(words string) {
f := FailedFirst{Machine: meshconfig.Seat, Words: words}
if out == nil {
out = &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &f}
return
}
addFailure(out, f)
}
for _, p := range st.c.Pulls {
for _, module := range p.Modules {
was, now, ok, err := st.manifestPair(ctx, p, module)
if err != nil {
failed(fmt.Sprintf("%s (%s): its config renames cannot be judged: %v", module, pullKey(p), err))
continue
}
if !ok {
was = catalogue.Manifest{} // a module new on the pull request brings every rename it declares
if now, ok = st.headManifest(ctx, p, module); !ok {
continue
}
}
rc := renamesOf(was, now)
if len(rc.brought) == 0 && len(rc.dropped) == 0 {
continue
}
if st.j.Renames == nil {
failed(fmt.Sprintf("%s (%s) changes a renamed-from, and this checker cannot ask %s", module, pullKey(p), RenamesSeat))
continue
}
answer, err := st.j.Renames.Renames(ctx, module)
switch {
case errors.Is(err, ErrRenamesNotServed):
for _, key := range slices.Sorted(maps.Keys(rc.brought)) {
failed(fmt.Sprintf("%s (%s) renames %s from %s: refused, %v, so the value cannot be moved", module, pullKey(p), key, rc.brought[key], err))
}
for _, key := range slices.Sorted(maps.Keys(rc.dropped)) {
failed(fmt.Sprintf("%s (%s) drops renamed-from %s of %s: refused, %v, so whether its window is open is not known", module, pullKey(p), rc.dropped[key], key, err))
}
continue
case err != nil:
failed(fmt.Sprintf("%s (%s) changes a renamed-from, and the check cannot ask %s: %v", module, pullKey(p), RenamesSeat, err))
continue
}
for _, key := range slices.Sorted(maps.Keys(rc.dropped)) {
for _, r := range answer.Renames {
if r.Module == module && r.From == rc.dropped[key] && r.To == key && len(r.Open) > 0 {
open := slices.Sorted(slices.Values(r.Open))
failed(fmt.Sprintf("%s (%s) drops renamed-from %s of %s while its rename window is open on %s", module, pullKey(p), r.From, key, strings.Join(open, ", ")))
}
}
}
st.c.note(fmt.Sprintf("%s.%s answered for %s (%s)", meshconfig.Seat, meshconfig.VerbRenames, module, pullKey(p)))
}
}
return out
}
// headManifest is a module's manifest at the pull request's head, new on it: by its candidate's digest, else the forge's.
func (st *state) headManifest(ctx context.Context, p Pull, module string) (catalogue.Manifest, bool) {
if digest := p.Fps[module].Manifest; digest != "" && st.j.Read != nil {
if st.j.Manifests != nil && st.j.Manifests.ManifestDigests()[module] == digest {
return catalogue.Manifest{}, false // unchanged
}
m, err := st.j.Read.Manifest(ctx, digest)
return m, err == nil
}
return catalogue.Manifest{}, false
}