Files
mesh-controller/cmd/mesh-controller/recorded_push.go
T
mesh-tools 7855ea126c Ask over mesh-sdk's composition: a question makes no pair credential and reads the mesh once (issue 1387)
The callers that only ask (status, the doctor's probes, facts, bindings, data, acts, nodes show, images, release, recorded push, consumers) resolve through resolveNode and compose through composeQuestion, over one Input read per operation (withInputCache). A question composes with Reading or Foreseeing and never calls PairCredential's maker: the credential a send would make is named, and perform makes it on a send.
2026-10-20 09:14:25 +00:00

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 := resolveNode(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 == ""
}