Seals are reused and compared by slot (resource, field), never across resources; one rule says which resolved config values are laid; a seat path that steps back is owned by the seat it steps back for, whatever the seat order; a folded module of an old-form build is composed from what runs of it; the operator export's file is carried as it runs; a differing resource names its differing JSON paths, found before its bodies are cut, and an answer is fitted to the node's key.
543 lines
18 KiB
Go
543 lines
18 KiB
Go
package compose
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
)
|
|
|
|
// The contracts of the node-engine's statement that gate what a machine is sent, as the controller's link package names them
|
|
// (mesh-controller internal/link/protocol.go at 6c2a8d62). An older engine parses strictly and refuses a field it does not
|
|
// know, whole.
|
|
const (
|
|
ReadinessContract = 2 // the engine reads a resource's `health` (novox/hq ADR 0240)
|
|
RootContract = 3 // the engine judges a user's declared `root` (ADR 0266)
|
|
BusyContract = 8 // the engine reads a container's `busy` (ADR 0335)
|
|
HandedContract = 11 // the engine writes a filled file on the provisioner client's word alone
|
|
VaultBoxContract = 12 // the engine opens the vault's box
|
|
)
|
|
|
|
// The provisioner client's seat and own secret (the controller's broker.NodeProvisionerSeat and ProvisionerSecret).
|
|
const (
|
|
NodeProvisionerSeat = "node-provisioner"
|
|
ProvisionerSecret = "provisioner-broker"
|
|
)
|
|
|
|
// ErrPortTaken is the controller's refusal for a machine port held twice on one machine.
|
|
var ErrPortTaken = errors.New("that port is already held on this machine")
|
|
|
|
// Placed is the place half of a rendering (plan.go renderingFor): where a machine puts what its modules need reachable, the
|
|
// seats and who holds them, the names of the mesh, the networks, accounts and the operator's export. It is a catalogue.Rendering
|
|
// with those fields set, and the ports it could not read named foreseen. Nothing was allocated.
|
|
type Placed struct {
|
|
Rendering catalogue.Rendering
|
|
// ForeseenPorts are the ports the mesh may assign that the Input holds none for, as "<module>:<port>": composed with the
|
|
// manifest's own number as the Reading mode composes them, and never invented. The controller's adapter allocates and
|
|
// composes again.
|
|
ForeseenPorts []string
|
|
}
|
|
|
|
// Places composes the place half of the rendering for a node (the controller's renderingFor in the Reading and Foreseeing modes,
|
|
// without the credentials): over an Input, allocating nothing and changing nothing in it. The refusals are the controller's,
|
|
// in its words.
|
|
func Places(in Input, node string) (Placed, error) {
|
|
c := newComposer(in)
|
|
resolved, err := c.planFor(node)
|
|
if err != nil {
|
|
return Placed{}, err
|
|
}
|
|
return c.places(node, resolved)
|
|
}
|
|
|
|
func (c *composer) places(nodeName string, resolved Resolved) (Placed, error) {
|
|
var out Placed
|
|
node, err := c.in.node(nodeName)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
plan, settings := resolved.Plan, resolved.Settings
|
|
r := &out.Rendering
|
|
|
|
// Where this machine puts what each module needs reachable (novox/hq ADR 0038): what it was already given, and the ports
|
|
// it was given over those (ADR 0100).
|
|
given, err := c.portsFor(node, plan, settings, r, &out.ForeseenPorts)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
|
|
// Hands: whether the engine writes a filled file on the provisioner client's word alone; engines older than a contract are
|
|
// not sent the field they would refuse.
|
|
r.Hands = takesHanded(node)
|
|
r.Provisioner = provisionerAccount(plan)
|
|
r.ReadsHealth = node.Stated && node.Contract >= ReadinessContract
|
|
r.JudgesRoot = node.Stated && node.Contract >= RootContract
|
|
r.ReadsBusy = node.Stated && node.Contract >= BusyContract
|
|
|
|
// And who else is on the private network, which is what a rule saying "from the mesh" resolves to.
|
|
r.Mesh = c.onThePrivateNetwork()
|
|
// The private network's range, offered to a module as ${machine:mesh-range}.
|
|
r.MeshRange = c.overlayRange()
|
|
|
|
// The artifact store as this node reaches it now, with what the mesh has built.
|
|
store, err := c.artifactStoreAddress(nodeName)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
r.ArtifactStore = store
|
|
r.Built = make(map[string]bool, len(c.in.Built))
|
|
for _, repository := range c.in.Built {
|
|
r.Built[repository] = true
|
|
}
|
|
|
|
// And every machine's name, so a container can reach one.
|
|
r.Names = c.namesInTheMesh()
|
|
|
|
// Each machine's operator account, so an ssh Host block can name the login for every node (novox/hq to-be 29).
|
|
r.Accounts = map[string]string{}
|
|
for _, n := range c.in.sorted() {
|
|
if n.Account != "" {
|
|
r.Accounts[n.Name] = n.Account
|
|
}
|
|
}
|
|
|
|
// The roster is the machines and nothing else (novox/hq ADR 0191).
|
|
r.Machines = make(map[string]string, len(r.Names))
|
|
for name, at := range r.Names {
|
|
r.Machines[name] = at
|
|
}
|
|
|
|
// And every zone a module in the mesh answers itself (novox/hq ADR 0199).
|
|
zones, err := c.zonesInTheMesh()
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
r.Zones = zones
|
|
|
|
// And who holds each replicated seat, where (novox/hq ADR 0223).
|
|
r.Holders = c.replicatedHolders()
|
|
|
|
r.Suffix, r.TunnelInterface = c.suffix(), overlay.Interface
|
|
|
|
// And, for a module that keeps them, every operator-sealed secret in the mesh (novox/hq ADR 0085, amended).
|
|
for _, m := range plan.Modules {
|
|
if m.Keeps == "" {
|
|
continue
|
|
}
|
|
r.Kept = c.in.OperatorExport
|
|
if r.Kept == nil {
|
|
r.KeptRuns = c.in.KeptRuns[node.Name]
|
|
}
|
|
break
|
|
}
|
|
|
|
// Whether this node is adopted (novox/hq ADR 0100), and which modules were taken there.
|
|
r.Adopted = node.Adopted
|
|
var taken map[string]bool
|
|
if node.Adopted {
|
|
taken = map[string]bool{}
|
|
for _, m := range node.Taken {
|
|
taken[m] = true
|
|
}
|
|
}
|
|
r.Taken = taken
|
|
|
|
// Where this node put the foundation's servers (novox/hq 04-ISSUES/102).
|
|
seats, err := c.seatsOn(node, plan.Modules, node.Adopted, taken)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
r.Seats = seats
|
|
|
|
// Which of this machine's links face outside (novox/hq ADR 0140), and which of its networks count as its local network
|
|
// (ADR 0340).
|
|
r.OutwardLinks = node.OutwardLinks
|
|
r.LocalNetworks = catalogue.CountedLocalNetworks(localNetworksOf(node))
|
|
|
|
// Where this machine reaches each mesh seat's holder, for ${seat:<seat>:reach} (novox/hq ADR 0222).
|
|
if store != "" {
|
|
r.SeatReach = map[string]string{"mesh-artifact-store": store}
|
|
}
|
|
r.Settings = settings
|
|
r.Given = given
|
|
return out, nil
|
|
}
|
|
|
|
// takesHanded says whether a machine's newest statement is of an engine that writes a filled file on the provisioner client's
|
|
// word alone.
|
|
func takesHanded(n Node) bool { return n.Stated && n.Contract >= HandedContract }
|
|
|
|
// provisionerAccount is the account the provisioner's client runs as on this machine (novox/hq ADR 0349 clause 4): the user
|
|
// the process of the module running it runs as, empty where no such module is on the machine.
|
|
func provisionerAccount(plan catalogue.Resolution) string {
|
|
for _, m := range plan.Modules {
|
|
if !runsTheProvisionerClient(m) {
|
|
continue
|
|
}
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "process" {
|
|
if user, _ := r["user"].(string); user != "" && user != "root" {
|
|
return user
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// runsTheProvisionerClient says a module runs its machine's provisioner client: it claims the seat and declares the own
|
|
// secret that account is delivered in.
|
|
func runsTheProvisionerClient(m catalogue.Manifest) bool {
|
|
_, reads := m.OwnSecrets[ProvisionerSecret]
|
|
return reads && m.ClaimsSeat(NodeProvisionerSeat)
|
|
}
|
|
|
|
// portsFor is where a node puts what each module needs reachable, in the Reading mode: the ports it was given win, then the
|
|
// ones the mesh assigned, and a port the Input holds none for is composed as the manifest says and named foreseen. One holder
|
|
// per machine port across the node's modules: settings written before this was refused are refused here.
|
|
func (c *composer) portsFor(node Node, plan catalogue.Resolution, settings catalogue.SettingsBy, r *catalogue.Rendering,
|
|
foreseen *[]string) (map[string]map[int]int, error) {
|
|
already := node.Ports
|
|
|
|
given := map[string]map[int]int{}
|
|
for _, m := range plan.Modules {
|
|
g, err := catalogue.GivenPorts(m, settings[m.Module])
|
|
if err != nil {
|
|
// A given port its definition no longer publishes: the module is left out of the declaration, by name, when it is
|
|
// composed (novox/hq ADR 0163, rule 6) -- never the machine refused here for it.
|
|
continue
|
|
}
|
|
if g != nil {
|
|
given[m.Module] = g
|
|
}
|
|
}
|
|
holders := map[int]string{}
|
|
for _, m := range plan.Modules {
|
|
for _, at := range given[m.Module] {
|
|
if other, twice := holders[at]; twice && other != m.Module {
|
|
return nil, fmt.Errorf("%w: %s and %s are both given machine port %d on %s", ErrPortTaken, other, m.Module,
|
|
at, node.Name)
|
|
}
|
|
holders[at] = m.Module
|
|
}
|
|
}
|
|
|
|
ports := map[string]map[int]int{}
|
|
for _, m := range plan.Modules {
|
|
for _, l := range m.Listens {
|
|
if at, isGiven := given[m.Module][l.Port]; isGiven {
|
|
if ports[m.Module] == nil {
|
|
ports[m.Module] = map[int]int{}
|
|
}
|
|
ports[m.Module][l.Port] = at
|
|
continue
|
|
}
|
|
// Only a port the module actually publishes is the mesh's to move. Recorded either way, because this map means where
|
|
// this module's port is on this machine.
|
|
where, mayAssign := m.MachineSide(l.Port)
|
|
if mayAssign {
|
|
// Whatever was chosen last time, and nothing if there was no last time.
|
|
if at, known := already[m.Module][l.Port]; known {
|
|
where = at
|
|
} else {
|
|
*foreseen = append(*foreseen, fmt.Sprintf("%s:%d", m.Module, l.Port))
|
|
}
|
|
}
|
|
if ports[m.Module] == nil {
|
|
ports[m.Module] = map[int]int{}
|
|
}
|
|
ports[m.Module][l.Port] = where
|
|
}
|
|
}
|
|
r.Ports = ports
|
|
return given, nil
|
|
}
|
|
|
|
// onThePrivateNetwork is every node's address on the private network, sorted: the same set the names mean by it, so a rule
|
|
// saying "from the mesh" admits exactly the machines the mesh names. A node with no address is left out, not rendered as an
|
|
// empty source.
|
|
func (c *composer) onThePrivateNetwork() []string {
|
|
var out []string
|
|
for _, p := range c.onTheNetwork() {
|
|
if strings.TrimSpace(p.Place.Address) != "" {
|
|
out = append(out, p.Place.Address)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// namesInTheMesh is every machine's internal name and the address behind it, for every machine on the private network, and
|
|
// the carried peers the operator has named (novox/hq issue 112).
|
|
func (c *composer) namesInTheMesh() map[string]string {
|
|
out := map[string]string{}
|
|
for _, p := range c.onTheNetwork() {
|
|
out[c.internalName(p.Name)] = p.Place.Address
|
|
}
|
|
for _, p := range c.in.CarriedPeers {
|
|
if p.Named == "" || p.EnrolledAs != "" {
|
|
continue
|
|
}
|
|
if _, taken := out[c.internalName(p.Named)]; taken {
|
|
continue // a node of the mesh owns the name; the stale statement loses
|
|
}
|
|
out[c.internalName(p.Named)] = p.Address
|
|
}
|
|
return out
|
|
}
|
|
|
|
// artifactStoreOnNetwork is the machine on this network that offers the artifact store, and the port that machine put it on.
|
|
func (c *composer) artifactStoreOnNetwork(on map[string]bool) (node, port string, found bool, err error) {
|
|
providers := map[string]catalogue.Manifest{}
|
|
for name, m := range c.in.Catalogue {
|
|
if _, offers := m.Serves[catalogue.ArtifactStoreProvision]; offers {
|
|
providers[name] = m
|
|
}
|
|
}
|
|
if len(providers) == 0 {
|
|
return "", "", false, nil
|
|
}
|
|
// In a stated order, so two machines offering it would always answer the same one.
|
|
machines := make([]string, 0, len(on))
|
|
for machine := range on {
|
|
machines = append(machines, machine)
|
|
}
|
|
sort.Strings(machines)
|
|
for _, machine := range machines {
|
|
n, err := c.in.node(machine)
|
|
if err != nil {
|
|
return "", "", false, fmt.Errorf("reading what %s is assigned: %w", machine, err)
|
|
}
|
|
for _, a := range n.Assigned {
|
|
m, offers := providers[a]
|
|
if !offers {
|
|
continue
|
|
}
|
|
serves, _, err := c.servedOnNode(machine, m, catalogue.ArtifactStoreProvision)
|
|
if err != nil {
|
|
return "", "", false, fmt.Errorf("reading where %s puts the artifact store: %w", machine, err)
|
|
}
|
|
if p, ok := serves["port"]; ok {
|
|
return machine, fmt.Sprintf("%v", p), true, nil
|
|
}
|
|
}
|
|
}
|
|
return "", "", false, nil
|
|
}
|
|
|
|
// ArtifactStoreAddress is the artifact store as node reaches it, by the rule its composition routes every kept reference
|
|
// with (artifactStoreAddress): empty when node reaches none. A node composing its own modules reads a templates archive
|
|
// there (issue 1126).
|
|
func ArtifactStoreAddress(in Input, node string) (string, error) {
|
|
return newComposer(in).artifactStoreAddress(node)
|
|
}
|
|
|
|
// artifactStoreAddress is the artifact store as `forNode` reaches it: `<node>.internal:<port>` over the private network, or
|
|
// `127.0.0.1:<port>` for the node that holds the store itself when nothing is on the network yet, and empty for any other
|
|
// (novox/hq 04-ISSUES/102).
|
|
func (c *composer) artifactStoreAddress(forNode string) (string, error) {
|
|
onNetwork, err := c.whereEveryoneIs()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
on := map[string]bool{}
|
|
for name := range onNetwork {
|
|
on[name] = true
|
|
}
|
|
node, port, found, err := c.artifactStoreOnNetwork(on)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if found {
|
|
return c.internalName(node) + ":" + port, nil
|
|
}
|
|
all := map[string]bool{}
|
|
for _, n := range c.in.Nodes {
|
|
all[n.Name] = true
|
|
}
|
|
holder, port, found, err := c.artifactStoreOnNetwork(all)
|
|
if err != nil || !found || holder != forNode {
|
|
return "", err
|
|
}
|
|
return "127.0.0.1:" + port, nil
|
|
}
|
|
|
|
// zonesInTheMesh is every zone a module in the mesh declares, where the mesh placed it (novox/hq ADR 0199): the zone settled
|
|
// from that node's settings, the node's private address, the port the answering listen is published on there. A node whose set
|
|
// does not compose declares nothing and is passed over; what the mesh refuses about the zones together is refused, by name.
|
|
func (c *composer) zonesInTheMesh() ([]catalogue.ZoneAt, error) {
|
|
address := map[string]string{}
|
|
for _, n := range c.in.sorted() {
|
|
if strings.TrimSpace(n.Place.Address) != "" {
|
|
address[n.Name] = n.Place.Address
|
|
}
|
|
}
|
|
var zones []catalogue.ZoneAt
|
|
var public []string
|
|
for _, n := range c.in.sorted() {
|
|
resolved, err := c.planFor(n.Name)
|
|
switch {
|
|
case unresolvable(err):
|
|
continue
|
|
case err != nil:
|
|
return nil, fmt.Errorf("the zones %s answers cannot be read: %w", n.Name, err)
|
|
}
|
|
plan := resolved.Plan
|
|
if plan.PublicDomain != "" {
|
|
public = append(public, plan.PublicDomain)
|
|
}
|
|
for _, m := range plan.Modules {
|
|
if m.Zone == nil {
|
|
continue
|
|
}
|
|
at := address[n.Name]
|
|
if at == "" {
|
|
// Not on the private network yet: nothing could reach its answerer.
|
|
continue
|
|
}
|
|
published, layers, err := c.portsGivenOn(n.Name, m)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the zone %s declares on %s cannot be read: %w", m.Module, n.Name, err)
|
|
}
|
|
z, err := catalogue.ZoneOn(m, layers, published, n.Name, at)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
zones = append(zones, *z)
|
|
}
|
|
}
|
|
if problems := catalogue.ZonesProblems(zones, c.suffix(), public); len(problems) > 0 {
|
|
return nil, fmt.Errorf("the mesh's zones cannot be forwarded:\n - %s", strings.Join(problems, "\n - "))
|
|
}
|
|
return zones, nil
|
|
}
|
|
|
|
// replicatedHolders is, for each replicated mesh seat, every machine on the private network holding it, by internal name at its
|
|
// private address (novox/hq ADR 0223): the holders on record, and only the sole claimant when there are none. A holder off
|
|
// the private network is left out.
|
|
func (c *composer) replicatedHolders() map[string]map[string]string {
|
|
var replicated []catalogue.Seat
|
|
for _, s := range catalogue.Seats() {
|
|
if s.Replicated && s.Scope == catalogue.ScopeMesh {
|
|
replicated = append(replicated, s)
|
|
}
|
|
}
|
|
if len(replicated) == 0 {
|
|
return nil
|
|
}
|
|
address := map[string]string{}
|
|
for _, p := range c.onTheNetwork() {
|
|
address[p.Name] = p.Place.Address
|
|
}
|
|
// Every module of the catalogue with the nodes it is assigned on, sorted, as the controller's Catalogued answers.
|
|
on := map[string][]string{}
|
|
for _, n := range c.in.sorted() {
|
|
for _, module := range n.Assigned {
|
|
on[module] = append(on[module], n.Name)
|
|
}
|
|
}
|
|
names := make([]string, 0, len(c.in.Catalogue))
|
|
for name := range c.in.Catalogue {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
out := map[string]map[string]string{}
|
|
for _, seat := range replicated {
|
|
var nodes []string
|
|
for _, h := range c.in.Holdings {
|
|
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == seat.Name && h.Scope == seat.Scope {
|
|
nodes = append(nodes, h.Node)
|
|
}
|
|
}
|
|
if len(nodes) == 0 {
|
|
var derived []string
|
|
for _, name := range names {
|
|
for _, cl := range c.in.Catalogue[name].Claims {
|
|
if cs, ok := catalogue.SeatNamed(cl.Name); ok && cs.Name == seat.Name && cl.At() == seat.Scope {
|
|
derived = append(derived, on[name]...)
|
|
}
|
|
}
|
|
}
|
|
if len(derived) == 1 {
|
|
nodes = derived
|
|
}
|
|
}
|
|
at := map[string]string{}
|
|
for _, n := range nodes {
|
|
if address[n] != "" {
|
|
at[c.internalName(n)] = address[n]
|
|
}
|
|
}
|
|
out[seat.Name] = at
|
|
}
|
|
return out
|
|
}
|
|
|
|
// seatsOn is where a node put the holder of each mesh-scoped seat, by seat and by the port the holder's software uses: what
|
|
// ${seat:...} answers with (novox/hq 04-ISSUES/102). Read for every module in the catalogue that claims a seat, in this node's set
|
|
// or not; a holder in this node's set wins over one that is not.
|
|
func (c *composer) seatsOn(node Node, inSet []catalogue.Manifest, adopted bool, taken map[string]bool) (map[string]map[int]int, error) {
|
|
assigned := map[string]bool{}
|
|
for _, m := range inSet {
|
|
assigned[m.Module] = true
|
|
}
|
|
names := make([]string, 0, len(c.in.Catalogue))
|
|
for name := range c.in.Catalogue {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
seats := map[string]map[int]int{}
|
|
for _, name := range names {
|
|
m := c.in.Catalogue[name]
|
|
var claims []string
|
|
for _, cl := range m.Claims {
|
|
if cl.At() == catalogue.ScopeMesh {
|
|
claims = append(claims, cl.Name)
|
|
}
|
|
}
|
|
if len(claims) == 0 {
|
|
continue
|
|
}
|
|
// Only a port the node was given or the mesh assigned, never the manifest's own number. And on an adopted node a holder
|
|
// assigned but not yet taken is the found container, so its mesh-assigned ports do not count there; a given port does.
|
|
ports, _, err := c.portsGivenOn(node.Name, m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if adopted && !taken[name] {
|
|
layers := node.Settings[m.Module]
|
|
ports = map[int]int{}
|
|
if given, err := catalogue.GivenPorts(m, layers); err == nil {
|
|
ports = given
|
|
}
|
|
}
|
|
if len(ports) == 0 {
|
|
continue
|
|
}
|
|
for _, seat := range claims {
|
|
if seats[seat] == nil {
|
|
seats[seat] = map[int]int{}
|
|
}
|
|
for wanted, at := range ports {
|
|
if _, said := seats[seat][wanted]; said && !assigned[name] {
|
|
continue
|
|
}
|
|
seats[seat][wanted] = at
|
|
}
|
|
}
|
|
}
|
|
return seats, nil
|
|
}
|
|
|
|
// localNetworksOf is a machine's attached networks judged (novox/hq ADR 0340): which count as its local network, from what it
|
|
// last said of itself and the links it said face outside it. A machine that has said none judges to nothing.
|
|
func localNetworksOf(n Node) []catalogue.LocalNetwork {
|
|
if !n.NetworksSaid {
|
|
return nil
|
|
}
|
|
return catalogue.JudgeLocalNetworks(n.Networks, n.OutwardLinks, overlay.Interface)
|
|
}
|