mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
One commit with exactly the tree of the reviewed head35b4509c0a, on main12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
472 lines
17 KiB
Go
472 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// The verb `config` (novox/hq ADR 0348 §1, §8, §9, §10, design 57 K3): set or clear a value at one level with why,
|
|
// list the values, and read their history, narrowed by module and node. A set refuses a key the module or the list
|
|
// of predefined keys does not declare, naming the declared keys; validates the value; writes it and the resolved
|
|
// values it moves; records the change in the history and the hand-act log; raises `config-changed`; and answers
|
|
// with each module on each node whose value moved. A config change opens no walk and waits at no first-node gate.
|
|
//
|
|
// **Trusted values at the terminal or on the operator's Approve.** A trusted value (ADR 0348 §8) is set only by a
|
|
// call the controller carries from its terminal, or on the operator's warrant for a proposal (proposals.go, design
|
|
// 57 K4); a set through the verb is refused and names the proposal as the way.
|
|
|
|
// ControllerCaller is the controller's bus user: the one caller that carries a value given at its terminal.
|
|
const ControllerCaller = "controller"
|
|
|
|
// Unproven is the caller of a call the runtime named none for (the SDK's word).
|
|
const Unproven = "unproven"
|
|
|
|
// Entry is one key's current value in a state.
|
|
type Entry struct {
|
|
Key string
|
|
Value json.RawMessage
|
|
Revision uint64
|
|
}
|
|
|
|
// Store is the bus as this module reaches it: the state it keeps and reads, its history log, its events, and the
|
|
// controller's hand-act log.
|
|
type Store interface {
|
|
Get(state, key string) (*Entry, error)
|
|
Put(state, key string, value any) error
|
|
Delete(state, key string) error
|
|
Keys(state string) ([]string, error)
|
|
Append(log, key string, value any) error
|
|
Read(log string) ([]LogEntry, error)
|
|
Emit(event string, body any) error
|
|
HandAct(what, why, cause string) error
|
|
}
|
|
|
|
// LogEntry is one entry of a log.
|
|
type LogEntry struct {
|
|
Key string
|
|
Value json.RawMessage
|
|
At time.Time
|
|
}
|
|
|
|
// ConfigChanged is the event every change raises: what was set or cleared at which level, by whom and why, and
|
|
// each module on each node whose resolved value moved. Never a secret: config holds none.
|
|
const ConfigChanged = "config-changed"
|
|
|
|
// Change is a set, a clear, or a move of resolved values with no set behind it (a declaration or an assignment
|
|
// changed), as the history and the event say it.
|
|
type Change struct {
|
|
Action string `json:"action"`
|
|
Level *Level `json:"level,omitempty"`
|
|
Key string `json:"key,omitempty"`
|
|
Value json.RawMessage `json:"value,omitempty"`
|
|
Was json.RawMessage `json:"was,omitempty"`
|
|
By string `json:"by,omitempty"`
|
|
Via string `json:"via,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
// Approved is who approved it on a channel that proves who answered, for a value set on the operator's warrant.
|
|
Approved string `json:"approved,omitempty"`
|
|
At time.Time `json:"at"`
|
|
Moved []Moved `json:"moved"`
|
|
// Pull and Head name the pull request a value is held for, dropped from, or applied from at its merge (novox/hq
|
|
// ADR 0502): the actions hold and drop are always about one, and a set with a pull is its merge.
|
|
Pull string `json:"pull,omitempty"`
|
|
Head string `json:"head,omitempty"`
|
|
}
|
|
|
|
// Config is the module: its store, its clock, and one lock, so a set and a resolution never interleave.
|
|
type Config struct {
|
|
Store Store
|
|
Now func() time.Time
|
|
Logf func(format string, a ...any)
|
|
|
|
mu sync.Mutex
|
|
// withheld is why the newest list of predefined keys was not published, empty when it was (publish).
|
|
withheld string
|
|
|
|
// Asks is the operator channel as this module asks on it (proposals.go); nil, nothing can be proposed.
|
|
Asks AskBus
|
|
// Forge is the forge as pulls are asked of it (pulls.go); nil, a merge or a close is heard from its events alone.
|
|
Forge PullForge
|
|
// RouterWithin and RouterEvery bound how long a proposal waits for the router's word on its ask, and how often
|
|
// it looks; zero is RouterAnswersWithin and RouterAnswersEvery.
|
|
RouterWithin, RouterEvery time.Duration
|
|
endedMu sync.Mutex
|
|
ended map[string]endedWord
|
|
|
|
// manifests is each manifest's declarations read, by digest: a manifest never changes (manifestDeclarations).
|
|
manifestsMu sync.Mutex
|
|
manifests map[string]ModuleDeclarations
|
|
}
|
|
|
|
// Call is one call of the verb, its arguments read.
|
|
type Call struct {
|
|
Action string
|
|
Key string
|
|
Value any
|
|
Module string
|
|
Node string
|
|
Why string
|
|
// Caller is the bus user the runtime proved; By is who gave the value at the controller's terminal, said by the
|
|
// controller alone.
|
|
Caller string
|
|
By string
|
|
Limit int
|
|
// Clear asks a proposal to clear the value instead of setting it; Proposal names one proposal to read.
|
|
Clear bool
|
|
Proposal string
|
|
// Pull names the open pull request that introduces the key, for propose (a held value) and pull-declares; Head is
|
|
// the commit its declarations are read at; Declares the config declarations the head adds or changes, by module;
|
|
// Pulls the pull requests held asks about, each at a head.
|
|
Pull string
|
|
Head string
|
|
Declares map[string]any
|
|
Pulls []PullAt
|
|
|
|
// approval is the operator's warrant a set or a clear is made on: given by a decided proposal alone, never by
|
|
// a call's arguments.
|
|
approval *approval
|
|
}
|
|
|
|
// SetAnswer is what a set or a clear answers.
|
|
type SetAnswer struct {
|
|
Said string `json:"said"`
|
|
Level Level `json:"level"`
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value,omitempty"`
|
|
Was json.RawMessage `json:"was,omitempty"`
|
|
Moved []Moved `json:"moved"`
|
|
Result []NodeResult `json:"result"`
|
|
// Unsaid is what could not be recorded or announced after the value was written: said, never dropped.
|
|
Unsaid []string `json:"unsaid,omitempty"`
|
|
}
|
|
|
|
// NodeResult is one affected node's result. Until the provisioner client fills files from mesh-config_resolved
|
|
// (design 57 P5, P6), a module on a node takes its new value at its next composition; that is said, not hidden.
|
|
type NodeResult struct {
|
|
Node string `json:"node"`
|
|
Modules []string `json:"modules"`
|
|
Result string `json:"result"`
|
|
}
|
|
|
|
// Do runs one call of the verb.
|
|
func (c *Config) Do(call Call) (any, error) {
|
|
switch call.Action {
|
|
case "", "list":
|
|
return c.List(call)
|
|
case "set", "clear":
|
|
return c.Change(call)
|
|
case "history":
|
|
return c.History(call)
|
|
case "propose":
|
|
return c.Propose(call)
|
|
case "proposals":
|
|
return c.Proposals(call)
|
|
case "pull-declares":
|
|
return c.PullDeclares(call)
|
|
case "held":
|
|
return c.Held(call)
|
|
}
|
|
return nil, fmt.Errorf("config does set, clear, list, history, propose, proposals, pull-declares or held; %q is "+
|
|
"none of them", call.Action)
|
|
}
|
|
|
|
// level is the level a call names, refused as ADR 0348 §3 says: a module's own value is for one node.
|
|
func (call Call) level() (Level, error) {
|
|
for _, name := range []string{call.Node, call.Module} {
|
|
if name != "" && !nodeOrModule.MatchString(name) {
|
|
return Level{}, fmt.Errorf("%q is not a node's or a module's name", name)
|
|
}
|
|
}
|
|
switch {
|
|
case call.Module != "" && call.Node != "":
|
|
return Level{Kind: LevelModule, Node: call.Node, Module: call.Module}, nil
|
|
case call.Module != "":
|
|
return Level{}, fmt.Errorf("a module's own value is set for one node, and no node is named: there is no "+
|
|
"module value for the whole mesh (novox/hq ADR 0348 §3); name the node, or set mesh config for the "+
|
|
"predefined key %s's value falls back to, or change its default in the catalog", call.Module)
|
|
case call.Node != "":
|
|
return Level{Kind: LevelNode, Node: call.Node}, nil
|
|
}
|
|
return Level{Kind: LevelMesh}, nil
|
|
}
|
|
|
|
// Change sets or clears one value.
|
|
func (c *Config) Change(call Call) (*SetAnswer, error) {
|
|
if call.Clear || call.Proposal != "" {
|
|
return nil, fmt.Errorf("clear and proposal are a proposal's: %s takes neither", call.Action)
|
|
}
|
|
if call.Pull != "" {
|
|
return nil, fmt.Errorf("pull names the pull request a value is held for, which only a proposal does: %s takes "+
|
|
"none", call.Action)
|
|
}
|
|
call.approval = nil // only a decided proposal gives one
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.change(call)
|
|
}
|
|
|
|
// change sets or clears one value; the caller holds the lock.
|
|
func (c *Config) change(call Call) (*SetAnswer, error) {
|
|
level, err := call.level()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !keyName.MatchString(call.Key) {
|
|
return nil, fmt.Errorf("name the key to %s: lower-case letters, digits, dots, dashes and underscores", call.Action)
|
|
}
|
|
if strings.TrimSpace(call.Why) == "" {
|
|
return nil, fmt.Errorf("say why the value is %s: every change keeps its why (novox/hq ADR 0348 §9)",
|
|
map[string]string{"set": "set", "clear": "cleared"}[call.Action])
|
|
}
|
|
if call.Caller == "" || call.Caller == Unproven {
|
|
return nil, errors.New("the call names no proven caller, and every change keeps who made it " +
|
|
"(novox/hq ADR 0348 §9): call config through the mesh, which names you")
|
|
}
|
|
by := call.Caller
|
|
if call.By != "" {
|
|
if call.Caller != ControllerCaller {
|
|
return nil, fmt.Errorf("by names who gave a value at the controller's terminal, which only the controller "+
|
|
"says; %s is not the controller", call.Caller)
|
|
}
|
|
by = call.By
|
|
}
|
|
s, err := c.sources()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, trusted, err := c.declared(s, level, call.Key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vk := level.ValueKey(call.Key)
|
|
was, had := s.Values[vk]
|
|
if call.approval != nil {
|
|
// **On Approve, the value stored must still be the one the operator was shown the change against** (ADR
|
|
// 0348 §8, ADR 0277 §13): a value set or cleared since, at the terminal or on another warrant, refuses it.
|
|
if had != call.approval.HadValue || valueDigest(was.Value, had) != call.approval.WasDigest {
|
|
return nil, fmt.Errorf("%s at %s is %s now, and the operator was shown the change against %s: it changed "+
|
|
"since, so nothing is set; propose it again", call.Key, level, shownOrNone(was.Value, had),
|
|
shownOrNone(call.approval.Was, call.approval.HadValue))
|
|
}
|
|
by = call.approval.By
|
|
}
|
|
answer := &SetAnswer{Level: level, Key: call.Key}
|
|
if had {
|
|
answer.Was = compact(was.Value)
|
|
}
|
|
now := c.now()
|
|
switch call.Action {
|
|
case "set":
|
|
if call.Value == nil {
|
|
return nil, fmt.Errorf("give the value to set %s to", call.Key)
|
|
}
|
|
raw, err := json.Marshal(call.Value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the value cannot be read: %v", err)
|
|
}
|
|
var value any
|
|
_ = json.Unmarshal(raw, &value)
|
|
if problems := r.check(value); len(problems) > 0 {
|
|
return nil, fmt.Errorf("%s is refused for %s at %s: %s", show(value), call.Key, level,
|
|
strings.Join(problems, "; "))
|
|
}
|
|
if trusted && call.approval == nil && !c.atTheTerminal(call) {
|
|
return nil, fmt.Errorf("%s is trusted (it decides what runs or who can reach what), so it is set only at "+
|
|
"the controller's terminal or on the operator's Approve (novox/hq ADR 0348 §8); %s is not the terminal: "+
|
|
"propose it instead, with config's action propose", call.Key, call.Caller)
|
|
}
|
|
v := Value{Value: compact(raw), By: by, Via: call.Caller, Why: strings.TrimSpace(call.Why), At: now}
|
|
if call.approval != nil {
|
|
v.Approved = call.approval.Says
|
|
}
|
|
if err := c.Store.Put(ValuesState, vk, v); err != nil {
|
|
return nil, fmt.Errorf("the value was not written: %v", err)
|
|
}
|
|
s.Values[vk] = v
|
|
answer.Value = v.Value
|
|
case "clear":
|
|
if !had {
|
|
return nil, fmt.Errorf("nothing is set for %s at %s, so there is nothing to clear", call.Key, level)
|
|
}
|
|
if trusted && call.approval == nil && !c.atTheTerminal(call) {
|
|
return nil, fmt.Errorf("%s is trusted, so it is cleared only at the controller's terminal or on the "+
|
|
"operator's Approve (novox/hq ADR 0348 §8); %s is not the terminal: propose the clear instead, with "+
|
|
"config's action propose and clear", call.Key, call.Caller)
|
|
}
|
|
if err := c.Store.Delete(ValuesState, vk); err != nil {
|
|
return nil, fmt.Errorf("the value was not cleared: %v", err)
|
|
}
|
|
delete(s.Values, vk)
|
|
}
|
|
moved, err := c.writeResolved(s)
|
|
answer.Moved = moved
|
|
if err != nil {
|
|
// The value is written and the resolved values are not all: said, and put right on the next resolution.
|
|
answer.Unsaid = append(answer.Unsaid, "the resolved values are not all written yet ("+err.Error()+"); "+
|
|
"the next resolution writes them")
|
|
}
|
|
change := Change{Action: call.Action, Level: &level, Key: call.Key, Value: answer.Value, Was: answer.Was, By: by,
|
|
Via: call.Caller, Why: strings.TrimSpace(call.Why), At: now, Moved: moved}
|
|
cause := "the config verb, called by " + call.Caller
|
|
if call.approval != nil {
|
|
change.Approved = call.approval.Says
|
|
cause = "the operator's Approve of proposal " + call.approval.Proposal + ", proposed by " + call.Caller
|
|
}
|
|
answer.Unsaid = append(answer.Unsaid, c.record(vk, change)...)
|
|
if err := c.Store.HandAct(handActWhat(change), change.Why, cause); err != nil {
|
|
answer.Unsaid = append(answer.Unsaid, "the hand-act log does not hold it: "+err.Error())
|
|
}
|
|
answer.Result = nodeResults(moved)
|
|
answer.Said = said(change)
|
|
return answer, nil
|
|
}
|
|
|
|
// atTheTerminal says a call is the controller's relay of its terminal, which holds only until the controller's lease is
|
|
// handed away: from the lease write a trusted value is set only through propose and the operator's Approve, whoever
|
|
// calls (novox/hq ADR 0565 §13).
|
|
func (c *Config) atTheTerminal(call Call) bool {
|
|
return call.Caller == ControllerCaller && !c.leaseAway()
|
|
}
|
|
|
|
// declared is the rule a value at a level is held to, and whether it is trusted; refused, naming the declared
|
|
// keys, for a key the module or the list of predefined keys does not declare (ADR 0348 §10).
|
|
func (c *Config) declared(s Sources, level Level, key string) (rule, bool, error) {
|
|
if level.Kind == LevelModule {
|
|
decl, ok := s.Declared[level.Module]
|
|
if !ok {
|
|
return rule{}, false, fmt.Errorf("%s is not a registered module, so it declares no config", level.Module)
|
|
}
|
|
d, ok := decl.Config[key]
|
|
if !ok {
|
|
return rule{}, false, fmt.Errorf("%s declares no value %q; it declares: %s", level.Module, key,
|
|
joinKeys(decl.Config))
|
|
}
|
|
if !assigned(s, level.Node, level.Module) {
|
|
return rule{}, false, fmt.Errorf("%s is not placed on %s, and a module's own value is set for a node it "+
|
|
"is placed on (novox/hq ADR 0348 §3); it is placed on: %s", level.Module, level.Node,
|
|
strings.Join(nodesOf(s, level.Module), ", "))
|
|
}
|
|
trusted := d.Trusted
|
|
if p, ok := s.Predefined[d.Predefined]; ok && p.Trusted {
|
|
trusted = true
|
|
}
|
|
return d.rule(), trusted, nil
|
|
}
|
|
if len(s.Predefined) == 0 {
|
|
return rule{}, false, fmt.Errorf("%s holds values only for the keys of the list of predefined keys, and no "+
|
|
"list is published yet (novox/hq ADR 0348 §7)", level)
|
|
}
|
|
p, ok := s.Predefined[key]
|
|
if !ok {
|
|
return rule{}, false, fmt.Errorf("%s holds values only for the predefined keys, and %q is not one; the "+
|
|
"predefined keys are: %s (novox/hq ADR 0348 §7)", level, key, joinKeys(s.Predefined))
|
|
}
|
|
return p.rule(), p.Trusted, nil
|
|
}
|
|
|
|
func assigned(s Sources, node, module string) bool {
|
|
for _, a := range s.Assignments {
|
|
if a.Node == node && a.Module == module {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func nodesOf(s Sources, module string) []string {
|
|
var out []string
|
|
for _, a := range s.Assignments {
|
|
if a.Module == module {
|
|
out = append(out, a.Node)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
if len(out) == 0 {
|
|
return []string{"no node"}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// record keeps a change in the history and raises its event; what could not be done is answered, not dropped.
|
|
func (c *Config) record(key string, change Change) []string {
|
|
var unsaid []string
|
|
if err := c.Store.Append(HistoryLog, key, change); err != nil {
|
|
unsaid = append(unsaid, "the history does not hold it: "+err.Error())
|
|
}
|
|
if err := c.Store.Emit(ConfigChanged, change); err != nil {
|
|
unsaid = append(unsaid, "config-changed was not raised: "+err.Error())
|
|
}
|
|
for _, u := range unsaid {
|
|
c.logf("[mesh-config] %s %s: %s", change.Action, key, u)
|
|
}
|
|
return unsaid
|
|
}
|
|
|
|
func handActWhat(ch Change) string {
|
|
what := fmt.Sprintf("config %s %s at %s", ch.Action, ch.Key, ch.Level)
|
|
if ch.Action == "set" {
|
|
what += " to " + string(ch.Value)
|
|
}
|
|
return what + ", by " + ch.By
|
|
}
|
|
|
|
func said(ch Change) string {
|
|
var b strings.Builder
|
|
if ch.Action == "set" {
|
|
fmt.Fprintf(&b, "%s is %s at %s", ch.Key, ch.Value, ch.Level)
|
|
} else {
|
|
fmt.Fprintf(&b, "%s is cleared at %s", ch.Key, ch.Level)
|
|
}
|
|
switch len(ch.Moved) {
|
|
case 0:
|
|
b.WriteString("; no module's value moved")
|
|
case 1:
|
|
m := ch.Moved[0]
|
|
fmt.Fprintf(&b, "; %s on %s takes %s (from %s)", m.Module, m.Node, orUnset(m.To), m.Source)
|
|
default:
|
|
fmt.Fprintf(&b, "; %d values moved on %d node(s)", len(ch.Moved), len(nodeResults(ch.Moved)))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func orUnset(raw json.RawMessage) string {
|
|
if len(raw) == 0 {
|
|
return "no value (unset)"
|
|
}
|
|
return string(raw)
|
|
}
|
|
|
|
func nodeResults(moved []Moved) []NodeResult {
|
|
byNode := map[string]map[string]bool{}
|
|
for _, m := range moved {
|
|
if byNode[m.Node] == nil {
|
|
byNode[m.Node] = map[string]bool{}
|
|
}
|
|
byNode[m.Node][m.Module] = true
|
|
}
|
|
out := []NodeResult{}
|
|
for _, node := range sortedKeys(byNode) {
|
|
out = append(out, NodeResult{Node: node, Modules: sortedKeys(byNode[node]),
|
|
Result: "resolved and written to mesh-config_resolved; the module takes it when its files are filled again"})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (c *Config) now() time.Time {
|
|
if c.Now == nil {
|
|
return time.Now().UTC()
|
|
}
|
|
return c.Now().UTC()
|
|
}
|
|
|
|
func (c *Config) logf(format string, a ...any) {
|
|
if c.Logf != nil {
|
|
c.Logf(format, a...)
|
|
}
|
|
}
|