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
- 48759e49fc92818a54b1870a5eab6c958a95a492
- author
- bamen · · 2025-06-07 13:13 UTC
- parent
- cf615ec
- signature
- unsigned
4 files · +231 -41
M cmd/start.go +214 -2
| @@ -2,8 +2,10 @@ package cmd | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "fmt" | |
| 5 | 6 | "os" | |
| 6 | 7 | "os/signal" | |
| 8 | + | "strings" | |
| 7 | 9 | "sync" | |
| 8 | 10 | "syscall" | |
| 9 | 11 | "time" | |
| @@ -65,6 +67,9 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 65 | 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 | 73 | // Setup config file watching for live reload | |
| 69 | 74 | viper.WatchConfig() | |
| 70 | 75 | viper.OnConfigChange(func(e fsnotify.Event) { | |
| @@ -80,6 +85,12 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 80 | 85 | 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 | 94 | // Update the configuration reference | |
| 84 | 95 | cfg = newCfg | |
| 85 | 96 | ||
| @@ -99,7 +110,6 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 99 | 110 | }) | |
| 100 | 111 | ||
| 101 | 112 | // Sync existing containers | |
| 102 | - | ctx, cancel := context.WithCancel(context.Background()) | |
| 103 | 113 | if err := manager.SyncContainers(ctx); err != nil { | |
| 104 | 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 | 156 | // Stop all managed containers with a separate context for shutdown | |
| 147 | 157 | shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) | |
| 148 | 158 | defer shutdownCancel() | |
| 149 | - | ||
| 159 | + | ||
| 150 | 160 | log.Info().Msg("Stopping all managed containers...") | |
| 151 | 161 | if err := manager.StopAllManagedContainers(shutdownCtx); err != nil { | |
| 152 | 162 | log.Error().Err(err).Msg("Failed to stop managed containers") | |
| @@ -171,3 +181,205 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 171 | 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 | + | } | |
| @@ -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 reload | 73 | // 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 | return | 85 | 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 reference | 94 | // Update the configuration reference | ||
| 84 | cfg = newCfg | 95 | cfg = newCfg | ||
| 85 | 96 | ||||
| @@ -99,7 +110,6 @@ func runStart(cmd *cobra.Command, args []string) { | |||||
| 99 | }) | 110 | }) | ||
| 100 | 111 | ||||
| 101 | // Sync existing containers | 112 | // 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 shutdown | 156 | // 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 { | |||
| 32 | 32 | } | |
| 33 | 33 | ||
| 34 | 34 | // 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 | 36 | runtimeType := RuntimeType(cfg.Server.Runtime) | |
| 37 | 37 | ||
| 38 | 38 | // Get socket path from config or environment | |
| @@ -40,11 +40,11 @@ func CreateRuntime(cfg *config.Config) (runtime.Runtime, error) { | |||
| 40 | 40 | ||
| 41 | 41 | switch runtimeType { | |
| 42 | 42 | case RuntimeAuto: | |
| 43 | - | return createAutoRuntime(socketPath) | |
| 43 | + | return createAutoRuntime(ctx, socketPath) | |
| 44 | 44 | case RuntimeDocker: | |
| 45 | - | return createDockerRuntime(socketPath) | |
| 45 | + | return createDockerRuntime(ctx, socketPath) | |
| 46 | 46 | case RuntimePodman, RuntimePodmanRootless: | |
| 47 | - | return createPodmanRuntime(socketPath, runtimeType == RuntimePodmanRootless) | |
| 47 | + | return createPodmanRuntime(ctx, socketPath, runtimeType == RuntimePodmanRootless) | |
| 48 | 48 | default: | |
| 49 | 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 | 71 | // 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 | 73 | var candidatePaths []string | |
| 74 | 74 | ||
| 75 | 75 | if customSocketPath != "" { | |
| @@ -82,7 +82,7 @@ func createAutoRuntime(customSocketPath string) (runtime.Runtime, error) { | |||
| 82 | 82 | ||
| 83 | 83 | // Try each socket path | |
| 84 | 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 | 86 | log.Info(). | |
| 87 | 87 | Str("runtime", string(info.Type)). | |
| 88 | 88 | Str("socket", info.SocketPath). | |
| @@ -96,7 +96,7 @@ func createAutoRuntime(customSocketPath string) (runtime.Runtime, error) { | |||
| 96 | 96 | } | |
| 97 | 97 | ||
| 98 | 98 | // 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 | 100 | var opts []client.Opt | |
| 101 | 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 | 112 | // Test connectivity | |
| 113 | - | ctx := context.Background() | |
| 114 | 113 | if _, err := cli.Ping(ctx); err != nil { | |
| 115 | 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 | 120 | // 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 | 122 | if socketPath == "" { | |
| 124 | 123 | socketPath = getDefaultPodmanSocket(rootless) | |
| 125 | 124 | } | |
| 126 | 125 | ||
| 127 | - | return createPodmanRuntimeWithSocket(socketPath) | |
| 126 | + | return createPodmanRuntimeWithSocket(ctx, socketPath) | |
| 128 | 127 | } | |
| 129 | 128 | ||
| 130 | 129 | // 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 | 131 | // Try to create client | |
| 133 | 132 | opts := []client.Opt{ | |
| 134 | 133 | client.WithHost(socketPath), | |
| @@ -141,7 +140,6 @@ func tryCreateRuntime(socketPath string) (runtime.Runtime, *RuntimeInfo, error) | |||
| 141 | 140 | } | |
| 142 | 141 | ||
| 143 | 142 | // Test connectivity and get version | |
| 144 | - | ctx := context.Background() | |
| 145 | 143 | ping, err := cli.Ping(ctx) | |
| 146 | 144 | if err != nil { | |
| 147 | 145 | return nil, nil, fmt.Errorf("ping failed: %w", err) | |
| @@ -229,7 +227,7 @@ func getDefaultPodmanSocket(rootless bool) string { | |||
| 229 | 227 | } | |
| 230 | 228 | ||
| 231 | 229 | // 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 | 231 | opts := []client.Opt{ | |
| 234 | 232 | client.WithHost(socketPath), | |
| 235 | 233 | client.WithAPIVersionNegotiation(), | |
| @@ -241,7 +239,6 @@ func createPodmanRuntimeWithSocket(socketPath string) (runtime.Runtime, error) { | |||
| 241 | 239 | } | |
| 242 | 240 | ||
| 243 | 241 | // Test connectivity | |
| 244 | - | ctx := context.Background() | |
| 245 | 242 | if _, err := cli.Ping(ctx); err != nil { | |
| 246 | 243 | return nil, fmt.Errorf("Podman ping failed: %w", err) | |
| 247 | 244 | } | |
| @@ -252,9 +249,10 @@ func createPodmanRuntimeWithSocket(socketPath string) (runtime.Runtime, error) { | |||
| 252 | 249 | // GetAvailableRuntimes returns information about all available runtimes | |
| 253 | 250 | func GetAvailableRuntimes() []RuntimeInfo { | |
| 254 | 251 | var runtimes []RuntimeInfo | |
| 252 | + | ctx := context.Background() | |
| 255 | 253 | ||
| 256 | 254 | for _, socketPath := range getDefaultSocketPaths() { | |
| 257 | - | if _, info, err := tryCreateRuntime(socketPath); err == nil { | |
| 255 | + | if _, info, err := tryCreateRuntime(ctx, socketPath); err == nil { | |
| 258 | 256 | runtimes = append(runtimes, *info) | |
| 259 | 257 | } | |
| 260 | 258 | } | |
| @@ -32,7 +32,7 @@ type RuntimeInfo struct { | |||||
| 32 | } | 32 | } | ||
| 33 | 33 | ||||
| 34 | // CreateRuntime creates a container runtime based on configuration | 34 | // 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 environment | 38 | // 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 runtime | 71 | // 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 []string | 73 | 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 path | 83 | // 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 socket | 98 | // 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.Opt | 100 | 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 connectivity | 112 | // 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 socket | 120 | // 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 path | 129 | // 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 client | 131 | // 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 version | 142 | // 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 socket | 229 | // 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 connectivity | 241 | // 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 runtimes | 249 | // GetAvailableRuntimes returns information about all available runtimes | ||
| 253 | func GetAvailableRuntimes() []RuntimeInfo { | 250 | func GetAvailableRuntimes() []RuntimeInfo { | ||
| 254 | var runtimes []RuntimeInfo | 251 | 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 { | |||
| 22 | 22 | ||
| 23 | 23 | // NewManager creates a new container manager | |
| 24 | 24 | func NewManager(cfg *config.Config) (*Manager, error) { | |
| 25 | + | // Test runtime connectivity with background context | |
| 26 | + | ctx := context.Background() | |
| 27 | + | ||
| 25 | 28 | // Create runtime using the factory | |
| 26 | - | rt, err := CreateRuntime(cfg) | |
| 29 | + | rt, err := CreateRuntime(ctx, cfg) | |
| 27 | 30 | if err != nil { | |
| 28 | 31 | return nil, fmt.Errorf("failed to create container runtime: %w", err) | |
| 29 | 32 | } | |
| 30 | 33 | ||
| 31 | 34 | // Test runtime connectivity | |
| 32 | - | ctx := context.Background() | |
| 33 | 35 | if err := rt.Ping(ctx); err != nil { | |
| 34 | 36 | return nil, fmt.Errorf("runtime not available: %w", err) | |
| 35 | 37 | } | |
| @@ -22,14 +22,16 @@ type Manager struct { | |||||
| 22 | 22 | ||||
| 23 | // NewManager creates a new container manager | 23 | // 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 factory | 28 | // 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 connectivity | 34 | // 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 | } | ||