diff --git a/internal/compose/resolution/declaration.go b/internal/compose/resolution/declaration.go index 4f4e73a..6f0f165 100644 --- a/internal/compose/resolution/declaration.go +++ b/internal/compose/resolution/declaration.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "fmt" + "maps" + "slices" "sort" "strconv" "strings" @@ -240,7 +242,14 @@ func resolutionCompose(r mcat.Resolution, with mcat.Rendering, owner map[string] } if answered { r.Needs[i].Serves = serves + continue } + // No module of the composed set serves it: its provider is held on this machine (composed apart, as what runs of + // it), and offers what its manifest serves. None of its own names is composed here, so a key that names one is + // left out and said on the need, as a provider elsewhere leaves it out (ServedLeftOut, novox/hq ADR 0346 §3), + // never handed to the consumer as a placeholder its binding then refuses (issue 1921, novox: "mesh-build-node has + // a file that says ${bound:route:name-web}, and mesh-build-node does not require route"). + r.Needs[i].Serves, r.Needs[i].LeftOut = withoutOwnNames(r.Needs[i], r.Node) } // Once, from every module's listens -- not per module. A module receiving only its own ports @@ -2526,3 +2535,26 @@ func resolutionGeneratedHere(r mcat.Resolution, with mcat.Rendering) (map[string } return generated, nil } + +// withoutOwnNames is what a need's provider serves without each key whose value names one of the provider's own composed +// names (${bound:…}), and the need's left-out keys with each such key said: a provider not composed in this set +// composes none of its names here. +func withoutOwnNames(n mcat.Needed, node string) (map[string]any, []string) { + var asking []string + for _, key := range sortedAnyKeys(n.Serves) { + if text, ok := n.Serves[key].(string); ok && bound.MatchString(text) { + asking = append(asking, key) + } + } + if len(asking) == 0 { + return n.Serves, n.LeftOut + } + out := maps.Clone(n.Serves) + said := slices.Clone(n.LeftOut) + for _, key := range asking { + delete(out, key) + said = append(said, fmt.Sprintf("%s serves %s without %q: it names %s, and %s composes no names of %s, which it "+ + "holds (novox/hq ADR 0346)", n.Module, n.Name, key, bound.FindString(n.Serves[key].(string)), orHere(node), n.Module)) + } + return out, said +} diff --git a/internal/compose/resolution/served_own_names_test.go b/internal/compose/resolution/served_own_names_test.go index 7586e93..b29f4b8 100644 --- a/internal/compose/resolution/served_own_names_test.go +++ b/internal/compose/resolution/served_own_names_test.go @@ -1,6 +1,7 @@ package resolution import ( + "slices" "strings" "testing" @@ -75,6 +76,29 @@ func TestAConsumerBesideTheProviderIsServedItsName(t *testing.T) { if strings.Contains(binding, "${bound:") { t.Fatalf("a placeholder reached the consumer:\n%s", binding) } + // Issue 1921 (the group check of 8a80e8e86858, novox): the provider held on this machine (composed apart, as what runs + // of it) is not in the composed set, so none of its own names is composed here. The consumer beside it is told + // what it serves without the keys that name them, each said on the need, never the placeholder: its binding refused + // the consumer with "has a file that says ${bound:route:name-web}, and lab does not require route". + held, err := Resolve(shelf(minimalRouteProxy(), m, consumer), []string{"route-proxy", "forge", "lab"}, node, mcat.World{}) + if err != nil { + t.Fatal(err) + } + held.Modules = slices.DeleteFunc(slices.Clone(held.Modules), func(m mcat.Manifest) bool { return m.Module == "forge" }) + out, err = Declaration(held, mcat.Rendering{}) + if err != nil { + t.Fatalf("a consumer beside a held provider: %v", err) + } + for _, r := range out { + if c, _ := r["content"].(string); r["path"] == "/var/lib/lab/package-registry.json" && (strings.Contains(c, "${bound:") || !strings.Contains(c, "npm-path")) { + t.Fatalf("the binding beside a held provider:\n%s", c) + } + } + for _, n := range held.Needs { + if n.Name == "npm-package-registry" && (len(n.LeftOut) != 1 || !strings.Contains(n.LeftOut[0], `"public-root"`)) { + t.Fatalf("the need beside a held provider says not what was left out: %+v", n) + } + } } // From another machine, the provider's served values carry what that machine composed, and a key it