Files
mesh-catalog/modules/zfs/cmd/zfs-tools/manifest_test.go
T
jochen 7c8497225d
mesh/merge-gate pass: builds new: modules/zfs, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestASnapshotIsAnsweredFromTheListAfterwardsAndRecorded (0.00s)
mesh/delivery superseded: a newer head of the same pull request
Add the zfs module, so the mesh can see the home server's pools and snapshot a dataset
The home server's pool holds the operator's media library, and no mesh tool could say whether it is
healthy, how full it is, when it was last scrubbed or what snapshots exist; the only way was a login.
This module reads all of it with fixed zfs and zpool commands (list, get, status, iostat, events,
history) and has one act, zfs_snapshot, which only adds a snapshot: refused for an unknown dataset, a
name outside [A-Za-z0-9._-], an existing snapshot or no why; run as sudo -n zfs snapshot -- ds@name;
answered from zfs list afterwards, with any other snapshot that came or went meanwhile; recorded as the
event snapshot.taken. No destroy, rollback, set or send exists, and a test holds every command to its
verb.

Health (ADR 0240): zfs_health on the watch process, unhealthy for a pool not ONLINE, device or data
errors, a pool past 90 % full or no ZFS at all; past 80 % and a scrub older than 35 days are warnings.
Beside it, the event follower (zpool events -f, as root) keeps ZFS's errors in its journal past a
reboot and is judged by its unit, the check the module does not run itself. No seat (ADR 0121), no
package: ZFS is the machine's.
2026-10-09 00:11:31 +02:00

