2.0.0 4803e67

manager_autostart_test.go RawHistoryFolder

1package container
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/assert"
7	"github.com/stretchr/testify/require"
8	"go.uber.org/mock/gomock"
9
10	"gordon/internal/config"
11	"gordon/internal/testutils"
12	"gordon/internal/testutils/mocks"
13	"gordon/pkg/runtime"
14)
15
16func TestManager_AutoStartContainers(t *testing.T) {
17	ctrl := gomock.NewController(t)
18	defer ctrl.Finish()
19
20	ctx := testutils.TestContext(t)
21	mockRuntime := mocks.NewMockRuntime(ctrl)
22
23	cfg := &config.Config{
24		Server: config.ServerConfig{
25			Runtime: "docker",
26		},
27		Routes: map[string]string{
28			"app1.example.com": "nginx:latest",
29			"app2.example.com": "apache:latest",
30		},
31		Volumes: config.VolumeConfig{
32			AutoCreate: false,
33		},
34		NetworkIsolation: config.NetworkIsolationConfig{
35			Enabled: false,
36		},
37	}
38
39	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
40	require.NoError(t, err)
41
42	// Mock existing containers - one Gordon-managed running, one stopped
43	existingContainers := []*runtime.Container{
44		{
45			ID:    "running-container",
46			Name:  "gordon-app1.example.com",
47			Image: "nginx:latest",
48			Labels: map[string]string{
49				"gordon.managed": "true",
50				"gordon.domain":  "app1.example.com",
51			},
52		},
53		{
54			ID:    "stopped-container",
55			Name:  "gordon-app2.example.com",
56			Image: "apache:latest",
57			Labels: map[string]string{
58				"gordon.managed": "true",
59				"gordon.domain":  "app2.example.com",
60			},
61		},
62	}
63
64	mockRuntime.EXPECT().ListContainers(ctx, true).Return(existingContainers, nil)
65
66	// First container is already running
67	mockRuntime.EXPECT().IsContainerRunning(ctx, "running-container").Return(true, nil)
68
69	// Second container is stopped, should be started
70	mockRuntime.EXPECT().IsContainerRunning(ctx, "stopped-container").Return(false, nil)
71	mockRuntime.EXPECT().StartContainer(ctx, "stopped-container").Return(nil)
72
73	// Mock inspection of restarted container
74	startedContainer := &runtime.Container{
75		ID:    "stopped-container",
76		Name:  "gordon-app2.example.com",
77		Image: "apache:latest",
78		Ports: []int{80},
79	}
80	mockRuntime.EXPECT().InspectContainer(ctx, "stopped-container").Return(startedContainer, nil)
81
82	err = manager.AutoStartContainers(ctx)
83
84	assert.NoError(t, err)
85	assert.Len(t, manager.containers, 2)
86	assert.Contains(t, manager.containers, "app1.example.com")
87	assert.Contains(t, manager.containers, "app2.example.com")
88}
89
90func TestManager_AutoStartContainers_DeployNew(t *testing.T) {
91	ctrl := gomock.NewController(t)
92	defer ctrl.Finish()
93
94	ctx := testutils.TestContext(t)
95	mockRuntime := mocks.NewMockRuntime(ctrl)
96
97	cfg := &config.Config{
98		Server: config.ServerConfig{
99			Runtime: "docker",
100		},
101		Routes: map[string]string{
102			"new-app.example.com": "redis:latest",
103		},
104		Volumes: config.VolumeConfig{
105			AutoCreate: false,
106		},
107		NetworkIsolation: config.NetworkIsolationConfig{
108			Enabled: false,
109		},
110	}
111
112	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
113	require.NoError(t, err)
114
115	// No existing containers
116	mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
117
118	// Should deploy new container
119	mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
120	mockRuntime.EXPECT().ListImages(ctx).Return([]string{"redis:latest"}, nil)
121	mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil)
122	mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil)
123
124	newContainer := &runtime.Container{
125		ID:    "new-container",
126		Name:  "gordon-new-app.example.com",
127		Image: "redis:latest",
128		Ports: []int{6379},
129	}
130
131	mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(newContainer, nil)
132	mockRuntime.EXPECT().StartContainer(ctx, "new-container").Return(nil)
133	mockRuntime.EXPECT().InspectContainer(ctx, "new-container").Return(newContainer, nil)
134
135	err = manager.AutoStartContainers(ctx)
136
137	assert.NoError(t, err)
138	assert.Len(t, manager.containers, 1)
139	assert.Contains(t, manager.containers, "new-app.example.com")
140}
141
142func TestManager_AutoStartContainers_CleanupConflicts(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		Server: config.ServerConfig{
151			Runtime: "docker",
152		},
153		Routes: map[string]string{
154			"app.example.com": "nginx:latest",
155		},
156		Volumes: config.VolumeConfig{
157			AutoCreate: false,
158		},
159		NetworkIsolation: config.NetworkIsolationConfig{
160			Enabled: false,
161		},
162	}
163
164	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
165	require.NoError(t, err)
166
167	// Existing containers include a non-Gordon container with same image
168	existingContainers := []*runtime.Container{
169		{
170			ID:    "unmanaged-container",
171			Name:  "some-nginx-container",
172			Image: "nginx:latest",
173			// No Gordon labels - not managed
174		},
175	}
176
177	mockRuntime.EXPECT().ListContainers(ctx, true).Return(existingContainers, nil)
178
179	// Should stop and remove the conflicting container
180	mockRuntime.EXPECT().IsContainerRunning(ctx, "unmanaged-container").Return(true, nil)
181	mockRuntime.EXPECT().StopContainer(ctx, "unmanaged-container").Return(nil)
182	mockRuntime.EXPECT().RemoveContainer(ctx, "unmanaged-container", true).Return(nil)
183
184	// Then deploy new Gordon-managed container
185	mockRuntime.EXPECT().ListContainers(ctx, true).Return([]*runtime.Container{}, nil)
186	mockRuntime.EXPECT().ListImages(ctx).Return([]string{"nginx:latest"}, nil)
187	mockRuntime.EXPECT().GetImageExposedPorts(ctx, "nginx:latest").Return([]int{80}, nil)
188	mockRuntime.EXPECT().InspectImageEnv(ctx, "nginx:latest").Return([]string{}, nil)
189
190	newContainer := &runtime.Container{
191		ID:    "gordon-container",
192		Name:  "gordon-app.example.com",
193		Image: "nginx:latest",
194		Ports: []int{80},
195	}
196
197	mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(newContainer, nil)
198	mockRuntime.EXPECT().StartContainer(ctx, "gordon-container").Return(nil)
199	mockRuntime.EXPECT().InspectContainer(ctx, "gordon-container").Return(newContainer, nil)
200
201	err = manager.AutoStartContainers(ctx)
202
203	assert.NoError(t, err)
204	assert.Len(t, manager.containers, 1)
205	assert.Contains(t, manager.containers, "app.example.com")
206}
207
208func TestManager_AutoStartContainers_NoRoutes(t *testing.T) {
209	ctrl := gomock.NewController(t)
210	defer ctrl.Finish()
211
212	ctx := testutils.TestContext(t)
213	mockRuntime := mocks.NewMockRuntime(ctrl)
214
215	cfg := &config.Config{
216		Server: config.ServerConfig{
217			Runtime: "docker",
218		},
219		// No routes configured
220		Routes: map[string]string{},
221	}
222
223	manager, err := NewManagerWithRuntime(cfg, mockRuntime)
224	require.NoError(t, err)
225
226	err = manager.AutoStartContainers(ctx)
227
228	assert.NoError(t, err)
229	assert.Empty(t, manager.containers)
230}
231
232func TestManager_normalizeImageName(t *testing.T) {
233	manager := &Manager{}
234
235	tests := []struct {
236		input    string
237		expected string
238	}{
239		{"nginx", "docker.io/library/nginx"},
240		{"nginx:latest", "docker.io/library/nginx"},
241		{"user/repo", "docker.io/user/repo"},
242		{"user/repo:tag", "docker.io/user/repo"},
243		{"registry.com/user/repo", "registry.com/user/repo"},
244		{"registry.com/user/repo:tag", "registry.com/user/repo"},
245	}
246
247	for _, tt := range tests {
248		t.Run(tt.input, func(t *testing.T) {
249			result := manager.normalizeImageName(tt.input)
250			assert.Equal(t, tt.expected, result)
251		})
252	}
253}

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