A module declaring tools, a container, and a build on mesh-tools' runtime image is a container whose purpose is serving tools — the pattern the node's tool runtime retires. Once node-tools is in the catalogue, registering one is refused by name, with the record that says why; before, it is accepted as it always was, so a mesh converts in the design's order and nothing is refused before there is anything to move to. This is the mechanism that keeps the old pattern from returning by habit. Judged from a repository manifest's own build.on, and for a built manifest — which carries no build — from what its build stood on, now recorded beside the commit as part of a module's provenance.
732 lines
28 KiB
Go
732 lines
28 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// 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.
|
|
|
|
// buildCommand builds a module from its source and records what came out.
|
|
//
|
|
// **Run where there is a container runtime**, which is why it is a command rather than something
|
|
// the control plane does on its own: building needs to run things on a machine, and what the
|
|
// control plane may send a machine is bounded by the declaration language. This is the shape the
|
|
// builder module will take when it is given work over the broker; today a person runs it, and the
|
|
// mesh records the result the same way either way.
|
|
// buildOn rebuilds every module the mesh holds that stands on the named module's artifacts — the
|
|
// rebuild a changed base needs, which nothing else asks for: their sources did not move, and
|
|
// "behind" does not see a base that did (novox/hq 04-ISSUES/131). Bases first among them too.
|
|
func buildOn(ctx context.Context, base string, wait time.Duration) error {
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
held, err := open.inventory.Catalogued(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
against, err := open.inventory.BuiltAgainst(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var on []inventory.Entry
|
|
for _, e := range held {
|
|
if standsOnModule(e, base, against) {
|
|
on = append(on, e)
|
|
}
|
|
}
|
|
if len(on) == 0 {
|
|
fmt.Printf("nothing the mesh holds stands on %s\n", base)
|
|
return nil
|
|
}
|
|
on = orderByBases(on, against)
|
|
fmt.Printf("%d module(s) stand on %s:\n", len(on), base)
|
|
var failed []string
|
|
for _, e := range on {
|
|
fmt.Printf("--- %s\n", e.Manifest.Module)
|
|
source := buildSource{Repository: e.Source.Repository, Seat: e.Source.Seat}
|
|
if err := buildOne(ctx, source, e.Source.Path, e.Source.Ref, wait); err != nil {
|
|
fmt.Printf(" %v\n", err)
|
|
failed = append(failed, e.Manifest.Module)
|
|
}
|
|
}
|
|
if len(failed) > 0 {
|
|
return fmt.Errorf("%d of %d could not be built: %s", len(failed), len(on), strings.Join(failed, ", "))
|
|
}
|
|
fmt.Printf("\n%d module(s) rebuilt on %s. `push --behind` sends them on\n", len(on), base)
|
|
return nil
|
|
}
|
|
|
|
func buildCommand(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("build", flag.ContinueOnError)
|
|
ref := set.String("ref", "", "the branch, tag or commit to build")
|
|
// A module is a repository and a path within it (novox/hq ADR 0069). Empty is the repository's
|
|
// root, which is the ordinary case and why this is a flag rather than a second argument.
|
|
path := set.String("path", "", "the module's directory inside the repository")
|
|
wait := set.Duration("wait", 10*time.Minute, "how long to wait for a builder to answer")
|
|
dryRun := set.Bool("dry-run", false, "build and print the manifest, recording nothing")
|
|
// Every module whose source has moved, rather than one named repository.
|
|
//
|
|
// **The mirror of `push --behind`, and the same argument** (novox/hq ADR 0010): the mesh
|
|
// already knows which modules are behind their source, so making a person read that list and
|
|
// retype each repository is asking them to be the loop. Naming a repository and asking which
|
|
// ones need building are different requests, so they are not combined.
|
|
behind := set.Bool("behind", false, "every module the mesh holds older than its source has")
|
|
on := set.String("on", "", "rebuild every module that stands on this module's artifacts — the rebuild a changed base needs")
|
|
// A repository on the mesh's own forge, named by its path there (novox/hq ADR 0111). Without it
|
|
// the repository is external, cloned exactly as given — see source.go.
|
|
self := set.Bool("self", false, "the repository is a path on the forge holding the git seat")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *on != "" {
|
|
if len(positionals) != 0 || *behind || *self {
|
|
return errors.New("build --on <module> names a base and nothing else")
|
|
}
|
|
return buildOn(ctx, *on, *wait)
|
|
}
|
|
|
|
if *behind {
|
|
if len(positionals) != 0 || *self {
|
|
return errors.New("build <repository> or build --behind, not both: one names a " +
|
|
"repository and the other asks which need building")
|
|
}
|
|
return buildBehind(ctx, *wait)
|
|
}
|
|
if len(positionals) != 1 {
|
|
return errors.New("build <repository> [--self] [--path P] [--ref R] [--wait D] [--dry-run] | build --behind | build --on <module>")
|
|
}
|
|
source := buildSource{Repository: positionals[0]}
|
|
if *self {
|
|
if err := onASeat(source.Repository); err != nil {
|
|
return err
|
|
}
|
|
source.Seat = gitSeat
|
|
}
|
|
|
|
if *dryRun {
|
|
return buildAndShow(ctx, source, *path, *ref, *wait)
|
|
}
|
|
return buildOne(ctx, source, *path, *ref, *wait)
|
|
}
|
|
|
|
// buildFrom turns what a builder said into what the mesh keeps.
|
|
//
|
|
// **By digest and path, never by where it was pushed** (novox/hq 04-ISSUES/102). The builder
|
|
// says `<registry>:<port>/<module>/<artifact>@sha256:…`; the mesh records the artifact-store
|
|
// reference and composes the store's address back in where a reference is used. `against` — what
|
|
// the build stood on, the catalogue's edge — is recorded the same way, so an edge names a module's
|
|
// artifact and not the machine it was pulled from.
|
|
func buildFrom(result link.BuildResult) inventory.Build {
|
|
kept := inventory.Build{
|
|
ID: result.ID, Repository: result.Repository, Ref: result.Ref,
|
|
Commit: result.Commit, On: result.On, Failed: result.Failed,
|
|
// **What the announcement carries, kept rather than discarded** (novox/hq 04-ISSUES/050).
|
|
// The catalogue turns the manifest into requires/provides edges and `against` into build
|
|
// edges, and it is not always listening when a build happens — on a fresh mesh it cannot
|
|
// be, for exactly the modules it needs most. Keeping them is what makes a replay able to
|
|
// rebuild the graph rather than a list of names.
|
|
Path: result.Path,
|
|
}
|
|
for _, ref := range result.Against {
|
|
kept.Against = append(kept.Against, catalogue.Recorded(ref))
|
|
}
|
|
for _, r := range result.Read {
|
|
kept.Read = append(kept.Read, inventory.ReadRepository{Repository: r.Repository, Ref: r.Ref})
|
|
}
|
|
var announced []inventory.Artifact
|
|
for _, made := range result.Made {
|
|
announced = append(announced, inventory.Artifact{
|
|
Name: made.Name, Kind: made.Kind, Reference: made.Reference,
|
|
})
|
|
kept.Made = append(kept.Made, inventory.Artifact{
|
|
Name: made.Name, Kind: made.Kind, Reference: catalogue.Recorded(made.Reference),
|
|
})
|
|
}
|
|
kept.Manifest = recordedManifest(result.Manifest, announced)
|
|
// The module name comes from the manifest, which only exists when the build got that far.
|
|
if len(kept.Manifest) > 0 {
|
|
if m, err := catalogue.ParseManifest(kept.Manifest); err == nil {
|
|
kept.Module = m.Module
|
|
}
|
|
}
|
|
return kept
|
|
}
|
|
|
|
// 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")
|
|
logOf := set.String("log", "", "a build's id: print what the build machine said, line by line")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *logOf != "" {
|
|
return buildLog(ctx, *logOf)
|
|
}
|
|
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 {
|
|
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.Printf("%-18s %-14s %-10s %s %s\n",
|
|
what, outcome, b.On, b.At.Local().Format("2006-01-02 15:04"), b.ID)
|
|
fmt.Printf(" %s", b.Repository)
|
|
if b.Ref != "" {
|
|
fmt.Printf(" at %s", b.Ref)
|
|
}
|
|
fmt.Println()
|
|
for _, made := range b.Made {
|
|
fmt.Printf(" %-10s %s\n", made.Kind, made.Reference)
|
|
}
|
|
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.Printf(" %s\n", firstLine(b.Failed))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// builderCommand issues a build machine its own broker credential.
|
|
//
|
|
// **A build machine is not a node**, and giving it a node's account would let it read another
|
|
// machine's declarations. This is narrower and different: read the build queue, write the
|
|
// exchange and an asker's reply queue, and nothing else.
|
|
//
|
|
// Issued rather than assumed, because until this the builder used whatever credential it was
|
|
// handed — which in practice meant the broker's own administrative one. A program documented as
|
|
// holding its own credential and given somebody else's is worse than one with no story at all.
|
|
func builderCommand(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("builder issue", flag.ContinueOnError)
|
|
// Which machine will use it. Given, the credential is delivered by the mesh rather than
|
|
// printed for somebody to carry — which is the difference between the builder being a module
|
|
// and being a program somebody configures.
|
|
forNode := set.String("node", "",
|
|
"the machine that will run it, so the mesh delivers the credential instead of printing it")
|
|
module := set.String("module", "builder", "the module on that machine that will read it")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 2 || positionals[0] != "issue" {
|
|
return errors.New("builder issue <name> [--node <machine>]")
|
|
}
|
|
name := positionals[1]
|
|
|
|
// **The build machine's credential is a module's credential** (novox/hq ADR 0131, design 28
|
|
// task 5.5): minted into the mesh's records and sealed to the machine as the builder module's
|
|
// broker secret, usable at the next push — the same act `module issue` performs, and the same
|
|
// account the composed user list carries. Nothing is created on a server; the bus reads the list.
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m, known := shelf[*module]
|
|
if !known {
|
|
return fmt.Errorf("%s is not in the catalogue; `module add` it first", *module)
|
|
}
|
|
fmt.Printf("build machine %s: ", name)
|
|
node := *forNode
|
|
if node == "" {
|
|
entries, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, e := range entries {
|
|
if e.Manifest.Module == *module && len(e.On) > 0 {
|
|
node = e.On[0]
|
|
}
|
|
}
|
|
}
|
|
if node == "" {
|
|
return fmt.Errorf("%s is assigned nowhere; `assign <machine> %s` first, or say --node", *module, *module)
|
|
}
|
|
address, err := broker.BusAddress()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return issueOnTheNewBus(ctx, inv, m, node, address)
|
|
}
|
|
|
|
// buildBehind builds every module the mesh holds older than its source has.
|
|
//
|
|
// **This is the loop novox/hq ADR 0010 replaced a pipeline with, closed.** The mesh already
|
|
// records where each module came from and what its source last had; until this, a person read
|
|
// that list and retyped each repository — which is a person being the loop, and the thing a
|
|
// pipeline was doing before it was taken away.
|
|
//
|
|
// Each is built and recorded on its own. **One failing does not stop the others**, for the same
|
|
// reason one broken module no longer blocks a machine's whole declaration: a mesh where one bad
|
|
// repository holds back nine good ones is a mesh where nobody dares add the tenth.
|
|
func buildBehind(ctx context.Context, wait time.Duration) error {
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
|
|
held, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var stale []inventory.Entry
|
|
for _, e := range held {
|
|
if !e.Source.Current() {
|
|
stale = append(stale, e)
|
|
}
|
|
}
|
|
if len(stale) == 0 {
|
|
// Said rather than doing nothing quietly: "nothing needed building" and "this did not
|
|
// run" must never look the same.
|
|
fmt.Println("every module the mesh holds is what its source last had")
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("%d module(s) behind their source:\n", len(stale))
|
|
for _, e := range stale {
|
|
fmt.Printf(" %s %s < %s\n",
|
|
e.Manifest.Module, short(e.Source.BuiltFrom), short(e.Source.Head))
|
|
}
|
|
fmt.Println()
|
|
|
|
// Bases first: a module built before the module it stands on is built against the old one
|
|
// and reports success (novox/hq 04-ISSUES/131).
|
|
against, err := inv.BuiltAgainst(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stale = orderByBases(stale, against)
|
|
|
|
var failed []string
|
|
for _, e := range stale {
|
|
fmt.Printf("--- %s\n", e.Manifest.Module)
|
|
// Its own recorded ref, not its head commit: a module tracking a branch should be built
|
|
// from that branch, and pinning to the commit the mesh happened to notice would quietly
|
|
// turn a tracked branch into a pin.
|
|
source := buildSource{Repository: e.Source.Repository, Seat: e.Source.Seat}
|
|
if err := buildOne(ctx, source, e.Source.Path, e.Source.Ref, wait); err != nil {
|
|
fmt.Printf(" %v\n", err)
|
|
failed = append(failed, e.Manifest.Module)
|
|
}
|
|
}
|
|
|
|
if len(failed) > 0 {
|
|
return fmt.Errorf("%d of %d could not be built: %s",
|
|
len(failed), len(stale), strings.Join(failed, ", "))
|
|
}
|
|
fmt.Printf("\n%d module(s) built. `push --behind` sends them to the machines running them\n",
|
|
len(stale))
|
|
return nil
|
|
}
|
|
|
|
// buildOne asks a build machine for one repository and records everything that came back.
|
|
//
|
|
// Separated from the command so `--behind` can walk a list without a second path to the same act.
|
|
func buildOne(ctx context.Context, source buildSource, path, ref string, wait time.Duration) error {
|
|
// Before anything is asked of a builder: a source on a seat nobody holds is refused here, with
|
|
// the reason, rather than sent to a machine to fail at `git clone`.
|
|
repository, err := cloneFrom(ctx, source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ident, err := openIdentity(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ident.Close()
|
|
|
|
server, err := connectLink(ctx, nil, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer server.Close()
|
|
|
|
// Correlated by something the control plane makes, not by the module's name: two builds of one
|
|
// module can be in flight, and the second answer is not the first one's.
|
|
request := link.BuildRequest{
|
|
ID: fmt.Sprintf("%s-%d", "build", time.Now().UnixNano()),
|
|
Repository: repository,
|
|
Path: path,
|
|
Ref: ref,
|
|
Held: heldBy(ctx),
|
|
Seats: seatBases(ctx),
|
|
}
|
|
fmt.Printf("asked for %s", source)
|
|
if source.Seat != "" {
|
|
fmt.Printf(" (%s)", repository)
|
|
}
|
|
// The id is how a person follows this build while it runs: `builds --log <id>`.
|
|
fmt.Printf(" as %s", request.ID)
|
|
if path != "" {
|
|
fmt.Printf(" at %s", path)
|
|
}
|
|
if ref != "" {
|
|
fmt.Printf(" on %s", ref)
|
|
}
|
|
fmt.Println()
|
|
|
|
ask, err := askOver(server)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ask.Close()
|
|
|
|
if wait == 0 {
|
|
// Asked and not waited for (novox/hq issue 176): the outcome is the role's event, and the
|
|
// controller takes it in — records the build, registers the module — whether or not anybody
|
|
// is still here. A tool call cannot hold a connection for the minutes a build takes; it
|
|
// follows the build by its id instead.
|
|
if err := ask.Ask(ctx, request); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("asked, not waited for: `builds --log %s` follows it as it runs, and `builds` "+
|
|
"shows what came of it; the module is registered when the outcome comes\n", request.ID)
|
|
return nil
|
|
}
|
|
|
|
result, err := ask.Submit(ctx, request, wait)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
manifest, kept, err := takeIn(ctx, open.inventory, result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Said as recorded: what each artifact is, not where this builder happened to push it.
|
|
for _, made := range kept.Made {
|
|
fmt.Printf(" %-12s %s %s\n", made.Name, made.Kind, made.Reference)
|
|
}
|
|
fmt.Printf("\n%s %s, built on %s from %s\n",
|
|
manifest.Module, manifest.Version, result.On, short(result.Commit))
|
|
saysWhenThePolicyActs(ctx, open.inventory, manifest.Module)
|
|
fmt.Printf(" run `assign <node> %s` to put it somewhere\n", manifest.Module)
|
|
return nil
|
|
}
|
|
|
|
// saysWhenThePolicyActs tells whoever built a module that its upgrade policy will send the
|
|
// result on at once (novox/hq issue 126, ADR 0163): a person choreographing a data move must
|
|
// know which module will not wait for them.
|
|
func saysWhenThePolicyActs(ctx context.Context, inv *inventory.Inventory, module string) {
|
|
if u, err := inv.UpgradeOf(ctx, module); err == nil && u.RollOut {
|
|
how := "one machine at a time"
|
|
if u.Together {
|
|
how = "every machine at once"
|
|
}
|
|
fmt.Printf(" %s rolls out on build: the machines running it are sent this now, %s — "+
|
|
"`upgrade %s record` first if something must move before it does\n", module, how, module)
|
|
}
|
|
}
|
|
|
|
// takeIn is what the mesh does with a build's outcome, whoever hears it: the waiting command and
|
|
// the daemon that follows the role's events both come here (novox/hq issue 176), so a build's
|
|
// result reaches the catalogue whether or not the asker was still listening.
|
|
//
|
|
// Kept before it is judged. A failed build that leaves no trace is indistinguishable from one
|
|
// nobody asked for, and the difference is the whole of whether somebody should be looking at
|
|
// something. Then parsed with the same parser a hand-written manifest goes through — a second path
|
|
// would be a second thing to disagree about what a manifest is — and registered with where it came
|
|
// from: **for a source on a seat, as the path and the seat, never the URL just cloned** (ADR 0111),
|
|
// which the request carried and the outcome echoes. A definition naming an installation is refused
|
|
// here, where it would enter the catalogue; the build stays recorded and the refusal says which.
|
|
//
|
|
// Idempotent: the same outcome taken in twice registers the same module twice, which is one row
|
|
// written with the same values.
|
|
func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResult) (
|
|
catalogue.Manifest, inventory.Build, error) {
|
|
kept := buildFrom(result)
|
|
if err := inv.RecordBuild(ctx, kept); err != nil {
|
|
return catalogue.Manifest{}, kept, err
|
|
}
|
|
if result.Failed != "" {
|
|
// The builder's own words. Wrapping them in something about the control plane would put
|
|
// two explanations between a person and a build log.
|
|
return catalogue.Manifest{}, kept, fmt.Errorf("%s could not build %s:\n%s",
|
|
result.On, result.Repository, result.Failed)
|
|
}
|
|
manifest, err := catalogue.ParseManifest(kept.Manifest)
|
|
if err != nil {
|
|
return catalogue.Manifest{}, kept, fmt.Errorf("%s built %s and what came back is not a manifest: %w",
|
|
result.On, result.Repository, err)
|
|
}
|
|
recorded := inventory.Source{
|
|
Repository: result.Repository, Path: result.Path, Ref: result.Ref,
|
|
BuiltFrom: result.Commit, Head: result.Commit,
|
|
// What it stood on, so registration can judge a built manifest's base (to-be 38 WP2.4).
|
|
Against: kept.Against,
|
|
}
|
|
if result.Source != nil && result.Source.Seat != "" {
|
|
recorded.Repository, recorded.Seat = result.Source.Repository, result.Source.Seat
|
|
}
|
|
if err := namesNoInstallation(manifest); err != nil {
|
|
return manifest, kept, fmt.Errorf("%s built %s (%s), and the mesh does not register it: %w",
|
|
result.On, result.Repository, short(result.Commit), err)
|
|
}
|
|
if err := inv.RegisterModule(ctx, manifest, recorded); err != nil {
|
|
return manifest, kept, err
|
|
}
|
|
return manifest, kept, nil
|
|
}
|
|
|
|
// buildAndShow builds and prints the manifest without recording anything.
|
|
func buildAndShow(ctx context.Context, source buildSource, path, ref string, wait time.Duration) error {
|
|
repository, err := cloneFrom(ctx, source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ident, err := openIdentity(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ident.Close()
|
|
server, err := connectLink(ctx, nil, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer server.Close()
|
|
|
|
ask, err := askOver(server)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ask.Close()
|
|
|
|
result, err := ask.Submit(ctx, link.BuildRequest{
|
|
ID: fmt.Sprintf("%s-%d", "build", time.Now().UnixNano()),
|
|
Repository: repository, Path: path, Ref: ref,
|
|
Held: heldBy(ctx), Seats: seatBases(ctx),
|
|
}, wait)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result.Failed != "" {
|
|
return fmt.Errorf("%s could not build %s:\n%s", result.On, result.Repository, result.Failed)
|
|
}
|
|
manifest, err := catalogue.ParseManifest(result.Manifest)
|
|
if err != nil {
|
|
return fmt.Errorf("%s built %s and what came back is not a manifest: %w",
|
|
result.On, result.Repository, err)
|
|
}
|
|
body, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(body))
|
|
return nil
|
|
}
|
|
|
|
// answers is what the three questions came back with, read once.
|
|
type answers struct {
|
|
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
|
|
// 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
|
|
// 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
|
|
}
|
|
|
|
// heldBy is every artifact this mesh has built, for a build that may need one as its base.
|
|
//
|
|
// **A failure here is not a failure to build.** A module that names no base does not need this at
|
|
// all, and one that does gets a refusal naming exactly what is missing — which is a better sentence
|
|
// than a build command refusing to start because a query did not run. So the store not opening is
|
|
// reported and the build goes ahead without it.
|
|
//
|
|
// Routed through the artifact store as the network reaches it now (novox/hq 04-ISSUES/102): a
|
|
// base is recorded by digest and path, and a build machine needs something it can pull.
|
|
func heldBy(ctx context.Context) map[string]string {
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "could not read what this mesh has built, so a module naming a "+
|
|
"base will be told that base is missing: %v\n", err)
|
|
return nil
|
|
}
|
|
defer open.Close()
|
|
held, err := open.inventory.Held(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "could not read what this mesh has built: %v\n", err)
|
|
return nil
|
|
}
|
|
address, err := whereABuilderReachesTheStore(ctx, open.inventory)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "could not find the artifact store on this mesh's network, so a "+
|
|
"module naming a base will be handed a reference nothing can fetch: %v\n", err)
|
|
return held
|
|
}
|
|
if address == "" {
|
|
return held
|
|
}
|
|
routed := make(map[string]string, len(held))
|
|
for repository, reference := range held {
|
|
routed[repository] = catalogue.Rerouted(reference, address)
|
|
}
|
|
return routed
|
|
}
|
|
|
|
// askOver opens the way a build is asked for, on whichever bus the mesh is on.
|
|
//
|
|
// **One place chooses**, as everywhere else the bus change went (novox/hq ADR 0116 step 5). On the bus
|
|
// the mesh runs on today this needs the controller's own connection, so it is handed one; on the bus
|
|
// being built it dials, because a build request is a one-shot and holds nothing else.
|
|
func askOver(_ *link.Server) (link.Builders, error) {
|
|
address, err := broker.BusAddress()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return link.BuildsOverNATS(address)
|
|
}
|
|
|
|
// buildLog prints everything a build machine said about one build, read back from the bus.
|
|
//
|
|
// **From the stream, not from a record** (novox/hq ADR 0157). A build's lines are the role's own
|
|
// events under the build's id, retained with every other event; the mesh keeps no second copy. Read
|
|
// with a consumer of its own that is gone when this returns, so nothing accumulates in the server
|
|
// for the reading, and filtered by subject, so one build's lines are all that travel.
|
|
func buildLog(ctx context.Context, id string) error {
|
|
address, err := broker.BusAddress()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
js, err := broker.Dial(address)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot reach the bus to read a build's log: %w", err)
|
|
}
|
|
defer js.Close()
|
|
|
|
sub, err := js.Context().PullSubscribe(link.BuildLog(id), "",
|
|
nats.BindStream(broker.EventsStream), nats.DeliverAll(), nats.AckNone())
|
|
if err != nil {
|
|
return fmt.Errorf("cannot read %s from the bus: %w", link.BuildLog(id), err)
|
|
}
|
|
defer func() { _ = sub.Unsubscribe() }()
|
|
|
|
printed := 0
|
|
for {
|
|
batch, err := sub.Fetch(200, nats.MaxWait(2*time.Second))
|
|
if err != nil && !errors.Is(err, nats.ErrTimeout) && !errors.Is(err, context.DeadlineExceeded) {
|
|
return fmt.Errorf("reading a build's log: %w", err)
|
|
}
|
|
for _, msg := range batch {
|
|
var line link.BuildLine
|
|
if err := json.Unmarshal(msg.Data, &line); err != nil {
|
|
fmt.Printf(" ? %s\n", string(msg.Data))
|
|
continue
|
|
}
|
|
at := line.At
|
|
if t, err := time.Parse(time.RFC3339Nano, line.At); err == nil {
|
|
at = t.Local().Format("15:04:05")
|
|
}
|
|
fmt.Printf("%s %4d [%s] %s\n", at, line.Seq, line.Step, line.Message)
|
|
printed++
|
|
}
|
|
if len(batch) < 200 {
|
|
break
|
|
}
|
|
}
|
|
if printed == 0 {
|
|
fmt.Printf("nothing on the bus for build %s: no build by that id in the last week, or a build "+
|
|
"machine older than this that said nothing while building\n", id)
|
|
}
|
|
return nil
|
|
}
|