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.Manager
16 config *config.Config
17}
18
19func NewContainerEventHandler(manager *container.Manager, 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, 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 ContainerStop:
42 return h.handleContainerStop(event)
43 case ContainerStart:
44 return h.handleContainerStart(event)
45 default:
46 return fmt.Errorf("unsupported event type: %s", event.Type)
47 }
48}
49
50func (h *ContainerEventHandler) handleImagePushed(event Event) error {
51 imageName := event.ImageName
52 tag := event.Tag
53
54 if imageName == "" {
55 return fmt.Errorf("image name is required for ImagePushed event")
56 }
57
58 if tag == "" {
59 tag = "latest"
60 }
61
62 fullImageName := fmt.Sprintf("%s:%s", imageName, tag)
63
64 log.Info().
65 Str("image", fullImageName).
66 Str("event_id", event.ID).
67 Msg("Processing image push event")
68
69 routes := h.findRoutesForImage(fullImageName)
70 if len(routes) == 0 {
71 log.Debug().
72 Str("image", fullImageName).
73 Msg("No routes configured for pushed image")
74 return nil
75 }
76
77 for _, route := range routes {
78 if err := h.deployContainerForRoute(route, fullImageName); err != nil {
79 log.Error().
80 Err(err).
81 Str("route", route).
82 Str("image", fullImageName).
83 Msg("Failed to deploy container for route")
84 }
85 }
86
87 return nil
88}
89
90func (h *ContainerEventHandler) handleConfigReload(event Event) error {
91 log.Info().
92 Str("event_id", event.ID).
93 Msg("Processing configuration reload event")
94
95 ctx := context.Background()
96
97 // Get current containers managed by Gordon
98 currentContainers := h.manager.ListContainers()
99
100 // Track which routes are currently active
101 activeRoutes := make(map[string]*runtime.Container)
102 for _, container := range currentContainers {
103 if route, exists := container.Labels["gordon.route"]; exists {
104 activeRoutes[route] = container
105 }
106 }
107
108 // Process each configured route
109 for route, imageName := range h.config.Routes {
110 if container, exists := activeRoutes[route]; exists {
111 // Route exists - check if image has changed
112 currentImage := container.Labels["gordon.image"]
113 if currentImage != imageName {
114 log.Info().
115 Str("route", route).
116 Str("old_image", currentImage).
117 Str("new_image", imageName).
118 Msg("Image changed for route, redeploying")
119
120 if err := h.deployContainerForRoute(route, imageName); err != nil {
121 log.Error().
122 Err(err).
123 Str("route", route).
124 Msg("Failed to redeploy container for route")
125 }
126 }
127 // Remove from active routes so we know it's been processed
128 delete(activeRoutes, route)
129 } else {
130 // New route - deploy container
131 log.Info().
132 Str("route", route).
133 Str("image", imageName).
134 Msg("New route detected, deploying container")
135
136 if err := h.deployContainerForRoute(route, imageName); err != nil {
137 log.Error().
138 Err(err).
139 Str("route", route).
140 Msg("Failed to deploy container for new route")
141 }
142 }
143 }
144
145 // Stop containers for routes that are no longer configured
146 for route, container := range activeRoutes {
147 log.Info().
148 Str("route", route).
149 Str("container_id", container.ID).
150 Msg("Route no longer configured, stopping container")
151
152 if err := h.manager.StopContainer(ctx, container.ID); err != nil {
153 log.Error().
154 Err(err).
155 Str("container_id", container.ID).
156 Msg("Failed to stop container for removed route")
157 }
158
159 if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil {
160 log.Error().
161 Err(err).
162 Str("container_id", container.ID).
163 Msg("Failed to remove container for removed route")
164 }
165 }
166
167 return nil
168}
169
170func (h *ContainerEventHandler) handleContainerStop(event Event) error {
171 containerID := event.ContainerID
172 if containerID == "" {
173 return fmt.Errorf("container ID is required for ContainerStop event")
174 }
175
176 log.Info().
177 Str("container_id", containerID).
178 Str("event_id", event.ID).
179 Msg("Processing container stop event")
180
181 ctx := context.Background()
182 if err := h.manager.StopContainer(ctx, containerID); err != nil {
183 return fmt.Errorf("failed to stop container %s: %w", containerID, err)
184 }
185
186 return nil
187}
188
189func (h *ContainerEventHandler) handleContainerStart(event Event) error {
190 containerID := event.ContainerID
191 if containerID == "" {
192 return fmt.Errorf("container ID is required for ContainerStart event")
193 }
194
195 log.Info().
196 Str("container_id", containerID).
197 Str("event_id", event.ID).
198 Msg("Processing container start event")
199
200 // StartContainer method doesn't exist on Manager - containers are started via DeployContainer
201 // For manual start, we'd need to add this method or use the runtime directly
202 return fmt.Errorf("container start not implemented - use automatic deployment instead")
203}
204
205func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string {
206 var routes []string
207
208 for route, configuredImage := range h.config.Routes {
209 if strings.EqualFold(configuredImage, imageName) {
210 routes = append(routes, route)
211 }
212 }
213
214 return routes
215}
216
217func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error {
218 ctx := context.Background()
219
220 containers := h.manager.ListContainers()
221
222 containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-"))
223
224 // Stop and remove existing container if it exists
225 for _, container := range containers {
226 if container.Name == containerName {
227 log.Info().
228 Str("container_name", containerName).
229 Str("old_container_id", container.ID).
230 Msg("Stopping existing container for route")
231
232 if err := h.manager.StopContainer(ctx, container.ID); err != nil {
233 log.Warn().
234 Err(err).
235 Str("container_id", container.ID).
236 Msg("Failed to stop existing container")
237 }
238
239 if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil {
240 log.Warn().
241 Err(err).
242 Str("container_id", container.ID).
243 Msg("Failed to remove existing container")
244 }
245 }
246 }
247
248 // Use DeployContainer which handles creation and starting
249 routeConfig := config.Route{
250 Domain: route,
251 Image: imageName,
252 HTTPS: true,
253 }
254
255 container, err := h.manager.DeployContainer(ctx, routeConfig)
256 if err != nil {
257 return fmt.Errorf("failed to deploy container for route %s: %w", route, err)
258 }
259
260 log.Info().
261 Str("route", route).
262 Str("image", imageName).
263 Str("container_id", container.ID).
264 Str("container_name", containerName).
265 Msg("Successfully deployed container for route")
266
267 return nil
268}