Read the controller's stores once into mesh-sdk's composition input: the nodes and the network (issue 1382)
readComposeInput asks the stores for the Input's mesh-wide and per-node fields (the shelf, each node's place, assignments, capabilities, accounts, pins, bindings, settings, ports, sealing key, taken modules, links, networks, order and builds, the recorded modules it keeps at the build it runs, its node-engine's contract) and writes nothing. A node's kept manifests are its own: the Input's Catalogue stays the current shelf (R4a review round 1, finding 1). Settings are read for the modules each node resolves to, as planFor read them. keepRecorded is split so the adapter reads the kept manifests with its refusals in the same words (applyKept). The credential and build half is issue 1383; vendor/modules.txt now lists go/compose and no longer the mesh-host validator.
This commit is contained in:
@@ -0,0 +1,392 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The controller's adapter from its stores to mesh-sdk's composition input (novox/hq ADR 0453 clause 3, design 47 R4b,
|
||||||
|
// issues 1382 and 1383).
|
||||||
|
//
|
||||||
|
// **The adapter reads, and perform writes.** readComposeInput asks the stores once for everything the composition reads
|
||||||
|
// and returns compose.Input, which the composition never changes; it makes, seals, allocates and records nothing. A
|
||||||
|
// pair credential, a port, an own secret or a core box that the next send would make is not made here: the composition
|
||||||
|
// names it foreseen, and compose_perform.go makes it for a send, then reads the stores again and composes again. A
|
||||||
|
// question (status, facts, probes, the merge gate's reading) never performs.
|
||||||
|
//
|
||||||
|
// **The Input is built once per operation.** A verb that composes every node reads the stores once and hands the Input to
|
||||||
|
// each node's composition of it.
|
||||||
|
|
||||||
|
// composeInputBuilt is called once for each Input the adapter builds: the seam a test counts the reads of an operation by
|
||||||
|
// (issue 1382, criterion 5).
|
||||||
|
var composeInputBuilt = func() {}
|
||||||
|
|
||||||
|
// readComposeInput reads the stores once and returns what the composition needs. ctx carries what a send keeps (the
|
||||||
|
// recorded modules kept at the build their machine runs, recorded_kept.go): the same contexts every send composes under.
|
||||||
|
func readComposeInput(ctx context.Context, open *stores) (*meshInput, error) {
|
||||||
|
in, shape, err := readMeshAndNodes(ctx, open)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// What each node resolves to, with no credential and no settings given: the modules whose settings are read, and the
|
||||||
|
// needs whose pair credentials are asked of the stores. Nothing is written by it.
|
||||||
|
plans := map[string]compose.Resolved{}
|
||||||
|
for _, n := range in.Nodes {
|
||||||
|
resolved, err := compose.Resolve(in, n.Name)
|
||||||
|
switch {
|
||||||
|
case compose.IsUnresolvable(err):
|
||||||
|
// Their set does not compose: nothing of theirs is read further, as the controller never read a thing for a
|
||||||
|
// node that did not resolve.
|
||||||
|
continue
|
||||||
|
case err != nil:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
plans[n.Name] = resolved
|
||||||
|
}
|
||||||
|
if err := readSettings(ctx, open, &in, plans); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := readCredentialsAndBuilds(ctx, open, &in, plans, shape); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
composeInputBuilt()
|
||||||
|
return &meshInput{Input: in}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readCredentialsAndBuilds is the credential and build half of the Input, which issue 1383 fills.
|
||||||
|
func readCredentialsAndBuilds(context.Context, *stores, *compose.Input, map[string]compose.Resolved, inputShape) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// meshInput is the Input an operation composes over.
|
||||||
|
type meshInput struct {
|
||||||
|
compose.Input
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeOf is the node of that name in an Input.
|
||||||
|
func nodeOf(in compose.Input, name string) (compose.Node, bool) {
|
||||||
|
for _, n := range in.Nodes {
|
||||||
|
if n.Name == name {
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compose.Node{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputShape is what the first half of the reading found that the second half needs.
|
||||||
|
type inputShape struct {
|
||||||
|
sent map[string]map[string]string // by node: module to the commit last sent
|
||||||
|
kept map[string]map[string]string // by node: module to the commit kept
|
||||||
|
current map[string]inventory.CurrentBuild
|
||||||
|
}
|
||||||
|
|
||||||
|
// readMeshAndNodes is the mesh-wide and per-node half of the Input (issue 1382).
|
||||||
|
func readMeshAndNodes(ctx context.Context, open *stores) (compose.Input, inputShape, error) {
|
||||||
|
inv := open.inventory
|
||||||
|
var in compose.Input
|
||||||
|
shape := inputShape{sent: map[string]map[string]string{}, kept: map[string]map[string]string{}}
|
||||||
|
|
||||||
|
shelf, err := inv.Catalogue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
in.Catalogue = shelf
|
||||||
|
if in.Holdings, err = inv.Holdings(ctx); err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
if in.Licences, err = readLicences(ctx, open); err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := inv.Nodes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
places, err := inv.Overlays(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
placed := map[string]inventory.Overlay{}
|
||||||
|
for _, p := range places {
|
||||||
|
placed[p.Name] = p
|
||||||
|
}
|
||||||
|
if shape.current, err = inv.CurrentBuilds(ctx); err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The range the mesh allocates from and what its internal names end in, read from the environment here and nowhere else.
|
||||||
|
in.OverlayCIDR = strings.TrimSpace(os.Getenv(OverlayCIDRVar))
|
||||||
|
in.InternalSuffix = overlay.Suffix()
|
||||||
|
if in.Tunnels, err = readTunnels(ctx, inv); err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
if in.Adopted, err = readAdopted(ctx, inv); err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
carried, err := inv.CarriedPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
for _, c := range carried {
|
||||||
|
in.CarriedPeers = append(in.CarriedPeers, compose.CarriedPeer{
|
||||||
|
PublicKey: c.PublicKey, Address: c.Address, EnrolledAs: c.EnrolledAs, Named: c.Named})
|
||||||
|
}
|
||||||
|
if in.Joining, err = inv.NodesWithALiveToken(ctx); err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keepsSecrets := false
|
||||||
|
for _, m := range shelf {
|
||||||
|
keepsSecrets = keepsSecrets || m.Keeps != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range nodes {
|
||||||
|
node, err := readNode(ctx, open, n, placed[n.Name])
|
||||||
|
if err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
shape.sent[n.Name], shape.kept[n.Name] = node.SentBuilds, node.KeptBuilds
|
||||||
|
in.Nodes = append(in.Nodes, node)
|
||||||
|
}
|
||||||
|
if keepsSecrets {
|
||||||
|
if in.OperatorExport, err = inv.OperatorExport(ctx); err != nil {
|
||||||
|
return in, shape, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in, shape, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readNode is one node of the Input: everything the composition reads about it, in the order the controller read it.
|
||||||
|
func readNode(ctx context.Context, open *stores, n inventory.Node, place inventory.Overlay) (compose.Node, error) {
|
||||||
|
inv := open.inventory
|
||||||
|
out := compose.Node{
|
||||||
|
Name: n.Name,
|
||||||
|
Place: compose.Place{Key: place.Key, Endpoint: place.Endpoint, Site: place.Site, Hub: place.Hub,
|
||||||
|
Address: place.Address},
|
||||||
|
Account: n.Account, AccountHome: n.AccountHome, AgentAccount: n.AgentAccount, AgentAccountHome: n.AgentAccountHome,
|
||||||
|
Adopted: n.Adopted,
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if out.Assigned, err = inv.Assigned(ctx, n.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if out.Capabilities, err = inv.ProfileOf(ctx, n.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if out.PublicDomain, err = inv.PublicDomainOf(ctx, n.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if out.Pins, err = inv.PinsFor(ctx, n.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if out.Bound, err = inv.BindingsFor(ctx, n.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
given, err := inv.PortsFor(ctx, n.Name)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
for _, a := range given {
|
||||||
|
if out.Ports == nil {
|
||||||
|
out.Ports = map[string]map[int]int{}
|
||||||
|
}
|
||||||
|
if out.Ports[a.Module] == nil {
|
||||||
|
out.Ports[a.Module] = map[int]int{}
|
||||||
|
}
|
||||||
|
out.Ports[a.Module][a.Wanted] = a.Machine
|
||||||
|
}
|
||||||
|
if out.SealingKey, err = inv.SealingKeyOf(ctx, n.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if n.Adopted {
|
||||||
|
if out.Taken, err = inv.Taken(ctx, n.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out.OutwardLinks, err = inv.OutwardLinksOf(ctx, n.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
account, had, err := inv.MachineOf(ctx, n.Name)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if had && account.Machine != nil {
|
||||||
|
out.NetworksSaid = true
|
||||||
|
for _, a := range account.Machine.Networks {
|
||||||
|
out.Networks = append(out.Networks, catalogue.AttachedNetwork{Link: a.Link, Network: a.Network})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out.Sequence, err = inv.Sequence(ctx, n.ID); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if out.Epoch, err = inv.SentEpoch(ctx, n.ID); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if out.Generation, err = inv.SentGeneration(ctx, n.ID); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if out.ReadsModules, err = inv.ReadsModules(ctx, n.ID); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if out.SentBuilds, out.SentKnown, err = inv.SentBuilds(ctx, n.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
runs, err := inv.AssignmentRuns(ctx, n.Name)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if len(runs) > 0 {
|
||||||
|
out.Runs = make(map[string]compose.Run, len(runs))
|
||||||
|
for module, ref := range runs {
|
||||||
|
out.Runs[module] = compose.Run{Commit: ref.Commit, Build: ref.Build}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A recorded module kept at the build this machine runs, in the context a send composes under (recorded_kept.go): its
|
||||||
|
// manifest and the commit, applied to this node's own resolution only.
|
||||||
|
kept, err := recordedKept(ctx, open, n.Name)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if len(kept) > 0 {
|
||||||
|
out.KeptBuilds = kept
|
||||||
|
if out.Kept, err = keptManifests(ctx, open, n.Name, kept); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stated, has, err := inv.HealthOf(ctx, n.Name)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
out.Stated = has
|
||||||
|
if has {
|
||||||
|
out.Contract = stated.Contract
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// keptManifests is the manifest of each module kept at the build a machine runs: keepRecorded's reads, and its refusals in its
|
||||||
|
// words, without putting any of it into a shelf (that is the node's own Kept, applied by the composition).
|
||||||
|
func keptManifests(ctx context.Context, open *stores, node string, kept map[string]string) (map[string]catalogue.Manifest, error) {
|
||||||
|
// applyKept puts each manifest into the shelf it is given and refuses a build that cannot be read; the shelf here is a
|
||||||
|
// scratch one, whose entries are the kept manifests.
|
||||||
|
scratch := map[string]catalogue.Manifest{}
|
||||||
|
if err := applyKept(ctx, open, node, kept, scratch); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return scratch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTunnels(ctx context.Context, inv *inventory.Inventory) (map[string]compose.FoundTunnel, error) {
|
||||||
|
found, err := inv.Tunnels(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(found) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
out := make(map[string]compose.FoundTunnel, len(found))
|
||||||
|
for name, t := range found {
|
||||||
|
out[name] = compose.FoundTunnel{Tunnel: tunnelOf(t.Tunnel), NodeAdopted: t.NodeAdopted}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAdopted(ctx context.Context, inv *inventory.Inventory) (*compose.AdoptedTunnel, error) {
|
||||||
|
tunnel, hub, adopted, err := inv.AdoptedTunnel(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if hub == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &compose.AdoptedTunnel{Tunnel: tunnelOf(tunnel), Hub: hub, Adopted: adopted}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tunnelOf(t inventory.Tunnel) compose.Tunnel {
|
||||||
|
out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU,
|
||||||
|
Address: t.Address, Range: t.Range, PublicKey: t.PublicKey}
|
||||||
|
for _, p := range t.Peers {
|
||||||
|
out.Peers = append(out.Peers, compose.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLicences is every licence the mesh holds, with who uses it and the key sealed to each holder: given even where empty,
|
||||||
|
// so a holder the mesh holds no key for composes as that, and not as a key that was not asked for.
|
||||||
|
func readLicences(ctx context.Context, open *stores) ([]compose.Licence, error) {
|
||||||
|
held, err := open.Licences(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
all, err := held.All(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var out []compose.Licence
|
||||||
|
for _, one := range all {
|
||||||
|
l := compose.Licence{Name: one.Name, Serves: one.Serves, Keys: map[compose.Holder]string{}}
|
||||||
|
holders, err := held.HoldersOf(ctx, one.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, h := range holders {
|
||||||
|
holder := compose.Holder{Node: h.Node, Module: h.Module}
|
||||||
|
l.Holders = append(l.Holders, holder)
|
||||||
|
key, err := held.KeyFor(ctx, one.Name, h.Node, h.Module)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
l.Keys[holder] = key
|
||||||
|
}
|
||||||
|
node, module, err := held.ManagerOf(ctx, one.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
l.Manager = compose.Holder{Node: node, Module: module}
|
||||||
|
out = append(out, l)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readSettings is each node's settings layers for the modules it resolves to: the ones the composition reads for it and for
|
||||||
|
// the rest of the mesh, and no module nothing resolves (a module's settings may fail to read where nothing runs it).
|
||||||
|
func readSettings(ctx context.Context, open *stores, in *compose.Input, plans map[string]compose.Resolved) error {
|
||||||
|
modules := map[string]map[string]bool{}
|
||||||
|
for node, resolved := range plans {
|
||||||
|
modules[node] = map[string]bool{}
|
||||||
|
for _, m := range resolved.Plan.Modules {
|
||||||
|
modules[node][m.Module] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The rest of the mesh reads the settings of the modules that offer something on another node (servedOnNode), which are
|
||||||
|
// among the modules that node resolves to.
|
||||||
|
for i := range in.Nodes {
|
||||||
|
n := &in.Nodes[i]
|
||||||
|
names := make([]string, 0, len(modules[n.Name]))
|
||||||
|
for m := range modules[n.Name] {
|
||||||
|
names = append(names, m)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
for _, module := range names {
|
||||||
|
layers, err := open.inventory.SettingsFor(ctx, n.Name, module)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(layers) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n.Settings == nil {
|
||||||
|
n.Settings = map[string][]catalogue.Layer{}
|
||||||
|
}
|
||||||
|
n.Settings[module] = layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1382: the adapter's Input names, for every node, what the inventory answers for it, each compared with the read named
|
||||||
|
// beside the field in go/compose/input.go (novox/hq ADR 0453 clause 3).
|
||||||
|
func TestTheAdaptersInputNamesWhatTheInventoryAnswersForEachNode(t *testing.T) {
|
||||||
|
open, _ := aMeshWithSecrets(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
inv := open.inventory
|
||||||
|
if err := inv.SetPublicDomain(ctx, "anchor", "example.test"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := inv.PortFor(ctx, "laptop", "files", 8080, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
in, err := readComposeInput(ctx, open)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shelf, err := inv.Catalogue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(in.Catalogue, shelf) {
|
||||||
|
t.Error("the Input's Catalogue is not the shelf the inventory holds")
|
||||||
|
}
|
||||||
|
nodes, err := inv.Nodes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(in.Nodes) != len(nodes) {
|
||||||
|
t.Fatalf("the Input names %d node(s), the inventory has %d", len(in.Nodes), len(nodes))
|
||||||
|
}
|
||||||
|
places, err := inv.Overlays(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, record := range nodes {
|
||||||
|
n, ok := nodeOf(in.Input, record.Name)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("%s is not in the Input", record.Name)
|
||||||
|
}
|
||||||
|
for _, p := range places {
|
||||||
|
if p.Name == record.Name {
|
||||||
|
if want := (compose.Place{Key: p.Key, Endpoint: p.Endpoint, Site: p.Site, Hub: p.Hub, Address: p.Address}); n.Place != want {
|
||||||
|
t.Errorf("%s: Place is %+v, the inventory's %+v", record.Name, n.Place, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assigned, _ := inv.Assigned(ctx, record.Name)
|
||||||
|
if !reflect.DeepEqual(n.Assigned, assigned) {
|
||||||
|
t.Errorf("%s: Assigned is %v, the inventory's %v", record.Name, n.Assigned, assigned)
|
||||||
|
}
|
||||||
|
caps, _ := inv.ProfileOf(ctx, record.Name)
|
||||||
|
if !reflect.DeepEqual(n.Capabilities, caps) {
|
||||||
|
t.Errorf("%s: Capabilities are %v, the inventory's %v", record.Name, n.Capabilities, caps)
|
||||||
|
}
|
||||||
|
domain, _ := inv.PublicDomainOf(ctx, record.Name)
|
||||||
|
if n.PublicDomain != domain {
|
||||||
|
t.Errorf("%s: PublicDomain is %q, the inventory's %q", record.Name, n.PublicDomain, domain)
|
||||||
|
}
|
||||||
|
if n.Account != record.Account || n.AccountHome != record.AccountHome || n.AgentAccount != record.AgentAccount ||
|
||||||
|
n.AgentAccountHome != record.AgentAccountHome || n.Adopted != record.Adopted {
|
||||||
|
t.Errorf("%s: the accounts or the mode differ from the inventory's: %+v", record.Name, n)
|
||||||
|
}
|
||||||
|
pins, _ := inv.PinsFor(ctx, record.Name)
|
||||||
|
if len(pins) != len(n.Pins) || (len(pins) > 0 && !reflect.DeepEqual(n.Pins, pins)) {
|
||||||
|
t.Errorf("%s: Pins are %v, the inventory's %v", record.Name, n.Pins, pins)
|
||||||
|
}
|
||||||
|
bound, _ := inv.BindingsFor(ctx, record.Name)
|
||||||
|
if len(bound) != len(n.Bound) || (len(bound) > 0 && !reflect.DeepEqual(n.Bound, bound)) {
|
||||||
|
t.Errorf("%s: Bound is %v, the inventory's %v", record.Name, n.Bound, bound)
|
||||||
|
}
|
||||||
|
key, _ := inv.SealingKeyOf(ctx, record.Name)
|
||||||
|
if n.SealingKey != key {
|
||||||
|
t.Errorf("%s: SealingKey is %q, the inventory's %q", record.Name, n.SealingKey, key)
|
||||||
|
}
|
||||||
|
ports, _ := inv.PortsFor(ctx, record.Name)
|
||||||
|
want := map[string]map[int]int{}
|
||||||
|
for _, a := range ports {
|
||||||
|
if want[a.Module] == nil {
|
||||||
|
want[a.Module] = map[int]int{}
|
||||||
|
}
|
||||||
|
want[a.Module][a.Wanted] = a.Machine
|
||||||
|
}
|
||||||
|
if len(want) != len(n.Ports) || (len(want) > 0 && !reflect.DeepEqual(n.Ports, want)) {
|
||||||
|
t.Errorf("%s: Ports are %v, the inventory's %v", record.Name, n.Ports, want)
|
||||||
|
}
|
||||||
|
// The settings of every module it resolves to, layer for layer.
|
||||||
|
resolved, err := compose.Resolve(in.Input, record.Name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, m := range resolved.Plan.Modules {
|
||||||
|
layers, err := inv.SettingsFor(ctx, record.Name, m.Module)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(layers) != len(n.Settings[m.Module]) || (len(layers) > 0 && !reflect.DeepEqual(n.Settings[m.Module], layers)) {
|
||||||
|
t.Errorf("%s: the settings of %s are %v, the inventory's %v", record.Name, m.Module, n.Settings[m.Module], layers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if domain := nodeDomain(in.Input, "anchor"); domain != "example.test" {
|
||||||
|
t.Errorf("anchor's PublicDomain is %q", domain)
|
||||||
|
}
|
||||||
|
if got := nodePorts(in.Input, "laptop"); got["files"][8080] == 0 {
|
||||||
|
t.Errorf("laptop's port for files:8080 is not in the Input: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodeDomain(in compose.Input, name string) string {
|
||||||
|
n, _ := nodeOf(in, name)
|
||||||
|
return n.PublicDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodePorts(in compose.Input, name string) map[string]map[int]int {
|
||||||
|
n, _ := nodeOf(in, name)
|
||||||
|
return n.Ports
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1382/AC2: a node keeping a recorded module at the build it runs has that module in Kept and KeptBuilds, and the Input's
|
||||||
|
// Catalogue is the current shelf, not the kept manifest (R4a review round 1, finding 1).
|
||||||
|
func TestANodeKeepingARecordedBuildHasItInKeptAndTheCatalogueStaysCurrent(t *testing.T) {
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
inv := open.inventory
|
||||||
|
start := time.Now().Add(-time.Hour)
|
||||||
|
image := "registry.invalid:5000/postgres/server@sha256:" + strings.Repeat("a", 64)
|
||||||
|
for _, b := range []link.BuildResult{
|
||||||
|
aContainerBuild(t, "postgres", "c1111111", catalogue.PolicyRecord, start, map[string][2]string{"server": {image, ""}}),
|
||||||
|
} {
|
||||||
|
if _, _, err := takeIn(ctx, inv, asTheOperator(t, inv, b)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := inv.Assign(ctx, "anchor", "postgres"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor", map[string]string{"postgres": "c1111111"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, _, err := takeIn(ctx, inv, aContainerBuild(t, "postgres", "c2222222", catalogue.PolicyRecord, start.Add(time.Minute),
|
||||||
|
map[string][2]string{"server": {image, "runtime"}})); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A send that is not a person's push keeps the build the machine runs.
|
||||||
|
in, err := readComposeInput(keepingRecorded(ctx), open)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
anchor, _ := nodeOf(in.Input, "anchor")
|
||||||
|
if anchor.KeptBuilds["postgres"] != "c1111111" {
|
||||||
|
t.Fatalf("anchor keeps %v, want postgres at c1111111", anchor.KeptBuilds)
|
||||||
|
}
|
||||||
|
kept, found := anchor.Kept["postgres"]
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("the kept manifest of postgres is not in anchor's Kept: %v", anchor.Kept)
|
||||||
|
}
|
||||||
|
current, err := inv.Catalogue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(in.Catalogue["postgres"], current["postgres"]) {
|
||||||
|
t.Error("the Input's Catalogue is not the current shelf")
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(kept, current["postgres"]) {
|
||||||
|
t.Error("the kept manifest is the current one: nothing is kept")
|
||||||
|
}
|
||||||
|
if laptop, _ := nodeOf(in.Input, "laptop"); len(laptop.Kept) != 0 || len(laptop.KeptBuilds) != 0 {
|
||||||
|
t.Errorf("laptop keeps what only anchor runs: %v", laptop.KeptBuilds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A person's push keeps nothing.
|
||||||
|
pushed, err := readComposeInput(ctx, open)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if a, _ := nodeOf(pushed.Input, "anchor"); len(a.Kept) != 0 || len(a.KeptBuilds) != 0 {
|
||||||
|
t.Errorf("a person's push keeps %v", a.KeptBuilds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1382/AC3: the seats on record, the tunnels, the adopted tunnel, its carried peers and the nodes joining equal the
|
||||||
|
// inventory's answers; the range and the suffix are read from the environment once, here.
|
||||||
|
func TestTheAdaptersInputCarriesTheNetworkAsTheInventoryHoldsIt(t *testing.T) {
|
||||||
|
open, _ := aMeshWithSecrets(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
inv := open.inventory
|
||||||
|
t.Setenv(OverlayCIDRVar, "10.99.0.0/16")
|
||||||
|
t.Setenv("MESH_INTERNAL_SUFFIX", "mesh.test")
|
||||||
|
in, err := readComposeInput(ctx, open)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
holdings, err := inv.Holdings(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(in.Holdings, holdings) {
|
||||||
|
t.Errorf("Holdings are %v, the inventory's %v", in.Holdings, holdings)
|
||||||
|
}
|
||||||
|
tunnels, err := inv.Tunnels(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(tunnels) != len(in.Tunnels) {
|
||||||
|
t.Errorf("Tunnels are %v, the inventory's %v", in.Tunnels, tunnels)
|
||||||
|
}
|
||||||
|
if _, hub, _, err := inv.AdoptedTunnel(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if (hub == "") != (in.Adopted == nil) {
|
||||||
|
t.Errorf("Adopted is %v, the inventory's hub is %q", in.Adopted, hub)
|
||||||
|
}
|
||||||
|
carried, err := inv.CarriedPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(carried) != len(in.CarriedPeers) {
|
||||||
|
t.Errorf("CarriedPeers are %v, the inventory's %v", in.CarriedPeers, carried)
|
||||||
|
}
|
||||||
|
joining, err := inv.NodesWithALiveToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(joining) != len(in.Joining) {
|
||||||
|
t.Errorf("Joining is %v, the inventory's %v", in.Joining, joining)
|
||||||
|
}
|
||||||
|
if in.OverlayCIDR != "10.99.0.0/16" || in.InternalSuffix != "mesh.test" {
|
||||||
|
t.Errorf("the range and the suffix are %q and %q", in.OverlayCIDR, in.InternalSuffix)
|
||||||
|
}
|
||||||
|
// Read once: what the environment says afterwards moves nothing already read.
|
||||||
|
t.Setenv(OverlayCIDRVar, "10.1.0.0/16")
|
||||||
|
if in.OverlayCIDR != "10.99.0.0/16" {
|
||||||
|
t.Error("the range moved with the environment")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -187,6 +187,13 @@ func keepRecorded(ctx context.Context, open *stores, node string, shelf map[stri
|
|||||||
if err != nil || len(kept) == 0 {
|
if err != nil || len(kept) == 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return kept, applyKept(ctx, open, node, kept, shelf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyKept puts the manifest of each module kept at the build its machine runs into a shelf, and refuses, in the words of
|
||||||
|
// keepRecorded, a build the records no longer hold. The adapter to the composition's input (compose_input.go) calls it with
|
||||||
|
// a scratch shelf, to read the manifests it gives the node as its own Kept.
|
||||||
|
func applyKept(ctx context.Context, open *stores, node string, kept map[string]string, shelf map[string]catalogue.Manifest) error {
|
||||||
names := make([]string, 0, len(kept))
|
names := make([]string, 0, len(kept))
|
||||||
for m := range kept {
|
for m := range kept {
|
||||||
names = append(names, m)
|
names = append(names, m)
|
||||||
@@ -195,28 +202,28 @@ func keepRecorded(ctx context.Context, open *stores, node string, shelf map[stri
|
|||||||
for _, m := range names {
|
for _, m := range names {
|
||||||
ran, found, err := open.inventory.ManifestAt(ctx, m, kept[m])
|
ran, found, err := open.inventory.ManifestAt(ctx, m, kept[m])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if !found && settingsKeptOn(ctx, node) {
|
if !found && settingsKeptOn(ctx, node) {
|
||||||
return nil, fmt.Errorf("%w: %s runs %s's build %s, which the build records no longer hold (novox/hq ADR 0344)",
|
return fmt.Errorf("%w: %s runs %s's build %s, which the build records no longer hold (novox/hq ADR 0344)",
|
||||||
errSettingsCannotKeep, node, m, short(kept[m]))
|
errSettingsCannotKeep, node, m, short(kept[m]))
|
||||||
}
|
}
|
||||||
if !found && everyBuildKept(ctx, node) {
|
if !found && everyBuildKept(ctx, node) {
|
||||||
return nil, fmt.Errorf("%s runs %s's build %s, which the build records no longer hold: the bus's user "+
|
return fmt.Errorf("%s runs %s's build %s, which the build records no longer hold: the bus's user "+
|
||||||
"list cannot be sent there alone with it kept (novox/hq issue 490)", node, m, short(kept[m]))
|
"list cannot be sent there alone with it kept (novox/hq issue 490)", node, m, short(kept[m]))
|
||||||
}
|
}
|
||||||
if !found && guarded(coreComponent(m)) {
|
if !found && guarded(coreComponent(m)) {
|
||||||
return nil, fmt.Errorf("%s was last sent %s's build %s, newer than the one the mesh holds, and the build records "+
|
return fmt.Errorf("%s was last sent %s's build %s, newer than the one the mesh holds, and the build records "+
|
||||||
"no longer hold it: this send cannot keep it and does not move it back — `push %s` sends the older build on "+
|
"no longer hold it: this send cannot keep it and does not move it back — `push %s` sends the older build on "+
|
||||||
"a person's word, and a newer merge or `rebuild %s` moves the record forward (novox/hq issue 201)",
|
"a person's word, and a newer merge or `rebuild %s` moves the record forward (novox/hq issue 201)",
|
||||||
node, m, short(kept[m]), node, m)
|
node, m, short(kept[m]), node, m)
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
return nil, fmt.Errorf("%s records rather than rolls out, and %s runs its build %s, which the build "+
|
return fmt.Errorf("%s records rather than rolls out, and %s runs its build %s, which the build "+
|
||||||
"records no longer hold: this send cannot keep it and does not move it — `push %s` sends the new "+
|
"records no longer hold: this send cannot keep it and does not move it — `push %s` sends the new "+
|
||||||
"one on a person's word (novox/hq ADR 0245)", m, node, short(kept[m]), node)
|
"one on a person's word (novox/hq ADR 0245)", m, node, short(kept[m]), node)
|
||||||
}
|
}
|
||||||
shelf[m] = ran
|
shelf[m] = ran
|
||||||
}
|
}
|
||||||
return kept, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-2
@@ -2,6 +2,7 @@
|
|||||||
## explicit; go 1.26.0
|
## explicit; go 1.26.0
|
||||||
git.novox.be/novox/mesh-sdk/go/asks
|
git.novox.be/novox/mesh-sdk/go/asks
|
||||||
git.novox.be/novox/mesh-sdk/go/catalogue
|
git.novox.be/novox/mesh-sdk/go/catalogue
|
||||||
|
git.novox.be/novox/mesh-sdk/go/compose
|
||||||
git.novox.be/novox/mesh-sdk/go/declaration
|
git.novox.be/novox/mesh-sdk/go/declaration
|
||||||
git.novox.be/novox/mesh-sdk/go/delivery
|
git.novox.be/novox/mesh-sdk/go/delivery
|
||||||
git.novox.be/novox/mesh-sdk/go/overlay
|
git.novox.be/novox/mesh-sdk/go/overlay
|
||||||
@@ -86,9 +87,7 @@ github.com/nats-io/nkeys
|
|||||||
github.com/nats-io/nuid
|
github.com/nats-io/nuid
|
||||||
# github.com/novox/mesh-host v0.0.0 => git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5
|
# github.com/novox/mesh-host v0.0.0 => git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5
|
||||||
## explicit; go 1.26.0
|
## explicit; go 1.26.0
|
||||||
github.com/novox/mesh-host/internal/declaration
|
|
||||||
github.com/novox/mesh-host/rootsearch
|
github.com/novox/mesh-host/rootsearch
|
||||||
github.com/novox/mesh-host/validate
|
|
||||||
# github.com/therootcompany/xz v1.0.1
|
# github.com/therootcompany/xz v1.0.1
|
||||||
## explicit; go 1.15
|
## explicit; go 1.15
|
||||||
github.com/therootcompany/xz
|
github.com/therootcompany/xz
|
||||||
|
|||||||
Reference in New Issue
Block a user