feat(proxy): support gordon.proxy.port label for port selection
- Check for gordon.proxy.port label on image to determine proxy port - Fall back to first exposed port if label not present - Use container.Image (actual running image) instead of route.Image - Add debug logging for proxy target resolution
- commit
- a340e3d8f980a67d12f3bcfe7d5cf7efb088fcc5
- author
- brice · · 2026-01-14 06:23 UTC
- parent
- 757fa05
- signature
- unsigned
1 file · +54 -5
M internal/usecase/proxy/service.go +54 -5
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | "net/http/httputil" | |
| 9 | 9 | "net/url" | |
| 10 | 10 | "os" | |
| 11 | + | "strconv" | |
| 11 | 12 | "strings" | |
| 12 | 13 | "sync" | |
| 13 | 14 | ||
| @@ -79,6 +80,12 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
| 79 | 80 | return | |
| 80 | 81 | } | |
| 81 | 82 | ||
| 83 | + | log.Debug(). | |
| 84 | + | Str("host", target.Host). | |
| 85 | + | Int("port", target.Port). | |
| 86 | + | Str("container_id", target.ContainerID). | |
| 87 | + | Msg("resolved proxy target") | |
| 88 | + | ||
| 82 | 89 | s.proxyToTarget(w, r, target) | |
| 83 | 90 | } | |
| 84 | 91 | ||
| @@ -95,6 +102,11 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (*domain.Pro | |||
| 95 | 102 | s.mu.RLock() | |
| 96 | 103 | if target, exists := s.targets[domainName]; exists { | |
| 97 | 104 | s.mu.RUnlock() | |
| 105 | + | log.Debug(). | |
| 106 | + | Str("host", target.Host). | |
| 107 | + | Int("port", target.Port). | |
| 108 | + | Str("container_id", target.ContainerID). | |
| 109 | + | Msg("using cached proxy target") | |
| 98 | 110 | return target, nil | |
| 99 | 111 | } | |
| 100 | 112 | s.mu.RUnlock() | |
| @@ -102,8 +114,10 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (*domain.Pro | |||
| 102 | 114 | // Get container for this domain | |
| 103 | 115 | container, exists := s.containerSvc.Get(ctx, domainName) | |
| 104 | 116 | if !exists { | |
| 117 | + | log.Debug().Msg("container not found for domain") | |
| 105 | 118 | return nil, domain.ErrNoTargetAvailable | |
| 106 | 119 | } | |
| 120 | + | log.Debug().Str("container_id", container.ID).Str("image", container.Image).Msg("found container for domain") | |
| 107 | 121 | ||
| 108 | 122 | // Build target based on runtime mode | |
| 109 | 123 | var target *domain.ProxyTarget | |
| @@ -135,14 +149,16 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (*domain.Pro | |||
| 135 | 149 | return nil, domain.ErrRouteNotFound | |
| 136 | 150 | } | |
| 137 | 151 | ||
| 138 | - | exposedPorts, err := s.runtime.GetImageExposedPorts(ctx, route.Image) | |
| 139 | - | if err != nil || len(exposedPorts) == 0 { | |
| 140 | - | return nil, log.WrapErr(err, "failed to get exposed ports") | |
| 152 | + | // Determine target port: check for label first, then fall back to first exposed port | |
| 153 | + | // Use container.Image (the actual running image) instead of route.Image (config shorthand) | |
| 154 | + | targetPort, err := s.getProxyPort(ctx, container.Image) | |
| 155 | + | if err != nil { | |
| 156 | + | return nil, log.WrapErr(err, "failed to determine proxy port") | |
| 141 | 157 | } | |
| 142 | 158 | ||
| 143 | - | hostPort, err := s.runtime.GetContainerPort(ctx, container.ID, exposedPorts[0]) | |
| 159 | + | hostPort, err := s.runtime.GetContainerPort(ctx, container.ID, targetPort) | |
| 144 | 160 | if err != nil { | |
| 145 | - | return nil, log.WrapErrWithFields(err, "failed to get host port mapping", map[string]any{"internal_port": exposedPorts[0]}) | |
| 161 | + | return nil, log.WrapErrWithFields(err, "failed to get host port mapping", map[string]any{"internal_port": targetPort}) | |
| 146 | 162 | } | |
| 147 | 163 | ||
| 148 | 164 | target = &domain.ProxyTarget{ | |
| @@ -297,3 +313,36 @@ func (s *Service) isRunningInContainer() bool { | |||
| 297 | 313 | ||
| 298 | 314 | return false | |
| 299 | 315 | } | |
| 316 | + | ||
| 317 | + | // getProxyPort determines the port to proxy to for an image. | |
| 318 | + | // It checks for the gordon.proxy.port label first, then falls back to the first exposed port. | |
| 319 | + | func (s *Service) getProxyPort(ctx context.Context, imageRef string) (int, error) { | |
| 320 | + | log := zerowrap.FromCtx(ctx) | |
| 321 | + | ||
| 322 | + | log.Debug().Str("image_ref", imageRef).Msg("determining proxy port for image") | |
| 323 | + | ||
| 324 | + | // Check for explicit port label | |
| 325 | + | labels, err := s.runtime.GetImageLabels(ctx, imageRef) | |
| 326 | + | if err != nil { | |
| 327 | + | log.Debug().Err(err).Msg("failed to get image labels, falling back to exposed ports") | |
| 328 | + | } else if portStr, ok := labels[domain.LabelProxyPort]; ok { | |
| 329 | + | port, err := strconv.Atoi(portStr) | |
| 330 | + | if err == nil && port > 0 { | |
| 331 | + | log.Debug().Int("port", port).Msg("using proxy port from image label") | |
| 332 | + | return port, nil | |
| 333 | + | } | |
| 334 | + | log.Warn().Str("port_value", portStr).Msg("invalid gordon.proxy.port label value") | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | // Fall back to first exposed port | |
| 338 | + | exposedPorts, err := s.runtime.GetImageExposedPorts(ctx, imageRef) | |
| 339 | + | if err != nil { | |
| 340 | + | return 0, err | |
| 341 | + | } | |
| 342 | + | if len(exposedPorts) == 0 { | |
| 343 | + | return 0, fmt.Errorf("no exposed ports found for image %s", imageRef) | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | log.Debug().Int("port", exposedPorts[0]).Msg("using first exposed port") | |
| 347 | + | return exposedPorts[0], nil | |
| 348 | + | } | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | "net/http/httputil" | 8 | "net/http/httputil" | ||
| 9 | "net/url" | 9 | "net/url" | ||
| 10 | "os" | 10 | "os" | ||
| 11 | + | "strconv" | |||
| 11 | "strings" | 12 | "strings" | ||
| 12 | "sync" | 13 | "sync" | ||
| 13 | 14 | ||||
| @@ -79,6 +80,12 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||||
| 79 | return | 80 | return | ||
| 80 | } | 81 | } | ||
| 81 | 82 | ||||
| 83 | + | log.Debug(). | |||
| 84 | + | Str("host", target.Host). | |||
| 85 | + | Int("port", target.Port). | |||
| 86 | + | Str("container_id", target.ContainerID). | |||
| 87 | + | Msg("resolved proxy target") | |||
| 88 | + | ||||
| 82 | s.proxyToTarget(w, r, target) | 89 | s.proxyToTarget(w, r, target) | ||
| 83 | } | 90 | } | ||
| 84 | 91 | ||||
| @@ -95,6 +102,11 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (*domain.Pro | |||||
| 95 | s.mu.RLock() | 102 | s.mu.RLock() | ||
| 96 | if target, exists := s.targets[domainName]; exists { | 103 | if target, exists := s.targets[domainName]; exists { | ||
| 97 | s.mu.RUnlock() | 104 | s.mu.RUnlock() | ||
| 105 | + | log.Debug(). | |||
| 106 | + | Str("host", target.Host). | |||
| 107 | + | Int("port", target.Port). | |||
| 108 | + | Str("container_id", target.ContainerID). | |||
| 109 | + | Msg("using cached proxy target") | |||
| 98 | return target, nil | 110 | return target, nil | ||
| 99 | } | 111 | } | ||
| 100 | s.mu.RUnlock() | 112 | s.mu.RUnlock() | ||
| @@ -102,8 +114,10 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (*domain.Pro | |||||
| 102 | // Get container for this domain | 114 | // Get container for this domain | ||
| 103 | container, exists := s.containerSvc.Get(ctx, domainName) | 115 | container, exists := s.containerSvc.Get(ctx, domainName) | ||
| 104 | if !exists { | 116 | if !exists { | ||
| 117 | + | log.Debug().Msg("container not found for domain") | |||
| 105 | return nil, domain.ErrNoTargetAvailable | 118 | return nil, domain.ErrNoTargetAvailable | ||
| 106 | } | 119 | } | ||
| 120 | + | log.Debug().Str("container_id", container.ID).Str("image", container.Image).Msg("found container for domain") | |||
| 107 | 121 | ||||
| 108 | // Build target based on runtime mode | 122 | // Build target based on runtime mode | ||
| 109 | var target *domain.ProxyTarget | 123 | var target *domain.ProxyTarget | ||
| @@ -135,14 +149,16 @@ func (s *Service) GetTarget(ctx context.Context, domainName string) (*domain.Pro | |||||
| 135 | return nil, domain.ErrRouteNotFound | 149 | return nil, domain.ErrRouteNotFound | ||
| 136 | } | 150 | } | ||
| 137 | 151 | ||||
| 138 | - | exposedPorts, err := s.runtime.GetImageExposedPorts(ctx, route.Image) | 152 | + | // Determine target port: check for label first, then fall back to first exposed port |
| 139 | - | if err != nil || len(exposedPorts) == 0 { | 153 | + | // Use container.Image (the actual running image) instead of route.Image (config shorthand) |
| 140 | - | return nil, log.WrapErr(err, "failed to get exposed ports") | 154 | + | targetPort, err := s.getProxyPort(ctx, container.Image) |
| 155 | + | if err != nil { | |||
| 156 | + | return nil, log.WrapErr(err, "failed to determine proxy port") | |||
| 141 | } | 157 | } | ||
| 142 | 158 | ||||
| 143 | - | hostPort, err := s.runtime.GetContainerPort(ctx, container.ID, exposedPorts[0]) | 159 | + | hostPort, err := s.runtime.GetContainerPort(ctx, container.ID, targetPort) |
| 144 | if err != nil { | 160 | if err != nil { | ||
| 145 | - | return nil, log.WrapErrWithFields(err, "failed to get host port mapping", map[string]any{"internal_port": exposedPorts[0]}) | 161 | + | return nil, log.WrapErrWithFields(err, "failed to get host port mapping", map[string]any{"internal_port": targetPort}) |
| 146 | } | 162 | } | ||
| 147 | 163 | ||||
| 148 | target = &domain.ProxyTarget{ | 164 | target = &domain.ProxyTarget{ | ||
| @@ -297,3 +313,36 @@ func (s *Service) isRunningInContainer() bool { | |||||
| 297 | 313 | ||||
| 298 | return false | 314 | return false | ||
| 299 | } | 315 | } | ||
| 316 | + | ||||
| 317 | + | // getProxyPort determines the port to proxy to for an image. | |||
| 318 | + | // It checks for the gordon.proxy.port label first, then falls back to the first exposed port. | |||
| 319 | + | func (s *Service) getProxyPort(ctx context.Context, imageRef string) (int, error) { | |||
| 320 | + | log := zerowrap.FromCtx(ctx) | |||
| 321 | + | ||||
| 322 | + | log.Debug().Str("image_ref", imageRef).Msg("determining proxy port for image") | |||
| 323 | + | ||||
| 324 | + | // Check for explicit port label | |||
| 325 | + | labels, err := s.runtime.GetImageLabels(ctx, imageRef) | |||
| 326 | + | if err != nil { | |||
| 327 | + | log.Debug().Err(err).Msg("failed to get image labels, falling back to exposed ports") | |||
| 328 | + | } else if portStr, ok := labels[domain.LabelProxyPort]; ok { | |||
| 329 | + | port, err := strconv.Atoi(portStr) | |||
| 330 | + | if err == nil && port > 0 { | |||
| 331 | + | log.Debug().Int("port", port).Msg("using proxy port from image label") | |||
| 332 | + | return port, nil | |||
| 333 | + | } | |||
| 334 | + | log.Warn().Str("port_value", portStr).Msg("invalid gordon.proxy.port label value") | |||
| 335 | + | } | |||
| 336 | + | ||||
| 337 | + | // Fall back to first exposed port | |||
| 338 | + | exposedPorts, err := s.runtime.GetImageExposedPorts(ctx, imageRef) | |||
| 339 | + | if err != nil { | |||
| 340 | + | return 0, err | |||
| 341 | + | } | |||
| 342 | + | if len(exposedPorts) == 0 { | |||
| 343 | + | return 0, fmt.Errorf("no exposed ports found for image %s", imageRef) | |||
| 344 | + | } | |||
| 345 | + | ||||
| 346 | + | log.Debug().Int("port", exposedPorts[0]).Msg("using first exposed port") | |||
| 347 | + | return exposedPorts[0], nil | |||
| 348 | + | } | |||