From 8fa5443862e299114bcaa5da41a0ba20cf1536ea Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 24 Sep 2026 18:54:35 +0200 Subject: [PATCH] contributes: a module's grant carries no value where it contributed several times MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ContributionsFrom settled to whichever of a module's several contributions to one requirement sorted first, arbitrarily — the grant minted for it then carried that contribution's label and port under a credential the OTHER contribution's consumer never sees, and collided with that same contribution's own entry from contributions() besides. Confirmed live: minio's two route contributions (files-api, files) produced three entries in route-adapter's received file — files-api twice, once credentialed and once not, files not credentialed at all. Every single-contribution module (gitea, keycloak, umami) already mints an unused credential for `route` too — route never needs one, by its own documentation — but with exactly one contribution to match there was nothing to collide with, so it never surfaced. Where a module contributes more than once, there is no single value to settle on. The module still asks, still gets its one credential — a pair credential is not a place for a label or a port anyway — and each named contribution reaches the provider on its own, unchanged. No cleanup needed for the secret already minted live for minio+route: the sealed blob is a random pair credential unrelated to Values, which is recomputed fresh on every plan/push regardless. --- internal/catalogue/declaration.go | 19 +++++++- .../catalogue/several_contributions_test.go | 43 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index c0ccc82..5d3c077 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -1120,17 +1120,34 @@ func boundFile(n Needed, path, as string) (map[string]any, error) { // arrangement refused is the ordinary one. A node running eight services against one database is // not an edge case; it is what a machine looks like. Now each consumer has its own credential and // there is nothing left to refuse. +// +// **One credential, even where a module contributes several times.** A module may answer one +// requirement more than once (ADR 0094's sibling for `contributes`) — an object store's data API +// and its console are two different names, not one. There is still only one `Needed` for it, one +// credential minted, one grant to settle: a pair credential is not a place to put a label or a +// port. So where several of this module's contributions reach the same requirement, none of them +// is "the" value — settling to the first, arbitrarily, would hand the grant one contribution's +// values under a credential the OTHER contribution's consumer never sees, and would collide with +// that contribution's own entry from contributions() besides. Empty values, still granted: the +// module asked, gets its credential, and each named contribution reaches the provider on its own. func (r Resolution) ContributionsFrom(requirement, module string, settings SettingsBy) ( map[string]any, bool, error) { all, err := r.contributions(settings, nil, nil) if err != nil { return nil, false, err } + var mine []map[string]any for _, g := range all[requirement] { if g.From == module { - return g.Values, true, nil + mine = append(mine, g.Values) } } + if len(mine) == 1 { + return mine[0], true, nil + } + if len(mine) > 1 { + return map[string]any{}, true, nil + } // It contributes no payload — but a require-only consumer of a parameterless provision (one whose // `serves` names no consumer-supplied key: `redis-cache`, `amqp`) still ASKS for it and must be // granted a credential. Keying "asks" on contributions alone marked those grants withdrawn diff --git a/internal/catalogue/several_contributions_test.go b/internal/catalogue/several_contributions_test.go index 61b04c0..b0fe881 100644 --- a/internal/catalogue/several_contributions_test.go +++ b/internal/catalogue/several_contributions_test.go @@ -122,3 +122,46 @@ func TestASingleRouteStillResolvesTheOrdinaryWay(t *testing.T) { t.Fatalf("the plain shape regressed: %+v", given) } } + +// ContributionsFrom is what mints the ONE pair credential a requiring module is granted +// (cmd/mesh-controller/plan.go's grantsFor) — a separate path from Declaration()'s raw file, and +// the one the two-routes test above never exercised. Where a module contributes several times, +// there is no single "the" value: settling to whichever sorts first would both misrepresent the +// grant and collide with that same contribution's own entry from contributions(), which is +// exactly the duplicate a live plan against minio surfaced (files-api appearing once with a +// credential, once without, while files got neither). +func TestContributionsFromHasNoSingleValueWhenAModuleContributesSeveralTimes(t *testing.T) { + minio, err := ParseManifest([]byte(twoRoutes)) + if err != nil { + t.Fatal(err) + } + got, err := Resolve(shelf(minimalRouteProxy(), minio), []string{"route-proxy", "minio"}, workstation(), World{}) + if err != nil { + t.Fatal(err) + } + values, asks, err := got.ContributionsFrom("route", "minio", nil) + if err != nil { + t.Fatal(err) + } + if !asks { + t.Fatal("minio still requires route, so it still asks") + } + if len(values) != 0 { + t.Fatalf("no single value represents two contributions, got %+v", values) + } +} + +// The ordinary, single-contribution case is unchanged: exactly one match still settles to it. +func TestContributionsFromReturnsTheOneValueForAnOrdinaryContribution(t *testing.T) { + got, err := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), World{}) + if err != nil { + t.Fatal(err) + } + values, asks, err := got.ContributionsFrom("reverse-proxy", "board", nil) + if err != nil { + t.Fatal(err) + } + if !asks || values["host"] != "board" { + t.Fatalf("the ordinary single contribution should still settle to its own value: %+v", values) + } +} -- 2.54.0