From 8b2d1bce9dd4e1edea4a525f3cb15338977e9150 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 31 Aug 2026 01:35:23 +0200 Subject: [PATCH] Something answered on this machine is still bound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A binding was skipped when the provider turned out to be on the same node, reasoning that a file saying "it is on this node" is a fact nobody needs. That is right about the location and wrong about everything beside it: a binding also carries what the provider said a consumer must know, which is the port, and a consumer cannot invent that. A build machine sharing a node with the registry it pushes to sat in a loop saying it could not read its own binding. Nothing was wrong with the machine, the module, the credential or the provision — the file was never written, and the absence looked exactly like a mistake in the module. The original intent is kept where it was right: a provision whose provider said nothing a consumer must know is still not written. A shell is answered here and there is nothing to say about it. A registry is answered here and the port is still unguessable. The address is this machine's name on the private network, or loopback when it has none — a machine off the network still reaches itself, and a name nothing resolves is worse than an address that always works. --- internal/catalogue/declaration.go | 45 +++++++++++++++++-- internal/catalogue/filtering_test.go | 65 ++++++++++++++++++++++++++++ internal/catalogue/resolve.go | 6 ++- 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index 4a272f1..6804040 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -193,10 +193,22 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) { } } if found == nil { - // Bound to something answered on this machine rather than from the mesh. Nothing - // to write: the answer is here, and a file saying "it is on this node" would be - // a fact nobody needs and one more thing to keep true. - continue + // Answered on this same machine rather than from elsewhere in the mesh. + // + // **Still written.** It used to be skipped, reasoning that "it is on this node" + // is a fact nobody needs — and that is true of the *location* and false of + // everything beside it. A binding also carries what the provider said a consumer + // must know, which is the port; a consumer cannot invent that, and got an absent + // file with no explanation. A build machine sharing a node with the registry it + // pushes to sat in a loop saying it could not read its own binding. + here := here(r, to) + if here == nil { + // Nothing in this node's set offers it either, so there is genuinely nothing + // to say. Resolution has already refused anything unanswerable, so this is a + // requirement met by the module itself. + continue + } + found = here } file, err := boundFile(*found, m.Binds[to]) if err != nil { @@ -429,3 +441,28 @@ func (r Resolution) ContributionsTo(requirement string, settings SettingsBy) ( } return given[0].From, given[0].Values, nil } + +// here is the module on this same machine that answers a requirement, as a binding. +// +// **Only when the provider said something a consumer must know.** That is the line: the original +// reasoning — a file saying "it is on this node" is a fact nobody needs — is right about the +// location and wrong about everything beside it. A shell is answered here and there is nothing to +// say about it. A registry is answered here and the consumer still cannot guess the port. +// +// The address is this machine's own name on the private network when it has one, and loopback +// when it does not — a machine not on the network still reaches itself, and naming it by a name +// nothing resolves would be worse than naming it by an address that always works. +func here(r Resolution, requirement string) *Needed { + for _, m := range r.Modules { + serves, said := m.Serves[requirement] + if !said || len(serves) == 0 { + continue + } + at := r.At + if at == "" { + at = "127.0.0.1" + } + return &Needed{Name: requirement, From: r.Node, At: at, Serves: serves} + } + return nil +} diff --git a/internal/catalogue/filtering_test.go b/internal/catalogue/filtering_test.go index 7e17ce2..4a99c07 100644 --- a/internal/catalogue/filtering_test.go +++ b/internal/catalogue/filtering_test.go @@ -299,3 +299,68 @@ func (computedOnce) Resources(string) ([]map[string]any, bool, error) { return []map[string]any{{"id": "wg", "type": "file", "path": "/etc/wireguard/wg0.conf", "content": "x", "mode": "0600"}}, true, nil } + +// A consumer bound to something answered on its own machine still has to be told where it is. +// +// The location is a fact nobody needs — "it is here" — and everything beside it is not: the +// binding carries what the provider said a consumer must know, which is the port. A build machine +// sharing a node with the registry it pushes to sat in a loop saying it could not read its own +// binding, because none was written. +func TestSomethingAnsweredOnThisMachineIsStillBound(t *testing.T) { + r := Resolution{ + Node: "anchor", At: "anchor.internal", + Modules: []Manifest{ + {Module: "registry", Provides: []Offer{{Name: "artifact-store", Scope: ScopeMesh}}, + Serves: map[string]map[string]any{"artifact-store": {"port": 5000}}}, + {Module: "builder", Requires: []string{"artifact-store"}, + Binds: map[string]string{"artifact-store": "/var/lib/builder/store.json"}}, + }, + } + out, err := r.Declaration(Rendering{}) + if err != nil { + t.Fatalf("declaration: %v", err) + } + var content string + for _, res := range out { + if res["path"] == "/var/lib/builder/store.json" { + content, _ = res["content"].(string) + } + } + if content == "" { + t.Fatal("nothing was written, so the consumer cannot find a provider on its own machine") + } + if !strings.Contains(content, "5000") { + t.Fatalf("the binding does not carry the port, which is the part nobody can guess:\n%s", content) + } + if !strings.Contains(content, "anchor.internal") { + t.Fatalf("the binding does not say where to reach it:\n%s", content) + } +} + +// And a machine not on the private network still reaches itself. +func TestAMachineOffTheNetworkIsBoundToItselfByAnAddressThatWorks(t *testing.T) { + r := Resolution{ + Node: "alone", + Modules: []Manifest{ + {Module: "registry", Provides: []Offer{{Name: "artifact-store", Scope: ScopeMesh}}, + Serves: map[string]map[string]any{"artifact-store": {"port": 5000}}}, + {Module: "builder", Requires: []string{"artifact-store"}, + Binds: map[string]string{"artifact-store": "/var/lib/builder/store.json"}}, + }, + } + out, err := r.Declaration(Rendering{}) + if err != nil { + t.Fatalf("declaration: %v", err) + } + for _, res := range out { + if res["path"] != "/var/lib/builder/store.json" { + continue + } + content, _ := res["content"].(string) + if !strings.Contains(content, "127.0.0.1") { + t.Fatalf("bound by a name nothing on this machine resolves:\n%s", content) + } + return + } + t.Fatal("nothing was written") +} diff --git a/internal/catalogue/resolve.go b/internal/catalogue/resolve.go index 5f87950..4b3201c 100644 --- a/internal/catalogue/resolve.go +++ b/internal/catalogue/resolve.go @@ -74,6 +74,10 @@ type Held struct { type Resolution struct { // Node is which machine this was resolved for, so a generator can be asked about it. Node string + // At is this machine's own name on the private network, empty if it is not on one. Kept + // because a consumer bound to something answered on this same machine still has to be told + // where it is — the answer being local does not make the port guessable. + At string // Modules in the order they were resolved: assigned first, then what they pulled in. Modules []Manifest @@ -320,7 +324,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world } } - resolution := Resolution{Node: node.Name, Because: because, Needs: needs} + resolution := Resolution{Node: node.Name, At: node.At, Because: because, Needs: needs} for _, n := range order { resolution.Modules = append(resolution.Modules, catalogue[n]) }