detached c7c4c6a

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
4141 - **Push-to-Deploy**: New images deploy instantly
4242 - **Multi-Domain Routing**: Unlimited apps with automatic HTTPS via Cloudflare
4343 - **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
4545
4646 ## Quick Start (5 minutes)
4747
@@ -160,15 +160,54 @@ podman push registry.mydomain.com/myapp:latest
160160 ```
161161
162162 ### 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)
163167 ```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
165170 podman push registry.mydomain.com/myapp:v1.0.1
166171
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
168177 # 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
170207 ```
171208
209+**Recommendation**: Use Method 2 for instant deployments without config file edits. Method 1 is best for simple workflows.
210+
172211 ### Auto-Route Creation
173212 ```toml
174213 [auto_route]
@@ -183,30 +222,35 @@ podman push registry.mydomain.com/staging.example.com:latest
183222
184223 ## Advanced Configuration
185224
186-### Full Config Structure
225+### Full Config Structure (with default values)
187226 ```toml
188227 # gordon.toml - Auto-generated on first run
189228 [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)
193236 # runtime auto-detects: docker → podman-rootless → podman
194237 # Override with env: CONTAINER_HOST=unix:///custom/socket
195238
196239 [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)
200243
201244 [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://
203247
204248 [auto_route]
205-enabled = false # Auto-create routes from image names
249+enabled = false # Default: false
206250
207251 [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"]
210254 # Example env file: ./data/env/app_mydomain_com.env
211255 # NODE_ENV=production
212256 # DATABASE_URL=postgresql://localhost:5432/prod
@@ -214,16 +258,16 @@ providers = ["pass", "sops"] # Optional secret management
214258 # JWT_SECRET=${sops:secrets.yaml:jwt} # SOPS integration
215259
216260 [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)
227271 # Creates: gordon.log, proxy.log, containers/*.log
228272 ```
229273
@@ -236,7 +280,7 @@ A: Those are just reverse proxies. Gordon is a complete deployment platform with
236280 A: On YOUR machine. If it runs locally, it runs in production.
237281
238282 **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.
240284
241285 **Q: Do I need Cloudflare?**
242286 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 instantly41 - **Push-to-Deploy**: New images deploy instantly
42 - **Multi-Domain Routing**: Unlimited apps with automatic HTTPS via Cloudflare42 - **Multi-Domain Routing**: Unlimited apps with automatic HTTPS via Cloudflare
43 - **Zero-Downtime Updates**: Graceful container swaps43 - **Zero-Downtime Updates**: Graceful container swaps
44-- **Auto-Route Creation**: Push `myapp.mydomain.com:latest` → route created automatically44+- **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 & Rollbacks162 ### 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 ```bash167 ```bash
164-# Push versioned releases168+# 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.1170 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 Creation211 ### Auto-Route Creation
173 ```toml212 ```toml
174 [auto_route]213 [auto_route]
@@ -183,30 +222,35 @@ podman push registry.mydomain.com/staging.example.com:latest
183 222
184 ## Advanced Configuration223 ## Advanced Configuration
185 224
186-### Full Config Structure225+### Full Config Structure (with default values)
187 ```toml226 ```toml
188 # gordon.toml - Auto-generated on first run227 # gordon.toml - Auto-generated on first run
189 [server]228 [server]
190-port = 8080229+port = 8080 # Default: 8080
191-registry_domain = "registry.mydomain.com"230+registry_port = 5000 # Default: 5000
192-runtime = "podman-rootless" # auto, docker, podman, podman-rootless231+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 → podman236 # runtime auto-detects: docker → podman-rootless → podman
194 # Override with env: CONTAINER_HOST=unix:///custom/socket237 # Override with env: CONTAINER_HOST=unix:///custom/socket
195 238
196 [registry_auth]239 [registry_auth]
197-enabled = true240+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 names249+enabled = false # Default: false
206 250
207 [env]251 [env]
208-dir = "./data/env" # Gordon auto-creates env files here252+dir = "{data_dir}/env" # Default: {data_dir}/env
209-providers = ["pass", "sops"] # Optional secret management253+providers = ["pass", "sops"] # Default: ["pass", "sops"]
210 # Example env file: ./data/env/app_mydomain_com.env254 # Example env file: ./data/env/app_mydomain_com.env
211 # NODE_ENV=production255 # NODE_ENV=production
212 # DATABASE_URL=postgresql://localhost:5432/prod256 # DATABASE_URL=postgresql://localhost:5432/prod
@@ -214,16 +258,16 @@ providers = ["pass", "sops"] # Optional secret management
214 # JWT_SECRET=${sops:secrets.yaml:jwt} # SOPS integration258 # JWT_SECRET=${sops:secrets.yaml:jwt} # SOPS integration
215 259
216 [logging]260 [logging]
217-enabled = false261+enabled = true # Default: true
218-level = "info" # trace, debug, info, warn, error262+level = "info" # Default: "info" (debug, info, warn, error, fatal, panic)
219-dir = "./logs" # Log directory263+dir = "{data_dir}/logs" # Default: {data_dir}/logs
220-main_log_file = "gordon.log" # Main app log264+main_log_file = "gordon.log" # Default: "gordon.log"
221-proxy_log_file = "proxy.log" # HTTP traffic log265+proxy_log_file = "proxy.log" # Default: "proxy.log"
222-container_log_dir = "containers" # Container logs subdirectory266+container_log_dir = "containers" # Default: "containers"
223-max_size = 100 # MB before rotation267+max_size = 100 # Default: 100 (MB before rotation)
224-max_backups = 3 # Number of old files to keep268+max_backups = 3 # Default: 3 (old files to keep)
225-max_age = 28 # Days to keep old files269+max_age = 28 # Default: 28 (days to keep old files)
226-compress = true # Gzip rotated logs270+compress = true # Default: true (gzip rotated logs)
227 # Creates: gordon.log, proxy.log, containers/*.log271 # 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) {
8787 log.Fatal().Err(err).Msg("Failed to subscribe auto-route event handler")
8888 }
8989
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+
9097 // Start proxy server (needs to be accessible for config reload)
9198 proxyServer := proxy.NewServer(cfg, manager)
9299
@@ -148,6 +155,9 @@ func runStart(cmd *cobra.Command, args []string) {
148155 if err := eventBus.Unsubscribe(autoRouteHandler); err != nil {
149156 log.Warn().Err(err).Msg("Failed to unsubscribe old auto-route event handler")
150157 }
158+ if err := eventBus.Unsubscribe(versionHandler); err != nil {
159+ log.Warn().Err(err).Msg("Failed to unsubscribe old version event handler")
160+ }
151161 if err := eventBus.Unsubscribe(proxyHandler); err != nil {
152162 log.Warn().Err(err).Msg("Failed to unsubscribe old proxy event handler")
153163 }
@@ -166,6 +176,13 @@ func runStart(cmd *cobra.Command, args []string) {
166176 return
167177 }
168178
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+
169186 // Re-subscribe proxy handler (config ref updated automatically)
170187 proxyHandler = proxy.NewProxyEventHandler(proxyServer)
171188 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 return176 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
4646 - Production and development logging strategies
4747 - Log rotation and monitoring examples
4848
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+
4955 ## 🚀 Quick Start
5056
5157 1. **Choose an example** that matches your use case
@@ -161,27 +167,52 @@ For the development example, add to `/etc/hosts`:
161167 ### Simple Deployment
162168 ```bash
163169 # 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 .
168172
169173 # Push to deploy
170-docker push registry.your-domain.com/myapp:latest
174+docker push registry.your-domain.com/myapp:$VERSION
171175 ```
172176
173177 ### Version Management
174178 ```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
177183
178184 # Update config
179185 # "app.your-domain.com" = "myapp:v1.2.0"
186+```
180187
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
183212 ```
184213
214+See [`rollback-workflow-example.md`](rollback-workflow-example.md) for complete version deployment workflow documentation.
215+
185216 ## 🤝 Need Help?
186217
187218 - 📖 **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 strategies46 - Production and development logging strategies
47 - Log rotation and monitoring examples47 - 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 Start55 ## 🚀 Quick Start
50 56
51 1. **Choose an example** that matches your use case57 1. **Choose an example** that matches your use case
@@ -161,27 +167,52 @@ For the development example, add to `/etc/hosts`:
161 ### Simple Deployment167 ### Simple Deployment
162 ```bash168 ```bash
163 # Build your app169 # 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 deploy173 # Push to deploy
170-docker push registry.your-domain.com/myapp:latest174+docker push registry.your-domain.com/myapp:$VERSION
171 ```175 ```
172 176
173 ### Version Management177 ### Version Management
174 ```bash178 ```bash
175-# Tag with version179+# Build and push specific version
176-docker tag myapp:latest registry.your-domain.com/myapp:v1.2.0180+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 config184 # 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 version188+### Simple Version Deployment with Manifest Annotations
182-docker push registry.your-domain.com/myapp:v1.2.0189+```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 documentation218 - 📖 **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
55 require (
66 github.com/docker/docker v28.2.2+incompatible
77 github.com/docker/go-connections v0.5.0
8+ github.com/google/uuid v1.6.0
89 github.com/rs/zerolog v1.34.0
910 github.com/spf13/cobra v1.9.1
1011 github.com/spf13/viper v1.20.1
12+ gopkg.in/natefinch/lumberjack.v2 v2.2.1
1113 )
1214
13-require gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
14-
1515 require (
1616 github.com/Microsoft/go-winio v0.4.14 // indirect
1717 github.com/containerd/errdefs v1.0.0 // indirect
@@ -25,7 +25,6 @@ require (
2525 github.com/go-logr/stdr v1.2.2 // indirect
2626 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
2727 github.com/gogo/protobuf v1.3.2 // indirect
28- github.com/google/uuid v1.6.0 // direct
2928 github.com/inconshreveable/mousetrap v1.1.0 // indirect
3029 github.com/mattn/go-colorable v0.1.13 // indirect
3130 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+incompatible6 github.com/docker/docker v28.2.2+incompatible
7 github.com/docker/go-connections v0.5.07 github.com/docker/go-connections v0.5.0
8+ github.com/google/uuid v1.6.0
8 github.com/rs/zerolog v1.34.09 github.com/rs/zerolog v1.34.0
9 github.com/spf13/cobra v1.9.110 github.com/spf13/cobra v1.9.1
10 github.com/spf13/viper v1.20.111 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 // indirect16 github.com/Microsoft/go-winio v0.4.14 // indirect
17 github.com/containerd/errdefs v1.0.0 // indirect17 github.com/containerd/errdefs v1.0.0 // indirect
@@ -25,7 +25,6 @@ require (
25 github.com/go-logr/stdr v1.2.2 // indirect25 github.com/go-logr/stdr v1.2.2 // indirect
26 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect26 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
27 github.com/gogo/protobuf v1.3.2 // indirect27 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 // indirect28 github.com/inconshreveable/mousetrap v1.1.0 // indirect
30 github.com/mattn/go-colorable v0.1.13 // indirect29 github.com/mattn/go-colorable v0.1.13 // indirect
31 github.com/mattn/go-isatty v0.0.19 // indirect30 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 {
339339 return nil
340340 }
341341
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+
342416 // validateEnvConfig validates the environment configuration
343417 func validateEnvConfig(envCfg *EnvConfig) error {
344418 // Validate env directory path
@@ -339,6 +339,80 @@ func (c *Config) AddRoute(domain, image string) error {
339 return nil339 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 configuration416 // validateEnvConfig validates the environment configuration
343 func validateEnvConfig(envCfg *EnvConfig) error {417 func validateEnvConfig(envCfg *EnvConfig) error {
344 // Validate env directory path418 // Validate env directory path
M internal/events/handlers.go +9 -0
@@ -340,6 +340,15 @@ func (h *AutoRouteHandler) Handle(event Event) error {
340340 return fmt.Errorf("invalid payload type for ImagePushed event")
341341 }
342342
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+
343352 imageName := payload.Name
344353 reference := payload.Reference
345354 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.Name352 imageName := payload.Name
344 reference := payload.Reference353 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 {
2828 }
2929
3030 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"`
3435 }
3536
3637 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 (
1111 "gordon/internal/config"
1212 "gordon/internal/events"
1313 "gordon/internal/middleware"
14+ "gordon/pkg/manifest"
1415
1516 "github.com/rs/zerolog/log"
1617 )
@@ -130,11 +131,28 @@ func (s *Server) handlePutManifest(w http.ResponseWriter, r *http.Request) {
130131 w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference))
131132 w.WriteHeader(http.StatusCreated)
132133
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+
133150 // Emit image pushed event
134151 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,
138156 })
139157 }
140158
@@ -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 event150 // 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+}⏎̸

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close