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:
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user