fix: honor labeled ports in readiness probes
Readiness probes were still resolving the first exposed port, which could miss the actual proxied service on multi-port images. Probe resolution now respects labeled proxy ports so auto readiness checks match live routing.
- commit
- 50a9fab98539917323a02c8bbb357f1518c01e09
- author
- brice · · 2026-03-12 19:03 UTC
- parent
- a8e226f
- signature
- unsigned
2 files · +40 -22
M internal/usecase/container/service.go +31 -10
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "fmt" | |
| 8 | 8 | "maps" | |
| 9 | 9 | "slices" | |
| 10 | + | "strconv" | |
| 10 | 11 | "strings" | |
| 11 | 12 | "sync" | |
| 12 | 13 | "time" | |
| @@ -2344,7 +2345,7 @@ func (s *Service) tryHTTPProbe(ctx context.Context, containerID string, containe | |||
| 2344 | 2345 | if !ok || healthPath == "" { | |
| 2345 | 2346 | return false, nil | |
| 2346 | 2347 | } | |
| 2347 | - | ip, port, probeErr := s.resolveContainerEndpoint(ctx, containerID) | |
| 2348 | + | ip, port, probeErr := s.resolveProbeEndpoint(ctx, containerID, containerConfig) | |
| 2348 | 2349 | if probeErr != nil || ip == "" || port <= 0 { | |
| 2349 | 2350 | log.Debug().Err(probeErr).Msg("readiness cascade: HTTP probe skipped, could not resolve container endpoint") | |
| 2350 | 2351 | return false, nil | |
| @@ -2367,7 +2368,7 @@ func (s *Service) tryDefaultHTTPProbe(ctx context.Context, containerID string, c | |||
| 2367 | 2368 | if containerConfig == nil || len(containerConfig.Ports) == 0 { | |
| 2368 | 2369 | return false, nil | |
| 2369 | 2370 | } | |
| 2370 | - | ip, port, probeErr := s.resolveContainerEndpoint(ctx, containerID) | |
| 2371 | + | ip, port, probeErr := s.resolveProbeEndpoint(ctx, containerID, containerConfig) | |
| 2371 | 2372 | if probeErr != nil || ip == "" || port <= 0 { | |
| 2372 | 2373 | log.Debug().Err(probeErr).Msg("readiness cascade: default HTTP probe skipped, could not resolve container endpoint") | |
| 2373 | 2374 | return false, nil | |
| @@ -2388,7 +2389,7 @@ func (s *Service) tryTCPProbe(ctx context.Context, containerID string, container | |||
| 2388 | 2389 | if containerConfig == nil || len(containerConfig.Ports) == 0 { | |
| 2389 | 2390 | return false, nil | |
| 2390 | 2391 | } | |
| 2391 | - | ip, port, probeErr := s.resolveContainerEndpoint(ctx, containerID) | |
| 2392 | + | ip, port, probeErr := s.resolveProbeEndpoint(ctx, containerID, containerConfig) | |
| 2392 | 2393 | if probeErr != nil || ip == "" || port <= 0 { | |
| 2393 | 2394 | log.Debug().Err(probeErr).Msg("readiness cascade: TCP probe skipped, could not resolve container endpoint") | |
| 2394 | 2395 | return false, nil | |
| @@ -2407,16 +2408,33 @@ func (s *Service) tryTCPProbe(ctx context.Context, containerID string, container | |||
| 2407 | 2408 | // from the host. We use the host port binding (127.0.0.1:<mapped_port>) | |
| 2408 | 2409 | // which is always reachable. Falls back to the container's internal IP | |
| 2409 | 2410 | // only if no host port mapping exists (e.g. host-network mode). | |
| 2410 | - | func (s *Service) resolveContainerEndpoint(ctx context.Context, containerID string) (string, int, error) { | |
| 2411 | + | func (s *Service) resolveProbeEndpoint(ctx context.Context, containerID string, containerConfig *domain.ContainerConfig) (string, int, error) { | |
| 2411 | 2412 | log := zerowrap.FromCtx(ctx) | |
| 2412 | 2413 | ||
| 2413 | - | // First, get the container's internal port from network info | |
| 2414 | - | _, internalPort, err := s.runtime.GetContainerNetworkInfo(ctx, containerID) | |
| 2415 | - | if err != nil { | |
| 2416 | - | return "", 0, err | |
| 2414 | + | internalPort := 0 | |
| 2415 | + | if containerConfig != nil { | |
| 2416 | + | if labels := containerConfig.Labels; labels != nil { | |
| 2417 | + | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} { | |
| 2418 | + | if portStr, ok := labels[key]; ok && portStr != "" { | |
| 2419 | + | port, err := strconv.Atoi(portStr) | |
| 2420 | + | if err == nil && port > 0 { | |
| 2421 | + | internalPort = port | |
| 2422 | + | break | |
| 2423 | + | } | |
| 2424 | + | log.Warn().Str("label", key).Str("port_value", portStr).Msg("invalid probe port label value") | |
| 2425 | + | } | |
| 2426 | + | } | |
| 2427 | + | } | |
| 2428 | + | } | |
| 2429 | + | ||
| 2430 | + | if internalPort == 0 { | |
| 2431 | + | _, resolvedPort, err := s.runtime.GetContainerNetworkInfo(ctx, containerID) | |
| 2432 | + | if err != nil { | |
| 2433 | + | return "", 0, err | |
| 2434 | + | } | |
| 2435 | + | internalPort = resolvedPort | |
| 2417 | 2436 | } | |
| 2418 | 2437 | ||
| 2419 | - | // Try to resolve via host port binding (works in rootless podman/Docker) | |
| 2420 | 2438 | hostPort, hostErr := s.runtime.GetContainerPort(ctx, containerID, internalPort) | |
| 2421 | 2439 | if hostErr == nil && hostPort > 0 { | |
| 2422 | 2440 | log.Debug(). | |
| @@ -2426,7 +2444,6 @@ func (s *Service) resolveContainerEndpoint(ctx context.Context, containerID stri | |||
| 2426 | 2444 | return "127.0.0.1", hostPort, nil | |
| 2427 | 2445 | } | |
| 2428 | 2446 | ||
| 2429 | - | // Fallback: use internal IP (works in Docker rootful / host network) | |
| 2430 | 2447 | ip, _, fallbackErr := s.runtime.GetContainerNetworkInfo(ctx, containerID) | |
| 2431 | 2448 | if fallbackErr != nil { | |
| 2432 | 2449 | return "", 0, fallbackErr | |
| @@ -2438,6 +2455,10 @@ func (s *Service) resolveContainerEndpoint(ctx context.Context, containerID stri | |||
| 2438 | 2455 | return ip, internalPort, nil | |
| 2439 | 2456 | } | |
| 2440 | 2457 | ||
| 2458 | + | func (s *Service) resolveContainerEndpoint(ctx context.Context, containerID string) (string, int, error) { | |
| 2459 | + | return s.resolveProbeEndpoint(ctx, containerID, nil) | |
| 2460 | + | } | |
| 2461 | + | ||
| 2441 | 2462 | // waitForReadyByDelay waits using the legacy running+delay strategy. | |
| 2442 | 2463 | func (s *Service) waitForReadyByDelay(ctx context.Context, containerID string) error { | |
| 2443 | 2464 | log := zerowrap.FromCtx(ctx) | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | "maps" | 8 | "maps" | ||
| 9 | "slices" | 9 | "slices" | ||
| 10 | + | "strconv" | |||
| 10 | "strings" | 11 | "strings" | ||
| 11 | "sync" | 12 | "sync" | ||
| 12 | "time" | 13 | "time" | ||
| @@ -2344,7 +2345,7 @@ func (s *Service) tryHTTPProbe(ctx context.Context, containerID string, containe | |||||
| 2344 | if !ok || healthPath == "" { | 2345 | if !ok || healthPath == "" { | ||
| 2345 | return false, nil | 2346 | return false, nil | ||
| 2346 | } | 2347 | } | ||
| 2347 | - | ip, port, probeErr := s.resolveContainerEndpoint(ctx, containerID) | 2348 | + | ip, port, probeErr := s.resolveProbeEndpoint(ctx, containerID, containerConfig) |
| 2348 | if probeErr != nil || ip == "" || port <= 0 { | 2349 | if probeErr != nil || ip == "" || port <= 0 { | ||
| 2349 | log.Debug().Err(probeErr).Msg("readiness cascade: HTTP probe skipped, could not resolve container endpoint") | 2350 | log.Debug().Err(probeErr).Msg("readiness cascade: HTTP probe skipped, could not resolve container endpoint") | ||
| 2350 | return false, nil | 2351 | return false, nil | ||
| @@ -2367,7 +2368,7 @@ func (s *Service) tryDefaultHTTPProbe(ctx context.Context, containerID string, c | |||||
| 2367 | if containerConfig == nil || len(containerConfig.Ports) == 0 { | 2368 | if containerConfig == nil || len(containerConfig.Ports) == 0 { | ||
| 2368 | return false, nil | 2369 | return false, nil | ||
| 2369 | } | 2370 | } | ||
| 2370 | - | ip, port, probeErr := s.resolveContainerEndpoint(ctx, containerID) | 2371 | + | ip, port, probeErr := s.resolveProbeEndpoint(ctx, containerID, containerConfig) |
| 2371 | if probeErr != nil || ip == "" || port <= 0 { | 2372 | if probeErr != nil || ip == "" || port <= 0 { | ||
| 2372 | log.Debug().Err(probeErr).Msg("readiness cascade: default HTTP probe skipped, could not resolve container endpoint") | 2373 | log.Debug().Err(probeErr).Msg("readiness cascade: default HTTP probe skipped, could not resolve container endpoint") | ||
| 2373 | return false, nil | 2374 | return false, nil | ||
| @@ -2388,7 +2389,7 @@ func (s *Service) tryTCPProbe(ctx context.Context, containerID string, container | |||||
| 2388 | if containerConfig == nil || len(containerConfig.Ports) == 0 { | 2389 | if containerConfig == nil || len(containerConfig.Ports) == 0 { | ||
| 2389 | return false, nil | 2390 | return false, nil | ||
| 2390 | } | 2391 | } | ||
| 2391 | - | ip, port, probeErr := s.resolveContainerEndpoint(ctx, containerID) | 2392 | + | ip, port, probeErr := s.resolveProbeEndpoint(ctx, containerID, containerConfig) |
| 2392 | if probeErr != nil || ip == "" || port <= 0 { | 2393 | if probeErr != nil || ip == "" || port <= 0 { | ||
| 2393 | log.Debug().Err(probeErr).Msg("readiness cascade: TCP probe skipped, could not resolve container endpoint") | 2394 | log.Debug().Err(probeErr).Msg("readiness cascade: TCP probe skipped, could not resolve container endpoint") | ||
| 2394 | return false, nil | 2395 | return false, nil | ||
| @@ -2407,16 +2408,33 @@ func (s *Service) tryTCPProbe(ctx context.Context, containerID string, container | |||||
| 2407 | // from the host. We use the host port binding (127.0.0.1:<mapped_port>) | 2408 | // from the host. We use the host port binding (127.0.0.1:<mapped_port>) | ||
| 2408 | // which is always reachable. Falls back to the container's internal IP | 2409 | // which is always reachable. Falls back to the container's internal IP | ||
| 2409 | // only if no host port mapping exists (e.g. host-network mode). | 2410 | // only if no host port mapping exists (e.g. host-network mode). | ||
| 2410 | - | func (s *Service) resolveContainerEndpoint(ctx context.Context, containerID string) (string, int, error) { | 2411 | + | func (s *Service) resolveProbeEndpoint(ctx context.Context, containerID string, containerConfig *domain.ContainerConfig) (string, int, error) { |
| 2411 | log := zerowrap.FromCtx(ctx) | 2412 | log := zerowrap.FromCtx(ctx) | ||
| 2412 | 2413 | ||||
| 2413 | - | // First, get the container's internal port from network info | 2414 | + | internalPort := 0 |
| 2414 | - | _, internalPort, err := s.runtime.GetContainerNetworkInfo(ctx, containerID) | 2415 | + | if containerConfig != nil { |
| 2415 | - | if err != nil { | 2416 | + | if labels := containerConfig.Labels; labels != nil { |
| 2416 | - | return "", 0, err | 2417 | + | for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} { |
| 2418 | + | if portStr, ok := labels[key]; ok && portStr != "" { | |||
| 2419 | + | port, err := strconv.Atoi(portStr) | |||
| 2420 | + | if err == nil && port > 0 { | |||
| 2421 | + | internalPort = port | |||
| 2422 | + | break | |||
| 2423 | + | } | |||
| 2424 | + | log.Warn().Str("label", key).Str("port_value", portStr).Msg("invalid probe port label value") | |||
| 2425 | + | } | |||
| 2426 | + | } | |||
| 2427 | + | } | |||
| 2428 | + | } | |||
| 2429 | + | ||||
| 2430 | + | if internalPort == 0 { | |||
| 2431 | + | _, resolvedPort, err := s.runtime.GetContainerNetworkInfo(ctx, containerID) | |||
| 2432 | + | if err != nil { | |||
| 2433 | + | return "", 0, err | |||
| 2434 | + | } | |||
| 2435 | + | internalPort = resolvedPort | |||
| 2417 | } | 2436 | } | ||
| 2418 | 2437 | ||||
| 2419 | - | // Try to resolve via host port binding (works in rootless podman/Docker) | |||
| 2420 | hostPort, hostErr := s.runtime.GetContainerPort(ctx, containerID, internalPort) | 2438 | hostPort, hostErr := s.runtime.GetContainerPort(ctx, containerID, internalPort) | ||
| 2421 | if hostErr == nil && hostPort > 0 { | 2439 | if hostErr == nil && hostPort > 0 { | ||
| 2422 | log.Debug(). | 2440 | log.Debug(). | ||
| @@ -2426,7 +2444,6 @@ func (s *Service) resolveContainerEndpoint(ctx context.Context, containerID stri | |||||
| 2426 | return "127.0.0.1", hostPort, nil | 2444 | return "127.0.0.1", hostPort, nil | ||
| 2427 | } | 2445 | } | ||
| 2428 | 2446 | ||||
| 2429 | - | // Fallback: use internal IP (works in Docker rootful / host network) | |||
| 2430 | ip, _, fallbackErr := s.runtime.GetContainerNetworkInfo(ctx, containerID) | 2447 | ip, _, fallbackErr := s.runtime.GetContainerNetworkInfo(ctx, containerID) | ||
| 2431 | if fallbackErr != nil { | 2448 | if fallbackErr != nil { | ||
| 2432 | return "", 0, fallbackErr | 2449 | return "", 0, fallbackErr | ||
| @@ -2438,6 +2455,10 @@ func (s *Service) resolveContainerEndpoint(ctx context.Context, containerID stri | |||||
| 2438 | return ip, internalPort, nil | 2455 | return ip, internalPort, nil | ||
| 2439 | } | 2456 | } | ||
| 2440 | 2457 | ||||
| 2458 | + | func (s *Service) resolveContainerEndpoint(ctx context.Context, containerID string) (string, int, error) { | |||
| 2459 | + | return s.resolveProbeEndpoint(ctx, containerID, nil) | |||
| 2460 | + | } | |||
| 2461 | + | ||||
| 2441 | // waitForReadyByDelay waits using the legacy running+delay strategy. | 2462 | // waitForReadyByDelay waits using the legacy running+delay strategy. | ||
| 2442 | func (s *Service) waitForReadyByDelay(ctx context.Context, containerID string) error { | 2463 | func (s *Service) waitForReadyByDelay(ctx context.Context, containerID string) error { | ||
| 2443 | log := zerowrap.FromCtx(ctx) | 2464 | log := zerowrap.FromCtx(ctx) | ||
M internal/usecase/container/service_readiness_test.go +9 -12
| @@ -47,10 +47,11 @@ func TestService_WaitForReady_AutoFallsBackToDelayWhenNoHealthcheck(t *testing.T | |||
| 47 | 47 | assert.NoError(t, err) | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | - | func TestService_WaitForReady_AutoCascadeUsesTCPWhenNoHealthcheckAndNoHealthLabel(t *testing.T) { | |
| 50 | + | func TestService_WaitForReady_AutoCascadeUsesDefaultHTTPAliveProbeWhenNoHealthcheckAndNoHealthLabel(t *testing.T) { | |
| 51 | 51 | runtime := mocks.NewMockContainerRuntime(t) | |
| 52 | 52 | svc := NewService(runtime, nil, nil, nil, Config{ | |
| 53 | - | ReadinessMode: "auto", | |
| 53 | + | ReadinessMode: "auto", | |
| 54 | + | HTTPProbeTimeout: 50 * time.Millisecond, | |
| 54 | 55 | }) | |
| 55 | 56 | ||
| 56 | 57 | ctx := testContext() | |
| @@ -68,21 +69,17 @@ func TestService_WaitForReady_AutoCascadeUsesTCPWhenNoHealthcheckAndNoHealthLabe | |||
| 68 | 69 | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | |
| 69 | 70 | // No Docker healthcheck | |
| 70 | 71 | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | |
| 71 | - | // Cascade resolves container endpoint for TCP probe | |
| 72 | + | // Cascade resolves container endpoint for default HTTP alive probe | |
| 72 | 73 | runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, containerID).Return(probeIP, probePort, nil).Once() | |
| 73 | 74 | // Host port binding resolution — return the same loopback addr so probe hits it | |
| 74 | 75 | runtime.EXPECT().GetContainerPort(mock.Anything, containerID, probePort).Return(probePort, nil).Once() | |
| 75 | 76 | ||
| 76 | - | // TCP probe will try to connect — which will fail since the port is closed. | |
| 77 | - | // Use a short health timeout so the test doesn't hang. | |
| 78 | - | svc.mu.Lock() | |
| 79 | - | svc.config.TCPProbeTimeout = 50 * time.Millisecond | |
| 80 | - | svc.mu.Unlock() | |
| 77 | + | // Default HTTP alive probe will try to connect — which will fail since the port is closed. | |
| 81 | 78 | ||
| 82 | 79 | err := svc.waitForReady(ctx, containerID, containerConfig) | |
| 83 | - | // Expect TCP probe timeout (no server listening) | |
| 80 | + | // Expect HTTP alive probe timeout (no server listening) | |
| 84 | 81 | assert.Error(t, err) | |
| 85 | - | assert.Contains(t, err.Error(), "TCP probe timeout") | |
| 82 | + | assert.Contains(t, err.Error(), "HTTP alive probe timeout") | |
| 86 | 83 | } | |
| 87 | 84 | ||
| 88 | 85 | func TestService_WaitForReady_AutoCascadeUsesHTTPProbeWhenHealthLabelSet(t *testing.T) { | |
| @@ -169,8 +166,8 @@ func TestService_WaitForReady_AutoCascadeFallsToDelayWhenNoEndpoint(t *testing.T | |||
| 169 | 166 | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | |
| 170 | 167 | // No Docker healthcheck | |
| 171 | 168 | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | |
| 172 | - | // Network info fails — can't do TCP probe, fall through to delay | |
| 173 | - | runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, containerID).Return("", 0, assert.AnError).Once() | |
| 169 | + | // Network info fails for default HTTP alive probe, then again for TCP fallback before delay | |
| 170 | + | runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, containerID).Return("", 0, assert.AnError).Twice() | |
| 174 | 171 | // Delay-mode fallback verification | |
| 175 | 172 | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | |
| 176 | 173 | ||
| @@ -47,10 +47,11 @@ func TestService_WaitForReady_AutoFallsBackToDelayWhenNoHealthcheck(t *testing.T | |||||
| 47 | assert.NoError(t, err) | 47 | assert.NoError(t, err) | ||
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | - | func TestService_WaitForReady_AutoCascadeUsesTCPWhenNoHealthcheckAndNoHealthLabel(t *testing.T) { | 50 | + | func TestService_WaitForReady_AutoCascadeUsesDefaultHTTPAliveProbeWhenNoHealthcheckAndNoHealthLabel(t *testing.T) { |
| 51 | runtime := mocks.NewMockContainerRuntime(t) | 51 | runtime := mocks.NewMockContainerRuntime(t) | ||
| 52 | svc := NewService(runtime, nil, nil, nil, Config{ | 52 | svc := NewService(runtime, nil, nil, nil, Config{ | ||
| 53 | - | ReadinessMode: "auto", | 53 | + | ReadinessMode: "auto", |
| 54 | + | HTTPProbeTimeout: 50 * time.Millisecond, | |||
| 54 | }) | 55 | }) | ||
| 55 | 56 | ||||
| 56 | ctx := testContext() | 57 | ctx := testContext() | ||
| @@ -68,21 +69,17 @@ func TestService_WaitForReady_AutoCascadeUsesTCPWhenNoHealthcheckAndNoHealthLabe | |||||
| 68 | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | 69 | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | ||
| 69 | // No Docker healthcheck | 70 | // No Docker healthcheck | ||
| 70 | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | 71 | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | ||
| 71 | - | // Cascade resolves container endpoint for TCP probe | 72 | + | // Cascade resolves container endpoint for default HTTP alive probe |
| 72 | runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, containerID).Return(probeIP, probePort, nil).Once() | 73 | runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, containerID).Return(probeIP, probePort, nil).Once() | ||
| 73 | // Host port binding resolution — return the same loopback addr so probe hits it | 74 | // Host port binding resolution — return the same loopback addr so probe hits it | ||
| 74 | runtime.EXPECT().GetContainerPort(mock.Anything, containerID, probePort).Return(probePort, nil).Once() | 75 | runtime.EXPECT().GetContainerPort(mock.Anything, containerID, probePort).Return(probePort, nil).Once() | ||
| 75 | 76 | ||||
| 76 | - | // TCP probe will try to connect — which will fail since the port is closed. | 77 | + | // Default HTTP alive probe will try to connect — which will fail since the port is closed. |
| 77 | - | // Use a short health timeout so the test doesn't hang. | |||
| 78 | - | svc.mu.Lock() | |||
| 79 | - | svc.config.TCPProbeTimeout = 50 * time.Millisecond | |||
| 80 | - | svc.mu.Unlock() | |||
| 81 | 78 | ||||
| 82 | err := svc.waitForReady(ctx, containerID, containerConfig) | 79 | err := svc.waitForReady(ctx, containerID, containerConfig) | ||
| 83 | - | // Expect TCP probe timeout (no server listening) | 80 | + | // Expect HTTP alive probe timeout (no server listening) |
| 84 | assert.Error(t, err) | 81 | assert.Error(t, err) | ||
| 85 | - | assert.Contains(t, err.Error(), "TCP probe timeout") | 82 | + | assert.Contains(t, err.Error(), "HTTP alive probe timeout") |
| 86 | } | 83 | } | ||
| 87 | 84 | ||||
| 88 | func TestService_WaitForReady_AutoCascadeUsesHTTPProbeWhenHealthLabelSet(t *testing.T) { | 85 | func TestService_WaitForReady_AutoCascadeUsesHTTPProbeWhenHealthLabelSet(t *testing.T) { | ||
| @@ -169,8 +166,8 @@ func TestService_WaitForReady_AutoCascadeFallsToDelayWhenNoEndpoint(t *testing.T | |||||
| 169 | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | 166 | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | ||
| 170 | // No Docker healthcheck | 167 | // No Docker healthcheck | ||
| 171 | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | 168 | runtime.EXPECT().GetContainerHealthStatus(mock.Anything, containerID).Return("", false, nil).Once() | ||
| 172 | - | // Network info fails — can't do TCP probe, fall through to delay | 169 | + | // Network info fails for default HTTP alive probe, then again for TCP fallback before delay |
| 173 | - | runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, containerID).Return("", 0, assert.AnError).Once() | 170 | + | runtime.EXPECT().GetContainerNetworkInfo(mock.Anything, containerID).Return("", 0, assert.AnError).Twice() |
| 174 | // Delay-mode fallback verification | 171 | // Delay-mode fallback verification | ||
| 175 | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | 172 | runtime.EXPECT().IsContainerRunning(mock.Anything, containerID).Return(true, nil).Once() | ||
| 176 | 173 | ||||