Files
mesh-controller/internal/catalogue/bound.go
T
jochen fc65215c25 Grant a provider only the consumers bound to it (hq issue 274)
grantsFor granted every consumer a pair credential from the provider was
ever made for, so a consumer pinned back to its own store was still asked
of the store it left, which then never retired it. A credential whose
consumer's resolution binds it elsewhere is now withdrawn like one nobody
asks for, kept on record for the login the provider keeps, and said on
plan and push.
2026-10-06 16:07:12 +02:00

184 lines
8.3 KiB
Go

package catalogue
import (
"fmt"
"slices"
"strings"
)
// 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, ""
}
// Unbound is one consumer that still asks for a provision and whose own resolution binds it to
// another provider than the one a pair credential on record was made with (novox/hq issue 274).
//
// **A provider is granted exactly the consumers bound to it.** The credential from the old provider
// stays on record — it is the key to a login that provider keeps, disabled, with the consumer's data,
// until a person deletes it with `cleanup delete` (ADR 0230), and a pin back must find it — but it is
// no longer granted, so the provider stops being asked for it and retires it. Said on every plan and
// push of the provider, so a credential the mesh keeps and does not use is never kept silently.
type Unbound struct {
// Provision is what was required, Provider the machine the credential on record is from.
Provision string `json:"provision"`
Provider string `json:"provider"`
// Consumer is the machine, Module the module on it that requires it, Local the credential's
// name inside it where it keeps several (ADR 0094).
Consumer string `json:"consumer"`
Module string `json:"module"`
Local string `json:"local,omitempty"`
// BoundTo is every provider the consumer's resolution binds this credential to now; empty when
// it binds it nowhere — the module asks for the provision under other local names.
BoundTo []string `json:"bound_to,omitempty"`
}
func (u Unbound) String() string {
who := u.Module
if u.Local != "" {
who += " (as " + u.Local + ")"
}
now := "is bound to no provider under that name"
if len(u.BoundTo) > 0 {
now = "is bound to " + strings.Join(u.BoundTo, ", ")
}
return fmt.Sprintf("%s on %s %s for %s, not to %s — %s no longer grants it, so it retires that "+
"login and keeps its data until `cleanup delete` (ADR 0230); the credential from %s stays on "+
"record while that login does", who, u.Consumer, now, u.Provision, u.Provider, u.Provider, u.Provider)
}
// BindsFrom is the providers this resolution binds a pair credential's need to — the provision, the
// module that requires it and the credential's local name — answered by a machine rather than by a
// record. None means this machine states no such binding.
func (r Resolution) BindsFrom(provision, module, local string) []string {
var from []string
for _, n := range r.Needs {
if n.ByRecord || n.Name != provision || n.For != module || n.Local != local {
continue
}
if !slices.Contains(from, n.From) {
from = append(from, n.From)
}
}
return from
}