234 lines
8.8 KiB
Go
234 lines
8.8 KiB
Go
// 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
|
|
}
|