Files
mesh-controller/internal/inventory/gate.go
T
mesh-tools af267cb557 Take the catalogue from mesh-sdk in internal/inventory and internal/link, and keep the tests that read this repository's own manifest, Dockerfile and Makefile in internal/ownmanifest (issue 1381, 5 of 6 import swaps)
The three tests of the controller's own module.json (process, claimed seat, addresses), the Dockerfile and Makefile pin test, the build-agent registry test and the manifest-in-use seat test move with their old package, as tests of mesh-sdk's exported API; a moved test is exempt from failing first.
2026-10-20 08:27:10 +00:00

336 lines
12 KiB
Go

package inventory
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The gate's verdicts (novox/hq ADR 0236, to-be 45 §8): what a build did on its first machine, and,
// for one that failed there, how it was put back. One row per build, written by the plan that rolled it
// out, under the lease.
// The gate's verdicts and a failed build's rollback.
const (
GatePassed = "passed"
GateFailed = "failed"
// GateSuperseded is a judging ended by a later send to the judged machine that moved the module to
// another build (novox/hq issue 352): no verdict on the build, and nothing put back.
GateSuperseded = "superseded"
RollingBack = "rolling-back"
RolledBack = "rolled-back"
NotRolledBack = "not-rolled-back"
)
// GateVerdict is one build's verdict at its gate.
type GateVerdict struct {
Build string
Module string
Commit string
Previous string
Plan string
Machines []string
Verdict string
Rollback string
Why string
Component string
// JudgingFrom is when the first machine reported the build applied and the judging began.
JudgingFrom *time.Time
JudgedAt time.Time
Epoch uint64
}
// RecordGate writes a build's verdict. A build that passed on one machine may still fail on the next one
// judged; **a failed build's row is written once**: any later verdict for it is refused with ErrGateKept, which is what keeps a rollback to one per build — the row
// is written before the rollback's send, and a controller replaced in between finds it.
func (i *Inventory) RecordGate(ctx context.Context, v GateVerdict) error {
epoch, err := i.actingEpoch(ctx)
if err != nil {
return fmt.Errorf("the gate's verdict on %s is not written: %w", v.Build, err)
}
if v.Machines == nil {
v.Machines = []string{}
}
tag, err := i.store.Pool().Exec(ctx,
`insert into build_gate (build, module, commit_hash, previous, plan, machines, verdict, rollback, why,
component, judging_from, judged_at, epoch)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), $12)
on conflict (build) do update set verdict = excluded.verdict, rollback = excluded.rollback,
why = excluded.why, previous = excluded.previous, machines = excluded.machines,
judged_at = now(), epoch = excluded.epoch
where build_gate.verdict = 'passed'`,
v.Build, v.Module, v.Commit, v.Previous, v.Plan, v.Machines, v.Verdict, v.Rollback, v.Why, v.Component,
v.JudgingFrom, epoch)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("%w: %s", ErrGateKept, v.Build)
}
return nil
}
// ErrGateKept is a verdict already kept for the build, which is not written over.
var ErrGateKept = errors.New("this build's verdict at its gate is already kept")
// SetRollback records how a failed build's rollback went.
func (i *Inventory) SetRollback(ctx context.Context, build, rollback, why string) error {
if _, err := i.actingEpoch(ctx); err != nil {
return err
}
_, err := i.store.Pool().Exec(ctx,
`update build_gate set rollback = $2, why = $3, judged_at = now() where build = $1 and verdict = 'failed'`,
build, rollback, why)
return err
}
// GateOf is a build's verdict, and whether it has one.
func (i *Inventory) GateOf(ctx context.Context, build string) (GateVerdict, bool, error) {
rows, err := i.store.Pool().Query(ctx, gateSelect+` where build = $1`, build)
if err != nil {
return GateVerdict{}, false, err
}
list, err := scanGates(rows)
if err != nil || len(list) == 0 {
return GateVerdict{}, false, err
}
return list[0], true, nil
}
// GateFailed is whether a build failed its gate: one the mesh never registers or sends again on its own.
func (i *Inventory) GateFailed(ctx context.Context, build string) (bool, error) {
v, found, err := i.GateOf(ctx, build)
return found && v.Verdict == GateFailed, err
}
// LatestGates is the newest verdict of every module that has one: what the gate probe (DG) reads.
func (i *Inventory) LatestGates(ctx context.Context) ([]GateVerdict, error) {
rows, err := i.store.Pool().Query(ctx, `select distinct on (module) build, module, commit_hash, previous, plan,
machines, verdict, rollback, why, component, judging_from, judged_at, coalesce(epoch, 0)
from build_gate order by module, judged_at desc`)
if err != nil {
return nil, err
}
return scanGates(rows)
}
// Gates is the newest verdicts, newest first: what `plans gates` lists.
func (i *Inventory) Gates(ctx context.Context, limit int) ([]GateVerdict, error) {
rows, err := i.store.Pool().Query(ctx, gateSelect+` order by judged_at desc limit $1`, limit)
if err != nil {
return nil, err
}
return scanGates(rows)
}
const gateSelect = `select build, module, commit_hash, previous, plan, machines, verdict, rollback, why, component,
judging_from, judged_at, coalesce(epoch, 0) from build_gate`
func scanGates(rows pgx.Rows) ([]GateVerdict, error) {
defer rows.Close()
var out []GateVerdict
for rows.Next() {
var v GateVerdict
var epoch int64
if err := rows.Scan(&v.Build, &v.Module, &v.Commit, &v.Previous, &v.Plan, &v.Machines, &v.Verdict,
&v.Rollback, &v.Why, &v.Component, &v.JudgingFrom, &v.JudgedAt, &epoch); err != nil {
return nil, err
}
v.Epoch = uint64(epoch)
out = append(out, v)
}
return out, rows.Err()
}
// PreviousBuild is the build a module goes back to when a build of it fails its gate: the newest build
// that worked, made from the commit the first machine ran before, asked before the failed one, and not
// itself failed at a gate. Among the builds whose artifacts the mesh keeps (KeptBuilds): an older one
// may already be gone from the artifact store. False when there is none to go back to.
func (i *Inventory) PreviousBuild(ctx context.Context, module, commit string, failed Build) (Build, bool, error) {
builds, err := i.Builds(ctx, module, 50)
if err != nil {
return Build{}, false, err
}
kept := 0
for _, b := range builds {
if !b.Worked() {
continue
}
kept++
if kept > KeptBuilds {
break
}
if b.ID == failed.ID || (commit != "" && b.Commit != commit) {
continue
}
// Only a build of the repository the failed build was made from — the module's, since its take-in
// registered it (novox/hq ADR 0266): a build of the module's name from another repository is recorded
// and was never registered, and putting it back would register it now.
if failed.Repository != "" && !sameRepositoryAs(b.Repository, failed.Repository) {
continue
}
if !failed.AskedOrAt().IsZero() && !b.AskedOrAt().Before(failed.AskedOrAt()) {
continue
}
if bad, err := i.GateFailed(ctx, b.ID); err != nil {
return Build{}, false, err
} else if bad {
continue
}
manifest, err := i.manifestOf(ctx, b.ID)
if err != nil {
return Build{}, false, err
}
if len(manifest) == 0 || string(manifest) == "null" {
continue
}
b.Manifest = manifest
return b, true, nil
}
return Build{}, false, nil
}
// sameRepositoryAs says two recorded repositories are one, however their case or `.git` is spelled.
func sameRepositoryAs(a, b string) bool {
trim := func(s string) string {
return strings.TrimSuffix(strings.TrimRight(strings.ToLower(strings.TrimSpace(s)), "/"), ".git")
}
return trim(a) == trim(b)
}
// RestoreModule puts a module's registered build back to an earlier one: its manifest, the commit it
// was built from, and when it was asked — as now, so the build that failed its gate, asked before, can
// never register over it again (issue 219's order). The source's head is left where the merge moved it:
// the module IS behind its source, and `status` says so.
func (i *Inventory) RestoreModule(ctx context.Context, b Build) error {
if _, err := i.actingEpoch(ctx); err != nil {
return fmt.Errorf("%s is not put back: %w", b.Module, err)
}
m, err := catalogue.ParseManifest(b.Manifest)
if err != nil {
return fmt.Errorf("%s's build %s is not a manifest the mesh can register again: %w", b.Module, b.ID, err)
}
raw, err := json.Marshal(m)
if err != nil {
return err
}
tag, err := i.store.Pool().Exec(ctx,
`update module set manifest = $2, version = nullif($3, ''), built_from = nullif($4, ''),
built_asked = now(), registered = now()
where name = $1`, b.Module, raw, m.Version, b.Commit)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("%w: %s", ErrNoSuchModule, b.Module)
}
return nil
}
// BuildFingerprints is, per commit, what the newest successful build of a module from it would put on a
// machine — its artifacts and its manifest, hashed — so a rebuild that changes nothing there is told
// from one that does (the bus's planned step, ADR 0236).
func (i *Inventory) BuildFingerprints(ctx context.Context, module string) (map[string]string, error) {
c, err := i.theChain(ctx)
if err != nil {
return nil, err
}
return i.chainBuildFingerprints(ctx, c, module)
}
// Fingerprints is BuildFingerprints for every module at once: module → commit → fingerprint.
func (i *Inventory) Fingerprints(ctx context.Context) (map[string]map[string]string, error) {
c, err := i.theChain(ctx)
if err != nil {
return nil, err
}
return i.chainFingerprints(ctx, c)
}
// PassedCommits is, per module, the commits a build of which passed its gate on some machine.
func (i *Inventory) PassedCommits(ctx context.Context) (map[string]map[string]bool, error) {
rows, err := i.store.Pool().Query(ctx, `select module, commit_hash from build_gate where verdict = 'passed'`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]map[string]bool{}
for rows.Next() {
var module, commit string
if err := rows.Scan(&module, &commit); err != nil {
return nil, err
}
if out[module] == nil {
out[module] = map[string]bool{}
}
out[module][commit] = true
}
return out, rows.Err()
}
// BuildOf is the id of the newest successful build of a module from a commit; empty when none is
// recorded (a manifest handed over by hand).
func (i *Inventory) BuildOf(ctx context.Context, module, commit string) (string, error) {
c, err := i.theChain(ctx)
if err != nil {
return "", err
}
return i.chainBuildOf(ctx, c, module, commit)
}
// ManifestAt is the manifest a module was registered with at a commit (or a commit it abbreviates): the
// newest successful build from it, with the artifacts of the build standing for it when its source was
// unchanged (issue 280) — what a machine last sent that build runs. False when no such build, or none
// with a manifest, is kept (novox/hq issue 295: a recorded module is composed at the build its machine
// runs until a person's push moves it).
func (i *Inventory) ManifestAt(ctx context.Context, module, commit string) (catalogue.Manifest, bool, error) {
if commit == "" {
return catalogue.Manifest{}, false, nil
}
id, raw, found, err := i.newestManifestAt(ctx, module, commit)
if err != nil {
return catalogue.Manifest{}, false, err
}
if !found {
return catalogue.Manifest{}, false, nil
}
if stands, same, err := i.StandingBuild(ctx, module, id); err != nil {
return catalogue.Manifest{}, false, err
} else if stands != "" && stands != id && len(same) > 0 {
raw = same
}
m, err := catalogue.ParseManifest(raw)
if err != nil {
return catalogue.Manifest{}, false, fmt.Errorf("%s's build from %s is not a manifest the mesh can compose: %w",
module, commit, err)
}
return m, true, nil
}
// manifestOf is the manifest a build was recorded with; nil when it kept none.
func (i *Inventory) manifestOf(ctx context.Context, id string) ([]byte, error) {
c, err := i.theChain(ctx)
if err != nil {
return nil, err
}
return i.chainManifestOf(ctx, c, id)
}
// newestManifestAt is the newest successful build of a module from a commit, or one it abbreviates, that kept a manifest:
// its id and manifest; false when there is none.
func (i *Inventory) newestManifestAt(ctx context.Context, module, commit string) (string, []byte, bool, error) {
c, err := i.theChain(ctx)
if err != nil {
return "", nil, false, err
}
return i.chainManifestBuild(ctx, c, module, commit)
}