205 lines
6.6 KiB
Go
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
|
|
}
|