1package container
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/bnema/zerowrap"
9
10 "gordon/internal/boundaries/in"
11 "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(event domain.Event) error {
32 ctx := zerowrap.CtxWithFields(h.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(event domain.Event) error {
126 ctx := zerowrap.CtxWithFields(h.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 currentContainers := h.containerSvc.List(ctx)
137
138 activeRoutes := make(map[string]*domain.Container)
139 for _, container := range currentContainers {
140 if route, exists := container.Labels["gordon.route"]; exists {
141 activeRoutes[route] = container
142 }
143 }
144
145 routes := h.configSvc.GetRoutes(ctx)
146 for _, route := range routes {
147 if container, exists := activeRoutes[route.Domain]; exists {
148 currentImage := container.Labels["gordon.image"]
149 if currentImage != route.Image {
150 log.Info().
151 Str("domain", route.Domain).
152 Str("old_image", currentImage).
153 Str("new_image", route.Image).
154 Msg("image changed for route, redeploying")
155
156 if _, err := h.containerSvc.Deploy(ctx, route); err != nil {
157 log.WrapErrWithFields(err, "failed to redeploy container", map[string]any{"domain": route.Domain})
158 }
159 }
160 delete(activeRoutes, route.Domain)
161 } else {
162 log.Info().
163 Str("domain", route.Domain).
164 Str("image", route.Image).
165 Msg("new route detected, deploying container")
166
167 if _, err := h.containerSvc.Deploy(ctx, route); err != nil {
168 log.WrapErrWithFields(err, "failed to deploy container for new route", map[string]any{"domain": route.Domain})
169 }
170 }
171 }
172
173 for route, container := range activeRoutes {
174 log.Info().
175 Str("domain", route).
176 Str(zerowrap.FieldEntityID, container.ID).
177 Msg("route no longer configured, stopping container")
178
179 if err := h.containerSvc.Stop(ctx, container.ID); err != nil {
180 log.WrapErrWithFields(err, "failed to stop container", map[string]any{zerowrap.FieldEntityID: container.ID})
181 }
182
183 if err := h.containerSvc.Remove(ctx, container.ID, true); err != nil {
184 log.WrapErrWithFields(err, "failed to remove container", map[string]any{zerowrap.FieldEntityID: container.ID})
185 }
186 }
187
188 return nil
189}
190
191// CanHandle returns whether this handler can handle the given event type.
192func (h *ConfigReloadHandler) CanHandle(eventType domain.EventType) bool {
193 return eventType == domain.EventConfigReload
194}
195
196// ManualReloadHandler handles manual.reload events.
197type ManualReloadHandler struct {
198 containerSvc in.ContainerService
199 configSvc in.ConfigService
200 ctx context.Context
201}
202
203// NewManualReloadHandler creates a new ManualReloadHandler.
204func NewManualReloadHandler(ctx context.Context, containerSvc in.ContainerService, configSvc in.ConfigService) *ManualReloadHandler {
205 return &ManualReloadHandler{
206 containerSvc: containerSvc,
207 configSvc: configSvc,
208 ctx: ctx,
209 }
210}
211
212// Handle handles an event.
213func (h *ManualReloadHandler) Handle(event domain.Event) error {
214 ctx := zerowrap.CtxWithFields(h.ctx, map[string]any{
215 zerowrap.FieldLayer: "usecase",
216 zerowrap.FieldHandler: "ManualReloadHandler",
217 zerowrap.FieldEvent: string(event.Type),
218 "event_id": event.ID,
219 })
220 log := zerowrap.FromCtx(ctx)
221
222 log.Info().Msg("processing manual reload event - redeploying all containers")
223
224 routes := h.configSvc.GetRoutes(ctx)
225 redeployedCount := 0
226 errorCount := 0
227
228 for _, route := range routes {
229 if _, exists := h.containerSvc.Get(ctx, route.Domain); !exists {
230 log.Debug().Str("domain", route.Domain).Msg("no container found for route, skipping")
231 continue
232 }
233
234 log.Info().
235 Str("domain", route.Domain).
236 Str("image", route.Image).
237 Msg("redeploying container with updated environment")
238
239 if _, err := h.containerSvc.Deploy(ctx, route); err != nil {
240 log.WrapErrWithFields(err, "failed to redeploy container", map[string]any{"domain": route.Domain})
241 errorCount++
242 continue
243 }
244
245 redeployedCount++
246 log.Info().Str("domain", route.Domain).Msg("container redeployed successfully")
247 }
248
249 if errorCount > 0 {
250 log.Warn().
251 Int("redeployed", redeployedCount).
252 Int("errors", errorCount).
253 Msg("manual reload completed with some errors")
254 return fmt.Errorf("manual reload completed with %d errors", errorCount)
255 }
256
257 log.Info().
258 Int("redeployed", redeployedCount).
259 Int("total", len(routes)).
260 Msg("manual reload completed successfully")
261 return nil
262}
263
264// CanHandle returns whether this handler can handle the given event type.
265func (h *ManualReloadHandler) CanHandle(eventType domain.EventType) bool {
266 return eventType == domain.EventManualReload
267}