Key a provision's credential as the vault's own provision secret-<provision> 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)
This commit is contained in:
@@ -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-<provision>`.
|
||||
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-<provision>` (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 {
|
||||
|
||||
@@ -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): `<consumer node>.<consumer>.secret-<provision>[.<local>]` for a value of the
|
||||
// consumer's own (pair, issued, given), and `<provider node>.<provider>.secret-<provision>` 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"`
|
||||
|
||||
@@ -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-<provision>`, 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user