A dry run of an unreviewed branch was heard by the daemon like any build, registered, and its definition reached a machine (novox/hq issue 240). The mark now travels with the build and the daemon records, registers and plans nothing for it.
170 lines
8.4 KiB
Go
170 lines
8.4 KiB
Go
package link
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Asking a machine to build a module, and hearing what came out.
|
|
//
|
|
// **A build is work, not state.** Everything else the control plane sends a node is a declaration
|
|
// — *this is what you should be* — and the node reconciles toward it forever. A build happens
|
|
// once, produces something, and is finished. Putting it in a declaration would mean a machine
|
|
// rebuilding on every reconcile, or the declaration carrying "and I already did this", which is
|
|
// state about an event rather than about the machine.
|
|
//
|
|
// So it travels on its own queue, and the reply comes back correlated. That is also why the
|
|
// builder is a **separate consumer** rather than the host: the host applies declarations and
|
|
// holds no opinion about what they contain, and a host that also built things would be a host
|
|
// with a container runtime requirement and a git dependency (novox/hq ADR 0005).
|
|
|
|
// NewBuildID is the correlation for a build asked at that moment: `build-<unix nanoseconds>`.
|
|
//
|
|
// **The id carries when the build was asked, and that is read back** (novox/hq 04-ISSUES/219). Builds
|
|
// of one module can be in flight together and finish in any order; what a module currently is must
|
|
// be the newest *request's* outcome, not the last one heard, and the id is the one thing every
|
|
// outcome echoes whichever builder answered it. One place writes the shape and one reads it.
|
|
func NewBuildID(asked time.Time) string {
|
|
return "build-" + strconv.FormatInt(asked.UnixNano(), 10)
|
|
}
|
|
|
|
// BuildAskedAt is when the build with this id was asked, as NewBuildID wrote it. False for an id
|
|
// of any other shape — one written before this was read, or by hand — whose request time the mesh
|
|
// does not know.
|
|
func BuildAskedAt(id string) (time.Time, bool) {
|
|
digits, ok := strings.CutPrefix(id, "build-")
|
|
if !ok || digits == "" {
|
|
return time.Time{}, false
|
|
}
|
|
nanos, err := strconv.ParseInt(digits, 10, 64)
|
|
// A number too small to be a moment this mesh could have asked at is a name, not a time.
|
|
if err != nil || nanos < time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).UnixNano() {
|
|
return time.Time{}, false
|
|
}
|
|
return time.Unix(0, nanos).UTC(), true
|
|
}
|
|
|
|
// BuildRequest is one module to build.
|
|
type BuildRequest struct {
|
|
// ID correlates the answer with the asking. Not the module name: two builds of one module can
|
|
// be in flight, and the second answer is not the first one's.
|
|
ID string `json:"id"`
|
|
// Repository is where the source is, as git would clone it.
|
|
Repository string `json:"repository"`
|
|
// Ref is the branch, tag or commit. Empty means whatever the repository's default is, which
|
|
// is the only case where the mesh does not know what it built until it has built it.
|
|
Ref string `json:"ref,omitempty"`
|
|
// Path is the module's directory inside that repository (novox/hq ADR 0069). Empty means the
|
|
// repository root, which is the ordinary case; a repository holding several modules names
|
|
// each by its own directory.
|
|
Path string `json:"path,omitempty"`
|
|
// Held is every artifact this mesh has built, keyed "<module>/<artifact>".
|
|
//
|
|
// **Sent with the asking rather than fetched by the builder** (novox/hq issue 044). A module
|
|
// says which module's artifact its build stands on; only the mesh knows which copy of that
|
|
// artifact *this* mesh holds, and the builder is deliberately a thing that clones, runs a
|
|
// build and answers — giving it a way to ask the mesh questions would make it something else.
|
|
// So the answer travels with the question.
|
|
//
|
|
// It is everything rather than only what this module needs, because what this module needs is
|
|
// written in a manifest the mesh has not read: it is inside the repository, and reading it is
|
|
// the build's first act.
|
|
Held map[string]string `json:"held,omitempty"`
|
|
// Seats is the clone base — `scheme://host:port` — of each seat a recipe's context may name
|
|
// (novox/hq ADR 0155): `git` for this mesh's own forge. Sent with the asking for the reason
|
|
// Held is: the context is written in a manifest the mesh has not read, and only the mesh knows
|
|
// which forge holds the seat here. A builder handed no base for a seat a context names refuses
|
|
// the build and says so.
|
|
Seats map[string]string `json:"seats,omitempty"`
|
|
// Source is the repository as the mesh records it when it lives on a seat's holder — the seat
|
|
// and the path on it, never the URL just composed (novox/hq ADR 0111). Carried with the
|
|
// asking and echoed in the outcome, so whoever hears the outcome can register the module with
|
|
// its true source, whether or not they were the one who asked (novox/hq issue 176).
|
|
Source *SourceOnSeat `json:"source,omitempty"`
|
|
// DryRun says the asker wants the outcome to look at and nothing else (novox/hq issue 240): the
|
|
// builder echoes it, and whoever hears the outcome takes nothing in — no record, no registration,
|
|
// no plan, nothing a push could send.
|
|
DryRun bool `json:"dry-run,omitempty"`
|
|
}
|
|
|
|
// SourceOnSeat names a repository by the seat whose holder serves it and its path there.
|
|
type SourceOnSeat struct {
|
|
Seat string `json:"seat"`
|
|
Repository string `json:"repository"`
|
|
}
|
|
|
|
// BuildResult is what a builder says back.
|
|
//
|
|
// **Failure is a result, not an absence.** A build that fails and says nothing is
|
|
// indistinguishable from a builder that is not running, and those want completely different
|
|
// responses — the same rule the host follows about a service that does not exist.
|
|
type BuildResult struct {
|
|
ID string `json:"id"`
|
|
Repository string `json:"repository"`
|
|
// Path is echoed back, so what the mesh records as this module's source is what was actually
|
|
// built rather than what the asker meant (novox/hq ADR 0069).
|
|
Path string `json:"path,omitempty"`
|
|
Ref string `json:"ref,omitempty"`
|
|
|
|
// On is the machine that did it, so a failure that is about one machine can be told from one
|
|
// about the source.
|
|
On string `json:"on"`
|
|
|
|
// Module is what was built, read out of the manifest — the only place it is authoritative, since
|
|
// a request names a repository and a path.
|
|
//
|
|
// **Here because one message now reaches three audiences** (novox/hq ADR 0121). On the bus the
|
|
// mesh runs on today the answer and the announcement were two publishes to two topologies, so a
|
|
// result needed no module name and the announcement carried one. On the bus being built the
|
|
// outcome is the role's own event, and the catalogue reading it needs to know what was built.
|
|
// Empty on a failed build, which produced no module version.
|
|
Module string `json:"module,omitempty"`
|
|
|
|
// Commit is what was actually built. The mesh records it, which is what makes "is this
|
|
// current?" answerable without building again.
|
|
Commit string `json:"commit,omitempty"`
|
|
|
|
// Manifest is the module as the mesh should hold it, artifacts resolved to digests. Raw,
|
|
// because the control plane parses it with the same parser it uses for one handed over by
|
|
// hand — a second path would be a second thing to disagree.
|
|
Manifest json.RawMessage `json:"manifest,omitempty"`
|
|
|
|
// Made is each artifact, for reporting.
|
|
Made []MadeArtifact `json:"made,omitempty"`
|
|
|
|
// Against is every pinned image this was built on top of, read out of the build's own inputs
|
|
// (novox/hq ADR 0009). The catalogue turns these into edges; nothing else need care.
|
|
Against []string `json:"against,omitempty"`
|
|
|
|
// Read is every repository this build read source from besides the module's own. A module whose
|
|
// recipe packages source that lives elsewhere is affected when that repository moves, and the
|
|
// manifest the mesh keeps says nothing about it (novox/hq 04-ISSUES/131).
|
|
Read []ReadRepository `json:"read,omitempty"`
|
|
|
|
// Failed is why, when it did.
|
|
Failed string `json:"failed,omitempty"`
|
|
|
|
// Source is the request's, echoed: the seat form of the repository, for whoever registers.
|
|
Source *SourceOnSeat `json:"source,omitempty"`
|
|
|
|
// DryRun is the request's, echoed: an outcome nobody may take in (novox/hq issue 240).
|
|
DryRun bool `json:"dry-run,omitempty"`
|
|
}
|
|
|
|
// ReadRepository is a repository a build read source from besides the module's own, at the branch,
|
|
// tag or commit it read. Spelled here as well as in the catalogue and the inventory, for the reason
|
|
// MadeArtifact is: one direction of dependency.
|
|
type ReadRepository struct {
|
|
Repository string `json:"repository"`
|
|
Ref string `json:"ref,omitempty"`
|
|
}
|
|
|
|
// MadeArtifact is one thing a build produced, as a person would want it reported.
|
|
type MadeArtifact struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Reference string `json:"reference"`
|
|
}
|