v2.1.0 0fb867d

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	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}

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 7

Tags 106

↑↓ moveenter openesc close