Adoption mode: a node in use is adopted before it is converged (hq ADR 0100–0103) #44

Merged
jschoubben merged 33 commits from feat/adoption-mode into main 2026-09-22 19:01:51 +00:00
4 changed files with 71 additions and 11 deletions
Showing only changes of commit 28b7fb81ba - Show all commits
+45
View File
@@ -1,6 +1,7 @@
package catalogue
import (
"fmt"
"strings"
"testing"
)
@@ -157,3 +158,47 @@ func TestTwoWaysToBeOnAPrivateNetworkRefuseAndNameBoth(t *testing.T) {
}
}
}
// reloading answers the runtime's trust as the networking module does (novox/hq ADR 0102): a file
// written into, and the runtime reloaded on it.
type reloading struct{}
func (reloading) Resources(node string) ([]map[string]any, bool, error) {
return []map[string]any{
{"id": "registry-trust", "type": "file", "path": "/etc/docker/daemon.json",
"merge": MergeJSON, "into": "json", "content": `{"insecure-registries":["r:5000"]}`},
{"id": "registry-trust-reload", "type": "service", "unit": "docker.service",
"state": "running", "reload-on": []string{"registry-trust"}},
}, true, nil
}
func TestWhatAServiceIsReloadedOnIsNamedAsTheHostWillSeeIt(t *testing.T) {
// Ids are prefixed with their module on the way out. An unprefixed reload-on would name a
// resource the host never sees, and the runtime would never be reloaded for its trust.
got, err := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
out, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": reloading{}}})
if err != nil {
t.Fatal(err)
}
var file, service map[string]any
for _, r := range out {
switch r["type"] {
case "file":
file = r
case "service":
service = r
}
}
if file == nil || service == nil {
t.Fatalf("got %v", out)
}
if file["into"] != "json" {
t.Errorf("into did not reach the host: %v", file)
}
if want := "[" + file["id"].(string) + "]"; fmt.Sprint(service["reload-on"]) != want {
t.Errorf("reload-on names %v, the file is %v", service["reload-on"], file["id"])
}
}
+5
View File
@@ -559,6 +559,11 @@ func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[stri
if renamed := reflectsRenamed(m.Module, resource["restart-on"]); renamed != nil {
copied["restart-on"] = renamed
}
// And what it is reloaded on, by the same rule (novox/hq ADR 0102): an id left
// unprefixed matches nothing, and the service is never reloaded.
if renamed := reflectsRenamed(m.Module, resource["reload-on"]); renamed != nil {
copied["reload-on"] = renamed
}
owner[fmt.Sprint(copied["id"])] = m.Module
out = append(out, copied)
}
+10 -8
View File
@@ -140,18 +140,20 @@ func (g *Generator) Resources(node string) ([]map[string]any, bool, error) {
}
resources = append(resources,
map[string]any{
// Merged, not owned: the runtime's daemon file is the machine's, and this states
// one fact into it. The registry speaks plain HTTP because every path to it is
// already inside the overlay's encryption (ADR 0082) — this line is the runtime
// being told what the mesh already means.
// Written into, not over (novox/hq ADR 0102): the runtime's daemon file is the
// machine's — its data directory, its logging, whatever a predecessor set — and
// this states one fact in it. The host sets this key and keeps every other.
// ("merge" is the operator's settings merged into this content; "into" is the
// content written into the machine's file.) The registry speaks plain HTTP
// because every path to it is already inside the overlay's encryption (ADR 0082).
"id": "registry-trust", "type": "file", "path": "/etc/docker/daemon.json",
"content": string(trust) + "\n", "mode": "0644", "merge": "json",
"content": string(trust) + "\n", "mode": "0644", "merge": "json", "into": "json",
},
map[string]any{
// The runtime reloads nothing for this setting, so it is restarted when the fact
// changes — once, at joining, before the machine runs anything that would mind.
// Reloaded, not restarted: the runtime re-reads its trusted registries on a reload,
// and a restart stops every container on the machine (measured; ADR 0102).
"id": "registry-trust-reload", "type": "service", "unit": "docker.service",
"state": "running", "restart-on": []string{"registry-trust"},
"state": "running", "reload-on": []string{"registry-trust"},
})
}
return resources, true, nil
+11 -3
View File
@@ -1,6 +1,7 @@
package overlay
import (
"fmt"
"strings"
"testing"
)
@@ -44,13 +45,20 @@ func TestTheNetworkCarriesRegistryTrust(t *testing.T) {
if file == nil || service == nil {
t.Fatalf("the trust file or its reload is missing: %v", trusted)
}
if file["path"] != "/etc/docker/daemon.json" || file["merge"] != "json" {
t.Fatalf("the trust is not a merged daemon.json: %v", file)
if file["path"] != "/etc/docker/daemon.json" || file["merge"] != "json" || file["into"] != "json" {
t.Fatalf("the trust is not written into daemon.json (ADR 0102): %v", file)
}
if content, _ := file["content"].(string); !strings.Contains(content, `"anchor.internal:5000"`) {
t.Fatalf("the trust does not name the store: %v", file["content"])
}
if service["unit"] != "docker.service" {
t.Fatalf("the reload does not restart the runtime: %v", service)
t.Fatalf("the reload is not the runtime's: %v", service)
}
// Reloaded, never restarted: a restart stops every container on the machine (ADR 0102).
if _, restarts := service["restart-on"]; restarts {
t.Fatalf("the runtime is restarted for its trust: %v", service)
}
if fmt.Sprint(service["reload-on"]) != "[registry-trust]" {
t.Fatalf("the runtime is not reloaded for its trust: %v", service)
}
}