detached d9ab304

feat(container): Enhance config reload handling and container deployment

- Implement handleConfigReload function to manage new and changed routes based on configuration updates.
- Add logic to deploy containers for new routes and stop/remove containers for removed routes.
- Refactor CreateRuntime and related functions to accept context for improved runtime management.
- Improve logging for route detection, image availability checks, and deployment processes.
commit
d9ab3047f5779d16fe68a95f6bbde26d844bba52
author
bamen · · 2025-06-07 13:13 UTC
parent
702f8ae
signature
unsigned
4 files · +231 -41
M cmd/start.go +214 -2
@@ -2,8 +2,10 @@ package cmd
22
33 import (
44 "context"
5+ "fmt"
56 "os"
67 "os/signal"
8+ "strings"
79 "sync"
810 "syscall"
911 "time"
@@ -65,6 +67,9 @@ func runStart(cmd *cobra.Command, args []string) {
6567 log.Fatal().Err(err).Msg("Failed to subscribe container event handler")
6668 }
6769
70+ // Create context for the entire server lifecycle
71+ ctx, cancel := context.WithCancel(context.Background())
72+
6873 // Setup config file watching for live reload
6974 viper.WatchConfig()
7075 viper.OnConfigChange(func(e fsnotify.Event) {
@@ -80,6 +85,12 @@ func runStart(cmd *cobra.Command, args []string) {
8085 return
8186 }
8287
88+ // Check for new routes and deploy containers if needed
89+ if err := handleConfigReload(ctx, cfg, newCfg, manager); err != nil {
90+ log.Error().Err(err).Msg("Failed to handle config reload")
91+ return
92+ }
93+
8394 // Update the configuration reference
8495 cfg = newCfg
8596
@@ -99,7 +110,6 @@ func runStart(cmd *cobra.Command, args []string) {
99110 })
100111
101112 // Sync existing containers
102- ctx, cancel := context.WithCancel(context.Background())
103113 if err := manager.SyncContainers(ctx); err != nil {
104114 log.Warn().Err(err).Msg("Failed to sync existing containers")
105115 }
@@ -146,7 +156,7 @@ func runStart(cmd *cobra.Command, args []string) {
146156 // Stop all managed containers with a separate context for shutdown
147157 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
148158 defer shutdownCancel()
149-
159+
150160 log.Info().Msg("Stopping all managed containers...")
151161 if err := manager.StopAllManagedContainers(shutdownCtx); err != nil {
152162 log.Error().Err(err).Msg("Failed to stop managed containers")
@@ -171,3 +181,205 @@ func runStart(cmd *cobra.Command, args []string) {
171181 log.Warn().Msg("Force shutdown after timeout")
172182 }
173183 }
184+
185+// handleConfigReload compares old and new configurations to detect new routes
186+// and automatically deploys containers for them if images are available
187+func handleConfigReload(ctx context.Context, oldCfg, newCfg *config.Config, manager *container.Manager) error {
188+ oldRoutes := make(map[string]string) // domain -> image
189+ newRoutes := make(map[string]string) // domain -> image
190+
191+ // Build maps of routes for comparison
192+ for _, route := range oldCfg.GetRoutes() {
193+ oldRoutes[route.Domain] = route.Image
194+ }
195+
196+ for _, route := range newCfg.GetRoutes() {
197+ newRoutes[route.Domain] = route.Image
198+ }
199+
200+ // Find new routes and changed routes
201+ var routesToDeploy []config.Route
202+
203+ for domain, newImage := range newRoutes {
204+ if oldImage, exists := oldRoutes[domain]; !exists {
205+ // New route detected
206+ log.Info().Str("domain", domain).Str("image", newImage).Msg("New route detected in config")
207+ routesToDeploy = append(routesToDeploy, config.Route{
208+ Domain: domain,
209+ Image: newImage,
210+ HTTPS: true, // Default to HTTPS
211+ })
212+ } else if oldImage != newImage {
213+ // Route with changed image
214+ log.Info().
215+ Str("domain", domain).
216+ Str("old_image", oldImage).
217+ Str("new_image", newImage).
218+ Msg("Route image changed in config")
219+ routesToDeploy = append(routesToDeploy, config.Route{
220+ Domain: domain,
221+ Image: newImage,
222+ HTTPS: true, // Default to HTTPS
223+ })
224+ }
225+ }
226+
227+ // Check for removed routes
228+ for domain := range oldRoutes {
229+ if _, exists := newRoutes[domain]; !exists {
230+ log.Info().Str("domain", domain).Msg("Route removed from config, stopping container")
231+ if err := manager.StopContainerByDomain(ctx, domain); err != nil {
232+ log.Warn().Err(err).Str("domain", domain).Msg("Failed to stop container for removed route")
233+ }
234+ if err := manager.RemoveContainerByDomain(ctx, domain, true); err != nil {
235+ log.Warn().Err(err).Str("domain", domain).Msg("Failed to remove container for removed route")
236+ }
237+ }
238+ }
239+
240+ if len(routesToDeploy) == 0 {
241+ log.Info().Msg("No new or changed routes detected")
242+ return nil
243+ }
244+
245+ log.Info().Int("count", len(routesToDeploy)).Msg("Processing new/changed routes")
246+
247+ // For each new/changed route, check if image is available and deploy
248+ deployedCount := 0
249+ for _, route := range routesToDeploy {
250+ if err := deployRouteIfImageAvailable(ctx, route, newCfg, manager); err != nil {
251+ log.Error().
252+ Err(err).
253+ Str("domain", route.Domain).
254+ Str("image", route.Image).
255+ Msg("Failed to deploy route")
256+ } else {
257+ deployedCount++
258+ }
259+ }
260+
261+ log.Info().
262+ Int("deployed", deployedCount).
263+ Int("total", len(routesToDeploy)).
264+ Msg("Config reload deployment completed")
265+
266+ return nil
267+}
268+
269+// deployRouteIfImageAvailable checks if an image is available and deploys the container
270+func deployRouteIfImageAvailable(ctx context.Context, route config.Route, cfg *config.Config, manager *container.Manager) error {
271+ // Build the full image reference
272+ imageRef := route.Image
273+
274+ // If registry auth is enabled and image doesn't already contain a registry domain, prepend it
275+ if cfg.RegistryAuth.Enabled && cfg.Server.RegistryDomain != "" {
276+ // Check if image already contains a registry domain (has a '.' and doesn't start with official Docker Hub libraries)
277+ if !strings.Contains(strings.Split(imageRef, ":")[0], ".") {
278+ imageRef = fmt.Sprintf("%s/%s", cfg.Server.RegistryDomain, route.Image)
279+ }
280+ }
281+
282+ log.Info().
283+ Str("domain", route.Domain).
284+ Str("image", route.Image).
285+ Str("full_image_ref", imageRef).
286+ Msg("Checking if image is available for new route")
287+
288+ // Check if image is available locally first
289+ images, err := manager.Runtime().ListImages(ctx)
290+ if err != nil {
291+ log.Warn().Err(err).Msg("Failed to list local images, will attempt to pull")
292+ } else {
293+ // Check if image exists locally
294+ imageAvailableLocally := false
295+ normalizedImageRef := normalizeImageRef(imageRef)
296+
297+ for _, localImage := range images {
298+ if normalizeImageRef(localImage) == normalizedImageRef {
299+ imageAvailableLocally = true
300+ log.Info().
301+ Str("domain", route.Domain).
302+ Str("image", imageRef).
303+ Msg("Image available locally, deploying container")
304+ break
305+ }
306+ }
307+
308+ if imageAvailableLocally {
309+ // Image is available locally, deploy immediately
310+ _, err := manager.DeployContainer(ctx, route)
311+ if err != nil {
312+ return fmt.Errorf("failed to deploy container for route %s: %w", route.Domain, err)
313+ }
314+
315+ log.Info().
316+ Str("domain", route.Domain).
317+ Str("image", route.Image).
318+ Msg("Container deployed successfully for new route")
319+
320+ return nil
321+ }
322+ }
323+
324+ // Image not available locally, try to pull it
325+ log.Info().
326+ Str("domain", route.Domain).
327+ Str("image", imageRef).
328+ Msg("Image not available locally, attempting to pull")
329+
330+ // Use authenticated pull if registry auth is configured
331+ if cfg.RegistryAuth.Enabled {
332+ err = manager.Runtime().PullImageWithAuth(ctx, imageRef, cfg.RegistryAuth.Username, cfg.RegistryAuth.Password)
333+ } else {
334+ err = manager.Runtime().PullImage(ctx, imageRef)
335+ }
336+
337+ if err != nil {
338+ log.Warn().
339+ Err(err).
340+ Str("domain", route.Domain).
341+ Str("image", imageRef).
342+ Msg("Failed to pull image for new route, container will be deployed when image becomes available")
343+ return fmt.Errorf("image %s not available for route %s: %w", imageRef, route.Domain, err)
344+ }
345+
346+ log.Info().
347+ Str("domain", route.Domain).
348+ Str("image", imageRef).
349+ Msg("Image pulled successfully, deploying container")
350+
351+ // Image pulled successfully, now deploy the container
352+ _, err = manager.DeployContainer(ctx, route)
353+ if err != nil {
354+ return fmt.Errorf("failed to deploy container for route %s after pulling image: %w", route.Domain, err)
355+ }
356+
357+ log.Info().
358+ Str("domain", route.Domain).
359+ Str("image", route.Image).
360+ Msg("Container deployed successfully for new route after pulling image")
361+
362+ return nil
363+}
364+
365+// normalizeImageRef normalizes image references for comparison
366+func normalizeImageRef(imageRef string) string {
367+ // Split image and tag
368+ parts := strings.Split(imageRef, ":")
369+ image := parts[0]
370+ tag := "latest"
371+ if len(parts) > 1 {
372+ tag = parts[1]
373+ }
374+
375+ // Normalize Docker Hub references
376+ if !strings.Contains(image, "/") {
377+ // Official library image (e.g., "nginx" -> "docker.io/library/nginx")
378+ image = "docker.io/library/" + image
379+ } else if strings.Count(image, "/") == 1 && !strings.Contains(strings.Split(image, "/")[0], ".") {
380+ // User repository (e.g., "user/repo" -> "docker.io/user/repo")
381+ image = "docker.io/" + image
382+ }
383+
384+ return image + ":" + tag
385+}
@@ -2,8 +2,10 @@ package cmd
2 2
3 import (3 import (
4 "context"4 "context"
5+ "fmt"
5 "os"6 "os"
6 "os/signal"7 "os/signal"
8+ "strings"
7 "sync"9 "sync"
8 "syscall"10 "syscall"
9 "time"11 "time"
@@ -65,6 +67,9 @@ func runStart(cmd *cobra.Command, args []string) {
65 log.Fatal().Err(err).Msg("Failed to subscribe container event handler")67 log.Fatal().Err(err).Msg("Failed to subscribe container event handler")
66 }68 }
67 69
70+ // Create context for the entire server lifecycle
71+ ctx, cancel := context.WithCancel(context.Background())
72+
68 // Setup config file watching for live reload73 // Setup config file watching for live reload
69 viper.WatchConfig()74 viper.WatchConfig()
70 viper.OnConfigChange(func(e fsnotify.Event) {75 viper.OnConfigChange(func(e fsnotify.Event) {
@@ -80,6 +85,12 @@ func runStart(cmd *cobra.Command, args []string) {
80 return85 return
81 }86 }
82 87
88+ // Check for new routes and deploy containers if needed
89+ if err := handleConfigReload(ctx, cfg, newCfg, manager); err != nil {
90+ log.Error().Err(err).Msg("Failed to handle config reload")
91+ return
92+ }
93+
83 // Update the configuration reference94 // Update the configuration reference
84 cfg = newCfg95 cfg = newCfg
85 96
@@ -99,7 +110,6 @@ func runStart(cmd *cobra.Command, args []string) {
99 })110 })
100 111
101 // Sync existing containers112 // Sync existing containers
102- ctx, cancel := context.WithCancel(context.Background())
103 if err := manager.SyncContainers(ctx); err != nil {113 if err := manager.SyncContainers(ctx); err != nil {
104 log.Warn().Err(err).Msg("Failed to sync existing containers")114 log.Warn().Err(err).Msg("Failed to sync existing containers")
105 }115 }
@@ -146,7 +156,7 @@ func runStart(cmd *cobra.Command, args []string) {
146 // Stop all managed containers with a separate context for shutdown156 // Stop all managed containers with a separate context for shutdown
147 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)157 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
148 defer shutdownCancel()158 defer shutdownCancel()
149- 159+
150 log.Info().Msg("Stopping all managed containers...")160 log.Info().Msg("Stopping all managed containers...")
151 if err := manager.StopAllManagedContainers(shutdownCtx); err != nil {161 if err := manager.StopAllManagedContainers(shutdownCtx); err != nil {
152 log.Error().Err(err).Msg("Failed to stop managed containers")162 log.Error().Err(err).Msg("Failed to stop managed containers")
@@ -171,3 +181,205 @@ func runStart(cmd *cobra.Command, args []string) {
171 log.Warn().Msg("Force shutdown after timeout")181 log.Warn().Msg("Force shutdown after timeout")
172 }182 }
173 }183 }
184+
185+// handleConfigReload compares old and new configurations to detect new routes
186+// and automatically deploys containers for them if images are available
187+func handleConfigReload(ctx context.Context, oldCfg, newCfg *config.Config, manager *container.Manager) error {
188+ oldRoutes := make(map[string]string) // domain -> image
189+ newRoutes := make(map[string]string) // domain -> image
190+
191+ // Build maps of routes for comparison
192+ for _, route := range oldCfg.GetRoutes() {
193+ oldRoutes[route.Domain] = route.Image
194+ }
195+
196+ for _, route := range newCfg.GetRoutes() {
197+ newRoutes[route.Domain] = route.Image
198+ }
199+
200+ // Find new routes and changed routes
201+ var routesToDeploy []config.Route
202+
203+ for domain, newImage := range newRoutes {
204+ if oldImage, exists := oldRoutes[domain]; !exists {
205+ // New route detected
206+ log.Info().Str("domain", domain).Str("image", newImage).Msg("New route detected in config")
207+ routesToDeploy = append(routesToDeploy, config.Route{
208+ Domain: domain,
209+ Image: newImage,
210+ HTTPS: true, // Default to HTTPS
211+ })
212+ } else if oldImage != newImage {
213+ // Route with changed image
214+ log.Info().
215+ Str("domain", domain).
216+ Str("old_image", oldImage).
217+ Str("new_image", newImage).
218+ Msg("Route image changed in config")
219+ routesToDeploy = append(routesToDeploy, config.Route{
220+ Domain: domain,
221+ Image: newImage,
222+ HTTPS: true, // Default to HTTPS
223+ })
224+ }
225+ }
226+
227+ // Check for removed routes
228+ for domain := range oldRoutes {
229+ if _, exists := newRoutes[domain]; !exists {
230+ log.Info().Str("domain", domain).Msg("Route removed from config, stopping container")
231+ if err := manager.StopContainerByDomain(ctx, domain); err != nil {
232+ log.Warn().Err(err).Str("domain", domain).Msg("Failed to stop container for removed route")
233+ }
234+ if err := manager.RemoveContainerByDomain(ctx, domain, true); err != nil {
235+ log.Warn().Err(err).Str("domain", domain).Msg("Failed to remove container for removed route")
236+ }
237+ }
238+ }
239+
240+ if len(routesToDeploy) == 0 {
241+ log.Info().Msg("No new or changed routes detected")
242+ return nil
243+ }
244+
245+ log.Info().Int("count", len(routesToDeploy)).Msg("Processing new/changed routes")
246+
247+ // For each new/changed route, check if image is available and deploy
248+ deployedCount := 0
249+ for _, route := range routesToDeploy {
250+ if err := deployRouteIfImageAvailable(ctx, route, newCfg, manager); err != nil {
251+ log.Error().
252+ Err(err).
253+ Str("domain", route.Domain).
254+ Str("image", route.Image).
255+ Msg("Failed to deploy route")
256+ } else {
257+ deployedCount++
258+ }
259+ }
260+
261+ log.Info().
262+ Int("deployed", deployedCount).
263+ Int("total", len(routesToDeploy)).
264+ Msg("Config reload deployment completed")
265+
266+ return nil
267+}
268+
269+// deployRouteIfImageAvailable checks if an image is available and deploys the container
270+func deployRouteIfImageAvailable(ctx context.Context, route config.Route, cfg *config.Config, manager *container.Manager) error {
271+ // Build the full image reference
272+ imageRef := route.Image
273+
274+ // If registry auth is enabled and image doesn't already contain a registry domain, prepend it
275+ if cfg.RegistryAuth.Enabled && cfg.Server.RegistryDomain != "" {
276+ // Check if image already contains a registry domain (has a '.' and doesn't start with official Docker Hub libraries)
277+ if !strings.Contains(strings.Split(imageRef, ":")[0], ".") {
278+ imageRef = fmt.Sprintf("%s/%s", cfg.Server.RegistryDomain, route.Image)
279+ }
280+ }
281+
282+ log.Info().
283+ Str("domain", route.Domain).
284+ Str("image", route.Image).
285+ Str("full_image_ref", imageRef).
286+ Msg("Checking if image is available for new route")
287+
288+ // Check if image is available locally first
289+ images, err := manager.Runtime().ListImages(ctx)
290+ if err != nil {
291+ log.Warn().Err(err).Msg("Failed to list local images, will attempt to pull")
292+ } else {
293+ // Check if image exists locally
294+ imageAvailableLocally := false
295+ normalizedImageRef := normalizeImageRef(imageRef)
296+
297+ for _, localImage := range images {
298+ if normalizeImageRef(localImage) == normalizedImageRef {
299+ imageAvailableLocally = true
300+ log.Info().
301+ Str("domain", route.Domain).
302+ Str("image", imageRef).
303+ Msg("Image available locally, deploying container")
304+ break
305+ }
306+ }
307+
308+ if imageAvailableLocally {
309+ // Image is available locally, deploy immediately
310+ _, err := manager.DeployContainer(ctx, route)
311+ if err != nil {
312+ return fmt.Errorf("failed to deploy container for route %s: %w", route.Domain, err)
313+ }
314+
315+ log.Info().
316+ Str("domain", route.Domain).
317+ Str("image", route.Image).
318+ Msg("Container deployed successfully for new route")
319+
320+ return nil
321+ }
322+ }
323+
324+ // Image not available locally, try to pull it
325+ log.Info().
326+ Str("domain", route.Domain).
327+ Str("image", imageRef).
328+ Msg("Image not available locally, attempting to pull")
329+
330+ // Use authenticated pull if registry auth is configured
331+ if cfg.RegistryAuth.Enabled {
332+ err = manager.Runtime().PullImageWithAuth(ctx, imageRef, cfg.RegistryAuth.Username, cfg.RegistryAuth.Password)
333+ } else {
334+ err = manager.Runtime().PullImage(ctx, imageRef)
335+ }
336+
337+ if err != nil {
338+ log.Warn().
339+ Err(err).
340+ Str("domain", route.Domain).
341+ Str("image", imageRef).
342+ Msg("Failed to pull image for new route, container will be deployed when image becomes available")
343+ return fmt.Errorf("image %s not available for route %s: %w", imageRef, route.Domain, err)
344+ }
345+
346+ log.Info().
347+ Str("domain", route.Domain).
348+ Str("image", imageRef).
349+ Msg("Image pulled successfully, deploying container")
350+
351+ // Image pulled successfully, now deploy the container
352+ _, err = manager.DeployContainer(ctx, route)
353+ if err != nil {
354+ return fmt.Errorf("failed to deploy container for route %s after pulling image: %w", route.Domain, err)
355+ }
356+
357+ log.Info().
358+ Str("domain", route.Domain).
359+ Str("image", route.Image).
360+ Msg("Container deployed successfully for new route after pulling image")
361+
362+ return nil
363+}
364+
365+// normalizeImageRef normalizes image references for comparison
366+func normalizeImageRef(imageRef string) string {
367+ // Split image and tag
368+ parts := strings.Split(imageRef, ":")
369+ image := parts[0]
370+ tag := "latest"
371+ if len(parts) > 1 {
372+ tag = parts[1]
373+ }
374+
375+ // Normalize Docker Hub references
376+ if !strings.Contains(image, "/") {
377+ // Official library image (e.g., "nginx" -> "docker.io/library/nginx")
378+ image = "docker.io/library/" + image
379+ } else if strings.Count(image, "/") == 1 && !strings.Contains(strings.Split(image, "/")[0], ".") {
380+ // User repository (e.g., "user/repo" -> "docker.io/user/repo")
381+ image = "docker.io/" + image
382+ }
383+
384+ return image + ":" + tag
385+}
D examples/minimal.toml +0 -22
@@ -1,22 +0,0 @@
1-# Gordon v2 - Minimal Configuration Example
2-# Simplest possible setup to get started
3-
4-[server]
5-runtime = "auto" # auto-detects Docker/Podman
6-ssl_email = "you@example.com"
7-
8-# Just one route - perfect for getting started
9-[routes]
10-"example.com" = "nginx:latest"
11-
12-# That's it! Gordon will use these defaults:
13-# - port = 8080 (main proxy)
14-# - registry_port = 5000 (internal registry)
15-# - registry_auth.enabled = false (no auth)
16-# - data_dir = "./data" (local storage)
17-
18-# Setup:
19-# 1. Point example.com DNS to your server
20-# 2. docker tag nginx:latest localhost:5000/nginx:latest
21-# 3. docker push localhost:5000/nginx:latest
22-# 4. Visit https://example.com (via Cloudflare) → nginx container!⏎̸
@@ -1,22 +0,0 @@
1-# Gordon v2 - Minimal Configuration Example
2-# Simplest possible setup to get started
3-
4-[server]
5-runtime = "auto" # auto-detects Docker/Podman
6-ssl_email = "you@example.com"
7-
8-# Just one route - perfect for getting started
9-[routes]
10-"example.com" = "nginx:latest"
11-
12-# That's it! Gordon will use these defaults:
13-# - port = 8080 (main proxy)
14-# - registry_port = 5000 (internal registry)
15-# - registry_auth.enabled = false (no auth)
16-# - data_dir = "./data" (local storage)
17-
18-# Setup:
19-# 1. Point example.com DNS to your server
20-# 2. docker tag nginx:latest localhost:5000/nginx:latest
21-# 3. docker push localhost:5000/nginx:latest
22-# 4. Visit https://example.com (via Cloudflare) → nginx container!⏎̸
M internal/container/factory.go +13 -15
@@ -32,7 +32,7 @@ type RuntimeInfo struct {
3232 }
3333
3434 // CreateRuntime creates a container runtime based on configuration
35-func CreateRuntime(cfg *config.Config) (runtime.Runtime, error) {
35+func CreateRuntime(ctx context.Context, cfg *config.Config) (runtime.Runtime, error) {
3636 runtimeType := RuntimeType(cfg.Server.Runtime)
3737
3838 // Get socket path from config or environment
@@ -40,11 +40,11 @@ func CreateRuntime(cfg *config.Config) (runtime.Runtime, error) {
4040
4141 switch runtimeType {
4242 case RuntimeAuto:
43- return createAutoRuntime(socketPath)
43+ return createAutoRuntime(ctx, socketPath)
4444 case RuntimeDocker:
45- return createDockerRuntime(socketPath)
45+ return createDockerRuntime(ctx, socketPath)
4646 case RuntimePodman, RuntimePodmanRootless:
47- return createPodmanRuntime(socketPath, runtimeType == RuntimePodmanRootless)
47+ return createPodmanRuntime(ctx, socketPath, runtimeType == RuntimePodmanRootless)
4848 default:
4949 return nil, fmt.Errorf("unsupported runtime: %s", runtimeType)
5050 }
@@ -69,7 +69,7 @@ func getSocketPath(configPath string) string {
6969 }
7070
7171 // createAutoRuntime automatically detects and creates the best available runtime
72-func createAutoRuntime(customSocketPath string) (runtime.Runtime, error) {
72+func createAutoRuntime(ctx context.Context, customSocketPath string) (runtime.Runtime, error) {
7373 var candidatePaths []string
7474
7575 if customSocketPath != "" {
@@ -82,7 +82,7 @@ func createAutoRuntime(customSocketPath string) (runtime.Runtime, error) {
8282
8383 // Try each socket path
8484 for _, socketPath := range candidatePaths {
85- if rt, info, err := tryCreateRuntime(socketPath); err == nil {
85+ if rt, info, err := tryCreateRuntime(ctx, socketPath); err == nil {
8686 log.Info().
8787 Str("runtime", string(info.Type)).
8888 Str("socket", info.SocketPath).
@@ -96,7 +96,7 @@ func createAutoRuntime(customSocketPath string) (runtime.Runtime, error) {
9696 }
9797
9898 // createDockerRuntime creates a Docker runtime with optional custom socket
99-func createDockerRuntime(socketPath string) (runtime.Runtime, error) {
99+func createDockerRuntime(ctx context.Context, socketPath string) (runtime.Runtime, error) {
100100 var opts []client.Opt
101101 opts = append(opts, client.FromEnv, client.WithAPIVersionNegotiation())
102102
@@ -110,7 +110,6 @@ func createDockerRuntime(socketPath string) (runtime.Runtime, error) {
110110 }
111111
112112 // Test connectivity
113- ctx := context.Background()
114113 if _, err := cli.Ping(ctx); err != nil {
115114 return nil, fmt.Errorf("Docker ping failed: %w", err)
116115 }
@@ -119,16 +118,16 @@ func createDockerRuntime(socketPath string) (runtime.Runtime, error) {
119118 }
120119
121120 // createPodmanRuntime creates a Podman runtime with optional custom socket
122-func createPodmanRuntime(socketPath string, rootless bool) (runtime.Runtime, error) {
121+func createPodmanRuntime(ctx context.Context, socketPath string, rootless bool) (runtime.Runtime, error) {
123122 if socketPath == "" {
124123 socketPath = getDefaultPodmanSocket(rootless)
125124 }
126125
127- return createPodmanRuntimeWithSocket(socketPath)
126+ return createPodmanRuntimeWithSocket(ctx, socketPath)
128127 }
129128
130129 // tryCreateRuntime attempts to create a runtime for a given socket path
131-func tryCreateRuntime(socketPath string) (runtime.Runtime, *RuntimeInfo, error) {
130+func tryCreateRuntime(ctx context.Context, socketPath string) (runtime.Runtime, *RuntimeInfo, error) {
132131 // Try to create client
133132 opts := []client.Opt{
134133 client.WithHost(socketPath),
@@ -141,7 +140,6 @@ func tryCreateRuntime(socketPath string) (runtime.Runtime, *RuntimeInfo, error)
141140 }
142141
143142 // Test connectivity and get version
144- ctx := context.Background()
145143 ping, err := cli.Ping(ctx)
146144 if err != nil {
147145 return nil, nil, fmt.Errorf("ping failed: %w", err)
@@ -229,7 +227,7 @@ func getDefaultPodmanSocket(rootless bool) string {
229227 }
230228
231229 // createPodmanRuntimeWithSocket creates a Podman runtime for a specific socket
232-func createPodmanRuntimeWithSocket(socketPath string) (runtime.Runtime, error) {
230+func createPodmanRuntimeWithSocket(ctx context.Context, socketPath string) (runtime.Runtime, error) {
233231 opts := []client.Opt{
234232 client.WithHost(socketPath),
235233 client.WithAPIVersionNegotiation(),
@@ -241,7 +239,6 @@ func createPodmanRuntimeWithSocket(socketPath string) (runtime.Runtime, error) {
241239 }
242240
243241 // Test connectivity
244- ctx := context.Background()
245242 if _, err := cli.Ping(ctx); err != nil {
246243 return nil, fmt.Errorf("Podman ping failed: %w", err)
247244 }
@@ -252,9 +249,10 @@ func createPodmanRuntimeWithSocket(socketPath string) (runtime.Runtime, error) {
252249 // GetAvailableRuntimes returns information about all available runtimes
253250 func GetAvailableRuntimes() []RuntimeInfo {
254251 var runtimes []RuntimeInfo
252+ ctx := context.Background()
255253
256254 for _, socketPath := range getDefaultSocketPaths() {
257- if _, info, err := tryCreateRuntime(socketPath); err == nil {
255+ if _, info, err := tryCreateRuntime(ctx, socketPath); err == nil {
258256 runtimes = append(runtimes, *info)
259257 }
260258 }
@@ -32,7 +32,7 @@ type RuntimeInfo struct {
32 }32 }
33 33
34 // CreateRuntime creates a container runtime based on configuration34 // CreateRuntime creates a container runtime based on configuration
35-func CreateRuntime(cfg *config.Config) (runtime.Runtime, error) {35+func CreateRuntime(ctx context.Context, cfg *config.Config) (runtime.Runtime, error) {
36 runtimeType := RuntimeType(cfg.Server.Runtime)36 runtimeType := RuntimeType(cfg.Server.Runtime)
37 37
38 // Get socket path from config or environment38 // Get socket path from config or environment
@@ -40,11 +40,11 @@ func CreateRuntime(cfg *config.Config) (runtime.Runtime, error) {
40 40
41 switch runtimeType {41 switch runtimeType {
42 case RuntimeAuto:42 case RuntimeAuto:
43- return createAutoRuntime(socketPath)43+ return createAutoRuntime(ctx, socketPath)
44 case RuntimeDocker:44 case RuntimeDocker:
45- return createDockerRuntime(socketPath)45+ return createDockerRuntime(ctx, socketPath)
46 case RuntimePodman, RuntimePodmanRootless:46 case RuntimePodman, RuntimePodmanRootless:
47- return createPodmanRuntime(socketPath, runtimeType == RuntimePodmanRootless)47+ return createPodmanRuntime(ctx, socketPath, runtimeType == RuntimePodmanRootless)
48 default:48 default:
49 return nil, fmt.Errorf("unsupported runtime: %s", runtimeType)49 return nil, fmt.Errorf("unsupported runtime: %s", runtimeType)
50 }50 }
@@ -69,7 +69,7 @@ func getSocketPath(configPath string) string {
69 }69 }
70 70
71 // createAutoRuntime automatically detects and creates the best available runtime71 // createAutoRuntime automatically detects and creates the best available runtime
72-func createAutoRuntime(customSocketPath string) (runtime.Runtime, error) {72+func createAutoRuntime(ctx context.Context, customSocketPath string) (runtime.Runtime, error) {
73 var candidatePaths []string73 var candidatePaths []string
74 74
75 if customSocketPath != "" {75 if customSocketPath != "" {
@@ -82,7 +82,7 @@ func createAutoRuntime(customSocketPath string) (runtime.Runtime, error) {
82 82
83 // Try each socket path83 // Try each socket path
84 for _, socketPath := range candidatePaths {84 for _, socketPath := range candidatePaths {
85- if rt, info, err := tryCreateRuntime(socketPath); err == nil {85+ if rt, info, err := tryCreateRuntime(ctx, socketPath); err == nil {
86 log.Info().86 log.Info().
87 Str("runtime", string(info.Type)).87 Str("runtime", string(info.Type)).
88 Str("socket", info.SocketPath).88 Str("socket", info.SocketPath).
@@ -96,7 +96,7 @@ func createAutoRuntime(customSocketPath string) (runtime.Runtime, error) {
96 }96 }
97 97
98 // createDockerRuntime creates a Docker runtime with optional custom socket98 // createDockerRuntime creates a Docker runtime with optional custom socket
99-func createDockerRuntime(socketPath string) (runtime.Runtime, error) {99+func createDockerRuntime(ctx context.Context, socketPath string) (runtime.Runtime, error) {
100 var opts []client.Opt100 var opts []client.Opt
101 opts = append(opts, client.FromEnv, client.WithAPIVersionNegotiation())101 opts = append(opts, client.FromEnv, client.WithAPIVersionNegotiation())
102 102
@@ -110,7 +110,6 @@ func createDockerRuntime(socketPath string) (runtime.Runtime, error) {
110 }110 }
111 111
112 // Test connectivity112 // Test connectivity
113- ctx := context.Background()
114 if _, err := cli.Ping(ctx); err != nil {113 if _, err := cli.Ping(ctx); err != nil {
115 return nil, fmt.Errorf("Docker ping failed: %w", err)114 return nil, fmt.Errorf("Docker ping failed: %w", err)
116 }115 }
@@ -119,16 +118,16 @@ func createDockerRuntime(socketPath string) (runtime.Runtime, error) {
119 }118 }
120 119
121 // createPodmanRuntime creates a Podman runtime with optional custom socket120 // createPodmanRuntime creates a Podman runtime with optional custom socket
122-func createPodmanRuntime(socketPath string, rootless bool) (runtime.Runtime, error) {121+func createPodmanRuntime(ctx context.Context, socketPath string, rootless bool) (runtime.Runtime, error) {
123 if socketPath == "" {122 if socketPath == "" {
124 socketPath = getDefaultPodmanSocket(rootless)123 socketPath = getDefaultPodmanSocket(rootless)
125 }124 }
126 125
127- return createPodmanRuntimeWithSocket(socketPath)126+ return createPodmanRuntimeWithSocket(ctx, socketPath)
128 }127 }
129 128
130 // tryCreateRuntime attempts to create a runtime for a given socket path129 // tryCreateRuntime attempts to create a runtime for a given socket path
131-func tryCreateRuntime(socketPath string) (runtime.Runtime, *RuntimeInfo, error) {130+func tryCreateRuntime(ctx context.Context, socketPath string) (runtime.Runtime, *RuntimeInfo, error) {
132 // Try to create client131 // Try to create client
133 opts := []client.Opt{132 opts := []client.Opt{
134 client.WithHost(socketPath),133 client.WithHost(socketPath),
@@ -141,7 +140,6 @@ func tryCreateRuntime(socketPath string) (runtime.Runtime, *RuntimeInfo, error)
141 }140 }
142 141
143 // Test connectivity and get version142 // Test connectivity and get version
144- ctx := context.Background()
145 ping, err := cli.Ping(ctx)143 ping, err := cli.Ping(ctx)
146 if err != nil {144 if err != nil {
147 return nil, nil, fmt.Errorf("ping failed: %w", err)145 return nil, nil, fmt.Errorf("ping failed: %w", err)
@@ -229,7 +227,7 @@ func getDefaultPodmanSocket(rootless bool) string {
229 }227 }
230 228
231 // createPodmanRuntimeWithSocket creates a Podman runtime for a specific socket229 // createPodmanRuntimeWithSocket creates a Podman runtime for a specific socket
232-func createPodmanRuntimeWithSocket(socketPath string) (runtime.Runtime, error) {230+func createPodmanRuntimeWithSocket(ctx context.Context, socketPath string) (runtime.Runtime, error) {
233 opts := []client.Opt{231 opts := []client.Opt{
234 client.WithHost(socketPath),232 client.WithHost(socketPath),
235 client.WithAPIVersionNegotiation(),233 client.WithAPIVersionNegotiation(),
@@ -241,7 +239,6 @@ func createPodmanRuntimeWithSocket(socketPath string) (runtime.Runtime, error) {
241 }239 }
242 240
243 // Test connectivity241 // Test connectivity
244- ctx := context.Background()
245 if _, err := cli.Ping(ctx); err != nil {242 if _, err := cli.Ping(ctx); err != nil {
246 return nil, fmt.Errorf("Podman ping failed: %w", err)243 return nil, fmt.Errorf("Podman ping failed: %w", err)
247 }244 }
@@ -252,9 +249,10 @@ func createPodmanRuntimeWithSocket(socketPath string) (runtime.Runtime, error) {
252 // GetAvailableRuntimes returns information about all available runtimes249 // GetAvailableRuntimes returns information about all available runtimes
253 func GetAvailableRuntimes() []RuntimeInfo {250 func GetAvailableRuntimes() []RuntimeInfo {
254 var runtimes []RuntimeInfo251 var runtimes []RuntimeInfo
252+ ctx := context.Background()
255 253
256 for _, socketPath := range getDefaultSocketPaths() {254 for _, socketPath := range getDefaultSocketPaths() {
257- if _, info, err := tryCreateRuntime(socketPath); err == nil {255+ if _, info, err := tryCreateRuntime(ctx, socketPath); err == nil {
258 runtimes = append(runtimes, *info)256 runtimes = append(runtimes, *info)
259 }257 }
260 }258 }
M internal/container/manager.go +4 -2
@@ -22,14 +22,16 @@ type Manager struct {
2222
2323 // NewManager creates a new container manager
2424 func NewManager(cfg *config.Config) (*Manager, error) {
25+ // Test runtime connectivity with background context
26+ ctx := context.Background()
27+
2528 // Create runtime using the factory
26- rt, err := CreateRuntime(cfg)
29+ rt, err := CreateRuntime(ctx, cfg)
2730 if err != nil {
2831 return nil, fmt.Errorf("failed to create container runtime: %w", err)
2932 }
3033
3134 // Test runtime connectivity
32- ctx := context.Background()
3335 if err := rt.Ping(ctx); err != nil {
3436 return nil, fmt.Errorf("runtime not available: %w", err)
3537 }
@@ -22,14 +22,16 @@ type Manager struct {
22 22
23 // NewManager creates a new container manager23 // NewManager creates a new container manager
24 func NewManager(cfg *config.Config) (*Manager, error) {24 func NewManager(cfg *config.Config) (*Manager, error) {
25+ // Test runtime connectivity with background context
26+ ctx := context.Background()
27+
25 // Create runtime using the factory28 // Create runtime using the factory
26- rt, err := CreateRuntime(cfg)29+ rt, err := CreateRuntime(ctx, cfg)
27 if err != nil {30 if err != nil {
28 return nil, fmt.Errorf("failed to create container runtime: %w", err)31 return nil, fmt.Errorf("failed to create container runtime: %w", err)
29 }32 }
30 33
31 // Test runtime connectivity34 // Test runtime connectivity
32- ctx := context.Background()
33 if err := rt.Ping(ctx); err != nil {35 if err := rt.Ping(ctx); err != nil {
34 return nil, fmt.Errorf("runtime not available: %w", err)36 return nil, fmt.Errorf("runtime not available: %w", err)
35 }37 }

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