Name, before the switch, which modules it moves: the held build's digests against the builder's done key, images and packages said as what cannot be compared (issue 1851, A13)
This commit is contained in:
@@ -222,3 +222,30 @@ func TestAKeyWithTheHeldBuildsDigestsIsNoMove(t *testing.T) {
|
||||
t.Fatalf("A9: sudo is registered as %q, not with the held build's artifacts", shelf["sudo"].Version)
|
||||
}
|
||||
}
|
||||
|
||||
// A13 against the store: the switch's list reads the build the mesh holds of each module and the builder's key of its
|
||||
// trunk fingerprint, and names each module unmoved, moved or not known.
|
||||
func TestTheSwitchsListReadsTheHeldBuildsAndTheKeys(t *testing.T) {
|
||||
open := theCatalogue(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
keys, trunk := aKeyFor("sudo", "1", fpOf('b'), "novox/mesh-catalog", "modules/sudo", "c0ffee0123456789")
|
||||
k := keys.keys["sudo"]
|
||||
if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-sudo-held", Module: "sudo", Repository: "novox/mesh-catalog",
|
||||
Commit: "c0ffee0123456789", Made: []inventory.Artifact{{Name: "a", Kind: "archive",
|
||||
Reference: k.Build.Artifacts[0].Reference}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reads := planKeysOf{values: map[string]sdkdelivery.Module{"sudo@": trunk}, keys: map[string]builderkeys.Key{"sudo": k}}
|
||||
lines, err := switchList(ctx, inv, reads)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := map[string]string{}
|
||||
for _, l := range lines {
|
||||
got[l.Module] = l.Verdict
|
||||
}
|
||||
if got["sudo"] != switchUnmoved || got["mesh-host"] != switchUnknown {
|
||||
t.Fatalf("A13: the switch's list is %+v", lines)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ func run() error {
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "switch":
|
||||
return switchCommand(ctx, args[1:])
|
||||
case "register":
|
||||
return registerCommand(ctx, args[1:])
|
||||
case "build", "rebuild":
|
||||
|
||||
@@ -1714,6 +1714,8 @@ var commandReadForms = map[string]func(rest []string) bool{
|
||||
"status": flagsOnly, "version": flagsOnly, "help": flagsOnly, "seats": flagsOnly, "healers": flagsOnly,
|
||||
"hand-acts": flagsOnly, "durations": flagsOnly, "collection": flagsOnly, "images": flagsOnly,
|
||||
"artifacts": flagsOnly, "data": flagsOnly, "builds": flagsOnly, "queue": flagsOnly,
|
||||
// The switch's list reads the builder's keys and the builds held, and moves nothing (issue 1851, A13).
|
||||
"switch": flagsOnly,
|
||||
// `plan <node>` previews a node's declaration; it sends nothing.
|
||||
"plan": func([]string) bool { return true },
|
||||
// `plans` lists and `plans <id>` shows one; `plans stop|close|go` acts.
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/novox/mesh-controller/internal/builderkeys"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// A13 (ADR 0453 §6; issue 1851): the switch's list names every module moved or unmoved and why: the held build's
|
||||
// digests against the done key's; an image or a package as what cannot be compared; a key not done as not known.
|
||||
func TestTheSwitchsListNamesWhatMovesAndWhy(t *testing.T) {
|
||||
const d1, d2 = "sha256:" + "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"sha256:" + "2222222222222222222222222222222222222222222222222222222222222222"
|
||||
trunk := sdkdelivery.Module{FP2: "src2:" + strings.Repeat("a", 64)}
|
||||
done := func(arts ...sdkdelivery.BuiltArtifact) builderkeys.Key {
|
||||
return builderkeys.Key{Found: true, Fingerprint: trunk.FP2, Build: sdkdelivery.BuilderBuild{State: sdkdelivery.BuildDone,
|
||||
Artifacts: arts}}
|
||||
}
|
||||
held := []inventory.Artifact{{Name: "tools", Kind: "bundle", Reference: "artifact-store://x/tools/blobs/" + d1}}
|
||||
for _, c := range []struct {
|
||||
what string
|
||||
held []inventory.Artifact
|
||||
trunk sdkdelivery.Module
|
||||
key builderkeys.Key
|
||||
verdict string
|
||||
says string
|
||||
}{
|
||||
{"the same digest", held, trunk, done(sdkdelivery.BuiltArtifact{Name: "tools", Kind: "bundle", Digest: d1}), switchUnmoved, "no move"},
|
||||
{"another digest", held, trunk, done(sdkdelivery.BuiltArtifact{Name: "tools", Kind: "bundle", Digest: d2}), switchMoves, "tools 111111111111 -> 222222222222"},
|
||||
{"an image", []inventory.Artifact{{Name: "img", Kind: "image", Reference: "artifact-store://x/img@" + d1}}, trunk,
|
||||
done(sdkdelivery.BuiltArtifact{Name: "img", Kind: "image", Digest: d2}), switchUncompared, "img is an image"},
|
||||
{"a package", []inventory.Artifact{{Name: "lib", Kind: "package", Reference: "@novox/lib@1.2.0"}}, trunk,
|
||||
done(sdkdelivery.BuiltArtifact{Name: "lib", Kind: "package", Digest: d2}), switchUncompared, "published by its version"},
|
||||
{"a key building", held, trunk, builderkeys.Key{Module: "x", Fingerprint: trunk.FP2, Found: true,
|
||||
Build: sdkdelivery.BuilderBuild{State: sdkdelivery.BuildBuilding}}, switchUnknown, "is building"},
|
||||
{"no fingerprint", held, sdkdelivery.Module{None2: "no lock"}, builderkeys.Key{}, switchUnknown, "no lock"},
|
||||
} {
|
||||
l := switchLineOf("x", c.held, c.trunk, c.key)
|
||||
if l.Verdict != c.verdict || !strings.Contains(l.Why, c.says) {
|
||||
t.Errorf("%s: %+v, want %s saying %q", c.what, l, c.verdict, c.says)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user