The controller plans config, templates and moved providers' provisions, hands bus accounts to the vault and judges the new manifest words, on the feature's SDK and node-engine validator (feature 57, config and provisioning).
214 lines
8.3 KiB
Go
214 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// Under a module's own `record`, a move that recreates no container and moves no data rolls out (novox/hq ADR
|
|
// 0377, changing ADR 0242 §1 and ADR 0360 §1).
|
|
//
|
|
// A module records because its manifest says so, because it keeps irreplaceable data, because a person set it, or
|
|
// because it provides the bus. The first two hold for a reason the manifest shows — a change that recreates mail's
|
|
// containers interrupts everyone's mail, a new database image may upgrade its databases in place — and that hold
|
|
// also held the moves that interrupt nothing: on 2026-10-15 the database server moved with only its tools bundle
|
|
// changed, its walk said it "recreates none of its containers", and ace stayed behind until a person pushed.
|
|
//
|
|
// So **a move under a module's own `record` is judged per machine, against the build that machine was last sent**
|
|
// (§2): when it recreates none of the module's containers and moves none of its data (catalogue.Holds, §3) it
|
|
// goes as a module that rolls out goes — a walk sends it, and every other send treats it as a move waiting for its
|
|
// walk — and otherwise it is kept at the build the machine runs, as before. The module's policy stays `record`. A
|
|
// move that cannot be judged waits (§4); a person's `record` and the bus hold every move (§5); and each walk says
|
|
// per machine which way a recorded module went (§6).
|
|
|
|
// recordedRollsWhy is what a walk says of a recorded module's move that went out (ADR 0377 §6).
|
|
const recordedRollsWhy = "went out: it recreates none of its containers and moves none of its data (novox/hq ADR 0377)"
|
|
|
|
// recordVerdict is one machine's move of a recorded module: whether it rolls out, and what the walk says of it.
|
|
type recordVerdict struct {
|
|
rolls bool
|
|
// said is what the walk says: recordedRollsWhy, or "waits: " and why — the recreate words, the data change
|
|
// named, the person's or the bus's hold, or that it could not be judged.
|
|
said string
|
|
}
|
|
|
|
// recordedMove judges a module's move on a machine from the build it was last sent (`was`) to the one it would be
|
|
// sent now. A module that rolls out rolls; one whose policy is a person's or the bus's waits, said; one under its
|
|
// own `record` rolls when the move recreates nothing and moves no data, and waits when it does or cannot be judged.
|
|
func (f moveFacts) recordedMove(module, was string) recordVerdict {
|
|
now := f.current[module]
|
|
switch {
|
|
case now.RollOut:
|
|
return recordVerdict{rolls: true}
|
|
case now.From == catalogue.FromPerson:
|
|
return recordVerdict{said: "waits: a person set it to record, and holds every move of it until they push (novox/hq ADR 0377 §5)"}
|
|
case now.From == catalogue.FromBus:
|
|
return recordVerdict{said: "waits: it provides the bus, which moves only by the bus step a person starts (novox/hq ADR 0377 §5)"}
|
|
case !now.OwnRecord():
|
|
return recordVerdict{said: "waits: its policy records"}
|
|
case f.judge == nil:
|
|
return recordVerdict{said: "waits: it could not be judged — the build records were not read (novox/hq ADR 0377 §4)"}
|
|
}
|
|
return f.judge(module, was, now.Commit)
|
|
}
|
|
|
|
// rolls is whether a module's move on a machine from `was` goes as a move that rolls out (ADR 0377 §1).
|
|
func (f moveFacts) rolls(module, was string) bool {
|
|
return f.recordedMove(module, was).rolls
|
|
}
|
|
|
|
// recordJudge is the judging of moves under a module's own `record`, reading the build records once per move and
|
|
// the catalogue once (ADR 0377 §2–§4).
|
|
func recordJudge(ctx context.Context, inv *inventory.Inventory) func(module, was, now string) recordVerdict {
|
|
judged := map[string]recordVerdict{}
|
|
var shelf map[string]catalogue.Manifest
|
|
var shelfErr error
|
|
shelfRead := false
|
|
return func(module, was, now string) recordVerdict {
|
|
key := module + "\x00" + was + "\x00" + now
|
|
if v, seen := judged[key]; seen {
|
|
return v
|
|
}
|
|
v := func() recordVerdict {
|
|
cannot := func(why string) recordVerdict {
|
|
return recordVerdict{said: "waits: it could not be judged — " + why + " (novox/hq ADR 0377 §4)"}
|
|
}
|
|
if was == "" {
|
|
return cannot("what the machine was last sent of it is not known")
|
|
}
|
|
from, found, err := inv.ManifestAt(ctx, module, was)
|
|
if err != nil {
|
|
return cannot(fmt.Sprintf("its build %s, which the machine was last sent, cannot be read: %v", short(was), err))
|
|
}
|
|
if !found {
|
|
return cannot(fmt.Sprintf("its build %s, which the machine was last sent, is not in the build records", short(was)))
|
|
}
|
|
if !shelfRead {
|
|
shelf, shelfErr = inv.Catalogue(ctx)
|
|
shelfRead = true
|
|
}
|
|
if shelfErr != nil {
|
|
return cannot(fmt.Sprintf("the build it would be sent cannot be read: %v", shelfErr))
|
|
}
|
|
to, held := shelf[module]
|
|
if !held {
|
|
return cannot("the build it would be sent is not in the catalogue")
|
|
}
|
|
if why := catalogue.Holds(from, to); why != "" {
|
|
return recordVerdict{said: "waits: " + why}
|
|
}
|
|
return recordVerdict{rolls: true, said: recordedRollsWhy}
|
|
}()
|
|
judged[key] = v
|
|
return v
|
|
}
|
|
}
|
|
|
|
// judgeRecordedInWalk judges, for a walk carrying a module whose policy records, each machine running it whose
|
|
// build would move (ADR 0377 §2), keeps on the plan which way each went (§6), says it on the controller's log, and
|
|
// answers the machines its move goes out to. Judged once per walk: a walk read again answers what it kept.
|
|
func judgeRecordedInWalk(ctx context.Context, inv *inventory.Inventory, p *inventory.Plan, module string,
|
|
state *inventory.PlanModule) ([]string, error) {
|
|
if state.Recorded != nil {
|
|
return wentOut(state.Recorded), nil
|
|
}
|
|
running, err := inv.Running(ctx, module)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := readMoveFacts(ctx, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := f.current[module]
|
|
said := map[string]string{}
|
|
for _, n := range running {
|
|
sent, known, err := inv.SentBuilds(ctx, n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
was, carried := sent[module]
|
|
if known && carried && f.identical(module, was, now.Commit) {
|
|
continue // no move there
|
|
}
|
|
v := f.recordedMove(module, was)
|
|
if !known {
|
|
v = recordVerdict{said: "waits: it could not be judged — what " + n + " was last sent is not known (novox/hq ADR 0377 §4)"}
|
|
}
|
|
said[n] = v.said
|
|
fmt.Printf("%s: %s on %s %s\n", p.ID, module, n, v.said)
|
|
}
|
|
if len(said) > 0 {
|
|
state.Recorded = said
|
|
}
|
|
return wentOut(said), nil
|
|
}
|
|
|
|
// wentOut is the machines a recorded module's move went out to, sorted.
|
|
func wentOut(recorded map[string]string) []string {
|
|
var out []string
|
|
for _, n := range sortedKeysOf(recorded) {
|
|
if recorded[n] == recordedRollsWhy {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// recordedSaid is, for a walk's note at its end, which way each recorded module it carried went on each machine
|
|
// (ADR 0377 §6).
|
|
func recordedSaid(p inventory.Plan) []string {
|
|
var out []string
|
|
for _, m := range sortedKeysOf(planModuleNames(p)) {
|
|
s := p.Modules[m]
|
|
for _, n := range sortedKeysOf(s.Recorded) {
|
|
out = append(out, fmt.Sprintf("%s on %s %s", m, n, s.Recorded[n]))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func planModuleNames(p inventory.Plan) map[string]string {
|
|
out := map[string]string{}
|
|
for m, s := range p.Modules {
|
|
if s != nil && len(s.Recorded) > 0 {
|
|
out[m] = m
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// recordedWaiting is, on one machine, every move of a recorded module that waits for a person, each said with why
|
|
// (ADR 0377 §6): what `status` names the machine behind for, with `push <node>` as the remedy. A move under a
|
|
// module's own `record` that recreates nothing and moves no data is not among them: it goes with its walk.
|
|
func recordedWaiting(ctx context.Context, inv *inventory.Inventory, f moveFacts, node string) ([]string, error) {
|
|
sent, known, err := inv.SentBuilds(ctx, node)
|
|
if err != nil || !known {
|
|
return nil, err
|
|
}
|
|
var out []string
|
|
for _, m := range sortedKeysOf(sent) {
|
|
was := sent[m]
|
|
now, held := f.current[m]
|
|
if !held || now.RollOut || now.Commit == "" || was == "" || f.identical(m, was, now.Commit) {
|
|
continue
|
|
}
|
|
if v := f.recordedMove(m, was); !v.rolls {
|
|
out = append(out, fmt.Sprintf("%s %s → %s %s", m, short(was), short(now.Commit), v.said))
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// stringsKeys is a map of lists as a map of its keys, for sortedKeysOf.
|
|
func stringsKeys(m map[string][]string) map[string]string {
|
|
out := map[string]string{}
|
|
for k := range m {
|
|
out[k] = k
|
|
}
|
|
return out
|
|
}
|