v2.2.7 aefe19e

events.go RawHistoryFolder

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	// 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(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(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(event domain.Event) error {
223	ctx := zerowrap.CtxWithFields(h.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(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(event domain.Event) error {
306	ctx := zerowrap.CtxWithFields(h.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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 106

↑↓ moveenter openesc close