2.0.0 4803e67

handlers.go RawHistoryFolder

1package events
2
3import (
4	"context"
5	"fmt"
6	"strings"
7
8	"github.com/rs/zerolog/log"
9	"gordon/internal/config"
10	"gordon/internal/container"
11	"gordon/pkg/runtime"
12)
13
14type ContainerEventHandler struct {
15	manager container.ManagerInterface
16	config  *config.Config
17}
18
19func NewContainerEventHandler(manager container.ManagerInterface, config *config.Config) *ContainerEventHandler {
20	return &ContainerEventHandler{
21		manager: manager,
22		config:  config,
23	}
24}
25
26func (h *ContainerEventHandler) CanHandle(eventType EventType) bool {
27	switch eventType {
28	case ImagePushed, ConfigReload, ManualReload, ContainerStop, ContainerStart:
29		return true
30	default:
31		return false
32	}
33}
34
35func (h *ContainerEventHandler) Handle(event Event) error {
36	switch event.Type {
37	case ImagePushed:
38		return h.handleImagePushed(event)
39	case ConfigReload:
40		return h.handleConfigReload(event)
41	case ManualReload:
42		return h.handleManualReload(event)
43	case ContainerStop:
44		return h.handleContainerStop(event)
45	case ContainerStart:
46		return h.handleContainerStart(event)
47	default:
48		return fmt.Errorf("unsupported event type: %s", event.Type)
49	}
50}
51
52func (h *ContainerEventHandler) handleImagePushed(event Event) error {
53	imageName := event.ImageName
54	tag := event.Tag
55	
56	if imageName == "" {
57		return fmt.Errorf("image name is required for ImagePushed event")
58	}
59	
60	if tag == "" {
61		tag = "latest"
62	}
63	
64	fullImageName := fmt.Sprintf("%s:%s", imageName, tag)
65	
66	log.Info().
67		Str("image", fullImageName).
68		Str("event_id", event.ID).
69		Msg("Processing image push event")
70	
71	routes := h.findRoutesForImage(fullImageName)
72	if len(routes) == 0 {
73		log.Debug().
74			Str("image", fullImageName).
75			Msg("No routes configured for pushed image")
76		return nil
77	}
78	
79	for _, route := range routes {
80		if err := h.deployContainerForRoute(route, fullImageName); err != nil {
81			log.Error().
82				Err(err).
83				Str("route", route).
84				Str("image", fullImageName).
85				Msg("Failed to deploy container for route")
86		}
87	}
88	
89	return nil
90}
91
92func (h *ContainerEventHandler) handleConfigReload(event Event) error {
93	log.Info().
94		Str("event_id", event.ID).
95		Msg("Processing configuration reload event")
96	
97	ctx := context.Background()
98	
99	// Get current containers managed by Gordon
100	currentContainers := h.manager.ListContainers()
101	
102	// Track which routes are currently active
103	activeRoutes := make(map[string]*runtime.Container)
104	for _, container := range currentContainers {
105		if route, exists := container.Labels["gordon.route"]; exists {
106			activeRoutes[route] = container
107		}
108	}
109	
110	// Process each configured route
111	for route, imageName := range h.config.Routes {
112		if container, exists := activeRoutes[route]; exists {
113			// Route exists - check if image has changed
114			currentImage := container.Labels["gordon.image"]
115			if currentImage != imageName {
116				log.Info().
117					Str("route", route).
118					Str("old_image", currentImage).
119					Str("new_image", imageName).
120					Msg("Image changed for route, redeploying")
121				
122				if err := h.deployContainerForRoute(route, imageName); err != nil {
123					log.Error().
124						Err(err).
125						Str("route", route).
126						Msg("Failed to redeploy container for route")
127				}
128			}
129			// Remove from active routes so we know it's been processed
130			delete(activeRoutes, route)
131		} else {
132			// New route - deploy container
133			log.Info().
134				Str("route", route).
135				Str("image", imageName).
136				Msg("New route detected, deploying container")
137			
138			if err := h.deployContainerForRoute(route, imageName); err != nil {
139				log.Error().
140					Err(err).
141					Str("route", route).
142					Msg("Failed to deploy container for new route")
143			}
144		}
145	}
146	
147	// Stop containers for routes that are no longer configured
148	for route, container := range activeRoutes {
149		log.Info().
150			Str("route", route).
151			Str("container_id", container.ID).
152			Msg("Route no longer configured, stopping container")
153		
154		if err := h.manager.StopContainer(ctx, container.ID); err != nil {
155			log.Error().
156				Err(err).
157				Str("container_id", container.ID).
158				Msg("Failed to stop container for removed route")
159		}
160		
161		if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil {
162			log.Error().
163				Err(err).
164				Str("container_id", container.ID).
165				Msg("Failed to remove container for removed route")
166		}
167	}
168	
169	return nil
170}
171
172func (h *ContainerEventHandler) handleManualReload(event Event) error {
173	log.Info().
174		Str("event_id", event.ID).
175		Msg("Processing manual reload event - redeploying all containers with updated environment variables")
176	
177	ctx := context.Background()
178	routes := h.config.GetRoutes()
179	redeployedCount := 0
180	var errors []error
181	
182	// Redeploy each container to pick up new environment variables
183	for _, route := range routes {
184		// Check if container exists for this route
185		if _, exists := h.manager.GetContainer(route.Domain); !exists {
186			log.Debug().Str("domain", route.Domain).Msg("No container found for route, skipping")
187			continue
188		}
189		
190		log.Info().Str("domain", route.Domain).Str("image", route.Image).Msg("Redeploying container with updated environment variables")
191		
192		// Deploy container (this will stop the old one and start a new one with updated env vars)
193		_, err := h.manager.DeployContainer(ctx, route)
194		if err != nil {
195			log.Error().Str("domain", route.Domain).Err(err).Msg("Failed to redeploy container during manual reload")
196			errors = append(errors, fmt.Errorf("failed to redeploy container for %s: %w", route.Domain, err))
197			continue
198		}
199		
200		redeployedCount++
201		log.Info().Str("domain", route.Domain).Msg("Container redeployed successfully with updated environment variables")
202	}
203	
204	if len(errors) > 0 {
205		log.Warn().Int("redeployed", redeployedCount).Int("errors", len(errors)).Msg("Manual reload completed with some errors")
206		return fmt.Errorf("manual reload completed with %d errors: %v", len(errors), errors)
207	}
208	
209	log.Info().Int("redeployed", redeployedCount).Int("total", len(routes)).Msg("Manual reload completed successfully")
210	return nil
211}
212
213func (h *ContainerEventHandler) handleContainerStop(event Event) error {
214	containerID := event.ContainerID
215	if containerID == "" {
216		return fmt.Errorf("container ID is required for ContainerStop event")
217	}
218	
219	log.Info().
220		Str("container_id", containerID).
221		Str("event_id", event.ID).
222		Msg("Processing container stop event")
223	
224	ctx := context.Background()
225	if err := h.manager.StopContainer(ctx, containerID); err != nil {
226		return fmt.Errorf("failed to stop container %s: %w", containerID, err)
227	}
228	
229	return nil
230}
231
232func (h *ContainerEventHandler) handleContainerStart(event Event) error {
233	containerID := event.ContainerID
234	if containerID == "" {
235		return fmt.Errorf("container ID is required for ContainerStart event")
236	}
237	
238	log.Info().
239		Str("container_id", containerID).
240		Str("event_id", event.ID).
241		Msg("Processing container start event")
242	
243	// StartContainer method doesn't exist on Manager - containers are started via DeployContainer
244	// For manual start, we'd need to add this method or use the runtime directly
245	return fmt.Errorf("container start not implemented - use automatic deployment instead")
246}
247
248func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string {
249	var routes []string
250	
251	for route, configuredImage := range h.config.Routes {
252		if strings.EqualFold(configuredImage, imageName) {
253			routes = append(routes, route)
254		}
255	}
256	
257	return routes
258}
259
260func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error {
261	ctx := context.Background()
262	
263	containers := h.manager.ListContainers()
264	
265	containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-"))
266	
267	// Stop and remove existing container if it exists
268	for _, container := range containers {
269		if container.Name == containerName {
270			log.Info().
271				Str("container_name", containerName).
272				Str("old_container_id", container.ID).
273				Msg("Stopping existing container for route")
274			
275			if err := h.manager.StopContainer(ctx, container.ID); err != nil {
276				log.Warn().
277					Err(err).
278					Str("container_id", container.ID).
279					Msg("Failed to stop existing container")
280			}
281			
282			if err := h.manager.RemoveContainer(ctx, container.ID, true); err != nil {
283				log.Warn().
284					Err(err).
285					Str("container_id", container.ID).
286					Msg("Failed to remove existing container")
287			}
288		}
289	}
290	
291	// Use DeployContainer which handles creation and starting
292	routeConfig := config.Route{
293		Domain: route,
294		Image:  imageName,
295		HTTPS:  true,
296	}
297	
298	container, err := h.manager.DeployContainer(ctx, routeConfig)
299	if err != nil {
300		return fmt.Errorf("failed to deploy container for route %s: %w", route, err)
301	}
302	
303	log.Info().
304		Str("route", route).
305		Str("image", imageName).
306		Str("container_id", container.ID).
307		Str("container_name", containerName).
308		Msg("Successfully deployed container for route")
309	
310	return nil
311}
312
313type AutoRouteHandler struct {
314	config  *config.Config
315	manager container.ManagerInterface
316}
317
318func NewAutoRouteHandler(cfg *config.Config, manager container.ManagerInterface) *AutoRouteHandler {
319	return &AutoRouteHandler{
320		config:  cfg,
321		manager: manager,
322	}
323}
324
325func (h *AutoRouteHandler) CanHandle(eventType EventType) bool {
326	return eventType == ImagePushed
327}
328
329func (h *AutoRouteHandler) Handle(event Event) error {
330	if !h.config.AutoRoute.Enabled {
331		return nil
332	}
333
334	if event.Type != ImagePushed {
335		return nil
336	}
337
338	payload, ok := event.Data.(ImagePushedPayload)
339	if !ok {
340		return fmt.Errorf("invalid payload type for ImagePushed event")
341	}
342
343	// Log annotations if present
344	if len(payload.Annotations) > 0 {
345		log.Debug().
346			Str("image", payload.Name).
347			Str("reference", payload.Reference).
348			Interface("annotations", payload.Annotations).
349			Msg("Image pushed with annotations - auto-route processing")
350	}
351
352	imageName := payload.Name
353	reference := payload.Reference
354	fullImageName := fmt.Sprintf("%s:%s", imageName, reference)
355	
356	// Build the full registry image path for deployment
357	registryImageName := fullImageName
358	if h.config.Server.RegistryDomain != "" {
359		registryImageName = fmt.Sprintf("%s/%s", h.config.Server.RegistryDomain, fullImageName)
360	}
361
362	// Try to extract domain from the image name part (before the colon)
363	domain, isDomain := config.ExtractDomainFromImageName(imageName)
364	if !isDomain {
365		log.Debug().
366			Str("image", imageName).
367			Msg("Image name does not contain a valid domain, skipping auto-route creation")
368		return nil
369	}
370
371	// Check if route already exists
372	if _, exists := h.config.Routes[domain]; exists {
373		log.Debug().
374			Str("domain", domain).
375			Str("existing_image", h.config.Routes[domain]).
376			Str("new_image", fullImageName).
377			Msg("Route already exists for domain, skipping auto-route creation")
378		return nil
379	}
380
381	// Add the route to config with the full registry image path
382	if err := h.config.AddRoute(domain, registryImageName); err != nil {
383		return fmt.Errorf("failed to add auto-route for domain %s: %w", domain, err)
384	}
385
386	log.Info().
387		Str("domain", domain).
388		Str("image", registryImageName).
389		Msg("Successfully auto-created route - deployment will be handled by config reload event")
390
391	return nil
392}
393

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