feat(config): Add route update functionality and enhance logging for manifest annotations
- Implement UpdateRoute method to modify existing route configurations while preserving formatting in the config file. - Enhance logging to include debug information for image pushes with annotations and info logs for manifest annotations discovered. - Update README and examples to reflect new version management and rollback strategies, including manifest annotation-based deployments.
- commit
- c7c4c6ae136bcb4dd28d461f20f08002a6da5641
- author
- bamen · · 2025-06-08 14:22 UTC
- parent
- 53b10ad
- signature
- unsigned
13 files · +741 -44
M README.md +70 -26
| @@ -41,7 +41,7 @@ podman push registry.mydomain.com/myapp:latest | |||
| 41 | 41 | - **Push-to-Deploy**: New images deploy instantly | |
| 42 | 42 | - **Multi-Domain Routing**: Unlimited apps with automatic HTTPS via Cloudflare | |
| 43 | 43 | - **Zero-Downtime Updates**: Graceful container swaps | |
| 44 | - | - **Auto-Route Creation**: Push `myapp.mydomain.com:latest` → route created automatically | |
| 44 | + | - **Auto-Route Creation** (Optional): Push `myapp.mydomain.com:latest` → route created automatically | |
| 45 | 45 | ||
| 46 | 46 | ## Quick Start (5 minutes) | |
| 47 | 47 | ||
| @@ -160,15 +160,54 @@ podman push registry.mydomain.com/myapp:latest | |||
| 160 | 160 | ``` | |
| 161 | 161 | ||
| 162 | 162 | ### Version Control & Rollbacks | |
| 163 | + | ||
| 164 | + | Gordon supports multiple rollback strategies. Choose the approach that fits your workflow: | |
| 165 | + | ||
| 166 | + | #### Method 1: Tag-Based Rollbacks (Simple) | |
| 163 | 167 | ```bash | |
| 164 | - | # Push versioned releases | |
| 168 | + | # Tag and push a specific version | |
| 169 | + | podman tag myapp:v1.0.1 registry.mydomain.com/myapp:v1.0.1 | |
| 165 | 170 | podman push registry.mydomain.com/myapp:v1.0.1 | |
| 166 | 171 | ||
| 167 | - | # Instant rollback in gordon.toml: | |
| 172 | + | # Tag and push latest (v1.0.2 in this example) | |
| 173 | + | podman tag myapp:v1.0.2 registry.mydomain.com/myapp:v1.0.2 && podman tag myapp:v1.0.2 registry.mydomain.com/myapp:latest | |
| 174 | + | podman push registry.mydomain.com/myapp:v1.0.2 && podman push registry.mydomain.com/myapp:latest | |
| 175 | + | ||
| 176 | + | # Rollback: Update gordon.toml and reload | |
| 168 | 177 | # From: "app.mydomain.com" = "myapp:latest" | |
| 169 | - | # To: "app.mydomain.com" = "myapp:v1.0.0" | |
| 178 | + | # To: "app.mydomain.com" = "myapp:v1.0.1" | |
| 179 | + | # Then: systemctl --user reload gordon | |
| 180 | + | ``` | |
| 181 | + | ||
| 182 | + | #### Method 2: Manifest Annotation Deployment (Advanced) | |
| 183 | + | ```bash | |
| 184 | + | # Build and push your versioned images first | |
| 185 | + | export VERSION=v1.0.1 | |
| 186 | + | podman build --tag myapp:$VERSION --tag registry.mydomain.com/myapp:$VERSION . | |
| 187 | + | podman push registry.mydomain.com/myapp:$VERSION | |
| 188 | + | ||
| 189 | + | export VERSION=v1.0.2 | |
| 190 | + | podman build --tag myapp:$VERSION --tag registry.mydomain.com/myapp:$VERSION . | |
| 191 | + | podman push registry.mydomain.com/myapp:$VERSION | |
| 192 | + | ||
| 193 | + | # Deploy v1.0.2 with version annotation | |
| 194 | + | export VERSION=v1.0.2 | |
| 195 | + | podman manifest create myapp:latest | |
| 196 | + | podman manifest add myapp:latest registry.mydomain.com/myapp:$VERSION | |
| 197 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.mydomain.com/myapp:$VERSION | |
| 198 | + | podman manifest push myapp:latest registry.mydomain.com/myapp:latest | |
| 199 | + | ||
| 200 | + | # Rollback to v1.0.1 (just change the version) | |
| 201 | + | export VERSION=v1.0.1 | |
| 202 | + | podman manifest create myapp:latest --amend | |
| 203 | + | podman manifest add myapp:latest registry.mydomain.com/myapp:$VERSION | |
| 204 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.mydomain.com/myapp:$VERSION | |
| 205 | + | podman manifest push myapp:latest registry.mydomain.com/myapp:latest | |
| 206 | + | # Gordon automatically deploys the specified version | |
| 170 | 207 | ``` | |
| 171 | 208 | ||
| 209 | + | **Recommendation**: Use Method 2 for instant deployments without config file edits. Method 1 is best for simple workflows. | |
| 210 | + | ||
| 172 | 211 | ### Auto-Route Creation | |
| 173 | 212 | ```toml | |
| 174 | 213 | [auto_route] | |
| @@ -183,30 +222,35 @@ podman push registry.mydomain.com/staging.example.com:latest | |||
| 183 | 222 | ||
| 184 | 223 | ## Advanced Configuration | |
| 185 | 224 | ||
| 186 | - | ### Full Config Structure | |
| 225 | + | ### Full Config Structure (with default values) | |
| 187 | 226 | ```toml | |
| 188 | 227 | # gordon.toml - Auto-generated on first run | |
| 189 | 228 | [server] | |
| 190 | - | port = 8080 | |
| 191 | - | registry_domain = "registry.mydomain.com" | |
| 192 | - | runtime = "podman-rootless" # auto, docker, podman, podman-rootless | |
| 229 | + | port = 8080 # Default: 8080 | |
| 230 | + | registry_port = 5000 # Default: 5000 | |
| 231 | + | registry_domain = "registry.mydomain.com" # No default (required) | |
| 232 | + | runtime = "auto" # Default: "auto" (auto, docker, podman, podman-rootless) | |
| 233 | + | socket_path = "" # Default: "" (auto-detected) | |
| 234 | + | data_dir = "~/.local/share/gordon" # Default: ~/.local/share/gordon (or ./data for root) | |
| 235 | + | ssl_email = "" # No default (optional) | |
| 193 | 236 | # runtime auto-detects: docker → podman-rootless → podman | |
| 194 | 237 | # Override with env: CONTAINER_HOST=unix:///custom/socket | |
| 195 | 238 | ||
| 196 | 239 | [registry_auth] | |
| 197 | - | enabled = true | |
| 198 | - | username = "admin" | |
| 199 | - | password = "your-secure-password" | |
| 240 | + | enabled = true # Default: true | |
| 241 | + | username = "admin" # No default (required when enabled) | |
| 242 | + | password = "your-secure-password" # No default (required when enabled) | |
| 200 | 243 | ||
| 201 | 244 | [routes] | |
| 202 | - | "app.mydomain.com" = "myapp:latest" | |
| 245 | + | "app.mydomain.com" = "myapp:latest" # Default: {} (empty map) | |
| 246 | + | # Routes default to HTTPS unless prefixed with http:// | |
| 203 | 247 | ||
| 204 | 248 | [auto_route] | |
| 205 | - | enabled = false # Auto-create routes from image names | |
| 249 | + | enabled = false # Default: false | |
| 206 | 250 | ||
| 207 | 251 | [env] | |
| 208 | - | dir = "./data/env" # Gordon auto-creates env files here | |
| 209 | - | providers = ["pass", "sops"] # Optional secret management | |
| 252 | + | dir = "{data_dir}/env" # Default: {data_dir}/env | |
| 253 | + | providers = ["pass", "sops"] # Default: ["pass", "sops"] | |
| 210 | 254 | # Example env file: ./data/env/app_mydomain_com.env | |
| 211 | 255 | # NODE_ENV=production | |
| 212 | 256 | # DATABASE_URL=postgresql://localhost:5432/prod | |
| @@ -214,16 +258,16 @@ providers = ["pass", "sops"] # Optional secret management | |||
| 214 | 258 | # JWT_SECRET=${sops:secrets.yaml:jwt} # SOPS integration | |
| 215 | 259 | ||
| 216 | 260 | [logging] | |
| 217 | - | enabled = false | |
| 218 | - | level = "info" # trace, debug, info, warn, error | |
| 219 | - | dir = "./logs" # Log directory | |
| 220 | - | main_log_file = "gordon.log" # Main app log | |
| 221 | - | proxy_log_file = "proxy.log" # HTTP traffic log | |
| 222 | - | container_log_dir = "containers" # Container logs subdirectory | |
| 223 | - | max_size = 100 # MB before rotation | |
| 224 | - | max_backups = 3 # Number of old files to keep | |
| 225 | - | max_age = 28 # Days to keep old files | |
| 226 | - | compress = true # Gzip rotated logs | |
| 261 | + | enabled = true # Default: true | |
| 262 | + | level = "info" # Default: "info" (debug, info, warn, error, fatal, panic) | |
| 263 | + | dir = "{data_dir}/logs" # Default: {data_dir}/logs | |
| 264 | + | main_log_file = "gordon.log" # Default: "gordon.log" | |
| 265 | + | proxy_log_file = "proxy.log" # Default: "proxy.log" | |
| 266 | + | container_log_dir = "containers" # Default: "containers" | |
| 267 | + | max_size = 100 # Default: 100 (MB before rotation) | |
| 268 | + | max_backups = 3 # Default: 3 (old files to keep) | |
| 269 | + | max_age = 28 # Default: 28 (days to keep old files) | |
| 270 | + | compress = true # Default: true (gzip rotated logs) | |
| 227 | 271 | # Creates: gordon.log, proxy.log, containers/*.log | |
| 228 | 272 | ``` | |
| 229 | 273 | ||
| @@ -236,7 +280,7 @@ A: Those are just reverse proxies. Gordon is a complete deployment platform with | |||
| 236 | 280 | A: On YOUR machine. If it runs locally, it runs in production. | |
| 237 | 281 | ||
| 238 | 282 | **Q: How do I rollback?** | |
| 239 | - | A: Edit gordon.toml to point to a previous version. Takes seconds. | |
| 283 | + | A: Change the version annotation in your manifest and push, or edit gordon.toml. Takes seconds. | |
| 240 | 284 | ||
| 241 | 285 | **Q: Do I need Cloudflare?** | |
| 242 | 286 | A: Yes, for SSL certificates and DDoS protection. | |
| @@ -41,7 +41,7 @@ podman push registry.mydomain.com/myapp:latest | |||||
| 41 | - **Push-to-Deploy**: New images deploy instantly | 41 | - **Push-to-Deploy**: New images deploy instantly | ||
| 42 | - **Multi-Domain Routing**: Unlimited apps with automatic HTTPS via Cloudflare | 42 | - **Multi-Domain Routing**: Unlimited apps with automatic HTTPS via Cloudflare | ||
| 43 | - **Zero-Downtime Updates**: Graceful container swaps | 43 | - **Zero-Downtime Updates**: Graceful container swaps | ||
| 44 | - | - **Auto-Route Creation**: Push `myapp.mydomain.com:latest` → route created automatically | 44 | + | - **Auto-Route Creation** (Optional): Push `myapp.mydomain.com:latest` → route created automatically |
| 45 | 45 | ||||
| 46 | ## Quick Start (5 minutes) | 46 | ## Quick Start (5 minutes) | ||
| 47 | 47 | ||||
| @@ -160,15 +160,54 @@ podman push registry.mydomain.com/myapp:latest | |||||
| 160 | ``` | 160 | ``` | ||
| 161 | 161 | ||||
| 162 | ### Version Control & Rollbacks | 162 | ### Version Control & Rollbacks | ||
| 163 | + | ||||
| 164 | + | Gordon supports multiple rollback strategies. Choose the approach that fits your workflow: | |||
| 165 | + | ||||
| 166 | + | #### Method 1: Tag-Based Rollbacks (Simple) | |||
| 163 | ```bash | 167 | ```bash | ||
| 164 | - | # Push versioned releases | 168 | + | # Tag and push a specific version |
| 169 | + | podman tag myapp:v1.0.1 registry.mydomain.com/myapp:v1.0.1 | |||
| 165 | podman push registry.mydomain.com/myapp:v1.0.1 | 170 | podman push registry.mydomain.com/myapp:v1.0.1 | ||
| 166 | 171 | ||||
| 167 | - | # Instant rollback in gordon.toml: | 172 | + | # Tag and push latest (v1.0.2 in this example) |
| 173 | + | podman tag myapp:v1.0.2 registry.mydomain.com/myapp:v1.0.2 && podman tag myapp:v1.0.2 registry.mydomain.com/myapp:latest | |||
| 174 | + | podman push registry.mydomain.com/myapp:v1.0.2 && podman push registry.mydomain.com/myapp:latest | |||
| 175 | + | ||||
| 176 | + | # Rollback: Update gordon.toml and reload | |||
| 168 | # From: "app.mydomain.com" = "myapp:latest" | 177 | # From: "app.mydomain.com" = "myapp:latest" | ||
| 169 | - | # To: "app.mydomain.com" = "myapp:v1.0.0" | 178 | + | # To: "app.mydomain.com" = "myapp:v1.0.1" |
| 179 | + | # Then: systemctl --user reload gordon | |||
| 180 | + | ``` | |||
| 181 | + | ||||
| 182 | + | #### Method 2: Manifest Annotation Deployment (Advanced) | |||
| 183 | + | ```bash | |||
| 184 | + | # Build and push your versioned images first | |||
| 185 | + | export VERSION=v1.0.1 | |||
| 186 | + | podman build --tag myapp:$VERSION --tag registry.mydomain.com/myapp:$VERSION . | |||
| 187 | + | podman push registry.mydomain.com/myapp:$VERSION | |||
| 188 | + | ||||
| 189 | + | export VERSION=v1.0.2 | |||
| 190 | + | podman build --tag myapp:$VERSION --tag registry.mydomain.com/myapp:$VERSION . | |||
| 191 | + | podman push registry.mydomain.com/myapp:$VERSION | |||
| 192 | + | ||||
| 193 | + | # Deploy v1.0.2 with version annotation | |||
| 194 | + | export VERSION=v1.0.2 | |||
| 195 | + | podman manifest create myapp:latest | |||
| 196 | + | podman manifest add myapp:latest registry.mydomain.com/myapp:$VERSION | |||
| 197 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.mydomain.com/myapp:$VERSION | |||
| 198 | + | podman manifest push myapp:latest registry.mydomain.com/myapp:latest | |||
| 199 | + | ||||
| 200 | + | # Rollback to v1.0.1 (just change the version) | |||
| 201 | + | export VERSION=v1.0.1 | |||
| 202 | + | podman manifest create myapp:latest --amend | |||
| 203 | + | podman manifest add myapp:latest registry.mydomain.com/myapp:$VERSION | |||
| 204 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.mydomain.com/myapp:$VERSION | |||
| 205 | + | podman manifest push myapp:latest registry.mydomain.com/myapp:latest | |||
| 206 | + | # Gordon automatically deploys the specified version | |||
| 170 | ``` | 207 | ``` | ||
| 171 | 208 | ||||
| 209 | + | **Recommendation**: Use Method 2 for instant deployments without config file edits. Method 1 is best for simple workflows. | |||
| 210 | + | ||||
| 172 | ### Auto-Route Creation | 211 | ### Auto-Route Creation | ||
| 173 | ```toml | 212 | ```toml | ||
| 174 | [auto_route] | 213 | [auto_route] | ||
| @@ -183,30 +222,35 @@ podman push registry.mydomain.com/staging.example.com:latest | |||||
| 183 | 222 | ||||
| 184 | ## Advanced Configuration | 223 | ## Advanced Configuration | ||
| 185 | 224 | ||||
| 186 | - | ### Full Config Structure | 225 | + | ### Full Config Structure (with default values) |
| 187 | ```toml | 226 | ```toml | ||
| 188 | # gordon.toml - Auto-generated on first run | 227 | # gordon.toml - Auto-generated on first run | ||
| 189 | [server] | 228 | [server] | ||
| 190 | - | port = 8080 | 229 | + | port = 8080 # Default: 8080 |
| 191 | - | registry_domain = "registry.mydomain.com" | 230 | + | registry_port = 5000 # Default: 5000 |
| 192 | - | runtime = "podman-rootless" # auto, docker, podman, podman-rootless | 231 | + | registry_domain = "registry.mydomain.com" # No default (required) |
| 232 | + | runtime = "auto" # Default: "auto" (auto, docker, podman, podman-rootless) | |||
| 233 | + | socket_path = "" # Default: "" (auto-detected) | |||
| 234 | + | data_dir = "~/.local/share/gordon" # Default: ~/.local/share/gordon (or ./data for root) | |||
| 235 | + | ssl_email = "" # No default (optional) | |||
| 193 | # runtime auto-detects: docker → podman-rootless → podman | 236 | # runtime auto-detects: docker → podman-rootless → podman | ||
| 194 | # Override with env: CONTAINER_HOST=unix:///custom/socket | 237 | # Override with env: CONTAINER_HOST=unix:///custom/socket | ||
| 195 | 238 | ||||
| 196 | [registry_auth] | 239 | [registry_auth] | ||
| 197 | - | enabled = true | 240 | + | enabled = true # Default: true |
| 198 | - | username = "admin" | 241 | + | username = "admin" # No default (required when enabled) |
| 199 | - | password = "your-secure-password" | 242 | + | password = "your-secure-password" # No default (required when enabled) |
| 200 | 243 | ||||
| 201 | [routes] | 244 | [routes] | ||
| 202 | - | "app.mydomain.com" = "myapp:latest" | 245 | + | "app.mydomain.com" = "myapp:latest" # Default: {} (empty map) |
| 246 | + | # Routes default to HTTPS unless prefixed with http:// | |||
| 203 | 247 | ||||
| 204 | [auto_route] | 248 | [auto_route] | ||
| 205 | - | enabled = false # Auto-create routes from image names | 249 | + | enabled = false # Default: false |
| 206 | 250 | ||||
| 207 | [env] | 251 | [env] | ||
| 208 | - | dir = "./data/env" # Gordon auto-creates env files here | 252 | + | dir = "{data_dir}/env" # Default: {data_dir}/env |
| 209 | - | providers = ["pass", "sops"] # Optional secret management | 253 | + | providers = ["pass", "sops"] # Default: ["pass", "sops"] |
| 210 | # Example env file: ./data/env/app_mydomain_com.env | 254 | # Example env file: ./data/env/app_mydomain_com.env | ||
| 211 | # NODE_ENV=production | 255 | # NODE_ENV=production | ||
| 212 | # DATABASE_URL=postgresql://localhost:5432/prod | 256 | # DATABASE_URL=postgresql://localhost:5432/prod | ||
| @@ -214,16 +258,16 @@ providers = ["pass", "sops"] # Optional secret management | |||||
| 214 | # JWT_SECRET=${sops:secrets.yaml:jwt} # SOPS integration | 258 | # JWT_SECRET=${sops:secrets.yaml:jwt} # SOPS integration | ||
| 215 | 259 | ||||
| 216 | [logging] | 260 | [logging] | ||
| 217 | - | enabled = false | 261 | + | enabled = true # Default: true |
| 218 | - | level = "info" # trace, debug, info, warn, error | 262 | + | level = "info" # Default: "info" (debug, info, warn, error, fatal, panic) |
| 219 | - | dir = "./logs" # Log directory | 263 | + | dir = "{data_dir}/logs" # Default: {data_dir}/logs |
| 220 | - | main_log_file = "gordon.log" # Main app log | 264 | + | main_log_file = "gordon.log" # Default: "gordon.log" |
| 221 | - | proxy_log_file = "proxy.log" # HTTP traffic log | 265 | + | proxy_log_file = "proxy.log" # Default: "proxy.log" |
| 222 | - | container_log_dir = "containers" # Container logs subdirectory | 266 | + | container_log_dir = "containers" # Default: "containers" |
| 223 | - | max_size = 100 # MB before rotation | 267 | + | max_size = 100 # Default: 100 (MB before rotation) |
| 224 | - | max_backups = 3 # Number of old files to keep | 268 | + | max_backups = 3 # Default: 3 (old files to keep) |
| 225 | - | max_age = 28 # Days to keep old files | 269 | + | max_age = 28 # Default: 28 (days to keep old files) |
| 226 | - | compress = true # Gzip rotated logs | 270 | + | compress = true # Default: true (gzip rotated logs) |
| 227 | # Creates: gordon.log, proxy.log, containers/*.log | 271 | # Creates: gordon.log, proxy.log, containers/*.log | ||
| 228 | ``` | 272 | ``` | ||
| 229 | 273 | ||||
| @@ -236,7 +280,7 @@ A: Those are just reverse proxies. Gordon is a complete deployment platform with | |||||
| 236 | A: On YOUR machine. If it runs locally, it runs in production. | 280 | A: On YOUR machine. If it runs locally, it runs in production. | ||
| 237 | 281 | ||||
| 238 | **Q: How do I rollback?** | 282 | **Q: How do I rollback?** | ||
| 239 | - | A: Edit gordon.toml to point to a previous version. Takes seconds. | 283 | + | A: Change the version annotation in your manifest and push, or edit gordon.toml. Takes seconds. |
| 240 | 284 | ||||
| 241 | **Q: Do I need Cloudflare?** | 285 | **Q: Do I need Cloudflare?** | ||
| 242 | A: Yes, for SSL certificates and DDoS protection. | 286 | A: Yes, for SSL certificates and DDoS protection. | ||
M cmd/start.go +17 -0
| @@ -87,6 +87,13 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 87 | 87 | log.Fatal().Err(err).Msg("Failed to subscribe auto-route event handler") | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | + | // Create and subscribe version event handler | |
| 91 | + | var versionHandler *events.VersionHandler | |
| 92 | + | versionHandler = events.NewVersionHandler(manager, cfg) | |
| 93 | + | if err := eventBus.Subscribe(versionHandler); err != nil { | |
| 94 | + | log.Fatal().Err(err).Msg("Failed to subscribe version event handler") | |
| 95 | + | } | |
| 96 | + | ||
| 90 | 97 | // Start proxy server (needs to be accessible for config reload) | |
| 91 | 98 | proxyServer := proxy.NewServer(cfg, manager) | |
| 92 | 99 | ||
| @@ -148,6 +155,9 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 148 | 155 | if err := eventBus.Unsubscribe(autoRouteHandler); err != nil { | |
| 149 | 156 | log.Warn().Err(err).Msg("Failed to unsubscribe old auto-route event handler") | |
| 150 | 157 | } | |
| 158 | + | if err := eventBus.Unsubscribe(versionHandler); err != nil { | |
| 159 | + | log.Warn().Err(err).Msg("Failed to unsubscribe old version event handler") | |
| 160 | + | } | |
| 151 | 161 | if err := eventBus.Unsubscribe(proxyHandler); err != nil { | |
| 152 | 162 | log.Warn().Err(err).Msg("Failed to unsubscribe old proxy event handler") | |
| 153 | 163 | } | |
| @@ -166,6 +176,13 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 166 | 176 | return | |
| 167 | 177 | } | |
| 168 | 178 | ||
| 179 | + | // Update version handler config | |
| 180 | + | versionHandler = events.NewVersionHandler(manager, cfg) | |
| 181 | + | if err := eventBus.Subscribe(versionHandler); err != nil { | |
| 182 | + | log.Error().Err(err).Msg("Failed to re-subscribe version event handler") | |
| 183 | + | return | |
| 184 | + | } | |
| 185 | + | ||
| 169 | 186 | // Re-subscribe proxy handler (config ref updated automatically) | |
| 170 | 187 | proxyHandler = proxy.NewProxyEventHandler(proxyServer) | |
| 171 | 188 | if err := eventBus.Subscribe(proxyHandler); err != nil { | |
| @@ -87,6 +87,13 @@ func runStart(cmd *cobra.Command, args []string) { | |||||
| 87 | log.Fatal().Err(err).Msg("Failed to subscribe auto-route event handler") | 87 | log.Fatal().Err(err).Msg("Failed to subscribe auto-route event handler") | ||
| 88 | } | 88 | } | ||
| 89 | 89 | ||||
| 90 | + | // Create and subscribe version event handler | |||
| 91 | + | var versionHandler *events.VersionHandler | |||
| 92 | + | versionHandler = events.NewVersionHandler(manager, cfg) | |||
| 93 | + | if err := eventBus.Subscribe(versionHandler); err != nil { | |||
| 94 | + | log.Fatal().Err(err).Msg("Failed to subscribe version event handler") | |||
| 95 | + | } | |||
| 96 | + | ||||
| 90 | // Start proxy server (needs to be accessible for config reload) | 97 | // Start proxy server (needs to be accessible for config reload) | ||
| 91 | proxyServer := proxy.NewServer(cfg, manager) | 98 | proxyServer := proxy.NewServer(cfg, manager) | ||
| 92 | 99 | ||||
| @@ -148,6 +155,9 @@ func runStart(cmd *cobra.Command, args []string) { | |||||
| 148 | if err := eventBus.Unsubscribe(autoRouteHandler); err != nil { | 155 | if err := eventBus.Unsubscribe(autoRouteHandler); err != nil { | ||
| 149 | log.Warn().Err(err).Msg("Failed to unsubscribe old auto-route event handler") | 156 | log.Warn().Err(err).Msg("Failed to unsubscribe old auto-route event handler") | ||
| 150 | } | 157 | } | ||
| 158 | + | if err := eventBus.Unsubscribe(versionHandler); err != nil { | |||
| 159 | + | log.Warn().Err(err).Msg("Failed to unsubscribe old version event handler") | |||
| 160 | + | } | |||
| 151 | if err := eventBus.Unsubscribe(proxyHandler); err != nil { | 161 | if err := eventBus.Unsubscribe(proxyHandler); err != nil { | ||
| 152 | log.Warn().Err(err).Msg("Failed to unsubscribe old proxy event handler") | 162 | log.Warn().Err(err).Msg("Failed to unsubscribe old proxy event handler") | ||
| 153 | } | 163 | } | ||
| @@ -166,6 +176,13 @@ func runStart(cmd *cobra.Command, args []string) { | |||||
| 166 | return | 176 | return | ||
| 167 | } | 177 | } | ||
| 168 | 178 | ||||
| 179 | + | // Update version handler config | |||
| 180 | + | versionHandler = events.NewVersionHandler(manager, cfg) | |||
| 181 | + | if err := eventBus.Subscribe(versionHandler); err != nil { | |||
| 182 | + | log.Error().Err(err).Msg("Failed to re-subscribe version event handler") | |||
| 183 | + | return | |||
| 184 | + | } | |||
| 185 | + | ||||
| 169 | // Re-subscribe proxy handler (config ref updated automatically) | 186 | // Re-subscribe proxy handler (config ref updated automatically) | ||
| 170 | proxyHandler = proxy.NewProxyEventHandler(proxyServer) | 187 | proxyHandler = proxy.NewProxyEventHandler(proxyServer) | ||
| 171 | if err := eventBus.Subscribe(proxyHandler); err != nil { | 188 | if err := eventBus.Subscribe(proxyHandler); err != nil { | ||
M examples/README.md +40 -9
| @@ -46,6 +46,12 @@ This directory contains configuration examples for different use cases and envir | |||
| 46 | 46 | - Production and development logging strategies | |
| 47 | 47 | - Log rotation and monitoring examples | |
| 48 | 48 | ||
| 49 | + | ### 🔄 [`rollback-workflow-example.md`](rollback-workflow-example.md) | |
| 50 | + | **Simple version deployment and rollback workflow** | |
| 51 | + | - Manifest annotation-based deployments | |
| 52 | + | - Simple version changes for deployments and rollbacks | |
| 53 | + | - Version tracking and deployment strategies | |
| 54 | + | ||
| 49 | 55 | ## 🚀 Quick Start | |
| 50 | 56 | ||
| 51 | 57 | 1. **Choose an example** that matches your use case | |
| @@ -161,27 +167,52 @@ For the development example, add to `/etc/hosts`: | |||
| 161 | 167 | ### Simple Deployment | |
| 162 | 168 | ```bash | |
| 163 | 169 | # Build your app | |
| 164 | - | docker build -t myapp:latest . | |
| 165 | - | ||
| 166 | - | # Tag for registry | |
| 167 | - | docker tag myapp:latest registry.your-domain.com/myapp:latest | |
| 170 | + | export VERSION=latest | |
| 171 | + | docker build --tag myapp:$VERSION --tag registry.your-domain.com/myapp:$VERSION . | |
| 168 | 172 | ||
| 169 | 173 | # Push to deploy | |
| 170 | - | docker push registry.your-domain.com/myapp:latest | |
| 174 | + | docker push registry.your-domain.com/myapp:$VERSION | |
| 171 | 175 | ``` | |
| 172 | 176 | ||
| 173 | 177 | ### Version Management | |
| 174 | 178 | ```bash | |
| 175 | - | # Tag with version | |
| 176 | - | docker tag myapp:latest registry.your-domain.com/myapp:v1.2.0 | |
| 179 | + | # Build and push specific version | |
| 180 | + | export VERSION=v1.2.0 | |
| 181 | + | docker build --tag myapp:$VERSION --tag registry.your-domain.com/myapp:$VERSION . | |
| 182 | + | docker push registry.your-domain.com/myapp:$VERSION | |
| 177 | 183 | ||
| 178 | 184 | # Update config | |
| 179 | 185 | # "app.your-domain.com" = "myapp:v1.2.0" | |
| 186 | + | ``` | |
| 180 | 187 | ||
| 181 | - | # Push to deploy specific version | |
| 182 | - | docker push registry.your-domain.com/myapp:v1.2.0 | |
| 188 | + | ### Simple Version Deployment with Manifest Annotations | |
| 189 | + | ```bash | |
| 190 | + | # Build and push your versions first | |
| 191 | + | export VERSION=v1.1.0 | |
| 192 | + | podman build --tag myapp:$VERSION --tag registry.your-domain.com/myapp:$VERSION . | |
| 193 | + | podman push registry.your-domain.com/myapp:$VERSION | |
| 194 | + | ||
| 195 | + | export VERSION=v1.2.0 | |
| 196 | + | podman build --tag myapp:$VERSION --tag registry.your-domain.com/myapp:$VERSION . | |
| 197 | + | podman push registry.your-domain.com/myapp:$VERSION | |
| 198 | + | ||
| 199 | + | # Deploy v1.2.0 | |
| 200 | + | export VERSION=v1.2.0 | |
| 201 | + | podman manifest create myapp:latest | |
| 202 | + | podman manifest add myapp:latest registry.your-domain.com/myapp:$VERSION | |
| 203 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.your-domain.com/myapp:$VERSION | |
| 204 | + | podman manifest push myapp:latest registry.your-domain.com/myapp:latest | |
| 205 | + | ||
| 206 | + | # Rollback to v1.1.0 (just change the version) | |
| 207 | + | export VERSION=v1.1.0 | |
| 208 | + | podman manifest create myapp:latest --amend | |
| 209 | + | podman manifest add myapp:latest registry.your-domain.com/myapp:$VERSION | |
| 210 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.your-domain.com/myapp:$VERSION | |
| 211 | + | podman manifest push myapp:latest registry.your-domain.com/myapp:latest | |
| 183 | 212 | ``` | |
| 184 | 213 | ||
| 214 | + | See [`rollback-workflow-example.md`](rollback-workflow-example.md) for complete version deployment workflow documentation. | |
| 215 | + | ||
| 185 | 216 | ## 🤝 Need Help? | |
| 186 | 217 | ||
| 187 | 218 | - 📖 **Read the main [README](../README.md)** for detailed documentation | |
| @@ -46,6 +46,12 @@ This directory contains configuration examples for different use cases and envir | |||||
| 46 | - Production and development logging strategies | 46 | - Production and development logging strategies | ||
| 47 | - Log rotation and monitoring examples | 47 | - Log rotation and monitoring examples | ||
| 48 | 48 | ||||
| 49 | + | ### 🔄 [`rollback-workflow-example.md`](rollback-workflow-example.md) | |||
| 50 | + | **Simple version deployment and rollback workflow** | |||
| 51 | + | - Manifest annotation-based deployments | |||
| 52 | + | - Simple version changes for deployments and rollbacks | |||
| 53 | + | - Version tracking and deployment strategies | |||
| 54 | + | ||||
| 49 | ## 🚀 Quick Start | 55 | ## 🚀 Quick Start | ||
| 50 | 56 | ||||
| 51 | 1. **Choose an example** that matches your use case | 57 | 1. **Choose an example** that matches your use case | ||
| @@ -161,27 +167,52 @@ For the development example, add to `/etc/hosts`: | |||||
| 161 | ### Simple Deployment | 167 | ### Simple Deployment | ||
| 162 | ```bash | 168 | ```bash | ||
| 163 | # Build your app | 169 | # Build your app | ||
| 164 | - | docker build -t myapp:latest . | 170 | + | export VERSION=latest |
| 165 | - | 171 | + | docker build --tag myapp:$VERSION --tag registry.your-domain.com/myapp:$VERSION . | |
| 166 | - | # Tag for registry | |||
| 167 | - | docker tag myapp:latest registry.your-domain.com/myapp:latest | |||
| 168 | 172 | ||||
| 169 | # Push to deploy | 173 | # Push to deploy | ||
| 170 | - | docker push registry.your-domain.com/myapp:latest | 174 | + | docker push registry.your-domain.com/myapp:$VERSION |
| 171 | ``` | 175 | ``` | ||
| 172 | 176 | ||||
| 173 | ### Version Management | 177 | ### Version Management | ||
| 174 | ```bash | 178 | ```bash | ||
| 175 | - | # Tag with version | 179 | + | # Build and push specific version |
| 176 | - | docker tag myapp:latest registry.your-domain.com/myapp:v1.2.0 | 180 | + | export VERSION=v1.2.0 |
| 181 | + | docker build --tag myapp:$VERSION --tag registry.your-domain.com/myapp:$VERSION . | |||
| 182 | + | docker push registry.your-domain.com/myapp:$VERSION | |||
| 177 | 183 | ||||
| 178 | # Update config | 184 | # Update config | ||
| 179 | # "app.your-domain.com" = "myapp:v1.2.0" | 185 | # "app.your-domain.com" = "myapp:v1.2.0" | ||
| 186 | + | ``` | |||
| 180 | 187 | ||||
| 181 | - | # Push to deploy specific version | 188 | + | ### Simple Version Deployment with Manifest Annotations |
| 182 | - | docker push registry.your-domain.com/myapp:v1.2.0 | 189 | + | ```bash |
| 190 | + | # Build and push your versions first | |||
| 191 | + | export VERSION=v1.1.0 | |||
| 192 | + | podman build --tag myapp:$VERSION --tag registry.your-domain.com/myapp:$VERSION . | |||
| 193 | + | podman push registry.your-domain.com/myapp:$VERSION | |||
| 194 | + | ||||
| 195 | + | export VERSION=v1.2.0 | |||
| 196 | + | podman build --tag myapp:$VERSION --tag registry.your-domain.com/myapp:$VERSION . | |||
| 197 | + | podman push registry.your-domain.com/myapp:$VERSION | |||
| 198 | + | ||||
| 199 | + | # Deploy v1.2.0 | |||
| 200 | + | export VERSION=v1.2.0 | |||
| 201 | + | podman manifest create myapp:latest | |||
| 202 | + | podman manifest add myapp:latest registry.your-domain.com/myapp:$VERSION | |||
| 203 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.your-domain.com/myapp:$VERSION | |||
| 204 | + | podman manifest push myapp:latest registry.your-domain.com/myapp:latest | |||
| 205 | + | ||||
| 206 | + | # Rollback to v1.1.0 (just change the version) | |||
| 207 | + | export VERSION=v1.1.0 | |||
| 208 | + | podman manifest create myapp:latest --amend | |||
| 209 | + | podman manifest add myapp:latest registry.your-domain.com/myapp:$VERSION | |||
| 210 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.your-domain.com/myapp:$VERSION | |||
| 211 | + | podman manifest push myapp:latest registry.your-domain.com/myapp:latest | |||
| 183 | ``` | 212 | ``` | ||
| 184 | 213 | ||||
| 214 | + | See [`rollback-workflow-example.md`](rollback-workflow-example.md) for complete version deployment workflow documentation. | |||
| 215 | + | ||||
| 185 | ## 🤝 Need Help? | 216 | ## 🤝 Need Help? | ||
| 186 | 217 | ||||
| 187 | - 📖 **Read the main [README](../README.md)** for detailed documentation | 218 | - 📖 **Read the main [README](../README.md)** for detailed documentation | ||
A examples/rollback-workflow-example.md +192 -0
| @@ -0,0 +1,192 @@ | |||
| 1 | + | # Gordon Version Deployment Workflow Example | |
| 2 | + | ||
| 3 | + | This document demonstrates how to use manifest annotations for simple version deployments and rollbacks in Gordon. | |
| 4 | + | ||
| 5 | + | ## Prerequisites | |
| 6 | + | ||
| 7 | + | - Gordon server running with the registry and auto-route features enabled | |
| 8 | + | - Podman or Docker with OCI manifest support | |
| 9 | + | - A container image to deploy | |
| 10 | + | ||
| 11 | + | ## Basic Versioned Deployment | |
| 12 | + | ||
| 13 | + | ### 1. Build and Push Your Versioned Images | |
| 14 | + | ||
| 15 | + | ```bash | |
| 16 | + | # Build and push your application images with versions | |
| 17 | + | export VERSION=v1.0.1 | |
| 18 | + | podman build --tag myapp:$VERSION --tag registry.domain.com/myapp:$VERSION . | |
| 19 | + | podman push registry.domain.com/myapp:$VERSION | |
| 20 | + | ||
| 21 | + | export VERSION=v1.0.2 | |
| 22 | + | podman build --tag myapp:$VERSION --tag registry.domain.com/myapp:$VERSION . | |
| 23 | + | podman push registry.domain.com/myapp:$VERSION | |
| 24 | + | ``` | |
| 25 | + | ||
| 26 | + | ### 2. Create Manifest and Deploy | |
| 27 | + | ||
| 28 | + | ```bash | |
| 29 | + | # Deploy v1.0.1 with version annotation | |
| 30 | + | export VERSION=v1.0.1 | |
| 31 | + | podman manifest create myapp:latest | |
| 32 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |
| 33 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |
| 34 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |
| 35 | + | ``` | |
| 36 | + | ||
| 37 | + | ### 3. Gordon Processing | |
| 38 | + | ||
| 39 | + | When Gordon receives the manifest, it will: | |
| 40 | + | ||
| 41 | + | 1. Parse the manifest annotations from the JSON structure: | |
| 42 | + | ```json | |
| 43 | + | { | |
| 44 | + | "schemaVersion": 2, | |
| 45 | + | "mediaType": "application/vnd.oci.image.manifest.v1+json", | |
| 46 | + | "annotations": { | |
| 47 | + | "version": "v1.0.1" | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | ``` | |
| 51 | + | ||
| 52 | + | 2. Extract version information and deploy the specified version | |
| 53 | + | 3. Update routes to use the versioned image | |
| 54 | + | ||
| 55 | + | ## Simple Version Deployment and Rollback | |
| 56 | + | ||
| 57 | + | ### Deploying Different Versions | |
| 58 | + | ||
| 59 | + | ```bash | |
| 60 | + | # Deploy v1.0.2 | |
| 61 | + | export VERSION=v1.0.2 | |
| 62 | + | podman manifest create myapp:latest --amend | |
| 63 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |
| 64 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |
| 65 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |
| 66 | + | ||
| 67 | + | # Rollback to v1.0.1 (just change the version) | |
| 68 | + | export VERSION=v1.0.1 | |
| 69 | + | podman manifest create myapp:latest --amend | |
| 70 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |
| 71 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |
| 72 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |
| 73 | + | ||
| 74 | + | # Deploy v1.0.3 (assuming you built and pushed it first) | |
| 75 | + | export VERSION=v1.0.3 | |
| 76 | + | podman manifest create myapp:latest --amend | |
| 77 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |
| 78 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |
| 79 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |
| 80 | + | ``` | |
| 81 | + | ||
| 82 | + | ### Gordon Version Processing | |
| 83 | + | ||
| 84 | + | When Gordon detects version annotations, it will: | |
| 85 | + | ||
| 86 | + | 1. **Parse version annotations**: Extract the version from the `version` annotation | |
| 87 | + | 2. **Execute deployment**: Deploy the specified version to all matching routes | |
| 88 | + | 3. **Log the deployment** | |
| 89 | + | ||
| 90 | + | **Example log output**: | |
| 91 | + | ``` | |
| 92 | + | INFO Processing versioned deployment image=myapp reference=latest version=v1.0.1 | |
| 93 | + | INFO Performing version deployment route=myapp.domain.com versioned_image=myapp:v1.0.1 | |
| 94 | + | INFO Version deployment completed successfully route=myapp.domain.com version=v1.0.1 | |
| 95 | + | ``` | |
| 96 | + | ||
| 97 | + | ## Supported Annotation Keys | |
| 98 | + | ||
| 99 | + | ### Version Annotations | |
| 100 | + | - `version` - The version to deploy (required) | |
| 101 | + | ||
| 102 | + | ## Real-World Example | |
| 103 | + | ||
| 104 | + | ```bash | |
| 105 | + | # Deploy production v1.0.1 | |
| 106 | + | export VERSION=v1.0.1 | |
| 107 | + | podman manifest create myapp:latest --amend | |
| 108 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |
| 109 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |
| 110 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |
| 111 | + | ||
| 112 | + | # Deploy new version v1.0.2 | |
| 113 | + | export VERSION=v1.0.2 | |
| 114 | + | podman manifest create myapp:latest --amend | |
| 115 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |
| 116 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |
| 117 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |
| 118 | + | ||
| 119 | + | # Oh no! Bug found - rollback to v1.0.1 | |
| 120 | + | export VERSION=v1.0.1 | |
| 121 | + | podman manifest create myapp:latest --amend | |
| 122 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |
| 123 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |
| 124 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |
| 125 | + | ||
| 126 | + | # Fix deployed - move forward to v1.0.3 | |
| 127 | + | export VERSION=v1.0.3 | |
| 128 | + | podman manifest create myapp:latest --amend | |
| 129 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |
| 130 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |
| 131 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |
| 132 | + | ``` | |
| 133 | + | ||
| 134 | + | ## Configuration Requirements | |
| 135 | + | ||
| 136 | + | Ensure your Gordon configuration supports this workflow: | |
| 137 | + | ||
| 138 | + | ```toml | |
| 139 | + | [auto_route] | |
| 140 | + | enabled = true | |
| 141 | + | ||
| 142 | + | [registry_auth] | |
| 143 | + | enabled = true | |
| 144 | + | username = "gordon" | |
| 145 | + | password = "your-password" | |
| 146 | + | ||
| 147 | + | [server] | |
| 148 | + | registry_domain = "registry.domain.com" | |
| 149 | + | ``` | |
| 150 | + | ||
| 151 | + | ## Monitoring and Observability | |
| 152 | + | ||
| 153 | + | Gordon logs provide visibility into the version deployment process: | |
| 154 | + | ||
| 155 | + | - Manifest annotation parsing | |
| 156 | + | - Version detection and tracking | |
| 157 | + | - Deployment execution | |
| 158 | + | - Success/failure status | |
| 159 | + | ||
| 160 | + | Check Gordon logs for detailed version deployment information: | |
| 161 | + | ||
| 162 | + | ```bash | |
| 163 | + | # View Gordon logs for version operations | |
| 164 | + | journalctl -u gordon -f | grep -i version | |
| 165 | + | ||
| 166 | + | # Or check Docker/Podman logs if running in container | |
| 167 | + | docker logs gordon-container | grep -i version | |
| 168 | + | ``` | |
| 169 | + | ||
| 170 | + | ## Troubleshooting | |
| 171 | + | ||
| 172 | + | ### Common Issues | |
| 173 | + | ||
| 174 | + | 1. **Annotations not parsed**: Ensure you're using OCI manifest format, not Docker v2.2 | |
| 175 | + | 2. **Version target not found**: Verify the target version exists in your registry | |
| 176 | + | 3. **Route not found**: Ensure the image has configured routes in Gordon | |
| 177 | + | 4. **Permission errors**: Check registry authentication and permissions | |
| 178 | + | ||
| 179 | + | ### Debugging Commands | |
| 180 | + | ||
| 181 | + | ```bash | |
| 182 | + | # Inspect manifest annotations | |
| 183 | + | podman manifest inspect myapp:latest | |
| 184 | + | ||
| 185 | + | # Check Gordon configuration | |
| 186 | + | gordon config show | |
| 187 | + | ||
| 188 | + | # Verify image exists | |
| 189 | + | podman search registry.domain.com/myapp | |
| 190 | + | ``` | |
| 191 | + | ||
| 192 | + | This version deployment workflow provides a simple, declarative way to manage deployments and rollbacks using standard container registry operations and OCI manifest annotations.⏎̸ | |
| @@ -0,0 +1,192 @@ | |||||
| 1 | + | # Gordon Version Deployment Workflow Example | |||
| 2 | + | ||||
| 3 | + | This document demonstrates how to use manifest annotations for simple version deployments and rollbacks in Gordon. | |||
| 4 | + | ||||
| 5 | + | ## Prerequisites | |||
| 6 | + | ||||
| 7 | + | - Gordon server running with the registry and auto-route features enabled | |||
| 8 | + | - Podman or Docker with OCI manifest support | |||
| 9 | + | - A container image to deploy | |||
| 10 | + | ||||
| 11 | + | ## Basic Versioned Deployment | |||
| 12 | + | ||||
| 13 | + | ### 1. Build and Push Your Versioned Images | |||
| 14 | + | ||||
| 15 | + | ```bash | |||
| 16 | + | # Build and push your application images with versions | |||
| 17 | + | export VERSION=v1.0.1 | |||
| 18 | + | podman build --tag myapp:$VERSION --tag registry.domain.com/myapp:$VERSION . | |||
| 19 | + | podman push registry.domain.com/myapp:$VERSION | |||
| 20 | + | ||||
| 21 | + | export VERSION=v1.0.2 | |||
| 22 | + | podman build --tag myapp:$VERSION --tag registry.domain.com/myapp:$VERSION . | |||
| 23 | + | podman push registry.domain.com/myapp:$VERSION | |||
| 24 | + | ``` | |||
| 25 | + | ||||
| 26 | + | ### 2. Create Manifest and Deploy | |||
| 27 | + | ||||
| 28 | + | ```bash | |||
| 29 | + | # Deploy v1.0.1 with version annotation | |||
| 30 | + | export VERSION=v1.0.1 | |||
| 31 | + | podman manifest create myapp:latest | |||
| 32 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |||
| 33 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |||
| 34 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |||
| 35 | + | ``` | |||
| 36 | + | ||||
| 37 | + | ### 3. Gordon Processing | |||
| 38 | + | ||||
| 39 | + | When Gordon receives the manifest, it will: | |||
| 40 | + | ||||
| 41 | + | 1. Parse the manifest annotations from the JSON structure: | |||
| 42 | + | ```json | |||
| 43 | + | { | |||
| 44 | + | "schemaVersion": 2, | |||
| 45 | + | "mediaType": "application/vnd.oci.image.manifest.v1+json", | |||
| 46 | + | "annotations": { | |||
| 47 | + | "version": "v1.0.1" | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | ``` | |||
| 51 | + | ||||
| 52 | + | 2. Extract version information and deploy the specified version | |||
| 53 | + | 3. Update routes to use the versioned image | |||
| 54 | + | ||||
| 55 | + | ## Simple Version Deployment and Rollback | |||
| 56 | + | ||||
| 57 | + | ### Deploying Different Versions | |||
| 58 | + | ||||
| 59 | + | ```bash | |||
| 60 | + | # Deploy v1.0.2 | |||
| 61 | + | export VERSION=v1.0.2 | |||
| 62 | + | podman manifest create myapp:latest --amend | |||
| 63 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |||
| 64 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |||
| 65 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |||
| 66 | + | ||||
| 67 | + | # Rollback to v1.0.1 (just change the version) | |||
| 68 | + | export VERSION=v1.0.1 | |||
| 69 | + | podman manifest create myapp:latest --amend | |||
| 70 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |||
| 71 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |||
| 72 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |||
| 73 | + | ||||
| 74 | + | # Deploy v1.0.3 (assuming you built and pushed it first) | |||
| 75 | + | export VERSION=v1.0.3 | |||
| 76 | + | podman manifest create myapp:latest --amend | |||
| 77 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |||
| 78 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |||
| 79 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |||
| 80 | + | ``` | |||
| 81 | + | ||||
| 82 | + | ### Gordon Version Processing | |||
| 83 | + | ||||
| 84 | + | When Gordon detects version annotations, it will: | |||
| 85 | + | ||||
| 86 | + | 1. **Parse version annotations**: Extract the version from the `version` annotation | |||
| 87 | + | 2. **Execute deployment**: Deploy the specified version to all matching routes | |||
| 88 | + | 3. **Log the deployment** | |||
| 89 | + | ||||
| 90 | + | **Example log output**: | |||
| 91 | + | ``` | |||
| 92 | + | INFO Processing versioned deployment image=myapp reference=latest version=v1.0.1 | |||
| 93 | + | INFO Performing version deployment route=myapp.domain.com versioned_image=myapp:v1.0.1 | |||
| 94 | + | INFO Version deployment completed successfully route=myapp.domain.com version=v1.0.1 | |||
| 95 | + | ``` | |||
| 96 | + | ||||
| 97 | + | ## Supported Annotation Keys | |||
| 98 | + | ||||
| 99 | + | ### Version Annotations | |||
| 100 | + | - `version` - The version to deploy (required) | |||
| 101 | + | ||||
| 102 | + | ## Real-World Example | |||
| 103 | + | ||||
| 104 | + | ```bash | |||
| 105 | + | # Deploy production v1.0.1 | |||
| 106 | + | export VERSION=v1.0.1 | |||
| 107 | + | podman manifest create myapp:latest --amend | |||
| 108 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |||
| 109 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |||
| 110 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |||
| 111 | + | ||||
| 112 | + | # Deploy new version v1.0.2 | |||
| 113 | + | export VERSION=v1.0.2 | |||
| 114 | + | podman manifest create myapp:latest --amend | |||
| 115 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |||
| 116 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |||
| 117 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |||
| 118 | + | ||||
| 119 | + | # Oh no! Bug found - rollback to v1.0.1 | |||
| 120 | + | export VERSION=v1.0.1 | |||
| 121 | + | podman manifest create myapp:latest --amend | |||
| 122 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |||
| 123 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |||
| 124 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |||
| 125 | + | ||||
| 126 | + | # Fix deployed - move forward to v1.0.3 | |||
| 127 | + | export VERSION=v1.0.3 | |||
| 128 | + | podman manifest create myapp:latest --amend | |||
| 129 | + | podman manifest add myapp:latest registry.domain.com/myapp:$VERSION | |||
| 130 | + | podman manifest annotate myapp:latest --annotation version=$VERSION registry.domain.com/myapp:$VERSION | |||
| 131 | + | podman manifest push myapp:latest registry.domain.com/myapp:latest | |||
| 132 | + | ``` | |||
| 133 | + | ||||
| 134 | + | ## Configuration Requirements | |||
| 135 | + | ||||
| 136 | + | Ensure your Gordon configuration supports this workflow: | |||
| 137 | + | ||||
| 138 | + | ```toml | |||
| 139 | + | [auto_route] | |||
| 140 | + | enabled = true | |||
| 141 | + | ||||
| 142 | + | [registry_auth] | |||
| 143 | + | enabled = true | |||
| 144 | + | username = "gordon" | |||
| 145 | + | password = "your-password" | |||
| 146 | + | ||||
| 147 | + | [server] | |||
| 148 | + | registry_domain = "registry.domain.com" | |||
| 149 | + | ``` | |||
| 150 | + | ||||
| 151 | + | ## Monitoring and Observability | |||
| 152 | + | ||||
| 153 | + | Gordon logs provide visibility into the version deployment process: | |||
| 154 | + | ||||
| 155 | + | - Manifest annotation parsing | |||
| 156 | + | - Version detection and tracking | |||
| 157 | + | - Deployment execution | |||
| 158 | + | - Success/failure status | |||
| 159 | + | ||||
| 160 | + | Check Gordon logs for detailed version deployment information: | |||
| 161 | + | ||||
| 162 | + | ```bash | |||
| 163 | + | # View Gordon logs for version operations | |||
| 164 | + | journalctl -u gordon -f | grep -i version | |||
| 165 | + | ||||
| 166 | + | # Or check Docker/Podman logs if running in container | |||
| 167 | + | docker logs gordon-container | grep -i version | |||
| 168 | + | ``` | |||
| 169 | + | ||||
| 170 | + | ## Troubleshooting | |||
| 171 | + | ||||
| 172 | + | ### Common Issues | |||
| 173 | + | ||||
| 174 | + | 1. **Annotations not parsed**: Ensure you're using OCI manifest format, not Docker v2.2 | |||
| 175 | + | 2. **Version target not found**: Verify the target version exists in your registry | |||
| 176 | + | 3. **Route not found**: Ensure the image has configured routes in Gordon | |||
| 177 | + | 4. **Permission errors**: Check registry authentication and permissions | |||
| 178 | + | ||||
| 179 | + | ### Debugging Commands | |||
| 180 | + | ||||
| 181 | + | ```bash | |||
| 182 | + | # Inspect manifest annotations | |||
| 183 | + | podman manifest inspect myapp:latest | |||
| 184 | + | ||||
| 185 | + | # Check Gordon configuration | |||
| 186 | + | gordon config show | |||
| 187 | + | ||||
| 188 | + | # Verify image exists | |||
| 189 | + | podman search registry.domain.com/myapp | |||
| 190 | + | ``` | |||
| 191 | + | ||||
| 192 | + | This version deployment workflow provides a simple, declarative way to manage deployments and rollbacks using standard container registry operations and OCI manifest annotations.⏎̸ | |||
M go.mod +2 -3
| @@ -5,13 +5,13 @@ go 1.24.3 | |||
| 5 | 5 | require ( | |
| 6 | 6 | github.com/docker/docker v28.2.2+incompatible | |
| 7 | 7 | github.com/docker/go-connections v0.5.0 | |
| 8 | + | github.com/google/uuid v1.6.0 | |
| 8 | 9 | github.com/rs/zerolog v1.34.0 | |
| 9 | 10 | github.com/spf13/cobra v1.9.1 | |
| 10 | 11 | github.com/spf13/viper v1.20.1 | |
| 12 | + | gopkg.in/natefinch/lumberjack.v2 v2.2.1 | |
| 11 | 13 | ) | |
| 12 | 14 | ||
| 13 | - | require gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | |
| 14 | - | ||
| 15 | 15 | require ( | |
| 16 | 16 | github.com/Microsoft/go-winio v0.4.14 // indirect | |
| 17 | 17 | github.com/containerd/errdefs v1.0.0 // indirect | |
| @@ -25,7 +25,6 @@ require ( | |||
| 25 | 25 | github.com/go-logr/stdr v1.2.2 // indirect | |
| 26 | 26 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect | |
| 27 | 27 | github.com/gogo/protobuf v1.3.2 // indirect | |
| 28 | - | github.com/google/uuid v1.6.0 // direct | |
| 29 | 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | |
| 30 | 29 | github.com/mattn/go-colorable v0.1.13 // indirect | |
| 31 | 30 | github.com/mattn/go-isatty v0.0.19 // indirect | |
| @@ -5,13 +5,13 @@ go 1.24.3 | |||||
| 5 | require ( | 5 | require ( | ||
| 6 | github.com/docker/docker v28.2.2+incompatible | 6 | github.com/docker/docker v28.2.2+incompatible | ||
| 7 | github.com/docker/go-connections v0.5.0 | 7 | github.com/docker/go-connections v0.5.0 | ||
| 8 | + | github.com/google/uuid v1.6.0 | |||
| 8 | github.com/rs/zerolog v1.34.0 | 9 | github.com/rs/zerolog v1.34.0 | ||
| 9 | github.com/spf13/cobra v1.9.1 | 10 | github.com/spf13/cobra v1.9.1 | ||
| 10 | github.com/spf13/viper v1.20.1 | 11 | github.com/spf13/viper v1.20.1 | ||
| 12 | + | gopkg.in/natefinch/lumberjack.v2 v2.2.1 | |||
| 11 | ) | 13 | ) | ||
| 12 | 14 | ||||
| 13 | - | require gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | |||
| 14 | - | ||||
| 15 | require ( | 15 | require ( | ||
| 16 | github.com/Microsoft/go-winio v0.4.14 // indirect | 16 | github.com/Microsoft/go-winio v0.4.14 // indirect | ||
| 17 | github.com/containerd/errdefs v1.0.0 // indirect | 17 | github.com/containerd/errdefs v1.0.0 // indirect | ||
| @@ -25,7 +25,6 @@ require ( | |||||
| 25 | github.com/go-logr/stdr v1.2.2 // indirect | 25 | github.com/go-logr/stdr v1.2.2 // indirect | ||
| 26 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect | 26 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect | ||
| 27 | github.com/gogo/protobuf v1.3.2 // indirect | 27 | github.com/gogo/protobuf v1.3.2 // indirect | ||
| 28 | - | github.com/google/uuid v1.6.0 // direct | |||
| 29 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||
| 30 | github.com/mattn/go-colorable v0.1.13 // indirect | 29 | github.com/mattn/go-colorable v0.1.13 // indirect | ||
| 31 | github.com/mattn/go-isatty v0.0.19 // indirect | 30 | github.com/mattn/go-isatty v0.0.19 // indirect | ||
M internal/config/config.go +74 -0
| @@ -339,6 +339,80 @@ func (c *Config) AddRoute(domain, image string) error { | |||
| 339 | 339 | return nil | |
| 340 | 340 | } | |
| 341 | 341 | ||
| 342 | + | // UpdateRoute updates an existing route configuration | |
| 343 | + | func (c *Config) UpdateRoute(domain, image string) error { | |
| 344 | + | if c.Routes == nil { | |
| 345 | + | c.Routes = make(map[string]string) | |
| 346 | + | } | |
| 347 | + | ||
| 348 | + | // Check if route exists | |
| 349 | + | if _, exists := c.Routes[domain]; !exists { | |
| 350 | + | return fmt.Errorf("route %s does not exist", domain) | |
| 351 | + | } | |
| 352 | + | ||
| 353 | + | c.Routes[domain] = image | |
| 354 | + | ||
| 355 | + | // Read the current config file to preserve formatting | |
| 356 | + | configFile := viper.ConfigFileUsed() | |
| 357 | + | if configFile == "" { | |
| 358 | + | return fmt.Errorf("no config file path available") | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | // Read existing content | |
| 362 | + | content, err := os.ReadFile(configFile) | |
| 363 | + | if err != nil { | |
| 364 | + | return fmt.Errorf("failed to read config file: %w", err) | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | // Parse and update only the routes section while preserving other formatting | |
| 368 | + | lines := strings.Split(string(content), "\n") | |
| 369 | + | var newLines []string | |
| 370 | + | inRoutesSection := false | |
| 371 | + | routeUpdated := false | |
| 372 | + | ||
| 373 | + | for _, line := range lines { | |
| 374 | + | trimmed := strings.TrimSpace(line) | |
| 375 | + | ||
| 376 | + | // Check if we're entering or leaving the routes section | |
| 377 | + | if trimmed == "[routes]" { | |
| 378 | + | inRoutesSection = true | |
| 379 | + | newLines = append(newLines, line) | |
| 380 | + | continue | |
| 381 | + | } | |
| 382 | + | ||
| 383 | + | // Check if we're entering a new section | |
| 384 | + | if strings.HasPrefix(trimmed, "[") && trimmed != "[routes]" { | |
| 385 | + | inRoutesSection = false | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | // If we're in routes section, check if this route matches (check both quote styles) | |
| 389 | + | if inRoutesSection && (strings.Contains(trimmed, fmt.Sprintf("'%s'", domain)) || strings.Contains(trimmed, fmt.Sprintf("\"%s\"", domain))) { | |
| 390 | + | // Update existing route using double quotes for consistency | |
| 391 | + | newLines = append(newLines, fmt.Sprintf("\"%s\" = \"%s\"", domain, image)) | |
| 392 | + | routeUpdated = true | |
| 393 | + | } else { | |
| 394 | + | newLines = append(newLines, line) | |
| 395 | + | } | |
| 396 | + | } | |
| 397 | + | ||
| 398 | + | if !routeUpdated { | |
| 399 | + | return fmt.Errorf("route %s not found in config file", domain) | |
| 400 | + | } | |
| 401 | + | ||
| 402 | + | // Write the updated content back | |
| 403 | + | newContent := strings.Join(newLines, "\n") | |
| 404 | + | if err := os.WriteFile(configFile, []byte(newContent), 0644); err != nil { | |
| 405 | + | return fmt.Errorf("failed to write config file: %w", err) | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | log.Info(). | |
| 409 | + | Str("domain", domain). | |
| 410 | + | Str("image", image). | |
| 411 | + | Msg("Updated route configuration") | |
| 412 | + | ||
| 413 | + | return nil | |
| 414 | + | } | |
| 415 | + | ||
| 342 | 416 | // validateEnvConfig validates the environment configuration | |
| 343 | 417 | func validateEnvConfig(envCfg *EnvConfig) error { | |
| 344 | 418 | // Validate env directory path | |
| @@ -339,6 +339,80 @@ func (c *Config) AddRoute(domain, image string) error { | |||||
| 339 | return nil | 339 | return nil | ||
| 340 | } | 340 | } | ||
| 341 | 341 | ||||
| 342 | + | // UpdateRoute updates an existing route configuration | |||
| 343 | + | func (c *Config) UpdateRoute(domain, image string) error { | |||
| 344 | + | if c.Routes == nil { | |||
| 345 | + | c.Routes = make(map[string]string) | |||
| 346 | + | } | |||
| 347 | + | ||||
| 348 | + | // Check if route exists | |||
| 349 | + | if _, exists := c.Routes[domain]; !exists { | |||
| 350 | + | return fmt.Errorf("route %s does not exist", domain) | |||
| 351 | + | } | |||
| 352 | + | ||||
| 353 | + | c.Routes[domain] = image | |||
| 354 | + | ||||
| 355 | + | // Read the current config file to preserve formatting | |||
| 356 | + | configFile := viper.ConfigFileUsed() | |||
| 357 | + | if configFile == "" { | |||
| 358 | + | return fmt.Errorf("no config file path available") | |||
| 359 | + | } | |||
| 360 | + | ||||
| 361 | + | // Read existing content | |||
| 362 | + | content, err := os.ReadFile(configFile) | |||
| 363 | + | if err != nil { | |||
| 364 | + | return fmt.Errorf("failed to read config file: %w", err) | |||
| 365 | + | } | |||
| 366 | + | ||||
| 367 | + | // Parse and update only the routes section while preserving other formatting | |||
| 368 | + | lines := strings.Split(string(content), "\n") | |||
| 369 | + | var newLines []string | |||
| 370 | + | inRoutesSection := false | |||
| 371 | + | routeUpdated := false | |||
| 372 | + | ||||
| 373 | + | for _, line := range lines { | |||
| 374 | + | trimmed := strings.TrimSpace(line) | |||
| 375 | + | ||||
| 376 | + | // Check if we're entering or leaving the routes section | |||
| 377 | + | if trimmed == "[routes]" { | |||
| 378 | + | inRoutesSection = true | |||
| 379 | + | newLines = append(newLines, line) | |||
| 380 | + | continue | |||
| 381 | + | } | |||
| 382 | + | ||||
| 383 | + | // Check if we're entering a new section | |||
| 384 | + | if strings.HasPrefix(trimmed, "[") && trimmed != "[routes]" { | |||
| 385 | + | inRoutesSection = false | |||
| 386 | + | } | |||
| 387 | + | ||||
| 388 | + | // If we're in routes section, check if this route matches (check both quote styles) | |||
| 389 | + | if inRoutesSection && (strings.Contains(trimmed, fmt.Sprintf("'%s'", domain)) || strings.Contains(trimmed, fmt.Sprintf("\"%s\"", domain))) { | |||
| 390 | + | // Update existing route using double quotes for consistency | |||
| 391 | + | newLines = append(newLines, fmt.Sprintf("\"%s\" = \"%s\"", domain, image)) | |||
| 392 | + | routeUpdated = true | |||
| 393 | + | } else { | |||
| 394 | + | newLines = append(newLines, line) | |||
| 395 | + | } | |||
| 396 | + | } | |||
| 397 | + | ||||
| 398 | + | if !routeUpdated { | |||
| 399 | + | return fmt.Errorf("route %s not found in config file", domain) | |||
| 400 | + | } | |||
| 401 | + | ||||
| 402 | + | // Write the updated content back | |||
| 403 | + | newContent := strings.Join(newLines, "\n") | |||
| 404 | + | if err := os.WriteFile(configFile, []byte(newContent), 0644); err != nil { | |||
| 405 | + | return fmt.Errorf("failed to write config file: %w", err) | |||
| 406 | + | } | |||
| 407 | + | ||||
| 408 | + | log.Info(). | |||
| 409 | + | Str("domain", domain). | |||
| 410 | + | Str("image", image). | |||
| 411 | + | Msg("Updated route configuration") | |||
| 412 | + | ||||
| 413 | + | return nil | |||
| 414 | + | } | |||
| 415 | + | ||||
| 342 | // validateEnvConfig validates the environment configuration | 416 | // validateEnvConfig validates the environment configuration | ||
| 343 | func validateEnvConfig(envCfg *EnvConfig) error { | 417 | func validateEnvConfig(envCfg *EnvConfig) error { | ||
| 344 | // Validate env directory path | 418 | // Validate env directory path | ||
M internal/events/handlers.go +9 -0
| @@ -340,6 +340,15 @@ func (h *AutoRouteHandler) Handle(event Event) error { | |||
| 340 | 340 | return fmt.Errorf("invalid payload type for ImagePushed event") | |
| 341 | 341 | } | |
| 342 | 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 | + | ||
| 343 | 352 | imageName := payload.Name | |
| 344 | 353 | reference := payload.Reference | |
| 345 | 354 | fullImageName := fmt.Sprintf("%s:%s", imageName, reference) | |
| @@ -340,6 +340,15 @@ func (h *AutoRouteHandler) Handle(event Event) error { | |||||
| 340 | return fmt.Errorf("invalid payload type for ImagePushed event") | 340 | return fmt.Errorf("invalid payload type for ImagePushed event") | ||
| 341 | } | 341 | } | ||
| 342 | 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 | + | ||||
| 343 | imageName := payload.Name | 352 | imageName := payload.Name | ||
| 344 | reference := payload.Reference | 353 | reference := payload.Reference | ||
| 345 | fullImageName := fmt.Sprintf("%s:%s", imageName, reference) | 354 | fullImageName := fmt.Sprintf("%s:%s", imageName, reference) | ||
A internal/events/rollback_handler.go +188 -0
| @@ -0,0 +1,188 @@ | |||
| 1 | + | package events | |
| 2 | + | ||
| 3 | + | import ( | |
| 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 | |
| 15 | + | type VersionHandler struct { | |
| 16 | + | manager *container.Manager | |
| 17 | + | config *config.Config | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // NewVersionHandler creates a new version handler | |
| 21 | + | func NewVersionHandler(manager *container.Manager, config *config.Config) *VersionHandler { | |
| 22 | + | return &VersionHandler{ | |
| 23 | + | manager: manager, | |
| 24 | + | config: config, | |
| 25 | + | } | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // CanHandle returns true for ImagePushed events | |
| 29 | + | func (h *VersionHandler) CanHandle(eventType EventType) bool { | |
| 30 | + | return eventType == ImagePushed | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // Handle processes ImagePushed events and performs version deployments if necessary | |
| 34 | + | func (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 | |
| 68 | + | func (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 | |
| 85 | + | func (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 | |
| 124 | + | func (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 | |
| 137 | + | func (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 | + | }⏎̸ | |
| @@ -0,0 +1,188 @@ | |||||
| 1 | + | package events | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 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 | |||
| 15 | + | type VersionHandler struct { | |||
| 16 | + | manager *container.Manager | |||
| 17 | + | config *config.Config | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // NewVersionHandler creates a new version handler | |||
| 21 | + | func NewVersionHandler(manager *container.Manager, config *config.Config) *VersionHandler { | |||
| 22 | + | return &VersionHandler{ | |||
| 23 | + | manager: manager, | |||
| 24 | + | config: config, | |||
| 25 | + | } | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // CanHandle returns true for ImagePushed events | |||
| 29 | + | func (h *VersionHandler) CanHandle(eventType EventType) bool { | |||
| 30 | + | return eventType == ImagePushed | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // Handle processes ImagePushed events and performs version deployments if necessary | |||
| 34 | + | func (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 | |||
| 68 | + | func (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 | |||
| 85 | + | func (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 | |||
| 124 | + | func (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 | |||
| 137 | + | func (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 | + | }⏎̸ | |||
M internal/events/types.go +4 -3
| @@ -28,9 +28,10 @@ type Event struct { | |||
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | 30 | type ImagePushedPayload struct { | |
| 31 | - | Name string `json:"name"` | |
| 32 | - | Reference string `json:"reference"` | |
| 33 | - | Manifest []byte `json:"-"` | |
| 31 | + | Name string `json:"name"` | |
| 32 | + | Reference string `json:"reference"` | |
| 33 | + | Manifest []byte `json:"-"` | |
| 34 | + | Annotations map[string]string `json:"annotations,omitempty"` | |
| 34 | 35 | } | |
| 35 | 36 | ||
| 36 | 37 | type EventHandler interface { | |
| @@ -28,9 +28,10 @@ type Event struct { | |||||
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | type ImagePushedPayload struct { | 30 | type ImagePushedPayload struct { | ||
| 31 | - | Name string `json:"name"` | 31 | + | Name string `json:"name"` |
| 32 | - | Reference string `json:"reference"` | 32 | + | Reference string `json:"reference"` |
| 33 | - | Manifest []byte `json:"-"` | 33 | + | Manifest []byte `json:"-"` |
| 34 | + | Annotations map[string]string `json:"annotations,omitempty"` | |||
| 34 | } | 35 | } | ||
| 35 | 36 | ||||
| 36 | type EventHandler interface { | 37 | type EventHandler interface { | ||
A internal/registry/manifest.go +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | package registry | |
| 2 | + | ||
| 3 | + | // This file is deprecated - manifest processing has been moved to pkg/manifest | |
| 4 | + | // to avoid circular import issues. This file will be removed in a future version.⏎̸ | |
| @@ -0,0 +1,4 @@ | |||||
| 1 | + | package registry | |||
| 2 | + | ||||
| 3 | + | // This file is deprecated - manifest processing has been moved to pkg/manifest | |||
| 4 | + | // to avoid circular import issues. This file will be removed in a future version.⏎̸ | |||
M internal/registry/server.go +21 -3
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | "gordon/internal/config" | |
| 12 | 12 | "gordon/internal/events" | |
| 13 | 13 | "gordon/internal/middleware" | |
| 14 | + | "gordon/pkg/manifest" | |
| 14 | 15 | ||
| 15 | 16 | "github.com/rs/zerolog/log" | |
| 16 | 17 | ) | |
| @@ -130,11 +131,28 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) { | |||
| 130 | 131 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference)) | |
| 131 | 132 | w.WriteHeader(http.StatusCreated) | |
| 132 | 133 | ||
| 134 | + | // Parse manifest annotations | |
| 135 | + | annotations, err := manifest.ParseManifestAnnotations(data, contentType) | |
| 136 | + | if err != nil { | |
| 137 | + | log.Warn().Err(err).Str("name", name).Str("reference", reference).Msg("Failed to parse manifest annotations, continuing without them") | |
| 138 | + | annotations = map[string]string{} | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | // Log discovered annotations | |
| 142 | + | if len(annotations) > 0 { | |
| 143 | + | log.Info(). | |
| 144 | + | Str("name", name). | |
| 145 | + | Str("reference", reference). | |
| 146 | + | Interface("annotations", annotations). | |
| 147 | + | Msg("Manifest annotations discovered") | |
| 148 | + | } | |
| 149 | + | ||
| 133 | 150 | // Emit image pushed event | |
| 134 | 151 | s.eventBus.Publish(events.ImagePushed, events.ImagePushedPayload{ | |
| 135 | - | Name: name, | |
| 136 | - | Reference: reference, | |
| 137 | - | Manifest: data, | |
| 152 | + | Name: name, | |
| 153 | + | Reference: reference, | |
| 154 | + | Manifest: data, | |
| 155 | + | Annotations: annotations, | |
| 138 | 156 | }) | |
| 139 | 157 | } | |
| 140 | 158 | ||
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | "gordon/internal/config" | 11 | "gordon/internal/config" | ||
| 12 | "gordon/internal/events" | 12 | "gordon/internal/events" | ||
| 13 | "gordon/internal/middleware" | 13 | "gordon/internal/middleware" | ||
| 14 | + | "gordon/pkg/manifest" | |||
| 14 | 15 | ||||
| 15 | "github.com/rs/zerolog/log" | 16 | "github.com/rs/zerolog/log" | ||
| 16 | ) | 17 | ) | ||
| @@ -130,11 +131,28 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) { | |||||
| 130 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference)) | 131 | w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference)) | ||
| 131 | w.WriteHeader(http.StatusCreated) | 132 | w.WriteHeader(http.StatusCreated) | ||
| 132 | 133 | ||||
| 134 | + | // Parse manifest annotations | |||
| 135 | + | annotations, err := manifest.ParseManifestAnnotations(data, contentType) | |||
| 136 | + | if err != nil { | |||
| 137 | + | log.Warn().Err(err).Str("name", name).Str("reference", reference).Msg("Failed to parse manifest annotations, continuing without them") | |||
| 138 | + | annotations = map[string]string{} | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | // Log discovered annotations | |||
| 142 | + | if len(annotations) > 0 { | |||
| 143 | + | log.Info(). | |||
| 144 | + | Str("name", name). | |||
| 145 | + | Str("reference", reference). | |||
| 146 | + | Interface("annotations", annotations). | |||
| 147 | + | Msg("Manifest annotations discovered") | |||
| 148 | + | } | |||
| 149 | + | ||||
| 133 | // Emit image pushed event | 150 | // Emit image pushed event | ||
| 134 | s.eventBus.Publish(events.ImagePushed, events.ImagePushedPayload{ | 151 | s.eventBus.Publish(events.ImagePushed, events.ImagePushedPayload{ | ||
| 135 | - | Name: name, | 152 | + | Name: name, |
| 136 | - | Reference: reference, | 153 | + | Reference: reference, |
| 137 | - | Manifest: data, | 154 | + | Manifest: data, |
| 155 | + | Annotations: annotations, | |||
| 138 | }) | 156 | }) | ||
| 139 | } | 157 | } | ||
| 140 | 158 | ||||
A pkg/manifest/annotations.go +68 -0
| @@ -0,0 +1,68 @@ | |||
| 1 | + | package manifest | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/json" | |
| 5 | + | "fmt" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // ParseManifestAnnotations extracts annotations from manifest data | |
| 9 | + | func ParseManifestAnnotations(manifestData []byte, contentType string) (map[string]string, error) { | |
| 10 | + | switch contentType { | |
| 11 | + | case "application/vnd.oci.image.manifest.v1+json": | |
| 12 | + | return parseOCIManifestAnnotations(manifestData) | |
| 13 | + | case "application/vnd.docker.distribution.manifest.v2+json": | |
| 14 | + | // Docker v2.2 manifests don't support annotations at manifest level | |
| 15 | + | return map[string]string{}, nil | |
| 16 | + | case "application/vnd.oci.image.index.v1+json", | |
| 17 | + | "application/vnd.docker.distribution.manifest.list.v2+json": | |
| 18 | + | return parseManifestListAnnotations(manifestData) | |
| 19 | + | default: | |
| 20 | + | return map[string]string{}, fmt.Errorf("unsupported manifest media type: %s", contentType) | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // parseOCIManifestAnnotations parses annotations from an OCI image manifest | |
| 25 | + | func parseOCIManifestAnnotations(manifestData []byte) (map[string]string, error) { | |
| 26 | + | var manifest OCIManifest | |
| 27 | + | if err := json.Unmarshal(manifestData, &manifest); err != nil { | |
| 28 | + | return nil, fmt.Errorf("failed to parse OCI manifest: %w", err) | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | if manifest.Annotations == nil { | |
| 32 | + | return map[string]string{}, nil | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | return manifest.Annotations, nil | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // parseManifestListAnnotations parses annotations from a manifest list | |
| 39 | + | func parseManifestListAnnotations(manifestData []byte) (map[string]string, error) { | |
| 40 | + | var manifestList ManifestList | |
| 41 | + | if err := json.Unmarshal(manifestData, &manifestList); err != nil { | |
| 42 | + | return nil, fmt.Errorf("failed to parse manifest list: %w", err) | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | if manifestList.Annotations == nil { | |
| 46 | + | return map[string]string{}, nil | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | return manifestList.Annotations, nil | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // IsVersionedDeployment checks if manifest contains version annotations for deployment | |
| 53 | + | func IsVersionedDeployment(annotations map[string]string) bool { | |
| 54 | + | // Just check for the "version" annotation - keep it simple | |
| 55 | + | _, exists := annotations["version"] | |
| 56 | + | return exists | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // GetDeploymentVersion extracts the deployment version from annotations | |
| 60 | + | func GetDeploymentVersion(annotations map[string]string) string { | |
| 61 | + | // Just use the "version" annotation - keep it simple | |
| 62 | + | if version, exists := annotations["version"]; exists && version != "" { | |
| 63 | + | return version | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | return "" | |
| 67 | + | } | |
| 68 | + | ||
| @@ -0,0 +1,68 @@ | |||||
| 1 | + | package manifest | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/json" | |||
| 5 | + | "fmt" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // ParseManifestAnnotations extracts annotations from manifest data | |||
| 9 | + | func ParseManifestAnnotations(manifestData []byte, contentType string) (map[string]string, error) { | |||
| 10 | + | switch contentType { | |||
| 11 | + | case "application/vnd.oci.image.manifest.v1+json": | |||
| 12 | + | return parseOCIManifestAnnotations(manifestData) | |||
| 13 | + | case "application/vnd.docker.distribution.manifest.v2+json": | |||
| 14 | + | // Docker v2.2 manifests don't support annotations at manifest level | |||
| 15 | + | return map[string]string{}, nil | |||
| 16 | + | case "application/vnd.oci.image.index.v1+json", | |||
| 17 | + | "application/vnd.docker.distribution.manifest.list.v2+json": | |||
| 18 | + | return parseManifestListAnnotations(manifestData) | |||
| 19 | + | default: | |||
| 20 | + | return map[string]string{}, fmt.Errorf("unsupported manifest media type: %s", contentType) | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // parseOCIManifestAnnotations parses annotations from an OCI image manifest | |||
| 25 | + | func parseOCIManifestAnnotations(manifestData []byte) (map[string]string, error) { | |||
| 26 | + | var manifest OCIManifest | |||
| 27 | + | if err := json.Unmarshal(manifestData, &manifest); err != nil { | |||
| 28 | + | return nil, fmt.Errorf("failed to parse OCI manifest: %w", err) | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | if manifest.Annotations == nil { | |||
| 32 | + | return map[string]string{}, nil | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | return manifest.Annotations, nil | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // parseManifestListAnnotations parses annotations from a manifest list | |||
| 39 | + | func parseManifestListAnnotations(manifestData []byte) (map[string]string, error) { | |||
| 40 | + | var manifestList ManifestList | |||
| 41 | + | if err := json.Unmarshal(manifestData, &manifestList); err != nil { | |||
| 42 | + | return nil, fmt.Errorf("failed to parse manifest list: %w", err) | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | if manifestList.Annotations == nil { | |||
| 46 | + | return map[string]string{}, nil | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | return manifestList.Annotations, nil | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // IsVersionedDeployment checks if manifest contains version annotations for deployment | |||
| 53 | + | func IsVersionedDeployment(annotations map[string]string) bool { | |||
| 54 | + | // Just check for the "version" annotation - keep it simple | |||
| 55 | + | _, exists := annotations["version"] | |||
| 56 | + | return exists | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | // GetDeploymentVersion extracts the deployment version from annotations | |||
| 60 | + | func GetDeploymentVersion(annotations map[string]string) string { | |||
| 61 | + | // Just use the "version" annotation - keep it simple | |||
| 62 | + | if version, exists := annotations["version"]; exists && version != "" { | |||
| 63 | + | return version | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | return "" | |||
| 67 | + | } | |||
| 68 | + | ||||
A pkg/manifest/types.go +52 -0
| @@ -0,0 +1,52 @@ | |||
| 1 | + | package manifest | |
| 2 | + | ||
| 3 | + | // OCIManifest represents an OCI image manifest structure | |
| 4 | + | type OCIManifest struct { | |
| 5 | + | SchemaVersion int `json:"schemaVersion"` | |
| 6 | + | MediaType string `json:"mediaType"` | |
| 7 | + | Config OCIDescriptor `json:"config"` | |
| 8 | + | Layers []OCIDescriptor `json:"layers"` | |
| 9 | + | Annotations map[string]string `json:"annotations,omitempty"` | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | // DockerManifest represents a Docker v2.2 manifest structure | |
| 13 | + | type DockerManifest struct { | |
| 14 | + | SchemaVersion int `json:"schemaVersion"` | |
| 15 | + | MediaType string `json:"mediaType"` | |
| 16 | + | Config OCIDescriptor `json:"config"` | |
| 17 | + | Layers []OCIDescriptor `json:"layers"` | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // OCIDescriptor represents a content descriptor | |
| 21 | + | type OCIDescriptor struct { | |
| 22 | + | MediaType string `json:"mediaType"` | |
| 23 | + | Digest string `json:"digest"` | |
| 24 | + | Size int64 `json:"size"` | |
| 25 | + | Annotations map[string]string `json:"annotations,omitempty"` | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // ManifestList represents a multi-platform manifest list | |
| 29 | + | type ManifestList struct { | |
| 30 | + | SchemaVersion int `json:"schemaVersion"` | |
| 31 | + | MediaType string `json:"mediaType"` | |
| 32 | + | Manifests []ManifestDescriptor `json:"manifests"` | |
| 33 | + | Annotations map[string]string `json:"annotations,omitempty"` | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // ManifestDescriptor represents a manifest in a manifest list | |
| 37 | + | type ManifestDescriptor struct { | |
| 38 | + | MediaType string `json:"mediaType"` | |
| 39 | + | Digest string `json:"digest"` | |
| 40 | + | Size int64 `json:"size"` | |
| 41 | + | Platform *Platform `json:"platform,omitempty"` | |
| 42 | + | Annotations map[string]string `json:"annotations,omitempty"` | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // Platform represents the platform information | |
| 46 | + | type Platform struct { | |
| 47 | + | Architecture string `json:"architecture"` | |
| 48 | + | OS string `json:"os"` | |
| 49 | + | OSVersion string `json:"os.version,omitempty"` | |
| 50 | + | OSFeatures []string `json:"os.features,omitempty"` | |
| 51 | + | Variant string `json:"variant,omitempty"` | |
| 52 | + | }⏎̸ | |
| @@ -0,0 +1,52 @@ | |||||
| 1 | + | package manifest | |||
| 2 | + | ||||
| 3 | + | // OCIManifest represents an OCI image manifest structure | |||
| 4 | + | type OCIManifest struct { | |||
| 5 | + | SchemaVersion int `json:"schemaVersion"` | |||
| 6 | + | MediaType string `json:"mediaType"` | |||
| 7 | + | Config OCIDescriptor `json:"config"` | |||
| 8 | + | Layers []OCIDescriptor `json:"layers"` | |||
| 9 | + | Annotations map[string]string `json:"annotations,omitempty"` | |||
| 10 | + | } | |||
| 11 | + | ||||
| 12 | + | // DockerManifest represents a Docker v2.2 manifest structure | |||
| 13 | + | type DockerManifest struct { | |||
| 14 | + | SchemaVersion int `json:"schemaVersion"` | |||
| 15 | + | MediaType string `json:"mediaType"` | |||
| 16 | + | Config OCIDescriptor `json:"config"` | |||
| 17 | + | Layers []OCIDescriptor `json:"layers"` | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // OCIDescriptor represents a content descriptor | |||
| 21 | + | type OCIDescriptor struct { | |||
| 22 | + | MediaType string `json:"mediaType"` | |||
| 23 | + | Digest string `json:"digest"` | |||
| 24 | + | Size int64 `json:"size"` | |||
| 25 | + | Annotations map[string]string `json:"annotations,omitempty"` | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // ManifestList represents a multi-platform manifest list | |||
| 29 | + | type ManifestList struct { | |||
| 30 | + | SchemaVersion int `json:"schemaVersion"` | |||
| 31 | + | MediaType string `json:"mediaType"` | |||
| 32 | + | Manifests []ManifestDescriptor `json:"manifests"` | |||
| 33 | + | Annotations map[string]string `json:"annotations,omitempty"` | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // ManifestDescriptor represents a manifest in a manifest list | |||
| 37 | + | type ManifestDescriptor struct { | |||
| 38 | + | MediaType string `json:"mediaType"` | |||
| 39 | + | Digest string `json:"digest"` | |||
| 40 | + | Size int64 `json:"size"` | |||
| 41 | + | Platform *Platform `json:"platform,omitempty"` | |||
| 42 | + | Annotations map[string]string `json:"annotations,omitempty"` | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // Platform represents the platform information | |||
| 46 | + | type Platform struct { | |||
| 47 | + | Architecture string `json:"architecture"` | |||
| 48 | + | OS string `json:"os"` | |||
| 49 | + | OSVersion string `json:"os.version,omitempty"` | |||
| 50 | + | OSFeatures []string `json:"os.features,omitempty"` | |||
| 51 | + | Variant string `json:"variant,omitempty"` | |||
| 52 | + | }⏎̸ | |||