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 ":": 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 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::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 `forNode` reaches it: `.internal:` over the private network, or // `127.0.0.1:` 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) }