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