Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/resolve.go
T

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
}