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.
193 lines
7.8 KiB
Go
193 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// The change plan (novox/hq ADR 0238): **one commit, one plan** — what a change does to the mesh, computed
|
|
// from its diffset (a repository, the branch it merges into, the commit at hand) by the planner, never by
|
|
// a mapping of its own. reachOfMerge answers what moves and what follows it; this adds where each module
|
|
// goes — the deploy plan, machine by machine in the build plan's order — and what is not an ordinary
|
|
// send. A pull request's check posts it with its verdict; the release a merge makes follows the same
|
|
// planner, so the two can be compared.
|
|
|
|
// policyOf is a module's upgrade policy, as the controller holds it; false when it cannot be read.
|
|
type policyOf func(module string) (inventory.Upgrade, bool)
|
|
|
|
// changePlanOf is the change plan of a reach: pure, so it is tested without a store.
|
|
func changePlanOf(repository, base, head string, r mergeReach, entries []inventory.Entry, policy policyOf) link.ChangePlan {
|
|
p := link.ChangePlan{Repository: repository, Base: base, Head: head, Moved: r.Moved(), Dependents: r.Dependents(),
|
|
New: r.Added, Unread: r.Unread, Tiers: r.Plan.Tiers}
|
|
byName := map[string]inventory.Entry{}
|
|
for _, e := range entries {
|
|
byName[e.Manifest.Module] = e
|
|
}
|
|
// The machine holding the bus, whose declaration carries every module's grants (novox/hq issue 490).
|
|
holder := ""
|
|
for _, e := range entries {
|
|
if e.Manifest.BusUsers != "" && e.Manifest.ClaimsSeat(catalogue.BrokerSeat) && len(e.On) > 0 {
|
|
holder = e.On[0]
|
|
}
|
|
}
|
|
var granting []string
|
|
machines := map[string]*link.MachinePlan{}
|
|
machine := func(name string) *link.MachinePlan {
|
|
if machines[name] == nil {
|
|
machines[name] = &link.MachinePlan{Machine: name}
|
|
}
|
|
return machines[name]
|
|
}
|
|
for _, tier := range r.Plan.Tiers {
|
|
for _, name := range tier {
|
|
e, held := byName[name]
|
|
if !held {
|
|
continue
|
|
}
|
|
u, known := policy(name)
|
|
waits := known && !u.RollOut
|
|
for _, on := range e.On {
|
|
if waits {
|
|
machine(on).Waits = append(machine(on).Waits, name)
|
|
} else {
|
|
machine(on).Receives = append(machine(on).Receives, name)
|
|
}
|
|
}
|
|
if !waits && holder != "" && !(len(e.On) == 1 && e.On[0] == holder) && len(e.On) > 0 &&
|
|
!slices.Contains(granting, name) {
|
|
granting = append(granting, name)
|
|
}
|
|
switch {
|
|
case (name == "nats" || catalogue.ProvidesBus(e.Manifest)) && len(e.On) > 0:
|
|
// Said before the merge (novox/hq issue 336): a new bus build holds every send to the bus's machine
|
|
// until a person takes the step, so merging it is a promise to take it.
|
|
p.Steps = append(p.Steps, fmt.Sprintf("%s: %s is never sent by an ordinary send, and until %s runs, "+
|
|
"nothing else is sent to %s either — unless the new build turns out the same as the one running there "+
|
|
"(issue 280)", busUpgradeNeeded, name, busUpgradeVerb, strings.Join(e.On, ", ")))
|
|
case name == "nats" || catalogue.ProvidesBus(e.Manifest):
|
|
p.Steps = append(p.Steps, "a planned bus step: the bus is upgraded by `bus upgrade`, never by an ordinary send")
|
|
case waits && len(e.On) > 0 && (u.From == catalogue.FromModule || u.From == catalogue.FromData):
|
|
// Judged per machine by the walk, against the build each was last sent (novox/hq ADR 0377).
|
|
p.Steps = append(p.Steps, fmt.Sprintf("%s waits for a person: its policy records (%s) — unless on a machine "+
|
|
"its move recreates none of its containers and moves none of its data, which the walk judges and then sends",
|
|
name, orNone(u.From)))
|
|
case waits && len(e.On) > 0:
|
|
p.Steps = append(p.Steps, fmt.Sprintf("%s waits for a person: its policy records (%s)", name,
|
|
orNone(u.From)))
|
|
}
|
|
if len(e.Manifest.Provides) > 0 && len(e.On) > 0 {
|
|
var offers []string
|
|
for _, o := range e.Manifest.Provides {
|
|
offers = append(offers, o.Name)
|
|
}
|
|
p.Steps = append(p.Steps, fmt.Sprintf("%s provides %s: its consumers are sent again after it",
|
|
name, strings.Join(offers, ", ")))
|
|
}
|
|
if e.Manifest.Data != nil && len(e.On) > 0 {
|
|
p.Steps = append(p.Steps, fmt.Sprintf("%s keeps data (ADR 0233): what it holds is backed up before it moves", name))
|
|
}
|
|
}
|
|
}
|
|
if len(granting) > 0 {
|
|
// Said before the merge (novox/hq issue 490): what the walk does when the change alters the bus's
|
|
// user list. Whether it does is known only once the builds are registered, so it is said as a rule.
|
|
p.Steps = append(p.Steps, fmt.Sprintf("%s: if this changes what the bus's user list says (a new tool, "+
|
|
"subject or user), %s is sent that list alone, every build there kept, before %s is judged on its first "+
|
|
"machine", grantsStepWord, holder, strings.Join(granting, ", ")))
|
|
}
|
|
var names []string
|
|
for name := range machines {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
p.Machines = append(p.Machines, *machines[name])
|
|
}
|
|
p.Summary = summaryOf(p)
|
|
p.Class = changeClassOf(p, entries)
|
|
return p
|
|
}
|
|
|
|
// classNone is the class of a change that moves no module: nothing for a walk to move (novox/hq ADR 0313 §2).
|
|
const classNone = "none"
|
|
|
|
// changeClassOf is a change plan's class (novox/hq ADR 0313 §2): that of the slowest-budgeted module it moves,
|
|
// itself or as a dependent — core when one is on the controller's own path or holds the mesh's resolver, leaf
|
|
// otherwise — and none when it moves nothing. A module it adds a directory for counts by its own class: the
|
|
// catalogue names a module's directory by the module, so the directory's last element is its name. The graph holds
|
|
// no manifest of it yet, so a new module is core by its name alone (on the controller's own path), never by a
|
|
// resolver claim its manifest makes.
|
|
func changeClassOf(p link.ChangePlan, entries []inventory.Entry) string {
|
|
if len(p.Moved) == 0 && len(p.Dependents) == 0 && len(p.New) == 0 {
|
|
return classNone
|
|
}
|
|
modules := append(append([]string{}, p.Moved...), p.Dependents...)
|
|
for _, dir := range p.New {
|
|
if name := path.Base(strings.Trim(dir, "/")); name != "." && name != "/" && name != "" {
|
|
modules = append(modules, name)
|
|
}
|
|
}
|
|
return classOfModules(modules, entries)
|
|
}
|
|
|
|
// grantsStepWord names the grants step (novox/hq issue 490): the bus's user list sent alone to the machine
|
|
// holding the bus ahead of a walk's gate. Not the bus step, which replaces the bus itself.
|
|
const grantsStepWord = "grants step"
|
|
|
|
// busUpgradeNeeded is how a change plan says that merging it holds the bus's machine for a person's step.
|
|
const busUpgradeNeeded = "merging this needs a person's bus upgrade"
|
|
|
|
// summaryOf is a change plan in one line: what it builds, where it goes, and whether the bus moves.
|
|
func summaryOf(p link.ChangePlan) string {
|
|
if len(p.Moved) == 0 && len(p.New) == 0 {
|
|
return "builds nothing: the change touches no module of the mesh's graph"
|
|
}
|
|
var parts []string
|
|
what := strings.Join(p.Moved, ", ")
|
|
if len(p.Dependents) > 0 {
|
|
what += fmt.Sprintf(" (+%d dependent(s))", len(p.Dependents))
|
|
}
|
|
if len(p.New) > 0 {
|
|
if what != "" {
|
|
what += ", "
|
|
}
|
|
what += "new: " + strings.Join(p.New, ", ")
|
|
}
|
|
var to []string
|
|
for _, m := range p.Machines {
|
|
if len(m.Receives) > 0 {
|
|
to = append(to, m.Machine)
|
|
}
|
|
}
|
|
if len(to) > 0 {
|
|
parts = append(parts, "builds "+what+" → "+strings.Join(to, ", "))
|
|
} else {
|
|
parts = append(parts, "builds "+what+", sent nowhere")
|
|
}
|
|
bus := "no bus step"
|
|
for _, s := range p.Steps {
|
|
switch {
|
|
case strings.HasPrefix(s, busUpgradeNeeded):
|
|
bus = busUpgradeNeeded
|
|
case strings.HasPrefix(s, "a planned bus step") && bus == "no bus step":
|
|
bus = "a bus step"
|
|
}
|
|
}
|
|
parts = append(parts, bus)
|
|
waiting := 0
|
|
for _, m := range p.Machines {
|
|
waiting += len(m.Waits)
|
|
}
|
|
if waiting > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d wait(s) for a person", waiting))
|
|
}
|
|
return strings.Join(parts, "; ")
|
|
}
|