Files
mesh-controller/internal/catalogue/bound.go
T
jochen 8bfaf1523e Keep a consumer bound where its data is; only a pin moves it (hq ADR 0232, issue 273)
Issue 258's fix let a mesh seat's holder elsewhere answer before this machine's own provider. Right
for the resolver, which any provider answers alike; for the store's seat it re-bound every database
consumer on a machine running its own store to the holder on another, each was given a fresh, empty
database there, and nothing said so for twenty hours.

- An offer says whether it keeps its consumers' data (`keeps-consumer-data`); unsaid, a provider
  that grants each consumer a credential does. For such a provision the seat's holder no longer
  overrules a provider beside the consumer; a pin still does.
- Where each such consumer was sent is recorded (migration 0071). A resolution that would bind it
  elsewhere keeps the recorded provider and says the move; one whose provider is gone is refused,
  never answered by another.
- A push says a kept move and raises it as an urgent condition at once; the self-check's D12 raises
  it every run, with a pinned move not yet sent as a warning and any unasked move as urgent.
2026-10-06 15:19:23 +02:00

128 lines
5.9 KiB
Go

package catalogue
import "fmt"
// A consumer of a provision that keeps its data stays bound where its data is (novox/hq ADR 0232).
//
// **What a resolution chooses is not where a consumer's data is.** Resolving answers "which provider
// would I pick now", from the seats' holders, the pins and what is assigned where, and every one of
// those can change under a consumer without anybody meaning to move it. For a resolver that is the
// point: any provider answers alike. For a database it is the consumer's whole state: on 2026-10-05
// one change to how a seat's holder answers (issue 258) re-bound five database consumers on one
// machine to the store on another, each was given a fresh, empty database there, and nothing warned
// for twenty hours (issue 273). Nothing was lost only because the old provider kept everything.
//
// So the mesh records where each such consumer was last sent (the store's `binding` table), and a
// resolution that would answer it from anywhere else keeps the recorded provider instead and says so.
// **Only a pin moves it**, because a pin is a person: the one act that says "answer this machine's
// consumers from there", taken knowing the data must go first.
// KeptBinding is one consumer this resolution would have moved, and did not.
type KeptBinding struct {
// Machine is where the consumer runs, and Consumer the module there that is bound.
Machine string
Consumer string
// Provision is what it is bound for.
Provision string
// Bound is the provider it was recorded at, and still is; Would is the one the resolution chose.
Bound Chosen
Would Chosen
}
// String is the condition's sentence: the move, where the data is, and the act that confirms it.
func (k KeptBinding) String() string {
return fmt.Sprintf("would move %s's %s from %s to %s — its data is on %s; kept there. "+
"`pin %s %s %s %s` to confirm a move (and move the data first)",
k.Consumer, k.Provision, k.Bound, k.Would, k.Bound, k.Machine, k.Provision, k.Would.Node, k.Would.Module)
}
// keepBound holds every need for a provision that keeps its consumers' data at the provider its
// consumer was bound to, where the resolution chose another.
//
// A need with no record is a binding being made, and is left as resolved: it is recorded when it is
// first sent. A pin naming the provider the resolution chose is a person moving it, and is left too.
// Otherwise the recorded provider answers, if it still provides the provision — beside the consumer,
// or offered from elsewhere — and the move is returned as kept. **One that no longer does is refused,
// never answered by the provider chosen**: answering it there is exactly the silent move this exists
// to stop, and the consumer's data is still wherever it was.
func keepBound(needs []Needed, catalogue map[string]Manifest, node Node, world World,
keeps map[string]bool, here func(string) bool) ([]Needed, []KeptBinding, []string) {
var kept []KeptBinding
var problems []string
refused := map[[2]string]bool{}
out := make([]Needed, 0, len(needs))
for _, n := range needs {
if n.ByRecord || !keeps[n.Name] {
out = append(out, n)
continue
}
n.KeepsData = true
bound, recorded := world.Bound[n.For][n.Name]
chose := Chosen{Node: n.From, Module: n.Module}
if !recorded || sameProvider(bound, chose) {
out = append(out, n)
continue
}
if pin, pinned := world.Pinned[n.Name]; pinned && pin.matches(Provider{Node: chose.Node, Module: chose.Module}) {
out = append(out, n)
continue
}
held, ok, why := boundNeed(n, bound, catalogue, node, world, here)
if !ok {
if key := [2]string{n.For, n.Name}; !refused[key] {
refused[key] = true
problems = append(problems, fmt.Sprintf(
"%s on %s is bound to %s for %q, which keeps its data, and %s — it would move to %s, "+
"which holds none of it. Move its data and say so with `pin %s %s %s %s`, or give "+
"%s back what it provided",
n.For, node.Name, bound, n.Name, why, chose, node.Name, n.Name, chose.Node, chose.Module,
bound.Node))
}
continue
}
out = append(out, held)
kept = append(kept, KeptBinding{Machine: node.Name, Consumer: n.For, Provision: n.Name, Bound: bound, Would: chose})
}
return out, kept, problems
}
// sameProvider is whether a recorded provider is the one chosen. A record naming no module (none
// is written without one, but a person's hand might) matches any module on its node.
func sameProvider(bound, chose Chosen) bool {
return bound.Node == chose.Node && (bound.Module == "" || bound.Module == chose.Module)
}
// boundNeed is the need answered by the recorded provider, or why it cannot be.
func boundNeed(n Needed, bound Chosen, catalogue map[string]Manifest, node Node, world World,
here func(string) bool) (Needed, bool, string) {
held := Needed{Name: n.Name, For: n.For, Local: n.Local, KeepsData: true}
if bound.Node == node.Name {
m, known := catalogue[bound.Module]
if !known || !here(bound.Module) || !providesAt(m, n.Name, ScopeMesh) {
return Needed{}, false, fmt.Sprintf("%s no longer runs on %s", bound.Module, node.Name)
}
at := node.At
if at == "" {
at = "127.0.0.1"
}
held.From, held.At, held.Module = node.Name, at, m.Module
held.Serves, held.SharedOwn, held.Identity = servedByOne(m, n.Name), sharedByOne(m, n.Name), m.IdentityBoundOf(n.Name)
return held, true, ""
}
matching := bound.among(world.Offered[n.Name])
if len(matching) != 1 {
return Needed{}, false, fmt.Sprintf("%s no longer provides it", bound)
}
p := matching[0]
if node.At == "" || p.At == "" {
return Needed{}, false, fmt.Sprintf("%s and %s are not both on the private network", node.Name, p.Node)
}
identity := DefaultIdentityBound
if pm, known := catalogue[p.Module]; known {
held.SharedOwn, _ = pm.SharedCredentialOf(n.Name)
identity = pm.IdentityBoundOf(n.Name)
}
held.From, held.At, held.Module, held.Serves, held.Identity = p.Node, p.At, p.Module, p.Serves, identity
return held, true, ""
}