diff --git a/internal/catalogue/resolve.go b/internal/catalogue/resolve.go index b92f350..55c00b2 100644 --- a/internal/catalogue/resolve.go +++ b/internal/catalogue/resolve.go @@ -287,8 +287,19 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world if satisfied[want] && !isModule(catalogue, want) { if brokered[want] { // Answered here, and still a need: the provider is this node. + // + // **The same loopback fallback as the branch below, and it was missing here.** A + // machine with no private network has no `at`, and this branch passed that through + // — so a consumer whose file says `${bound::at}:${bound:...:port}` was + // handed `:5000`, a name with no host, written into its environment without + // complaint. The sibling case a few lines down had the fallback and the reasoning + // for it; only this one did not. A machine off the network still reaches itself. + at := node.At + if at == "" { + at = "127.0.0.1" + } needs = append(needs, Needed{ - Name: want, From: node.Name, At: node.At, + Name: want, From: node.Name, At: at, Serves: servedHere(catalogue, chosen, want), For: because[want]}) } else if served := servedHere(catalogue, chosen, want); len(served) > 0 { // Answered here with no credential to mint, but the provider serves facts the diff --git a/internal/catalogue/resolve_loopback_test.go b/internal/catalogue/resolve_loopback_test.go new file mode 100644 index 0000000..d9ca22c --- /dev/null +++ b/internal/catalogue/resolve_loopback_test.go @@ -0,0 +1,52 @@ +package catalogue + +import "testing" + +// A MESH-SCOPED provider on the consumer's own node must deliver a usable address too. +// +// The sibling case — a node-scoped provider minting no credential — has had this fallback and a +// test for it for some time. This branch did not, and the difference is invisible until something +// puts the address into a string: the mesh's own artifact store is mesh-scoped and lives on the +// same machine as the builder that pushes to it, so the builder's environment was written as +// `MESH_REGISTRY=:5000`. Nothing refused it. The runtime did, several steps later, with +// `":5000/mesh-tools/build" is not a valid repository/tag` — a message about a tag, for a fault in +// how a binding was resolved. +// +// A machine off the private network still reaches itself. +func TestAMeshScopedProviderOnThisNodeStillCarriesAnAddress(t *testing.T) { + provider := Manifest{Module: "registry", + Provides: []Offer{{Name: "artifact-store", Scope: ScopeMesh}}, + Serves: map[string]map[string]any{ + "artifact-store": {"port": 5000}}} + consumer := Manifest{Module: "builder", Requires: []string{"artifact-store"}} + + // No `At`: a mesh with no private network raised, which is every mesh before somebody places + // its nodes on one — including the moment just after it is installed. + got, err := Resolve( + map[string]Manifest{"registry": provider, "builder": consumer}, + []string{"builder", "registry"}, + Node{Name: "anchor"}, + World{}) + if err != nil { + t.Fatalf("a mesh-scoped provision answered on this very node was refused: %v", err) + } + + var found *Needed + for i := range got.Needs { + if got.Needs[i].Name == "artifact-store" && got.Needs[i].For == "builder" { + found = &got.Needs[i] + } + } + if found == nil { + t.Fatalf("the store was not delivered to the builder at all: %+v", got.Needs) + } + if found.At == "" { + t.Fatalf("the binding carries no address, so anything composing a host from it gets none: %+v", found) + } + if found.At != "127.0.0.1" { + t.Fatalf("off the private network the address must fall back to loopback, got %q", found.At) + } + if got, want := plainly(found.Serves["port"]), "5000"; got != want { + t.Fatalf("the port was not delivered: got %q want %q", got, want) + } +}