mesh/merge-gate head 62af89f85d0d merged into trunk 1a2677882380 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 62af89f85d0d merged into trunk 1a2677882380 — pass: its merge-check.sh passed
mesh/delivery delivered
Every 15 s the failed-units look read the own manager of every account the declaration creates, and reading one is a login that starts it: six module accounts' managers on the control node started and stopped all day, most of its journal. A look only reads (ADR 0240 rule 6) and never asks an account's manager (ADR 0177). An account's manager is read now only when the declaration places a user-scoped unit in it, and only after the machine's own manager says user@<uid>.service is active, as the apply asks before it. A manager not running is said away in the units statement, with no failed units; in liveness its units read unknown with the apply's words, never down. The operator's account on a desktop is still read while logged in, since a module places a user unit there (issue 315's Razer daemon stays said).
193 lines
7.2 KiB
Go
193 lines
7.2 KiB
Go
package liveness
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/novox/mesh-host/internal/system"
|
|
)
|
|
|
|
// Runner runs a command and answers what it printed — the apply's own (apply.ExecRunner), so a look
|
|
// reads the machine exactly as an apply does.
|
|
type Runner func(ctx context.Context, name string, args ...string) (string, error)
|
|
|
|
// Exec reads the container runtime and the service manager through their command lines: one inspect of
|
|
// every container, one show of every unit per manager. **Reads only** — the whole of what this file may
|
|
// ask either of them is `container inspect`, `version` and `show`, and of the machine's manager whether an
|
|
// account's own is active (ADR 0240 rule 6, and a test holds it).
|
|
type Exec struct {
|
|
Run Runner
|
|
// System is what the host was built for: it says how the machine's manager is asked whether an
|
|
// account's own runs (system.ManagerRunning).
|
|
System string
|
|
|
|
mu sync.Mutex
|
|
// cri is the runtime that answered, once one has.
|
|
cri string
|
|
}
|
|
|
|
// runtimes are the container runtimes the apply knows, asked for their version in the form each
|
|
// understands (apply.containerRuntimes): the first that answers is the machine's.
|
|
var runtimes = []struct {
|
|
command string
|
|
probe []string
|
|
}{
|
|
{"docker", []string{"version", "--format", "{{.Server.Version}}"}},
|
|
{"podman", []string{"version", "--format", "{{.Version}}"}},
|
|
}
|
|
|
|
func (e *Exec) runtime(ctx context.Context) (string, error) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
if e.cri != "" {
|
|
return e.cri, nil
|
|
}
|
|
var tried []string
|
|
for _, rt := range runtimes {
|
|
if _, err := e.Run(ctx, rt.command, rt.probe...); err == nil {
|
|
e.cri = rt.command
|
|
return rt.command, nil
|
|
}
|
|
tried = append(tried, rt.command)
|
|
}
|
|
return "", fmt.Errorf("no container runtime answers on this machine (tried %s)", strings.Join(tried, ", "))
|
|
}
|
|
|
|
// inspectFormat is what one inspect says of each container: its name, id, status, the runtime's restart
|
|
// count, when its current run started — and the state of the check the runtime runs as the container's
|
|
// own, read out of the whole state as one line of JSON, for a check declared exec or runtime (Phase B).
|
|
// The whole state and not its Health: a container that carries no check has no such key, and the runtime
|
|
// refuses a template naming a key that is not there — for every container in the read.
|
|
const inspectFormat = "{{.Name}}\t{{.Id}}\t{{.State.Status}}\t{{.RestartCount}}\t{{.State.StartedAt}}\t{{json .State}}"
|
|
|
|
// Containers is one inspect of every container named. A name the runtime does not have is not found;
|
|
// a runtime that does not answer is an error — unknown, never down.
|
|
func (e *Exec) Containers(ctx context.Context, names []string) (map[string]Observed, error) {
|
|
out := map[string]Observed{}
|
|
if len(names) == 0 {
|
|
return out, nil
|
|
}
|
|
cri, err := e.runtime(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
args := append([]string{"container", "inspect", "--format", inspectFormat}, names...)
|
|
said, err := e.Run(ctx, cri, args...)
|
|
if err != nil && !missing(err) {
|
|
// Asked again next look, in case another runtime is what answers now.
|
|
e.mu.Lock()
|
|
e.cri = ""
|
|
e.mu.Unlock()
|
|
return nil, fmt.Errorf("the container runtime could not be read: %w", err)
|
|
}
|
|
for _, line := range strings.Split(strings.TrimSpace(said), "\n") {
|
|
parts := strings.Split(line, "\t")
|
|
if len(parts) < 5 {
|
|
continue
|
|
}
|
|
name := strings.TrimPrefix(strings.TrimSpace(parts[0]), "/")
|
|
restarts, _ := strconv.ParseInt(strings.TrimSpace(parts[3]), 10, 64)
|
|
status := strings.TrimSpace(parts[2])
|
|
o := Observed{Found: true, Identity: strings.TrimSpace(parts[1]), Running: status == "running",
|
|
Restarting: status == "restarting", Restarts: restarts, Started: strings.TrimSpace(parts[4])}
|
|
if len(parts) > 5 {
|
|
o.Health, o.HealthSaid = runtimeHealth(strings.Join(parts[5:], "\t"))
|
|
}
|
|
out[name] = o
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// runtimeHealth reads, from a container's state, the runtime's state of its own check: its status and what its last look
|
|
// printed, in a line. Nothing when the container carries no check.
|
|
func runtimeHealth(said string) (string, string) {
|
|
var state struct {
|
|
Health *struct {
|
|
Status string
|
|
Log []struct {
|
|
ExitCode int
|
|
Output string
|
|
}
|
|
}
|
|
}
|
|
if err := json.Unmarshal([]byte(strings.TrimSpace(said)), &state); err != nil || state.Health == nil ||
|
|
state.Health.Status == "" {
|
|
return "", ""
|
|
}
|
|
h := state.Health
|
|
last := ""
|
|
if n := len(h.Log); n > 0 {
|
|
l := h.Log[n-1]
|
|
last = firstLine(l.Output)
|
|
if last == "" && l.ExitCode != 0 {
|
|
last = fmt.Sprintf("its check exited %d", l.ExitCode)
|
|
}
|
|
if len(last) > 200 {
|
|
last = last[:200] + "…"
|
|
}
|
|
}
|
|
return strings.ToLower(h.Status), last
|
|
}
|
|
|
|
// missing is an inspect that failed only because a name is not there: what it printed for the others is
|
|
// still the answer.
|
|
func missing(err error) bool {
|
|
s := strings.ToLower(err.Error())
|
|
return strings.Contains(s, "no such container") || strings.Contains(s, "no such object")
|
|
}
|
|
|
|
// unitProperties are what one show says of each unit.
|
|
const unitProperties = "Id,LoadState,ActiveState,SubState,NRestarts,InvocationID"
|
|
|
|
// Units is one show of every unit named, in the manager it is in: the machine's, or an account's own.
|
|
func (e *Exec) Units(ctx context.Context, scope, user string, units []string) (map[string]Observed, error) {
|
|
out := map[string]Observed{}
|
|
if len(units) == 0 {
|
|
return out, nil
|
|
}
|
|
args := []string{"show", "--property=" + unitProperties, "--"}
|
|
if scope == "user" && user != "" {
|
|
// **A look starts no account's manager** (novox/hq issue 764, ADR 0177): reading it is a login,
|
|
// which starts it when none runs. The machine's manager is asked first; a manager not running is
|
|
// away — its units unread, said unknown with why, never down — as the apply says them waiting.
|
|
running, exists, err := system.ManagerRunning(ctx, e.System, system.Runner(e.Run), user)
|
|
switch {
|
|
case err != nil:
|
|
return nil, fmt.Errorf("whether %s's own service manager runs could not be read: %w", user, err)
|
|
case !exists:
|
|
return nil, fmt.Errorf("the machine has no account %q, whose own manager these units are in", user)
|
|
case !running:
|
|
return nil, fmt.Errorf("%s's own service manager is not running: the account is not logged in "+
|
|
"and does not linger", user)
|
|
}
|
|
args = append([]string{"--user", "--machine=" + user + "@"}, args...)
|
|
}
|
|
said, err := e.Run(ctx, "systemctl", append(args, units...)...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the service manager could not be read: %w", err)
|
|
}
|
|
blocks := strings.Split(strings.TrimSpace(said), "\n\n")
|
|
if len(blocks) != len(units) {
|
|
return nil, errors.New("the service manager answered for a different number of units than were asked")
|
|
}
|
|
for i, block := range blocks {
|
|
props := map[string]string{}
|
|
for _, line := range strings.Split(block, "\n") {
|
|
if k, v, ok := strings.Cut(line, "="); ok {
|
|
props[strings.TrimSpace(k)] = strings.TrimSpace(v)
|
|
}
|
|
}
|
|
restarts, _ := strconv.ParseInt(props["NRestarts"], 10, 64)
|
|
out[units[i]] = Observed{Found: props["LoadState"] != "not-found", Identity: props["InvocationID"],
|
|
Running: props["ActiveState"] == "active",
|
|
Restarting: props["ActiveState"] == "activating" && props["SubState"] == "auto-restart",
|
|
Restarts: restarts}
|
|
}
|
|
return out, nil
|
|
}
|