From fda81c129eeaa2c2f829cd75b662ca80b7f6432f Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 12:17:27 +0100 Subject: [PATCH] Key each provision with the plan entry it is composed from mesh-provisioner keys provisions from a consumer's wanted and newest builds but writes the wanted build's plan alone, so a requirement the newest build adds was never bound. Each key now carries its plan entry, and a key ahead of its want says in its why that no plan names it yet and its provider binds it from the entry (issue 1126 review 1). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../cmd/mesh-provisioner/holder.go | 27 ++++++++++++++++--- .../cmd/mesh-provisioner/holder_test.go | 24 +++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go b/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go index a15e1abc..c1ddc169 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go @@ -194,14 +194,20 @@ func (h *Holder) ComposeNode(ctx context.Context, node string) (out Composed, er lap("plans written") needed := map[string]provisions.Provision{} elsewhere := map[string]provisions.Provision{} + // ahead are the keys only the newest build of their consumer requires: the plan written, its wanted build's, names + // them not yet, and the provider binds them from the entry they carry (issue 1126). + ahead := map[string]string{} vaultNode := h.vaultNode() - for _, plans := range []map[string]catalogue.Plan{atWants, newest} { + for i, plans := range []map[string]catalogue.Plan{atWants, newest} { for module, plan := range plans { for _, p := range plan.Provisions { consumer := provisions.End{Module: module, Node: node} provider := provisions.End{Module: p.ProviderModule, Node: p.Provider} + // The key carries the plan entry it is composed from (issue 1126): the wanted build's where its plan names + // it, the newest's otherwise, so its provider binds it whichever plan the seat keeps. + entry := p pv := provisions.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider, - DependsOn: provisions.DependsOn(consumer, p, h.providerPlan(p, node, atWants))} + DependsOn: provisions.DependsOn(consumer, p, h.providerPlan(p, node, atWants)), Plan: &entry} key, err := pv.Key() if err != nil { out.Problems = append(out.Problems, err.Error()) @@ -209,6 +215,9 @@ func (h *Holder) ComposeNode(ctx context.Context, node string) (out Composed, er } if _, seen := needed[key]; !seen { needed[key] = pv + if i == 1 { + ahead[key] = module + } } // Its credential is a provision of its own, the vault's (clause 6): on the consumer's node for a value of the // consumer's own, on the provider's for the provider's one shared value. @@ -237,7 +246,7 @@ func (h *Holder) ComposeNode(ctx context.Context, node string) (out Composed, er } } h.judgeElsewhere(elsewhere, &out) - h.judgeNode(node, needed, heldAtWants, &out) + h.judgeNode(node, needed, ahead, heldAtWants, &out) lap("provisions judged") return out, nil } @@ -468,7 +477,8 @@ func (h *Holder) writePlans(node string, plans map[string]catalogue.Plan, held m // judgeNode brings the node's provision keys to what is needed and what each provider said. A key of a module held on // the node (heldModules) that its composition no longer names is left as it is: not composed is not unneeded. -func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision, heldModules map[string]string, out *Composed) { +func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision, ahead map[string]string, + heldModules map[string]string, out *Composed) { held := h.held(node) for key, have := range held { if _, isNeeded := needed[key]; !isNeeded && have.Consumer.Node == node { @@ -497,6 +507,15 @@ func (h *Holder) judgeNode(node string, needed map[string]provisions.Provision, next.Reference = have.Reference } h.outcome(key, &next) + if module, isAhead := ahead[key]; isAhead && next.State == provisions.Asked { + // The reason the provider's helper says too: no plan names it yet, and it binds it from this key's entry. + why := "no plan names it yet: the newest build of " + module + " requires it and its wanted build does " + + "not, so its provider binds it from this key's plan entry" + if next.Why != "" { + why += "; " + next.Why + } + next.Why = why + } case have.State == provisions.Retiring: next = have if next.Ask == "" && next.Warrant == nil { diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go b/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go index 31638dc1..30a7a8bc 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go @@ -540,3 +540,27 @@ func TestAProvidersOutcomeIsReadPerOffer(t *testing.T) { } } } + +// Issue 1126 review 1 (mcp-endpoint asked on ace and shanks, 2026-11-02 11:56): a requirement the consumer's newest build +// adds is keyed before its want, and the plan the seat keeps is its wanted build's, which names it not; the key carries +// the plan entry it was composed from, so the provider binds it, and its why says no plan names it yet. A key its +// wanted plan names carries that plan's entry. +func TestAKeyAheadOfItsWantCarriesItsPlanEntryAndSaysWhy(t *testing.T) { + m := newMesh() + m.atWant["g14"] = map[string]catalogue.Plan{"gitea": planOf(pgDB)} + ahead := oidc + ahead.As = "gitea-g14" + m.atNewest["g14"] = map[string]catalogue.Plan{"gitea": planOf(pgDB, ahead)} + m.compose(t, "g14") + kc := m.state(t, "g14.gitea.oidc-client") + if kc.Plan == nil || kc.Plan.Provision != "oidc-client" || kc.Plan.ProviderModule != "keycloak" || kc.Plan.As != "gitea-g14" { + t.Errorf("the key ahead of its want carries %+v", kc.Plan) + } + if kc.State != provisions.Asked || !strings.Contains(kc.Why, "no plan names it") { + t.Errorf("the key ahead of its want: %s %q", kc.State, kc.Why) + } + if pg := m.state(t, "g14.gitea.postgres-database"); pg.Plan == nil || pg.Plan.ProviderModule != "postgres" || + strings.Contains(pg.Why, "no plan names it") { + t.Errorf("the planned key: %+v", pg) + } +}