154 lines
5.6 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
)
type manifest struct {
Tools []string `json:"tools"`
Claims []any `json:"claims"`
Emits []string `json:"emits"`
Replaces map[string][]string `json:"replaces"`
Resources []map[string]any `json:"resources"`
}
func readManifest(t *testing.T) (manifest, string) {
t.Helper()
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
return m, string(raw)
}
// The manifest lists exactly the tools the bundle serves; every one but zfs_snapshot says it only reads,
// and zfs_snapshot says it acts. Each says what it replaces (novox/hq ADR 0245), but the health check,
// which replaces nothing. No seat: a new mesh or node seat is the controller's to define (ADR 0121).
func TestTheManifestNamesTheToolsTheBundleServesAndWhatEachReplaces(t *testing.T) {
m, raw := readManifest(t)
var served []string
for _, tl := range Tools(newFake(t).machine()) {
served = append(served, tl.Name)
want := "(r)"
if tl.Name == "zfs_snapshot" {
want = "(a)"
}
if !strings.HasSuffix(tl.Description, want) {
t.Errorf("%s does not end with %s", tl.Name, want)
}
if tl.Name != "zfs_health" && len(m.Replaces[tl.Name]) == 0 {
t.Errorf("%s says nothing of what it replaces", tl.Name)
}
for _, c := range m.Replaces[tl.Name] {
if !strings.Contains(tl.Description, c) {
t.Errorf("%s replaces %q in the manifest and not in its description", tl.Name, c)
}
}
}
sort.Strings(served)
listed := append([]string(nil), m.Tools...)
sort.Strings(listed)
if strings.Join(served, ",") != strings.Join(listed, ",") {
t.Fatalf("served %v, listed %v", served, listed)
}
if _, ok := m.Replaces["zfs_health"]; ok {
t.Errorf("the health check replaces no command")
}
if len(m.Claims) != 0 {
t.Errorf("the module claims no seat: %v", m.Claims)
}
if strings.Join(m.Emits, ",") != SnapshotEvent {
t.Errorf("emits %v", m.Emits)
}
if hit := regexp.MustCompile(`\b(g14|novox|ace|shanks|jochen)\b|/home/`).FindString(strings.ReplaceAll(raw, "novox/hq", "")); hit != "" {
t.Errorf("the manifest says %q", hit)
}
}
// ADR 0240: the health is zfs_health on the watch, beside the event follower's unit, which the module does
// not judge itself; the module installs nothing, so it never puts ZFS on a machine.
func TestTheHealthIsTheWatchsToolCheckBesideTheFollowersUnit(t *testing.T) {
m, raw := readManifest(t)
byID := map[string]map[string]any{}
for _, r := range m.Resources {
byID[r["id"].(string)] = r
}
if len(byID) != 2 {
t.Fatalf("resources: %v", m.Resources)
}
for id, want := range map[string][2]string{"watch": {"tool", "watch"}, "events": {"unit", "events"}} {
r := byID[id]
h, _ := r["health"].(map[string]any)
run, _ := r["run"].([]any)
if r["type"] != "process" || h["kind"] != want[0] || len(run) != 2 || run[1] != want[1] {
t.Errorf("%s: %v", id, r)
}
}
if h := byID["watch"]["health"].(map[string]any); h["tool"] != "zfs_health" {
t.Errorf("the watch is judged by zfs_health: %v", h)
}
if strings.Contains(raw, `"package"`) {
t.Errorf("the module installs no package: ZFS is the machine's, and its kernel module is built against its kernel")
}
}
// No destroy, rollback, set, send or any other change but a snapshot. Every command the bundle runs goes
// through ask, AsRoot or Escalated with an argument list from one of the functions below, each of which is
// held to its verb here; a call site that builds its own list fails this test. A new verb needs a decision
// first, and then a line here.
func TestTheBundleRunsOnlyReadingVerbsAndSnapshot(t *testing.T) {
allowed := map[string]string{"DatasetsArgv": "list", "SnapshotsArgv": "list", "PoolsArgv": "list", "StatusArgv": "status",
"IOArgv": "iostat", "EventsArgv": "events", "HistoryArgv": "history", "PropertiesArgv": "get", "SnapshotArgv": "snapshot", "FollowArgv": "events"}
verbs := map[string][]string{"DatasetsArgv": DatasetsArgv(), "SnapshotsArgv": SnapshotsArgv(), "PoolsArgv": PoolsArgv(),
"StatusArgv": StatusArgv(), "IOArgv": IOArgv(3), "EventsArgv": EventsArgv(), "HistoryArgv": HistoryArgv("tank"),
"PropertiesArgv": PropertiesArgv("tank"), "SnapshotArgv": SnapshotArgv("tank", "x", true), "FollowArgv": FollowArgv()}
for fn, argv := range verbs {
if argv[0] != allowed[fn] {
t.Errorf("%s runs %q, not %q", fn, argv[0], allowed[fn])
}
}
call := regexp.MustCompile(`\.(ask|AsRoot|Escalated|Command|CommandContext)\(`)
site := regexp.MustCompile(`\.(?:ask|AsRoot|Escalated|CommandContext)\((?:\w+, )?"(zfs|zpool)", (\w+)\(`)
files, _ := filepath.Glob("*.go")
sites := 0
for _, file := range files {
if strings.HasSuffix(file, "_test.go") {
continue
}
b, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
for i, l := range strings.Split(string(b), "\n") {
if !call.MatchString(l) || strings.HasPrefix(strings.TrimSpace(l), "func ") || strings.HasPrefix(strings.TrimSpace(l), "//") {
continue
}
if strings.Contains(l, "m.Escalated(name, args...)") || (file == "machine.go" && strings.Contains(l, "exec.CommandContext(ctx, name, args...)")) {
continue // AsRoot's and the runner's own bodies
}
m := site.FindStringSubmatch(l)
if m == nil {
t.Errorf("%s:%d runs a command not built by an argument function: %s", file, i+1, strings.TrimSpace(l))
continue
}
if _, ok := allowed[m[2]]; !ok {
t.Errorf("%s:%d runs %s's list, which is not held here", file, i+1, m[2])
}
sites++
}
}
if sites < 10 {
t.Errorf("found %d call sites; the test no longer sees them", sites)
}
}