Merge pull request 'A mesh seat's holder elsewhere answers before this machine's own provider (hq issue 258)' (#59) from fix/a-mesh-seat-answers-before-the-machines-own into main

This commit was merged in pull request #59.
This commit is contained in:
2026-10-05 19:14:24 +00:00
3 changed files with 110 additions and 10 deletions
+26 -1
View File
@@ -341,7 +341,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
// trust boundary the moment both ends are containers**, and treating it as one gave the // trust boundary the moment both ends are containers**, and treating it as one gave the
// commonest arrangement of all — a service and its database on one node — the weakest // commonest arrangement of all — a service and its database on one node — the weakest
// handling, silently. // handling, silently.
if satisfied[want] && !isModule(catalogue, want) { if satisfied[want] && !isModule(catalogue, want) && !answeredElsewhere(want, node, world, brokered) {
here := func(name string) bool { return chosen[name] || assignedHere[name] } here := func(name string) bool { return chosen[name] || assignedHere[name] }
local := providersHere(catalogue, here, want) local := providersHere(catalogue, here, want)
// Which of them it matters to choose between. A plain capability — a shell, a display // Which of them it matters to choose between. A plain capability — a shell, a display
@@ -1134,3 +1134,28 @@ func machineReachRemedy(catalogue map[string]Manifest, want, node string) string
} }
return fmt.Sprintf("assign one to %s: %s", node, strings.Join(named, "; ")) return fmt.Sprintf("assign one to %s: %s", node, strings.Join(named, "; "))
} }
// answeredElsewhere says that a mesh-wide provision this machine could answer itself is answered by
// another machine all the same: one this machine was pinned to, or the holder of the mesh seat that
// delivers it (novox/hq ADR 0110, ADR 0194).
//
// **A provider on the machine was taken before either was asked.** The branch for a provision
// answered here bound the consumer to the local provider and consulted a pin only between two local
// ones, and the seat's holder never; both were read only for a provider on another machine. So when
// every machine ran a provider of `wildcard-resolution` — each machine's own resolver, still assigned
// while the mesh moved to one — every machine bound its resolver configuration to itself, with the
// mesh's one resolver recorded, held and pinned (novox/hq issue 258). The seat is the mesh's choice of
// who answers, made once; a provider that merely runs here does not overrule it, and neither does it
// overrule a pin somebody set on this machine.
//
// Not in the first pass, which asks only what this machine offers and has no providers to read.
func answeredElsewhere(want string, node Node, world World, brokered map[string]bool) bool {
if world.Unchecked || !brokered[want] {
return false
}
if c, pinned := world.Pinned[want]; pinned {
return c.Node != node.Name
}
holder, held := HolderAmong(want, world.Offered[want], world.Held)
return held && holder.Node != node.Name
}
+17 -9
View File
@@ -15,7 +15,8 @@ import (
// same arrangement, and to the two things a resolver here must never do — read resolv.conf for // same arrangement, and to the two things a resolver here must never do — read resolv.conf for
// its upstreams, or take an address systemd-resolved holds. // its upstreams, or take an address systemd-resolved holds.
// resolverShelf is the three resolver modules beside something that answers `mesh-addressing`. // resolverShelf is the three resolver modules and the container runtime beside something that
// answers `mesh-addressing`.
// The networking module that really does is composed in the controller and cannot be imported // The networking module that really does is composed in the controller and cannot be imported
// here, so a stand-in offers the same word; what is under test is the manifests, not the network. // here, so a stand-in offers the same word; what is under test is the manifests, not the network.
func resolverShelf(t *testing.T) map[string]Manifest { func resolverShelf(t *testing.T) map[string]Manifest {
@@ -23,7 +24,7 @@ func resolverShelf(t *testing.T) map[string]Manifest {
shelf := map[string]Manifest{ shelf := map[string]Manifest{
"net": {Module: "net", Version: "1", Provides: []Offer{{Name: "mesh-addressing"}}}, "net": {Module: "net", Version: "1", Provides: []Offer{{Name: "mesh-addressing"}}},
} }
for _, name := range []string{"dnsmasq", "resolv-conf", "resolved-split-dns"} { for _, name := range []string{"dnsmasq", "resolv-conf", "resolved-split-dns", "docker"} {
shelf[name] = catalogueManifest(t, name) shelf[name] = catalogueManifest(t, name)
} }
return shelf return shelf
@@ -117,8 +118,9 @@ func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T
// that file, the machine pointed at the resolver by address, and the runtime given no resolver of // that file, the machine pointed at the resolver by address, and the runtime given no resolver of
// its own but kept running across a restart (ADR 0196). // its own but kept running across a restart (ADR 0196).
func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) { func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
got, err := Resolve(resolverShelf(t), []string{"dnsmasq", "resolv-conf"}, got, err := Resolve(resolverShelf(t), []string{"dnsmasq", "resolv-conf", "docker"},
Node{Name: "anchor", At: "anchor.internal"}, World{}) Node{Name: "anchor", At: "anchor.internal", Capabilities: map[string]bool{
"package-manager": true, "service-manager": true, "privileged": true}}, World{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -167,13 +169,19 @@ func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
t.Errorf("the resolver was not told to forward the zone to its answerer:\n%s", z) t.Errorf("the resolver was not told to forward the zone to its answerer:\n%s", z)
} }
// The runtime's own file, written into (novox/hq ADR 0102) with one key, by what decides how the // The runtime's own file, written into (novox/hq ADR 0102) with one key, by the runtime's own
// machine resolves: a restart keeps every container running. No `dns` — a container copies its // module — a module does not write another software's configuration (issue 190): a restart keeps
// machine's resolvers (ADR 0196), and the mesh's resolver is not written into the runtime twice. // every container running. No `dns` — a container copies its machine's resolvers (ADR 0196), and
// neither the resolver nor what decides how the machine resolves writes the runtime's file.
if ids["dnsmasq.runtime-dns"] != nil { if ids["dnsmasq.runtime-dns"] != nil {
t.Errorf("the resolver still writes the runtime's dns: %v", ids["dnsmasq.runtime-dns"]) t.Errorf("the resolver still writes the runtime's dns: %v", ids["dnsmasq.runtime-dns"])
} }
runtime := ids["resolv-conf.runtime-config"] for id := range ids {
if strings.HasPrefix(id, "resolv-conf.runtime") {
t.Errorf("what the machine asks still writes the runtime's file: %s", id)
}
}
runtime := ids["docker.daemon"]
if runtime == nil || runtime["path"] != "/etc/docker/daemon.json" || runtime["into"] != "json" { if runtime == nil || runtime["path"] != "/etc/docker/daemon.json" || runtime["into"] != "json" {
t.Fatalf("live-restore is not written into the runtime's file: %v", runtime) t.Fatalf("live-restore is not written into the runtime's file: %v", runtime)
} }
@@ -195,7 +203,7 @@ func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
t.Errorf("the runtime is ordered restarted, which stops every container (ADR 0102): %v", r) t.Errorf("the runtime is ordered restarted, which stops every container (ADR 0102): %v", r)
} }
for _, on := range asStrings(r["reload-on"]) { for _, on := range asStrings(r["reload-on"]) {
if on == "resolv-conf.runtime-config" { if on == "docker.daemon" {
reloaded = true reloaded = true
} }
} }
@@ -0,0 +1,67 @@
package catalogue
import "testing"
// A mesh-wide provision whose seat is held on another machine is answered by that holder, even on a
// machine that runs a provider of its own (novox/hq issue 258). Every machine ran its own resolver
// while the mesh moved to one; each bound its resolver configuration to itself, with the mesh's
// resolver held elsewhere and pinned.
func resolverMesh() map[string]Manifest {
return map[string]Manifest{
"resolver": {Module: "resolver", Version: "1",
Provides: []Offer{{Name: "wildcard-resolution", Scope: ScopeMesh}},
Claims: []Claim{{Name: "mesh-dns-resolver", Scope: ScopeMesh}}},
"asker": {Module: "asker", Version: "1", Requires: []string{"wildcard-resolution"}},
}
}
func resolverWorld(held string, pin *Chosen) World {
w := World{
Offered: map[string][]Provider{"wildcard-resolution": {
{Node: "anchor", At: "anchor.internal", Module: "resolver"},
{Node: "laptop", At: "laptop.internal", Module: "resolver"},
}},
}
// Held is who holds it across the mesh; Holdings is the same holder on record, which lets this
// machine's own claimant stand beside it (ADR 0131).
w.Held = []Held{{Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: held, Module: "resolver"}}
w.Holdings = w.Held
if pin != nil {
w.Pinned = map[string]Chosen{"wildcard-resolution": *pin}
}
return w
}
func answeredFrom(t *testing.T, world World) string {
t.Helper()
laptop := Node{Name: "laptop", At: "laptop.internal"}
got, err := Resolve(resolverMesh(), []string{"resolver", "asker"}, laptop, world)
if err != nil {
t.Fatal(err)
}
for _, n := range got.Needs {
if n.Name == "wildcard-resolution" {
return n.From
}
}
t.Fatalf("no binding for wildcard-resolution: %+v", got.Needs)
return ""
}
func TestTheSeatsHolderElsewhereAnswersBeforeThisMachinesOwnProvider(t *testing.T) {
if from := answeredFrom(t, resolverWorld("anchor", nil)); from != "anchor" {
t.Fatalf("bound to %s; the seat is held on anchor", from)
}
}
func TestAPinElsewhereAnswersBeforeThisMachinesOwnProvider(t *testing.T) {
if from := answeredFrom(t, resolverWorld("laptop", &Chosen{Node: "anchor", Module: "resolver"})); from != "anchor" {
t.Fatalf("bound to %s; this machine was pinned to anchor", from)
}
}
func TestTheHolderOnThisMachineStillAnswersHere(t *testing.T) {
if from := answeredFrom(t, resolverWorld("laptop", nil)); from != "laptop" {
t.Fatalf("bound to %s; the seat is held here", from)
}
}