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) }