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.
259 lines
8.1 KiB
Go
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
|
|
}
|