From a60fd7ebb319d5be237b03187469b3ec1f6c5104 Mon Sep 17 00:00:00 2001 From: Desuuuu Date: Sat, 7 Mar 2026 14:48:25 +0100 Subject: [PATCH] feat: improve container conflict detection Modify the container conflict detection regex for improved Podman compatibility. --- docker.go | 2 +- reaper_test.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/docker.go b/docker.go index 9a2421485b..9cfe50fcc3 100644 --- a/docker.go +++ b/docker.go @@ -54,7 +54,7 @@ const ( var ( // createContainerFailDueToNameConflictRegex is a regular expression that matches the container is already in use error. - createContainerFailDueToNameConflictRegex = regexp.MustCompile("Conflict. The container name .* is already in use by container .*") + createContainerFailDueToNameConflictRegex = regexp.MustCompile("[Tt]he container name .* is already in use by .*") // minLogProductionTimeout is the minimum log production timeout. minLogProductionTimeout = time.Duration(5 * time.Second) diff --git a/reaper_test.go b/reaper_test.go index 1eea122f0e..18793854d7 100644 --- a/reaper_test.go +++ b/reaper_test.go @@ -3,12 +3,15 @@ package testcontainers import ( "context" "errors" + "fmt" "os" "strconv" "sync" + "syscall" "testing" "time" + "github.com/cenkalti/backoff/v4" "github.com/containerd/errdefs" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" @@ -532,6 +535,110 @@ func TestSpawnerBackoff(t *testing.T) { } } +type timeoutErr struct{} + +func (timeoutErr) Error() string { + return "timeout" +} + +func (timeoutErr) Timeout() bool { + return true +} + +func TestSpawnerRetryError(t *testing.T) { + t.Run("nil error", func(t *testing.T) { + err := spawner.retryError(nil) + require.NoError(t, err, "should return nil") + }) + + tests := []struct { + name string + err error + permanent bool + }{ + { + name: "docker conflict error", + err: errors.New("Conflict. The container name \"foo\" is already in use by container \"01234\"."), + permanent: false, + }, + { + name: "podman conflict error", + err: errors.New("creating container storage: the container name \"foo\" is already in use by 01234."), + permanent: false, + }, + { + name: "errdefs.ErrNotFound", + err: fmt.Errorf("foo: %w", errdefs.ErrNotFound), + permanent: false, + }, + { + name: "errdefs.Conflict", + err: fmt.Errorf("foo: %w", errdefs.ErrConflict), + permanent: true, + }, + { + name: "syscall.ECONNREFUSED", + err: fmt.Errorf("foo: %w", syscall.ECONNREFUSED), + permanent: false, + }, + { + name: "syscall.ECONNRESET", + err: fmt.Errorf("foo: %w", syscall.ECONNRESET), + permanent: false, + }, + { + name: "syscall.ECONNABORTED", + err: fmt.Errorf("foo: %w", syscall.ECONNABORTED), + permanent: false, + }, + { + name: "syscall.ETIMEDOUT", + err: fmt.Errorf("foo: %w", syscall.ETIMEDOUT), + permanent: false, + }, + { + name: "os.ErrDeadlineExceeded", + err: fmt.Errorf("foo: %w", os.ErrDeadlineExceeded), + permanent: false, + }, + { + name: "context.DeadlineExceeded", + err: fmt.Errorf("foo: %w", context.DeadlineExceeded), + permanent: false, + }, + { + name: "timeout error", + err: fmt.Errorf("foo: %w", timeoutErr{}), + permanent: false, + }, + { + name: "context.Canceled", + err: fmt.Errorf("foo: %w", context.Canceled), + permanent: false, + }, + { + name: "random error", + err: errors.New("some random error"), + permanent: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotErr := spawner.retryError(tt.err) + require.Error(t, gotErr, "should not return nil") + + permanentError := &backoff.PermanentError{} + isPermanent := errors.As(gotErr, &permanentError) + if tt.permanent { + require.True(t, isPermanent, "the error should be a PermanentError") + } else { + require.False(t, isPermanent, "the error should not be a PermanentError") + } + }) + } +} + // cleanupReaper schedules reaper for cleanup if it's not nil. func cleanupReaper(t *testing.T, reaper *Reaper, spawner *reaperSpawner) { t.Helper()