v2.31.0-dev17 bd898f0

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// ManualDeployHandler handles manual.deploy events for specific routes.
179type ManualDeployHandler struct {
180	containerSvc in.ContainerService
181	configSvc    in.ConfigService
182	ctx          context.Context
183}
184
185// NewManualDeployHandler creates a new ManualDeployHandler.
186func NewManualDeployHandler(ctx context.Context, containerSvc in.ContainerService, configSvc in.ConfigService) *ManualDeployHandler {
187	return &ManualDeployHandler{
188		containerSvc: containerSvc,
189		configSvc:    configSvc,
190		ctx:          ctx,
191	}
192}
193
194// Handle handles a manual deploy event for a specific route.
195func (h *ManualDeployHandler) Handle(ctx context.Context, event domain.Event) error {
196	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
197		zerowrap.FieldLayer:   "usecase",
198		zerowrap.FieldHandler: "ManualDeployHandler",
199		zerowrap.FieldEvent:   string(event.Type),
200		"event_id":            event.ID,
201	})
202	log := zerowrap.FromCtx(ctx)
203
204	payload, ok := event.Data.(*domain.ManualDeployPayload)
205	if !ok || payload == nil || payload.Domain == "" {
206		return fmt.Errorf("invalid manual deploy payload")
207	}
208
209	log.Info().Str("domain", payload.Domain).Msg("processing manual deploy event")
210
211	// Find the route in configuration
212	routes := h.configSvc.GetRoutes(ctx)
213	var targetRoute *domain.Route
214	for _, r := range routes {
215		if r.Domain == payload.Domain {
216			targetRoute = &r
217			break
218		}
219	}
220
221	if targetRoute == nil {
222		return fmt.Errorf("route not found for domain: %s", payload.Domain)
223	}
224
225	// Manual deploy is an internal trigger, so use internal deploy context.
226	if _, err := h.containerSvc.Deploy(domain.WithInternalDeploy(ctx), *targetRoute); err != nil {
227		return log.WrapErr(err, "failed to deploy container")
228	}
229
230	log.Info().Str("domain", payload.Domain).Msg("manual deploy completed successfully")
231	return nil
232}
233
234// CanHandle returns whether this handler can handle the given event type.
235func (h *ManualDeployHandler) CanHandle(eventType domain.EventType) bool {
236	return eventType == domain.EventManualDeploy
237}

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