mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792753090890815254 (novox/mesh-controller at c834e4f8 into ma…
mesh/check pass 149f89e8f9bf on 0173d9c70e59 | unit 1759 run, 0 kept, 4 skipped | integration 3037 run, 0 kept, 16 skipped | over bound: integration (…
A build heard and recorded and not registered kept its refusal only when a plan was open or the plans were held. A tier's first failure fails its plan, so later refusals were lost; a Retry then settled those builds built from their records, sent the modules' old versions and reported the plan done. A build without a readable manifest never reached its plan at all. Keep the refusal in the store and the process whatever the plans' state, and fail the asking plan by the build's id when the module is not known. Issue 1713.
511 lines
25 KiB
Go
511 lines
25 KiB
Go
// Command mesh-controller is the control plane: everything that needs to know about more than one
|
|
// node (novox/hq ADR 0006).
|
|
//
|
|
// It runs as one process holding several contexts, each owning its own store. Today it holds one,
|
|
// `inventory`, and does one thing with it — brings its schema up to date, which is step 3 of the
|
|
// bootstrap in novox/hq 07-the-foundation and the step the first node cannot get past without.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/identity"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/licences"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-controller/internal/store"
|
|
)
|
|
|
|
// version is stamped at link time. Unset in a development build, and it says so rather than
|
|
// claiming a number.
|
|
var version = "development build"
|
|
|
|
// held is a context this process was granted, and the schema it carries.
|
|
//
|
|
// novox/hq ADR 0006 names seven. One is built. The list is short because the others do not exist
|
|
// yet, not because they are optional.
|
|
var held = []struct {
|
|
name string
|
|
migrations func() ([]store.Migration, error)
|
|
}{
|
|
{inventory.Name, inventory.Migrations},
|
|
{identity.Name, identity.Migrations},
|
|
{licences.Name, licences.Migrations},
|
|
}
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "mesh-controller: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run() error {
|
|
args := os.Args[1:]
|
|
if len(args) == 0 {
|
|
usage()
|
|
return fmt.Errorf("no command given")
|
|
}
|
|
|
|
// The bus's users are composed from what each module announced it keeps (novox/hq ADR 0495, issue 1790).
|
|
inventory.Announcements = readAnnouncements
|
|
// Every connection this process dials says what it is (novox/hq issue 327).
|
|
broker.ConnectionName = connectionName(args[0], os.Getenv(verbVar))
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
// Whatever this process holds of the controller's lease is given back as it ends (novox/hq to-be
|
|
// 45 §6), so the next controller takes it at once rather than after its age.
|
|
defer theLease.release()
|
|
|
|
// Every walk a verb keeps is said on the bus, whichever verb kept it (novox/hq ADR 0308 decision 1): the save
|
|
// that starts or ends a walk keeps its mark, and no later save says it again. The serving controller says its
|
|
// own saves.
|
|
if args[0] != "serve" {
|
|
said, done := sayingWhatAVerbKept(ctx)
|
|
inventory.PlanSaved = said
|
|
defer done()
|
|
}
|
|
|
|
switch args[0] {
|
|
case "switch":
|
|
return switchCommand(ctx, args[1:])
|
|
case "register":
|
|
return registerCommand(ctx, args[1:])
|
|
case "build", "rebuild":
|
|
// **The hand build goes with the asks** (novox/hq ADR 0501 §5): refused by name, naming the replacement.
|
|
return removedByName(args[0])
|
|
case "builder":
|
|
return builderCommand(ctx, args[1:])
|
|
case "board":
|
|
return boardCommand(ctx, args[1:])
|
|
case "api":
|
|
return apiCommand(ctx, args[1:])
|
|
case "licence":
|
|
return licenceCommand(ctx, args[1:])
|
|
case "rotate":
|
|
return rotateCommand(ctx, args[1:])
|
|
case "ask":
|
|
return askCommand(ctx, args[1:])
|
|
case "rehearse":
|
|
return rehearseCommand(ctx, args[1:])
|
|
case "builds":
|
|
return buildsCommand(ctx, args[1:])
|
|
// The build queue, controlled by hand (novox/hq ADR 0219).
|
|
case "queue":
|
|
return queueCommand(ctx, args[1:])
|
|
case "cancel":
|
|
return cancelCommand(ctx, args[1:])
|
|
case "clear":
|
|
return clearCommand(ctx, args[1:])
|
|
case "replay":
|
|
return replayCommand(ctx, args[1:])
|
|
case "kill":
|
|
return killCommand(ctx, args[1:])
|
|
case "forget-checks":
|
|
return removedByName("forget-checks")
|
|
case "try":
|
|
return tryCommand(ctx, args[1:])
|
|
case "pause", "resume":
|
|
return pauseCommand(ctx, args[0], args[1:])
|
|
case "collection":
|
|
return collectionCommand(ctx, args[1:])
|
|
// What the records say the registries may keep, asked on demand (novox/hq ADR 0251).
|
|
case "artifacts":
|
|
return artifactsCommand(ctx, args[1:])
|
|
case "collect":
|
|
return collectCommand(ctx, args[1:])
|
|
case "images":
|
|
return imagesCommand(ctx, args[1:])
|
|
case "mirrors":
|
|
return mirrorsCommand(ctx, args[1:])
|
|
case "move-placement":
|
|
return movePlacementCommand(ctx, args[1:])
|
|
case "move-config":
|
|
return moveConfigCommand(ctx, args[1:])
|
|
case "plans":
|
|
return plansCommand(ctx, args[1:])
|
|
case "delivery":
|
|
// The verbs the delivery's owner asks with (novox/hq ADR 0239).
|
|
return deliveryCommand(ctx, args[1:])
|
|
case "pin":
|
|
return pinCommand(ctx, args[1:], true)
|
|
case "unpin":
|
|
return pinCommand(ctx, args[1:], false)
|
|
// `prepare` is how the mesh asks any module to bring its state to the shape this version needs
|
|
// (novox/hq ADR 0135), and the control plane answers it the same way as everything else — its
|
|
// own schema is not a special case. `migrate` remains the word a person types.
|
|
case "prepare", "migrate":
|
|
return migrate(ctx)
|
|
case "node":
|
|
return nodeCommand(ctx, args[1:])
|
|
case "token":
|
|
return tokenCommand(ctx, args[1:])
|
|
case "identity":
|
|
return identityCommand(ctx, args[1:])
|
|
case "broker":
|
|
return brokerCommand(ctx, args[1:])
|
|
case "serve":
|
|
return serve(ctx)
|
|
// The facts snapshot a merge check is fed (novox/hq to-be 45 §9).
|
|
case "facts":
|
|
return factsCommand(ctx, args[1:])
|
|
// The merge gate: every machine of the snapshot composed with a change (novox/hq to-be 45 §9).
|
|
case "merge-gate":
|
|
return mergeGateCommand(ctx, args[1:])
|
|
// A pull request's merge check run here exactly as the build seat runs it (novox/hq issue 286).
|
|
case "check-here":
|
|
return checkHereCommand(ctx, args[1:])
|
|
case "upgrade":
|
|
return upgradeCommand(ctx, args[1:])
|
|
// The bus as a planned step (novox/hq to-be 45 §8, ADR 0236).
|
|
case "bus":
|
|
return busCommand(ctx, args[1:])
|
|
case "declare":
|
|
return declare(ctx, args[1:])
|
|
case "overlay":
|
|
return overlayCommand(ctx, args[1:])
|
|
case "module":
|
|
return moduleCommand(ctx, args[1:])
|
|
case "assign", "unassign":
|
|
return assignCommand(ctx, args[0], args[1:])
|
|
case "take":
|
|
return takeCommand(ctx, args[1:])
|
|
case "converge":
|
|
return convergeCommand(ctx, args[1:])
|
|
case "adopt":
|
|
return adoptCommand(ctx, args[1:])
|
|
case "settings":
|
|
return settingsCommand(ctx, args[1:])
|
|
case "secret":
|
|
return secretCommand(ctx, args[1:])
|
|
case "operator":
|
|
return operatorCommand(ctx, args[1:])
|
|
case "plan":
|
|
return planCommand(ctx, args[1:])
|
|
case "push":
|
|
return pushCommand(ctx, args[1:])
|
|
case "rollout":
|
|
return rolloutCommand(ctx, args[1:])
|
|
case "seats":
|
|
return seatsCommand(ctx, args[1:])
|
|
case "seat":
|
|
return seatCommand(ctx, args[1:])
|
|
case "status":
|
|
return statusCommand(ctx, args[1:])
|
|
// Acts done by hand, and why (novox/hq to-be 45 §7).
|
|
case "hand-act":
|
|
return handActCommand(ctx, args[1:])
|
|
case "hand-acts":
|
|
return handActCommand(ctx, append([]string{"list"}, args[1:]...))
|
|
// What the mesh's bounds will be set from (novox/hq to-be 45 Phase 0).
|
|
case "durations":
|
|
return durationsCommand(ctx, args[1:])
|
|
// What is wrong, and the self-check (novox/hq to-be 45 §2, §4).
|
|
case "conditions":
|
|
return conditionsCommand(ctx, args[1:])
|
|
case "doctor":
|
|
return doctorCommand(ctx, args[1:])
|
|
// What the healers did, and their brake (novox/hq to-be 45 §7).
|
|
case "healers":
|
|
return healersCommand(ctx, args[1:])
|
|
// A consumer the mesh stopped asking for: retired, waiting for a person, deleted only by one
|
|
// (novox/hq ADR 0230).
|
|
case "retire":
|
|
return retireCommand(ctx, args[1:])
|
|
case "cleanup":
|
|
return cleanupCommand(ctx, args[1:])
|
|
// The data every machine declares, as the self-check last found it (novox/hq ADR 0233).
|
|
case "data":
|
|
return dataCommand(ctx, args[1:])
|
|
case "version":
|
|
fmt.Println(version)
|
|
return nil
|
|
case "help", "-h", "--help":
|
|
usage()
|
|
return nil
|
|
default:
|
|
usage()
|
|
return fmt.Errorf("%q is not a command", args[0])
|
|
}
|
|
}
|
|
|
|
// usageText is the terminal's help: every command it has, and none it refuses as removed (removedByName).
|
|
var usageText = `mesh-controller — the control plane
|
|
|
|
On a node the operator types these as 'mesh-cli <command>' (in zsh, 'nox <command>'): asked
|
|
through the node's engine, and run as the controller's terminal only on the control-node
|
|
(novox/hq ADR 0272).
|
|
|
|
migrate bring each context's schema up to date
|
|
prepare the same, asked the way the mesh asks any module (ADR 0135)
|
|
node add <name> [--adopted] create a node record; --adopted: the machine is in use
|
|
node list the nodes this mesh knows about
|
|
node show <name> what one machine reported it can do, and why
|
|
node public-domain <name> the domain it composes its routed names under
|
|
node public-domain <name> <d> ...set it to d
|
|
node public-domain <name> --clear ...it faces the outside no longer
|
|
node networks <name> the networks it routes for what it hosts
|
|
node networks <name> <cidr>... ...set them; its filter forwards these too
|
|
node networks <name> --clear ...only the container runtime's own
|
|
token issue --node <name> a one-time right to join, for an existing record
|
|
token issue --new <name> create the record and issue for it
|
|
token issue ... --adopted ...for a machine in use, which joins adopted
|
|
identity show this control plane's signing key
|
|
broker show where the broker is, and what to expect there
|
|
serve consume what nodes say, and answer
|
|
declare <node> <file> send a node a signed declaration
|
|
overlay place <node> [flags] say where a node is and how it is reached
|
|
overlay show the private network, as the mesh computes it
|
|
module add <file> register a module from its manifest
|
|
module check <file|dir>... judge manifests where they are written, with no mesh (exit 1 on any problem)
|
|
module list what modules this mesh knows about
|
|
module moved <name> <commit> the source has a newer commit than the mesh built
|
|
module forget <name> remove one, unless a node runs it or the mesh holds things for it
|
|
module forget <name> --and-what-it-holds ...and discard its settings, secrets and ports too
|
|
module issue <name> --node <m> a broker account for a module, scoped to its emits and consumes
|
|
upgrade <name> what happens when this module's current version moves
|
|
upgrade <name> roll-out [--together] ...send it to the machines running it
|
|
upgrade <name> record ...record that they are behind, and send nothing
|
|
status [--json] what is wrong, what is quiet, and what is out of date
|
|
retire [--json] every provider waiting for a person to approve a retirement (ADR 0230)
|
|
retire approve <node> <module> --why <text> retire what it waits with: access off, data kept
|
|
retire reject <node> <module> --why <text> keep them active; a warning stays open
|
|
cleanup [list] [--json] every retired consumer per provider: age, size, why
|
|
cleanup delete <node> <module> <consumer> --why <text> the provider deletes that one retired consumer
|
|
cleanup delete --older-than <days> --why <text> [--confirm] list those older; delete only with --confirm
|
|
artifacts [--json] [--repository <r>] [--collected] every artifact the mesh made: kept and why, eligible, collected (ADR 0251)
|
|
collect [--json] [--most <n>] what the store's sweep would let go of, now; nothing is done
|
|
collect --confirm --why <text> [--most <n>] run the sweep now: hold every kept archive, let go of the eligible
|
|
move-placement [--json] which settings layers still hold placement keys (endpoints, expose, reach, ports); nothing is moved
|
|
move-placement --confirm --why <text> move them beside the assignment, once, after the delivery (issue 736)
|
|
move-config [--json] every config value the settings layers hold, and where in mesh-config it goes; nothing is set
|
|
move-config --confirm --why <text> set them in mesh-config, read them back, and read config from there (issue 664)
|
|
images <node> [--json] every container image the machine's declaration names, now and as last sent
|
|
seats [--json] every seat this mesh defines, what it delivers, and who holds it
|
|
seat rename <from> <to> rename a seat; its former name still resolves (ADR 0122)
|
|
seat <name> --to <node>/<module> hand a seat to that assignment as one act; never empty in between (ADR 0131)
|
|
seat <name> --add <node>/<module> add a holder beside the others, for a replicated seat (ADR 0223)
|
|
board [--listen ADDR] the same three questions, as a page that holds nothing
|
|
api --issuer URL [--listen A] assign and unassign over http, for a surface that is not here
|
|
assign <node> <module>... put modules on a node, judged together (ADR 0207)
|
|
unassign <node> <module>... take them off
|
|
take <node> <module> preview a module's cutover on an adopted node: what runs beside
|
|
what it declares; --yes <digest> cuts it over as previewed
|
|
converge <node> [--yes <digest>] [--filter nftables] preview, then make, an adopted node converged
|
|
adopt <node> return a converged node to adopted; what was taken stays taken
|
|
settings set <module> <file> what a module's config should say, for the whole mesh
|
|
settings set <module> <file> --node <n> ...or for one machine
|
|
settings clear <module> [--node <n>] take a layer away
|
|
secret accept <node> <module> <name> carry a value the mesh did not make and cannot invent
|
|
secret accept ... --from <file> ...read it from a file rather than being asked
|
|
secret recover <node> <module> <name> --key <operator-key> [--out <file>] [--from-export <file>]
|
|
break-glass: open the operator-sealed copy, to a 0600 file
|
|
secret export [--out <file>] every operator-sealed copy, ciphertext — keep it with the key
|
|
operator key make [--out <file>] make the operator's sealing key, off the mesh; private half to the file only
|
|
operator key set <public> [--replace] tell the mesh which operator key to seal to
|
|
operator key show the operator key, and what it can recover
|
|
register <module> <owner>/<repository> <path> register a module from a repository the catalogue does not
|
|
build, from the builder's done key of its fingerprint (ADR 0501 §4)
|
|
switch [--json] the switch's list: each module, the build it runs, and what the builder holds
|
|
builds [<module>] what has been built lately, and what came of it
|
|
queue [--json] every ask in the build queue: waiting, in flight (where, how long), dead
|
|
cancel <id> drop a waiting or dead ask; recorded failed, cancelled by hand
|
|
clear [--dead] cancel every waiting ask (and the dead ones); never one in flight
|
|
replay <build-id> that build's commit again, a dry run: what a build of it makes
|
|
kill <id> end a build where it runs; recorded failed, killed by hand
|
|
pause [<node>] / resume [<node>] the build seat's holder there, or every holder, takes nothing new / again
|
|
plans retry <id> ask a failed plan's failed builds again, and carry the plan on
|
|
plans stop|close <id> --why <text> end a plan by hand; recorded in the hand-act log
|
|
hand-act record <what> --why <text> --cause <word> [--condition <key>]
|
|
record an act done by hand outside the mesh (to-be 45 §7)
|
|
hand-acts [--days N] [--json] what was done by hand lately, why, and which causes repeat
|
|
conditions [--scope S] [--severity S] [--machine M] [--json]
|
|
what is wrong now: every open condition, urgent first (to-be 45 §2)
|
|
conditions show <key> one condition whole, with its evidence
|
|
conditions silence <key> --for <d> --why <text> send no message for it a while; a hand act
|
|
conditions history [--days N] [--key K] every raising, change and clearing lately
|
|
doctor [run|probes|signals] [--json]
|
|
the self-check: the last verdict, a run now, the probes, the signals' ages
|
|
healers [--days N] [--json] the healers, what they did lately, and their brake (to-be 45 §7)
|
|
durations [--kind K] [--days N] [--json]
|
|
apply, heartbeat, plan-tier, build and walk-phase durations
|
|
collection [--json] kept archives held/unheld by a manifest, and what the sweep may let go
|
|
builder issue <name> a broker account for a build machine, scoped to build work,
|
|
delivered as the builder module's broker secret (module add it first)
|
|
licence add|list|use|key model access, under the name a person calls it
|
|
licence manager <name> <node> the node that holds a refreshable licence's refresh token
|
|
licence refresh <name> mint a new access token and seal it to every holder
|
|
rotate <provision> [--consumer <n>] [--module <m>] [--why <text>] a new credential for every holder, both ends at once (--why where the vault holds it)
|
|
ask <module> <tool> [json] call one of a module's tools over the broker, and print its answer
|
|
pin <node> <provision> <from-node> <module>
|
|
which provider this one gets a provision from: the module, and its node
|
|
unpin <node> <provision> put that question back
|
|
plan <node> [--files|--json] what that node would run, and why
|
|
push [<node>] [--behind] [--why <text>] send a node everything it should be, or only those
|
|
that need it; --why records it in the hand-act log
|
|
version what this binary is
|
|
|
|
Each context reaches its own store through its own credential (novox/hq ADR 0008), named
|
|
` + store.Variable("<context>") + ` — or ` + store.FileVariable("<context>") + `, naming a file that holds
|
|
the same thing and keeps the password out of the environment. This process holds:
|
|
|
|
`
|
|
|
|
func usage() {
|
|
fmt.Fprint(os.Stderr, usageText)
|
|
for _, c := range held {
|
|
fmt.Fprintf(os.Stderr, " %-12s database %-12s from %s\n",
|
|
c.name, store.Database(c.name), store.Variable(c.name))
|
|
}
|
|
fmt.Fprintln(os.Stderr)
|
|
}
|
|
|
|
// parseAround reads flags that may sit before, after or between positional arguments.
|
|
//
|
|
// The standard library stops at the first non-flag argument, so `module add thing.json --source x`
|
|
// parses no flags at all and silently ignores every one of them. The host learned this the same
|
|
// way and says so in its own parser: a flag that is quietly dropped is the fault this project
|
|
// keeps naming, and it looks exactly like success.
|
|
func parseAround(set *flag.FlagSet, args []string) ([]string, error) {
|
|
var positionals []string
|
|
rest := args
|
|
for {
|
|
if err := set.Parse(rest); err != nil {
|
|
return nil, err
|
|
}
|
|
rest = set.Args()
|
|
if len(rest) == 0 {
|
|
return positionals, nil
|
|
}
|
|
positionals = append(positionals, rest[0])
|
|
rest = rest[1:]
|
|
}
|
|
}
|
|
|
|
// Built is the daemon hearing a build's outcome on the bus — its own asking, an announcement's, or
|
|
// a tool's that did not wait (novox/hq issue 176) — and taking it in: recorded, and the module
|
|
// registered, the same as the waiting command does. Said either way, so the daemon's log tells what
|
|
// became of a build nobody was watching.
|
|
func (b builds) Built(ctx context.Context, result link.BuildResult) error {
|
|
// **A merge check builds nothing** (novox/hq to-be 45 §9): its verdict is said, and nothing of it is
|
|
// recorded or registered.
|
|
if result.Check != nil || result.Checked != nil {
|
|
if isTry(result) {
|
|
// A branch tried before its pull request (novox/hq issue 686): its asker reads the outcome from the
|
|
// build's log. Never said as `checked`, so the forge's holder sets no status.
|
|
fmt.Printf("%s: a try of %s at %.8s, read with `try --id`: said to nobody, nothing taken in\n", result.ID,
|
|
result.Repository, result.Ref)
|
|
return nil
|
|
}
|
|
checked(ctx, result)
|
|
return nil
|
|
}
|
|
// **A dry run is looked at, never taken in** (novox/hq issue 240). On 2026-10-04 a dry run of an
|
|
// unmerged branch was heard here like any build, registered, and its definition reached a machine
|
|
// before anyone had reviewed it.
|
|
if result.DryRun {
|
|
fmt.Printf("%s: a dry run of %s on %s, not taken in\n", result.ID, result.Repository, result.Ref)
|
|
return nil
|
|
}
|
|
manifest, _, err := takeIn(ctx, b.inv, result)
|
|
// The assignments pending on this build, made or ended (novox/hq issue 325), said in the daemon's log
|
|
// after what became of the build.
|
|
if b.open != nil {
|
|
defer func() {
|
|
for _, line := range settlePendingOnBuild(ctx, b.open, result, manifest.Module, err) {
|
|
fmt.Printf("%s: %s\n", result.ID, line)
|
|
}
|
|
}()
|
|
}
|
|
// When it was asked, so a plan takes as its outcome only a build asked for it or after it
|
|
// (novox/hq 04-ISSUES/219). Zero when the id does not say.
|
|
asked, _ := link.BuildAskedAt(result.ID)
|
|
// And how long it took, asked to heard, which a build's bound will be set from (novox/hq to-be 45
|
|
// Phase 0). Said if lost; never a reason not to take the build in.
|
|
recordBuildDuration(ctx, b.inv, result, asked)
|
|
switch {
|
|
case err != nil && result.Failed != "":
|
|
fmt.Printf("%s: %v\n", result.ID, err)
|
|
if result.Module != "" {
|
|
planBuilt(ctx, b.open, result.Module, result.Commit, result.Failed, asked, result.ID)
|
|
} else {
|
|
planFailedBuild(ctx, b.open, result)
|
|
}
|
|
return nil
|
|
case errors.Is(err, inventory.ErrSuperseded):
|
|
// Not a failure: the module is already at what a later request built. A plan that asked
|
|
// before that later request is answered by it; one that asked after it ignores this.
|
|
fmt.Printf("%s: %v\n", result.ID, err)
|
|
planBuilt(ctx, b.open, manifest.Module, result.Commit, "", asked, result.ID)
|
|
return nil
|
|
case err != nil:
|
|
fmt.Printf("%s: heard and recorded, and not registered: %v\n", result.ID, err)
|
|
takeRefused(ctx, b.inv, result, manifest.Module, err.Error(), refusedOutcome{
|
|
built: func(module, why string) {
|
|
planBuilt(ctx, b.open, module, result.Commit, why, asked, result.ID)
|
|
},
|
|
failedBuild: func(failed link.BuildResult) { planFailedBuild(ctx, b.open, failed) },
|
|
})
|
|
return nil
|
|
}
|
|
fmt.Printf("%s: %s %s registered, built on %s from %s\n",
|
|
result.ID, manifest.Module, manifest.Version, result.On, short(result.Commit))
|
|
// What the walk's build did, kept on the walk whose ask it answers (novox/hq ADR 0350 §4); a build seat from
|
|
// before it says nothing, and that is what is kept.
|
|
made := result.MadeWords()
|
|
if result.Reused != "" || result.BuiltWhy != "" {
|
|
fmt.Printf("%s: %s %s\n", result.ID, manifest.Module, made)
|
|
}
|
|
saysWhenThePolicyActs(ctx, b.inv, manifest.Module)
|
|
planBuiltSaying(ctx, b.open, manifest.Module, result.Commit, "", asked, result.ID, made)
|
|
// A module registered may be one a machine is now behind: `status` is composed again.
|
|
statusFrom.nudge()
|
|
return nil
|
|
}
|
|
|
|
// verbVar carries the seat verb a command runs for, from the serving controller to the process it starts.
|
|
const verbVar = "MESH_VERB"
|
|
|
|
// connectionName is what this process's connections say they are in the bus's list (novox/hq issue 327):
|
|
// the serving controller, a verb's own process and which verb, or a command run at a shell and which.
|
|
func connectionName(command, verb string) string {
|
|
switch {
|
|
case command == "serve":
|
|
return "mesh-controller serving"
|
|
case verb != "":
|
|
return "mesh-controller verb " + verb
|
|
}
|
|
return "mesh-controller command " + command
|
|
}
|
|
|
|
// removedByName refuses a command the controller no longer has, naming what replaced it (novox/hq ADR 0501 §4, §5;
|
|
// ADR 0429 §18; ADR 0503 §3; issue 1851, A11, and issue 1318).
|
|
func removedByName(command string) error {
|
|
replacement := map[string]string{
|
|
"build": "the controller asks no build (ADR 0501 §5): the builder builds every fingerprint the indexer writes, a " +
|
|
"module is registered from its done key, and a branch is tried with `try`; a module from a repository the " +
|
|
"catalogue does not build is registered at the controller's terminal with `register`",
|
|
"rebuild": "the controller asks no build (ADR 0501 §5): a failed key is built again by the builder as a new " +
|
|
"revision (ADR 0429 §18), and a walk's Retry waits for it",
|
|
"replay --register": "a build is registered only from the builder's done key of a trunk fingerprint (ADR 0501 " +
|
|
"§1, §2); `replay` without --register still shows what a build of a recorded commit makes",
|
|
"forget-checks": "build-agent makes no check build from R4e (ADR 0503 §3), so there is nothing to let go of",
|
|
"delivery-plan": "a delivery's plan is read with `delivery-order` and the checker's verdict (ADR 0503 §3)",
|
|
}[command]
|
|
return &removedCommand{msg: fmt.Sprintf("`%s` is removed: %s", command, replacement)}
|
|
}
|
|
|
|
// removedCommand is a command or verb the controller no longer has: a refusal that is the verb's whole answer, not a
|
|
// verb of a newer build this one cannot run.
|
|
type removedCommand struct{ msg string }
|
|
|
|
func (r *removedCommand) Error() string { return r.msg }
|