A recorded build moves only by a person's push (ADR 0242), so that push is the word its upgrade policy asks for; S15 counted it as a repair and wanted a healer for split-dns, words and uplink-verbs. The push now reads what it carries before it is recorded and says so in its kind, and the ten pushes of 2026-10-07 are named so their three warnings clear on the next tick.
154 lines
6.7 KiB
Go
154 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A push of recorded builds is no repair (novox/hq issue 301, ADR 0242, to-be 45 §7).
|
|
//
|
|
// **A recorded build moves only by a person's push** (ADR 0242): a module whose upgrade policy is
|
|
// `record` — the network path, the providers whose restart costs, mail — has its new build registered
|
|
// at its merge and sent by nothing but `push <node>`. That push is the person's word the policy asks
|
|
// for: the mesh working as decided. The hand-act log counted it as a repair all the same, because a push
|
|
// by hand is the repair roll-out by default exists to end. On 2026-10-07 the operator approved, node by
|
|
// node, new builds of the resolver, the FortiClient adapter, the network managers and the packet filter;
|
|
// ten pushes later S15 wanted a healer for `split-dns`, `words` and `uplink-verbs`.
|
|
//
|
|
// **The push says what it was, from what it carried.** Never from a word the person gives — that is
|
|
// how a repair could pass for anything (issue 292). Before it is recorded, a push that names one machine
|
|
// reads what that machine was last sent against what the mesh holds. It is a **push of recorded builds**
|
|
// when:
|
|
//
|
|
// - what the machine was last sent is known;
|
|
// - its last report is no failure or refusal, so the push does not re-send to mend one;
|
|
// - it moves at least one module's build; and
|
|
// - every module it moves records rather than rolls out: a held new build of one the machine runs, or
|
|
// the first build of one newly assigned there (whose `assign` says "run `push <node>` to send it").
|
|
//
|
|
// Anything else — a build that rolls out, a module taken off, nothing moved at all — is a push by hand
|
|
// as before, and counts. The act is written with `kind: recorded-builds` and what it carried; the table
|
|
// of verbs (handActVerbs) reads a push of that kind as a person's decision, and S15 never counts it.
|
|
//
|
|
// What is not compared: settings and grants. A push of a recorded build carries whatever else
|
|
// changed in the machine's declaration with it, as any push does.
|
|
|
|
// recordedPush answers what a push to one machine carries, one "module from → to" each, when every
|
|
// build it moves is a recorded one held for a person's word; otherwise nil and why it is not.
|
|
//
|
|
// `modules` is the machine's set; `sent` and `known` what it was last sent (Inventory.SentBuilds);
|
|
// `current` what the mesh holds; `identical` whether two builds of a module put the same thing on a
|
|
// machine (moveFacts.identical); `failing` the machine's last report when it was a failure or a refusal.
|
|
func recordedPush(modules []string, sent map[string]string, known bool, current map[string]inventory.CurrentBuild,
|
|
identical func(module, a, b string) bool, failing string) ([]string, string) {
|
|
if !known {
|
|
return nil, "what the machine was last sent is not known"
|
|
}
|
|
if failing != "" {
|
|
return nil, "its last report was " + failing + ": the push sends again what failed"
|
|
}
|
|
in := map[string]bool{}
|
|
var carried []string
|
|
for _, m := range modules {
|
|
in[m] = true
|
|
// A module the mesh holds no build of, or holds without a source, has no build to move.
|
|
now, held := current[m]
|
|
if !held || now.Commit == "" {
|
|
continue
|
|
}
|
|
was, ran := sent[m]
|
|
if ran && identical(m, was, now.Commit) {
|
|
continue
|
|
}
|
|
if now.RollOut {
|
|
return nil, fmt.Sprintf("%s would move %sto %s, and it rolls out: its build is a plan's to send", m,
|
|
fromBuild(was, ran), buildName(now.Commit))
|
|
}
|
|
carried = append(carried, fmt.Sprintf("%s %s→ %s", m, fromBuild(was, ran), buildName(now.Commit)))
|
|
}
|
|
for m := range sent {
|
|
if !in[m] {
|
|
return nil, m + " is taken off the machine"
|
|
}
|
|
}
|
|
if len(carried) == 0 {
|
|
return nil, "it moves no build: a send again"
|
|
}
|
|
sort.Strings(carried)
|
|
return carried, ""
|
|
}
|
|
|
|
// recordedPushOf reads, for a push about to name one machine, whether it is a push of recorded builds: what
|
|
// it carries,
|
|
// or nil and why not. An error is that it could not be read; the push is then recorded as a push by hand,
|
|
// as every push was before, and says why.
|
|
func recordedPushOf(ctx context.Context, node string) ([]string, string, error) {
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
plan, _, err := planFor(ctx, open, node)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("its set cannot be worked out: %w", err)
|
|
}
|
|
modules := make([]string, 0, len(plan.Modules))
|
|
for _, m := range plan.Modules {
|
|
modules = append(modules, m.Module)
|
|
}
|
|
sent, known, err := inv.SentBuilds(ctx, node)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
f, err := readMoveFacts(ctx, inv)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
doing, said, err := inv.DoingOf(ctx, node)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
failing := ""
|
|
if said && (doing.Outcome == inventory.OutcomeFailed || doing.Outcome == inventory.OutcomeRefused) {
|
|
failing = doing.Outcome
|
|
}
|
|
carried, why := recordedPush(modules, sent, known, f.current, f.identical, failing)
|
|
return carried, why, nil
|
|
}
|
|
|
|
// recordedBefore are the pushes recorded before a push said its kind, each a push of recorded builds by
|
|
// what it carried (novox/hq issue 301): the operator's word, machine by machine, for new builds of modules
|
|
// whose upgrade policy records — systemd-resolved, forticlient, networkmanager, systemd-networkd,
|
|
// nftables, mailu. The log did not keep what a push carried then, so the record names them; no push
|
|
// recorded since needs it, because every push now says its kind. S15 reads these as it reads a push of
|
|
// that kind, and the three `healer-wanted` they opened clear on the next tick.
|
|
var recordedBefore = map[string]string{
|
|
"act-1791404241010309198-1": "systemd-resolved, first build on its machine (ADR 0247)",
|
|
"act-1791404587689907257-1": "systemd-resolved, first build; nftables, held build (catalogue #111)",
|
|
"act-1791404809925218992-1": "nftables, held build (catalogue #111)",
|
|
"act-1791404844504887009-1": "mailu and nftables, held builds (catalogue #106, #111)",
|
|
"act-1791408072745552019-1": "forticlient, held build (catalogue #114)",
|
|
"act-1791408101286318350-1": "forticlient, held build (catalogue #114)",
|
|
"act-1791409209593713522-1": "networkmanager, held build (catalogue #107)",
|
|
"act-1791409280125022264-1": "networkmanager, held build (catalogue #107)",
|
|
"act-1791409336586475835-1": "networkmanager, held build (catalogue #107)",
|
|
"act-1791409393494198352-1": "systemd-networkd, held build (catalogue #107)",
|
|
}
|
|
|
|
// pushedRecorded is whether an act in the log is a push of recorded builds.
|
|
func pushedRecorded(a link.HandAct) bool {
|
|
if a.Verb != "push" {
|
|
return false
|
|
}
|
|
if a.Kind == link.KindRecordedBuilds {
|
|
return true
|
|
}
|
|
_, before := recordedBefore[a.ID]
|
|
return before && a.Kind == ""
|
|
}
|