From 7bc90e53849747e3ff5cf3e57931e367c15f8f5e Mon Sep 17 00:00:00 2001 From: seemeroland Date: Fri, 24 Jul 2026 21:33:09 +0000 Subject: [PATCH] Improve error message for Docker image pull failures (REMOTE-2322) When oz-agent-worker fails to pull a Docker image due to a network connectivity error (connection refused, dial timeout, no such host), add a hint that the failure may be caused by Docker Hub adding new egress IP addresses not yet allowlisted in the environment. This addresses REMOTE-2322 action item #3: improve user-facing error messages when cloud runs fail due to network connectivity issues. Previously the error was a generic 'failed to pull image X: ' which gave no context about the likely root cause. The improved message distinguishes network errors from other pull failures (auth errors, missing images) so operators can quickly identify allowlist gaps vs. other issues. Co-Authored-By: Oz --- internal/worker/docker.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/internal/worker/docker.go b/internal/worker/docker.go index 5446cd5..f6b7f60 100644 --- a/internal/worker/docker.go +++ b/internal/worker/docker.go @@ -2,8 +2,11 @@ package worker import ( "context" + "errors" "fmt" "io" + "net" + "net/url" "strings" "time" @@ -242,6 +245,32 @@ func (b *DockerBackend) PreservesTasksOnShutdown() bool { return false } +// isDockerNetworkError returns true when err is a network-level failure (dial, +// timeout, connection refused) that suggests the Docker registry may be +// unreachable. These errors are distinct from authentication failures or +// missing images and typically indicate a network-connectivity or IP-allowlist +// issue between the worker and Docker Hub (e.g. Docker Hub added a new egress +// IP not yet allowlisted). See REMOTE-2322. +func isDockerNetworkError(err error) bool { + var netErr *net.OpError + var urlErr *url.Error + switch { + case errors.As(err, &netErr): + return true + case errors.As(err, &urlErr): + // url.Error wraps net.OpError for dial/timeout errors. + return true + default: + // Fall back to string matching for errors surfaced as plain strings + // by the Docker client (e.g. "context deadline exceeded"). + msg := err.Error() + return strings.Contains(msg, "connection refused") || + strings.Contains(msg, "no such host") || + strings.Contains(msg, "dial tcp") || + strings.Contains(msg, "i/o timeout") + } +} + // pullImage pulls a Docker image. If authStr is non-empty, it will be used for registry authentication. // Docker only downloads changed layers, so this is efficient even if the image exists locally. func (b *DockerBackend) pullImage(ctx context.Context, imageName string, authStr string) error { @@ -252,6 +281,14 @@ func (b *DockerBackend) pullImage(ctx context.Context, imageName string, authStr } reader, err := b.dockerClient.ImagePull(ctx, imageName, pullOptions) if err != nil { + if isDockerNetworkError(err) { + return fmt.Errorf( + "failed to pull image %s: network connectivity error reaching Docker Hub — "+ + "this may indicate that Docker Hub has added new egress IP addresses that "+ + "are not yet allowlisted in this environment's network configuration: %w", + imageName, err, + ) + } return fmt.Errorf("failed to pull image %s: %w", imageName, err) } defer func() {