Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/resolve.go
T
jschoubben 96a37f58ca
mesh/delivery closed: the pull request closed unmerged
mesh/check pass 96a37f58ca76 on 030f9ee4e780 | unit 1173 run, 3272 kept, 13 skipped | integration 1212 run, 3274 kept, 22 skipped | over bound: unit, …
mesh-config: read declarations from each placed module's own manifests as built, and placement alone, so a module from its own repository resolves (issue 1953)
The controller's declared-config mirror never held de-spiegel's keys, and Resolve skipped
an assigned module it held no declarations of, silently: de-spiegel stayed held on novox
with its values set. Declarations now come from the indexer's trunk manifest with the
wanted build's laid over it per node; which module runs where from the seat
mesh-placement alone. A placed module with no readable manifest is listed as unresolved
with why, logged, and keeps its last resolved values.
2026-10-29 15:19:24 +01:00

283 lines
10 KiB
Go

package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// 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"
// PlacementRead is the seat mesh-placement's state, held by the assigner (novox/hq ADR 0567 §1, §8): each
// node's key `placement.<node>` with the modules placed there. It is the one source of which module runs where
// (issue 1953): the controller's assignments are no longer read.
PlacementRead = delivery.PlacementRead
// WantsRead is the assigner's wants, `want.<node>.<module>`: the build each node is to run (ADR 0539 §1).
WantsRead = delivery.AssignerModule + ".wants"
// BuildsRead is the builder's builds, `<module>.<fingerprint hex>`: each build's manifest digest (ADR 0525).
BuildsRead = delivery.BuilderModule + ".builds"
// ModulesRead is the indexer's modules, `<module>`: the trunk's manifest digest of each module, from whichever
// repository holds it (ADR 0437 §5, ADR 0478 §9); ManifestsRead its manifests by digest (ADR 0429 §16).
ModulesRead = delivery.IndexerModule + ".modules"
ManifestsRead = delivery.IndexerModule + ".manifests"
// PredefinedRead is the controller's list of predefined keys, which ADR 0565 §2 moves to the indexer's
// `predefined`: read here until that state exists.
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: where each module is placed, every module's declarations, the published
// list of predefined keys, and every value set.
type Sources struct {
Assignments []Assignment
// Declared is each module's declarations as its manifests declare them: the trunk's, with every key only a wanted
// build still declares (declarationsOf). It is what a set is judged against.
Declared map[string]ModuleDeclarations
// OnNode is, by `<node>.<module>`, the declarations of the build the node's want names laid over the trunk's: what
// that node resolves. A placed module absent here resolves from Declared.
OnNode map[string]ModuleDeclarations
// Unresolved is each placed module whose declarations could not be read, with why: never skipped silently
// (issue 1953). Its resolved values are kept as they were until its declarations read again.
Unresolved []Unresolved
Predefined map[string]Published
Values map[string]Value
// Pulls is what the controller told of each open pull request's declarations, and Kept the approved values held
// for them (pulls.go); neither is resolved.
Pulls map[string]PullDecl
Kept map[string]KeptValue
}
// Unresolved is a module placed on a node whose values are not resolved, and why.
type Unresolved struct {
Node string `json:"node"`
Module string `json:"module"`
Why string `json:"why"`
}
// declarationsOn is the declarations a module on a node resolves from, and whether any are held.
func (s Sources) declarationsOn(node, module string) (ModuleDeclarations, bool) {
if d, ok := s.OnNode[node+"."+module]; ok {
return d, true
}
d, ok := s.Declared[module]
return d, ok
}
// unresolved is whether a module on a node is unresolved.
func (s Sources) unresolved(node, module string) bool {
for _, u := range s.Unresolved {
if u.Node == node && u.Module == module {
return true
}
}
return false
}
// Resolve is every placed module's every declared value on its node, keyed as mesh-config_resolved is. A placed
// module with no declarations is in s.Unresolved, said there (sources).
func Resolve(s Sources) map[string]Resolved {
out := map[string]Resolved{}
for _, a := range s.Assignments {
decl, ok := s.declarationsOn(a.Node, 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: no longer placed, 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
}