v2.0.0 ae3e752

manager_volume_test.go RawHistoryFolder

1package container
2
3import (
4	"errors"
5	"testing"
6
7	"github.com/stretchr/testify/assert"
8	"github.com/stretchr/testify/require"
9	"go.uber.org/mock/gomock"
10
11	"gordon/internal/config"
12	"gordon/internal/testutils"
13	"gordon/internal/testutils/mocks"
14	"gordon/pkg/runtime"
15)
16
17func TestManager_cleanupVolumesForDomain(t *testing.T) {
18	ctrl := gomock.NewController(t)
19	defer ctrl.Finish()
20
21	ctx := testutils.TestContext(t)
22	mockRuntime := mocks.NewMockRuntime(ctrl)
23
24	cfg := &config.Config{
25		Volumes: config.VolumeConfig{
26			Prefix: "gordon",
27		},
28		Routes: map[string]string{
29			"test.example.com": "nginx:latest",
30		},
31	}
32
33	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
34	require.NoError(t, err)
35
36	// Mock image inspection for volume paths
37	mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx", "/etc/nginx"}, nil)
38
39	// Mock volume existence checks and removal
40	varLogNginxVolume := generateVolumeName("gordon", "test.example.com", "/var/log/nginx")
41	etcNginxVolume := generateVolumeName("gordon", "test.example.com", "/etc/nginx")
42	
43	mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(true, nil)
44	mockRuntime.EXPECT().RemoveVolume(ctx, varLogNginxVolume, true).Return(nil)
45
46	mockRuntime.EXPECT().VolumeExists(ctx, etcNginxVolume).Return(true, nil)
47	mockRuntime.EXPECT().RemoveVolume(ctx, etcNginxVolume, true).Return(nil)
48
49	err = manager.cleanupVolumesForDomain(ctx, "test.example.com")
50
51	assert.NoError(t, err)
52}
53
54func TestManager_cleanupVolumesForDomain_NoRoute(t *testing.T) {
55	ctrl := gomock.NewController(t)
56	defer ctrl.Finish()
57
58	ctx := testutils.TestContext(t)
59	mockRuntime := mocks.NewMockRuntime(ctrl)
60
61	cfg := &config.Config{
62		Volumes: config.VolumeConfig{
63			Prefix: "gordon",
64		},
65		Routes: map[string]string{},
66	}
67
68	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
69	require.NoError(t, err)
70
71	// Should return nil without doing anything
72	err = manager.cleanupVolumesForDomain(ctx, "nonexistent.example.com")
73
74	assert.NoError(t, err)
75}
76
77func TestManager_cleanupVolumesForDomain_WithRegistryAuth(t *testing.T) {
78	ctrl := gomock.NewController(t)
79	defer ctrl.Finish()
80
81	ctx := testutils.TestContext(t)
82	mockRuntime := mocks.NewMockRuntime(ctrl)
83
84	cfg := &config.Config{
85		Server: config.ServerConfig{
86			RegistryDomain: "registry.example.com",
87		},
88		RegistryAuth: config.RegistryAuthConfig{
89			Enabled: true,
90		},
91		Volumes: config.VolumeConfig{
92			Prefix: "gordon",
93		},
94		Routes: map[string]string{
95			"test.example.com": "myapp:latest",
96		},
97	}
98
99	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
100	require.NoError(t, err)
101
102	// Should use prefixed image reference
103	expectedImageRef := "registry.example.com/myapp:latest"
104	mockRuntime.EXPECT().InspectImageVolumes(ctx, expectedImageRef).Return([]string{"/data"}, nil)
105
106	dataVolume := generateVolumeName("gordon", "test.example.com", "/data")
107	mockRuntime.EXPECT().VolumeExists(ctx, dataVolume).Return(true, nil)
108	mockRuntime.EXPECT().RemoveVolume(ctx, dataVolume, true).Return(nil)
109
110	err = manager.cleanupVolumesForDomain(ctx, "test.example.com")
111
112	assert.NoError(t, err)
113}
114
115func TestManager_cleanupVolumesForDomain_NoVolumes(t *testing.T) {
116	ctrl := gomock.NewController(t)
117	defer ctrl.Finish()
118
119	ctx := testutils.TestContext(t)
120	mockRuntime := mocks.NewMockRuntime(ctrl)
121
122	cfg := &config.Config{
123		Volumes: config.VolumeConfig{
124			Prefix: "gordon",
125		},
126		Routes: map[string]string{
127			"test.example.com": "alpine:latest",
128		},
129	}
130
131	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
132	require.NoError(t, err)
133
134	// Image has no volumes
135	mockRuntime.EXPECT().InspectImageVolumes(ctx, "alpine:latest").Return([]string{}, nil)
136
137	err = manager.cleanupVolumesForDomain(ctx, "test.example.com")
138
139	assert.NoError(t, err)
140}
141
142func TestManager_cleanupVolumesForDomain_VolumeNotExists(t *testing.T) {
143	ctrl := gomock.NewController(t)
144	defer ctrl.Finish()
145
146	ctx := testutils.TestContext(t)
147	mockRuntime := mocks.NewMockRuntime(ctrl)
148
149	cfg := &config.Config{
150		Volumes: config.VolumeConfig{
151			Prefix: "gordon",
152		},
153		Routes: map[string]string{
154			"test.example.com": "nginx:latest",
155		},
156	}
157
158	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
159	require.NoError(t, err)
160
161	mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx"}, nil)
162
163	// Volume doesn't exist, should skip removal
164	varLogNginxVolume := generateVolumeName("gordon", "test.example.com", "/var/log/nginx")
165	mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(false, nil)
166
167	err = manager.cleanupVolumesForDomain(ctx, "test.example.com")
168
169	assert.NoError(t, err)
170}
171
172func TestManager_cleanupVolumesForDomain_Errors(t *testing.T) {
173	ctrl := gomock.NewController(t)
174	defer ctrl.Finish()
175
176	ctx := testutils.TestContext(t)
177	mockRuntime := mocks.NewMockRuntime(ctrl)
178
179	cfg := &config.Config{
180		Volumes: config.VolumeConfig{
181			Prefix: "gordon",
182		},
183		Routes: map[string]string{
184			"test.example.com": "nginx:latest",
185		},
186	}
187
188	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
189	require.NoError(t, err)
190
191	// Test error inspecting image volumes
192	mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return(nil, errors.New("inspect failed"))
193
194	err = manager.cleanupVolumesForDomain(ctx, "test.example.com")
195	assert.Error(t, err)
196
197	// Test error checking volume existence
198	mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx"}, nil)
199	varLogNginxVolume := generateVolumeName("gordon", "test.example.com", "/var/log/nginx")
200	mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(false, errors.New("check failed"))
201
202	err = manager.cleanupVolumesForDomain(ctx, "test.example.com")
203	assert.NoError(t, err) // Should continue despite check error
204
205	// Test error removing volume
206	mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx"}, nil)
207	mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(true, nil)
208	mockRuntime.EXPECT().RemoveVolume(ctx, varLogNginxVolume, true).Return(errors.New("remove failed"))
209
210	err = manager.cleanupVolumesForDomain(ctx, "test.example.com")
211	assert.Error(t, err)
212	assert.Contains(t, err.Error(), "failed to cleanup 1 volumes")
213}
214
215func TestManager_DeployContainer_VolumeAutoCreation(t *testing.T) {
216	ctrl := gomock.NewController(t)
217	defer ctrl.Finish()
218
219	ctx := testutils.TestContext(t)
220	mockRuntime := mocks.NewMockRuntime(ctrl)
221
222	cfg := &config.Config{
223		Server: config.ServerConfig{
224			Runtime: "docker",
225		},
226		Volumes: config.VolumeConfig{
227			AutoCreate: true,
228			Prefix:     "gordon",
229			Preserve:   false,
230		},
231		NetworkIsolation: config.NetworkIsolationConfig{
232			Enabled: false,
233		},
234	}
235
236	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
237	require.NoError(t, err)
238
239	route := config.Route{
240		Domain: "test.example.com",
241		Image:  "postgres:latest",
242	}
243
244	// Set up expectations for deployment with volume creation
245	mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
246	mockRuntime.EXPECT().ListImages(ctx).Return([]string{"postgres:latest"}, nil)
247	mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil)
248	mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil)
249
250	// Mock volume discovery and creation
251	mockRuntime.EXPECT().InspectImageVolumes(ctx, "postgres:latest").Return([]string{"/var/lib/postgresql/data", "/etc/postgresql"}, nil)
252
253	// First volume
254	dataVolume := generateVolumeName("gordon", "test.example.com", "/var/lib/postgresql/data")
255	mockRuntime.EXPECT().VolumeExists(ctx, dataVolume).Return(false, nil)
256	mockRuntime.EXPECT().CreateVolume(ctx, dataVolume).Return(nil)
257
258	// Second volume already exists
259	etcVolume := generateVolumeName("gordon", "test.example.com", "/etc/postgresql")
260	mockRuntime.EXPECT().VolumeExists(ctx, etcVolume).Return(true, nil)
261
262	expectedContainer := &runtime.Container{
263		ID:     "postgres-container",
264		Name:   "gordon-test.example.com",
265		Image:  "postgres:latest",
266		Status: "running",
267		Ports:  []int{5432},
268	}
269
270	mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
271	mockRuntime.EXPECT().StartContainer(ctx, "postgres-container").Return(nil)
272	mockRuntime.EXPECT().InspectContainer(ctx, "postgres-container").Return(expectedContainer, nil)
273
274	container, err := manager.DeployContainer(ctx, route)
275
276	assert.NoError(t, err)
277	assert.Equal(t, expectedContainer, container)
278}
279
280func TestManager_DeployContainer_VolumeCreationError(t *testing.T) {
281	ctrl := gomock.NewController(t)
282	defer ctrl.Finish()
283
284	ctx := testutils.TestContext(t)
285	mockRuntime := mocks.NewMockRuntime(ctrl)
286
287	cfg := &config.Config{
288		Server: config.ServerConfig{
289			Runtime: "docker",
290		},
291		Volumes: config.VolumeConfig{
292			AutoCreate: true,
293			Prefix:     "gordon",
294		},
295		NetworkIsolation: config.NetworkIsolationConfig{
296			Enabled: false,
297		},
298	}
299
300	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
301	require.NoError(t, err)
302
303	route := config.Route{
304		Domain: "test.example.com",
305		Image:  "postgres:latest",
306	}
307
308	mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
309	mockRuntime.EXPECT().ListImages(ctx).Return([]string{"postgres:latest"}, nil)
310	mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil)
311	mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil)
312
313	// Mock volume creation failure
314	mockRuntime.EXPECT().InspectImageVolumes(ctx, "postgres:latest").Return([]string{"/var/lib/postgresql/data"}, nil)
315	dataVolume := generateVolumeName("gordon", "test.example.com", "/var/lib/postgresql/data")
316	mockRuntime.EXPECT().VolumeExists(ctx, dataVolume).Return(false, nil)
317	mockRuntime.EXPECT().CreateVolume(ctx, dataVolume).Return(errors.New("create failed"))
318
319	// Should still continue with container creation (volume creation is best-effort)
320	expectedContainer := &runtime.Container{
321		ID:     "postgres-container",
322		Name:   "gordon-test.example.com",
323		Image:  "postgres:latest",
324		Status: "running",
325		Ports:  []int{5432},
326	}
327
328	mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
329	mockRuntime.EXPECT().StartContainer(ctx, "postgres-container").Return(nil)
330	mockRuntime.EXPECT().InspectContainer(ctx, "postgres-container").Return(expectedContainer, nil)
331
332	container, err := manager.DeployContainer(ctx, route)
333
334	assert.NoError(t, err)
335	assert.Equal(t, expectedContainer, container)
336}
337
338func TestManager_DeployContainer_VolumeAutoCreationDisabled(t *testing.T) {
339	ctrl := gomock.NewController(t)
340	defer ctrl.Finish()
341
342	ctx := testutils.TestContext(t)
343	mockRuntime := mocks.NewMockRuntime(ctrl)
344
345	cfg := &config.Config{
346		Server: config.ServerConfig{
347			Runtime: "docker",
348		},
349		Volumes: config.VolumeConfig{
350			AutoCreate: false, // Disabled
351		},
352		NetworkIsolation: config.NetworkIsolationConfig{
353			Enabled: false,
354		},
355	}
356
357	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
358	require.NoError(t, err)
359
360	route := config.Route{
361		Domain: "test.example.com",
362		Image:  "postgres:latest",
363	}
364
365	mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
366	mockRuntime.EXPECT().ListImages(ctx).Return([]string{"postgres:latest"}, nil)
367	mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil)
368	mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil)
369
370	// Should NOT inspect image volumes when auto-creation is disabled
371
372	expectedContainer := &runtime.Container{
373		ID:     "postgres-container",
374		Name:   "gordon-test.example.com",
375		Image:  "postgres:latest",
376		Status: "running",
377		Ports:  []int{5432},
378	}
379
380	mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
381	mockRuntime.EXPECT().StartContainer(ctx, "postgres-container").Return(nil)
382	mockRuntime.EXPECT().InspectContainer(ctx, "postgres-container").Return(expectedContainer, nil)
383
384	container, err := manager.DeployContainer(ctx, route)
385
386	assert.NoError(t, err)
387	assert.Equal(t, expectedContainer, container)
388}

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