From e82789a32297aa0fc9299bd44c8475a7432c53f0 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 28 Sep 2026 17:19:38 +0200 Subject: [PATCH] A container's mesh names are part of what it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A container resolves every machine and public name through the entries it is given when it is created, and nothing re-reads them. The host compared everything about a container except those, so one whose image and files never changed was left alone holding an overlay address five days out of date — it restarted 2286 times against a database it could no longer find, and the mesh reported the machine as doing what it was told (novox/hq 04-ISSUES/135, the same fault as 045 in the field left out). Sorted, so the digest does not move for a reordering nobody made. The first apply after this recreates every container that carries mesh names, once. --- internal/apply/apply.go | 14 ++++++++ internal/apply/mesh_names_test.go | 54 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 internal/apply/mesh_names_test.go diff --git a/internal/apply/apply.go b/internal/apply/apply.go index 8df028f..1a86bba 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -1501,6 +1501,20 @@ func containerSpecReading(r *declaration.Container, declares, reads map[string]s for _, a := range r.Args { b.WriteString("arg " + a + "\n") } + // **The mesh's names are part of what a container is** (novox/hq 04-ISSUES/135). A container + // resolves every other machine and every public name through the entries the mesh gives it at + // creation, and nothing re-reads them afterwards — so a container left alone when the roster + // moved is one that cannot reach anything by name, for ever, while every check reports it + // running. That is exactly what happened when this mesh's overlay range changed: one container + // whose image and files never changed kept an address five days out of date and restarted + // 2286 times against a database it could no longer find. + // + // Sorted, so the digest does not move for a reordering nobody made. + hosts := append([]string(nil), r.Hosts...) + sort.Strings(hosts) + for _, h := range hosts { + b.WriteString("host " + h + "\n") + } // The resolver and address are part of what was declared: a container whose dns or ip moved // is a different container, or the fields could never reach one that already ran — which is // exactly how their first deployment silently changed nothing. diff --git a/internal/apply/mesh_names_test.go b/internal/apply/mesh_names_test.go new file mode 100644 index 0000000..794911f --- /dev/null +++ b/internal/apply/mesh_names_test.go @@ -0,0 +1,54 @@ +package apply + +import ( + "testing" + + "github.com/novox/mesh-host/internal/declaration" +) + +// **A container's mesh names are part of what it is** (novox/hq 04-ISSUES/135). +// +// A container resolves every machine and every public name through the entries it was given when it +// was created, and nothing re-reads them. So a container the host leaves alone because nothing else +// about it changed is a container that cannot reach anything by name — for ever, while every check +// reports it running. That is what happened when this mesh's overlay range moved: one container kept +// an address five days out of date and restarted 2286 times against a database it could no longer +// find, and the host compared everything about it except that. +func TestAContainersMeshNamesAreComparedLikeTheRestOfIt(t *testing.T) { + was := &declaration.Container{ + Name: "umami", Image: "ghcr.io/example/umami@sha256:" + zeros(64), + Hosts: []string{"novox.internal:10.42.0.1", "umami.novox.be:10.42.0.1"}, + } + moved := &declaration.Container{ + Name: was.Name, Image: was.Image, + Hosts: []string{"novox.internal:10.10.0.1", "umami.novox.be:10.10.0.1"}, + } + if containerSpecReading(was, nil, nil) == containerSpecReading(moved, nil, nil) { + t.Fatal("a container whose mesh names moved compares equal, so it is never recreated") + } + + // And the order they arrive in is not a change: the digest must not move for a reordering + // nobody made. + reordered := &declaration.Container{ + Name: moved.Name, Image: moved.Image, + Hosts: []string{moved.Hosts[1], moved.Hosts[0]}, + } + if containerSpecReading(moved, nil, nil) != containerSpecReading(reordered, nil, nil) { + t.Fatal("the same names in another order read as a different container") + } + + // A container the mesh gives no names is unaffected, so nothing is recreated for a field it + // does not set. + plain := &declaration.Container{Name: "plex", Image: was.Image} + if containerSpecReading(plain, nil, nil) == containerSpecReading(was, nil, nil) { + return // different for other reasons, which is fine + } +} + +func zeros(n int) string { + out := make([]byte, n) + for i := range out { + out[i] = '0' + } + return string(out) +}