Files
mesh-catalog/modules/docker/cmd/docker-tools/docker_test.go
T
jochen 73bb597c16
mesh/merge-gate pass: builds docker, gitea, lab, nftables, slack, systemd → ace, g14, novox, shanks; no bus step; 4 wait(s) for a person; every machine com…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Hold what the tools say to the glossary's retired words (hq ADR 0244)
An agent meets the mesh's words most often in tool descriptions, and
nothing compared them with the glossary: several still said "the host"
for the node-engine and the forge's pull request comment was headed
"Change plan", a word retired twice over. retired-words is the copy of
the words the glossary retires for the tools, and checks/words fails the
repository check when any string a module's code can show, or any
manifest description, uses one. Those found are reworded here.
2026-10-07 20:02:09 +02:00

365 lines
15 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"io/fs"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
)
type call struct {
name string
args []string
}
// fake answers each command by the first rule whose prefix matches "name arg arg…".
type fake struct {
mu sync.Mutex
rules []rule
calls []call
}
type rule struct {
prefix string
ran Ran
}
func (f *fake) on(prefix string, r Ran) *fake { f.rules = append(f.rules, rule{prefix, r}); return f }
func (f *fake) run(_ context.Context, name string, args ...string) Ran {
f.mu.Lock()
defer f.mu.Unlock()
f.calls = append(f.calls, call{name, args})
line := strings.Join(append([]string{name}, args...), " ")
for _, r := range f.rules {
if strings.HasPrefix(line, r.prefix) {
return r.ran
}
}
return Ran{Status: 1, Stderr: "unexpected: " + line}
}
func (f *fake) ran(prefix string) bool {
for _, c := range f.calls {
if strings.HasPrefix(strings.Join(append([]string{c.name}, c.args...), " "), prefix) {
return true
}
}
return false
}
func client(f *fake, uid int) *Client {
return &Client{Run: f.run, UID: uid, ReadFile: func(string) ([]byte, error) { return nil, fs.ErrNotExist },
Now: func() time.Time { return time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC) }}
}
const held = `{"Id":"aaaaaaaaaaaaaaaa","Name":"/mesh-web","Created":"2026-10-01T00:00:00Z","Image":"sha256:img1",
"Config":{"Image":"web:1","Labels":{"mesh-host.id":"hello-web.server","mesh-host.spec":"x"},"Env":["PASSWORD=hunter2","PATH=/bin"]},
"State":{"Status":"running","Running":true,"StartedAt":"2026-10-01T00:00:01Z","FinishedAt":"0001-01-01T00:00:00Z","Health":{"Status":"healthy"}},
"HostConfig":{"RestartPolicy":{"Name":"unless-stopped"},"NetworkMode":"bridge"},
"NetworkSettings":{"Ports":{"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}},
"Mounts":[{"Type":"volume","Name":"webdata","Destination":"/data","RW":true}]}`
const stray = `{"Id":"bbbbbbbbbbbbbbbb","Name":"/dev-db","Created":"2026-09-01T00:00:00Z","Image":"sha256:img2",
"Config":{"Image":"postgres:16","Labels":{"com.docker.compose.project":"dev","com.docker.compose.project.working_dir":"/home/op/dev"}},
"State":{"Status":"exited","ExitCode":1,"FinishedAt":"2026-09-02T00:00:00Z"},
"HostConfig":{"RestartPolicy":{"Name":"no"}},"NetworkSettings":{"Ports":{}},
"Mounts":[{"Type":"volume","Name":"dbdata","Destination":"/var/lib/postgresql/data","RW":true}]}`
func machine() *fake {
return (&fake{}).
on("docker ps --all --quiet --no-trunc", Ran{Stdout: "aaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbb\n"}).
on("docker container inspect aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbb", Ran{Stdout: "[" + held + "," + stray + "]"}).
on("docker container inspect mesh-web", Ran{Stdout: "[" + held + "]"}).
on("docker container inspect dev-db", Ran{Stdout: "[" + stray + "]"})
}
func TestARefusedSocketIsAskedAgainThroughSudoWithoutAPromptUnlessThisIsRoot(t *testing.T) {
denied := Ran{Status: 1, Stderr: "permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get ...: dial unix /var/run/docker.sock: connect: permission denied\n"}
f := (&fake{}).on("docker ", denied).on("sudo -n docker info", Ran{Stdout: "{}"})
if _, err := client(f, 1000).docker(context.Background(), "info", "--format", "{{json .}}"); err != nil {
t.Fatal(err)
}
if !f.ran("sudo -n docker info --format") {
t.Fatalf("not escalated: %+v", f.calls)
}
f = (&fake{}).on("docker ", denied)
if _, err := client(f, 0).docker(context.Background(), "info"); err == nil || f.ran("sudo") {
t.Fatalf("root escalated or answered: %v %+v", err, f.calls)
}
}
func TestFailuresAreNamedByHowTheyFailed(t *testing.T) {
denied := Ran{Status: 1, Stderr: "permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock\n"}
cases := map[string]*fake{
"may not escalate without a prompt": (&fake{}).on("docker ", denied).on("sudo ", Ran{Status: 1, Stderr: "sudo: a password is required\n"}),
"sudo is not installed": (&fake{}).on("docker ", denied).on("sudo ", Ran{Status: 127, Err: "ENOENT"}),
"docker is not installed": (&fake{}).on("docker ", Ran{Status: 127, Err: "ENOENT"}),
"daemon is not answering": (&fake{}).on("docker ", Ran{Status: 1, Stderr: "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?\n"}),
"did not answer: no answer within": (&fake{}).on("docker ", Ran{Status: 124, Err: "no answer within 20 s"}),
"docker info failed (3): boom": (&fake{}).on("docker ", Ran{Status: 3, Stderr: "boom\n"}),
}
for want, f := range cases {
_, err := client(f, 1000).docker(context.Background(), "info")
if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("want %q, got %v", want, err)
}
}
}
func TestANameIsNeverAnOption(t *testing.T) {
for _, bad := range []string{"--help", "-v", "", "a b", "x;y"} {
if _, err := Ref(bad); err == nil {
t.Errorf("%q accepted", bad)
}
}
for _, good := range []string{"mesh-web", "aaaaaaaaaaaa", "registry.mesh.internal:5100/x@sha256:abc", "dev_db.1"} {
if _, err := Ref(good); err != nil {
t.Errorf("%q refused: %v", good, err)
}
}
f := machine()
for _, verb := range []string{"start", "stop", "restart"} {
if _, err := client(f, 1000).Act(context.Background(), verb, "--rm"); err == nil {
t.Errorf("%s took an option", verb)
}
}
if len(f.calls) != 0 {
t.Fatalf("docker was called: %+v", f.calls)
}
}
func TestEveryContainerIsListedAndTheMeshsAreMarked(t *testing.T) {
c := client(machine(), 1000)
all, err := c.Containers(context.Background(), "", "", "")
if err != nil || len(all) != 2 {
t.Fatalf("%v %+v", err, all)
}
web, db := all[1], all[0]
if !web.MeshHeld || web.HeldBy != "hello-web.server" || web.Module != "hello-web" || web.Health != "healthy" {
t.Errorf("held: %+v", web)
}
if !reflect.DeepEqual(web.Ports, []string{"0.0.0.0:8080->80/tcp"}) || web.Mounts[0].Name != "webdata" {
t.Errorf("ports/mounts: %+v", web)
}
if db.MeshHeld || db.Compose != "dev" || db.ComposeDir != "/home/op/dev" || db.FinishedAt == "" {
t.Errorf("stray: %+v", db)
}
mesh, _ := c.Containers(context.Background(), "mesh", "", "")
other, _ := c.Containers(context.Background(), "other", "", "")
if len(mesh) != 1 || mesh[0].Name != "mesh-web" || len(other) != 1 || other[0].Name != "dev-db" {
t.Errorf("held filter: %+v / %+v", mesh, other)
}
if _, err := c.Containers(context.Background(), "mine", "", ""); err == nil {
t.Error("an unknown held filter was accepted")
}
}
func TestNoContainersIsAnEmptyListAndAFailureIsAnError(t *testing.T) {
got, err := client((&fake{}).on("docker ps", Ran{}), 1000).Containers(context.Background(), "", "", "")
if err != nil || got == nil || len(got) != 0 {
t.Fatalf("%v %v", got, err)
}
if _, err := client((&fake{}).on("docker ps", Ran{Status: 1, Stderr: "Cannot connect to the Docker daemon\n"}), 1000).Containers(context.Background(), "", "", ""); err == nil {
t.Fatal("a daemon that does not answer read as no containers")
}
}
func TestInspectLeavesTheEnvironmentsValuesOut(t *testing.T) {
got, err := client(machine(), 1000).Inspect(context.Background(), "mesh-web")
if err != nil {
t.Fatal(err)
}
b, _ := json.Marshal(got)
if strings.Contains(string(b), "hunter2") || !strings.Contains(string(b), `"PASSWORD"`) || got["mesh_held"] != true {
t.Fatalf("%s", b)
}
}
func TestActingOnAMeshContainerSaysTheHostRestoresIt(t *testing.T) {
f := machine().on("docker stop", Ran{}).on("docker start", Ran{})
got, err := client(f, 1000).Act(context.Background(), "stop", "mesh-web")
if err != nil {
t.Fatal(err)
}
if !f.ran("docker stop --time 10 mesh-web") || got["mesh_held"] != true || !strings.Contains(got["note"].(string), "node-engine restores") {
t.Fatalf("%v %+v", got, f.calls)
}
got, _ = client(f, 1000).Act(context.Background(), "start", "dev-db")
if _, noted := got["note"]; noted || got["mesh_held"] != false {
t.Fatalf("a stray was noted: %v", got)
}
}
func TestPruneIsADryRunByDefaultAndNeverTouchesAVolumeOrAMeshContainer(t *testing.T) {
f := machine().
on("docker image ls --no-trunc --filter dangling=true", Ran{Stdout: `{"ID":"sha256:dead","Size":"1.5GB"}` + "\n"}).
on("docker system df --format", Ran{Stdout: `{"Type":"Build Cache","TotalCount":"3","Size":"2GB","Reclaimable":"1GB"}` + "\n"}).
on("docker image prune", Ran{Stdout: "Deleted Images:\nx\n\nTotal reclaimed space: 1.5GB\n"}).
on("docker builder prune", Ran{Stdout: "Total:\t1GB\n"}).
on("docker container rm", Ran{})
c := client(f, 1000)
got, err := c.Prune(context.Background(), PruneAsk{Images: true, BuildCache: true, Containers: true, DryRun: true})
if err != nil {
t.Fatal(err)
}
if f.ran("docker image prune") || f.ran("docker builder prune") || f.ran("docker container rm") {
t.Fatalf("a dry run removed something: %+v", f.calls)
}
if got["images"].(map[string]any)["dangling"] != 1 || !reflect.DeepEqual(got["containers"].(map[string]any)["stopped_not_held"], []string{"dev-db"}) {
t.Fatalf("%v", got)
}
got, err = c.Prune(context.Background(), PruneAsk{Images: true, BuildCache: true, Containers: true, OlderThanH: 24})
if err != nil {
t.Fatal(err)
}
if !f.ran("docker image prune --force --filter until=24h") || !f.ran("docker builder prune --force --filter until=24h") || !f.ran("docker container rm dev-db") {
t.Fatalf("not pruned: %+v", f.calls)
}
for _, c := range f.calls {
line := strings.Join(c.args, " ")
if strings.Contains(line, "volume") || strings.Contains(line, "mesh-web") && c.args[0] != "container" || strings.Contains(line, "--volumes") || strings.Contains(line, "--all") && c.args[0] != "ps" {
t.Errorf("prune reached too far: %s", line)
}
}
if got["images"].(map[string]any)["reclaimed"] != "1.5GB" || got["build_cache"].(map[string]any)["reclaimed"] != "1GB" {
t.Errorf("reclaimed: %v", got)
}
}
func TestLogsMergeBothStreamsInOrderAndKeepTheTail(t *testing.T) {
f := (&fake{}).on("docker logs", Ran{Stdout: "2026-10-04T10:00:01Z out one\n2026-10-04T10:00:03Z out two\n", Stderr: "2026-10-04T10:00:02Z err one\n"})
got, err := client(f, 1000).Logs(context.Background(), "web", 2, "30m")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got["lines"], []string{"2026-10-04T10:00:02Z err one", "2026-10-04T10:00:03Z out two"}) {
t.Fatalf("%v", got["lines"])
}
if !f.ran("docker logs --timestamps --tail 2 --since 30m web") {
t.Fatalf("%+v", f.calls)
}
if _, err := client(f, 1000).Logs(context.Background(), "web", 2, "--follow"); err == nil {
t.Fatal("since took an option")
}
f = (&fake{}).on("docker logs", Ran{Status: 1, Stderr: "Error response from daemon: No such container: nope\n"})
if _, err := client(f, 1000).Logs(context.Background(), "nope", 2, ""); err == nil {
t.Fatal("a missing container read as no lines")
}
}
func TestSizesAreReadAsDockerPrintsThem(t *testing.T) {
for in, want := range map[string]int64{"0B": 0, "55.63GB": 55630000000, "33.2MiB": 34812723, "1.5kB": 1500, "12MB (34%)": 12000000, "N/A": -1} {
if got := Bytes(in); got != want {
t.Errorf("%s: %d, want %d", in, got, want)
}
}
}
func TestDaemonConfigSaysWhatTheDaemonHasNotTakenYet(t *testing.T) {
f := (&fake{}).on("docker info", Ran{Stdout: `{"ServerVersion":"29.8.2","LiveRestoreEnabled":false,"LoggingDriver":"json-file","RegistryConfig":{"IndexConfigs":{"docker.io":{"Secure":true},"registry.mesh.internal:5100":{"Secure":false}}}}`})
c := client(f, 1000)
c.ReadFile = func(string) ([]byte, error) {
return []byte(`{"live-restore": true, "dns": ["10.0.0.1"], "log-driver": "local"}`), nil
}
got, err := c.DaemonConfig(context.Background())
if err != nil {
t.Fatal(err)
}
pending := strings.Join(got["pending"].([]string), "\n")
if !strings.Contains(pending, "live-restore is true in the file and false") || !strings.Contains(pending, "log-driver is local") {
t.Errorf("pending: %s", pending)
}
if !reflect.DeepEqual(got["daemon"].(map[string]any)["InsecureRegistries"], []string{"registry.mesh.internal:5100"}) {
t.Errorf("registries: %v", got["daemon"])
}
if !reflect.DeepEqual(got["read_only_at_start"], []string{"dns", "log-driver"}) {
t.Errorf("start-only: %v", got["read_only_at_start"])
}
c.ReadFile = func(string) ([]byte, error) { return nil, os.ErrNotExist }
got, _ = c.DaemonConfig(context.Background())
if !strings.HasPrefix(got["file_state"].(string), "absent") {
t.Errorf("absent: %v", got["file_state"])
}
c.ReadFile = func(string) ([]byte, error) { return nil, errors.New("permission denied") }
got, _ = c.DaemonConfig(context.Background())
if !strings.HasPrefix(got["file_state"].(string), "unreadable") {
t.Errorf("unreadable: %v", got["file_state"])
}
}
func TestEventsAreABoundedWindowWithoutExecNoise(t *testing.T) {
out := `{"Type":"container","Action":"exec_start: pg_isready","Actor":{"ID":"aaaaaaaaaaaaaaaa","Attributes":{"name":"db"}},"timeNano":1}
{"Type":"container","Action":"die","Actor":{"ID":"aaaaaaaaaaaaaaaa","Attributes":{"name":"web","mesh-host.id":"hello-web.server","exitCode":"137"}},"timeNano":2}
`
f := (&fake{}).on("docker events", Ran{Stdout: out})
got, err := client(f, 1000).Events(context.Background(), 30, "container", 10, false)
if err != nil {
t.Fatal(err)
}
evs := got["events"].([]map[string]any)
if len(evs) != 1 || evs[0]["action"] != "die" || evs[0]["mesh_held"] != true || evs[0]["exit_code"] != "137" {
t.Fatalf("%v", evs)
}
if !f.ran("docker events --since 30m --until 0s --format {{json .}} --filter type=container") {
t.Fatalf("%+v", f.calls)
}
if _, err := client(f, 1000).Events(context.Background(), 30, "secret", 10, false); err == nil {
t.Fatal("an unknown type was accepted")
}
}
func TestVolumesSayWhoMountsThemAndWhetherTheMeshDoes(t *testing.T) {
f := machine().
on("docker volume ls --quiet", Ran{Stdout: "webdata\ndbdata\nloose\n"}).
on("docker volume inspect", Ran{Stdout: `[{"Name":"webdata","Driver":"local"},{"Name":"dbdata","Driver":"local"},{"Name":"loose","Driver":"local","Labels":{"com.docker.volume.anonymous":""}}]`})
got, err := client(f, 1000).Volumes(context.Background(), false, false)
if err != nil {
t.Fatal(err)
}
vols := got["volumes"].([]map[string]any)
if vols[0]["mesh_held"] != true || vols[1]["mesh_held"] != false || len(vols[2]["mounted_by"].([]map[string]any)) != 0 || vols[2]["anonymous"] != true {
t.Fatalf("%v", vols)
}
got, _ = client(f, 1000).Volumes(context.Background(), true, false)
if got["count"] != 1 {
t.Fatalf("unmounted: %v", got)
}
}
func TestImagesNameTheirUsers(t *testing.T) {
f := machine().on("docker image ls", Ran{Stdout: `{"ID":"sha256:img1","Repository":"web","Tag":"1","Size":"100MB"}
{"ID":"sha256:img3","Repository":"<none>","Tag":"<none>","Size":"2GB"}
`})
got, err := client(f, 1000).Images(context.Background(), "", "", 10)
if err != nil {
t.Fatal(err)
}
imgs := got["images"].([]Image)
if imgs[0].ID != "sha256:img3" || !imgs[0].Dangling || imgs[1].UsedBy[0] != "mesh-web" || !imgs[1].MeshUsed {
t.Fatalf("%+v", imgs)
}
got, _ = client(f, 1000).Images(context.Background(), "unused", "", 10)
if got["count"] != 1 {
t.Fatalf("unused: %v", got)
}
}
func TestProblemsNameWhyAndUnlabelledIsTheCleanupList(t *testing.T) {
c := client(machine(), 1000)
p, err := c.Problems(context.Background())
if err != nil || len(p) != 1 || p[0]["name"] != "dev-db" || p[0]["why"].([]string)[0] != "exited 1" {
t.Fatalf("%v %v", p, err)
}
u, err := c.Unlabelled(context.Background())
if err != nil || u["count"] != 1 {
t.Fatalf("%v %v", u, err)
}
}