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/manifest"
12)
13
14// VersionHandler handles version deployments based on manifest annotations
15type VersionHandler struct {
16 manager container.ManagerInterface
17 config *config.Config
18}
19
20// NewVersionHandler creates a new version handler
21func NewVersionHandler(manager container.ManagerInterface, config *config.Config) *VersionHandler {
22 return &VersionHandler{
23 manager: manager,
24 config: config,
25 }
26}
27
28// CanHandle returns true for ImagePushed events
29func (h *VersionHandler) CanHandle(eventType EventType) bool {
30 return eventType == ImagePushed
31}
32
33// Handle processes ImagePushed events and performs version deployments if necessary
34func (h *VersionHandler) Handle(event Event) error {
35 if event.Type != ImagePushed {
36 return nil
37 }
38
39 payload, ok := event.Data.(ImagePushedPayload)
40 if !ok {
41 return fmt.Errorf("invalid payload type for ImagePushed event")
42 }
43
44 // Skip if no annotations
45 if len(payload.Annotations) == 0 {
46 return nil
47 }
48
49 imageName := payload.Name
50 reference := payload.Reference
51 annotations := payload.Annotations
52
53 log.Debug().
54 Str("image", imageName).
55 Str("reference", reference).
56 Interface("annotations", annotations).
57 Msg("Processing annotations for version deployment")
58
59 // Check if this is a versioned deployment
60 if manifest.IsVersionedDeployment(annotations) {
61 return h.handleVersionedDeployment(imageName, reference, annotations)
62 }
63
64 return nil
65}
66
67// handleVersionedDeployment handles a versioned deployment
68func (h *VersionHandler) handleVersionedDeployment(imageName, reference string, annotations map[string]string) error {
69 version := manifest.GetDeploymentVersion(annotations)
70 if version == "" {
71 return nil
72 }
73
74 log.Info().
75 Str("image", imageName).
76 Str("reference", reference).
77 Str("version", version).
78 Msg("Deploying version via manifest annotation")
79
80 // Deploy the versioned image - treat this as a deployment to the specified version
81 return h.performVersionedDeployment(imageName, reference, version, annotations)
82}
83
84// performVersionedDeployment deploys a specific version based on manifest annotations
85func (h *VersionHandler) performVersionedDeployment(imageName, reference, version string, annotations map[string]string) error {
86 ctx := context.Background()
87
88 // Find routes that use this image
89 routes := h.findRoutesForImage(fmt.Sprintf("%s:%s", imageName, reference))
90 if len(routes) == 0 {
91 log.Debug().
92 Str("image", imageName).
93 Str("reference", reference).
94 Msg("No routes configured for versioned deployment image")
95 return nil
96 }
97
98 var errors []error
99 for _, route := range routes {
100 if err := h.deployVersion(ctx, route, imageName, version, annotations); err != nil {
101 log.Error().
102 Err(err).
103 Str("route", route).
104 Str("version", version).
105 Msg("Failed to deploy version to route")
106 errors = append(errors, fmt.Errorf("version deployment failed for route %s: %w", route, err))
107 } else {
108 log.Info().
109 Str("route", route).
110 Str("version", version).
111 Msg("Version deployment completed successfully")
112 }
113 }
114
115 if len(errors) > 0 {
116 return fmt.Errorf("version deployment completed with %d errors: %v", len(errors), errors)
117 }
118
119 return nil
120}
121
122
123// findRoutesForImage finds all routes that match the given image
124func (h *VersionHandler) findRoutesForImage(imageName string) []string {
125 var routes []string
126
127 for route, configuredImage := range h.config.Routes {
128 if strings.EqualFold(configuredImage, imageName) {
129 routes = append(routes, route)
130 }
131 }
132
133 return routes
134}
135
136// deployVersion deploys a specific version to a route
137func (h *VersionHandler) deployVersion(ctx context.Context, route, imageName, version string, annotations map[string]string) error {
138 // Construct the versioned image name
139 var versionedImage string
140
141 if strings.Contains(version, ":") {
142 // Full image reference provided
143 versionedImage = version
144 } else {
145 // Just a version/tag provided, construct full image reference
146 versionedImage = fmt.Sprintf("%s:%s", imageName, version)
147 }
148
149 log.Info().
150 Str("route", route).
151 Str("current_image", h.config.Routes[route]).
152 Str("versioned_image", versionedImage).
153 Msg("Performing version deployment")
154
155 // Update the route configuration to the versioned image
156 if err := h.config.UpdateRoute(route, versionedImage); err != nil {
157 return fmt.Errorf("failed to update route configuration for version deployment: %w", err)
158 }
159
160 // Deploy the versioned image
161 routeConfig := config.Route{
162 Domain: route,
163 Image: versionedImage,
164 HTTPS: true,
165 }
166
167 _, err := h.manager.DeployContainer(ctx, routeConfig)
168 if err != nil {
169 // Attempt to restore original configuration
170 if originalImage := h.config.Routes[route]; originalImage != versionedImage {
171 log.Warn().
172 Str("route", route).
173 Str("original_image", originalImage).
174 Msg("Version deployment failed, attempting to restore original configuration")
175 _ = h.config.UpdateRoute(route, originalImage)
176 }
177 return fmt.Errorf("failed to deploy versioned container: %w", err)
178 }
179
180 // Log successful deployment
181 log.Info().
182 Str("route", route).
183 Str("versioned_image", versionedImage).
184 Interface("version_annotations", annotations).
185 Msg("Version deployment completed successfully")
186
187 return nil
188}