v2.16.0 ebd2ab3

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

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