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_DeployContainer_ImagePull(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 Server: config.ServerConfig{
26 Runtime: "docker",
27 },
28 Volumes: config.VolumeConfig{
29 AutoCreate: false,
30 },
31 NetworkIsolation: config.NetworkIsolationConfig{
32 Enabled: false,
33 },
34 }
35
36 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
37 require.NoError(t, err)
38
39 route := config.Route{
40 Domain: "test.example.com",
41 Image: "alpine:latest",
42 }
43
44 // Image not found locally, should pull
45 mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
46 mockRuntime.EXPECT().ListImages(ctx).Return([]string{}, nil)
47 mockRuntime.EXPECT().PullImage(ctx, "alpine:latest").Return(nil)
48 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "alpine:latest").Return([]int{}, nil)
49 mockRuntime.EXPECT().InspectImageEnv(ctx, "alpine:latest").Return([]string{}, nil)
50
51 expectedContainer := &runtime.Container{
52 ID: "container456",
53 Name: "gordon-test.example.com",
54 Image: "alpine:latest",
55 Status: "running",
56 Ports: []int{},
57 }
58
59 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
60 mockRuntime.EXPECT().StartContainer(ctx, "container456").Return(nil)
61 mockRuntime.EXPECT().InspectContainer(ctx, "container456").Return(expectedContainer, nil)
62
63 container, err := manager.DeployContainer(ctx, route)
64
65 assert.NoError(t, err)
66 assert.Equal(t, expectedContainer, container)
67}
68
69func TestManager_DeployContainer_WithAuth(t *testing.T) {
70 ctrl := gomock.NewController(t)
71 defer ctrl.Finish()
72
73 ctx := testutils.TestContext(t)
74 mockRuntime := mocks.NewMockRuntime(ctrl)
75
76 cfg := &config.Config{
77 Server: config.ServerConfig{
78 Runtime: "docker",
79 RegistryDomain: "registry.example.com",
80 },
81 RegistryAuth: config.RegistryAuthConfig{
82 Enabled: true,
83 Username: "testuser",
84 Password: "testpass",
85 },
86 Volumes: config.VolumeConfig{
87 AutoCreate: false,
88 },
89 NetworkIsolation: config.NetworkIsolationConfig{
90 Enabled: false,
91 },
92 }
93
94 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
95 require.NoError(t, err)
96
97 route := config.Route{
98 Domain: "test.example.com",
99 Image: "myapp:latest",
100 }
101
102 // Should use authenticated pull with registry domain prefix
103 expectedImageRef := "registry.example.com/myapp:latest"
104 mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
105 mockRuntime.EXPECT().ListImages(ctx).Return([]string{}, nil)
106 mockRuntime.EXPECT().PullImageWithAuth(ctx, expectedImageRef, "testuser", "testpass").Return(nil)
107 mockRuntime.EXPECT().GetImageExposedPorts(ctx, expectedImageRef).Return([]int{8080}, nil)
108 mockRuntime.EXPECT().InspectImageEnv(ctx, expectedImageRef).Return([]string{}, nil)
109
110 expectedContainer := &runtime.Container{
111 ID: "container789",
112 Name: "gordon-test.example.com",
113 Image: expectedImageRef,
114 Status: "running",
115 Ports: []int{8080},
116 }
117
118 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
119 mockRuntime.EXPECT().StartContainer(ctx, "container789").Return(nil)
120 mockRuntime.EXPECT().InspectContainer(ctx, "container789").Return(expectedContainer, nil)
121
122 container, err := manager.DeployContainer(ctx, route)
123
124 assert.NoError(t, err)
125 assert.Equal(t, expectedContainer, container)
126}
127
128func TestManager_DeployContainer_ReplaceExisting(t *testing.T) {
129 ctrl := gomock.NewController(t)
130 defer ctrl.Finish()
131
132 ctx := testutils.TestContext(t)
133 mockRuntime := mocks.NewMockRuntime(ctrl)
134
135 cfg := &config.Config{
136 Server: config.ServerConfig{
137 Runtime: "docker",
138 },
139 Volumes: config.VolumeConfig{
140 AutoCreate: false,
141 },
142 NetworkIsolation: config.NetworkIsolationConfig{
143 Enabled: false,
144 },
145 }
146
147 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
148 require.NoError(t, err)
149
150 // Add existing container
151 existingContainer := &runtime.Container{
152 ID: "old-container",
153 Name: "gordon-test.example.com",
154 Status: "running",
155 }
156 manager.containers["test.example.com"] = existingContainer
157
158 route := config.Route{
159 Domain: "test.example.com",
160 Image: "nginx:latest",
161 }
162
163 // Should stop and remove existing container
164 mockRuntime.EXPECT().StopContainer(ctx, "old-container").Return(nil)
165 mockRuntime.EXPECT().RemoveContainer(ctx, "old-container", true).Return(nil)
166
167 // Check for orphaned containers
168 mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
169 mockRuntime.EXPECT().ListImages(ctx).Return([]string{"nginx:latest"}, nil)
170 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "nginx:latest").Return([]int{80}, nil)
171 mockRuntime.EXPECT().InspectImageEnv(ctx, "nginx:latest").Return([]string{}, nil)
172
173 newContainer := &runtime.Container{
174 ID: "new-container",
175 Name: "gordon-test.example.com",
176 Image: "nginx:latest",
177 Status: "running",
178 Ports: []int{80},
179 }
180
181 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(newContainer, nil)
182 mockRuntime.EXPECT().StartContainer(ctx, "new-container").Return(nil)
183 mockRuntime.EXPECT().InspectContainer(ctx, "new-container").Return(newContainer, nil)
184
185 container, err := manager.DeployContainer(ctx, route)
186
187 assert.NoError(t, err)
188 assert.Equal(t, newContainer, container)
189 assert.Equal(t, newContainer, manager.containers["test.example.com"])
190}
191
192func TestManager_DeployContainer_Errors(t *testing.T) {
193 tests := []struct {
194 name string
195 setupMocks func(*mocks.MockRuntime)
196 expectedError string
197 }{
198 {
199 name: "pull image error",
200 setupMocks: func(m *mocks.MockRuntime) {
201 m.EXPECT().ListContainers(gomock.Any(), true).Return([]*runtime.Container{}, nil)
202 m.EXPECT().ListImages(gomock.Any()).Return([]string{}, nil)
203 m.EXPECT().PullImage(gomock.Any(), "nginx:latest").Return(errors.New("pull failed"))
204 },
205 expectedError: "failed to pull image",
206 },
207 {
208 name: "create container error",
209 setupMocks: func(m *mocks.MockRuntime) {
210 m.EXPECT().ListContainers(gomock.Any(), true).Return([]*runtime.Container{}, nil)
211 m.EXPECT().ListImages(gomock.Any()).Return([]string{"nginx:latest"}, nil)
212 m.EXPECT().GetImageExposedPorts(gomock.Any(), "nginx:latest").Return([]int{80}, nil)
213 m.EXPECT().InspectImageEnv(gomock.Any(), "nginx:latest").Return([]string{}, nil)
214 m.EXPECT().CreateContainer(gomock.Any(), gomock.Any()).Return(nil, errors.New("create failed"))
215 },
216 expectedError: "failed to create container",
217 },
218 {
219 name: "start container error",
220 setupMocks: func(m *mocks.MockRuntime) {
221 m.EXPECT().ListContainers(gomock.Any(), true).Return([]*runtime.Container{}, nil)
222 m.EXPECT().ListImages(gomock.Any()).Return([]string{"nginx:latest"}, nil)
223 m.EXPECT().GetImageExposedPorts(gomock.Any(), "nginx:latest").Return([]int{80}, nil)
224 m.EXPECT().InspectImageEnv(gomock.Any(), "nginx:latest").Return([]string{}, nil)
225 container := &runtime.Container{ID: "test123"}
226 m.EXPECT().CreateContainer(gomock.Any(), gomock.Any()).Return(container, nil)
227 m.EXPECT().StartContainer(gomock.Any(), "test123").Return(errors.New("start failed"))
228 m.EXPECT().RemoveContainer(gomock.Any(), "test123", true).Return(nil)
229 },
230 expectedError: "failed to start container",
231 },
232 }
233
234 for _, tt := range tests {
235 t.Run(tt.name, func(t *testing.T) {
236 ctrl := gomock.NewController(t)
237 defer ctrl.Finish()
238
239 ctx := testutils.TestContext(t)
240 mockRuntime := mocks.NewMockRuntime(ctrl)
241
242 cfg := &config.Config{
243 Server: config.ServerConfig{
244 Runtime: "docker",
245 },
246 Volumes: config.VolumeConfig{
247 AutoCreate: false,
248 },
249 NetworkIsolation: config.NetworkIsolationConfig{
250 Enabled: false,
251 },
252 }
253
254 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
255 require.NoError(t, err)
256
257 route := config.Route{
258 Domain: "test.example.com",
259 Image: "nginx:latest",
260 }
261
262 tt.setupMocks(mockRuntime)
263
264 _, err = manager.DeployContainer(ctx, route)
265 assert.Error(t, err)
266 assert.Contains(t, err.Error(), tt.expectedError)
267 })
268 }
269}
270
271func TestManager_DeployContainer_WithNetworkAttachments(t *testing.T) {
272 ctrl := gomock.NewController(t)
273 defer ctrl.Finish()
274
275 ctx := testutils.TestContext(t)
276 mockRuntime := mocks.NewMockRuntime(ctrl)
277
278 cfg := &config.Config{
279 Server: config.ServerConfig{
280 Runtime: "docker",
281 },
282 Volumes: config.VolumeConfig{
283 AutoCreate: false,
284 },
285 NetworkIsolation: config.NetworkIsolationConfig{
286 Enabled: true,
287 NetworkPrefix: "gordon",
288 },
289 NetworkGroups: map[string][]string{
290 "backend": {"api.example.com", "worker.example.com"},
291 },
292 Attachments: map[string][]string{
293 "api.example.com": {"redis:latest"},
294 "backend": {"postgres:latest"},
295 },
296 }
297
298 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
299 require.NoError(t, err)
300
301 route := config.Route{
302 Domain: "api.example.com",
303 Image: "myapi:latest",
304 }
305
306 // Main container deployment expectations
307 mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
308 mockRuntime.EXPECT().ListImages(ctx).Return([]string{"myapi:latest"}, nil)
309 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "myapi:latest").Return([]int{8080}, nil)
310 mockRuntime.EXPECT().InspectImageEnv(ctx, "myapi:latest").Return([]string{}, nil)
311 mockRuntime.EXPECT().NetworkExists(ctx, "gordon-backend").Return(true, nil)
312
313 expectedContainer := &runtime.Container{
314 ID: "main-container",
315 Name: "gordon-api.example.com",
316 Image: "myapi:latest",
317 Status: "running",
318 Ports: []int{8080},
319 }
320
321 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
322 mockRuntime.EXPECT().StartContainer(ctx, "main-container").Return(nil)
323 mockRuntime.EXPECT().InspectContainer(ctx, "main-container").Return(expectedContainer, nil)
324
325 // Attachment deployment expectations for app-specific service
326 mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil)
327 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil)
328 mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil)
329
330 redisContainer := &runtime.Container{
331 ID: "redis-container",
332 Name: "api-example-com-redis",
333 }
334 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(redisContainer, nil)
335 mockRuntime.EXPECT().StartContainer(ctx, "redis-container").Return(nil)
336
337 // Attachment deployment expectations for network group service
338 mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{redisContainer}, nil)
339 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil)
340 mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil)
341
342 pgContainer := &runtime.Container{
343 ID: "pg-container",
344 Name: "gordon-shared-postgres",
345 }
346 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(pgContainer, nil)
347 mockRuntime.EXPECT().StartContainer(ctx, "pg-container").Return(nil)
348
349 container, err := manager.DeployContainer(ctx, route)
350
351 assert.NoError(t, err)
352 assert.Equal(t, expectedContainer, container)
353}
354
355func TestManager_DeployAttachedService(t *testing.T) {
356 ctrl := gomock.NewController(t)
357 defer ctrl.Finish()
358
359 ctx := testutils.TestContext(t)
360 mockRuntime := mocks.NewMockRuntime(ctrl)
361
362 cfg := &config.Config{
363 Volumes: config.VolumeConfig{
364 AutoCreate: true,
365 Prefix: "gordon",
366 },
367 NetworkIsolation: config.NetworkIsolationConfig{
368 NetworkPrefix: "gordon",
369 },
370 }
371
372 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
373 require.NoError(t, err)
374
375 // Test deploying redis service for an app
376 mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil)
377 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil)
378 mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{"REDIS_VERSION=7.0"}, nil)
379 mockRuntime.EXPECT().InspectImageVolumes(ctx, "redis:latest").Return([]string{"/data"}, nil)
380 mockRuntime.EXPECT().VolumeExists(ctx, "gordon-test-example-com-data").Return(false, nil)
381 mockRuntime.EXPECT().CreateVolume(ctx, "gordon-test-example-com-data").Return(nil)
382
383 expectedContainer := &runtime.Container{
384 ID: "redis-service",
385 Name: "test-example-com-redis",
386 }
387
388 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
389 mockRuntime.EXPECT().StartContainer(ctx, "redis-service").Return(nil)
390
391 err = manager.DeployAttachedService(ctx, "test.example.com", "redis:latest")
392
393 assert.NoError(t, err)
394}