Stage 1 — the host reports what a machine is and can do
Tier 0's first slice, per novox/hq 03-DESIGN/01-to-be/05-the-node-host.md. It applies nothing, connects to nothing, listens on nothing. 2.9 MB, static, no dynamic dependencies: copy it onto a machine and run it is the whole install, which is the property ADR 0041 rests on. A capability is detected, never assumed. Every detector runs something that only succeeds if the thing FUNCTIONS — the daemon is asked for its version, the package database is queried, the firewall is asked to list a ruleset, which needs the privilege as well as the tool. 04-ISSUES/007 is the fault this prevents: a client on disk with its daemon down looks exactly like a working runtime, and a node assigned work on that basis fails when the work arrives. Every verdict carries the reason and the method. A capability reported absent with no reason is the same fault in a new place: something nobody can act on. Two bugs found by running rather than reasoning, both silent: systemctl is-system-running exits non-zero for every state except `running` — including `degraded`, which means units failed and the init is emphatically there. Reading the exit code reported NO service manager on a machine whose init it was. That is 007 in the mirror, and both directions place work wrongly. A verdict now reads what a tool says about itself, not only how it exited. And `mesh-host inventory --json` printed text: the standard library stops parsing at the first non-flag argument, so the flag sat unread and the command exited 0 having ignored what was asked. The parser now takes the subcommand off the front, and a stray or mistyped argument is refused rather than dropped. Detection deliberately does NOT follow ADR 0008. That rule governs applying state, where a failed step means the machine is not what was asked for. A failed probe is a finding — "absent, because the probe failed" — and aborting would replace one legible absence with total ignorance of the rest. 25 tests: structure and logic with a fake runner, and the same detectors against this machine, because a test that fakes the system under detection asserts only that the fake behaves as expected.
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Named capabilities. Constants rather than strings at the call site, because a capability
|
||||
// nothing declares is a capability nothing can require, and a typo would produce exactly that.
|
||||
const (
|
||||
CapContainerRuntime = "container-runtime"
|
||||
CapPackageManager = "package-manager"
|
||||
CapServiceManager = "service-manager"
|
||||
CapFirewall = "firewall"
|
||||
CapOverlay = "overlay"
|
||||
CapGraphicalSession = "graphical-session"
|
||||
CapPrivileged = "privileged"
|
||||
)
|
||||
|
||||
// commandCapability is the shape most detectors take: run something, and treat a working
|
||||
// invocation as evidence.
|
||||
//
|
||||
// It runs a command that only succeeds if the thing is FUNCTIONING, never `--version` alone.
|
||||
// A version string proves a binary is on disk, which is the assumption 04-ISSUES/007 records
|
||||
// as false: the package was installed and the daemon was not running.
|
||||
type commandCapability struct {
|
||||
name string
|
||||
command string
|
||||
args []string
|
||||
// why describes what a success actually proves, and is reported as the detector's `How`.
|
||||
why string
|
||||
// interpret decides the verdict from what the command said and how it exited.
|
||||
//
|
||||
// Exists because "exit zero" is not a universal answer. A degraded service manager reports
|
||||
// its state on stdout and exits non-zero — it is running, and reading only the exit code
|
||||
// declared no service manager on a machine whose init it was. That is 04-ISSUES/007 in the
|
||||
// mirror: 007 is installed-but-broken reported present; this is working-but-imperfect
|
||||
// reported absent. Both place work wrongly, and this one was only visible by running
|
||||
// against a real machine.
|
||||
//
|
||||
// nil means the ordinary rule: success is exit zero.
|
||||
interpret func(stdout string, err error) (present bool, detail string)
|
||||
runner Runner
|
||||
}
|
||||
|
||||
func (c commandCapability) Name() string { return c.name }
|
||||
|
||||
func (c commandCapability) Detect(ctx context.Context) Verdict {
|
||||
out, err := c.runner(ctx, c.command, c.args...)
|
||||
|
||||
interpret := c.interpret
|
||||
if interpret == nil {
|
||||
interpret = exitZero
|
||||
}
|
||||
present, detail := interpret(out, err)
|
||||
|
||||
if strings.TrimSpace(detail) == "" {
|
||||
// A verdict with no reason is the fault in a new place: something nobody can act on.
|
||||
// Reached when a command fails silently, which systemctl does.
|
||||
if present {
|
||||
detail = "responded"
|
||||
} else {
|
||||
detail = fmt.Sprintf("%s gave no reason", c.command)
|
||||
}
|
||||
}
|
||||
return Verdict{Name: c.name, Present: present, Detail: firstLine(detail), How: c.why}
|
||||
}
|
||||
|
||||
// exitZero is the ordinary rule: the command worked, so the capability is there.
|
||||
func exitZero(stdout string, err error) (bool, string) {
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, stdout
|
||||
}
|
||||
|
||||
// systemRunning reads what an init system says about itself rather than how it exited.
|
||||
//
|
||||
// `is-system-running` exits non-zero for every state except `running` — including `degraded`,
|
||||
// which means units failed and the init is emphatically present. Treating that as absent made
|
||||
// a machine running systemd report no service manager.
|
||||
func systemRunning(stdout string, err error) (bool, string) {
|
||||
state := strings.TrimSpace(firstLine(stdout))
|
||||
switch state {
|
||||
case "running", "degraded", "starting", "maintenance", "stopping":
|
||||
return true, state
|
||||
case "":
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return false, "said nothing"
|
||||
default:
|
||||
// `offline` and `unknown` mean it is not managing this machine.
|
||||
return false, state
|
||||
}
|
||||
}
|
||||
|
||||
func firstLine(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
if len(s) > 200 {
|
||||
s = s[:200] + "…"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// privileged reports whether the host can change this machine at all.
|
||||
//
|
||||
// Reported as a capability rather than checked at startup on purpose: a host that cannot act
|
||||
// is still a host that can report, and novox/hq ADR 0036 says what varies between nodes lives
|
||||
// here rather than in the definition of a node.
|
||||
type privileged struct{}
|
||||
|
||||
func (privileged) Name() string { return CapPrivileged }
|
||||
|
||||
func (privileged) Detect(context.Context) Verdict {
|
||||
uid := os.Geteuid()
|
||||
if uid == 0 {
|
||||
return Verdict{
|
||||
Name: CapPrivileged, Present: true,
|
||||
Detail: "effective uid 0",
|
||||
How: "effective uid — the host changes a machine, which needs root",
|
||||
}
|
||||
}
|
||||
return Verdict{
|
||||
Name: CapPrivileged, Present: false,
|
||||
Detail: fmt.Sprintf("effective uid %d, not 0", uid),
|
||||
How: "effective uid — the host changes a machine, which needs root",
|
||||
}
|
||||
}
|
||||
|
||||
// graphicalSession reports whether anything could display a window here.
|
||||
//
|
||||
// Environment rather than a probe, because a display server is reachable through a socket a
|
||||
// detector would have to guess at, and the variables are what an application would use anyway.
|
||||
// Stated so the limit is visible: this detects that a session is ADVERTISED, which is weaker
|
||||
// than the other detectors here.
|
||||
type graphicalSession struct{}
|
||||
|
||||
func (graphicalSession) Name() string { return CapGraphicalSession }
|
||||
|
||||
func (graphicalSession) Detect(context.Context) Verdict {
|
||||
const how = "DISPLAY / WAYLAND_DISPLAY — weaker than the other checks: advertised, not probed"
|
||||
if d := os.Getenv("WAYLAND_DISPLAY"); d != "" {
|
||||
return Verdict{Name: CapGraphicalSession, Present: true, Detail: "wayland: " + d, How: how}
|
||||
}
|
||||
if d := os.Getenv("DISPLAY"); d != "" {
|
||||
return Verdict{Name: CapGraphicalSession, Present: true, Detail: "x11: " + d, How: how}
|
||||
}
|
||||
return Verdict{
|
||||
Name: CapGraphicalSession, Present: false,
|
||||
Detail: "neither DISPLAY nor WAYLAND_DISPLAY is set",
|
||||
How: how,
|
||||
}
|
||||
}
|
||||
|
||||
// Default returns the detectors the host runs when nobody says otherwise.
|
||||
//
|
||||
// Each command is chosen to prove the thing WORKS rather than exists:
|
||||
// - the container runtime is asked for server-side information, which fails when the daemon
|
||||
// is down even though the client is installed — the exact shape of 04-ISSUES/007;
|
||||
// - the service manager is asked whether it is the running init, not whether it is present;
|
||||
// - the firewall is asked to list a ruleset, which needs both the tool and the permission.
|
||||
func Default(runner Runner) []Detector {
|
||||
if runner == nil {
|
||||
runner = ExecRunner
|
||||
}
|
||||
return []Detector{
|
||||
privileged{},
|
||||
graphicalSession{},
|
||||
commandCapability{
|
||||
name: CapContainerRuntime, command: "docker", args: []string{"info", "--format", "{{.ServerVersion}}"},
|
||||
why: "asks the daemon for its version — a running daemon, not an installed client",
|
||||
runner: runner,
|
||||
},
|
||||
commandCapability{
|
||||
name: CapPackageManager, command: "pacman", args: []string{"-Q", "pacman"},
|
||||
why: "queries the package database — a working database, not a binary on disk",
|
||||
runner: runner,
|
||||
},
|
||||
commandCapability{
|
||||
name: CapServiceManager, command: "systemctl", args: []string{"is-system-running"},
|
||||
why: "reads the init's own account of its state — degraded is still running",
|
||||
interpret: systemRunning,
|
||||
runner: runner,
|
||||
},
|
||||
commandCapability{
|
||||
name: CapFirewall, command: "nft", args: []string{"list", "ruleset"},
|
||||
why: "lists the ruleset — needs the tool AND the privilege to use it",
|
||||
runner: runner,
|
||||
},
|
||||
commandCapability{
|
||||
name: CapOverlay, command: "wg", args: []string{"show", "interfaces"},
|
||||
why: "asks the kernel for interfaces — needs the module, not just the tool",
|
||||
runner: runner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// isRoot is the same question `privileged` answers, exposed for tests that must check the
|
||||
// detector against something other than itself.
|
||||
func isRoot() bool { return os.Geteuid() == 0 }
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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 0034).
|
||||
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 0008: 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
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Against the real machine. novox/hq ADR 0034: structure and logic are tested first, behaviour
|
||||
// against a real system alongside, and mocking the boundary is forbidden — a test that fakes
|
||||
// the system under detection asserts that the fake behaves as expected.
|
||||
//
|
||||
// These do not assert WHICH capabilities this machine has; that varies per machine and is the
|
||||
// point of detecting. They assert that detection tells the truth about whatever is here.
|
||||
|
||||
func TestAgainstThisMachine_detectionAgreesWithReality(t *testing.T) {
|
||||
got := Detect(context.Background(), Default(nil), 10*time.Second)
|
||||
|
||||
if got.Architecture == "" || got.Kernel == "" {
|
||||
t.Fatal("the machine did not report its own architecture or kernel")
|
||||
}
|
||||
if len(got.Capabilities) == 0 {
|
||||
t.Fatal("no capability was reported at all")
|
||||
}
|
||||
|
||||
// The claim is checkable independently: a capability reported present must have a command
|
||||
// that is actually on this machine. The reverse is deliberately NOT asserted — a command
|
||||
// being present while the capability is absent is exactly the fault 04-ISSUES/007 records,
|
||||
// and this suite exists partly to let that state be observed rather than assumed away.
|
||||
commands := map[string]string{
|
||||
CapContainerRuntime: "docker",
|
||||
CapPackageManager: "pacman",
|
||||
CapServiceManager: "systemctl",
|
||||
CapFirewall: "nft",
|
||||
CapOverlay: "wg",
|
||||
}
|
||||
for name, command := range commands {
|
||||
if !got.Has(name) {
|
||||
continue
|
||||
}
|
||||
if _, err := exec.LookPath(command); err != nil {
|
||||
t.Errorf("%s reported present, but %q is not on this machine: %v", name, command, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range got.Capabilities {
|
||||
t.Logf(" %-20s present=%-5v %s", v.Name, v.Present, v.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgainstThisMachine_privilegeIsReportedHonestly(t *testing.T) {
|
||||
// The host changes machines, so whether it can is the capability that decides what the
|
||||
// rest of it may attempt. Reporting it wrongly in either direction is worse than not
|
||||
// reporting it: claimed-and-absent means work is accepted and fails, and absent-when-held
|
||||
// means a capable node refuses work.
|
||||
var verdict Verdict
|
||||
for _, v := range Detect(context.Background(), Default(nil), 5*time.Second).Capabilities {
|
||||
if v.Name == CapPrivileged {
|
||||
verdict = v
|
||||
}
|
||||
}
|
||||
if verdict.Name == "" {
|
||||
t.Fatal("privilege was not reported at all")
|
||||
}
|
||||
|
||||
// Checked against the process's own view rather than against the detector's.
|
||||
root := isRoot()
|
||||
if verdict.Present != root {
|
||||
t.Errorf("privilege reported %v; this process is root=%v", verdict.Present, root)
|
||||
}
|
||||
if !strings.Contains(verdict.Detail, "uid") {
|
||||
t.Errorf("privilege detail does not say what it observed: %q", verdict.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgainstThisMachine_detectionIsBounded(t *testing.T) {
|
||||
// Every probe runs a command on a real machine. If any of them can block, the host has a
|
||||
// startup that sometimes never finishes — the least debuggable failure there is.
|
||||
start := time.Now()
|
||||
Detect(context.Background(), Default(nil), 2*time.Second)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
budget := 2 * time.Second * time.Duration(len(Default(nil)))
|
||||
if elapsed > budget {
|
||||
t.Fatalf("detection took %s, past its own %s budget", elapsed, budget)
|
||||
}
|
||||
t.Logf("detected %d capabilities in %s", len(Default(nil)), elapsed)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The decision each test defends is named in the test, per novox/hq ADR 0034. These cover
|
||||
// structure and logic; profile_system_test.go covers the same detectors against the real
|
||||
// machine, because a test that fakes the system under detection asserts only that the fake
|
||||
// behaves as expected.
|
||||
|
||||
func TestIssue007_installedIsNotUsable(t *testing.T) {
|
||||
// 04-ISSUES/007 — an installed package is not a capability. The client was present and the
|
||||
// daemon was down, and the node took work it could not do. A detector whose command fails
|
||||
// must report ABSENT, however installed the thing looks.
|
||||
failing := func(context.Context, string, ...string) (string, error) {
|
||||
return "", errors.New("docker exited 1: Cannot connect to the Docker daemon")
|
||||
}
|
||||
got := Detect(context.Background(), Default(failing), time.Second)
|
||||
|
||||
if got.Has(CapContainerRuntime) {
|
||||
t.Fatal("a runtime whose daemon refuses the connection was reported present")
|
||||
}
|
||||
for _, v := range got.Capabilities {
|
||||
if v.Name != CapContainerRuntime {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(v.Detail, "Cannot connect") {
|
||||
t.Fatalf("the reason was lost; detail was %q", v.Detail)
|
||||
}
|
||||
if v.How == "" {
|
||||
t.Fatal("a verdict with no stated method cannot be checked when it is wrong")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryVerdictSaysHowItKnows(t *testing.T) {
|
||||
// A capability reported absent with no reason is the fault in a new place: something
|
||||
// nobody can act on. Both outcomes must carry a method.
|
||||
for _, runner := range []Runner{
|
||||
func(context.Context, string, ...string) (string, error) { return "ok", nil },
|
||||
func(context.Context, string, ...string) (string, error) { return "", errors.New("nope") },
|
||||
} {
|
||||
for _, v := range Detect(context.Background(), Default(runner), time.Second).Capabilities {
|
||||
if strings.TrimSpace(v.How) == "" {
|
||||
t.Errorf("%s reports present=%v with no stated method", v.Name, v.Present)
|
||||
}
|
||||
if strings.TrimSpace(v.Detail) == "" {
|
||||
t.Errorf("%s reports present=%v with no detail", v.Name, v.Present)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectionSurvivesAFailingProbe(t *testing.T) {
|
||||
// Deliberately NOT ADR 0008. That rule governs APPLYING state, where a failed step means
|
||||
// the machine is not what was asked for. A failed probe is a finding, and aborting would
|
||||
// replace one legible absence with total ignorance of the rest.
|
||||
only := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if name == "pacman" {
|
||||
return "pacman 7.0.0", nil
|
||||
}
|
||||
return "", errors.New("not here")
|
||||
}
|
||||
got := Detect(context.Background(), Default(only), time.Second)
|
||||
|
||||
if len(got.Capabilities) != len(Default(nil)) {
|
||||
t.Fatalf("expected every detector to report, got %d of %d",
|
||||
len(got.Capabilities), len(Default(nil)))
|
||||
}
|
||||
if !got.Has(CapPackageManager) {
|
||||
t.Error("the one working capability was lost among the failures")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAProbeCannotHangTheHost(t *testing.T) {
|
||||
// A host that blocks forever on a wedged command reports nothing at all, which is worse
|
||||
// than reporting the capability absent.
|
||||
blocking := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
<-ctx.Done()
|
||||
return "", ctx.Err()
|
||||
}
|
||||
done := make(chan Profile, 1)
|
||||
go func() { done <- Detect(context.Background(), Default(blocking), 50*time.Millisecond) }()
|
||||
|
||||
select {
|
||||
case got := <-done:
|
||||
if got.Has(CapFirewall) {
|
||||
t.Error("a probe that never answered was reported present")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("detection did not return — a wedged probe hung the host")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownCapabilityIsAbsentNotAnError(t *testing.T) {
|
||||
// Asking about a capability nothing detects is a question with a true answer.
|
||||
p := Detect(context.Background(), nil, time.Second)
|
||||
if p.Has("something-nothing-detects") {
|
||||
t.Error("an undetected capability reported present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingListsWhatCannotBeAskedOf(t *testing.T) {
|
||||
// What a node CANNOT do is the half that decides whether work may be placed on it.
|
||||
none := func(context.Context, string, ...string) (string, error) { return "", errors.New("no") }
|
||||
missing := Detect(context.Background(), Default(none), time.Second).Missing()
|
||||
|
||||
if len(missing) == 0 {
|
||||
t.Fatal("everything failed and nothing was reported missing")
|
||||
}
|
||||
for i := 1; i < len(missing); i++ {
|
||||
if missing[i-1] > missing[i] {
|
||||
t.Fatalf("Missing() is not ordered: %v", missing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheProfileIsOrdered(t *testing.T) {
|
||||
// Two runs on an unchanged machine produce the same profile. Without this, comparing what
|
||||
// a node was against what it is would report differences that are only ordering.
|
||||
ok := func(context.Context, string, ...string) (string, error) { return "fine", nil }
|
||||
first := Detect(context.Background(), Default(ok), time.Second)
|
||||
second := Detect(context.Background(), Default(ok), time.Second)
|
||||
|
||||
if len(first.Capabilities) != len(second.Capabilities) {
|
||||
t.Fatal("two runs disagreed on how many capabilities exist")
|
||||
}
|
||||
for i := range first.Capabilities {
|
||||
if first.Capabilities[i].Name != second.Capabilities[i].Name {
|
||||
t.Fatalf("ordering is not stable at %d: %q then %q",
|
||||
i, first.Capabilities[i].Name, second.Capabilities[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestADegradedInitIsStillAnInit(t *testing.T) {
|
||||
// Found by running against a real machine, not by reasoning. `systemctl is-system-running`
|
||||
// exits non-zero for every state except `running` — including `degraded`, which means some
|
||||
// units failed and the init is emphatically present. Reading the exit code declared no
|
||||
// service manager on a machine whose init it was.
|
||||
//
|
||||
// This is 04-ISSUES/007 in the mirror: 007 is installed-but-broken reported present; this
|
||||
// is working-but-imperfect reported absent. Both make the mesh place work wrongly.
|
||||
degraded := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if name == "systemctl" {
|
||||
return "degraded\n", errors.New("systemctl exited 1: ")
|
||||
}
|
||||
return "", errors.New("not here")
|
||||
}
|
||||
got := Detect(context.Background(), Default(degraded), time.Second)
|
||||
|
||||
if !got.Has(CapServiceManager) {
|
||||
t.Fatal("a degraded init was reported absent — the machine would refuse work it can do")
|
||||
}
|
||||
for _, v := range got.Capabilities {
|
||||
if v.Name == CapServiceManager && v.Detail != "degraded" {
|
||||
t.Errorf("the state was lost; detail was %q", v.Detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnInitThatIsNotManagingThisMachineIsAbsent(t *testing.T) {
|
||||
// The other side of the same rule. `offline` means it is installed and not in charge —
|
||||
// which must not be read as present just because a state came back.
|
||||
for _, state := range []string{"offline", "unknown"} {
|
||||
runner := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if name == "systemctl" {
|
||||
return state + "\n", errors.New("systemctl exited 1: ")
|
||||
}
|
||||
return "", errors.New("not here")
|
||||
}
|
||||
if Detect(context.Background(), Default(runner), time.Second).Has(CapServiceManager) {
|
||||
t.Errorf("state %q was reported as a working service manager", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFailureWithNoMessageStillCarriesAReason(t *testing.T) {
|
||||
// systemctl fails with empty stderr, which produced a verdict reading "exited 1:" — absent,
|
||||
// with nothing anyone could act on.
|
||||
silent := func(context.Context, string, ...string) (string, error) { return "", errors.New("") }
|
||||
for _, v := range Detect(context.Background(), Default(silent), time.Second).Capabilities {
|
||||
if v.Present {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(v.Detail) == "" || strings.HasSuffix(strings.TrimSpace(v.Detail), ":") {
|
||||
t.Errorf("%s is absent for no stated reason: %q", v.Name, v.Detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user