// 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`; 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 }