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, …
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.
283 lines
10 KiB
Go
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
|
|
}
|