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}