mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
130 lines
4.9 KiB
Go
130 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// A recorded build reaches a machine only by a person's push (novox/hq issue 295, ADR 0242).
|
|
//
|
|
// **A send carries the machine's whole declaration** (ADR 0221), composed from the build the mesh holds
|
|
// of every module on it. A module whose upgrade policy records — postgres, mongodb, keycloak, the
|
|
// network path — has its new build registered at its merge and sent nowhere, "until a person pushes".
|
|
// But every other send to its machine composed it too: on 2026-10-07 a catalogue merge adopting the
|
|
// images' health checks rebuilt postgres and mongodb with the rest, and the plan's gated send to the
|
|
// control node for mail — and to the anchor for the spreadsheet app — carried both, recreating the
|
|
// providers every consumer on those machines drops with. No gate judged them (the gate judges only what
|
|
// rolls out), and nobody had pushed.
|
|
//
|
|
// So **every send but a person's push composes a recorded module at the build its machine was last
|
|
// sent**: the manifest of that build, from the build records, in place of the one the mesh holds. The
|
|
// machine runs what it ran; the send records that it still carries that build; `status` keeps saying
|
|
// the machine is behind, and `push <node>` — a person's word — sends the new one. The bus step is a
|
|
// person's too, and carries the bus; any other recorded module waiting on the bus's machine stays.
|
|
//
|
|
// A recorded module the machine was never sent (a new assignment) is composed as the mesh holds it —
|
|
// there is nothing running to keep. One whose kept build is no longer in the records refuses the send,
|
|
// said: composing the new build would be the very move this exists to stop.
|
|
|
|
type keepRecordedKey struct{}
|
|
|
|
// sendKeeps is the context a send that is not a person's push composes under: every recorded module
|
|
// kept at the build its machine runs — except, on the bus step, the bus.
|
|
func sendKeeps(ctx context.Context, inv *inventory.Inventory) (context.Context, error) {
|
|
if !busStepSending(ctx) {
|
|
return keepingRecorded(ctx), nil
|
|
}
|
|
bus, err := pendingBus(ctx, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return keepingRecorded(ctx, bus.module), nil
|
|
}
|
|
|
|
// keepingRecorded is a context whose sends compose every recorded module at the build its machine runs,
|
|
// except the modules named (the bus, on the bus step).
|
|
func keepingRecorded(ctx context.Context, except ...string) context.Context {
|
|
skip := map[string]bool{}
|
|
for _, m := range except {
|
|
skip[m] = true
|
|
}
|
|
return context.WithValue(ctx, keepRecordedKey{}, skip)
|
|
}
|
|
|
|
// keptExcept is whether this context keeps recorded modules, and the modules it lets move.
|
|
func keptExcept(ctx context.Context) (map[string]bool, bool) {
|
|
skip, on := ctx.Value(keepRecordedKey{}).(map[string]bool)
|
|
return skip, on
|
|
}
|
|
|
|
// recordedKept is, for a send under keepingRecorded, every recorded module the machine was last sent a
|
|
// build of that the mesh's build is not identical to: module → the commit it keeps. Nil when the context
|
|
// keeps nothing, or when what the machine was last sent is not known (it is then held whole elsewhere —
|
|
// ADR 0221).
|
|
func recordedKept(ctx context.Context, open *stores, node string) (map[string]string, error) {
|
|
skip, on := keptExcept(ctx)
|
|
if !on {
|
|
return nil, nil
|
|
}
|
|
inv := open.inventory
|
|
sent, known, err := inv.SentBuilds(ctx, node)
|
|
if err != nil || !known {
|
|
return nil, err
|
|
}
|
|
current, err := inv.CurrentBuilds(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var f *moveFacts
|
|
out := map[string]string{}
|
|
for m, was := range sent {
|
|
now, held := current[m]
|
|
if !held || now.RollOut || skip[m] || was == "" || sameCommit(was, now.Commit) {
|
|
continue
|
|
}
|
|
if f == nil {
|
|
read, err := readMoveFacts(ctx, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f = &read
|
|
}
|
|
if f.identical(m, was, now.Commit) {
|
|
continue
|
|
}
|
|
out[m] = was
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// keepRecorded puts, in a shelf about to be resolved for a machine, the build each recorded module there
|
|
// runs in place of the one the mesh holds; it answers what it kept, module → commit.
|
|
func keepRecorded(ctx context.Context, open *stores, node string, shelf map[string]catalogue.Manifest) (map[string]string, error) {
|
|
kept, err := recordedKept(ctx, open, node)
|
|
if err != nil || len(kept) == 0 {
|
|
return nil, err
|
|
}
|
|
names := make([]string, 0, len(kept))
|
|
for m := range kept {
|
|
names = append(names, m)
|
|
}
|
|
sort.Strings(names)
|
|
for _, m := range names {
|
|
ran, found, err := open.inventory.ManifestAt(ctx, m, kept[m])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("%s records rather than rolls out, and %s runs its build %s, which the build "+
|
|
"records no longer hold: this send cannot keep it and does not move it — `push %s` sends the new "+
|
|
"one on a person's word (novox/hq ADR 0242)", m, node, short(kept[m]), node)
|
|
}
|
|
shelf[m] = ran
|
|
}
|
|
return kept, nil
|
|
}
|