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
400 lines
21 KiB
Go
400 lines
21 KiB
Go
package frombus
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
)
|
|
|
|
// bus is a Reader over values held in memory, counting whose config values were read.
|
|
type bus struct {
|
|
placements map[string]delivery.Placement
|
|
records map[string]delivery.NodeRecord
|
|
modules map[string]delivery.Module
|
|
manifests map[string][]byte
|
|
values map[string]map[string]any // by node.module
|
|
valuesRead []string
|
|
own []compose.OwnSecretRow
|
|
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) {
|
|
return b.placements, nil
|
|
}
|
|
func (b *bus) Records(context.Context) (map[string]delivery.NodeRecord, error) { return b.records, nil }
|
|
func (b *bus) Modules(context.Context) (map[string]delivery.Module, error) { return b.modules, nil }
|
|
func (b *bus) Manifest(_ context.Context, digest string) ([]byte, error) {
|
|
raw, ok := b.manifests[digest]
|
|
if !ok {
|
|
return nil, errors.New("no such manifest")
|
|
}
|
|
return raw, nil
|
|
}
|
|
func (b *bus) OwnSecrets(context.Context, string) ([]compose.OwnSecretRow, error) {
|
|
return b.own, b.ownErr
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (b *bus) Values(_ context.Context, node, module string) (map[string]any, error) {
|
|
b.valuesRead = append(b.valuesRead, node+"."+module)
|
|
return b.values[node+"."+module], nil
|
|
}
|
|
|
|
func publicKey(t *testing.T) string {
|
|
t.Helper()
|
|
k, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return base64.StdEncoding.EncodeToString(k.PublicKey().Bytes())
|
|
}
|
|
|
|
// Issue 1126 (S3, S4, S5): the bus's placement, node records, manifests and config values fill the same compose.Input the
|
|
// controller's store fills for compose's own two-machine mesh (compose's aMesh: anchor the hub, laptop), and that Input
|
|
// composes; a node reads only its own config values; what cannot be composed is refused, naming it.
|
|
func TestInput(t *testing.T) {
|
|
ctx := context.Background()
|
|
xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := delivery.ManifestDigest(xterm)
|
|
// 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"}},"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)}
|
|
caps := []string{"container-runtime", "systemd", "wireguard"}
|
|
tunnel := &delivery.RecordTunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24",
|
|
Peers: []delivery.RecordTunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}, {PublicKey: keys["laptop"], Address: "10.77.0.2/32"}}}
|
|
mk := func() *bus {
|
|
return &bus{
|
|
placements: map[string]delivery.Placement{
|
|
"anchor": {Node: "anchor", Decided: delivery.NodeDecided{Endpoint: "anchor.example:51820", Site: "here", Hub: true, Address: "10.77.0.1",
|
|
Tunnel: tunnel, PeerNames: map[string]string{"peer": "phone"}},
|
|
Modules: []delivery.PlacedModule{{Module: overlay.Name}},
|
|
Holds: []delivery.Holding{{Claim: "mesh-private-network", Scope: "mesh", Module: overlay.Name}}},
|
|
"laptop": {Node: "laptop", Decided: delivery.NodeDecided{Endpoint: "laptop.example:51820", Site: "here", Address: "10.77.0.2"},
|
|
Modules: []delivery.PlacedModule{{Module: overlay.Name},
|
|
{Module: "xterm", HasLayer: true, Ports: map[string]int{"80": 8080}},
|
|
{Module: "media", HasMesh: true, MeshValues: map[string]any{"name": "mesh"}, HasLayer: true,
|
|
Layer: map[string]any{"places": map[string]any{"data": "/srv/data"}}, Values: map[string]any{"lines": float64(1000)}}}},
|
|
},
|
|
records: map[string]delivery.NodeRecord{
|
|
"anchor": {Node: "anchor", TunnelKey: keys["anchor"], Capabilities: caps, Contract: 2},
|
|
"laptop": {Node: "laptop", TunnelKey: keys["laptop"], Capabilities: caps, Authority: "the mesh's certificate"},
|
|
},
|
|
modules: map[string]delivery.Module{"xterm": {Manifest: digest}, "media": {Manifest: mediaDigest}, "unread": {}},
|
|
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: "media", Name: "token", Origin: "made", SealedTo: "sk", Sealed: "{}"}},
|
|
}
|
|
}
|
|
|
|
// What the controller's store gives for the same mesh, as compose's aMesh and SettingsFor build it.
|
|
var private catalogue.Manifest
|
|
raw, _ := json.Marshal(overlay.Manifest())
|
|
if err := json.Unmarshal(raw, &private); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parsed, err := catalogue.ParseManifestForInputs(xterm)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parsedMedia, err := catalogue.ParseManifestForInputs(media)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
capsSet := map[string]bool{"container-runtime": true, "systemd": true, "wireguard": true}
|
|
tun := compose.Tunnel{Interface: "wg0", PublicKey: keys["anchor"], Address: "10.77.0.1/24",
|
|
Peers: []compose.TunnelPeer{{PublicKey: "peer", Address: "10.77.0.9/32"}, {PublicKey: keys["laptop"], Address: "10.77.0.2/32"}}}
|
|
want := compose.Input{
|
|
Catalogue: map[string]catalogue.Manifest{overlay.Name: private, "xterm": parsed, "media": parsedMedia},
|
|
Nodes: []compose.Node{
|
|
{Name: "anchor", Place: compose.Place{Key: keys["anchor"], Endpoint: "anchor.example:51820", Site: "here", Hub: true,
|
|
Address: "10.77.0.1"}, Assigned: []string{overlay.Name}, Capabilities: capsSet, Contract: 2, Stated: true},
|
|
{Name: "laptop", Place: compose.Place{Key: keys["laptop"], Endpoint: "laptop.example:51820", Site: "here",
|
|
Address: "10.77.0.2"}, Assigned: []string{overlay.Name, "xterm", "media"}, Capabilities: capsSet,
|
|
Ports: map[string]map[int]int{"xterm": {80: 8080}},
|
|
Settings: map[string][]catalogue.Layer{"xterm": {{From: "laptop", Values: map[string]any{"font": "mono"}}},
|
|
"media": {{From: catalogue.MeshWideLayer, Values: map[string]any{"name": "mesh"}},
|
|
{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: "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: "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.
|
|
Authority: "the mesh's certificate",
|
|
CarriedPeers: []compose.CarriedPeer{{PublicKey: keys["laptop"], Address: "10.77.0.2", EnrolledAs: "laptop"},
|
|
{PublicKey: "peer", Address: "10.77.0.9", Named: "phone"}},
|
|
}
|
|
|
|
b := mk()
|
|
got, err := Input(ctx, b, Mesh{}, Scope{Node: "laptop"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
gj, _ := json.MarshalIndent(got, "", " ")
|
|
wj, _ := json.MarshalIndent(want, "", " ")
|
|
t.Fatalf("Input from the bus differs from the store's:\n got %s\nwant %s", gj, wj)
|
|
}
|
|
if !reflect.DeepEqual(b.valuesRead, []string{"laptop." + overlay.Name, "laptop.xterm", "laptop.media"}) {
|
|
t.Errorf("laptop read config values %v; a node reads its own slice alone", b.valuesRead)
|
|
}
|
|
|
|
// The same composer over both: the compositions from the bus are the store's, byte for byte.
|
|
fps := map[string]string{overlay.Name: "src2:" + strings.Repeat("a", 64), "xterm": "src2:" + strings.Repeat("b", 64),
|
|
"media": "src2:" + strings.Repeat("c", 64)}
|
|
fromBus, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop"}, fps)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fromStore, _, err := compose.Compositions(want, "laptop", fps, compose.Reading)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(fromBus) == 0 || !reflect.DeepEqual(fromBus, fromStore) {
|
|
t.Fatalf("compositions differ or are empty: %d from the bus, %d from the store", len(fromBus), len(fromStore))
|
|
}
|
|
|
|
// Issue 1126: a candidate placement equal to the node's composes what it runs, and CompareComposed says so; one
|
|
// thing changed in the group's candidate of another node (the hub's endpoint) names exactly the module it moves
|
|
// here. Placed puts the same placement over the store's input unchanged.
|
|
running := make([]delivery.Composed, 0, len(fromBus))
|
|
for _, c := range fromBus {
|
|
running = append(running, c)
|
|
}
|
|
laptop := mk().placements["laptop"]
|
|
asIs, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop}}, fps)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c := delivery.CompareComposed(asIs, running); len(c.Differs)+len(c.Added)+len(c.Removed) != 0 || len(c.Same) != len(running) {
|
|
t.Errorf("the candidate equal to the placement: %+v", c)
|
|
}
|
|
moved := mk().placements["anchor"]
|
|
moved.Decided.Endpoint = "anchor.example:51821"
|
|
changed, _, err := Compose(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": &laptop, "anchor": &moved}}, fps)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c := delivery.CompareComposed(changed, running); len(c.Differs) != 1 || c.Differs[0].Module != overlay.Name || c.Differs[0].Resource != overlay.Name+".overlay-config" {
|
|
t.Errorf("the hub's endpoint changed: %+v, want %s alone, naming its config", c, overlay.Name)
|
|
}
|
|
if c := delivery.CompareComposed(changed, running[:0]); len(c.Added) != len(changed) {
|
|
t.Errorf("nothing runs: %+v", c)
|
|
}
|
|
|
|
// ADR 0559 §4, the node's answer: the same; one module differing; a module the group adds, whose own secret is not
|
|
// yet issued (no refusal); a value missing for a module the node runs (a refusal); a module the candidate removes.
|
|
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"}]}`)
|
|
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 {
|
|
p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
|
for _, m := range placed {
|
|
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
|
|
}
|
|
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"}}},
|
|
modules: map[string]delivery.Module{"keeper": {Manifest: delivery.ManifestDigest(keeper)}, "lemurs": {Manifest: delivery.ManifestDigest(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"`}}}
|
|
}
|
|
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)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var runsNow []delivery.Composed
|
|
for _, c := range runs {
|
|
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 {
|
|
var out []delivery.Composed
|
|
for _, c := range cs {
|
|
if c.Module != "" {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
placedAs := func(modules ...string) map[string]*delivery.Placement {
|
|
p := one(modules...).placements["n1"]
|
|
return map[string]*delivery.Placement{"n1": &p}
|
|
}
|
|
for name, c := range map[string]struct {
|
|
b *bus
|
|
given map[string]*delivery.Placement
|
|
running []delivery.Composed
|
|
verdict string
|
|
check func(delivery.CandidateAnswer) bool
|
|
}{
|
|
"the same": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), runsNow, delivery.CandidateSame,
|
|
func(a delivery.CandidateAnswer) bool { return len(a.Same) == 3 && a.Head == "h1" && a.Revision == 7 }},
|
|
"the node runs no own composition": {one("keeper", "lemurs"), placedAs("keeper", "lemurs"), withoutOwn(runsNow), delivery.CandidateSame,
|
|
func(a delivery.CandidateAnswer) bool { return len(a.Same) == 2 && len(a.Added) == 0 }},
|
|
"one removed": {one("keeper", "lemurs"), placedAs("keeper"), runsNow, delivery.CandidateDiffers,
|
|
func(a delivery.CandidateAnswer) bool { return reflect.DeepEqual(a.Removed, []string{"lemurs"}) }},
|
|
"one added, its secret not yet issued": {func() *bus { b := one("lemurs"); b.own = nil; return b }(), placedAs("keeper", "lemurs"),
|
|
nil, delivery.CandidateDiffers, func(a delivery.CandidateAnswer) bool {
|
|
for _, m := range a.Added {
|
|
if m.Module == "keeper" {
|
|
return reflect.DeepEqual(m.Unissued, []string{"keeper/password"}) && len(a.Refusals) == 0
|
|
}
|
|
}
|
|
return false
|
|
}},
|
|
"a value missing for a module that runs": {func() *bus { b := one("keeper", "lemurs"); 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" && 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,
|
|
func(a delivery.CandidateAnswer) bool { return a.Refused != "" }},
|
|
} {
|
|
a := Answer(ctx, c.b, Mesh{}, Scope{Node: "n1", Placements: c.given}, oneFps, c.running, "h1", 7)
|
|
if a.Verdict != c.verdict || !c.check(a) {
|
|
t.Errorf("%s: %+v", name, a)
|
|
}
|
|
if raw, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": a}}); err != nil {
|
|
t.Errorf("%s: the answer is not one a have holds: %v (%s)", name, err, raw)
|
|
}
|
|
}
|
|
if got, err := Placed(want, map[string]*delivery.Placement{"laptop": &laptop}); err != nil || !reflect.DeepEqual(got, want) {
|
|
t.Errorf("Placed with the node's own placement changes the input: %v", err)
|
|
}
|
|
if got, err := Placed(want, map[string]*delivery.Placement{"laptop": nil}); err != nil || len(got.Nodes[1].Assigned) != 0 || got.Nodes[1].Place.Key != keys["laptop"] {
|
|
t.Errorf("Placed with no placement: %+v, %v", got.Nodes[1], err)
|
|
}
|
|
if _, err := Placed(want, map[string]*delivery.Placement{"stranger": {Node: "stranger"}}); err == nil {
|
|
t.Error("Placed a node the input holds no node for")
|
|
}
|
|
if _, err := Input(ctx, mk(), Mesh{}, Scope{Node: "laptop", Placements: map[string]*delivery.Placement{"laptop": {Node: "anchor"}}}); err == nil {
|
|
t.Error("a given placement naming another node was taken")
|
|
}
|
|
|
|
// ADR 0555 §6: Export is Input's inverse for what a person decides: the store's input exported, node by node, is the
|
|
// placement the bus holds, and read back it is the store's input again.
|
|
for node, placed := range mk().placements {
|
|
got, err := Export(want, node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gj, _ := delivery.EncodePlacement(got); !delivery.SamePlacement(got, placed) {
|
|
pj, _ := delivery.EncodePlacement(placed)
|
|
t.Errorf("%s exported as %s, want %s", node, gj, pj)
|
|
}
|
|
}
|
|
if _, err := Export(want, "stranger"); err == nil {
|
|
t.Error("a node the input lacks was exported")
|
|
}
|
|
|
|
b = mk()
|
|
if every, err := Input(ctx, b, Mesh{}, Scope{Every: true}); err != nil || every.OwnSecrets != nil {
|
|
t.Fatalf("composing every node: %v, own secrets %v (the vault answers a node about itself alone)", err, every.OwnSecrets)
|
|
}
|
|
if !reflect.DeepEqual(b.valuesRead, []string{"anchor." + overlay.Name, "laptop." + overlay.Name, "laptop.xterm", "laptop.media"}) {
|
|
// Every node's placed modules, and nothing placed nowhere: anchor places no xterm.
|
|
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"}})
|
|
if err != nil || !reflect.DeepEqual(in.Nodes[1].Kept, map[string]catalogue.Manifest{"xterm": parsed}) || in.Nodes[0].Kept != nil {
|
|
t.Errorf("the want's manifest is kept on the composing node alone: %v, %v", in.Nodes, err)
|
|
}
|
|
|
|
for name, c := range map[string]struct {
|
|
change func(*bus)
|
|
scope Scope
|
|
says string
|
|
}{
|
|
"no record": {func(b *bus) { delete(b.records, "laptop") }, Scope{Node: "anchor"}, "laptop, whose agent has written no record"},
|
|
"no placement": {func(b *bus) {}, Scope{Node: "stranger"}, "stranger has no placement"},
|
|
"a manifest unread": {func(b *bus) { delete(b.manifests, digest) }, Scope{Node: "laptop"}, "the manifest of xterm"},
|
|
"a kept one unread": {func(b *bus) {}, Scope{Node: "laptop", Kept: map[string]string{"xterm": "gone"}}, "digest gone"},
|
|
} {
|
|
b := mk()
|
|
c.change(b)
|
|
if _, err := Input(ctx, b, Mesh{}, c.scope); err == nil || !strings.Contains(err.Error(), c.says) {
|
|
t.Errorf("%s: %v, want it to say %q", name, err, c.says)
|
|
}
|
|
}
|
|
}
|