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.
147 lines
4.7 KiB
Go
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()
|
|
}
|