Resolver modules: one that serves, and two ways of deciding what a machine asks

Three manifests and the rule that keeps them apart. Serving and asking are
genuinely different roles, and systemd-resolved can only do the second — it
cannot answer a wildcard, it routes the mesh's suffix to something that can. A
module that treated them as one role could not work, which is the mistake worth
naming rather than discovering.

So `the-dns-port` and `the-resolver-configuration` are two claims. A machine
gets one of each, and two of either is refused by the mesh rather than fought
over on the machine — which is what ADR 0009's table meant by listing resolvers
beside the seat and pid 1. That table names the resource `/etc/resolv.conf`,
which is what it is; a claim is a name in the catalogue's own form, and the
catalogue refuses the path as one.

Neither module knows anything about the machine it is on, which is what lets
them be static manifests: they name `mesh0` and `127.0.0.54`, both chosen by
the mesh, rather than an address only that machine has. Not 127.0.0.1 and not
127.0.0.53 — taking either would be a module claiming something it did not say
it claims.

A service can now reflect a file another module put on the machine, written
`<module>.<id>`. The resolver has to restart when the mesh rewrites the names;
without it, it would serve the names it started with for ever, with every
machine that joined afterwards unreachable and every check passing.
This commit is contained in:
2026-08-31 12:21:58 +02:00
parent 3954157555
commit bff893f8af
7 changed files with 310 additions and 1 deletions
+13 -1
View File
@@ -314,10 +314,22 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
copied["id"] = m.Module + "." + fmt.Sprint(resource["id"])
// A service saying what it reflects names resources within its own module, so those
// are prefixed too or they would point at nothing.
//
// **Unless it already names one.** A module may reflect a file another module put on
// the machine — the case this exists for is a resolver restarting when the mesh
// rewrites the names, which are computed by the mesh and belong to its module, not to
// the daemon's. Written `<module>.<id>`, and a dot is what marks it as already
// answered: prefixing it again would point at nothing, silently, and the daemon would
// serve the old names for ever while everything reported success.
if reflects, ok := resource["restart-on"].([]any); ok {
var renamed []any
for _, id := range reflects {
renamed = append(renamed, m.Module+"."+fmt.Sprint(id))
named := fmt.Sprint(id)
if strings.Contains(named, ".") {
renamed = append(renamed, named)
continue
}
renamed = append(renamed, m.Module+"."+named)
}
copied["restart-on"] = renamed
}
+44
View File
@@ -458,3 +458,47 @@ func TestAGeneratorThatOpensNothingNeedsNoMethod(t *testing.T) {
t.Fatalf("a generator that says nothing about ports opened one: %+v", rules)
}
}
// A module may reflect a file another module put on the machine.
//
// The case this exists for: a resolver restarting when the mesh rewrites the names. Those are
// computed by the mesh and belong to its module, not to the daemon's — so without this the
// daemon would serve the names it started with for ever, while every check reported success.
func TestAServiceCanReflectAnotherModulesFile(t *testing.T) {
out, err := Resolution{Node: "anchor", Modules: []Manifest{{
Module: "dnsmasq",
Resources: []map[string]any{
{"id": "config", "type": "file", "path": "/etc/dnsmasq.conf",
"content": "x", "mode": "0644"},
{"id": "run", "type": "service", "unit": "dnsmasq.service", "state": "running",
"restart-on": []any{"config", "mesh-resolver.nodes"}},
},
}}}.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
for _, r := range out {
if r["type"] != "service" {
continue
}
reflects := r["restart-on"].([]any)
var own, other bool
for _, id := range reflects {
switch id.(string) {
case "dnsmasq.config":
own = true
case "mesh-resolver.nodes":
other = true
}
}
if !own {
t.Fatalf("its own file was not prefixed with its module: %v", reflects)
}
if !other {
t.Fatalf("a file it named in full was prefixed again and now points at nothing: %v",
reflects)
}
return
}
t.Fatal("no service in the declaration")
}