Check a plan's credential form against mesh-sdk's one list, so the client takes plans naming none again (issue 774)
mesh/check checking 67dcf252ecfa on 82ac613b4ed0 | novox/mesh-catalog#564 in group fix/credential-forms-one-list
mesh/delivery merged: source.main moved to novox/mesh-catalog@72cee59acc32
mesh/delivery-group group fix/credential-forms-one-list merged: every member is merged

Since 13:55 the client refused every plan with credential-form none
(gitea's forge-account, the artifact store), and g14 and shanks placed no
files for mesh-build-go, mesh-build-node, mesh-build-image or ssh-client.
Second meeting of the credential-form list kind: the controller's copy was
fixed alone before. Thorough: one list in mesh-sdk; this client drops its
copy and pins mesh-sdk 9379e9a. A none entry is never a holder's issued
value: its bound values come from the provider's entry, and a secret named
for it fails loudly instead of waiting on a vault that mints nothing.
This commit is contained in:
2026-10-30 16:06:46 +01:00
parent 82ac613b4e
commit 67dcf252ec
5 changed files with 46 additions and 14 deletions
@@ -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))
@@ -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)
@@ -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).
+1 -1
View File
@@ -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
+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.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=