diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go index 43cc4dfd..34caced4 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/plan.go @@ -11,6 +11,8 @@ import ( "slices" "strconv" "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // The provisioner's plan of a module on this node (novox/hq ADR 0349 clause 4, ADR 0570 clause 2): the seat @@ -90,7 +92,7 @@ type PlannedProvision struct { Provider string `json:"provider"` ProviderModule string `json:"provider-module,omitempty"` // CredentialForm is the form the provider's offer declares for its credential (issue 768), which the vault acts - // on and this client only reads: one of credentialForms, or none from a controller before it. + // on and this client only reads: one of the SDK's catalogue.CredentialForms, or empty from a controller before it. CredentialForm string `json:"credential-form,omitempty"` Path string `json:"path,omitempty"` // Contributes is what this module tells the provider about itself for this provision (a database's name, its @@ -110,12 +112,12 @@ type PlannedProvision struct { // issuedForHolder is whether the entry names a value its provider's software issued for this holder alone (issue // 757). An entry of the login-per-consumer form that names a file (issue 779) is still the bind it was: its provider -// lists this consumer and its bound values are read from it. -func (p PlannedProvision) issuedForHolder() bool { return p.Name != "" && p.CredentialForm != "pair" } - -// credentialForms are the forms a provider's offer may declare for its credential (mesh-vault pair.go): one value -// every holder shares, a login per consumer, a value its software issues, or one a person gives. -var credentialForms = []string{"shared", "pair", "issued", "given"} +// lists this consumer and its bound values are read from it. So is an entry of an offer with no credential (form +// none, issue 774): the vault holds nothing for it, and its bound values are the provider's entry. +func (p PlannedProvision) issuedForHolder() bool { + return p.Name != "" && p.CredentialForm != catalogue.CredentialFormPair && + p.CredentialForm != catalogue.CredentialFormNone +} // PlannedSecret is one secret of the module's own, by its name in the module's manifest; Path, when set, is the // file holding it alone (the manifest's `own-secrets`), which the client fills with its value and nothing else. @@ -266,9 +268,10 @@ func (p Plan) problems() []string { if !nodeShape.MatchString(pr.Provider) { out = append(out, fmt.Sprintf("%s: provider %q is not a machine's name", which, pr.Provider)) } - if pr.CredentialForm != "" && !slices.Contains(credentialForms, pr.CredentialForm) { + // The SDK's one list of forms (issue 774): a copy kept here once lacked "none" and refused every such plan. + if pr.CredentialForm != "" && !catalogue.IsCredentialForm(pr.CredentialForm) { out = append(out, fmt.Sprintf("%s: credential-form %q is not %s", which, pr.CredentialForm, - strings.Join(credentialForms, ", "))) + strings.Join(catalogue.CredentialForms(), ", "))) } if key := [2]string{pr.Provision, pr.Local}; bound[key] { out = append(out, fmt.Sprintf("%s: its provider is named twice", which)) diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/providers_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/providers_test.go index 4841a24c..0d55f2f8 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/providers_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/providers_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "reflect" "strings" "testing" @@ -63,7 +64,9 @@ func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) { `{"provision":"sonarr-api","provider":"ace","provider-module":"sonarr","credential-form":"shared"},` + `{"name":"forge-read","provision":"forge-read","provider":"novox","provider-module":"forge",` + `"credential-form":"issued","path":"/var/lib/app/forge.secret"},` + - `{"provision":"smtp-relay","provider":"ace","provider-module":"relay","credential-form":"given"}]}}` + `{"provision":"smtp-relay","provider":"ace","provider-module":"relay","credential-form":"given"},` + + // An offer with no credential (issue 774), as design 47's catalogue declares gitea's forge-account. + `{"provision":"forge-account","provider":"novox","provider-module":"gitea","credential-form":"none"}]}}` _, p, err := planOf("g14", "g14.app", []byte(value)) if err != nil { t.Fatal(err) @@ -72,9 +75,29 @@ func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) { for _, pr := range p.Provisions { forms = append(forms, pr.CredentialForm) } - if strings.Join(forms, ",") != "pair,shared,issued,given" { + if strings.Join(forms, ",") != "pair,shared,issued,given,none" { t.Fatalf("the credential forms are read as %v", forms) } + // A none entry places no file of its own and is never a holder's issued value: its bound values are read from + // its provider's entry, and the plan's files are placed as they are. + targets, fail := fillTargets(p, map[string]string{"app.conf": "x"}) + var paths []string + for _, f := range targets { + paths = append(paths, f.Path) + } + if fail != "" || strings.Join(paths, ",") != "/etc/app.conf,/var/lib/app/forge.secret" { + t.Fatalf("the plan places %v (%s)", paths, fail) + } + if none := (PlannedProvision{Name: "forge-account", CredentialForm: "none"}); none.issuedForHolder() { + t.Fatal("a credential of an offer with none is read as a holder's issued value") + } + // A template naming such a credential fails loudly, and the vault, which mints nothing for it, is never asked. + named := Plan{Provisions: []PlannedProvision{{Name: "forge-account", Provision: "forge-account", Provider: "novox", + CredentialForm: "none"}}} + if _, wait, fail := (&Client{Node: "g14"}).secretOf(context.Background(), nil, "app", named, + Need{Kind: "secret", Name: "forge-account"}); wait != "" || !strings.Contains(fail, "no credential (form none)") { + t.Fatalf("a secret of an offer with none: wait %q, fail %q", wait, fail) + } bad := strings.Replace(value, `"credential-form":"pair"`, `"credential-form":"one-for-all"`, 1) if _, _, err := planOf("g14", "g14.app", []byte(bad)); err == nil || !strings.Contains(err.Error(), "one-for-all") { t.Fatalf("a credential form the client does not know was taken: %v", err) diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/secrets.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/secrets.go index c8c4149f..530c9a0b 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/secrets.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/secrets.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/seal" "github.com/nats-io/nats.go" ) @@ -171,6 +172,11 @@ func (c *Client) secretOf(ctx context.Context, s *state, module string, p Plan, } else if i := slices.IndexFunc(p.Provisions, func(pr PlannedProvision) bool { return pr.Name == n.Name }); i >= 0 { // A credential of what the module requires, issued for this module on this machine (issue 757). pr := p.Provisions[i] + if pr.CredentialForm == catalogue.CredentialFormNone { + // An offer with no credential (issue 774): the vault mints nothing, so asking it would wait for ever. + return "", "", fmt.Sprintf("%s: the offer of %s by %s has no credential (form none), so there is no "+ + "secret %s to fill", n, pr.Provision, pr.Provider, n.Name) + } name = HolderName(pr.Provider, pr.Provision, c.Node, module) if pr.CredentialForm == "pair" { // This consumer's own login from a provider that gives each consumer one (issue 779, ADR 0158 ยง11). diff --git a/modules/mesh-provisioner-client/go.mod b/modules/mesh-provisioner-client/go.mod index cff5e3a4..be4b1a22 100644 --- a/modules/mesh-provisioner-client/go.mod +++ b/modules/mesh-provisioner-client/go.mod @@ -3,7 +3,7 @@ module mesh-provisioner-client go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030150314-9379e9a96ad4 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/sys v0.48.0 diff --git a/modules/mesh-provisioner-client/go.sum b/modules/mesh-provisioner-client/go.sum index a2b8e018..ed602ece 100644 --- a/modules/mesh-provisioner-client/go.sum +++ b/modules/mesh-provisioner-client/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030150314-9379e9a96ad4 h1:CPUqC3FS/ezvBYdhVgVdsQg2DdN8xgtROQkSaHRKeOI= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030150314-9379e9a96ad4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=