Files
mesh-contracts/placement/go/clients/meshmoduleassignerclient/placement.go
T

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
}