Files
mesh-controller/cmd/mesh-controller/switch_list.go
T

205 lines
6.6 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-controller/internal/builderkeys"
"github.com/novox/mesh-controller/internal/inventory"
)
// The switch's list (novox/hq ADR 0453 §6, ADR 0499; issue 1851, A13): which modules the switch to the builder's keys
// moves, read before R4e is proposed and while it is delivered. For each module the catalogue builds, the held build's
// artifacts are compared by digest with the builder's done key of the fingerprint the indexer's trunk value names:
// every digest the same is no move (the standing build, A9); one that differs moves. An image and a package are named
// with why they cannot be compared before the trunk builds them: an image of build-agent and one of mesh-build-image
// are not one byte-for-byte build, and a candidate's image is built again for the trunk (ADR 0498 §6); a package is
// published by its version, not its digest. It moves nothing.
// The verdicts of the switch's list.
const (
switchUnmoved = "unmoved"
switchMoves = "moves"
switchUncompared = "cannot be compared"
switchUnknown = "not known"
)
// switchLine is one module's line of the switch's list.
type switchLine struct {
Module string `json:"module"`
Verdict string `json:"verdict"`
Why string `json:"why"`
}
// switchReads is what the switch's list reads of the builder and the indexer.
type switchReads interface {
ModuleAt(ctx context.Context, module, commit string) (sdkdelivery.Module, error)
Build(ctx context.Context, module, fp string) (builderkeys.Key, error)
}
// switchLineOf judges one module: its held build's artifacts against its key, as each artifact's kind allows.
func switchLineOf(module string, held []inventory.Artifact, trunk sdkdelivery.Module, k builderkeys.Key) switchLine {
line := switchLine{Module: module}
switch {
case trunk.FP2 == "":
line.Verdict, line.Why = switchUnknown, "the indexer wrote no fingerprint of the new form: "+orWords(trunk.None2, "none said")
return line
case !k.Done():
line.Verdict, line.Why = switchUnknown, k.Says()
return line
case len(held) == 0:
line.Verdict, line.Why = switchMoves, "the mesh holds no build of it to compare"
return line
}
digests := map[string]string{}
for _, a := range held {
digests[a.Name] = digestOfReference(a.Reference)
}
var moved, uncompared []string
for _, a := range k.Build.Artifacts {
was, had := digests[a.Name]
delete(digests, a.Name)
switch {
case !had:
moved = append(moved, a.Name+" is new")
case was == a.Digest:
case a.Kind == catalogue.ArtifactImage:
uncompared = append(uncompared, a.Name+" is an image: build-agent's and mesh-build-image's are not one "+
"byte-for-byte build, and a candidate's image is built again for the trunk (ADR 0498 §6)")
case a.Kind == "package":
uncompared = append(uncompared, a.Name+" is a package: it is published by its version, not its digest")
default:
moved = append(moved, fmt.Sprintf("%s %s -> %s", a.Name, shortDigest(was), shortDigest(a.Digest)))
}
}
for name := range digests {
moved = append(moved, name+" is gone")
}
sort.Strings(moved)
sort.Strings(uncompared)
switch {
case len(moved) > 0:
line.Verdict, line.Why = switchMoves, strings.Join(append(moved, uncompared...), "; ")
case len(uncompared) > 0:
line.Verdict, line.Why = switchUncompared, strings.Join(uncompared, "; ")
default:
line.Verdict, line.Why = switchUnmoved, "every artifact's digest is the held build's: registered with it, no move"
}
return line
}
// digestOfReference is the `sha256:<hex>` a reference names; empty when it names none.
func digestOfReference(reference string) string {
for _, marker := range []string{"@sha256:", "/sha256:"} {
if _, after, ok := strings.Cut(reference, marker); ok {
return "sha256:" + after
}
}
return ""
}
func shortDigest(d string) string {
d = strings.TrimPrefix(d, "sha256:")
if len(d) > 12 {
return d[:12]
}
if d == "" {
return "none"
}
return d
}
// switchCommand is `switch [--json]`: the switch's list, read-only.
func switchCommand(ctx context.Context, args []string) error {
set := flag.NewFlagSet("switch", flag.ContinueOnError)
asJSON := set.Bool("json", false, "answer the list as JSON")
if err := set.Parse(args); err != nil {
return err
}
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
keys, closeKeys, err := openBuilderKeys()
if err != nil {
return err
}
defer closeKeys()
lines, err := switchList(ctx, open.inventory, keys)
if err != nil {
return err
}
if *asJSON {
return json.NewEncoder(os.Stdout).Encode(lines)
}
counts := map[string]int{}
for _, l := range lines {
counts[l.Verdict]++
fmt.Printf("%-28s %-18s %s\n", l.Module, l.Verdict, l.Why)
}
fmt.Printf("\n%d module(s): %d unmoved, %d move, %d cannot be compared, %d not known\n", len(lines),
counts[switchUnmoved], counts[switchMoves], counts[switchUncompared], counts[switchUnknown])
return nil
}
// switchList is the switch's line of every module the catalogue builds but the builder's own, by name.
func switchList(ctx context.Context, inv *inventory.Inventory, keys switchReads) ([]switchLine, error) {
entries, err := inv.Catalogued(ctx)
if err != nil {
return nil, err
}
var out []switchLine
for _, e := range entries {
m := e.Manifest.Module
if e.Provided || e.Source.Repository == "" || isBuildersOwn(m) {
continue
}
trunk, err := keys.ModuleAt(ctx, m, "")
if errors.Is(err, builderkeys.ErrNoValue) {
out = append(out, switchLine{Module: m, Verdict: switchUnknown, Why: "the indexer holds no trunk value of it"})
continue
}
if err != nil {
return nil, err
}
var k builderkeys.Key
if trunk.FP2 != "" {
if k, err = keys.Build(ctx, m, trunk.FP2); err != nil {
return nil, err
}
}
held, err := heldArtifacts(ctx, inv, m, e.Source.BuiltFrom)
if err != nil {
return nil, err
}
out = append(out, switchLineOf(m, held, trunk, k))
}
sort.Slice(out, func(i, j int) bool { return out[i].Module < out[j].Module })
return out, nil
}
// heldArtifacts are the artifacts of the build the mesh holds of a module: the newest that worked at the commit it is
// registered at.
func heldArtifacts(ctx context.Context, inv *inventory.Inventory, module, builtFrom string) ([]inventory.Artifact, error) {
builds, err := inv.Builds(ctx, module, 20)
if err != nil {
return nil, err
}
for _, b := range builds {
if b.Worked() && sameCommitID(b.Commit, builtFrom) {
return b.Made, nil
}
}
// empty-on-error: not an error; the mesh holds no build of that commit on record, which the line says
return nil, nil
}