frombus exports a node's placement from a composition input, Input's inverse for what a person decides, and the controller's seat declares placement-export (issue 1126, ADR 0555 §6)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking dfe3ba94750c 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 dfe3ba94750c on a7a1fbd34e1d | novox/mesh-sdk#98 in group feat/1126-nodes-compose
This commit is contained in:
@@ -126,6 +126,13 @@ var ControllerVerbs = []Verb{
|
||||
{Name: "node", Description: "What one machine reported it can do, what it is assigned, and why.",
|
||||
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"}),
|
||||
Replaces: []string{"hostnamectl", "uptime"}},
|
||||
{Name: "placement-export", Description: "Each machine's placement as the catalogue's placement file says it (novox/hq ADR 0555 " +
|
||||
"§6, issue 1126): what a person decided of the machine (endpoint, site, hub, address on the private network, public " +
|
||||
"domain, accounts, adoption, the tunnel found at adoption) and where its modules run (in order, with host ports, " +
|
||||
"bindings, config placement layers, pins and holdings), read by the controller's own code from its own store, as JSON. " +
|
||||
"Read-only; the one verb the controller gains for its exit (the operator, 2026-10-26 19:06:40), read once by the seed " +
|
||||
"of the placement files.",
|
||||
Input: schema(map[string]string{"node": "one machine's name; every machine when absent"}, nil)},
|
||||
{Name: "modules", Description: "Every module the mesh holds: version, the commit it was built from, " +
|
||||
"and which machines run it.",
|
||||
Input: schema(nil, nil)},
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package frombus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Export is one node's placement file as the composition input the controller fills says it (novox/hq ADR 0555 §6,
|
||||
// issue 1126): the inverse of Input for what a person decides, so the controller's one read-only verb
|
||||
// `placement-export` answers, from its own store by its own code, exactly what the seed writes into the catalogue.
|
||||
// What the machine observes (its record) is not in it.
|
||||
func Export(in compose.Input, node string) (delivery.Placement, error) {
|
||||
var n *compose.Node
|
||||
for i := range in.Nodes {
|
||||
if in.Nodes[i].Name == node {
|
||||
n = &in.Nodes[i]
|
||||
}
|
||||
}
|
||||
if n == nil {
|
||||
return delivery.Placement{}, fmt.Errorf("frombus: the input has no node %q", node)
|
||||
}
|
||||
p := delivery.Placement{Node: node, 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}}
|
||||
// The tunnel found at the node's adoption: the one its private network took over, or, on the hub, the mesh's adopted
|
||||
// one whether or not the hub took it; and the names a person gave the hub's carried peers.
|
||||
if t, ok := in.Tunnels[node]; ok {
|
||||
rt := recordTunnelOf(t.Tunnel)
|
||||
p.Decided.Tunnel = &rt
|
||||
} else if in.Adopted != nil && in.Adopted.Hub == node {
|
||||
rt := recordTunnelOf(in.Adopted.Tunnel)
|
||||
p.Decided.Tunnel = &rt
|
||||
}
|
||||
if n.Place.Hub {
|
||||
for _, c := range in.CarriedPeers {
|
||||
if c.Named != "" {
|
||||
if p.Decided.PeerNames == nil {
|
||||
p.Decided.PeerNames = map[string]string{}
|
||||
}
|
||||
p.Decided.PeerNames[c.PublicKey] = c.Named
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, module := range n.Assigned {
|
||||
m := delivery.PlacedModule{Module: module}
|
||||
for port, at := range n.Ports[module] {
|
||||
if m.Ports == nil {
|
||||
m.Ports = map[string]int{}
|
||||
}
|
||||
m.Ports[strconv.Itoa(port)] = at
|
||||
}
|
||||
for provision, to := range n.Bound[module] {
|
||||
if m.Bindings == nil {
|
||||
m.Bindings = map[string]delivery.Provider{}
|
||||
}
|
||||
m.Bindings[provision] = delivery.Provider{Node: to.Node, Module: to.Module}
|
||||
}
|
||||
for _, layer := range n.Settings[module] {
|
||||
if layer.From == catalogue.MeshWideLayer {
|
||||
m.HasMesh, m.MeshWide = true, layer.Placement
|
||||
} else {
|
||||
m.HasLayer, m.Layer = true, layer.Placement
|
||||
}
|
||||
}
|
||||
p.Modules = append(p.Modules, m)
|
||||
}
|
||||
for provision, to := range n.Pins {
|
||||
if p.Pins == nil {
|
||||
p.Pins = map[string]delivery.Provider{}
|
||||
}
|
||||
p.Pins[provision] = delivery.Provider{Node: to.Node, Module: to.Module}
|
||||
}
|
||||
for _, h := range in.Holdings {
|
||||
if h.Node == node {
|
||||
p.Holds = append(p.Holds, delivery.Holding{Claim: h.Claim, Scope: h.Scope, Module: h.Module, Site: h.Site, Kind: h.Kind})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(p.Holds, func(i, j int) bool {
|
||||
if p.Holds[i].Claim != p.Holds[j].Claim {
|
||||
return p.Holds[i].Claim < p.Holds[j].Claim
|
||||
}
|
||||
return p.Holds[i].Module < p.Holds[j].Module
|
||||
})
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func recordTunnelOf(t compose.Tunnel) delivery.RecordTunnel {
|
||||
out := delivery.RecordTunnel{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 _, peer := range t.Peers {
|
||||
out.Peers = append(out.Peers, delivery.RecordTunnelPeer{PublicKey: peer.PublicKey, Address: peer.Address})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func TestInput(t *testing.T) {
|
||||
"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-terminal", Scope: "mesh", Module: "xterm"}}},
|
||||
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}}}},
|
||||
@@ -116,7 +116,7 @@ func TestInput(t *testing.T) {
|
||||
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"}},
|
||||
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: "{}"}},
|
||||
Tunnels: map[string]compose.FoundTunnel{"anchor": {Tunnel: tun}},
|
||||
Adopted: &compose.AdoptedTunnel{Tunnel: tun, Hub: "anchor", Adopted: true},
|
||||
@@ -153,6 +153,22 @@ func TestInput(t *testing.T) {
|
||||
t.Fatalf("compositions differ or are empty: %d from the bus, %d from the store", len(fromBus), len(fromStore))
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user