While the mesh-delivery seat has a holder on record, a merge that moves no core module opens its walk and asks nothing until mesh-delivery or a person says go; nothing of it is registered before its turn, so no other send carries it. The controller keeps the planner, the gate, sending and the walk, and gains the verbs the owner asks with: delivery-plan, -order, -check (a group composed as one future state), deliver, delivery-stop, delivery-walks; every walk kept is said as plan-moved.
269 lines
10 KiB
Go
269 lines
10 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"slices"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/nats-io/nats.go"
|
||
|
||
"github.com/novox/mesh-controller/internal/broker"
|
||
"github.com/novox/mesh-controller/internal/link"
|
||
)
|
||
|
||
// Acts done by hand, and why (novox/hq to-be 45 §7).
|
||
//
|
||
// **Which verbs ask why.** `plans close` and `plans stop`, `broker consumer-reset` and `hand-act
|
||
// record` refuse without it everywhere: nothing automated runs them, so a call without a reason is a
|
||
// person who has not given one. A named `push` asks for it through the mesh-controller seat, which is
|
||
// how a person or an agent acts by hand on the mesh; at a shell `--why` is recorded when given and not
|
||
// required, because the installer and the lab push by command line as a step of what they do, and a
|
||
// step of a procedure is not a repair. `conditions silence` joins them when the condition store does
|
||
// (Phase 1).
|
||
|
||
// handActVerb is one verb that writes the hand-act log, and whether what it records is a repair.
|
||
type handActVerb struct {
|
||
Verb string
|
||
// Decision says why an act of this verb is a person's decision by design rather than a repair a
|
||
// healer could take over; empty for a repair.
|
||
Decision string
|
||
// DecidedFor limits Decision to these causes; empty, it holds for every act of the verb.
|
||
DecidedFor []string
|
||
}
|
||
|
||
// causeLeakedInLogs is the cause a rotation after a value was printed into a log gives.
|
||
const causeLeakedInLogs = "leaked-in-logs"
|
||
|
||
// handActVerbs is every verb that writes the hand-act log (novox/hq to-be 45 §7). **S15 reads it**:
|
||
// an act recorded by a verb whose entry names a decision is the mesh working as decided, never a
|
||
// repair, and does not count toward `healer-wanted` — whatever cause it gives. A verb not listed, or
|
||
// listed without a decision, counts, so a new verb is a repair until its entry says otherwise.
|
||
var handActVerbs = []handActVerb{
|
||
// Repairs: each repeated is a healer the mesh lacks. A push by hand is exactly what roll-out by
|
||
// default (ADR 0236) exists to end.
|
||
{Verb: "push"},
|
||
{Verb: "plans stop"},
|
||
{Verb: "plans close"},
|
||
// A walk started by a person instead of its delivery's owner (novox/hq ADR 0239): the owner down, or
|
||
// not trusted with it — either is a repair the owner should have made.
|
||
{Verb: "plans go"},
|
||
{Verb: "broker consumer-reset"},
|
||
// Silencing the same condition twice says the condition, or what it watches, wants mending.
|
||
{Verb: "conditions silence"},
|
||
// An act done outside the mesh: the mesh cannot tell a repair from a decision there, so it counts.
|
||
{Verb: "hand-act record"},
|
||
// A person's decisions by design.
|
||
{Verb: "retire approve", Decision: "nothing is retired past its bound without a person (ADR 0230)"},
|
||
{Verb: "retire reject", Decision: "keeping a consumer active is a person's word (ADR 0230)"},
|
||
{Verb: "cleanup delete", Decision: "nothing retired is deleted without a person (ADR 0230)"},
|
||
{Verb: "bus upgrade", Decision: "the bus is never rolled by the mesh: replacing it is a planned step a " +
|
||
"person starts (ADR 0236)"},
|
||
{Verb: "upgrade release-backlog", Decision: "after a release plan failed, the next opens only when a " +
|
||
"person releases it (ADR 0236)"},
|
||
// A leak is judged by a person — which value was exposed, to whom — and its rotation is the answer
|
||
// to that judgement. Several values rotate for one leak, and a leak that recurs is a defect of the
|
||
// module that prints them, an issue against it, not a healer that rotates. A rotation for any other
|
||
// cause — a credential that stopped working — counts: a schedule or a healer could take it over.
|
||
{Verb: "secret rotate", Decision: "a value a person judged disclosed is replaced on their word",
|
||
DecidedFor: []string{causeLeakedInLogs}},
|
||
}
|
||
|
||
// personsDecision is whether an act in the log is a person's decision by design, by the verb that
|
||
// recorded it (handActVerbs).
|
||
func personsDecision(a link.HandAct) bool {
|
||
for _, v := range handActVerbs {
|
||
if v.Verb != a.Verb {
|
||
continue
|
||
}
|
||
return v.Decision != "" && (len(v.DecidedFor) == 0 || slices.Contains(v.DecidedFor, a.Cause))
|
||
}
|
||
return false
|
||
}
|
||
|
||
// handActFlags are the flags every repairing verb takes.
|
||
type handActFlags struct {
|
||
why, cause, condition *string
|
||
}
|
||
|
||
func addHandActFlags(set *flag.FlagSet) handActFlags {
|
||
return handActFlags{
|
||
why: set.String("why", "", "why this is done by hand — recorded in the hand-act log (novox/hq to-be 45 §7)"),
|
||
cause: set.String("cause", "", "the cause, in a word or a condition's kind; the verb's own name when not given"),
|
||
condition: set.String("condition", "", "the key of the condition this act addresses, if any"),
|
||
}
|
||
}
|
||
|
||
// given is whether a reason was given.
|
||
func (f handActFlags) given() bool { return strings.TrimSpace(*f.why) != "" }
|
||
|
||
// require refuses an act without a reason, before anything is done.
|
||
func (f handActFlags) require(verb string) error {
|
||
if f.given() {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s is a repair done by hand, and says why: --why <text> (recorded in the hand-act "+
|
||
"log, novox/hq to-be 45 §7). Nothing was done", verb)
|
||
}
|
||
|
||
// handActConn is the serving controller's connection, for what it reads of the log itself; a
|
||
// command dials its own.
|
||
var handActConn *nats.Conn
|
||
|
||
// onTheBus runs f with a connection to the bus: the serving controller's, or one of its own.
|
||
func onTheBus(f func(*nats.Conn) error) error {
|
||
if handActConn != nil {
|
||
return f(handActConn)
|
||
}
|
||
address, err := broker.BusAddress()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
js, err := broker.Dial(address)
|
||
if err != nil {
|
||
return fmt.Errorf("cannot reach the bus: %w", err)
|
||
}
|
||
defer js.Close()
|
||
return f(js.Conn())
|
||
}
|
||
|
||
// record writes the entry for an act about to be done. **Before the act, and never instead of it**:
|
||
// a log that cannot be written is said loudly, and the repair it was about still happens — a mesh
|
||
// whose bus is down is exactly the mesh somebody is repairing by hand.
|
||
func (f handActFlags) record(ctx context.Context, verb string, args []string) {
|
||
if !f.given() {
|
||
return
|
||
}
|
||
act := link.HandAct{Verb: verb, Args: args, Why: strings.TrimSpace(*f.why),
|
||
Cause: strings.TrimSpace(*f.cause), Condition: strings.TrimSpace(*f.condition)}
|
||
err := onTheBus(func(conn *nats.Conn) error {
|
||
written, err := link.RecordHandAct(ctx, conn, act)
|
||
act = written
|
||
return err
|
||
})
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "this act by hand could NOT be recorded in the hand-act log, and is done anyway: %v\n", err)
|
||
return
|
||
}
|
||
fmt.Printf("recorded as %s in the hand-act log: %s, because %q (cause: %s)\n", act.ID, act.By, act.Why, act.Cause)
|
||
}
|
||
|
||
// handActCommand is `hand-act record` and `hand-acts`.
|
||
func handActCommand(ctx context.Context, args []string) error {
|
||
if len(args) > 0 && args[0] == "record" {
|
||
set := flag.NewFlagSet("hand-act record", flag.ContinueOnError)
|
||
f := addHandActFlags(set)
|
||
positionals, err := parseAround(set, args[1:])
|
||
if err != nil {
|
||
return err
|
||
}
|
||
what := strings.TrimSpace(strings.Join(positionals, " "))
|
||
if what == "" {
|
||
return errors.New("hand-act record <what was done> --why <text> [--cause <word>] [--condition <key>]")
|
||
}
|
||
if err := f.require("hand-act record"); err != nil {
|
||
return err
|
||
}
|
||
if strings.TrimSpace(*f.cause) == "" {
|
||
return errors.New("hand-act record says the cause too: --cause <word>, the word a second " +
|
||
"act for the same reason will use — it is how a repair done twice is found")
|
||
}
|
||
act := link.HandAct{Verb: "hand-act record", Args: []string{what}, Why: strings.TrimSpace(*f.why),
|
||
Cause: strings.TrimSpace(*f.cause), Condition: strings.TrimSpace(*f.condition)}
|
||
return onTheBus(func(conn *nats.Conn) error {
|
||
written, err := link.RecordHandAct(ctx, conn, act)
|
||
if err != nil {
|
||
return fmt.Errorf("the act could not be recorded: %w", err)
|
||
}
|
||
fmt.Printf("recorded as %s: %s did %q, because %q (cause: %s)\n", written.ID, written.By, what,
|
||
written.Why, written.Cause)
|
||
return nil
|
||
})
|
||
}
|
||
if len(args) > 0 && args[0] != "list" && !strings.HasPrefix(args[0], "-") {
|
||
return errors.New("hand-act record <what> --why <text> --cause <word> | hand-acts [--days N] [--json]")
|
||
}
|
||
if len(args) > 0 && args[0] == "list" {
|
||
args = args[1:]
|
||
}
|
||
set := flag.NewFlagSet("hand-acts", flag.ContinueOnError)
|
||
days := set.Int("days", 14, "how many days back")
|
||
asJSON := set.Bool("json", false, "as data")
|
||
if _, err := parseAround(set, args); err != nil {
|
||
return err
|
||
}
|
||
return onTheBus(func(conn *nats.Conn) error {
|
||
now := time.Now()
|
||
acts, err := link.HandActs(ctx, conn, now.Add(-time.Duration(*days)*24*time.Hour))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
repeated := link.RepeatedCauses(repairs(acts), now)
|
||
if *asJSON {
|
||
body, err := json.MarshalIndent(map[string]any{"acts": acts, "repeated": repeated}, "", " ")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
fmt.Println(string(body))
|
||
return nil
|
||
}
|
||
if len(acts) == 0 {
|
||
fmt.Printf("nothing was done by hand in the last %d day(s)\n", *days)
|
||
return nil
|
||
}
|
||
for i := len(acts) - 1; i >= 0; i-- {
|
||
a := acts[i]
|
||
fmt.Printf("%s %s %s %s\n by %s — %s (cause: %s", a.At.Local().Format("2006-01-02 15:04"), a.ID,
|
||
a.Verb, strings.Join(a.Args, " "), a.By, a.Why, a.Cause)
|
||
if a.Condition != "" {
|
||
fmt.Printf(", condition %s", a.Condition)
|
||
}
|
||
fmt.Println(")")
|
||
}
|
||
if len(repeated) > 0 {
|
||
causes := make([]string, 0, len(repeated))
|
||
for c, n := range repeated {
|
||
causes = append(causes, fmt.Sprintf("%s ×%d", c, n))
|
||
}
|
||
sort.Strings(causes)
|
||
fmt.Printf("\ndone by hand more than once in a fortnight — a healer is wanted (to-be 45 S15): %s\n",
|
||
strings.Join(causes, ", "))
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// repairs are the acts that are not a person's decision by design: what S15 counts.
|
||
func repairs(acts []link.HandAct) []link.HandAct {
|
||
out := make([]link.HandAct, 0, len(acts))
|
||
for _, a := range acts {
|
||
if !personsDecision(a) {
|
||
out = append(out, a)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// handActsThisWeek is how many acts were done by hand in the last seven days, for `status`; -1 when
|
||
// the log could not be read, which status says rather than reading as none.
|
||
func handActsThisWeek(ctx context.Context) (int, string) {
|
||
n := -1
|
||
err := onTheBus(func(conn *nats.Conn) error {
|
||
reading, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
defer cancel()
|
||
acts, err := link.HandActs(reading, conn, time.Now().Add(-7*24*time.Hour))
|
||
n = len(acts)
|
||
return err
|
||
})
|
||
if err != nil {
|
||
return -1, err.Error()
|
||
}
|
||
return n, ""
|
||
}
|