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}