1package container
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/bnema/zerowrap"
9
10 "github.com/bnema/gordon/internal/boundaries/in"
11 "github.com/bnema/gordon/internal/domain"
12)
13
14// ImagePushedHandler handles image.pushed events.
15type ImagePushedHandler struct {
16 containerSvc in.ContainerService
17 configSvc in.ConfigService
18 ctx context.Context
19}
20
21// NewImagePushedHandler creates a new ImagePushedHandler.
22func NewImagePushedHandler(ctx context.Context, containerSvc in.ContainerService, configSvc in.ConfigService) *ImagePushedHandler {
23 return &ImagePushedHandler{
24 containerSvc: containerSvc,
25 configSvc: configSvc,
26 ctx: ctx,
27 }
28}
29
30// Handle handles an event.
31func (h *ImagePushedHandler) Handle(ctx context.Context, event domain.Event) error {
32 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
33 zerowrap.FieldLayer: "usecase",
34 zerowrap.FieldHandler: "ImagePushedHandler",
35 zerowrap.FieldEvent: string(event.Type),
36 "event_id": event.ID,
37 })
38 log := zerowrap.FromCtx(ctx)
39
40 if event.ImageName == "" {
41 return domain.ErrInvalidImageFormat
42 }
43
44 tag := event.Tag
45 if tag == "" {
46 tag = "latest"
47 }
48
49 fullImageName := fmt.Sprintf("%s:%s", event.ImageName, tag)
50
51 log.Info().Str("image", fullImageName).Msg("processing image push event")
52
53 routes := h.findRoutesForImage(fullImageName)
54 if len(routes) == 0 {
55 log.Debug().Str("image", fullImageName).Msg("no routes configured for pushed image")
56 return nil
57 }
58
59 // Mark context as internal deploy - the event originated from our own registry,
60 // so we can use internal registry auth when pulling images.
61 internalCtx := domain.WithInternalDeploy(ctx)
62
63 for _, route := range routes {
64 if _, err := h.containerSvc.Deploy(internalCtx, route); err != nil {
65 log.WrapErrWithFields(err, "failed to deploy container for route", map[string]any{"domain": route.Domain})
66 }
67 }
68
69 return nil
70}
71
72// CanHandle returns whether this handler can handle the given event type.
73func (h *ImagePushedHandler) CanHandle(eventType domain.EventType) bool {
74 return eventType == domain.EventImagePushed
75}
76
77func (h *ImagePushedHandler) findRoutesForImage(imageName string) []domain.Route {
78 var routes []domain.Route
79
80 registryDomain := h.configSvc.GetRegistryDomain()
81 normalizedImage := normalizeRegistryImage(imageName, registryDomain)
82
83 configuredRoutes := h.configSvc.GetRoutes(h.ctx)
84 for _, route := range configuredRoutes {
85 normalizedRouteImage := normalizeRegistryImage(route.Image, registryDomain)
86 if strings.EqualFold(normalizedRouteImage, normalizedImage) {
87 routes = append(routes, route)
88 }
89 }
90
91 return routes
92}
93
94func normalizeRegistryImage(imageName, registryDomain string) string {
95 registryDomain = strings.TrimSuffix(registryDomain, "/")
96 if registryDomain == "" {
97 return imageName
98 }
99
100 prefix := registryDomain + "/"
101 if strings.HasPrefix(imageName, prefix) {
102 return strings.TrimPrefix(imageName, prefix)
103 }
104
105 return imageName
106}
107
108// ConfigReloadHandler handles config.reload events.
109type ConfigReloadHandler struct {
110 containerSvc in.ContainerService
111 configSvc in.ConfigService
112 ctx context.Context
113}
114
115// NewConfigReloadHandler creates a new ConfigReloadHandler.
116func NewConfigReloadHandler(ctx context.Context, containerSvc in.ContainerService, configSvc in.ConfigService) *ConfigReloadHandler {
117 return &ConfigReloadHandler{
118 containerSvc: containerSvc,
119 configSvc: configSvc,
120 ctx: ctx,
121 }
122}
123
124// Handle handles an event.
125func (h *ConfigReloadHandler) Handle(ctx context.Context, event domain.Event) error {
126 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
127 zerowrap.FieldLayer: "usecase",
128 zerowrap.FieldHandler: "ConfigReloadHandler",
129 zerowrap.FieldEvent: string(event.Type),
130 "event_id": event.ID,
131 })
132 log := zerowrap.FromCtx(ctx)
133
134 log.Info().Msg("processing configuration reload event")
135
136 // Sync containers first to ensure we have accurate state
137 // This removes tracking for containers that were stopped externally
138 if err := h.containerSvc.SyncContainers(ctx); err != nil {
139 log.Warn().Err(err).Msg("failed to sync containers before reload, proceeding with current state")
140 }
141
142 currentContainers := h.containerSvc.List(ctx)
143
144 activeRoutes := make(map[string]*domain.Container)
145 for _, container := range currentContainers {
146 if route, exists := container.Labels["gordon.route"]; exists {
147 activeRoutes[route] = container
148 }
149 }
150
151 routes := h.configSvc.GetRoutes(ctx)
152 for _, route := range routes {
153 if container, exists := activeRoutes[route.Domain]; exists {
154 currentImage := container.Labels["gordon.image"]
155 if currentImage != route.Image {
156 log.Info().
157 Str("domain", route.Domain).
158 Str("old_image", currentImage).
159 Str("new_image", route.Image).
160 Msg("image changed for route, redeploying")
161
162 if _, err := h.containerSvc.Deploy(domain.WithInternalDeploy(ctx), route); err != nil {
163 log.WrapErrWithFields(err, "failed to redeploy container", map[string]any{"domain": route.Domain})
164 }
165 }
166 delete(activeRoutes, route.Domain)
167 } else {
168 // Route exists in config but no running container (new route or missing container)
169 log.Info().
170 Str("domain", route.Domain).
171 Str("image", route.Image).
172 Msg("route missing container, deploying")
173
174 if _, err := h.containerSvc.Deploy(domain.WithInternalDeploy(ctx), route); err != nil {
175 log.WrapErrWithFields(err, "failed to deploy container for route", map[string]any{"domain": route.Domain})
176 }
177 }
178 }
179
180 for route, container := range activeRoutes {
181 log.Info().
182 Str("domain", route).
183 Str(zerowrap.FieldEntityID, container.ID).
184 Msg("route no longer configured, stopping container")
185
186 if err := h.containerSvc.Stop(ctx, container.ID); err != nil {
187 log.WrapErrWithFields(err, "failed to stop container", map[string]any{zerowrap.FieldEntityID: container.ID})
188 }
189
190 if err := h.containerSvc.Remove(ctx, container.ID, true); err != nil {
191 log.WrapErrWithFields(err, "failed to remove container", map[string]any{zerowrap.FieldEntityID: container.ID})
192 }
193 }
194
195 return nil
196}
197
198// CanHandle returns whether this handler can handle the given event type.
199func (h *ConfigReloadHandler) CanHandle(eventType domain.EventType) bool {
200 return eventType == domain.EventConfigReload
201}
202
203// ManualReloadHandler handles manual.reload events.
204type ManualReloadHandler struct {
205 containerSvc in.ContainerService
206 configSvc in.ConfigService
207 ctx context.Context
208}
209
210// NewManualReloadHandler creates a new ManualReloadHandler.
211func NewManualReloadHandler(ctx context.Context, containerSvc in.ContainerService, configSvc in.ConfigService) *ManualReloadHandler {
212 return &ManualReloadHandler{
213 containerSvc: containerSvc,
214 configSvc: configSvc,
215 ctx: ctx,
216 }
217}
218
219// Handle handles an event.
220// It starts containers for configured routes that don't have a running container.
221// Running containers are NEVER restarted to ensure 100% uptime.
222func (h *ManualReloadHandler) Handle(ctx context.Context, event domain.Event) error {
223 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
224 zerowrap.FieldLayer: "usecase",
225 zerowrap.FieldHandler: "ManualReloadHandler",
226 zerowrap.FieldEvent: string(event.Type),
227 "event_id": event.ID,
228 })
229 log := zerowrap.FromCtx(ctx)
230
231 log.Info().Msg("processing manual reload event - starting missing containers")
232
233 // Sync containers first to ensure we have accurate state
234 // This removes tracking for containers that were stopped externally
235 if err := h.containerSvc.SyncContainers(ctx); err != nil {
236 log.Warn().Err(err).Msg("failed to sync containers before reload, proceeding with current state")
237 }
238
239 routes := h.configSvc.GetRoutes(ctx)
240 startedCount := 0
241 skippedCount := 0
242 errorCount := 0
243
244 for _, route := range routes {
245 if _, exists := h.containerSvc.Get(ctx, route.Domain); exists {
246 log.Debug().Str("domain", route.Domain).Msg("container already running, skipping")
247 skippedCount++
248 continue
249 }
250
251 log.Info().
252 Str("domain", route.Domain).
253 Str("image", route.Image).
254 Msg("starting container for route")
255
256 if _, err := h.containerSvc.Deploy(domain.WithInternalDeploy(ctx), route); err != nil {
257 log.WrapErrWithFields(err, "failed to start container", map[string]any{"domain": route.Domain})
258 errorCount++
259 continue
260 }
261
262 startedCount++
263 log.Info().Str("domain", route.Domain).Msg("container started successfully")
264 }
265
266 if errorCount > 0 {
267 log.Warn().
268 Int("started", startedCount).
269 Int("skipped", skippedCount).
270 Int("errors", errorCount).
271 Msg("manual reload completed with some errors")
272 return fmt.Errorf("manual reload completed with %d errors", errorCount)
273 }
274
275 log.Info().
276 Int("started", startedCount).
277 Int("skipped", skippedCount).
278 Int("total", len(routes)).
279 Msg("manual reload completed successfully")
280 return nil
281}
282
283// CanHandle returns whether this handler can handle the given event type.
284func (h *ManualReloadHandler) CanHandle(eventType domain.EventType) bool {
285 return eventType == domain.EventManualReload
286}
287
288// ManualDeployHandler handles manual.deploy events for specific routes.
289type ManualDeployHandler struct {
290 containerSvc in.ContainerService
291 configSvc in.ConfigService
292 ctx context.Context
293}
294
295// NewManualDeployHandler creates a new ManualDeployHandler.
296func NewManualDeployHandler(ctx context.Context, containerSvc in.ContainerService, configSvc in.ConfigService) *ManualDeployHandler {
297 return &ManualDeployHandler{
298 containerSvc: containerSvc,
299 configSvc: configSvc,
300 ctx: ctx,
301 }
302}
303
304// Handle handles a manual deploy event for a specific route.
305func (h *ManualDeployHandler) Handle(ctx context.Context, event domain.Event) error {
306 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
307 zerowrap.FieldLayer: "usecase",
308 zerowrap.FieldHandler: "ManualDeployHandler",
309 zerowrap.FieldEvent: string(event.Type),
310 "event_id": event.ID,
311 })
312 log := zerowrap.FromCtx(ctx)
313
314 payload, ok := event.Data.(*domain.ManualDeployPayload)
315 if !ok || payload == nil || payload.Domain == "" {
316 return fmt.Errorf("invalid manual deploy payload")
317 }
318
319 log.Info().Str("domain", payload.Domain).Msg("processing manual deploy event")
320
321 // Find the route in configuration
322 routes := h.configSvc.GetRoutes(ctx)
323 var targetRoute *domain.Route
324 for _, r := range routes {
325 if r.Domain == payload.Domain {
326 targetRoute = &r
327 break
328 }
329 }
330
331 if targetRoute == nil {
332 return fmt.Errorf("route not found for domain: %s", payload.Domain)
333 }
334
335 // Deploy the container (this will replace any existing container)
336 if _, err := h.containerSvc.Deploy(ctx, *targetRoute); err != nil {
337 return log.WrapErr(err, "failed to deploy container")
338 }
339
340 log.Info().Str("domain", payload.Domain).Msg("manual deploy completed successfully")
341 return nil
342}
343
344// CanHandle returns whether this handler can handle the given event type.
345func (h *ManualDeployHandler) CanHandle(eventType domain.EventType) bool {
346 return eventType == domain.EventManualDeploy
347}