Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/config.go
T

439 lines
16 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"`
}
// 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
// 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
}
// 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
// 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)
}
return nil, fmt.Errorf("config does set, clear, list, history, propose or proposals; %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 catalogue", 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)
}
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.Caller != ControllerCaller && call.approval == nil {
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.Caller != ControllerCaller && call.approval == nil {
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
}
// 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 assigned to %s, and a module's own value is set for a node it "+
"is assigned to (novox/hq ADR 0348 §3); it is assigned to: %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...)
}
}