1package container
2
3import (
4 "context"
5 "fmt"
6 "sync"
7
8 "github.com/rs/zerolog/log"
9
10 "gordon/internal/config"
11 "gordon/pkg/runtime"
12)
13
14// Manager handles container lifecycle and management
15type Manager struct {
16 runtime runtime.Runtime
17 config *config.Config
18 containers map[string]*runtime.Container // map[domain] -> container
19 mu sync.RWMutex
20}
21
22// NewManager creates a new container manager
23func NewManager(cfg *config.Config) (*Manager, error) {
24 // Create runtime using the factory
25 rt, err := CreateRuntime(cfg)
26 if err != nil {
27 return nil, fmt.Errorf("failed to create container runtime: %w", err)
28 }
29
30 // Test runtime connectivity
31 ctx := context.Background()
32 if err := rt.Ping(ctx); err != nil {
33 return nil, fmt.Errorf("runtime not available: %w", err)
34 }
35
36 version, err := rt.Version(ctx)
37 if err != nil {
38 log.Warn().Err(err).Msg("Could not get runtime version")
39 } else {
40 log.Info().Str("runtime", cfg.Server.Runtime).Str("version", version).Msg("Container runtime connected")
41 }
42
43 return &Manager{
44 runtime: rt,
45 config: cfg,
46 containers: make(map[string]*runtime.Container),
47 }, nil
48}
49
50// DeployContainer deploys a container for a specific route
51func (m *Manager) DeployContainer(ctx context.Context, route config.Route) (*runtime.Container, error) {
52 m.mu.Lock()
53 defer m.mu.Unlock()
54
55 // Check if container already exists for this domain
56 if existing, exists := m.containers[route.Domain]; exists {
57 log.Info().Str("domain", route.Domain).Str("container", existing.ID).Msg("Container already exists, restarting")
58
59 // Stop and remove existing container
60 if err := m.runtime.StopContainer(ctx, existing.ID); err != nil {
61 log.Warn().Err(err).Str("container", existing.ID).Msg("Failed to stop existing container")
62 }
63
64 if err := m.runtime.RemoveContainer(ctx, existing.ID, true); err != nil {
65 log.Warn().Err(err).Str("container", existing.ID).Msg("Failed to remove existing container")
66 }
67
68 delete(m.containers, route.Domain)
69 }
70
71 // Pull the image first
72 log.Info().Str("image", route.Image).Msg("Pulling image")
73 if err := m.runtime.PullImage(ctx, route.Image); err != nil {
74 return nil, fmt.Errorf("failed to pull image %s: %w", route.Image, err)
75 }
76
77 // Get exposed ports from the image
78 exposedPorts, err := m.runtime.GetImageExposedPorts(ctx, route.Image)
79 if err != nil {
80 log.Warn().Err(err).Str("image", route.Image).Msg("Failed to get exposed ports from image, using defaults")
81 exposedPorts = []int{80, 8080, 3000} // Fallback to common web server ports
82 }
83
84 // Create container configuration
85 containerConfig := &runtime.ContainerConfig{
86 Image: route.Image,
87 Name: fmt.Sprintf("gordon-%s", route.Domain),
88 Ports: exposedPorts,
89 Labels: map[string]string{
90 "gordon.domain": route.Domain,
91 "gordon.image": route.Image,
92 "gordon.managed": "true",
93 "gordon.route": route.Domain,
94 },
95 AutoRemove: false, // Keep containers for inspection
96 }
97
98 // Create the container
99 container, err := m.runtime.CreateContainer(ctx, containerConfig)
100 if err != nil {
101 return nil, fmt.Errorf("failed to create container for %s: %w", route.Domain, err)
102 }
103
104 // Start the container
105 if err := m.runtime.StartContainer(ctx, container.ID); err != nil {
106 // Clean up the created container
107 m.runtime.RemoveContainer(ctx, container.ID, true)
108 return nil, fmt.Errorf("failed to start container for %s: %w", route.Domain, err)
109 }
110
111 // Re-inspect to get updated port information
112 container, err = m.runtime.InspectContainer(ctx, container.ID)
113 if err != nil {
114 return nil, fmt.Errorf("failed to inspect started container: %w", err)
115 }
116
117 // Store the container mapping
118 m.containers[route.Domain] = container
119
120 log.Info().
121 Str("domain", route.Domain).
122 Str("image", route.Image).
123 Str("container", container.ID).
124 Ints("ports", container.Ports).
125 Msg("Container deployed successfully")
126
127 return container, nil
128}
129
130// GetContainer returns the container for a domain
131func (m *Manager) GetContainer(domain string) (*runtime.Container, bool) {
132 m.mu.RLock()
133 defer m.mu.RUnlock()
134
135 container, exists := m.containers[domain]
136 return container, exists
137}
138
139// GetContainerPort returns the host port for a domain's container
140func (m *Manager) GetContainerPort(ctx context.Context, domain string, internalPort int) (int, error) {
141 m.mu.RLock()
142 container, exists := m.containers[domain]
143 m.mu.RUnlock()
144
145 if !exists {
146 return 0, fmt.Errorf("no container found for domain %s", domain)
147 }
148
149 return m.runtime.GetContainerPort(ctx, container.ID, internalPort)
150}
151
152// ListContainers returns all managed containers
153func (m *Manager) ListContainers() map[string]*runtime.Container {
154 m.mu.RLock()
155 defer m.mu.RUnlock()
156
157 // Return a copy of the map to avoid race conditions
158 result := make(map[string]*runtime.Container)
159 for domain, container := range m.containers {
160 result[domain] = container
161 }
162 return result
163}
164
165// FindContainerByDomain returns the container ID for a domain
166func (m *Manager) FindContainerByDomain(domain string) (string, bool) {
167 m.mu.RLock()
168 defer m.mu.RUnlock()
169
170 if container, exists := m.containers[domain]; exists {
171 return container.ID, true
172 }
173 return "", false
174}
175
176// FindDomainByContainerID returns the domain for a container ID
177func (m *Manager) FindDomainByContainerID(containerID string) (string, bool) {
178 m.mu.RLock()
179 defer m.mu.RUnlock()
180
181 for domain, container := range m.containers {
182 if container.ID == containerID {
183 return domain, true
184 }
185 }
186 return "", false
187}
188
189// StopContainer stops a container by ID
190func (m *Manager) StopContainer(ctx context.Context, containerID string) error {
191 if err := m.runtime.StopContainer(ctx, containerID); err != nil {
192 return fmt.Errorf("failed to stop container %s: %w", containerID, err)
193 }
194
195 log.Info().Str("container", containerID).Msg("Container stopped")
196 return nil
197}
198
199// StopContainerByDomain stops a container for a domain
200func (m *Manager) StopContainerByDomain(ctx context.Context, domain string) error {
201 m.mu.RLock()
202 container, exists := m.containers[domain]
203 m.mu.RUnlock()
204
205 if !exists {
206 return fmt.Errorf("no container found for domain %s", domain)
207 }
208
209 return m.StopContainer(ctx, container.ID)
210}
211
212// RemoveContainer removes a container by ID
213func (m *Manager) RemoveContainer(ctx context.Context, containerID string, force bool) error {
214 if err := m.runtime.RemoveContainer(ctx, containerID, force); err != nil {
215 return fmt.Errorf("failed to remove container %s: %w", containerID, err)
216 }
217
218 // Remove from our tracking map
219 m.mu.Lock()
220 defer m.mu.Unlock()
221 for domain, container := range m.containers {
222 if container.ID == containerID {
223 delete(m.containers, domain)
224 log.Info().Str("domain", domain).Str("container", containerID).Msg("Container removed")
225 break
226 }
227 }
228
229 return nil
230}
231
232// RemoveContainerByDomain removes a container for a domain
233func (m *Manager) RemoveContainerByDomain(ctx context.Context, domain string, force bool) error {
234 m.mu.RLock()
235 container, exists := m.containers[domain]
236 m.mu.RUnlock()
237
238 if !exists {
239 return fmt.Errorf("no container found for domain %s", domain)
240 }
241
242 return m.RemoveContainer(ctx, container.ID, force)
243}
244
245// SyncContainers synchronizes the internal state with the actual running containers
246func (m *Manager) SyncContainers(ctx context.Context) error {
247 m.mu.Lock()
248 defer m.mu.Unlock()
249
250 // Get all containers from the runtime
251 allContainers, err := m.runtime.ListContainers(ctx, false) // only running containers
252 if err != nil {
253 return fmt.Errorf("failed to list containers: %w", err)
254 }
255
256 // Find containers managed by Gordon
257 managedContainers := make(map[string]*runtime.Container)
258 for _, container := range allContainers {
259 if container.Labels != nil {
260 if domain, exists := container.Labels["gordon.domain"]; exists && container.Labels["gordon.managed"] == "true" {
261 managedContainers[domain] = container
262 }
263 }
264 }
265
266 // Update our internal state
267 m.containers = managedContainers
268
269 log.Info().Int("count", len(managedContainers)).Msg("Container state synchronized")
270 return nil
271}
272
273// HealthCheck checks the health of all managed containers
274func (m *Manager) HealthCheck(ctx context.Context) map[string]bool {
275 m.mu.RLock()
276 defer m.mu.RUnlock()
277
278 health := make(map[string]bool)
279 for domain, container := range m.containers {
280 running, err := m.runtime.IsContainerRunning(ctx, container.ID)
281 if err != nil {
282 log.Warn().Err(err).Str("domain", domain).Str("container", container.ID).Msg("Failed to check container health")
283 health[domain] = false
284 } else {
285 health[domain] = running
286 }
287 }
288
289 return health
290}
291
292// Runtime returns the underlying runtime interface
293func (m *Manager) Runtime() runtime.Runtime {
294 return m.runtime
295}