Files
mesh-sdk/go/health/health.go
T
jochen 33917f91ac
mesh/merge-gate pass: builds mesh-sdk, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)
mesh/delivery delivered
mesh/delivery-group group feat/386-a-wait-for-the-operator merged: every member is merged
Refuse every form of path or address in a wait's words, a contradicting healthy answer, and a malformed wait at Waiting
From the review of mesh-sdk#18: quoted and bracketed paths, home and relative
paths, IPv6, host and port, UNC and drive paths passed the check.
2026-10-10 21:10:01 +02:00

192 lines
6.9 KiB
Go

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