v2.31.0-dev9 062dcae

handler.go RawHistoryFolder

1package preview
2
3import (
4	"context"
5	"fmt"
6	"strings"
7
8	"github.com/bnema/zerowrap"
9
10	"github.com/bnema/gordon/internal/domain"
11	"github.com/bnema/gordon/internal/usecase/auto"
12)
13
14// AutoPreviewHandler processes EventImagePushed for preview tag patterns.
15type AutoPreviewHandler struct {
16	serviceCtx     context.Context
17	config         auto.AutoConfigProvider
18	previewService *Service
19}
20
21func NewAutoPreviewHandler(
22	serviceCtx context.Context,
23	config auto.AutoConfigProvider,
24	previewService *Service,
25) *AutoPreviewHandler {
26	return &AutoPreviewHandler{
27		serviceCtx:     serviceCtx,
28		config:         config,
29		previewService: previewService,
30	}
31}
32
33func (h *AutoPreviewHandler) CanHandle(t domain.EventType) bool {
34	return t == domain.EventImagePushed
35}
36
37func (h *AutoPreviewHandler) Handle(ctx context.Context, event domain.Event) error {
38	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
39		zerowrap.FieldLayer:   "usecase",
40		zerowrap.FieldHandler: "AutoPreviewHandler",
41		zerowrap.FieldEvent:   string(event.Type),
42		"event_id":            event.ID,
43	})
44	log := zerowrap.FromCtx(ctx)
45
46	payload, ok := event.Data.(domain.ImagePushedPayload)
47	if !ok {
48		log.Debug().Str("actual_type", fmt.Sprintf("%T", event.Data)).Msg("unexpected event data type, skipping")
49		return nil
50	}
51
52	previewConfig := h.config.GetPreviewConfig()
53	previewName := ExtractPreviewName(payload.Reference, previewConfig.TagPatterns)
54
55	routes := h.config.FindRoutesByImage(ctx, payload.Name)
56	baseRoutes := resolveBaseRoutes(routes, previewConfig)
57	if len(baseRoutes) == 0 {
58		log.Debug().Str("image", payload.Name).Msg("no trusted route found for preview base, skipping")
59		return nil
60	}
61
62	allowedDomains := h.config.GetAllowedDomains()
63
64	imageName := payload.Name + ":" + payload.Reference
65	for _, baseRoute := range baseRoutes {
66		previewDomain, err := GeneratePreviewDomain(baseRoute.Domain, previewName, previewConfig.Separator)
67		if err != nil {
68			log.Debug().Err(err).Str("base_domain", baseRoute.Domain).Msg("failed to generate preview domain")
69			continue
70		}
71		if !auto.MatchesDomainAllowlist(previewDomain, allowedDomains) {
72			log.Debug().Str("domain", previewDomain).Strs("patterns", allowedDomains).Msg("preview domain not in allowlist, skipping")
73			continue
74		}
75
76		// Launch async — volume cloning may exceed event bus timeout
77		go func(baseRoute baseRouteInfo, previewDomain string) {
78			if err := h.previewService.CreatePreview(h.serviceCtx, CreatePreviewRequest{
79				Name:          previewName,
80				Domain:        previewDomain,
81				BaseRoute:     baseRoute.Domain,
82				Image:         imageName,
83				HTTPS:         baseRoute.HTTPS,
84				PreviewConfig: previewConfig,
85			}); err != nil {
86				log.Error().Err(err).Str("preview", previewName).Str("base_route", baseRoute.Domain).Msg("failed to create preview")
87			}
88		}(baseRoute, previewDomain)
89	}
90
91	return nil
92}
93
94// resolveBaseRoutes determines the eligible base routes for preview env/data inheritance.
95type baseRouteInfo struct {
96	Domain string
97	HTTPS  bool
98}
99
100func resolveBaseRoutes(routes []domain.Route, previewConfig domain.PreviewConfig) []baseRouteInfo {
101	domainSet := make(map[string]struct{}, len(routes))
102	for _, r := range routes {
103		domainSet[r.Domain] = struct{}{}
104	}
105
106	baseRoutes := make([]baseRouteInfo, 0, len(routes))
107	for _, r := range routes {
108		if !isPreviewDomain(r.Domain, previewConfig.Separator, domainSet) {
109			baseRoutes = append(baseRoutes, baseRouteInfo{Domain: r.Domain, HTTPS: r.HTTPS})
110		}
111	}
112	return baseRoutes
113}
114
115// isPreviewDomain checks if a domain is a preview domain by examining the first
116// DNS label for the separator AND verifying that the implied base domain exists
117// in the route set. This avoids false positives like "my--app.example.com" when
118// no route "my.example.com" exists — that domain just happens to contain the
119// separator in its name.
120// If separator is empty, no domain is treated as a preview domain.
121func isPreviewDomain(d, separator string, routeSet map[string]struct{}) bool {
122	if separator == "" {
123		return false
124	}
125	// Extract the first label (subdomain) before the first dot
126	firstLabel := d
127	rest := ""
128	if idx := strings.Index(d, "."); idx != -1 {
129		firstLabel = d[:idx]
130		rest = d[idx:] // includes leading dot
131	}
132	// Check if the first label contains the separator
133	sepIdx := strings.Index(firstLabel, separator)
134	if sepIdx < 0 {
135		return false
136	}
137	// Reconstruct the potential base domain: everything before the separator + rest
138	baseLabel := firstLabel[:sepIdx]
139	if baseLabel == "" {
140		return false
141	}
142	baseDomain := baseLabel + rest
143	// Only treat as preview if the base domain actually exists in the route set
144	_, exists := routeSet[baseDomain]
145	return exists
146}

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