From e4da83496f2da3bd5e5c9e1a30cfacaf3c2eada7 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:31:06 +0200 Subject: [PATCH 1/4] A run-once step may name what it reads: the pair run-once + restart-on is no longer refused (novox/hq ADR 0099) --- internal/catalogue/manifest.go | 18 +++++------------- internal/catalogue/runonce_test.go | 15 +++++++-------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go index 9f37ebc..601c647 100644 --- a/internal/catalogue/manifest.go +++ b/internal/catalogue/manifest.go @@ -985,11 +985,11 @@ func ParseManifest(raw []byte) (Manifest, error) { } // **A run-once container is a step the host runs to completion** (novox/hq ADR 0052). It is a // boolean modifier on the container shape — the host runs the container, requires it to exit 0, - // and starts whatever the declaration places after it only once it has. Two things are refused - // here rather than only on the machine, for the same near-versus-far reason the action ban - // above records: a value that is not a boolean, and the pair run-once + restart-on, which asks - // for two contradictory lifecycles — restart-on brings a *running* container back, and a - // run-once step does not stay running. + // and starts whatever the declaration places after it only once it has. A value that is not a + // boolean is refused here rather than only on the machine, for the same near-versus-far reason + // the action ban above records. A run-once step may name what it reads under restart-on: for a + // step the word means *run again* when one of those changed, which is how a fact fetched from a + // provider is fetched again when the provider moved (novox/hq ADR 0099). for _, r := range m.Resources { if fmt.Sprint(r["type"]) != "container" { continue @@ -1003,14 +1003,6 @@ func ParseManifest(raw []byte) (Manifest, error) { m.Module, r["id"], raw)) } else { runOnce = once - if once { - if _, hasRestart := r["restart-on"]; hasRestart { - problems = append(problems, fmt.Sprintf( - "%s declares %v as run-once and with restart-on; a run-once step runs to "+ - "completion rather than staying running to be restarted (novox/hq ADR 0052)", - m.Module, r["id"])) - } - } } } // **A scheduled container runs on a recurring cadence** (novox/hq ADR 0053), the recurring diff --git a/internal/catalogue/runonce_test.go b/internal/catalogue/runonce_test.go index e7c9fb4..9b17a58 100644 --- a/internal/catalogue/runonce_test.go +++ b/internal/catalogue/runonce_test.go @@ -77,17 +77,16 @@ func TestARunOnceMustBeABoolean(t *testing.T) { } } -func TestARunOnceContainerCannotAlsoDeclareRestartOn(t *testing.T) { - // restart-on brings a running container back; a run-once step does not stay running. The pair - // is a contradiction, refused at the manifest rather than surfacing far away on the host. +func TestARunOnceStepMayNameWhatItReads(t *testing.T) { + // For a step, restart-on means *run again* when what it reads changed: a gate that fetches a + // provider's root names the binding file it reads, so a provider that moved is fetched again + // (novox/hq ADR 0099). Accepted here, and the host's digest does the rest. digest := "@sha256:" + strings.Repeat("a", 64) - bad := []byte(`{"module":"m","resources":[ + good := []byte(`{"module":"m","resources":[ {"id":"conf","type":"file","path":"/x","content":"y"}, {"id":"seed","type":"container","name":"seed","image":"registry.example/x` + digest + `","run-once":true,"restart-on":["conf"]} ]}`) - if _, err := ParseManifest(bad); err == nil { - t.Error("a run-once container that also declared restart-on was accepted") - } else if !strings.Contains(err.Error(), "restart-on") { - t.Errorf("refused for the wrong reason: %v", err) + if _, err := ParseManifest(good); err != nil { + t.Errorf("a run-once step naming what it reads was refused: %v", err) } } -- 2.54.0 From 35c5c2bb9b9d87b2729a27462ee928bb478599d5 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:31:06 +0200 Subject: [PATCH 2/4] secret accept is refused for a name the module does not declare, and a pair delivery for a requirement or local it has not got (novox/hq issue 078) --- internal/inventory/forget_test.go | 15 ++++-- internal/inventory/operator_test.go | 3 +- internal/inventory/secrets.go | 72 +++++++++++++++++++++++++++++ internal/inventory/secrets_test.go | 69 ++++++++++++++++++++++++--- 4 files changed, 148 insertions(+), 11 deletions(-) diff --git a/internal/inventory/forget_test.go b/internal/inventory/forget_test.go index a8a8ca3..5c675a0 100644 --- a/internal/inventory/forget_test.go +++ b/internal/inventory/forget_test.go @@ -32,7 +32,7 @@ func TestRegisteringAModuleAgainKeepsWhatTheMeshHoldsForIt(t *testing.T) { t.Fatal(err) } m := catalogue.Manifest{Module: "step-ca", Version: "1", - Provides: catalogue.Offers("acme-ca")} + Provides: catalogue.Offers("acme-ca"), OwnSecrets: map[string]string{"password": "/run/password"}} if err := inv.RegisterModule(ctx, m, Source{}); err != nil { t.Fatal(err) } @@ -101,7 +101,7 @@ func TestForgettingAModuleRefusesRatherThanDiscardingWhatTheMeshHolds(t *testing if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil { t.Fatal(err) } - if err := inv.RegisterModule(ctx, manifest("step-ca", nil, nil), Source{}); err != nil { + if err := inv.RegisterModule(ctx, withOwnSecret(manifest("step-ca", nil, nil), "password"), Source{}); err != nil { t.Fatal(err) } if err := inv.SetSettings(ctx, "anchor", "step-ca", map[string]any{"port": 9000}); err != nil { @@ -156,7 +156,7 @@ func TestDiscardingAModuleSaysWhatWentWithIt(t *testing.T) { if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil { t.Fatal(err) } - if err := inv.RegisterModule(ctx, manifest("step-ca", nil, nil), Source{}); err != nil { + if err := inv.RegisterModule(ctx, withOwnSecret(manifest("step-ca", nil, nil), "password"), Source{}); err != nil { t.Fatal(err) } if err := inv.SetSettings(ctx, "", "step-ca", map[string]any{"issuer": "the mesh"}); err != nil { @@ -206,7 +206,7 @@ func TestAModuleStillAssignedRefusesBeforeAnythingAboutWhatItHolds(t *testing.T) if _, err := inv.AddNode(ctx, "anchor"); err != nil { t.Fatal(err) } - if err := inv.RegisterModule(ctx, manifest("step-ca", nil, nil), Source{}); err != nil { + if err := inv.RegisterModule(ctx, withOwnSecret(manifest("step-ca", nil, nil), "password"), Source{}); err != nil { t.Fatal(err) } if err := inv.SetSettings(ctx, "anchor", "step-ca", map[string]any{"a": 1}); err != nil { @@ -224,3 +224,10 @@ func TestAModuleStillAssignedRefusesBeforeAnythingAboutWhatItHolds(t *testing.T) } } } + +// withOwnSecret gives a fixture manifest an own secret, so a delivery to it is one the module +// declares (novox/hq 04-ISSUES/078). +func withOwnSecret(m catalogue.Manifest, name string) catalogue.Manifest { + m.OwnSecrets = map[string]string{name: "/run/" + name} + return m +} diff --git a/internal/inventory/operator_test.go b/internal/inventory/operator_test.go index 9cfe052..dd328ae 100644 --- a/internal/inventory/operator_test.go +++ b/internal/inventory/operator_test.go @@ -13,7 +13,8 @@ import ( // opens exactly the value the node was given. func TestAnOwnSecretIsSealedToTheOperatorToo(t *testing.T) { inv, ctx := twoNodesWithKeys(t) - if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "postgres", Version: "1"}, Source{}); err != nil { + if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "postgres", Version: "1", + OwnSecrets: map[string]string{"superuser": "/run/superuser", "replication": "/run/replication"}}, Source{}); err != nil { t.Fatal(err) } diff --git a/internal/inventory/secrets.go b/internal/inventory/secrets.go index 1364e13..235e929 100644 --- a/internal/inventory/secrets.go +++ b/internal/inventory/secrets.go @@ -4,9 +4,13 @@ import ( "context" "errors" "fmt" + "slices" + "sort" + "strings" "github.com/jackc/pgx/v5" + "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/secrets" ) @@ -137,6 +141,27 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul // what differs is that both ends of the pair are sealed to, and that the record says `accepted` // so a later read never replaces it with a minted one. The plaintext is discarded here. func (i *Inventory) AcceptSecretForPair(ctx context.Context, name, consumer, consumerModule, provider, local, value string) error { + // Refused for a requirement the module does not have, or a local it does not keep under it + // (novox/hq 04-ISSUES/078): the credential would sit in the pair unread. + m, err := i.declared(ctx, consumerModule) + if err != nil { + return err + } + if !slices.Contains(m.Requires, name) { + return fmt.Errorf("%s does not require %q; it requires: %s", consumerModule, name, orNone(m.Requires)) + } + if locals := m.SecretsMany[name]; len(locals) > 0 { + if local == "" { + return fmt.Errorf("%s keeps several secrets for %q; name one with --local: %s", + consumerModule, name, orNone(sortedNames(locals))) + } + if _, kept := locals[local]; !kept { + return fmt.Errorf("%s does not keep %q for %q; it keeps: %s", + consumerModule, local, name, orNone(sortedNames(locals))) + } + } else if local != "" { + return fmt.Errorf("%s keeps one secret for %q, not several; drop --local", consumerModule, name) + } consumerKey, err := i.SealingKeyOf(ctx, consumer) if err != nil { return err @@ -362,6 +387,16 @@ func (i *Inventory) SecretForModule(ctx context.Context, node, module, name stri // Sealed on the way in and the plaintext discarded, exactly as a generated one is — so the only // difference between the two is where the value came from. func (i *Inventory) AcceptSecretForModule(ctx context.Context, node, module, name, value string) error { + // Refused for a name the module does not declare. A value stored under a name nothing reads + // is a delivery that changed nothing and reported success — the shape of failure the mesh + // is built to refuse (novox/hq 04-ISSUES/078). + m, err := i.declared(ctx, module) + if err != nil { + return err + } + if _, own := m.OwnSecrets[name]; !own { + return fmt.Errorf("%s does not declare %q as an own secret; %s", module, name, declaresOwn(m)) + } key, err := i.SealingKeyOf(ctx, node) if err != nil { return err @@ -448,3 +483,40 @@ func localFlag(local string) string { } return " --local " + local } + +// declared is the manifest the mesh holds for a module — what a delivered value is checked +// against, so a delivery for a name the module does not have is refused rather than stored. +func (i *Inventory) declared(ctx context.Context, module string) (catalogue.Manifest, error) { + known, err := i.Catalogue(ctx) + if err != nil { + return catalogue.Manifest{}, err + } + m, ok := known[module] + if !ok { + return catalogue.Manifest{}, fmt.Errorf("%s is not a module the mesh knows; `module add` it first", module) + } + return m, nil +} + +func declaresOwn(m catalogue.Manifest) string { + if len(m.OwnSecrets) == 0 { + return "it declares no own secrets" + } + return "it declares: " + strings.Join(sortedNames(m.OwnSecrets), ", ") +} + +func sortedNames(of map[string]string) []string { + names := make([]string, 0, len(of)) + for name := range of { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func orNone(names []string) string { + if len(names) == 0 { + return "none" + } + return strings.Join(names, ", ") +} diff --git a/internal/inventory/secrets_test.go b/internal/inventory/secrets_test.go index 671f381..ac4942c 100644 --- a/internal/inventory/secrets_test.go +++ b/internal/inventory/secrets_test.go @@ -52,8 +52,10 @@ func twoNodesWithKeys(t *testing.T) (*Inventory, context.Context) { // before it can (novox/hq 04-ISSUES/022). Two of them, because "two consumers on one node" // is the case that key exists for. for _, m := range []string{"gitea", "keycloak"} { - if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"}, - Source{}); err != nil { + // Each requires what a test delivers to it: a pair credential is refused for a + // requirement the module does not have (novox/hq 04-ISSUES/078). + if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1", + Requires: []string{"secret", "postgres-database"}}, Source{}); err != nil { t.Fatal(err) } } @@ -404,8 +406,8 @@ func TestACredentialGoesWhenEitherMachineDoes(t *testing.T) { // secret was delivered — which it was. func TestASecretTheMeshWasGivenIsNotReinventedWhenTheMachineRejoins(t *testing.T) { inv, ctx := twoNodesWithKeys(t) - if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "builder", Version: "1"}, - Source{}); err != nil { + if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "builder", Version: "1", + OwnSecrets: map[string]string{"broker": "/run/broker"}}, Source{}); err != nil { t.Fatal(err) } const url = "amqps://builder:the-password-the-broker-was-told@broker/" @@ -436,8 +438,8 @@ func TestASecretTheMeshWasGivenIsNotReinventedWhenTheMachineRejoins(t *testing.T // that would make the refusal above useless if it were wrong. func TestASecretTheMeshWasGivenSurvivesAnOrdinaryPush(t *testing.T) { inv, ctx := twoNodesWithKeys(t) - if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "builder", Version: "1"}, - Source{}); err != nil { + if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "builder", Version: "1", + OwnSecrets: map[string]string{"broker": "/run/broker"}}, Source{}); err != nil { t.Fatal(err) } if err := inv.AcceptSecretForModule(ctx, "consumer", "builder", "broker", @@ -715,3 +717,58 @@ func TestTheOperatorRecoversEachLocalNameApart(t *testing.T) { t.Fatalf("the export does not name the local names: %v", kept) } } + +// **A delivered secret is accepted only under a name the module declares** (novox/hq +// 04-ISSUES/078). A value stored under a name nothing reads is a delivery that changed nothing +// and reported success; refused, naming what the module does declare. +func TestADeliveredSecretIsRefusedUnderANameTheModuleDoesNotDeclare(t *testing.T) { + inv, ctx := twoNodesWithKeys(t) + if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "step-ca", Version: "1", + OwnSecrets: map[string]string{"password": "/run/password"}}, Source{}); err != nil { + t.Fatal(err) + } + err := inv.AcceptSecretForModule(ctx, "consumer", "step-ca", "root-key", "not-a-key") + if err == nil { + t.Fatal("a secret delivered under a name the module does not declare was accepted") + } + for _, want := range []string{"root-key", "password"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("the refusal does not say %q: %v", want, err) + } + } + if err := inv.AcceptSecretForModule(ctx, "consumer", "step-ca", "password", "hunter2"); err != nil { + t.Fatalf("a secret delivered under a declared name was refused: %v", err) + } + // A module the mesh does not know is said, not stored. + err = inv.AcceptSecretForModule(ctx, "consumer", "nobody", "password", "hunter2") + if err == nil || !strings.Contains(err.Error(), "module add") { + t.Errorf("a delivery to an unknown module was not refused with the remedy: %v", err) + } +} + +func TestADeliveredPairCredentialIsRefusedForARequirementTheModuleDoesNotHave(t *testing.T) { + inv, ctx := twoNodesWithKeys(t) + // gitea requires secret and postgres-database (the fixture); not an object store. + err := inv.AcceptSecretForPair(ctx, "object-store", "consumer", "gitea", "provider", "", "hunter2") + if err == nil || !strings.Contains(err.Error(), "postgres-database") { + t.Fatalf("a pair credential for a requirement the module does not have was not refused naming what it requires: %v", err) + } + // A module keeping several secrets for one requirement (ADR 0094) takes a delivery only + // under one of its locals. + m := catalogue.Manifest{Module: "mailu", Version: "1", Requires: []string{"secret"}, + SecretsMany: map[string]map[string]string{"secret": {"admin": "/run/admin", "api-token": "/run/api-token"}}} + if err := inv.RegisterModule(ctx, m, Source{}); err != nil { + t.Fatal(err) + } + err = inv.AcceptSecretForPair(ctx, "secret", "consumer", "mailu", "provider", "", "hunter2") + if err == nil || !strings.Contains(err.Error(), "--local") { + t.Errorf("a delivery to a module keeping several secrets, with no local named, was not refused: %v", err) + } + err = inv.AcceptSecretForPair(ctx, "secret", "consumer", "mailu", "provider", "secret-key", "hunter2") + if err == nil || !strings.Contains(err.Error(), "api-token") { + t.Errorf("a delivery under a local the module does not keep was not refused naming the ones it keeps: %v", err) + } + if err := inv.AcceptSecretForPair(ctx, "secret", "consumer", "mailu", "provider", "admin", "hunter2"); err != nil { + t.Errorf("a delivery under a kept local was refused: %v", err) + } +} -- 2.54.0 From 66332705cdbad89dc84d30363427ff78388fef33 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:46:28 +0200 Subject: [PATCH 3/4] module issue refuses a module with no broker own secret before making the account (issue 078) --- cmd/mesh-controller/modules.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/mesh-controller/modules.go b/cmd/mesh-controller/modules.go index 95d6c04..ffb6770 100644 --- a/cmd/mesh-controller/modules.go +++ b/cmd/mesh-controller/modules.go @@ -253,6 +253,13 @@ func moduleCommand(ctx context.Context, args []string) error { if !ok { return fmt.Errorf("this mesh knows no module %q; `module add` it first", module) } + // The account is delivered as the module's own secret named broker; a module that declares + // none has nothing to read it with, and an account nothing reads is an orphan on the bus + // (novox/hq 04-ISSUES/078). Said before the account is made, not after. + if _, reads := m.OwnSecrets["broker"]; !reads { + return fmt.Errorf("%s declares no own secret named broker, so there is nowhere to deliver "+ + "an account; a module that speaks on the bus declares \"own-secrets\": {\"broker\": }", module) + } management, err := broker.ManagementFromEnvironment() if err != nil { -- 2.54.0 From 0d1f2a41529e7d44e2cdde374ef21c88497d87c3 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:58:36 +0200 Subject: [PATCH 4/4] Review: module issue's pre-check factored and tested; a pair delivery for a requirement the module keeps no secret for is refused; builder issue's usage says the module comes first --- cmd/mesh-controller/main.go | 3 ++- cmd/mesh-controller/modules.go | 20 ++++++++++++++------ cmd/mesh-controller/modules_test.go | 26 ++++++++++++++++++++++++++ internal/inventory/secrets.go | 3 +++ internal/inventory/secrets_test.go | 19 ++++++++++++++++--- 5 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 cmd/mesh-controller/modules_test.go diff --git a/cmd/mesh-controller/main.go b/cmd/mesh-controller/main.go index 70b6bfb..f03cd59 100644 --- a/cmd/mesh-controller/main.go +++ b/cmd/mesh-controller/main.go @@ -168,7 +168,8 @@ func usage() { build [--ref R] have a build machine build it, and record what came out build --behind build every module the mesh holds older than its source builds [] what has been built lately, and what came of it - builder issue a broker account for a build machine, scoped to build work + builder issue a broker account for a build machine, scoped to build work, + delivered as the builder module's broker secret (module add it first) licence add|list|use|key model access, under the name a person calls it licence manager the node that holds a refreshable licence's refresh token licence refresh mint a new access token and seal it to every holder diff --git a/cmd/mesh-controller/modules.go b/cmd/mesh-controller/modules.go index ffb6770..1d0d72d 100644 --- a/cmd/mesh-controller/modules.go +++ b/cmd/mesh-controller/modules.go @@ -253,12 +253,8 @@ func moduleCommand(ctx context.Context, args []string) error { if !ok { return fmt.Errorf("this mesh knows no module %q; `module add` it first", module) } - // The account is delivered as the module's own secret named broker; a module that declares - // none has nothing to read it with, and an account nothing reads is an orphan on the bus - // (novox/hq 04-ISSUES/078). Said before the account is made, not after. - if _, reads := m.OwnSecrets["broker"]; !reads { - return fmt.Errorf("%s declares no own secret named broker, so there is nowhere to deliver "+ - "an account; a module that speaks on the bus declares \"own-secrets\": {\"broker\": }", module) + if err := mayIssue(m); err != nil { + return err } management, err := broker.ManagementFromEnvironment() @@ -559,3 +555,15 @@ func brokerReachableAt(ctx context.Context, inv *inventory.Inventory, known brok } return net.JoinHostPort(brokerAt, port), nil } + +// mayIssue says whether a module can be given a broker account. The account is delivered as the +// module's own secret named broker; a module that declares none has nothing to read it with, and +// an account nothing reads is an orphan on the bus (novox/hq 04-ISSUES/078). Said before the +// account is made, not after. +func mayIssue(m catalogue.Manifest) error { + if _, reads := m.OwnSecrets["broker"]; !reads { + return fmt.Errorf("%s declares no own secret named broker, so there is nowhere to deliver "+ + "an account; a module that speaks on the bus declares \"own-secrets\": {\"broker\": }", m.Module) + } + return nil +} diff --git a/cmd/mesh-controller/modules_test.go b/cmd/mesh-controller/modules_test.go new file mode 100644 index 0000000..edfc937 --- /dev/null +++ b/cmd/mesh-controller/modules_test.go @@ -0,0 +1,26 @@ +package main + +import ( + "strings" + "testing" + + "github.com/novox/mesh-controller/internal/catalogue" +) + +// `module issue` makes a broker account and delivers it as the module's own secret named broker. +// A module that declares none is refused before the account exists, so the bus never carries an +// account nothing reads (novox/hq 04-ISSUES/078). +func TestAModuleWithNoBrokerSecretCannotBeIssued(t *testing.T) { + err := mayIssue(catalogue.Manifest{Module: "step-ca", OwnSecrets: map[string]string{"password": "/run/password"}}) + if err == nil { + t.Fatal("a module with no broker own secret was issued an account") + } + for _, want := range []string{"step-ca", "broker", "own-secrets"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("the refusal does not say %q: %v", want, err) + } + } + if err := mayIssue(catalogue.Manifest{Module: "redis", OwnSecrets: map[string]string{"broker": "/run/broker"}}); err != nil { + t.Errorf("a module declaring its broker secret was refused: %v", err) + } +} diff --git a/internal/inventory/secrets.go b/internal/inventory/secrets.go index 235e929..ae96644 100644 --- a/internal/inventory/secrets.go +++ b/internal/inventory/secrets.go @@ -159,6 +159,9 @@ func (i *Inventory) AcceptSecretForPair(ctx context.Context, name, consumer, con return fmt.Errorf("%s does not keep %q for %q; it keeps: %s", consumerModule, local, name, orNone(sortedNames(locals))) } + } else if m.Secrets[name] == "" { + return fmt.Errorf("%s requires %q but keeps no secret for it, so a delivered value would sit unread; "+ + "it keeps secrets for: %s", consumerModule, name, orNone(sortedNames(m.Secrets))) } else if local != "" { return fmt.Errorf("%s keeps one secret for %q, not several; drop --local", consumerModule, name) } diff --git a/internal/inventory/secrets_test.go b/internal/inventory/secrets_test.go index ac4942c..6cbcc69 100644 --- a/internal/inventory/secrets_test.go +++ b/internal/inventory/secrets_test.go @@ -55,7 +55,8 @@ func twoNodesWithKeys(t *testing.T) (*Inventory, context.Context) { // Each requires what a test delivers to it: a pair credential is refused for a // requirement the module does not have (novox/hq 04-ISSUES/078). if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1", - Requires: []string{"secret", "postgres-database"}}, Source{}); err != nil { + Requires: []string{"secret", "postgres-database", "object-store"}, + Secrets: map[string]string{"secret": "/run/secret", "postgres-database": "/run/pg"}}, Source{}); err != nil { t.Fatal(err) } } @@ -748,11 +749,23 @@ func TestADeliveredSecretIsRefusedUnderANameTheModuleDoesNotDeclare(t *testing.T func TestADeliveredPairCredentialIsRefusedForARequirementTheModuleDoesNotHave(t *testing.T) { inv, ctx := twoNodesWithKeys(t) - // gitea requires secret and postgres-database (the fixture); not an object store. - err := inv.AcceptSecretForPair(ctx, "object-store", "consumer", "gitea", "provider", "", "hunter2") + // gitea requires secret, postgres-database and object-store (the fixture); it keeps a secret + // for the first two only, and requires no cache at all. + err := inv.AcceptSecretForPair(ctx, "redis-cache", "consumer", "gitea", "provider", "", "hunter2") if err == nil || !strings.Contains(err.Error(), "postgres-database") { t.Fatalf("a pair credential for a requirement the module does not have was not refused naming what it requires: %v", err) } + err = inv.AcceptSecretForPair(ctx, "object-store", "consumer", "gitea", "provider", "", "hunter2") + if err == nil || !strings.Contains(err.Error(), "keeps no secret") { + t.Fatalf("a pair credential for a requirement the module keeps no secret for was not refused: %v", err) + } + err = inv.AcceptSecretForPair(ctx, "secret", "consumer", "gitea", "provider", "extra", "hunter2") + if err == nil || !strings.Contains(err.Error(), "drop --local") { + t.Fatalf("a local named where the module keeps one secret was not refused: %v", err) + } + if err := inv.AcceptSecretForPair(ctx, "secret", "consumer", "gitea", "provider", "", "hunter2"); err != nil { + t.Fatalf("a delivery for a requirement the module keeps one secret for was refused: %v", err) + } // A module keeping several secrets for one requirement (ADR 0094) takes a delivery only // under one of its locals. m := catalogue.Manifest{Module: "mailu", Version: "1", Requires: []string{"secret"}, -- 2.54.0