Review of L2: the export is a placement file at its version with its modules (S1); a node asks the vault only when a module placed on it declares an own secret, so one with none composes whether or not the vault answers (H8's failure) (issue 1126)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 3968849b1b36 on a7a1fbd34e1d | novox/mesh-sdk#98 in group feat/1126-nodes-compose
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 3968849b1b36 on a7a1fbd34e1d | novox/mesh-sdk#98 in group feat/1126-nodes-compose
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -39,6 +42,17 @@ func TestTheSeedWritesEachNodesPlacementFileOnce(t *testing.T) {
|
||||
if entries, _ := os.ReadDir(filepath.Join(other, "placement")); len(entries) != 0 {
|
||||
t.Errorf("a refused export wrote %v", entries)
|
||||
}
|
||||
// What the controller's placement-export prints, frombus.Export per node, is what the seed takes: a node with no
|
||||
// module placed included.
|
||||
in := compose.Input{Nodes: []compose.Node{{Name: "solo", Place: compose.Place{Hub: true, Address: "10.77.0.9"}}}}
|
||||
exported, err := frombus.Export(in, "solo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
answer, _ := json.Marshal(map[string]any{"answer": map[string]delivery.Placement{"solo": exported}})
|
||||
if written, err := Seed(strings.NewReader(string(answer)), t.TempDir()); err != nil || len(written) != 1 {
|
||||
t.Errorf("Export's answer %s was not seeded: %v, %v", answer, written, err)
|
||||
}
|
||||
for _, bad := range []string{`[]`, `{}`, `{"ace":{"v":1,"node":"novox","modules":[]}}`} {
|
||||
if _, err := Seed(strings.NewReader(bad), t.TempDir()); err == nil {
|
||||
t.Errorf("%s was seeded", bad)
|
||||
|
||||
@@ -24,7 +24,9 @@ func Export(in compose.Input, node string) (delivery.Placement, error) {
|
||||
if n == nil {
|
||||
return delivery.Placement{}, fmt.Errorf("frombus: the input has no node %q", node)
|
||||
}
|
||||
p := delivery.Placement{Node: node, Decided: delivery.NodeDecided{
|
||||
// At the version the placement file is read at, with a list of modules even when none is placed: the export is the
|
||||
// file the seed writes (cmd/placement-seed), and the reader refuses a value of version 0 or without its modules.
|
||||
p := delivery.Placement{V: delivery.Version, Node: node, Modules: []delivery.PlacedModule{}, Decided: delivery.NodeDecided{
|
||||
Endpoint: n.Place.Endpoint, Site: n.Place.Site, Hub: n.Place.Hub, Address: n.Place.Address,
|
||||
PublicDomain: n.PublicDomain, Account: n.Account, AccountHome: n.AccountHome, AgentAccount: n.AgentAccount,
|
||||
AgentHome: n.AgentAccountHome, Adopted: n.Adopted, Taken: n.Taken}}
|
||||
|
||||
@@ -221,8 +221,12 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
|
||||
return compose.Input{}, fmt.Errorf("frombus: %s has written no record of itself yet", s.Node)
|
||||
}
|
||||
// Its own modules' secrets, sealed to its own key by the vault: a composition carries them sealed (ADR 0550 §5).
|
||||
if in.OwnSecrets, err = r.OwnSecrets(ctx, s.Node); err != nil {
|
||||
return compose.Input{}, fmt.Errorf("frombus: the own secrets of %s's modules: %w", s.Node, err)
|
||||
// Asked only when a module placed on it declares one: a node none of whose modules has a secret composes
|
||||
// whether or not the vault answers.
|
||||
if declaresOwnSecrets(in, s.Node) {
|
||||
if in.OwnSecrets, err = r.OwnSecrets(ctx, s.Node); err != nil {
|
||||
return compose.Input{}, fmt.Errorf("frombus: the own secrets of %s's modules: %w", s.Node, err)
|
||||
}
|
||||
}
|
||||
// The core's credentials travel as the vault's box sealed to the machine (ADR 0349 §16): the same values, which
|
||||
// the composition reads for a core module as its kept boxes.
|
||||
@@ -257,6 +261,25 @@ func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]s
|
||||
return compose.CompositionsApplied(in, s.Node, fps)
|
||||
}
|
||||
|
||||
// declaresOwnSecrets says a module assigned to node declares an own secret, in the manifest the node composes it from.
|
||||
func declaresOwnSecrets(in compose.Input, node string) bool {
|
||||
for _, n := range in.Nodes {
|
||||
if n.Name != node {
|
||||
continue
|
||||
}
|
||||
for _, module := range n.Assigned {
|
||||
m, ok := n.Kept[module]
|
||||
if !ok {
|
||||
m = in.Catalogue[module]
|
||||
}
|
||||
if len(m.OwnSecrets) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person
|
||||
// decided of it, and where its modules run).
|
||||
func nodeOf(rec delivery.NodeRecord, p delivery.Placement) compose.Node {
|
||||
|
||||
@@ -27,6 +27,7 @@ type bus struct {
|
||||
values map[string]map[string]any // by node.module
|
||||
valuesRead []string
|
||||
own []compose.OwnSecretRow
|
||||
ownErr error
|
||||
}
|
||||
|
||||
func (b *bus) Placements(context.Context) (map[string]delivery.Placement, error) {
|
||||
@@ -41,7 +42,9 @@ func (b *bus) Manifest(_ context.Context, digest string) ([]byte, error) {
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
func (b *bus) OwnSecrets(context.Context, string) ([]compose.OwnSecretRow, error) { return b.own, nil }
|
||||
func (b *bus) OwnSecrets(context.Context, string) ([]compose.OwnSecretRow, error) {
|
||||
return b.own, b.ownErr
|
||||
}
|
||||
|
||||
func (b *bus) Credentials(context.Context, string, []compose.CredentialKey, []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) {
|
||||
return nil, nil, nil
|
||||
@@ -74,7 +77,7 @@ func TestInput(t *testing.T) {
|
||||
// A module in the settings form (plex and the media modules on one node, 2026-10-26): its config values are in its
|
||||
// layers, not mesh-config, so the placement carries them (MeshValues, Values) and mesh-config's are not read for it.
|
||||
media := []byte(`{"module":"media","version":"1","settings":{"lines":{"kind":"preference","default":0,"why":"kept"},` +
|
||||
`"name":{"kind":"preference","default":"x","why":"shown"}},"resources":[{"id":"conf","type":"file",` +
|
||||
`"name":{"kind":"preference","default":"x","why":"shown"}},"own-secrets":{"token":"/srv/media/token"},"resources":[{"id":"conf","type":"file",` +
|
||||
`"path":"/srv/media/conf","mode":"0644","content":"uid=${setting:lines} name=${setting:name}\n"}]}`)
|
||||
mediaDigest := delivery.ManifestDigest(media)
|
||||
keys := map[string]string{"anchor": publicKey(t), "laptop": publicKey(t)}
|
||||
@@ -102,7 +105,7 @@ func TestInput(t *testing.T) {
|
||||
manifests: map[string][]byte{digest: xterm, mediaDigest: media},
|
||||
values: map[string]map[string]any{"laptop.xterm": {"font": "mono"}, "anchor.xterm": {"font": "serif"},
|
||||
"laptop.media": {"lines": "not mesh-config's"}},
|
||||
own: []compose.OwnSecretRow{{Node: "laptop", Module: "xterm", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}},
|
||||
own: []compose.OwnSecretRow{{Node: "laptop", Module: "media", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,9 +139,9 @@ func TestInput(t *testing.T) {
|
||||
{From: "laptop", Values: map[string]any{"lines": float64(1000)}, Placement: map[string]any{"places": map[string]any{"data": "/srv/data"}}}}}},
|
||||
},
|
||||
Holdings: []catalogue.Held{{Claim: "mesh-private-network", Scope: "mesh", Node: "anchor", Module: overlay.Name}},
|
||||
OwnSecrets: []compose.OwnSecretRow{{Node: "laptop", Module: "xterm", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}},
|
||||
OwnSecrets: []compose.OwnSecretRow{{Node: "laptop", Module: "media", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}},
|
||||
// The same value as a core credential's box, for a core module (ADR 0349 §16).
|
||||
KeptCoreBoxes: []compose.CoreBox{{Node: "laptop", Module: "xterm", Name: "token", SealedTo: "sk", Box: json.RawMessage("{}")}},
|
||||
KeptCoreBoxes: []compose.CoreBox{{Node: "laptop", Module: "media", Name: "token", SealedTo: "sk", Box: json.RawMessage("{}")}},
|
||||
Tunnels: map[string]compose.FoundTunnel{"anchor": {Tunnel: tun}},
|
||||
Adopted: &compose.AdoptedTunnel{Tunnel: tun, Hub: "anchor", Adopted: true},
|
||||
// The controller's order is by address; laptop enrolled under its peer's key, and a person named the other.
|
||||
@@ -201,6 +204,17 @@ func TestInput(t *testing.T) {
|
||||
t.Errorf("the check read %v", b.valuesRead)
|
||||
}
|
||||
|
||||
// A node none of whose modules declares an own secret composes without asking the vault (the review of L2: a node
|
||||
// with no secret composed nothing while the vault did not answer); one whose module does, refuses without it.
|
||||
quiet := mk()
|
||||
quiet.ownErr = errors.New("no responders")
|
||||
if _, err := Input(ctx, quiet, Mesh{}, Scope{Node: "anchor"}); err != nil {
|
||||
t.Errorf("anchor, with no own secret, needed the vault: %v", err)
|
||||
}
|
||||
if _, err := Input(ctx, quiet, Mesh{}, Scope{Node: "laptop"}); err == nil {
|
||||
t.Error("laptop's media, which declares an own secret, composed without the vault")
|
||||
}
|
||||
|
||||
kept := mk()
|
||||
kept.manifests["old"] = xterm
|
||||
in, err := Input(ctx, kept, Mesh{}, Scope{Node: "laptop", Kept: map[string]string{"xterm": "old"}})
|
||||
|
||||
Reference in New Issue
Block a user