Files
mesh-host/internal/profile/profile.go
T
jschoubben ee2648188d Repoint ADR references after HQ consolidated 65 records to 23
96 comments across the two repos named records that no longer exist. Each now
points at the consolidated record that holds its reasoning -- ADR 0034 (a test
defends a decision) is 0017, the eight host records are 0005, the four lab
records are 0016.

Worth noting for next time: these are references from outside HQ, so renumbering
there is not free. It cost 38 files here.
2026-08-28 23:33:44 +02:00

119 lines
4.1 KiB
Go

// Package profile answers one question: what can this machine be asked to do?
//
// A capability is DETECTED, never assumed. That distinction is the whole point of this
// package and it is not pedantry — novox/hq 04-ISSUES/007 records the fault it exists to
// prevent: an installed package was treated as a capability, and a node was assigned work it
// could not perform because the package was present and the thing was not running.
//
// So a capability here is not "is it installed". It is "does it work", and every detector
// says how it knows.
package profile
import (
"context"
"errors"
"fmt"
"os/exec"
"runtime"
"sort"
"strings"
"time"
)
// Verdict is what a detector concluded, and why.
//
// Why is not decoration. A capability reported absent with no reason is the same problem in a
// new place: something that cannot be acted on. The reason is what a person reads when a node
// will not take work they expected it to take.
type Verdict struct {
Name string `json:"name"`
// Present is true only when the capability is usable, not merely installed.
Present bool `json:"present"`
// Detail says what was observed — a version, a path, or why it is absent.
Detail string `json:"detail"`
// How names the check that produced this, so a wrong answer can be found.
How string `json:"how"`
}
// Detector decides one capability. It is given a context so a hung probe cannot hang the host.
type Detector interface {
Name() string
Detect(ctx context.Context) Verdict
}
// Runner executes a command. Replaceable in tests for the pure-logic layer ONLY — every
// detector in this package is exercised against the real machine as well, because a test that
// fakes the system under detection asserts that the fake behaves as expected
// (novox/hq ADR 0017).
type Runner func(ctx context.Context, name string, args ...string) (stdout string, err error)
// ExecRunner runs a real command, with output captured and stdin closed.
func ExecRunner(ctx context.Context, name string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdin = nil
out, err := cmd.Output()
if err != nil {
var exit *exec.ExitError
if errors.As(err, &exit) {
return string(out), fmt.Errorf("%s exited %d: %s",
name, exit.ExitCode(), strings.TrimSpace(string(exit.Stderr)))
}
return string(out), fmt.Errorf("%s: %w", name, err)
}
return string(out), nil
}
// Profile is every verdict, in a stable order.
type Profile struct {
Architecture string `json:"architecture"`
Kernel string `json:"kernel"`
Capabilities []Verdict `json:"capabilities"`
}
// Has reports whether a named capability is present. Unknown names are absent, not an error:
// asking about a capability nothing detects is a question with a true answer.
func (p Profile) Has(name string) bool {
for _, v := range p.Capabilities {
if v.Name == name {
return v.Present
}
}
return false
}
// Missing lists the names that are not present, in order. What a node cannot do is the half
// that decides whether work may be placed on it.
func (p Profile) Missing() []string {
var out []string
for _, v := range p.Capabilities {
if !v.Present {
out = append(out, v.Name)
}
}
sort.Strings(out)
return out
}
// Detect runs every detector and collects the verdicts.
//
// A detector that fails does not fail the profile. This is deliberately NOT the rule in
// novox/hq ADR 0010: that rule governs applying state, where a failed step means the machine
// is not what was asked for. Detection is the opposite — a failed probe is a finding, and the
// finding is "absent, because the probe failed", which is exactly what a caller needs to know.
// Aborting would replace one legible absence with total ignorance.
func Detect(ctx context.Context, detectors []Detector, timeout time.Duration) Profile {
p := Profile{
Architecture: runtime.GOARCH,
Kernel: runtime.GOOS,
}
for _, d := range detectors {
probeCtx, cancel := context.WithTimeout(ctx, timeout)
p.Capabilities = append(p.Capabilities, d.Detect(probeCtx))
cancel()
}
sort.Slice(p.Capabilities, func(i, j int) bool {
return p.Capabilities[i].Name < p.Capabilities[j].Name
})
return p
}