// Package health is the answer a module's health tool gives, and how the node-engine reads it (novox/hq ADR 0240 // rule 2, ADR 0283). // // A health tool answers one of three: // // - healthy: everything the module runs works; // - unhealthy: something is wrong, said in Why; // - waits: nothing is wrong but a part that cannot work until the operator gives something — one of the module's // own secrets, or one of its settings. Each Wait names exactly one of them, the part, and what the operator // gives, in words with no path, address or value. // // The answer keeps `healthy` beside `state`, so a node-engine older than ADR 0283 reads a wait as not healthy, as // it did before. An answer without `state` is read from `healthy`. // // The module writes its answer with Healthy, Unhealthy or Waiting, and the node-engine reads it with Read. Read // never trusts a shape it cannot check: a wait it cannot read makes the answer unhealthy, saying why. package health import ( "encoding/json" "fmt" "regexp" "strings" ) // The three states an answer says. const ( StateHealthy = "healthy" StateUnhealthy = "unhealthy" StateWaits = "waits" ) // Answer is a health tool's answer. type Answer struct { // State is healthy, unhealthy or waits. State string `json:"state"` // Healthy is State == healthy, kept for a reader older than State. Healthy bool `json:"healthy"` // Why is the answer in one or more plain sentences. Why string `json:"why"` // Waits is what the module waits for, when State is waits; and, beside a fault, what it also waits for. Waits []Wait `json:"waits,omitempty"` } // Wait is one thing a module waits for the operator to give: exactly one of Secret and Setting. type Wait struct { // Part is what waits, in words: "the source games". Part string `json:"part"` // Secret is one of the module's own secrets, by name (a member of a secret family by its full name). Secret string `json:"secret,omitempty"` // Setting is one of the module's settings, by name. Setting string `json:"setting,omitempty"` // What is what the operator gives, in words: "the password of the source games". What string `json:"what"` } // Healthy is a healthy answer. func Healthy(why string) Answer { return Answer{State: StateHealthy, Healthy: true, Why: why} } // Unhealthy is an unhealthy answer. Waits may be given beside the fault: they are said, and excuse nothing. func Unhealthy(why string, waits ...Wait) Answer { return Answer{State: StateUnhealthy, Why: why, Waits: waits} } // Waiting is an answer that says nothing is wrong but what the operator has yet to give. With no wait it is // unhealthy: a module that waits names what for. func Waiting(why string, waits ...Wait) Answer { if len(waits) == 0 { return Unhealthy(orSaid(why, "it says it waits") + " (and names nothing it waits for)") } for _, w := range waits { if err := w.Check(); err != nil { return Unhealthy(orSaid(why, "it says it waits") + " (" + err.Error() + ")") } } return Answer{State: StateWaits, Why: why, Waits: waits} } // name is a secret's or a setting's name: what a manifest may declare. var name = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // looksLikeAddress finds what the operator's words never carry: a URL, an IPv4 or IPv6 address, a host and port, a // path (absolute, from a home or relative, on its own or after a bracket or a quote), a UNC path or a drive path. // It cannot find a value typed in plain words ("the password hunter2"): a module never puts one there, and the // words are its own, not the operator's. var looksLikeAddress = regexp.MustCompile(strings.Join([]string{ `://`, `\b[0-9]{1,3}(\.[0-9]{1,3}){3}\b`, `[0-9A-Fa-f]{0,4}::[0-9A-Fa-f]{0,4}`, `\b[0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){3,}\b`, `\b[A-Za-z0-9.-]+:[0-9]{1,5}\b`, `(^|[\s(\[{"'\x60])[~.]{0,2}/`, `\\\\`, `\b[A-Za-z]:\\`, }, "|")) // Check says why a wait cannot be read, or nil. func (w Wait) Check() error { switch { case w.Secret != "" && w.Setting != "": return fmt.Errorf("a wait names both the secret %q and the setting %q; it names one", w.Secret, w.Setting) case w.Secret == "" && w.Setting == "": return fmt.Errorf("a wait names neither a secret nor a setting") case w.Secret != "" && !name.MatchString(w.Secret): return fmt.Errorf("a wait names the secret %q, which is not a name", w.Secret) case w.Setting != "" && !name.MatchString(w.Setting): return fmt.Errorf("a wait names the setting %q, which is not a name", w.Setting) case strings.TrimSpace(w.What) == "": return fmt.Errorf("a wait for %s does not say what the operator gives", w.Names()) case strings.TrimSpace(w.Part) == "": return fmt.Errorf("a wait for %s does not say which part waits", w.Names()) } for _, words := range []string{w.What, w.Part} { if strings.ContainsAny(words, "\n\r") { return fmt.Errorf("a wait for %s says more than one line", w.Names()) } if looksLikeAddress.MatchString(words) { return fmt.Errorf("a wait for %s says a path or an address, which the operator's words never carry", w.Names()) } } return nil } // Names is what the wait names, as "the secret x" or "the setting y". func (w Wait) Names() string { if w.Secret != "" { return "the secret " + w.Secret } if w.Setting != "" { return "the setting " + w.Setting } return "nothing" } // Read reads a health tool's result. An answer it cannot read at all is an error; an answer whose wait it cannot // read is unhealthy, saying why, so a malformed wait never excuses anything. func Read(result []byte) (Answer, error) { var a Answer if err := json.Unmarshal(result, &a); err != nil { return Answer{}, fmt.Errorf("it answered something other than healthy or not: %w", err) } switch a.State { case "": if a.Healthy { a.State = StateHealthy } else { a.State = StateUnhealthy } case StateHealthy: if !a.Healthy { return Unhealthy(fmt.Sprintf("it answered the state healthy and healthy false, which contradict each other (%s)", orSaid(a.Why, "and said nothing more"))), nil } // A healthy module waits for nothing: a wait beside healthy is a contradiction, and is dropped. a.Waits = nil case StateUnhealthy: a.Healthy = false case StateWaits: a.Healthy = false if len(a.Waits) == 0 { return Unhealthy(orSaid(a.Why, "it says it waits") + " (and names nothing it waits for)"), nil } for _, w := range a.Waits { if err := w.Check(); err != nil { return Unhealthy(orSaid(a.Why, "it says it waits") + " (" + err.Error() + ")"), nil } } default: return Unhealthy(fmt.Sprintf("it answered the state %q, which is none of healthy, unhealthy or waits", a.State)), nil } if a.State != StateWaits { // Waits beside a fault are said and excuse nothing; one that cannot be read is dropped, not believed. var kept []Wait for _, w := range a.Waits { if w.Check() == nil { kept = append(kept, w) } } a.Waits = kept } return a, nil } func orSaid(s, otherwise string) string { if strings.TrimSpace(s) == "" { return otherwise } return s }