230 lines
7.5 KiB
Go
230 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The four sources (novox/hq ADR 0348 §2, §3, §9): a module's value on a node resolves, the most specific winning,
|
|
// from the default in its manifest, mesh config for the predefined key it falls back to, node config for that key
|
|
// on the node, and the module's own value on that node. A value naming no predefined key resolves from its default
|
|
// and the module's own value only.
|
|
|
|
// The states this module keeps, and those it reads (its manifest's `state` and `reads`).
|
|
const (
|
|
ValuesState = "values"
|
|
ResolvedState = "resolved"
|
|
PredefinedState = "predefined"
|
|
ProposalsState = "proposals"
|
|
|
|
AssignmentsRead = "mesh-controller.assignments"
|
|
DeclaredConfigRead = "mesh-controller.declared-config"
|
|
PredefinedRead = "mesh-controller.predefined"
|
|
|
|
// HistoryLog is the log of every set and clear, with the proven caller and why (ADR 0348 §9).
|
|
HistoryLog = "history"
|
|
)
|
|
|
|
// The sources a resolved value says it came from.
|
|
const (
|
|
SourceDefault = "default"
|
|
SourceMesh = "mesh"
|
|
SourceNode = "node"
|
|
SourceModule = "module"
|
|
// SourceUnset is a required value nothing sets: mesh-config answers it unset (ADR 0348 §4).
|
|
SourceUnset = "unset"
|
|
)
|
|
|
|
// The levels a value is set at, and how each is keyed in mesh-config_values (ADR 0348 §9).
|
|
const (
|
|
LevelMesh = "mesh"
|
|
LevelNode = "node"
|
|
LevelModule = "module"
|
|
)
|
|
|
|
// Level is where a value is set: the mesh, a node, or a module on a node.
|
|
type Level struct {
|
|
Kind string `json:"level"`
|
|
Node string `json:"node,omitempty"`
|
|
Module string `json:"module,omitempty"`
|
|
}
|
|
|
|
// ValueKey is the level's key for a config key in mesh-config_values.
|
|
func (l Level) ValueKey(key string) string {
|
|
switch l.Kind {
|
|
case LevelNode:
|
|
return "node." + l.Node + "." + key
|
|
case LevelModule:
|
|
return "module." + l.Node + "." + l.Module + "." + key
|
|
}
|
|
return "mesh." + key
|
|
}
|
|
|
|
func (l Level) String() string {
|
|
switch l.Kind {
|
|
case LevelNode:
|
|
return "node config on " + l.Node
|
|
case LevelModule:
|
|
return l.Module + "'s own value on " + l.Node
|
|
}
|
|
return "mesh config"
|
|
}
|
|
|
|
// parseValueKey is the level and the config key a key of mesh-config_values names.
|
|
func parseValueKey(k string) (Level, string, bool) {
|
|
parts := strings.SplitN(k, ".", 4)
|
|
switch {
|
|
case len(parts) >= 2 && parts[0] == LevelMesh:
|
|
return Level{Kind: LevelMesh}, strings.Join(parts[1:], "."), true
|
|
case len(parts) >= 3 && parts[0] == LevelNode:
|
|
return Level{Kind: LevelNode, Node: parts[1]}, strings.Join(parts[2:], "."), true
|
|
case len(parts) == 4 && parts[0] == LevelModule:
|
|
return Level{Kind: LevelModule, Node: parts[1], Module: parts[2]}, parts[3], true
|
|
}
|
|
return Level{}, "", false
|
|
}
|
|
|
|
// Value is one key of mesh-config_values: the value set, who set it and why, and when.
|
|
type Value struct {
|
|
Value json.RawMessage `json:"value"`
|
|
// By is who set it, as proven: the caller, or for a value given at the controller's terminal, who gave it there.
|
|
By string `json:"by"`
|
|
// Via is the bus account the call came from.
|
|
Via string `json:"via"`
|
|
Why string `json:"why"`
|
|
// Approved is who approved it, for a value set on the operator's warrant (ADR 0348 §8, ADR 0277 §15): told from
|
|
// one given at the terminal, which leaves it empty.
|
|
Approved string `json:"approved,omitempty"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// Resolved is one key of mesh-config_resolved, `<node>.<module>.<key>`: the value a module on a node takes for a
|
|
// key, and where it came from. Built from the sources alone, so the same sources give the same bytes and a pass
|
|
// over unchanged sources writes nothing.
|
|
type Resolved struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value,omitempty"`
|
|
Source string `json:"source"`
|
|
// Predefined is the predefined key the value falls back to, when it names one.
|
|
Predefined string `json:"predefined,omitempty"`
|
|
// Required says an unset value is the operator's own to set.
|
|
Required bool `json:"required,omitempty"`
|
|
Trusted bool `json:"trusted,omitempty"`
|
|
}
|
|
|
|
// ResolvedKey is a resolved value's key in mesh-config_resolved.
|
|
func ResolvedKey(node, module, key string) string { return node + "." + module + "." + key }
|
|
|
|
// Sources is everything a resolution reads: the assignments, every module's declarations, the published list of
|
|
// predefined keys, and every value set.
|
|
type Sources struct {
|
|
Assignments []Assignment
|
|
Declared map[string]ModuleDeclarations
|
|
Predefined map[string]Published
|
|
Values map[string]Value
|
|
}
|
|
|
|
// Resolve is every assigned module's every declared value on its node, keyed as mesh-config_resolved is.
|
|
func Resolve(s Sources) map[string]Resolved {
|
|
out := map[string]Resolved{}
|
|
for _, a := range s.Assignments {
|
|
decl, ok := s.Declared[a.Module]
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, key := range sortedKeys(decl.Config) {
|
|
r := resolveOne(s, a.Node, a.Module, key, decl.Config[key])
|
|
out[ResolvedKey(a.Node, a.Module, key)] = r
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func resolveOne(s Sources, node, module, key string, d Declaration) Resolved {
|
|
r := Resolved{Node: node, Module: module, Key: key, Source: SourceUnset, Predefined: d.Predefined,
|
|
Required: d.Required, Trusted: d.Trusted}
|
|
if d.HasDefault() {
|
|
r.Value, r.Source = compact(d.Default), SourceDefault
|
|
}
|
|
if d.Predefined != "" {
|
|
if p, ok := s.Predefined[d.Predefined]; ok && p.Trusted {
|
|
r.Trusted = true
|
|
}
|
|
if v, ok := s.Values[Level{Kind: LevelMesh}.ValueKey(d.Predefined)]; ok {
|
|
r.Value, r.Source = compact(v.Value), SourceMesh
|
|
}
|
|
if v, ok := s.Values[Level{Kind: LevelNode, Node: node}.ValueKey(d.Predefined)]; ok {
|
|
r.Value, r.Source = compact(v.Value), SourceNode
|
|
}
|
|
}
|
|
if v, ok := s.Values[Level{Kind: LevelModule, Node: node, Module: module}.ValueKey(key)]; ok {
|
|
r.Value, r.Source = compact(v.Value), SourceModule
|
|
}
|
|
return r
|
|
}
|
|
|
|
// compact is a JSON value encoded the one way: objects' keys sorted, no spaces.
|
|
func compact(raw json.RawMessage) json.RawMessage {
|
|
var v any
|
|
if err := json.Unmarshal(raw, &v); err != nil {
|
|
return raw
|
|
}
|
|
out, err := json.Marshal(v)
|
|
if err != nil {
|
|
return raw
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Moved is one resolved value that changed: a module on a node whose value for a key moved, from what to what.
|
|
type Moved struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Key string `json:"key"`
|
|
From json.RawMessage `json:"from,omitempty"`
|
|
To json.RawMessage `json:"to,omitempty"`
|
|
Source string `json:"source"`
|
|
// Gone says the module no longer takes the key there: unassigned, or no longer declared.
|
|
Gone bool `json:"gone,omitempty"`
|
|
}
|
|
|
|
// diff is what moved between the resolved values held and those wanted, in key order.
|
|
func diff(held, want map[string]Resolved) []Moved {
|
|
var out []Moved
|
|
for _, k := range sortedKeys(want) {
|
|
w := want[k]
|
|
h, had := held[k]
|
|
if had && string(encodeResolved(h)) == string(encodeResolved(w)) {
|
|
continue
|
|
}
|
|
m := Moved{Node: w.Node, Module: w.Module, Key: w.Key, To: w.Value, Source: w.Source}
|
|
if had {
|
|
m.From = h.Value
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
for _, k := range sortedKeys(held) {
|
|
if _, ok := want[k]; !ok {
|
|
h := held[k]
|
|
out = append(out, Moved{Node: h.Node, Module: h.Module, Key: h.Key, From: h.Value, Gone: true})
|
|
}
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
return ResolvedKey(out[i].Node, out[i].Module, out[i].Key) < ResolvedKey(out[j].Node, out[j].Module, out[j].Key)
|
|
})
|
|
return out
|
|
}
|
|
|
|
func encodeResolved(r Resolved) []byte {
|
|
raw, err := json.Marshal(r)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("a resolved value cannot be encoded: %v", err))
|
|
}
|
|
return raw
|
|
}
|