The mesh knew a machine's architecture and kernel family and nothing it is made of, and heard even that only when it sent the machine something. The profile now carries the machine's memory, each local filesystem's size and free space, processor model, cores and threads, distribution and version, kernel release, virtual or physical, boot time and timezone; a fact it cannot read is named unknown with why, never zeroed. The reconcile says the profile unasked once the mesh last heard it 23 hours ago, so on a machine nobody pushed to nothing the mesh holds of it is older than a day.
126 lines
4.5 KiB
Go
126 lines
4.5 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"`
|
|
// Machine is what it is made of (novox/hq issue 160), said with every profile: at enrolment, after
|
|
// every apply, and by the reconcile once a day when nothing else made it speak.
|
|
Machine *Machine `json:"machine,omitempty"`
|
|
}
|
|
|
|
// 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
|
|
})
|
|
describeCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
m := DescribeMachine(describeCtx, RealSources())
|
|
cancel()
|
|
p.Machine = &m
|
|
return p
|
|
}
|