Files
mesh-sdk/go/compose/frombus/frombus_test.go
T

182 lines
7.8 KiB
Go

package frombus
import (
"context"
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"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
}
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) 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)
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"}}}
mk := func() *bus {
return &bus{
placements: map[string]delivery.Placement{
"anchor": {Node: "anchor", Modules: []delivery.PlacedModule{{Module: overlay.Name}},
Holds: []delivery.Holding{{Claim: "mesh-terminal", Scope: "mesh", Module: "xterm"}}},
"laptop": {Node: "laptop", Modules: []delivery.PlacedModule{{Module: overlay.Name},
{Module: "xterm", HasLayer: true, Ports: map[string]int{"80": 8080}}}},
},
records: map[string]delivery.NodeRecord{
"anchor": {Node: "anchor", Place: delivery.NodePlace{Key: keys["anchor"], Endpoint: "anchor.example:51820",
Site: "here", Hub: true, Address: "10.77.0.1"}, Capabilities: caps, Tunnel: tunnel, Contract: 2,
Carried: []delivery.RecordCarriedPeer{{PublicKey: "peer", Address: "10.77.0.9", EnrolledAs: "phone"}}},
"laptop": {Node: "laptop", Place: delivery.NodePlace{Key: keys["laptop"], Endpoint: "laptop.example:51820",
Site: "here", Address: "10.77.0.2"}, Capabilities: caps},
},
modules: map[string]delivery.Module{"xterm": {Manifest: digest}, "unread": {}},
manifests: map[string][]byte{digest: xterm},
values: map[string]map[string]any{"laptop.xterm": {"font": "mono"}, "anchor.xterm": {"font": "serif"}},
}
}
// 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)
}
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"}}}
want := compose.Input{
Catalogue: map[string]catalogue.Manifest{overlay.Name: private, "xterm": parsed},
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"}, 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"}}}}},
},
Holdings: []catalogue.Held{{Claim: "mesh-terminal", Scope: "mesh", Node: "anchor", Module: "xterm"}},
Tunnels: map[string]compose.FoundTunnel{"anchor": {Tunnel: tun}},
Adopted: &compose.AdoptedTunnel{Tunnel: tun, Hub: "anchor"},
CarriedPeers: []compose.CarriedPeer{{PublicKey: "peer", Address: "10.77.0.9", EnrolledAs: "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"}) {
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)}
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))
}
b = mk()
if _, err := Input(ctx, b, Mesh{}, Scope{Every: true}); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(b.valuesRead, []string{"anchor." + overlay.Name, "laptop." + overlay.Name, "laptop.xterm"}) {
// Every node's placed modules, and nothing placed nowhere: anchor places no xterm.
t.Errorf("the check read %v", b.valuesRead)
}
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)
}
}
}