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