Merge pull request 'The provisioner client accepts credential form none through the mesh-sdk list (issue 774)' (#564) from fix/credential-forms-one-list into main

Reviewed-on: #564
This commit was merged in pull request #564.
This commit is contained in:
2026-10-30 15:10:28 +00:00
5 changed files with 46 additions and 14 deletions
@@ -11,6 +11,8 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "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 // 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"` Provider string `json:"provider"`
ProviderModule string `json:"provider-module,omitempty"` ProviderModule string `json:"provider-module,omitempty"`
// CredentialForm is the form the provider's offer declares for its credential (issue 768), which the vault acts // 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"` CredentialForm string `json:"credential-form,omitempty"`
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
// Contributes is what this module tells the provider about itself for this provision (a database's name, its // 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 // 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 // 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. // lists this consumer and its bound values are read from it. So is an entry of an offer with no credential (form
func (p PlannedProvision) issuedForHolder() bool { return p.Name != "" && p.CredentialForm != "pair" } // none, issue 774): the vault holds nothing for it, and its bound values are the provider's entry.
func (p PlannedProvision) issuedForHolder() bool {
// credentialForms are the forms a provider's offer may declare for its credential (mesh-vault pair.go): one value return p.Name != "" && p.CredentialForm != catalogue.CredentialFormPair &&
// every holder shares, a login per consumer, a value its software issues, or one a person gives. p.CredentialForm != catalogue.CredentialFormNone
var credentialForms = []string{"shared", "pair", "issued", "given"} }
// PlannedSecret is one secret of the module's own, by its name in the module's manifest; Path, when set, is the // 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. // 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) { if !nodeShape.MatchString(pr.Provider) {
out = append(out, fmt.Sprintf("%s: provider %q is not a machine's name", which, 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, 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] { if key := [2]string{pr.Provision, pr.Local}; bound[key] {
out = append(out, fmt.Sprintf("%s: its provider is named twice", which)) out = append(out, fmt.Sprintf("%s: its provider is named twice", which))
@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -63,7 +64,9 @@ func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) {
`{"provision":"sonarr-api","provider":"ace","provider-module":"sonarr","credential-form":"shared"},` + `{"provision":"sonarr-api","provider":"ace","provider-module":"sonarr","credential-form":"shared"},` +
`{"name":"forge-read","provision":"forge-read","provider":"novox","provider-module":"forge",` + `{"name":"forge-read","provision":"forge-read","provider":"novox","provider-module":"forge",` +
`"credential-form":"issued","path":"/var/lib/app/forge.secret"},` + `"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)) _, p, err := planOf("g14", "g14.app", []byte(value))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -72,9 +75,29 @@ func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) {
for _, pr := range p.Provisions { for _, pr := range p.Provisions {
forms = append(forms, pr.CredentialForm) 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) 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) 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") { 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) t.Fatalf("a credential form the client does not know was taken: %v", err)
@@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seal" "git.novox.be/novox/mesh-sdk/go/seal"
"github.com/nats-io/nats.go" "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 { } 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). // A credential of what the module requires, issued for this module on this machine (issue 757).
pr := p.Provisions[i] 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) name = HolderName(pr.Provider, pr.Provision, c.Node, module)
if pr.CredentialForm == "pair" { if pr.CredentialForm == "pair" {
// This consumer's own login from a provider that gives each consumer one (issue 779, ADR 0158 §11). // This consumer's own login from a provider that gives each consumer one (issue 779, ADR 0158 §11).
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-provisioner-client
go 1.26.0 go 1.26.0
require ( 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-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
golang.org/x/sys v0.48.0 golang.org/x/sys v0.48.0
+2 -2
View File
@@ -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.20261030150314-9379e9a96ad4 h1:CPUqC3FS/ezvBYdhVgVdsQg2DdN8xgtROQkSaHRKeOI=
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/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 h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= 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= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=