The controller asks no build of anybody (ADR 0501 §5, ADR 0525 clause 6, ADR 0503 §3). Every module of a walk is taken from the builder's done key; a key not done yet waits, named in the plan, and never fails it. - C2b/C2c: internal/builder and cmd/mesh-controller's asks go (askABuild, onlyTheBuildersOwn, buildOneAsked, takeIn from an outcome, buildAgentsOwn, the builder's say and its fallback, the daemon hearing an outcome). The server follows the retired seats' built subjects only to let go of them, because the installer's carried user list names them. - C2d: replay, queue, cancel, clear, kill, pause, resume, delivery-check, check-here and merge-gate are refused by name. - C3: the check queues (CHECKS_READY, CHECKS_OTHER) and their workers, the checks-running and plans-building buckets and their grants, the build queue and its hold (plans_building.go, queue.go, link/holds.go, builds_nats.go, checks_nats.go). - C4: merge_gate*.go, the pull request's repo-check path, the Checked event and its layers; a pull request asks the controller nothing but to tell mesh-config what its head declares. - C5: the builds bucket is written only by key_takein. - C10 (help and refusals): the terminal's help lists only what is served. Integration tests of the plan's tiers, sends, stops and retries now run on a stand-in builder with a done key for every module (walk_keys_scaffold_integration_test.go); the tests of the asks, the queue, the merge gate and the replays are deleted.
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 == "" {
|
|
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
|
|
}
|