From d3436abd8283c72b45e80d83406e8afbfbce1577 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 06:34:19 +0000 Subject: [PATCH] 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. --- go/compose/rendering.go | 532 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 go/compose/rendering.go diff --git a/go/compose/rendering.go b/go/compose/rendering.go new file mode 100644 index 0000000..c54e571 --- /dev/null +++ b/go/compose/rendering.go @@ -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 ":": 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::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: `.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 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) +}