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,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