219 lines
7.5 KiB
Go
219 lines
7.5 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/novox/mesh-controller/internal/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"
|
|
|
|
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 failed build's row is written once**: a second failure for
|
|
// the same build 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' and excluded.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
|
|
}
|
|
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
|
|
}
|
|
var manifest []byte
|
|
if err := i.store.Pool().QueryRow(ctx, `select manifest from build where id = $1`, b.ID).Scan(&manifest); 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
|
|
}
|
|
|
|
// 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
|
|
}
|