1package preview
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/bnema/zerowrap"
9
10 "github.com/bnema/gordon/internal/boundaries/out"
11 "github.com/bnema/gordon/internal/domain"
12 "github.com/bnema/gordon/internal/usecase/auto"
13)
14
15// AutoPreviewHandler processes EventImagePushed for preview tag patterns.
16type AutoPreviewHandler struct {
17 serviceCtx context.Context
18 config auto.AutoConfigProvider
19 blobStorage out.BlobStorage
20 previewService *Service
21}
22
23func NewAutoPreviewHandler(
24 serviceCtx context.Context,
25 config auto.AutoConfigProvider,
26 blobStorage out.BlobStorage,
27 previewService *Service,
28) *AutoPreviewHandler {
29 return &AutoPreviewHandler{
30 serviceCtx: serviceCtx,
31 config: config,
32 blobStorage: blobStorage,
33 previewService: previewService,
34 }
35}
36
37func (h *AutoPreviewHandler) CanHandle(t domain.EventType) bool {
38 return t == domain.EventImagePushed
39}
40
41func (h *AutoPreviewHandler) Handle(ctx context.Context, event domain.Event) error {
42 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
43 zerowrap.FieldLayer: "usecase",
44 zerowrap.FieldHandler: "AutoPreviewHandler",
45 zerowrap.FieldEvent: string(event.Type),
46 "event_id": event.ID,
47 })
48 log := zerowrap.FromCtx(ctx)
49
50 payload, ok := event.Data.(domain.ImagePushedPayload)
51 if !ok {
52 log.Debug().Str("actual_type", fmt.Sprintf("%T", event.Data)).Msg("unexpected event data type, skipping")
53 return nil
54 }
55
56 // Extract labels from manifest
57 labels, err := auto.ExtractLabels(ctx, payload.Manifest, h.blobStorage)
58 if err != nil {
59 log.Debug().Err(err).Msg("failed to extract labels, skipping preview handler")
60 return nil
61 }
62
63 previewConfig := h.config.GetPreviewConfig()
64 previewName := ExtractPreviewName(payload.Reference, previewConfig.TagPatterns)
65
66 // Resolve base route: labels first, then route config fallback.
67 // When falling back to routes, skip preview domains (they contain the separator)
68 // to avoid using a preview as the base for another preview.
69 domains := collectDomains(labels)
70 if len(domains) == 0 {
71 routes := h.config.FindRoutesByImage(ctx, payload.Name)
72 for _, r := range routes {
73 if !strings.Contains(r.Domain, previewConfig.Separator) {
74 domains = append(domains, r.Domain)
75 }
76 }
77 }
78 if len(domains) == 0 {
79 log.Debug().Str("image", payload.Name).Msg("no domain found from labels or routes, skipping preview")
80 return nil
81 }
82 baseRouteDomain := domains[0]
83
84 // Validate allowlist
85 allowedDomains := h.config.GetAllowedDomains()
86 previewDomain, err := GeneratePreviewDomain(baseRouteDomain, previewName, previewConfig.Separator)
87 if err != nil {
88 log.Debug().Err(err).Str("base_domain", baseRouteDomain).Msg("failed to generate preview domain")
89 return nil
90 }
91 if !auto.MatchesDomainAllowlist(previewDomain, allowedDomains) {
92 log.Debug().Str("domain", previewDomain).Strs("patterns", allowedDomains).Msg("preview domain not in allowlist, skipping")
93 return nil
94 }
95
96 imageName := payload.Name + ":" + payload.Reference
97
98 // Launch async — volume cloning may exceed event bus timeout
99 go func() {
100 if err := h.previewService.CreatePreview(h.serviceCtx, CreatePreviewRequest{
101 Name: previewName,
102 Domain: previewDomain,
103 BaseRoute: baseRouteDomain,
104 Image: imageName,
105 HTTPS: true,
106 PreviewConfig: previewConfig,
107 }); err != nil {
108 log.Error().Err(err).Str("preview", previewName).Msg("failed to create preview")
109 }
110 }()
111
112 return nil
113}
114
115func collectDomains(labels *domain.ImageLabels) []string {
116 var domains []string
117 if labels.Domain != "" {
118 domains = append(domains, labels.Domain)
119 }
120 domains = append(domains, labels.Domains...)
121 return domains
122}