// Package assignments is the assigner's own check of assignments and of a proposal that changes it (novox/hq ADR 0567 §2, // ADR 0634 §1 on where a seat's code goes): what the holder of the seat mesh-module-assigner decides before it writes // anything. The layouts it works on are the seat's contract's (package meshmoduleassigner); what every reader of the // assignments shares (the keys assembled into a node's assignments, the node key's digest, who holds a seat) is the seat's // client's (package meshmoduleassignerclient). Nothing here is read by another module. package assignment import ( "encoding/json" "fmt" "sort" "strings" client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient" seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/keys" ) // KeptApart are the modules never placed on one node, each row naming the record that asks it (novox/hq ADR 0564 clause 6 // and Limits 5; issue 1952 S8): no manifest word says it, so the check below holds the table. mesh-watcher hears the // self-check's `checked` from a second machine (ADR 0227 §6), which on the self-check's own node would fall silent with // it. var KeptApart = []struct{ A, B, Record string }{ {A: "mesh-watcher", B: "mesh-self-check", Record: "ADR 0227 §6, ADR 0564 clause 6"}, } // CheckAssignments is the holder's check of a proposal laid over every assignments (ADR 0567 §2), over the assignments by // node, the modules that are known and the nodes with a record. It returns every refusal, in node order, never only the // first: assignments naming a node not enrolled or a module that is not known (held by the indexer, or provided by the // mesh itself: the caller's `known` says both); two modules of one node on the same host port; a pin, binding or holding // naming a provider that is not placed where it says; a seat of scope mesh held twice, unless the seat is replicated // (catalogue.Seat's Replicated, ADR 0223), which is held once per node; two nodes with one address on the private // network; more than one hub; two modules KeptApart on one node. A host port a manifest declares without a number and a // requirement left unbound are the composition's to find (its ForeseenPorts and Unbound). func CheckAssignments(assignments map[string]seat.Assignments, known func(module string) bool, enrolled func(node string) bool) []string { var refusals []string nodes := sortedKeys(assignments) placed := func(p seat.Provider) bool { other, ok := assignments[p.Node] if !ok { return false } for _, m := range other.Modules { if m.Module == p.Module { return true } } return false } meshSeats := map[string]string{} addresses := map[string]string{} var hubs []string for _, node := range nodes { p := assignments[node] if p.Node != node { refusals = append(refusals, fmt.Sprintf("the assignments of %s names the node %q", node, p.Node)) } if a := strings.TrimSpace(p.Decided.Address); a != "" { if other, twice := addresses[a]; twice { refusals = append(refusals, fmt.Sprintf("%s and %s both have the address %s on the private network", other, node, a)) } addresses[a] = node } if p.Decided.Hub { hubs = append(hubs, node) } if !enrolled(node) { refusals = append(refusals, fmt.Sprintf("%s is a node with no record", node)) } placedHere := map[string]bool{} for _, m := range p.Modules { placedHere[m.Module] = true } for _, apart := range KeptApart { if placedHere[apart.A] && placedHere[apart.B] { refusals = append(refusals, fmt.Sprintf("%s: %s and %s are never placed on one node (%s)", node, apart.A, apart.B, apart.Record)) } } hostPorts := map[int64]string{} for _, m := range p.Modules { if !known(m.Module) { refusals = append(refusals, fmt.Sprintf("%s: the indexer holds no module %s", node, m.Module)) } for _, port := range sortedKeys(m.Ports) { at := m.Ports[port] if other, clash := hostPorts[at]; clash && other != m.Module { refusals = append(refusals, fmt.Sprintf("%s: %s and %s are both published on host port %d", node, other, m.Module, at)) } hostPorts[at] = m.Module } for _, provision := range sortedKeys(m.Bindings) { if to := m.Bindings[provision]; !placed(to) { refusals = append(refusals, fmt.Sprintf("%s: %s's %s is bound to %s/%s, which is not placed there", node, m.Module, provision, to.Node, to.Module)) } } } for _, provision := range sortedKeys(p.Pins) { if to := p.Pins[provision]; !placed(to) { refusals = append(refusals, fmt.Sprintf("%s: the pin of %s names %s/%s, which is not placed there", node, provision, to.Node, to.Module)) } } for _, h := range p.Holds { if h.Scope != "mesh" { continue } name := h.Claim + "/" + h.Kind if s, known := catalogue.SeatNamed(h.Claim); known && s.Replicated { // A replicated mesh seat (ADR 0223) is held on several nodes at once, each answering the same thing; // it is still held once on each node. name += "/" + node } if other, twice := meshSeats[name]; twice { refusals = append(refusals, fmt.Sprintf("the seat %s, held once for the mesh, is held on %s and %s", h.Claim, other, node)) } meshSeats[name] = node } } if len(hubs) > 1 { refusals = append(refusals, fmt.Sprintf("the private network has one hub, and %s are each placed as it", strings.Join(hubs, ", "))) } return refusals } // Change is what `assignments-changed` says of one node (ADR 0567 §6): the node, the revision of its key before (0: it had // none) and after (0: the key was deleted), and its modules added, removed or changed against the assignments before. type Change struct { Node string `json:"node"` Before uint64 `json:"before"` After uint64 `json:"after"` Added []string `json:"added,omitempty"` Removed []string `json:"removed,omitempty"` Changed []string `json:"changed,omitempty"` } // Body is the change as the seat's event carries it, one entry of its `nodes`. func (c Change) Body() map[string]any { raw, _ := json.Marshal(c) var out map[string]any _ = json.Unmarshal(raw, &out) return out } // ChangeOf is what changed from before to after on one node; before is the zero Assignments for a node new to assignments. // A change of the node's pins or holdings counts as a change of every module placed on it both times, since the // composition of each may read them. func ChangeOf(before, after seat.Assignments) Change { c := Change{Node: after.Node} if c.Node == "" { c.Node = before.Node } was := map[string]seat.PlacedModule{} for _, m := range before.Modules { was[m.Module] = m } nodeWide := !jsonEqual(before.Pins, after.Pins) || !jsonEqual(before.Holds, after.Holds) now := map[string]bool{} for _, m := range after.Modules { now[m.Module] = true old, ok := was[m.Module] switch { case !ok: c.Added = append(c.Added, m.Module) case nodeWide || !jsonEqual(old, m): c.Changed = append(c.Changed, m.Module) } } for _, m := range before.Modules { if !now[m.Module] { c.Removed = append(c.Removed, m.Module) } } sort.Strings(c.Added) sort.Strings(c.Removed) sort.Strings(c.Changed) return c } // SortChanges orders assignments-changed event's nodes by node. func SortChanges(c []Change) { sort.Slice(c, func(i, j int) bool { return c[i].Node < c[j].Node }) } func jsonEqual(a, b any) bool { ja, _ := json.Marshal(a) jb, _ := json.Marshal(b) return string(ja) == string(jb) } // SeatHoldersOf is the state composed from every node's assignments (ADR 0567 §1): each holding by the seat it claims, // keyed as the client's SeatHoldersKey says, with the module holding it and its node. A claim naming a seat's former name // is keyed by the seat's name (ADR 0122); one naming no seat is kept, marked Outside. Deterministic, so a writer writes // only a key that differs. func SeatHoldersOf(assignments map[string]seat.Assignments) map[string]seat.SeatHolders { out := map[string]seat.SeatHolders{} for _, n := range sortedKeys(assignments) { for _, h := range assignments[n].Holds { node := "" if h.Scope == "node" { node = n } s, known := catalogue.SeatNamed(h.Claim) name := h.Claim if known { name = s.Name } key, err := client.SeatHoldersKey(name, node) if err != nil { continue } sh := out[key] sh.V, sh.Seat, sh.Scope, sh.Node, sh.Outside = keys.Version, name, h.Scope, node, !known sh.Holders = append(sh.Holders, seat.SeatHoldersHoldersItem{Module: h.Module, Node: n}) out[key] = sh } } for k, sh := range out { sort.Slice(sh.Holders, func(i, j int) bool { return sh.Holders[i].Node+"/"+sh.Holders[i].Module < sh.Holders[j].Node+"/"+sh.Holders[j].Module }) out[k] = sh } return out } func sortedKeys[V any](m map[string]V) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out }