From 58c0e715c7d5bf19228640e13388f0fb29801157 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 28 Sep 2026 15:44:59 +0200 Subject: [PATCH] A resource's owner is read to the last dot, because a module's name may contain one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit novox.be is a module on this mesh. Reading a resource's owner to the first dot made its resources belong to something called "novox", and a step's gate would then skip whatever else happened to start that way — silently. A resource's own id never contains a dot, which is what makes the last one the boundary; the mesh's derived step was changed to add a hyphen rather than a dot for the same reason (mesh-controller #128). --- internal/apply/apply.go | 18 ++++++++++++------ internal/apply/runonce_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/internal/apply/apply.go b/internal/apply/apply.go index eeb9fc2..8df028f 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -2281,16 +2281,22 @@ func meshMadeUnits(known store.State) map[string]bool { // moduleOf is the module a declared resource belongs to. // -// The mesh composes a module's resource ids as `.`, so the part before the first dot is -// the module. False for what the mesh declares in its own right — a guard, an opening, the adoption's -// own resources — which belongs to no module, and whose gate is therefore the machine's. +// The mesh composes a module's resource ids as `.`, and **a module's name may +// contain a dot** — `novox.be` is one on this mesh — while a resource's own id never does. So the +// owner is everything before the *last* dot; reading to the first one would make `novox.be.server` +// belong to a module called "novox", and a gate would then skip whatever else happened to start that +// way. +// +// False for what the mesh declares in its own right: the foundation's resources carry no dot at all, +// and the adoption's are named for the mesh rather than for a module. Both belong to no module, and +// their gate is therefore the machine's. func moduleOf(identity string) (string, bool) { if strings.HasPrefix(identity, declaration.AdoptionPrefix) { return "", false } - module, _, ok := strings.Cut(identity, ".") - if !ok || module == "" { + at := strings.LastIndex(identity, ".") + if at <= 0 { return "", false } - return module, true + return identity[:at], true } diff --git a/internal/apply/runonce_test.go b/internal/apply/runonce_test.go index 32950c4..e19231f 100644 --- a/internal/apply/runonce_test.go +++ b/internal/apply/runonce_test.go @@ -343,7 +343,7 @@ func TestAFailedStepGatesItsModuleAndNotTheMachine(t *testing.T) { return "", nil } d := parseTrusted(t, `{"declaration":1,"resources":[ - {"id":"mesh-catalog.runtime.prepare","type":"container","name":"catalogue-prepare","image":"`+pinned+`","run-once":true}, + {"id":"mesh-catalog.runtime-prepare","type":"container","name":"catalogue-prepare","image":"`+pinned+`","run-once":true}, {"id":"mesh-catalog.runtime","type":"container","name":"catalogue","image":"`+pinned+`"}, {"id":"gitea.server","type":"container","name":"forge","image":"`+pinned+`"} ]}`) @@ -374,3 +374,27 @@ func TestAFailedStepGatesItsModuleAndNotTheMachine(t *testing.T) { t.Errorf("the report does not say what was not attempted: %q", skipped) } } + +func TestAResourcesOwnerIsReadToTheLastDot(t *testing.T) { + // **A module's name may contain a dot.** `novox.be` is one on this mesh, so reading a resource's + // owner to the first dot would make its resources belong to something called "novox" — and a gate + // would skip whatever else happened to start that way. A resource's own id never contains one, + // which is what makes the last dot the boundary. + for identity, want := range map[string]string{ + "novox.be.server": "novox.be", + "mesh-catalog.runtime-prepare": "mesh-catalog", + "gitea.admin-bootstrap": "gitea", + } { + got, ours := moduleOf(identity) + if !ours || got != want { + t.Errorf("%q belongs to %q (%v), want %q", identity, got, ours, want) + } + } + // What the mesh declares in its own right belongs to no module: the foundation's resources carry + // no dot, and the adoption's are the mesh's. + for _, identity := range []string{"container-runtime", "store-ready", "adoption.guard", ".server"} { + if _, ours := moduleOf(identity); ours { + t.Errorf("%q was read as a module's", identity) + } + } +} -- 2.54.0