Ask the vault in a candidate answer only for credentials of modules the node runs, and refuse a stand-in of a running module, so a placement that adds a module is answered as added (issue 1126, ADR 0559 §4)
mesh/delivery merged: source.main moved to novox/mesh-sdk@56de5ba84010
mesh/delivery-group group feat/1126-l2b merged: every member is merged
mesh/check being carried to cadd7b1b2f8f on 43eef46c4121 | from b0cbc4e6aca8, mesh-catalog 64530a8dcb08 | novox/mesh-sdk#106 in group feat/1126-l2b

This commit is contained in:
2026-10-27 02:43:10 +01:00
parent 062c29c3a2
commit cadd7b1b2f
2 changed files with 69 additions and 6 deletions
+21
View File
@@ -40,6 +40,23 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
if err != nil { if err != nil {
return refuse(err) return refuse(err)
} }
// What the node runs, by module. A consumer's credential is asked only for a module it runs: the vault refuses the
// whole ask for a consuming module not assigned to the node (mesh-vault's own), and a module the candidate adds is
// assigned only after the merge (ADR 0549 §5), so its credential is left missing, a stand-in answered as not yet
// issued under Added (review of L2b, round 1, finding 1).
runs := map[string]bool{}
for _, c := range running {
if c.Module != "" {
runs[c.Module] = true
}
}
var consumed []compose.CredentialKey
for _, k := range asConsumer {
if runs[k.ConsumerModule] {
consumed = append(consumed, k)
}
}
asConsumer = consumed
if len(asConsumer)+len(asProvider) > 0 { if len(asConsumer)+len(asProvider) > 0 {
if in.Credentials, in.Issued, err = r.Credentials(ctx, s.Node, asConsumer, asProvider); err != nil { if in.Credentials, in.Issued, err = r.Credentials(ctx, s.Node, asConsumer, asProvider); err != nil {
return refuse(fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err)) return refuse(fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err))
@@ -86,6 +103,10 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
for _, x := range stood { for _, x := range stood {
node, module := standsInFor(x, s.Node) node, module := standsInFor(x, s.Node)
switch { switch {
case node == s.Node && runs[module]:
// A value missing for a module the node runs is a refusal, whatever the trunk places (the seed's trunk is
// empty, and every module counts as the group's).
refusals[module] = append(refusals[module], x)
case module != "" && added[node+"/"+module]: case module != "" && added[node+"/"+module]:
if node == s.Node { if node == s.Node {
unissued[module] = append(unissued[module], x) unissued[module] = append(unissued[module], x)
+46 -4
View File
@@ -7,6 +7,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"os" "os"
"reflect" "reflect"
"strings" "strings"
@@ -28,6 +29,9 @@ type bus struct {
valuesRead []string valuesRead []string
own []compose.OwnSecretRow own []compose.OwnSecretRow
ownErr error ownErr error
// asked is every consumer credential asked; the fake refuses the whole ask, as the vault's own does, for a consuming
// module its placement does not put on the node.
asked []compose.CredentialKey
} }
func (b *bus) Placements(context.Context) (map[string]delivery.Placement, error) { func (b *bus) Placements(context.Context) (map[string]delivery.Placement, error) {
@@ -46,7 +50,17 @@ func (b *bus) OwnSecrets(context.Context, string) ([]compose.OwnSecretRow, error
return b.own, b.ownErr return b.own, b.ownErr
} }
func (b *bus) Credentials(context.Context, string, []compose.CredentialKey, []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) { func (b *bus) Credentials(_ context.Context, node string, asConsumer, _ []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) {
b.asked = append(b.asked, asConsumer...)
for _, k := range asConsumer {
placed := false
for _, m := range b.placements[node].Modules {
placed = placed || m.Module == k.ConsumerModule
}
if !placed {
return nil, nil, fmt.Errorf("%s is not on %s: neither assigned there nor required by a module there", k.ConsumerModule, node)
}
}
return nil, nil, nil return nil, nil, nil
} }
@@ -212,6 +226,12 @@ func TestInput(t *testing.T) {
keeper := []byte(`{"module":"keeper","version":"1","own-secrets":{"password":"/var/lib/mesh/keeper/password"},` + keeper := []byte(`{"module":"keeper","version":"1","own-secrets":{"password":"/var/lib/mesh/keeper/password"},` +
`"resources":[{"id":"state","type":"directory","path":"/var/lib/mesh/keeper","mode":"0700"}]}`) `"resources":[{"id":"state","type":"directory","path":"/var/lib/mesh/keeper","mode":"0700"}]}`)
lemurs := []byte(`{"module":"lemurs","version":"1","capabilities":["systemd"],"resources":[{"id":"service","type":"service","unit":"lemurs.service","state":"running","boot":"enabled"}]}`) lemurs := []byte(`{"module":"lemurs","version":"1","capabilities":["systemd"],"resources":[{"id":"service","type":"service","unit":"lemurs.service","state":"running","boot":"enabled"}]}`)
// A provider and a consumer of a pair credential on the one node (compose's objects and files).
objects, _ := json.Marshal(catalogue.Manifest{Module: "objects", Version: "1",
Provides: []catalogue.Offer{{Name: "s3-bucket", Scope: catalogue.ScopeMesh, Identity: &catalogue.OfferIdentity{Max: 20, In: "an S3 access key"}}},
Receives: map[string]string{"s3-bucket": "/var/lib/mesh/objects/mesh.json"}})
files, _ := json.Marshal(catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"},
Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/files/config.json", "mode": "0600", "content": "{}", "merge": "json"}}})
one := func(placed ...string) *bus { one := func(placed ...string) *bus {
p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}} p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
for _, m := range placed { for _, m := range placed {
@@ -219,11 +239,14 @@ func TestInput(t *testing.T) {
} }
return &bus{placements: map[string]delivery.Placement{"n1": p}, return &bus{placements: map[string]delivery.Placement{"n1": p},
records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: keys["anchor"], SealingKey: "sk", Capabilities: []string{"systemd"}}}, records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: keys["anchor"], SealingKey: "sk", Capabilities: []string{"systemd"}}},
modules: map[string]delivery.Module{"keeper": {Manifest: delivery.ManifestDigest(keeper)}, "lemurs": {Manifest: delivery.ManifestDigest(lemurs)}}, modules: map[string]delivery.Module{"keeper": {Manifest: delivery.ManifestDigest(keeper)}, "lemurs": {Manifest: delivery.ManifestDigest(lemurs)},
manifests: map[string][]byte{delivery.ManifestDigest(keeper): keeper, delivery.ManifestDigest(lemurs): lemurs}, "objects": {Manifest: delivery.ManifestDigest(objects)}, "files": {Manifest: delivery.ManifestDigest(files)}},
manifests: map[string][]byte{delivery.ManifestDigest(keeper): keeper, delivery.ManifestDigest(lemurs): lemurs,
delivery.ManifestDigest(objects): objects, delivery.ManifestDigest(files): files},
own: []compose.OwnSecretRow{{Node: "n1", Module: "keeper", Name: "password", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}} own: []compose.OwnSecretRow{{Node: "n1", Module: "keeper", Name: "password", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}}
} }
oneFps := map[string]string{"keeper": "src2:" + strings.Repeat("d", 64), "lemurs": "src2:" + strings.Repeat("e", 64)} oneFps := map[string]string{"keeper": "src2:" + strings.Repeat("d", 64), "lemurs": "src2:" + strings.Repeat("e", 64),
"objects": "src2:" + strings.Repeat("f", 64), "files": "src2:" + strings.Repeat("0", 64)}
runs, _, err := Compose(ctx, one("keeper", "lemurs"), Mesh{}, Scope{Node: "n1"}, oneFps) runs, _, err := Compose(ctx, one("keeper", "lemurs"), Mesh{}, Scope{Node: "n1"}, oneFps)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -232,6 +255,14 @@ func TestInput(t *testing.T) {
for _, c := range runs { for _, c := range runs {
runsNow = append(runsNow, c) runsNow = append(runsNow, c)
} }
objectsRun, _, err := Compose(ctx, one("objects"), Mesh{}, Scope{Node: "n1"}, oneFps)
if err != nil {
t.Fatal(err)
}
var consumerRuns []delivery.Composed
for _, c := range objectsRun {
consumerRuns = append(consumerRuns, c)
}
withoutOwn := func(cs []delivery.Composed) []delivery.Composed { withoutOwn := func(cs []delivery.Composed) []delivery.Composed {
var out []delivery.Composed var out []delivery.Composed
for _, c := range cs { for _, c := range cs {
@@ -271,6 +302,17 @@ func TestInput(t *testing.T) {
runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool { runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool {
return len(a.Refusals) == 1 && a.Refusals[0].Module == "keeper" && strings.Contains(a.Refusals[0].Why, "keeper/password") return len(a.Refusals) == 1 && a.Refusals[0].Module == "keeper" && strings.Contains(a.Refusals[0].Why, "keeper/password")
}}, }},
// Review of L2b, round 1, finding 1: a consumer the candidate adds has no credential yet, and the vault refuses an
// ask for a module not on the node; it is not asked, and the module is added, its credential not yet issued.
"a consumer added, the vault not asked for it": {one("objects"), placedAs("objects", "files"), consumerRuns, delivery.CandidateDiffers,
func(a delivery.CandidateAnswer) bool {
return len(a.Added) == 1 && a.Added[0].Module == "files" && len(a.Added[0].Unissued) > 0 && len(a.Refusals) == 0
}},
// And with no trunk at all (the seed), a stand-in of a module the node runs is a refusal, not an addition.
"the seed, a value missing for a module that runs": {func() *bus { b := one(); b.own = nil; return b }(), placedAs("keeper", "lemurs"),
runsNow, delivery.CandidateRefused, func(a delivery.CandidateAnswer) bool {
return len(a.Refusals) == 1 && a.Refusals[0].Module == "keeper" && len(a.Added) == 0
}},
"cannot compose": {one("keeper"), map[string]*delivery.Placement{"n1": {Node: "n2"}}, runsNow, delivery.CandidateRefused, "cannot compose": {one("keeper"), map[string]*delivery.Placement{"n1": {Node: "n2"}}, runsNow, delivery.CandidateRefused,
func(a delivery.CandidateAnswer) bool { return a.Refused != "" }}, func(a delivery.CandidateAnswer) bool { return a.Refused != "" }},
} { } {