v2.30.0-pre6 511705f

events.go RawHistoryFolder

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	// Propagate updated attachment configuration so new attachments take effect
111	// without requiring a Gordon restart (fixes issue #87 part 2).
112	attachments := h.configSvc.GetAllAttachments(ctx)
113	log.Debug().Int("attachment_groups", len(attachments)).Msg("propagating updated attachment configuration")
114	h.containerSvc.UpdateAttachments(attachments)
115	log.Debug().Int("attachment_groups", len(attachments)).Msg("attachment configuration propagated")
116
117	currentContainers := h.containerSvc.List(ctx)
118
119	activeRoutes := make(map[string]*domain.Container)
120	for _, container := range currentContainers {
121		if route, exists := container.Labels[domain.LabelRoute]; exists {
122			activeRoutes[route] = container
123		}
124	}
125
126	routes := h.configSvc.GetRoutes(ctx)
127	for _, route := range routes {
128		if container, exists := activeRoutes[route.Domain]; exists {
129			currentImage := container.Labels[domain.LabelImage]
130			if currentImage != route.Image {
131				log.Info().
132					Str("domain", route.Domain).
133					Str("old_image", currentImage).
134					Str("new_image", route.Image).
135					Msg("image changed for route, redeploying")
136
137				if _, err := h.containerSvc.Deploy(domain.WithInternalDeploy(ctx), route); err != nil {
138					log.WrapErrWithFields(err, "failed to redeploy container", map[string]any{"domain": route.Domain})
139				}
140			}
141			delete(activeRoutes, route.Domain)
142		} else {
143			// Route exists in config but no running container (new route or missing container)
144			log.Info().
145				Str("domain", route.Domain).
146				Str("image", route.Image).
147				Msg("route missing container, deploying")
148
149			if _, err := h.containerSvc.Deploy(domain.WithInternalDeploy(ctx), route); err != nil {
150				log.WrapErrWithFields(err, "failed to deploy container for route", map[string]any{"domain": route.Domain})
151			}
152		}
153	}
154
155	for route, container := range activeRoutes {
156		log.Info().
157			Str("domain", route).
158			Str(zerowrap.FieldEntityID, container.ID).
159			Msg("route no longer configured, stopping container")
160
161		if err := h.containerSvc.Stop(ctx, container.ID); err != nil {
162			log.WrapErrWithFields(err, "failed to stop container", map[string]any{zerowrap.FieldEntityID: container.ID})
163		}
164
165		if err := h.containerSvc.Remove(ctx, container.ID, true); err != nil {
166			log.WrapErrWithFields(err, "failed to remove container", map[string]any{zerowrap.FieldEntityID: container.ID})
167		}
168	}
169
170	return nil
171}
172
173// CanHandle returns whether this handler can handle the given event type.
174func (h *ConfigReloadHandler) CanHandle(eventType domain.EventType) bool {
175	return eventType == domain.EventConfigReload
176}
177
178// ManualReloadHandler handles manual.reload events.
179type ManualReloadHandler struct {
180	containerSvc in.ContainerService
181	configSvc    in.ConfigService
182	ctx          context.Context
183}
184
185// NewManualReloadHandler creates a new ManualReloadHandler.
186func NewManualReloadHandler(ctx context.Context, containerSvc in.ContainerService, configSvc in.ConfigService) *ManualReloadHandler {
187	return &ManualReloadHandler{
188		containerSvc: containerSvc,
189		configSvc:    configSvc,
190		ctx:          ctx,
191	}
192}
193
194// Handle handles an event.
195// It starts containers for configured routes that don't have a running container.
196// Running containers are NEVER restarted to ensure 100% uptime.
197func (h *ManualReloadHandler) Handle(ctx context.Context, event domain.Event) error {
198	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
199		zerowrap.FieldLayer:   "usecase",
200		zerowrap.FieldHandler: "ManualReloadHandler",
201		zerowrap.FieldEvent:   string(event.Type),
202		"event_id":            event.ID,
203	})
204	log := zerowrap.FromCtx(ctx)
205
206	log.Info().Msg("processing manual reload event - starting missing containers")
207
208	// Sync containers first to ensure we have accurate state
209	// This removes tracking for containers that were stopped externally
210	if err := h.containerSvc.SyncContainers(ctx); err != nil {
211		log.Warn().Err(err).Msg("failed to sync containers before reload, proceeding with current state")
212	}
213
214	routes := h.configSvc.GetRoutes(ctx)
215	startedCount := 0
216	skippedCount := 0
217	errorCount := 0
218
219	for _, route := range routes {
220		if _, exists := h.containerSvc.Get(ctx, route.Domain); exists {
221			log.Debug().Str("domain", route.Domain).Msg("container already running, skipping")
222			skippedCount++
223			continue
224		}
225
226		log.Info().
227			Str("domain", route.Domain).
228			Str("image", route.Image).
229			Msg("starting container for route")
230
231		if _, err := h.containerSvc.Deploy(domain.WithInternalDeploy(ctx), route); err != nil {
232			log.WrapErrWithFields(err, "failed to start container", map[string]any{"domain": route.Domain})
233			errorCount++
234			continue
235		}
236
237		startedCount++
238		log.Info().Str("domain", route.Domain).Msg("container started successfully")
239	}
240
241	if errorCount > 0 {
242		log.Warn().
243			Int("started", startedCount).
244			Int("skipped", skippedCount).
245			Int("errors", errorCount).
246			Msg("manual reload completed with some errors")
247		return fmt.Errorf("manual reload completed with %d errors", errorCount)
248	}
249
250	log.Info().
251		Int("started", startedCount).
252		Int("skipped", skippedCount).
253		Int("total", len(routes)).
254		Msg("manual reload completed successfully")
255	return nil
256}
257
258// CanHandle returns whether this handler can handle the given event type.
259func (h *ManualReloadHandler) CanHandle(eventType domain.EventType) bool {
260	return eventType == domain.EventManualReload
261}
262
263// ManualDeployHandler handles manual.deploy events for specific routes.
264type ManualDeployHandler struct {
265	containerSvc in.ContainerService
266	configSvc    in.ConfigService
267	ctx          context.Context
268}
269
270// NewManualDeployHandler creates a new ManualDeployHandler.
271func NewManualDeployHandler(ctx context.Context, containerSvc in.ContainerService, configSvc in.ConfigService) *ManualDeployHandler {
272	return &ManualDeployHandler{
273		containerSvc: containerSvc,
274		configSvc:    configSvc,
275		ctx:          ctx,
276	}
277}
278
279// Handle handles a manual deploy event for a specific route.
280func (h *ManualDeployHandler) Handle(ctx context.Context, event domain.Event) error {
281	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
282		zerowrap.FieldLayer:   "usecase",
283		zerowrap.FieldHandler: "ManualDeployHandler",
284		zerowrap.FieldEvent:   string(event.Type),
285		"event_id":            event.ID,
286	})
287	log := zerowrap.FromCtx(ctx)
288
289	payload, ok := event.Data.(*domain.ManualDeployPayload)
290	if !ok || payload == nil || payload.Domain == "" {
291		return fmt.Errorf("invalid manual deploy payload")
292	}
293
294	log.Info().Str("domain", payload.Domain).Msg("processing manual deploy event")
295
296	// Find the route in configuration
297	routes := h.configSvc.GetRoutes(ctx)
298	var targetRoute *domain.Route
299	for _, r := range routes {
300		if r.Domain == payload.Domain {
301			targetRoute = &r
302			break
303		}
304	}
305
306	if targetRoute == nil {
307		return fmt.Errorf("route not found for domain: %s", payload.Domain)
308	}
309
310	// Manual deploy is an internal trigger, so use internal deploy context.
311	if _, err := h.containerSvc.Deploy(domain.WithInternalDeploy(ctx), *targetRoute); err != nil {
312		return log.WrapErr(err, "failed to deploy container")
313	}
314
315	log.Info().Str("domain", payload.Domain).Msg("manual deploy completed successfully")
316	return nil
317}
318
319// CanHandle returns whether this handler can handle the given event type.
320func (h *ManualDeployHandler) CanHandle(eventType domain.EventType) bool {
321	return eventType == domain.EventManualDeploy
322}

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