The operator reads manifest, declaration and send, not definition, composition and update. D1 already resolves every machine, so the left-out judgement rides on it instead of resolving each machine again. A test holds that a wait's own needs-operator still clears beside it.
223 lines
10 KiB
Go
223 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
)
|
|
|
|
// A module assigned to a machine and left out of its declaration is said (novox/hq issue 380).
|
|
//
|
|
// **An omission is a finding, never a refusal of the push** (ADR 0163, rule 6): the machine is sent everything
|
|
// else, and the module's held things are kept. Until issue 380 the only place it showed was `plan`: nfs-server was
|
|
// assigned to the home server for days, every send left it out for a setting nobody gave, and the operator believed
|
|
// it ran. So the self-check (D1) judges every machine as the next send would (Resolution.LeftOutBecause, the send's
|
|
// own judgement) and raises a condition on that machine for each module it leaves out:
|
|
//
|
|
// - a setting nobody gave (catalogue.UnsetSettingError) is the operator's to give: kind needs-operator, naming
|
|
// the module, the setting and the command that sets it;
|
|
// - any other cause is said as a module not working is: kind left-out, a warning with the reason as evidence.
|
|
//
|
|
// Never urgent and never escalated by age (as ADR 0283 decision 5): nothing the machine ran was undone. It clears
|
|
// on the first run that no longer finds it — the module composed again, or no longer assigned. `status` and
|
|
// `node show` list the same modules under "assigned, not applied" with the same reason.
|
|
//
|
|
// A module left out because its stored manifest has a key this controller does not know is not raised here: the
|
|
// catalogue's own unknown-field condition says it once for the whole mesh (ADR 0262); it is still listed.
|
|
|
|
const (
|
|
// probeLeftOutID is the self-check's probe that raises and clears these conditions: D1, which already
|
|
// resolves every machine (probeDeclarations), so the judgement costs no resolution of its own.
|
|
probeLeftOutID = "D1"
|
|
// kindLeftOut is a module left out for any cause but a setting nobody gave; it is also every such condition's
|
|
// token, whichever its kind, so a cause that changes is the same condition said anew.
|
|
kindLeftOut = "left-out"
|
|
)
|
|
|
|
// leftOutModule is one module of a machine's set that its declaration leaves out, and why.
|
|
type leftOutModule struct {
|
|
Module string
|
|
// Setting is the setting nobody gave, when that is the cause.
|
|
Setting string
|
|
// Why is the declaration's own reason, whole.
|
|
Why string
|
|
// Unread is a stored manifest this controller cannot read whole (said by the catalogue's condition).
|
|
Unread bool
|
|
}
|
|
|
|
// leftOutOf is every module of a machine's resolution that a push would leave out, sorted. Pure: the judgement
|
|
// the push makes (catalogue.Resolution.LeftOutBecause), nothing allocated.
|
|
func leftOutOf(plan catalogue.Resolution, settings catalogue.SettingsBy, adopted bool) []leftOutModule {
|
|
because := plan.LeftOutBecause(settings, adopted)
|
|
out := make([]leftOutModule, 0, len(because))
|
|
for module, why := range because {
|
|
l := leftOutModule{Module: module, Why: oneLine(why.Error())}
|
|
var unset *catalogue.UnsetSettingError
|
|
var unread *catalogue.UnreadManifestError
|
|
switch {
|
|
case errors.As(why, &unset):
|
|
l.Setting = unset.Setting
|
|
case errors.As(why, &unread):
|
|
l.Unread = true
|
|
}
|
|
out = append(out, l)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Module < out[j].Module })
|
|
return out
|
|
}
|
|
|
|
// settingCommand is the command that gives a module's setting on one machine.
|
|
func settingCommand(module, setting, node string) string {
|
|
return fmt.Sprintf("`settings set %s '{%q: …}' --node %s`", module, setting, node)
|
|
}
|
|
|
|
// reason is why a module is left out, as `status`, `node show` and the condition's summary say it: for a setting
|
|
// nobody gave, the setting and the command that gives it; otherwise the declaration's own words.
|
|
func (l leftOutModule) reason(node string) string {
|
|
if l.Setting != "" {
|
|
return fmt.Sprintf("nothing sets its setting %q, so every send leaves it out of its declaration — %s sets it", l.Setting,
|
|
settingCommand(l.Module, l.Setting, node))
|
|
}
|
|
return "every send leaves it out of its declaration: " + l.Why
|
|
}
|
|
|
|
// leftOutObservations are the conditions a machine's left-out modules raise, one each.
|
|
func leftOutObservations(node string, left []leftOutModule) []conditions.Observation {
|
|
var out []conditions.Observation
|
|
for _, l := range left {
|
|
if l.Unread {
|
|
continue
|
|
}
|
|
out = append(out, leftOutObservation(node, l))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// leftOutObservation is one module left out of one machine's declaration: needs-operator for a setting nobody
|
|
// gave, left-out otherwise; a warning either way, the operator's to resolve.
|
|
func leftOutObservation(node string, l leftOutModule) conditions.Observation {
|
|
o := conditions.Observation{Scope: conditions.ScopeModule, ID: l.Module + "." + node, Token: kindLeftOut,
|
|
Kind: kindLeftOut, Machine: node, Severity: conditions.Warning, Resolver: conditions.ResolverOperator,
|
|
Summary: fmt.Sprintf("%s is assigned to %s and not applied: %s", l.Module, node, l.reason(node)),
|
|
Said: l.Why}
|
|
w := leftOutWords(l.Module, node, l.Setting)
|
|
if l.Setting != "" {
|
|
o.Kind = kindNeedsOperator
|
|
} else {
|
|
// The words of why may name a path or an address, which the operator's channel withholds: the
|
|
// summary sends them to the evidence, and `plan` says them in full.
|
|
o.Summary = fmt.Sprintf("%s is assigned to %s and not applied: every send leaves it out of its declaration, "+
|
|
"because what is set for it does not fit its manifest — the evidence and `plan %s` say why", l.Module, node, node)
|
|
}
|
|
o.Headline, o.Explanation, o.Needs, o.Resolved = w.Headline, w.Explanation, w.Needs, w.Resolved
|
|
return o
|
|
}
|
|
|
|
// leftOutWords is what the operator reads of a module left out (ADR 0253): plain, the act named.
|
|
func leftOutWords(module, node, setting string) words {
|
|
w := words{
|
|
Headline: fmt.Sprintf("%s is not applied on %s", module, node),
|
|
Explanation: fmt.Sprintf("%s is assigned to %s, and every send to %s leaves it out of the declaration because what "+
|
|
"is set for it does not fit its manifest. Nothing of it changes there; the rest of %s is sent as usual.",
|
|
module, node, node, node),
|
|
Needs: fmt.Sprintf("read why in the details, then change what is set for %s or unassign it.", module),
|
|
Resolved: fmt.Sprintf("%s on %s is no longer left out", module, node),
|
|
}
|
|
if setting != "" {
|
|
w.Explanation = fmt.Sprintf("%s is assigned to %s, and every send to %s leaves it out of the declaration because "+
|
|
"nothing sets its setting %s. Nothing of it runs there until it is set; the rest of %s is sent as usual.",
|
|
module, node, node, setting, node)
|
|
w.Needs = fmt.Sprintf("set %s for %s on %s, or approve it when it is proposed to you.", setting, module, node)
|
|
// A setting's name that is not plain (a dotted key) is in the summary instead.
|
|
if _, ok := conditions.PlainWords(w, node); !ok {
|
|
w.Explanation = fmt.Sprintf("%s is assigned to %s, and every send to %s leaves it out of the declaration because a "+
|
|
"setting it needs is not set. Nothing of it runs there until it is set; the details name it.",
|
|
module, node, node)
|
|
w.Needs = fmt.Sprintf("set what %s needs on %s; the details name the setting.", module, node)
|
|
}
|
|
}
|
|
return w
|
|
}
|
|
|
|
// notAppliedLines is a machine's "assigned, not applied" as `node show` prints it: one line a module, with the
|
|
// reason its condition says.
|
|
func notAppliedLines(node string, left []leftOutModule) []string {
|
|
if len(left) == 0 {
|
|
return nil
|
|
}
|
|
lines := []string{"", " assigned, not applied:"}
|
|
for _, l := range left {
|
|
lines = append(lines, fmt.Sprintf(" %-22s %s", l.Module, l.reason(node)))
|
|
}
|
|
return lines
|
|
}
|
|
|
|
// machineNotApplied is one module assigned to a machine and left out of its declaration, in `status --json`.
|
|
type machineNotApplied struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Setting string `json:"setting,omitempty"`
|
|
Reason string `json:"reason"`
|
|
// UnreadManifest is a module left out because this controller cannot read its manifest whole: it raises no
|
|
// condition of its own on the machine, since the catalogue's unknown-field condition says it once (ADR 0262).
|
|
UnreadManifest bool `json:"unread-manifest,omitempty"`
|
|
}
|
|
|
|
// notApplied is every machine's left-out modules, in a stated order, as `status --json` carries them.
|
|
func notApplied(left map[string][]leftOutModule) []machineNotApplied {
|
|
names := make([]string, 0, len(left))
|
|
for name := range left {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
var out []machineNotApplied
|
|
for _, name := range names {
|
|
for _, l := range left[name] {
|
|
out = append(out, machineNotApplied{Node: name, Module: l.Module, Setting: l.Setting, Reason: l.reason(name),
|
|
UnreadManifest: l.Unread})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// printNotApplied is status's "assigned, not applied", every machine's.
|
|
func printNotApplied(left map[string][]leftOutModule) {
|
|
rows := notApplied(left)
|
|
if len(rows) == 0 {
|
|
return
|
|
}
|
|
unread := 0
|
|
for _, r := range rows {
|
|
if r.UnreadManifest {
|
|
unread++
|
|
}
|
|
}
|
|
// Which raise a condition is said, not implied (review of #223): one whose manifest this controller cannot
|
|
// read is listed here and raises none of its own on the machine — the catalogue's condition says it.
|
|
raises := "each raises a condition on its machine"
|
|
switch {
|
|
case unread == len(rows):
|
|
raises = "none raises a condition on its machine: this controller cannot read their manifests, which the " +
|
|
"catalogue's own condition says"
|
|
case unread > 0:
|
|
raises += fmt.Sprintf(", except the %d whose manifest this controller cannot read, which the catalogue's own "+
|
|
"condition says", unread)
|
|
}
|
|
fmt.Printf("%d module(s) assigned, not applied — every send leaves them out of their declaration; %s:\n",
|
|
len(rows), raises)
|
|
for _, r := range rows {
|
|
fmt.Printf(" %-12s %-22s %s\n", r.Node, r.Module, r.Reason)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
// isLeftOutCondition is whether a condition is a module left out of its declaration, which the machine's health
|
|
// statements neither raise nor clear: by its token, which every one carries whatever its kind.
|
|
func isLeftOutCondition(c conditions.Condition) bool {
|
|
return c.Subject.Scope == conditions.ScopeModule && strings.HasSuffix(c.Key, "."+kindLeftOut)
|
|
}
|