driver/kubernetes: Add retry logic for transient TLS connection errors

Fixes #2668

When Kubernetes marks nodes as "Ready" before their Certificate Signing
Requests (CSRs) are approved, the buildx kubernetes driver can fail to
connect to builder pods with transient TLS errors like:
  - "tls: internal error"
  - "context deadline exceeded"
  - "use of closed network connection"
  - "i/o timeout"

This is particularly problematic on EKS clusters with ARM64 nodes under
heavy load, where multiple builders are being spawned simultaneously.

This commit adds retry logic with exponential backoff to the Dial()
function in the kubernetes driver. The implementation:
  - Attempts up to 5 connection retries
  - Uses exponential backoff starting at 500ms, capped at 10s
  - Only retries on known transient connection errors
  - Uses errors.Is/errors.As for proper error type checking
  - Logs retry attempts using logrus for visibility
  - Respects context cancellation

This allows buildx to gracefully handle the race condition where pods
are marked as Running before their TLS certificates are fully ready.

Signed-off-by: guimove <dasilva.guillaume@live.fr>
This commit is contained in:
guimove
2026-01-05 17:46:10 +01:00
parent 62857022a0
commit e758a6b917
+94 -4
View File
@@ -2,9 +2,11 @@ package kubernetes
import (
"context"
stderrors "errors"
"fmt"
"net"
"strings"
"syscall"
"time"
"github.com/docker/buildx/driver"
@@ -17,6 +19,7 @@ import (
"github.com/docker/go-units"
"github.com/moby/buildkit/client"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -212,11 +215,98 @@ func (d *Driver) Dial(ctx context.Context) (net.Conn, error) {
}
containerName := pod.Spec.Containers[0].Name
cmd := []string{"buildctl", "dial-stdio"}
conn, err := execconn.ExecConn(ctx, restClient, restClientConfig, pod.Namespace, pod.Name, containerName, cmd)
if err != nil {
return nil, err
// Retry connection with exponential backoff for transient errors
// See https://github.com/docker/buildx/issues/2668
var conn net.Conn
err = tryWithBackoff(ctx, pod.Name, func() error {
var err error
conn, err = execconn.ExecConn(ctx, restClient, restClientConfig, pod.Namespace, pod.Name, containerName, cmd)
return err
})
return conn, err
}
// tryWithBackoff retries a function with exponential backoff for transient errors.
// This handles the race condition where Kubernetes marks nodes as "Ready" before their
// Certificate Signing Requests (CSRs) are approved, causing transient TLS errors.
func tryWithBackoff(ctx context.Context, podName string, fn func() error) error {
const (
maxRetries = 5
baseDelay = 500 * time.Millisecond
maxDelay = 10 * time.Second
)
var lastErr error
for attempt := range maxRetries {
err := fn()
if err == nil {
return nil
}
lastErr = err
if !isTransientConnectionError(err) {
return err
}
if attempt < maxRetries-1 {
delay := calculateBackoff(attempt, baseDelay, maxDelay)
logrus.Warnf("Transient connection error to pod %s (attempt %d/%d): %v. Retrying in %v...",
podName, attempt+1, maxRetries, err, delay)
select {
case <-ctx.Done():
return context.Cause(ctx)
case <-time.After(delay):
}
}
}
return conn, nil
return errors.Wrapf(lastErr, "failed to connect to pod %s after %d attempts", podName, maxRetries)
}
// isTransientConnectionError checks if an error is transient and should be retried.
func isTransientConnectionError(err error) bool {
if err == nil {
return false
}
// Check for context deadline exceeded
if stderrors.Is(err, context.DeadlineExceeded) {
return true
}
// Check for closed network connection
if stderrors.Is(err, net.ErrClosed) {
return true
}
// Check for timeout errors using net.Error interface
var netErr net.Error
if stderrors.As(err, &netErr) && netErr.Timeout() {
return true
}
// Check for syscall errors (connection refused, connection reset)
var syscallErr syscall.Errno
if stderrors.As(err, &syscallErr) {
if syscallErr == syscall.ECONNREFUSED || syscallErr == syscall.ECONNRESET {
return true
}
}
// TLS internal errors don't have a specific type, so we still need to check the message
if strings.Contains(err.Error(), "tls: internal error") {
return true
}
return false
}
// calculateBackoff calculates the delay for the given attempt with exponential backoff.
func calculateBackoff(attempt int, baseDelay, maxDelay time.Duration) time.Duration {
return min(time.Duration(1<<uint(attempt))*baseDelay, maxDelay)
}
func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client.Client, error) {