Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/sources.go
T
jschoubben 224a7a66a5 mesh-config: hold a value for the open pull request that introduces its key (issue 1826)
A key a pull request adds is declared by no registered manifest, so its value was refused and the
group's composed check could not pass. A proposal may now name the pull request: judged against what
the controller told of its head, asked of the operator, kept apart on Approve, applied at the merge,
dropped at a close.
2026-10-22 10:58:28 +02:00

259 lines
8.1 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
// Reading the sources, writing the resolved values, and publishing the list of predefined keys.
// sources reads everything a resolution reads, from the bus as it is now.
func (c *Config) sources() (Sources, error) {
s := Sources{Declared: map[string]ModuleDeclarations{}, Predefined: map[string]Published{},
Values: map[string]Value{}}
var problems []string
if err := c.each(AssignmentsRead, func(key string, raw []byte) error {
var a Assignment
if err := json.Unmarshal(raw, &a); err != nil || a.Node == "" || a.Module == "" {
return fmt.Errorf("the assignment %s is not one: %s", key, raw)
}
if !a.Standing() {
return nil // ended or pending: read as a removed key (issue 1098)
}
s.Assignments = append(s.Assignments, a)
return nil
}); err != nil {
return s, fmt.Errorf("the assignments cannot be read: %w", err)
}
if err := c.each(DeclaredConfigRead, func(key string, raw []byte) error {
m, err := ParseModuleDeclarations(raw)
if err != nil {
// One module's declarations unread leaves that module unresolved, said; the rest resolve.
problems = append(problems, fmt.Sprintf("%s: %v", key, err))
return nil
}
s.Declared[m.Module] = m
return nil
}); err != nil {
return s, fmt.Errorf("the modules' declarations cannot be read: %w", err)
}
if err := c.each(PredefinedState, func(key string, raw []byte) error {
var p Published
if err := json.Unmarshal(raw, &p); err != nil {
return fmt.Errorf("the published predefined key %s is not one: %w", key, err)
}
s.Predefined[key] = p
return nil
}); err != nil {
return s, fmt.Errorf("the published list of predefined keys cannot be read: %w", err)
}
if err := c.each(ValuesState, func(key string, raw []byte) error {
var v Value
if err := json.Unmarshal(raw, &v); err != nil {
return fmt.Errorf("the value %s is not one: %w", key, err)
}
s.Values[key] = v
return nil
}); err != nil {
return s, fmt.Errorf("the values cannot be read: %w", err)
}
if err := c.readPulls(&s); err != nil {
return s, err
}
for _, p := range problems {
c.logf("[mesh-config] a module's declarations are not read, so its values are not resolved: %s", p)
}
return s, nil
}
// each hands every key of a state to visit.
func (c *Config) each(state string, visit func(key string, raw []byte) error) error {
keys, err := c.Store.Keys(state)
if err != nil {
return err
}
for _, k := range keys {
e, err := c.Store.Get(state, k)
if err != nil {
return err
}
if e == nil {
continue // removed between the listing and the read
}
if err := visit(k, e.Value); err != nil {
return err
}
}
return nil
}
// held is what mesh-config_resolved holds now.
func (c *Config) held() (map[string]Resolved, error) {
out := map[string]Resolved{}
err := c.each(ResolvedState, func(key string, raw []byte) error {
var r Resolved
if err := json.Unmarshal(raw, &r); err != nil {
// A value nobody can read is written again from the sources.
out[key] = Resolved{Key: key, Source: "unreadable"}
return nil
}
out[key] = r
return nil
})
return out, err
}
// writeResolved makes mesh-config_resolved hold what the sources resolve to, writing only what moved, and
// answers what moved.
func (c *Config) writeResolved(s Sources) ([]Moved, error) {
held, err := c.held()
if err != nil {
return nil, fmt.Errorf("the resolved values cannot be read: %w", err)
}
want := Resolve(s)
moved := diff(held, want)
var problems []error
for _, m := range moved {
key := ResolvedKey(m.Node, m.Module, m.Key)
if m.Gone {
if err := c.Store.Delete(ResolvedState, key); err != nil {
problems = append(problems, fmt.Errorf("removing %s: %w", key, err))
}
continue
}
if err := c.Store.Put(ResolvedState, key, want[key]); err != nil {
problems = append(problems, fmt.Errorf("writing %s: %w", key, err))
}
}
// A key that is no resolved value at all (written by an older mesh-config, or unreadable) is removed.
for key, h := range held {
if _, ok := want[key]; !ok && h.Source == "unreadable" {
if err := c.Store.Delete(ResolvedState, key); err != nil {
problems = append(problems, fmt.Errorf("removing %s: %w", key, err))
}
}
}
return moved, errors.Join(problems...)
}
// Resolution is what one pass of Reconcile did.
type Resolution struct {
Moved []Moved `json:"moved"`
Published string `json:"published,omitempty"`
Withheld string `json:"withheld,omitempty"`
}
// Reconcile publishes the newest list of predefined keys and brings mesh-config_resolved to what the sources
// resolve to: run at start, and whenever an assignment, a declaration or the list moves. A move it makes raises
// config-changed like a set, with no level, naming what moved.
func (c *Config) Reconcile() (Resolution, error) {
c.mu.Lock()
defer c.mu.Unlock()
var r Resolution
published, err := c.publish()
if err != nil {
c.logf("[mesh-config] the list of predefined keys is not published: %v", err)
}
r.Published, r.Withheld = published, c.withheld
s, err := c.sources()
if err != nil {
return r, err
}
moved, err := c.writeResolved(s)
r.Moved = moved
if len(moved) > 0 {
c.record("resolved", Change{Action: "resolve", Why: "an assignment, a declaration or the list of predefined " +
"keys changed", At: c.now(), Moved: moved})
}
return r, err
}
// publish makes mesh-config_predefined hold the list the controller kept, when it is newer than the one published
// (ADR 0348 §7: the newest trunk build wins). **A list dropping a key that still holds a value is withheld**: the
// list published stays, and why is said in every listing and in the log, because a value for a key no longer on
// the list could neither be resolved nor cleared by name (§7: removing a key still holding a value is refused).
// It answers what it published, empty when nothing changed.
func (c *Config) publish() (string, error) {
e, err := c.Store.Get(PredefinedRead, PredefinedMirrorKey)
if err != nil {
return "", fmt.Errorf("the controller's list cannot be read: %w", err)
}
if e == nil {
return "", nil // no list kept yet
}
var list PredefinedList
if err := json.Unmarshal(e.Value, &list); err != nil || list.Keys == nil {
return "", fmt.Errorf("the controller's list is not one: %s", e.Value)
}
current := map[string]Published{}
if err := c.each(PredefinedState, func(key string, raw []byte) error {
var p Published
if err := json.Unmarshal(raw, &p); err != nil {
return err
}
current[key] = p
return nil
}); err != nil {
return "", err
}
generation := 0
for _, p := range current {
generation = max(generation, p.Generation)
}
if len(current) > 0 && generation >= list.Generation {
c.withheld = ""
return "", nil
}
held := map[string][]string{}
if err := c.each(ValuesState, func(key string, _ []byte) error {
level, k, ok := parseValueKey(key)
if ok && level.Kind != LevelModule {
if _, kept := list.Keys[k]; !kept {
held[k] = append(held[k], level.String())
}
}
return nil
}); err != nil {
return "", err
}
if len(held) > 0 {
var why []string
for _, k := range sortedKeys(held) {
why = append(why, fmt.Sprintf("%s (still set as %s)", k, strings.Join(held[k], ", ")))
}
c.withheld = fmt.Sprintf("the list at %s (generation %d) is not published: it drops %s; clear those values "+
"first, or keep the key (novox/hq ADR 0348 §7)", short(list.Commit), list.Generation, strings.Join(why, "; "))
c.logf("[mesh-config] %s", c.withheld)
return "", nil
}
c.withheld = ""
var problems []error
for _, k := range sortedKeys(list.Keys) {
p := Published{PredefinedKey: list.Keys[k], Generation: list.Generation, Commit: list.Commit}
if err := c.Store.Put(PredefinedState, k, p); err != nil {
problems = append(problems, err)
}
}
for k := range current {
if _, kept := list.Keys[k]; !kept {
if err := c.Store.Delete(PredefinedState, k); err != nil {
problems = append(problems, err)
}
}
}
if err := errors.Join(problems...); err != nil {
return "", err
}
return fmt.Sprintf("the list at %s (generation %d): %s", short(list.Commit), list.Generation,
joinKeys(list.Keys)), nil
}
func short(commit string) string {
if len(commit) > 8 {
return commit[:8]
}
return commit
}