1package events
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/rs/zerolog/log"
9 "gordon/internal/config"
10 "gordon/internal/container"
11 "gordon/pkg/runtime"
12)
13
14type ContainerEventHandler struct {
15 manager container.ManagerInterface
16 config *config.Config
17}
18
19func NewContainerEventHandler(manager container.ManagerInterface, config *config.Config) *ContainerEventHandler {
20 return &ContainerEventHandler{
21 manager: manager,
22 config: config,
23 }
24}
25
26func (h *ContainerEventHandler) CanHandle(eventType EventType) bool {
27 switch eventType {
28 case ImagePushed, ConfigReload, ManualReload, ContainerStop, ContainerStart:
29 return true
30 default:
31 return false
32 }
33}
34
35func (h *ContainerEventHandler) Handle(event Event) error {
36 switch event.Type {
37 case ImagePushed:
38 return h.handleImagePushed(event)
39 case ConfigReload:
40 return h.handleConfigReload(event)
41 case ManualReload:
42 return h.handleManualReload(event)
43 case ContainerStop:
44 return h.handleContainerStop(event)
45 case ContainerStart:
46 return h.handleContainerStart(event)
47 default:
48 return fmt.Errorf("unsupported event type: %s", event.Type)
49 }
50}
51
52func (h *ContainerEventHandler) handleImagePushed(event Event) error {
53 imageName := event.ImageName
54 tag := event.Tag
55
56 if imageName == "" {
57 return fmt.Errorf("image name is required for ImagePushed event")
58 }
59
60 if tag == "" {
61 tag = "latest"
62 }
63
64 fullImageName := fmt.Sprintf("%s:%s", imageName, tag)
65
66 log.Info().
67 Str("image", fullImageName).
68 Str("event_id", event.ID).
69 Msg("Processing image push event")
70
71 routes := h.findRoutesForImage(fullImageName)
72 if len(routes) == 0 {
73 log.Debug().
74 Str("image", fullImageName).
75 Msg("No routes configured for pushed image")
76 return nil
77 }
78
79 for _, route := range routes {
80 if err := h.deployContainerForRoute(route, fullImageName); err != nil {
81 log.Error().
82 Err(err).
83 Str("route", route).
84 Str("image", fullImageName).
85 Msg("Failed to deploy container for route")
86 }
87 }
88
89 return nil
90}
91
92func (h *ContainerEventHandler) handleConfigReload(event Event) error {
93 log.Info().
94 Str("event_id", event.ID).
95 Msg("Processing configuration reload event")
96
97 ctx := context.Background()
98
99 // Get current containers managed by Gordon
100 currentContainers := h.manager.ListContainers()
101
102 // Track which routes are currently active
103 activeRoutes := make(map[string]*runtime.Container)
104 for _, container := range currentContainers {
105 if route, exists := container.Labels["gordon.route"]; exists {
106 activeRoutes[route] = container
107 }
108 }
109
110 // Process each configured route
111 for route, imageName := range h.config.Routes {
112 if container, exists := activeRoutes[route]; exists {
113 // Route exists - check if image has changed
114 currentImage := container.Labels["gordon.image"]
115 if currentImage != imageName {
116 log.Info().
117 Str("route", route).
118 Str("old_image", currentImage).
119 Str("new_image", imageName).
120 Msg("Image changed for route, redeploying")
121
122 if err := h.deployContainerForRoute(route, imageName); err != nil {
123 log.Error().
124 Err(err).
125 Str("route", route).
126 Msg("Failed to redeploy container for route")
127 }
128 }
129 // Remove from active routes so we know it's been processed
130 delete(activeRoutes, route)
131 } else {
132 // New route - deploy container
133 log.Info().
134 Str("route", route).
135 Str("image", imageName).
136 Msg("New route detected, deploying container")
137
138 if err := h.deployContainerForRoute(route, imageName); err != nil {
139 log.Error().
140 Err(err).
141 Str("route", route).
142 Msg("Failed to deploy container for new route")
143 }
144 }
145 }
146
147 // Stop containers for routes that are no longer configured
148 for route, container := range activeRoutes {
149 log.Info().
150 Str("route", route).
151 Str("container_id", container.ID).
152 Msg("Route no longer configured, stopping container")
153
154 if err := h.manager.StopContainer(ctx, container.ID); err != nil {
155 log.Error().
156 Err(err).
157 Str("container_id", container.ID).
158 Msg("Failed to stop container for removed route")
159 }
160
161 if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil {
162 log.Error().
163 Err(err).
164 Str("container_id", container.ID).
165 Msg("Failed to remove container for removed route")
166 }
167 }
168
169 return nil
170}
171
172func (h *ContainerEventHandler) handleManualReload(event Event) error {
173 log.Info().
174 Str("event_id", event.ID).
175 Msg("Processing manual reload event - redeploying all containers with updated environment variables")
176
177 ctx := context.Background()
178 routes := h.config.GetRoutes()
179 redeployedCount := 0
180 var errors []error
181
182 // Redeploy each container to pick up new environment variables
183 for _, route := range routes {
184 // Check if container exists for this route
185 if _, exists := h.manager.GetContainer(route.Domain); !exists {
186 log.Debug().Str("domain", route.Domain).Msg("No container found for route, skipping")
187 continue
188 }
189
190 log.Info().Str("domain", route.Domain).Str("image", route.Image).Msg("Redeploying container with updated environment variables")
191
192 // Deploy container (this will stop the old one and start a new one with updated env vars)
193 _, err := h.manager.DeployContainer(ctx, route)
194 if err != nil {
195 log.Error().Str("domain", route.Domain).Err(err).Msg("Failed to redeploy container during manual reload")
196 errors = append(errors, fmt.Errorf("failed to redeploy container for %s: %w", route.Domain, err))
197 continue
198 }
199
200 redeployedCount++
201 log.Info().Str("domain", route.Domain).Msg("Container redeployed successfully with updated environment variables")
202 }
203
204 if len(errors) > 0 {
205 log.Warn().Int("redeployed", redeployedCount).Int("errors", len(errors)).Msg("Manual reload completed with some errors")
206 return fmt.Errorf("manual reload completed with %d errors: %v", len(errors), errors)
207 }
208
209 log.Info().Int("redeployed", redeployedCount).Int("total", len(routes)).Msg("Manual reload completed successfully")
210 return nil
211}
212
213func (h *ContainerEventHandler) handleContainerStop(event Event) error {
214 containerID := event.ContainerID
215 if containerID == "" {
216 return fmt.Errorf("container ID is required for ContainerStop event")
217 }
218
219 log.Info().
220 Str("container_id", containerID).
221 Str("event_id", event.ID).
222 Msg("Processing container stop event")
223
224 ctx := context.Background()
225 if err := h.manager.StopContainer(ctx, containerID); err != nil {
226 return fmt.Errorf("failed to stop container %s: %w", containerID, err)
227 }
228
229 return nil
230}
231
232func (h *ContainerEventHandler) handleContainerStart(event Event) error {
233 containerID := event.ContainerID
234 if containerID == "" {
235 return fmt.Errorf("container ID is required for ContainerStart event")
236 }
237
238 log.Info().
239 Str("container_id", containerID).
240 Str("event_id", event.ID).
241 Msg("Processing container start event")
242
243 // StartContainer method doesn't exist on Manager - containers are started via DeployContainer
244 // For manual start, we'd need to add this method or use the runtime directly
245 return fmt.Errorf("container start not implemented - use automatic deployment instead")
246}
247
248func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string {
249 var routes []string
250
251 for route, configuredImage := range h.config.Routes {
252 if strings.EqualFold(configuredImage, imageName) {
253 routes = append(routes, route)
254 }
255 }
256
257 return routes
258}
259
260func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error {
261 ctx := context.Background()
262
263 containers := h.manager.ListContainers()
264
265 containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-"))
266
267 // Stop and remove existing container if it exists
268 for _, container := range containers {
269 if container.Name == containerName {
270 log.Info().
271 Str("container_name", containerName).
272 Str("old_container_id", container.ID).
273 Msg("Stopping existing container for route")
274
275 if err := h.manager.StopContainer(ctx, container.ID); err != nil {
276 log.Warn().
277 Err(err).
278 Str("container_id", container.ID).
279 Msg("Failed to stop existing container")
280 }
281
282 if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil {
283 log.Warn().
284 Err(err).
285 Str("container_id", container.ID).
286 Msg("Failed to remove existing container")
287 }
288 }
289 }
290
291 // Use DeployContainer which handles creation and starting
292 routeConfig := config.Route{
293 Domain: route,
294 Image: imageName,
295 HTTPS: true,
296 }
297
298 container, err := h.manager.DeployContainer(ctx, routeConfig)
299 if err != nil {
300 return fmt.Errorf("failed to deploy container for route %s: %w", route, err)
301 }
302
303 log.Info().
304 Str("route", route).
305 Str("image", imageName).
306 Str("container_id", container.ID).
307 Str("container_name", containerName).
308 Msg("Successfully deployed container for route")
309
310 return nil
311}
312
313type AutoRouteHandler struct {
314 config *config.Config
315 manager container.ManagerInterface
316}
317
318func NewAutoRouteHandler(cfg *config.Config, manager container.ManagerInterface) *AutoRouteHandler {
319 return &AutoRouteHandler{
320 config: cfg,
321 manager: manager,
322 }
323}
324
325func (h *AutoRouteHandler) CanHandle(eventType EventType) bool {
326 return eventType == ImagePushed
327}
328
329func (h *AutoRouteHandler) Handle(event Event) error {
330 if !h.config.AutoRoute.Enabled {
331 return nil
332 }
333
334 if event.Type != ImagePushed {
335 return nil
336 }
337
338 payload, ok := event.Data.(ImagePushedPayload)
339 if !ok {
340 return fmt.Errorf("invalid payload type for ImagePushed event")
341 }
342
343 // Log annotations if present
344 if len(payload.Annotations) > 0 {
345 log.Debug().
346 Str("image", payload.Name).
347 Str("reference", payload.Reference).
348 Interface("annotations", payload.Annotations).
349 Msg("Image pushed with annotations - auto-route processing")
350 }
351
352 imageName := payload.Name
353 reference := payload.Reference
354 fullImageName := fmt.Sprintf("%s:%s", imageName, reference)
355
356 // Build the full registry image path for deployment
357 registryImageName := fullImageName
358 if h.config.Server.RegistryDomain != "" {
359 registryImageName = fmt.Sprintf("%s/%s", h.config.Server.RegistryDomain, fullImageName)
360 }
361
362 // Try to extract domain from the image name part (before the colon)
363 domain, isDomain := config.ExtractDomainFromImageName(imageName)
364 if !isDomain {
365 log.Debug().
366 Str("image", imageName).
367 Msg("Image name does not contain a valid domain, skipping auto-route creation")
368 return nil
369 }
370
371 // Check if route already exists
372 if _, exists := h.config.Routes[domain]; exists {
373 log.Debug().
374 Str("domain", domain).
375 Str("existing_image", h.config.Routes[domain]).
376 Str("new_image", fullImageName).
377 Msg("Route already exists for domain, skipping auto-route creation")
378 return nil
379 }
380
381 // Add the route to config with the full registry image path
382 if err := h.config.AddRoute(domain, registryImageName); err != nil {
383 return fmt.Errorf("failed to add auto-route for domain %s: %w", domain, err)
384 }
385
386 log.Info().
387 Str("domain", domain).
388 Str("image", registryImageName).
389 Msg("Successfully auto-created route - deployment will be handled by config reload event")
390
391 return nil
392}
393