The controller asks no build of anybody (ADR 0501 §5, ADR 0525 clause 6, ADR 0503 §3). Every module of a walk is taken from the builder's done key; a key not done yet waits, named in the plan, and never fails it. - C2b/C2c: internal/builder and cmd/mesh-controller's asks go (askABuild, onlyTheBuildersOwn, buildOneAsked, takeIn from an outcome, buildAgentsOwn, the builder's say and its fallback, the daemon hearing an outcome). The server follows the retired seats' built subjects only to let go of them, because the installer's carried user list names them. - C2d: replay, queue, cancel, clear, kill, pause, resume, delivery-check, check-here and merge-gate are refused by name. - C3: the check queues (CHECKS_READY, CHECKS_OTHER) and their workers, the checks-running and plans-building buckets and their grants, the build queue and its hold (plans_building.go, queue.go, link/holds.go, builds_nats.go, checks_nats.go). - C4: merge_gate*.go, the pull request's repo-check path, the Checked event and its layers; a pull request asks the controller nothing but to tell mesh-config what its head declares. - C5: the builds bucket is written only by key_takein. - C10 (help and refusals): the terminal's help lists only what is served. Integration tests of the plan's tiers, sends, stops and retries now run on a stand-in builder with a done key for every module (walk_keys_scaffold_integration_test.go); the tests of the asks, the queue, the merge gate and the replays are deleted.
295 lines
14 KiB
Go
295 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"io"
|
|
"os"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// asking a build machine for a module, and what came back.
|
|
//
|
|
// Split out of main.go, which had reached 2,769 lines because appending was always the
|
|
// cheapest next step. That is how novox/hq ADR 0001 records `hal/sdk` reaching 34,636:
|
|
// nothing in it was wrong, and no one edit was the one that should have been a new file.
|
|
|
|
// buildsCommand says what has been built lately.
|
|
func buildsCommand(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("builds", flag.ContinueOnError)
|
|
limit := set.Int("n", 20, "how many to show")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
module := ""
|
|
if len(positionals) == 1 {
|
|
module = positionals[0]
|
|
} else if len(positionals) > 1 {
|
|
return errors.New("builds [<module>] [-n N]")
|
|
}
|
|
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
|
|
builds, err := inv.Builds(ctx, module, *limit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(builds) == 0 {
|
|
// Said rather than printed as nothing: an empty list and a failed read must never look
|
|
// the same, and getting here means the store answered.
|
|
if module != "" {
|
|
fmt.Printf("nothing has been built for %s\n", module)
|
|
return nil
|
|
}
|
|
fmt.Println("nothing has been built yet")
|
|
return nil
|
|
}
|
|
|
|
for _, b := range builds {
|
|
printBuild(os.Stdout, b)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// printBuild says one build: what, its outcome, where and when, what it made and published, its reuse key, and its
|
|
// source fingerprint (novox/hq issue 1204: the one tool that reads what the builder recorded it was made from).
|
|
func printBuild(w io.Writer, b inventory.Build) {
|
|
what := b.Module
|
|
if what == "" {
|
|
// It failed before knowing what it was building, which is most of the interesting
|
|
// failures. The repository is what a person has to go and look at.
|
|
what = "?"
|
|
}
|
|
outcome := "built " + short(b.Commit)
|
|
if !b.Worked() {
|
|
outcome = "failed"
|
|
}
|
|
fmt.Fprintf(w, "%-18s %-14s %-10s %s %s\n",
|
|
what, outcome, b.On, b.At.Local().Format("2006-01-02 15:04"), b.ID)
|
|
fmt.Fprintf(w, " %s", b.Repository)
|
|
if b.Ref != "" {
|
|
fmt.Fprintf(w, " at %s", b.Ref)
|
|
}
|
|
fmt.Fprintln(w)
|
|
for _, made := range b.Made {
|
|
fmt.Fprintf(w, " %-10s %s\n", made.Kind, made.Reference)
|
|
}
|
|
// What it published and under what reuse key (novox/hq ADR 0408 §1).
|
|
if p := b.Published; p != nil {
|
|
if p.Artifact != "" {
|
|
fmt.Fprintf(w, " published %s\n", p.Artifact)
|
|
}
|
|
if p.Package != nil {
|
|
fmt.Fprintf(w, " published %s@%s\n", p.Package.Name, p.Package.Version)
|
|
}
|
|
}
|
|
if b.ReuseKey != "" {
|
|
fmt.Fprintf(w, " reuse key %s\n", b.ReuseKey)
|
|
}
|
|
if b.SourceFingerprint != "" {
|
|
fmt.Fprintf(w, " source %s\n", b.SourceFingerprint)
|
|
}
|
|
if !b.Worked() {
|
|
// The builder's own first line. The whole failure is often a build log, and printing
|
|
// it here would bury every other row.
|
|
fmt.Fprintf(w, " %s\n", firstLine(b.Failed))
|
|
}
|
|
}
|
|
|
|
// takenBuild is a build the controller judges and registers, whatever told it of the build: build-agent's outcome (the
|
|
// builder's own module's, until R4g, ADR 0453 §5) or the builder's done key (ADR 0501 §1). **One judging for both**:
|
|
// the rules of who may register, the gate a build failed, the predefined keys and the rule that an unchanged build
|
|
// source moves nothing are the same whichever part built it (ADR 0501 §1, §3).
|
|
type takenBuild struct {
|
|
kept inventory.Build
|
|
manifest catalogue.Manifest
|
|
recorded inventory.Source
|
|
ask registrationAsk
|
|
path string
|
|
trunk string
|
|
// by and what name the build in what is said: who built it, and what was built ("ace", "novox/mesh-catalog").
|
|
by, what, commit string
|
|
// published refuses a build that may not become a module's version (off the trunk, in the scratch namespace);
|
|
// nil for a key, which is a trunk fingerprint's by construction (ADR 0501 §2).
|
|
published func() error
|
|
// carried publishes a list of predefined keys the build carried; nil when it carried none.
|
|
carried func() error
|
|
}
|
|
|
|
// registerJudged judges a recorded build and registers its module, or says why not.
|
|
func registerJudged(ctx context.Context, inv *inventory.Inventory, j takenBuild) (catalogue.Manifest, error) {
|
|
manifest, kept := j.manifest, j.kept
|
|
notRegistered := func(err error) (catalogue.Manifest, error) {
|
|
return manifest, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", j.by, manifest.Module,
|
|
short(j.commit), err)
|
|
}
|
|
if err := namesNoInstallation(manifest); err != nil {
|
|
return manifest, fmt.Errorf("%s built %s (%s), and the mesh does not register it: %w",
|
|
j.by, j.what, short(j.commit), err)
|
|
}
|
|
// **Only from the repository the catalogue builds the module from** (novox/hq ADR 0266): else the trunk
|
|
// below is the trunk of whatever repository was built, which may be one an agent made — and a module named
|
|
// `sudo` from it would be what the next push sends. Another repository is the operator's, at the terminal.
|
|
repoID, err := mayRegisterFrom(ctx, inv, manifest.Module, j.recorded, j.ask, j.path, j.trunk)
|
|
if err != nil {
|
|
return notRegistered(err)
|
|
}
|
|
if j.published != nil {
|
|
if err := j.published(); err != nil {
|
|
return notRegistered(err)
|
|
}
|
|
}
|
|
// **A build that failed its gate is never registered again** (novox/hq ADR 0236): an outcome heard
|
|
// twice, or replayed, would otherwise make the build a rollback put back what the module is again,
|
|
// and the next push would send it.
|
|
if failed, err := inv.GateFailed(ctx, kept.ID); err != nil {
|
|
return manifest, err
|
|
} else if failed {
|
|
return manifest, fmt.Errorf("%s built %s (%s), which failed its gate on its first machine and was put "+
|
|
"back: it is recorded and not registered again — a newer build is", j.by, manifest.Module,
|
|
short(j.commit))
|
|
}
|
|
// Then the module is judged against the list the mesh publishes, as every manifest read again is: one naming a
|
|
// key that list does not hold, or of another type, is refused here and never enters the catalogue.
|
|
if j.carried != nil {
|
|
if err := j.carried(); err != nil {
|
|
return manifest, err
|
|
}
|
|
}
|
|
if manifest.NamesPredefined() {
|
|
if _, err := catalogue.ParseManifest(kept.Manifest); err != nil {
|
|
return notRegistered(fmt.Errorf("judged against the list of predefined keys the mesh publishes, %w", err))
|
|
}
|
|
}
|
|
// **A build whose source is unchanged is never a move** (novox/hq issue 280): a rebuild made from
|
|
// what the build the mesh stands on was made from registers that build's artifacts at the new
|
|
// commit, so no machine is sent a new digest for a source nobody changed — an image is not
|
|
// byte-reproducible, and the bus rebuilt for another module's merge demanded a planned upgrade.
|
|
if kept.SourceFingerprint != "" {
|
|
stands, raw, err := inv.StandingBuild(ctx, manifest.Module, kept.ID)
|
|
if err != nil {
|
|
return manifest, err
|
|
}
|
|
if stands != "" && stands != kept.ID && len(raw) > 0 {
|
|
if was, err := catalogue.ParseManifest(raw); err == nil && was.Module == manifest.Module {
|
|
fmt.Printf("%s at %s was made from the source %s was: registered with its artifacts, no move\n",
|
|
manifest.Module, short(j.commit), stands)
|
|
manifest = was
|
|
}
|
|
}
|
|
}
|
|
if err := inv.RegisterModule(ctx, manifest, j.recorded); err != nil {
|
|
if errors.Is(err, inventory.ErrSuperseded) {
|
|
return notRegistered(err)
|
|
}
|
|
return manifest, err
|
|
}
|
|
// A bus process the new definition gains gets its account now, on each machine assigned the module, so the
|
|
// next push composes a real account (issue 1226).
|
|
issueGainedProcessAccounts(ctx, inv, manifest)
|
|
// Which repository it is registered from, by the forge's own id (novox/hq ADR 0266).
|
|
if err := inv.SetSourceIdentity(ctx, manifest.Module, repoID); err != nil {
|
|
return manifest, err
|
|
}
|
|
// The keep set just moved, and new bytes just landed (novox/hq ADR 0189). Asked here rather
|
|
// than on a timer of its own: this is the only moment either is true. Never fatal — the build
|
|
// worked and the module is registered.
|
|
collect(ctx, inv)
|
|
return manifest, nil
|
|
}
|
|
|
|
// answers is what the three questions came back with, read once.
|
|
type answers struct {
|
|
// notAnnounced are the owners some module reads the state of that have announced nothing yet (novox/hq ADR 0495,
|
|
// issue 1790): read from `reads` as before until they do; notAnnouncedWhy why that could not be read.
|
|
notAnnounced []string
|
|
notAnnouncedWhy string
|
|
wrong []inventory.Doing
|
|
nodes []inventory.Node
|
|
quiet []inventory.Node
|
|
behind map[string][]string
|
|
sources map[string]inventory.Source
|
|
// waiting is every machine not running what the mesh would send it.
|
|
waiting []inventory.Machine
|
|
// recordedWaits is, per machine, every move of a recorded module that waits there for a person (novox/hq ADR
|
|
// 0377 §6): the machine is behind for these, and `push <node>` sends them.
|
|
recordedWaits map[string][]string
|
|
// reported is every machine's last word beside when it was last sent a declaration — the
|
|
// pair that answers "has it caught up", which waiting alone cannot (the sent digest is
|
|
// recorded at send, not at apply).
|
|
reported []inventory.Reported
|
|
// plans is what the last merges produced and where each stands (novox/hq ADR 0162).
|
|
plans []inventory.Plan
|
|
// tierBounds is how long each repository's plan tier may take before it is late (novox/hq issue 296).
|
|
tierBounds tierBounds
|
|
// refused is why a machine cannot be worked out at all, by name. A different thing from every
|
|
// other answer here: those are about a machine that was told something, and this is about one
|
|
// that cannot be told anything — it never reaches waiting, because nothing was computed for it
|
|
// to compare against, so without this a wholly blocked mesh reads as a well one.
|
|
refused map[string]string
|
|
// network is why the private network could not be computed, when it could not. Almost always
|
|
// a consequence of the refusals above: a node that does not resolve is not on the network, and
|
|
// a mesh whose hub is that node has no hub.
|
|
network string
|
|
// filtered is every converged machine that is not filtered by the mesh alone (novox/hq ADR
|
|
// 0168): what filters it beyond the mesh's own, the runtime's plumbing and bans, by name — a
|
|
// predecessor's chain, a found firewall in force again. Such a machine is not "all well".
|
|
filtered map[string]inventory.Filtering
|
|
// untaken is, per machine, each assigned module whose resources the machine is holding as it
|
|
// found them, and how many — a module that was assigned, sent, and is running none of what it
|
|
// declares because nothing has taken it (novox/hq ADR 0100, 04-ISSUES/125).
|
|
//
|
|
// **Its absence cost an outage.** The module was assigned, the push reported success, this
|
|
// command said the machine was doing everything it was told, and the module's three containers
|
|
// did not exist. On the strength of those reports the predecessor's proxy was stopped and every
|
|
// public name on the machine went dark. The holds were correct; they were recorded only in the
|
|
// machine's own state file, and the one visible symptom was a count that did not add up.
|
|
untaken map[string]map[string]int
|
|
// leftOut is, per machine, every module of its set its composition leaves out, and why (novox/hq issue
|
|
// 380): assigned and not applied, which every push said only in passing. Not well while there is any.
|
|
leftOut map[string][]leftOutModule
|
|
// unheld is every module on a machine whose resources are applied through a seat nothing on
|
|
// that machine holds (novox/hq ADR 0207), with the modules that could hold it. Reported, not
|
|
// refused, until the switch — and while there is any, the mesh is not all well: the order the
|
|
// machines' modules are built in is the mesh's to keep, and this is where it says it is not kept.
|
|
unheld []catalogue.Unheld
|
|
// conditions is every open condition (novox/hq to-be 45 §2), urgent first and then oldest first:
|
|
// what leads status, and what its all-well sentence needs to be none of, silenced ones included.
|
|
// A provider failing a consumer is one of them (ADR 0224). conditionsUnread says why they could
|
|
// not be read when they could not — never read as none.
|
|
conditions []conditions.Condition
|
|
conditionsUnread string
|
|
// overflowing is every module whose identity overflows the bound of a provision it requires
|
|
// (novox/hq ADR 0225): its provider leaves it out of the grants and composes everything else, so
|
|
// this is the one place it is said across the mesh. Not well while there is any.
|
|
overflowing []catalogue.Overflow
|
|
// handActs is how many acts were done by hand in the last seven days (novox/hq to-be 45 §7), nil
|
|
// where the log is not on hand; handActsUnread why it could not be read when it could not.
|
|
handActs *int
|
|
handActsUnread string
|
|
// heals is what the healers did in the last seven days (novox/hq to-be 45 §7): acts, and conditions
|
|
// handed to the operator; healsUnread why it could not be read.
|
|
heals *healsCount
|
|
healsUnread string
|
|
// pending is every assignment waiting for its module's build, and every one ended in the last day
|
|
// (novox/hq issue 325); pendingUnread why they could not be read. One waiting, or one ended other than
|
|
// applied, is not well: an assignment somebody made is not made yet, or will not be.
|
|
pending []inventory.PendingAssignment
|
|
pendingUnread string
|
|
// lastSent is every machine's last recorded send, by name, with what the machine answered of it (novox/hq
|
|
// issue 485); lastSentUnread why it could not be read.
|
|
lastSent map[string]inventory.Send
|
|
lastSentUnread string
|
|
}
|