439 lines
16 KiB
Go
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...)
|
|
}
|
|
}
|