2.0.0 4803e67

manager_deploy_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_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}

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