296 lines
10 KiB
Go
296 lines
10 KiB
Go
// Package meshmoduleassignerclient is the logic every module that reads where modules are placed shares over the seat
|
|
// mesh-module-assigner (novox/hq ADR 0567, ADR 0634 §4, ADR 0625 §4): the keys of its state `placement` assembled into a
|
|
// node's placement, who stands on which node since when, who holds a seat, and what a placement says to the composition.
|
|
// The layouts are the seat's contract's (package meshmoduleassigner); the check of a proposal is the assigner's own.
|
|
package meshmoduleassignerclient
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
)
|
|
|
|
// Version is the version of every value of the seat's states.
|
|
const Version = keys.Version
|
|
|
|
// NodeWord is the last word of a node key, `<node>.node`; no module is named it.
|
|
const NodeWord = "node"
|
|
|
|
const (
|
|
legacyWord = "placement"
|
|
digestLen = 16
|
|
)
|
|
|
|
// ErrUnknownVersion is what a decode returns for a value whose version is missing or not Version.
|
|
var ErrUnknownVersion = errors.New("meshmoduleassignerclient: unknown value version")
|
|
|
|
var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`)
|
|
|
|
func checkName(kind, name string) error {
|
|
if name == "" {
|
|
return fmt.Errorf("meshmoduleassignerclient: the %s name is empty", kind)
|
|
}
|
|
if !nameOK.MatchString(name) {
|
|
return fmt.Errorf("meshmoduleassignerclient: the %s name %q holds a dot, a wildcard, whitespace or a character a key refuses", kind, name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkVersion(data []byte) error {
|
|
var probe struct {
|
|
V *int `json:"v"`
|
|
}
|
|
if err := json.Unmarshal(data, &probe); err != nil {
|
|
return fmt.Errorf("meshmoduleassignerclient: value is not JSON: %w", err)
|
|
}
|
|
if probe.V == nil {
|
|
return fmt.Errorf("%w: no version in the value, this reader reads %d", ErrUnknownVersion, Version)
|
|
}
|
|
if *probe.V != Version {
|
|
return fmt.Errorf("%w: the value is version %d, this reader reads %d", ErrUnknownVersion, *probe.V, Version)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CheckPlacement refuses a placement that names a node or module that is not a name, a module twice, a port that is not
|
|
// a number, a binding or pin naming no provider, and a holding by a module not placed on the node.
|
|
func CheckPlacement(p seat.Placement) error {
|
|
if err := checkName("node", p.Node); err != nil {
|
|
return err
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, m := range p.Modules {
|
|
if err := checkName("module", m.Module); err != nil {
|
|
return err
|
|
}
|
|
if seen[m.Module] {
|
|
return fmt.Errorf("meshmoduleassignerclient: the placement of %s names %s twice", p.Node, m.Module)
|
|
}
|
|
seen[m.Module] = true
|
|
for port, at := range m.Ports {
|
|
if n, err := strconv.Atoi(port); err != nil || n <= 0 || n > 65535 {
|
|
return fmt.Errorf("meshmoduleassignerclient: %s's port %q on %s is not a port number", m.Module, port, p.Node)
|
|
}
|
|
if at <= 0 || at > 65535 {
|
|
return fmt.Errorf("meshmoduleassignerclient: %s's port %s on %s is published on %d, which is not a port number", m.Module, port, p.Node, at)
|
|
}
|
|
}
|
|
for provision, to := range m.Bindings {
|
|
if provision == "" || to.Node == "" || to.Module == "" {
|
|
return fmt.Errorf("meshmoduleassignerclient: %s on %s has a binding with no provision, node or module", m.Module, p.Node)
|
|
}
|
|
}
|
|
}
|
|
for provision, to := range p.Pins {
|
|
if provision == "" || to.Node == "" || to.Module == "" {
|
|
return fmt.Errorf("meshmoduleassignerclient: the placement of %s has a pin with no provision, node or module", p.Node)
|
|
}
|
|
}
|
|
for _, h := range p.Holds {
|
|
if h.Claim == "" || !seen[h.Module] {
|
|
return fmt.Errorf("meshmoduleassignerclient: the placement of %s has %s hold %q, which is not a module placed there or names no seat", p.Node, h.Module, h.Claim)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodePlacement is p as JSON at Version; it refuses what DecodePlacement refuses.
|
|
func EncodePlacement(p seat.Placement) ([]byte, error) {
|
|
if err := CheckPlacement(p); err != nil {
|
|
return nil, err
|
|
}
|
|
p.V = Version
|
|
return json.Marshal(p)
|
|
}
|
|
|
|
// DecodePlacement reads a placement file or key value (the form before the node and module keys).
|
|
func DecodePlacement(data []byte) (seat.Placement, error) {
|
|
if err := checkVersion(data); err != nil {
|
|
return seat.Placement{}, err
|
|
}
|
|
var p seat.Placement
|
|
if err := json.Unmarshal(data, &p); err != nil {
|
|
return seat.Placement{}, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err)
|
|
}
|
|
if err := CheckPlacement(p); err != nil {
|
|
return seat.Placement{}, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// SamePlacement says whether two placements are the same value, so the holder writes only when they differ.
|
|
func SamePlacement(a, b seat.Placement) bool {
|
|
a.Revision, b.Revision = "", ""
|
|
ja, errA := EncodePlacement(a)
|
|
jb, errB := EncodePlacement(b)
|
|
return errA == nil && errB == nil && string(ja) == string(jb)
|
|
}
|
|
|
|
// Assigned is the modules placed on the node, in the file's order.
|
|
func Assigned(p seat.Placement) []string {
|
|
out := make([]string, 0, len(p.Modules))
|
|
for _, m := range p.Modules {
|
|
out = append(out, m.Module)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Chosen is p as the composition names a chosen provider.
|
|
func Chosen(p seat.Provider) catalogue.Chosen {
|
|
return catalogue.Chosen{Node: p.Node, Module: p.Module}
|
|
}
|
|
|
|
// ChosenPins is the node's pins as the composition reads them; nil when it has none.
|
|
func ChosenPins(p seat.Placement) map[string]catalogue.Chosen {
|
|
if len(p.Pins) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]catalogue.Chosen, len(p.Pins))
|
|
for provision, to := range p.Pins {
|
|
out[provision] = Chosen(to)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Bound is, by consumer module and provision, where each binding sends it; nil when none.
|
|
func Bound(p seat.Placement) map[string]map[string]catalogue.Chosen {
|
|
var out map[string]map[string]catalogue.Chosen
|
|
for _, m := range p.Modules {
|
|
if len(m.Bindings) == 0 {
|
|
continue
|
|
}
|
|
if out == nil {
|
|
out = map[string]map[string]catalogue.Chosen{}
|
|
}
|
|
out[m.Module] = make(map[string]catalogue.Chosen, len(m.Bindings))
|
|
for provision, to := range m.Bindings {
|
|
out[m.Module][provision] = Chosen(to)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// HostPorts is, by module and the port it declares, the host port it is published on; nil when none. The check has
|
|
// refused a port that is not a number, so none is dropped here.
|
|
func HostPorts(p seat.Placement) map[string]map[int]int {
|
|
var out map[string]map[int]int
|
|
for _, m := range p.Modules {
|
|
if len(m.Ports) == 0 {
|
|
continue
|
|
}
|
|
if out == nil {
|
|
out = map[string]map[int]int{}
|
|
}
|
|
out[m.Module] = make(map[int]int, len(m.Ports))
|
|
for port, at := range m.Ports {
|
|
n, _ := strconv.Atoi(port)
|
|
out[m.Module][n] = int(at)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Layers is one module's settings layers on the node, as the composition reads them: the whole mesh's layer when it has
|
|
// one, then the node's, which carries the module's config values as mesh-config resolved them for this node (values,
|
|
// nil when none) beside its placement; a module with no layer and no value has none. A module in the settings form
|
|
// carries its values in the file (MeshWideValues, Values), which then stand for mesh-config's.
|
|
func Layers(m seat.PlacedModule, node string, values map[string]any) []catalogue.Layer {
|
|
var out []catalogue.Layer
|
|
if m.HasMeshWide || len(m.MeshWide) > 0 || len(m.MeshWideValues) > 0 {
|
|
mesh := map[string]any{}
|
|
for k, v := range m.MeshWideValues {
|
|
mesh[k] = v
|
|
}
|
|
out = append(out, catalogue.Layer{From: catalogue.MeshWideLayer, Values: mesh, Placement: m.MeshWide})
|
|
}
|
|
if len(m.Values) > 0 {
|
|
values = make(map[string]any, len(m.Values))
|
|
for k, v := range m.Values {
|
|
values[k] = v
|
|
}
|
|
}
|
|
if m.HasLayer || len(m.Layer) > 0 || len(values) > 0 {
|
|
if values == nil {
|
|
values = map[string]any{}
|
|
}
|
|
out = append(out, catalogue.Layer{From: node, Values: values, Placement: m.Layer})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Held is the node's holdings as the composition reads them.
|
|
func Held(p seat.Placement) []catalogue.Held {
|
|
out := make([]catalogue.Held, 0, len(p.Holds))
|
|
for _, h := range p.Holds {
|
|
out = append(out, catalogue.Held{Claim: h.Claim, Scope: h.Scope, Node: p.Node, Module: h.Module, Site: h.Site, Kind: h.Kind})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// InsideAddresses is every node's address on the private network, sorted and without repeats.
|
|
func InsideAddresses(placements map[string]seat.Placement) []string {
|
|
seen := map[string]bool{}
|
|
out := []string{}
|
|
for _, p := range placements {
|
|
if a := strings.TrimSpace(p.Decided.Address); a != "" && !seen[a] {
|
|
seen[a] = true
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// digestForm is a placed module as its digest has always been made: the fields in the order the first writer wrote them,
|
|
// so a node key written before this client names a digest this client computes the same.
|
|
type digestForm struct {
|
|
Module string `json:"module"`
|
|
Ports map[string]int64 `json:"ports,omitempty"`
|
|
Bindings map[string]digestProvider `json:"bindings,omitempty"`
|
|
MeshWide map[string]any `json:"mesh-wide,omitempty"`
|
|
HasMesh bool `json:"has-mesh-wide,omitempty"`
|
|
Layer map[string]any `json:"layer,omitempty"`
|
|
HasLayer bool `json:"has-layer,omitempty"`
|
|
MeshValues map[string]any `json:"mesh-wide-values,omitempty"`
|
|
Values map[string]any `json:"values,omitempty"`
|
|
}
|
|
|
|
type digestProvider struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
}
|
|
|
|
// Digest is the digest of a placed module's value, as a node key names it.
|
|
func Digest(m seat.PlacedModule) string {
|
|
var bindings map[string]digestProvider
|
|
if len(m.Bindings) > 0 {
|
|
bindings = make(map[string]digestProvider, len(m.Bindings))
|
|
for k, v := range m.Bindings {
|
|
bindings[k] = digestProvider{Node: v.Node, Module: v.Module}
|
|
}
|
|
}
|
|
raw, _ := json.Marshal(digestForm{Module: m.Module, Ports: m.Ports, Bindings: bindings, MeshWide: m.MeshWide, HasMesh: m.HasMeshWide,
|
|
Layer: m.Layer, HasLayer: m.HasLayer, MeshValues: m.MeshWideValues, Values: m.Values})
|
|
sum := sha256.Sum256(raw)
|
|
return hex.EncodeToString(sum[:])[:digestLen]
|
|
}
|
|
|
|
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
|
|
}
|