v3.3.2 9697f9d

runtime_prune_test.go RawHistoryFolder

1package docker
2
3import (
4	"context"
5	"net/http"
6	"net/http/httptest"
7	"strings"
8	"testing"
9
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12
13	"github.com/bnema/gordon/internal/domain"
14)
15
16func TestRuntime_InventoryRuntime(t *testing.T) {
17	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18		w.Header().Set("Content-Type", "application/json")
19		switch r.URL.Path {
20		case "/v1.41/containers/json":
21			assert.Equal(t, "1", r.URL.Query().Get("all"))
22			_, _ = w.Write([]byte(`[
23				{"Id":"c-running","Names":["/shop-web-1"],"Image":"registry.example/shop:1","ImageID":"sha256:aaa","State":"running",
24				 "Mounts":[{"Type":"volume","Name":"gordon-shop--web--vol--data"}]},
25				{"Id":"c-stopped","Names":["/shop-web-2"],"Image":"registry.example/shop:2","ImageID":"sha256:bbb","State":"exited","Mounts":[]},
26				{"Id":"c-opaque","Names":["/foreign"],"Image":"","ImageID":"","State":"exited","Mounts":[]}
27			]`))
28		case "/v1.41/images/json":
29			assert.Equal(t, "1", r.URL.Query().Get("all"))
30			_, _ = w.Write([]byte(`[
31				{"Id":"sha256:aaa","RepoTags":["registry.example/shop:1","registry.example/shop:latest"],
32				 "RepoDigests":["registry.example/shop@sha256:1111"],"Labels":{"gordon.app":"shop"},"Size":10,"Created":1700000000},
33				{"Id":"sha256:bbb","RepoTags":["<none>:<none>"],"RepoDigests":[],"Labels":{},"Size":20,"Created":1700000000}
34			]`))
35		case "/v1.41/volumes":
36			_, _ = w.Write([]byte(`{"Volumes":[
37				{"Name":"gordon-shop--web--vol--data","Driver":"local","Mountpoint":"/var/lib/docker/volumes/x/_data","CreatedAt":"2026-01-01T00:00:00Z","Labels":{"gordon.managed":"true","gordon.app":"shop"}},
38				{"Name":"pgdata","Driver":"local","Mountpoint":"/var/lib/docker/volumes/pgdata/_data","CreatedAt":"not-a-time","Labels":{}}
39			]}`))
40		default:
41			t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
42			w.WriteHeader(http.StatusNotFound)
43		}
44	}))
45	defer server.Close()
46
47	rt := newRuntimeForHTTPServer(t, server)
48	inventory, err := rt.InventoryRuntime(context.Background())
49	require.NoError(t, err)
50	require.True(t, inventory.Complete(), "inventory gaps: %+v", inventory.Gaps)
51
52	require.Len(t, inventory.Containers, 3)
53	byContainer := map[string]domain.RuntimeContainerUse{}
54	for _, use := range inventory.Containers {
55		byContainer[use.ContainerID] = use
56	}
57	assert.True(t, byContainer["c-running"].Running)
58	assert.False(t, byContainer["c-stopped"].Running)
59	assert.Equal(t, "sha256:bbb", byContainer["c-stopped"].ImageID)
60	assert.True(t, byContainer["c-opaque"].ImageUnknown, "unresolvable container image must be flagged")
61
62	require.Len(t, inventory.Images, 2)
63	byImage := map[string]domain.RuntimeImage{}
64	for _, image := range inventory.Images {
65		byImage[image.ID] = image
66	}
67	// One image ID may carry several repo tags and repo digests.
68	assert.Equal(t, []string{"registry.example/shop:1", "registry.example/shop:latest"}, byImage["sha256:aaa"].RepoTags)
69	assert.Equal(t, []string{"registry.example/shop@sha256:1111"}, byImage["sha256:aaa"].RepoDigests)
70	assert.Equal(t, "<none>:<none>", byImage["sha256:bbb"].RepoTags[0])
71
72	require.Len(t, inventory.Volumes, 2)
73	byVolume := map[string]*domain.VolumeInfo{}
74	for _, volume := range inventory.Volumes {
75		byVolume[volume.Name] = volume
76	}
77	assert.True(t, byVolume["gordon-shop--web--vol--data"].InUse)
78	assert.Equal(t, []string{"shop-web-1"}, byVolume["gordon-shop--web--vol--data"].Containers)
79	assert.Equal(t, "shop", byVolume["gordon-shop--web--vol--data"].Labels[domain.LabelApp])
80	assert.False(t, byVolume["pgdata"].InUse)
81	assert.Empty(t, byVolume["pgdata"].Labels)
82}
83
84func TestRuntime_InventoryRuntimeReportsGapsNotAbsence(t *testing.T) {
85	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
86		w.Header().Set("Content-Type", "application/json")
87		if r.URL.Path == "/v1.41/images/json" {
88			w.WriteHeader(http.StatusInternalServerError)
89			_, _ = w.Write([]byte(`{"message":"boom"}`))
90			return
91		}
92		switch r.URL.Path {
93		case "/v1.41/containers/json":
94			_, _ = w.Write([]byte(`[]`))
95		case "/v1.41/volumes":
96			_, _ = w.Write([]byte(`{"Volumes":[]}`))
97		}
98	}))
99	defer server.Close()
100
101	rt := newRuntimeForHTTPServer(t, server)
102	inventory, err := rt.InventoryRuntime(context.Background())
103	require.NoError(t, err)
104	require.False(t, inventory.Complete())
105	require.Len(t, inventory.Gaps, 1)
106	assert.Equal(t, domain.InventorySourceRuntimeImages, inventory.Gaps[0].Source)
107	assert.Equal(t, domain.PruneReasonUnknownInventory, inventory.Gaps[0].Reason)
108}
109
110func TestRuntime_InventoryRuntimeContainerListFailureProtectsImages(t *testing.T) {
111	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
112		w.Header().Set("Content-Type", "application/json")
113		switch r.URL.Path {
114		case "/v1.41/containers/json":
115			w.WriteHeader(http.StatusInternalServerError)
116			_, _ = w.Write([]byte(`{"message":"boom"}`))
117		case "/v1.41/images/json":
118			_, _ = w.Write([]byte(`[]`))
119		case "/v1.41/volumes":
120			_, _ = w.Write([]byte(`{"Volumes":[]}`))
121		}
122	}))
123	defer server.Close()
124
125	rt := newRuntimeForHTTPServer(t, server)
126	inventory, err := rt.InventoryRuntime(context.Background())
127	require.NoError(t, err)
128	require.False(t, inventory.Complete())
129	require.Len(t, inventory.Gaps, 1)
130	assert.Equal(t, domain.InventorySourceRuntimeContainers, inventory.Gaps[0].Source)
131	assert.Equal(t, domain.PruneReasonUnknownContainerUse, inventory.Gaps[0].Reason)
132}
133
134func TestRuntime_RemoveImageExactUsesExactIdentityWithoutForce(t *testing.T) {
135	var method, path string
136	var force string
137	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
138		method = r.Method
139		path = r.URL.Path
140		force = r.URL.Query().Get("force")
141		w.Header().Set("Content-Type", "application/json")
142		_, _ = w.Write([]byte(`[]`))
143	}))
144	defer server.Close()
145
146	rt := newRuntimeForHTTPServer(t, server)
147	ref, err := domain.NewRuntimeImageRef("sha256:deadbeef")
148	require.NoError(t, err)
149	require.NoError(t, rt.RemoveImageExact(context.Background(), ref))
150
151	assert.Equal(t, http.MethodDelete, method)
152	assert.Equal(t, "/v1.41/images/sha256:deadbeef", path)
153	assert.NotEqual(t, "true", force)
154	assert.NotEqual(t, "1", force)
155}
156
157func TestRuntime_RemoveVolumeExactUsesExactIdentityWithoutForce(t *testing.T) {
158	var method, path string
159	var force string
160	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
161		method = r.Method
162		path = r.URL.Path
163		force = r.URL.Query().Get("force")
164		w.Header().Set("Content-Type", "application/json")
165		_, _ = w.Write([]byte(`{}`))
166	}))
167	defer server.Close()
168
169	rt := newRuntimeForHTTPServer(t, server)
170	ref, err := domain.NewRuntimeVolumeRef("gordon-shop--web--vol--data")
171	require.NoError(t, err)
172	require.NoError(t, rt.RemoveVolumeExact(context.Background(), ref))
173
174	assert.Equal(t, http.MethodDelete, method)
175	assert.Equal(t, "/v1.41/volumes/gordon-shop--web--vol--data", path)
176	assert.NotEqual(t, "true", force)
177	assert.NotEqual(t, "1", force)
178}
179
180func TestRuntime_ExactRemovalRejectsInvalidIdentity(t *testing.T) {
181	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
182		t.Errorf("invalid identity must not reach the runtime: %s %s", r.Method, r.URL.Path)
183	}))
184	defer server.Close()
185
186	rt := newRuntimeForHTTPServer(t, server)
187	err := rt.RemoveImageExact(context.Background(), domain.RuntimeImageRef{})
188	require.Error(t, err)
189	assert.Contains(t, err.Error(), "invalid identity")
190
191	err = rt.RemoveVolumeExact(context.Background(), domain.RuntimeVolumeRef{})
192	require.Error(t, err)
193	assert.Contains(t, err.Error(), "invalid identity")
194
195	err = rt.RemoveImageExact(context.Background(), domain.RuntimeImageRef{ID: "<none>"})
196	require.Error(t, err)
197}
198
199func TestRuntime_ExactRemovalPropagatesRuntimeErrors(t *testing.T) {
200	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
201		w.Header().Set("Content-Type", "application/json")
202		w.WriteHeader(http.StatusConflict)
203		_, _ = w.Write([]byte(`{"message":"volume is in use"}`))
204	}))
205	defer server.Close()
206
207	rt := newRuntimeForHTTPServer(t, server)
208	ref, err := domain.NewRuntimeVolumeRef("in-use")
209	require.NoError(t, err)
210	err = rt.RemoveVolumeExact(context.Background(), ref)
211	require.Error(t, err)
212	assert.True(t, strings.Contains(err.Error(), "remove volume") || strings.Contains(err.Error(), "in use"),
213		"unexpected error: %v", err)
214}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close