From f701db05162ce664191c710023797aed77a25f94 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 22:43:56 +0100 Subject: [PATCH] Key a provision's credential as the vault's own provision secret- and say what a provision depends on, so a provider waits for its credential and its own requirements (issue 1954, ADR 0570 clauses 3, 6) --- go/catalogue/manifest.go | 10 +++++++ go/provisions/provisions.go | 48 ++++++++++++++++++++++++++++++++ go/provisions/provisions_test.go | 24 ++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index 1423d11..0be481b 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -1775,6 +1775,10 @@ func (m Manifest) marshalFields() ([]byte, error) { return json.Marshal(keys) } +// CredentialProvisionPrefix starts the name of a provision's credential, the vault's provision beside it (novox/hq ADR +// 0570 clause 6): `secret-`. +const CredentialProvisionPrefix = "secret-" + // SecretFile is one file a module is given a credential in: the local name it goes by inside // the module (empty for the ordinary one-file case, where the requirement's name serves) and where. type SecretFile struct { @@ -1937,6 +1941,12 @@ func parseManifest(raw []byte, predefined PredefinedList) (Manifest, error) { if !name.MatchString(p) { problems = append(problems, fmt.Sprintf("%q is not a usable name to provide", p)) } + // A provision's credential is a provision of its own, `secret-` (novox/hq ADR 0570 clause 6, the + // lead 2026-10-29): no module provides a name of that form, so the two keys never meet. + if strings.HasPrefix(p, CredentialProvisionPrefix) { + problems = append(problems, fmt.Sprintf("%s provides %q; a name starting %q is the mesh's for a provision's "+ + "credential, kept by the vault (novox/hq ADR 0570)", m.Module, p, CredentialProvisionPrefix)) + } // A bound says what keeps the name, so the refusal it causes can say it (ADR 0225); and it // leaves room for the shortest identity the mesh makes, or it would refuse every consumer. if id := offer.Identity; id != nil && !id.None { diff --git a/go/provisions/provisions.go b/go/provisions/provisions.go index 18e1f17..a46753d 100644 --- a/go/provisions/provisions.go +++ b/go/provisions/provisions.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "regexp" + "sort" "strings" "time" @@ -121,6 +122,53 @@ func VaultName(form string, p Provision) string { return "" } +// VaultModule is the provider of every provision's credential (novox/hq ADR 0570 clauses 1, 6). +const VaultModule = "mesh-vault" + +// CredentialKey is the key of a provision's credential, itself a provision whose provider is the vault (clause 6; the +// key form the lead settled on 2026-10-29): `..secret-[.]` for a value of the +// consumer's own (pair, issued, given), and `..secret-` for the provider's one +// shared value. False for a form with no vault value. +func CredentialKey(form string, consumer, provider End, provision, local string) (string, bool) { + name := catalogue.CredentialProvisionPrefix + provision + switch form { + case FormPair, FormIssued, FormGiven: + k, err := Key(consumer.Node, consumer.Module, name, local) + return k, err == nil + case FormShared: + k, err := Key(provider.Node, provider.Module, name, "") + return k, err == nil + } + return "", false +} + +// DependsOn is what a provision waits for before its provider makes it (clause 3): its credential's key, where its form +// has a vault value, and the key of every provision its provider module itself requires on its own node, read from +// the provider's plan there (nil when the provider is not handed and has none). Sorted, none twice. +func DependsOn(consumer End, entry catalogue.PlanProvision, providerPlan *catalogue.Plan) []string { + seen := map[string]bool{} + var out []string + add := func(k string) { + if k != "" && !seen[k] { + seen[k] = true + out = append(out, k) + } + } + provider := End{Module: entry.ProviderModule, Node: entry.Provider} + if k, ok := CredentialKey(entry.CredentialForm, consumer, provider, entry.Provision, entry.Local); ok { + add(k) + } + if providerPlan != nil { + for _, own := range providerPlan.Provisions { + if k, err := Key(provider.Node, provider.Module, own.Provision, own.Local); err == nil { + add(k) + } + } + } + sort.Strings(out) + return out +} + // End is one end of a provision: a module on a node. type End struct { Module string `json:"module"` diff --git a/go/provisions/provisions_test.go b/go/provisions/provisions_test.go index c720f42..de95a80 100644 --- a/go/provisions/provisions_test.go +++ b/go/provisions/provisions_test.go @@ -82,6 +82,30 @@ func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) { if got := VaultName(FormPair, Provision{Provision: "p", Local: "r", Provider: End{Node: "a"}, Consumer: End{Node: "b", Module: "m"}}); got != "pair/a/p/b/m/r" { t.Errorf("a local name's pair is %q", got) } + // A provision waits for its credential, the vault's provision `secret-`, and for what its provider itself + // requires on its node (clause 3; the key form the lead settled on 2026-10-29). + keycloak, postgres := End{Module: "keycloak", Node: "novox"}, End{Module: "postgres", Node: "ace"} + providerPlan := &catalogue.Plan{Provisions: []catalogue.PlanProvision{{Provision: "backup-target", Provider: "ace", ProviderModule: "restic"}}} + for _, c := range []struct { + form string + plan *catalogue.Plan + want string + }{ + {FormPair, nil, "novox.keycloak.secret-postgres-database"}, + {FormShared, nil, "ace.postgres.secret-postgres-database"}, + {FormPair, providerPlan, "ace.postgres.backup-target,novox.keycloak.secret-postgres-database"}, + {"", nil, ""}, + } { + entry := catalogue.PlanProvision{Provision: "postgres-database", Provider: postgres.Node, ProviderModule: postgres.Module, + CredentialForm: c.form} + if got := strings.Join(DependsOn(keycloak, entry, c.plan), ","); got != c.want { + t.Errorf("%q depends on %q, want %q", c.form, got, c.want) + } + } + if _, err := catalogue.ParseManifest([]byte(`{"module":"x","version":"1","provides":[{"name":"secret-db","scope":"mesh"}]}`)); err == nil || + !strings.Contains(err.Error(), "secret-") { + t.Errorf("a provision named secret-*: %v", err) + } if ProvisionsBucket != "mesh-provisioner_provisions" || PlansRead != "mesh-provisioner.plans" { t.Errorf("the states are %s and %s", ProvisionsBucket, PlansRead) }