Compose the place half of a rendering over an Input: ports never allocated, seats, networks, names, zones, accounts, the operator's export (issue 1363). Ported from mesh-control 6c2a8d62 plan.go renderingFor, network.go, holdings.go, local_network.go in their Reading and Foreseeing modes. A port the Input holds none for is composed as the manifest says and named foreseen.
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
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 = overlay.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
|
||||
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[overlay.InternalName(p.Name)] = p.Place.Address
|
||||
}
|
||||
for _, p := range c.in.CarriedPeers {
|
||||
if p.Named == "" || p.EnrolledAs != "" {
|
||||
continue
|
||||
}
|
||||
if _, taken := out[overlay.InternalName(p.Named)]; taken {
|
||||
continue // a node of the mesh owns the name; the stale statement loses
|
||||
}
|
||||
out[overlay.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 `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 overlay.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, overlay.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[overlay.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)
|
||||
}
|
||||
Reference in New Issue
Block a user