package main import ( "context" "fmt" "sort" "strings" "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // A part that waits for the operator's secret or setting (novox/hq ADR 0283, issue 386). // // **A module's tool check may say it waits**: nothing of it is wrong but a part that cannot work until the operator // gives one of its own secrets or one of its settings. The node-engine states such a resource `waiting`, with what // it waits for. A module saying so is an assertion, so **the controller checks each wait before it excuses it**: // // - a wait for a secret names an own secret the module's manifest declares — by its name, or as a member of a // secret family — said `"issued-by": "outside"`, and the store holds no value a person gave for it on that // machine; // - a wait for a setting names a setting the manifest declares (checked by name only: the controller cannot tell // whether a free-form value covers a part, and the needs-operator condition is where a false one shows). // // An excused wait is read by the first-node gate as *waits for a person* (ADR 0254), a pass carried in the verdict, // for any build of the module — a secret not given is owed by every build alike. It is said to the operator as // `module...needs-operator`, naming the act. A wait that fails the check is judged unhealthy, saying // why, and raises the module's `unhealthy` condition. // kindNeedsOperator is a module's condition while a part of it waits for the operator's secret or setting. const kindNeedsOperator = "needs-operator" // needsOperatorKey is a module's needs-operator condition on a machine. func needsOperatorKey(module, node string) string { return conditions.Key(conditions.ScopeModule, module+"."+node, kindNeedsOperator) } // operatorWaitFacts is what the controller holds to check a module's waits: the manifest judged per module, and per // "@" the own secrets a person gave there, with when. A module or a machine absent is not known, // and no wait of it is excused. type operatorWaitFacts struct { manifests map[string]catalogue.Manifest given map[string]map[string]time.Time } // checkWait is nil when a wait is excused, and otherwise why not, in words. Pure. func checkWait(module, machine string, w inventory.Wait, f operatorWaitFacts) error { m, known := f.manifests[module] if !known { return fmt.Errorf("says it waits for %s, and the mesh holds no manifest of %s to check it against", waitNames(w), module) } switch { case w.Secret != "" && w.Setting != "", w.Secret == "" && w.Setting == "": return fmt.Errorf("says it waits, naming %s, where a wait names one secret or one setting", waitNames(w)) case w.Setting != "": if _, declared := m.Settings[w.Setting]; !declared { return fmt.Errorf("says it waits for the setting %s, which %s does not declare", w.Setting, module) } return nil } own, _, declared := m.OwnSecrets.Lookup(w.Secret) if !declared { return fmt.Errorf("says it waits for the secret %s, which %s does not declare", w.Secret, module) } if own.IssuedBy != catalogue.IssuedOutside { return fmt.Errorf("says it waits for the secret %s, which the mesh makes itself: only a secret issued outside "+ "the mesh waits for the operator", w.Secret) } given, readable := f.given[module+"@"+machine] if !readable { return fmt.Errorf("says it waits for the secret %s, and what was given on %s could not be read", w.Secret, machine) } if at, was := given[w.Secret]; was { return fmt.Errorf("says it waits for the secret %s, which was given at %s", w.Secret, at.UTC().Format("2006-01-02 15:04 MST")) } return nil } // waitRefusedPrefix opens every reason checkWait gives, so the words of a refused wait are told from a check's own. const waitRefusedPrefix = "says it waits" // waitNames is what a wait names, as "the secret x" or "the setting y". func waitNames(w inventory.Wait) string { switch { case w.Secret != "" && w.Setting != "": return "the secret " + w.Secret + " and the setting " + w.Setting case w.Secret != "": return "the secret " + w.Secret case w.Setting != "": return "the setting " + w.Setting } return "nothing" } // checkWaiting reads one machine's resources against the facts: every waiting resource whose waits all check out is // kept as said; one with a wait that does not, or with no wait at all, is answered as unhealthy with why. Pure; the // statement as kept is not changed. func checkWaiting(machine string, rs []inventory.ResourceHealth, f operatorWaitFacts) []inventory.ResourceHealth { out := make([]inventory.ResourceHealth, 0, len(rs)) for _, r := range rs { if r.State == link.StateWaiting { var why error if len(r.Waits) == 0 { why = fmt.Errorf("says it waits, and names nothing it waits for") } for _, w := range r.Waits { if why == nil { why = checkWait(r.Module, machine, w, f) } } if why != nil { r.State, r.Reason = link.StateUnhealthy, why.Error() } } out = append(out, r) } return out } // operatorWait is whether everything not healthy of a module on a machine is waiting with its waits checked // (checkWaiting already applied), and those waits. A module with anything unhealthy, starting or unknown beside it // does not wait: it is judged as before. func operatorWait(module string, rs []inventory.ResourceHealth) ([]inventory.Wait, bool) { var waits []inventory.Wait for _, r := range rs { if r.Module != module { continue } switch r.State { case link.StateHealthy: case link.StateWaiting: waits = append(waits, r.Waits...) default: return nil, false } } return waits, len(waits) > 0 } // operatorWaitSaid is a module's wait for the operator in one sentence, for the gate's verdict and the condition's // summary: what the operator gives and what it names, and for a secret the line that opens the desk prompt. func operatorWaitSaid(module, machine string, waits []inventory.Wait) string { var parts []string for _, w := range waits { part := fmt.Sprintf("%s (%s", w.What, waitNames(w)) if w.Secret != "" { part += fmt.Sprintf(", given with `nox secret ask %s %s %s`", machine, module, w.Secret) } parts = append(parts, part+")") } return fmt.Sprintf("%s on %s waits for the operator: %s", module, machine, strings.Join(parts, "; ")) } // needsOperatorObservation is a module whose only parts not healthy wait for the operator (ADR 0283): the operator's, // a warning however long it stands, its plain words naming the act and never saying there is nothing to do. func needsOperatorObservation(module, node string, waits []inventory.Wait, rs []inventory.ResourceHealth) conditions.Observation { o := moduleUnhealthyObservation(module, node, rs) o.Token, o.Kind, o.Resolver, o.Severity = kindNeedsOperator, kindNeedsOperator, conditions.ResolverOperator, conditions.Warning o.Summary = operatorWaitSaid(module, node, waits) w := needsOperatorWords(module, node, waits) o.Headline, o.Explanation, o.Needs, o.Resolved, o.Actions = w.Headline, w.Explanation, w.Needs, w.Resolved, nil return o } // needsOperatorWords is what the operator reads of a module waiting for them (ADR 0253, ADR 0283): the act, for a // secret typed at the machine's desk prompt and for a setting approved when an agent proposes it. The secret's and // the setting's names, and the line, are in the summary for whoever looks closer. func needsOperatorWords(module, node string, waits []inventory.Wait) words { var acts []string seen := map[string]bool{} secret := false for _, w := range waits { var act string switch { case w.Secret != "": act, secret = fmt.Sprintf("type %s at %s's desk prompt", w.What, node), true case w.Setting != "": act = fmt.Sprintf("approve %s of %s on %s when it is proposed to you", w.Setting, module, node) } if act != "" && !seen[act] { seen[act] = true acts = append(acts, act) } } needs := strings.Join(acts, "; and ") + "." // Plain words hold one sentence of at most conditions.NeedsMax characters: several acts are named in the // summary instead. if len(acts) == 0 || len(needs) > conditions.NeedsMax { needs = fmt.Sprintf("give what %s waits for on %s; the details name each secret and setting.", module, node) } explanation := fmt.Sprintf("Part of %s on %s cannot work until you give what it waits for.", module, node) if secret { explanation += " A hidden prompt opens at the desk when the secret is asked for, and what you type there " + "is sealed to the machine." } explanation += " Its update is in place and nothing was undone; it carries on by itself once it is given." return words{ Headline: fmt.Sprintf("%s waits for you on %s", module, node), Needs: needs, Explanation: explanation, Resolved: fmt.Sprintf("%s on %s no longer waits for you", module, node), } } // readWaitFacts reads what the controller holds to check the waits of the modules named on one machine: the // manifests (the catalogue's, or those given) and the secrets given there. A read that fails leaves that module // unknown, so none of its waits is excused. func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string, modules []string, manifests map[string]catalogue.Manifest, f *operatorWaitFacts) { if f.manifests == nil { f.manifests = map[string]catalogue.Manifest{} } if f.given == nil { f.given = map[string]map[string]time.Time{} } if inv == nil { return } var shelf map[string]catalogue.Manifest sort.Strings(modules) for _, module := range modules { if _, has := f.manifests[module]; !has { if m, given := manifests[module]; given { f.manifests[module] = m } else { if shelf == nil { var err error if shelf, err = inv.Catalogue(ctx); err != nil { shelf = map[string]catalogue.Manifest{} } } if m, known := shelf[module]; known { f.manifests[module] = m } } } if _, read := f.given[module+"@"+machine]; read { continue } if given, err := inv.GivenOwnSecrets(ctx, machine, module); err == nil { f.given[module+"@"+machine] = given } } } // waitingModules is every module with a waiting resource in a statement. func waitingModules(rs []inventory.ResourceHealth) []string { seen := map[string]bool{} var out []string for _, r := range rs { if r.State == link.StateWaiting && r.Module != "" && !seen[r.Module] { seen[r.Module] = true out = append(out, r.Module) } } return out }