Files
mesh-controller/internal/inventory/builds.go
T
jschoubben cfe2816495 A module names the module its build stands on, not a copy of it
A fingerprint written into a recipe names one particular copy of the base — the
copy on whichever machine the person typing it was using. On any other mesh that
copy has never existed, so the build stops on its first line with a message
about an image nobody can look up. Three modules in the catalogue were in
exactly that state, and the line each of them replaced was equally dead.

A module now names the module and artifact instead, and the mesh answers with
what it holds. The builder is still a thing that clones, builds and answers: the
answer travels with the question, because only the mesh knows what it has.

A base the mesh has not built is refused before anything is built, naming which
module has to exist first.
2026-09-13 23:53:22 +02:00

147 lines
4.7 KiB
Go

package inventory
import (
"context"
"encoding/json"
"time"
)
// What has been built.
//
// A build result was answered to whoever asked and kept nowhere, so "when did this last build",
// "why did it fail" and "which machine built what is running" had no answer. Failures are recorded
// too: one that leaves no trace is indistinguishable from a build nobody asked for, and the
// difference is the whole of whether somebody should be looking at something.
// Build is one attempt, whichever way it went.
type Build struct {
ID string
Repository string
Ref string
// Module is empty for a build that failed before it knew what it was building.
Module string
Commit string
// On is the machine that did it.
On string
// Failed is the builder's own words, empty when it worked.
Failed string
Made []Artifact
At time.Time
}
// Artifact is one thing a build published.
type Artifact struct {
Name string `json:"name"`
Kind string `json:"kind"`
Reference string `json:"reference"`
}
// Worked reports whether this build produced something.
func (b Build) Worked() bool { return b.Failed == "" }
// RecordBuild keeps what a builder said.
//
// Idempotent on the correlation id, because a result can arrive twice: once as the answer to
// whoever asked and once on the exchange when nobody was. Recording both would show one build as
// two, and which of the two is real is not a question anybody could answer afterwards.
func (i *Inventory) RecordBuild(ctx context.Context, b Build) error {
made, err := json.Marshal(b.Made)
if err != nil {
return err
}
var module *string
if b.Module != "" {
module = &b.Module
}
_, err = i.store.Pool().Exec(ctx,
`insert into build (id, repository, ref, module, commit_hash, built_on, failed, made)
values ($1, $2, $3, $4, $5, $6, $7, $8)
on conflict (id) do nothing`,
b.ID, b.Repository, b.Ref, module, b.Commit, b.On, b.Failed, made)
return err
}
// Builds is what has happened lately, newest first.
//
// For one module when named, or across the mesh when not. Both are asked: *what happened just
// now* after something goes wrong, and *what has happened to this* when deciding whether to
// trust it.
func (i *Inventory) Builds(ctx context.Context, module string, limit int) ([]Build, error) {
if limit <= 0 {
limit = 20
}
query := `select id, repository, ref, coalesce(module,''), commit_hash, built_on, failed, made, at
from build order by at desc limit $1`
args := []any{limit}
if module != "" {
query = `select id, repository, ref, coalesce(module,''), commit_hash, built_on, failed, made, at
from build where module = $2 order by at desc limit $1`
args = append(args, module)
}
rows, err := i.store.Pool().Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Build
for rows.Next() {
var b Build
var made []byte
if err := rows.Scan(&b.ID, &b.Repository, &b.Ref, &b.Module, &b.Commit,
&b.On, &b.Failed, &made, &b.At); err != nil {
return nil, err
}
if err := json.Unmarshal(made, &b.Made); err != nil {
return nil, err
}
out = append(out, b)
}
return out, rows.Err()
}
// Held is every artifact this mesh has built, keyed "<module>/<artifact>".
//
// **The newest successful build of each module wins**, which is the same rule the rest of the mesh
// uses for what a module currently is. A module rebuilt to something broken and then rebuilt again
// is at the second one; a module whose last build failed is at the last one that worked, because a
// failure published nothing and the thing it published before is still what exists.
//
// Only successes, and only builds that knew what they were building: a build that failed before it
// could read a manifest has no module to be the artifact of.
func (i *Inventory) Held(ctx context.Context) (map[string]string, error) {
rows, err := i.store.Pool().Query(ctx,
`select distinct on (module) module, made
from build
where module is not null and module <> '' and failed = ''
order by module, at desc`)
if err != nil {
return nil, err
}
defer rows.Close()
held := map[string]string{}
for rows.Next() {
var module string
var raw []byte
if err := rows.Scan(&module, &raw); err != nil {
return nil, err
}
var made []Artifact
if err := json.Unmarshal(raw, &made); err != nil {
// Skipped rather than fatal. One unreadable build record should not stop every other
// module's base from being answerable — and the build that needs this one will say
// plainly that it is missing.
continue
}
for _, artifact := range made {
if artifact.Name == "" || artifact.Reference == "" {
continue
}
held[module+"/"+artifact.Name] = artifact.Reference
}
}
return held, rows.Err()
}