From 8461aea6ffd9dd32eb774b49b50175b5ec1c38e2 Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Thu, 20 Nov 2025 11:38:32 -0500 Subject: [PATCH 1/5] Add the ability to configure the provider type in the properties file or as an env var Using WithProvider may not be portable when some developers running tests may be using Podman and others using Docker. Allowing setting the Provider type in the global configuration or environment ensures that the provider is a per-developer configurable setting. --- internal/config/config.go | 10 +++ internal/config/config_test.go | 39 ++++++++++++ provider.go | 32 +++++++--- provider_test.go | 110 +++++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 7 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index deb8f0a9f8..487bea83c9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -85,6 +85,11 @@ type Config struct { // // Environment variable: TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE TestcontainersHost string `properties:"tc.host,default="` + + // Provider is the container provider to use (e.g., "docker", "podman"). + // + // Environment variable: TESTCONTAINERS_PROVIDER + Provider string `properties:"provider,default="` } // } @@ -141,6 +146,11 @@ func read() Config { config.RyukConnectionTimeout = timeout } + providerEnv := os.Getenv("TESTCONTAINERS_PROVIDER") + if providerEnv != "" { + config.Provider = providerEnv + } + return config } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 591fcff11c..a46ae41624 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -26,6 +26,7 @@ func resetTestEnv(t *testing.T) { t.Setenv("RYUK_VERBOSE", "") t.Setenv("RYUK_RECONNECTION_TIMEOUT", "") t.Setenv("RYUK_CONNECTION_TIMEOUT", "") + t.Setenv("TESTCONTAINERS_PROVIDER", "") } func TestReadConfig(t *testing.T) { @@ -38,12 +39,14 @@ func TestReadConfig(t *testing.T) { t.Setenv("USERPROFILE", "") // Windows support t.Setenv("DOCKER_HOST", "") t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + t.Setenv("TESTCONTAINERS_PROVIDER", "podman") config := Read() expected := Config{ RyukDisabled: true, Host: "", // docker socket is empty at the properties file + Provider: "podman", } require.Equal(t, expected, config) @@ -79,6 +82,7 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") t.Setenv("RYUK_CONNECTION_TIMEOUT", "12s") + t.Setenv("TESTCONTAINERS_PROVIDER", "docker") config := read() @@ -89,6 +93,7 @@ func TestReadTCConfig(t *testing.T) { Host: "", // docker socket is empty at the properties file RyukReconnectionTimeout: 13 * time.Second, RyukConnectionTimeout: 12 * time.Second, + Provider: "docker", } assert.Equal(t, expected, config) @@ -516,6 +521,40 @@ func TestReadTCConfig(t *testing.T) { RyukReconnectionTimeout: defaultRyukReconnectionTimeout, }, }, + { + "With Provider set as property", + `provider=podman`, + map[string]string{}, + Config{ + Provider: "podman", + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + }, + }, + { + "With Provider set as env var", + ``, + map[string]string{ + "TESTCONTAINERS_PROVIDER": "podman", + }, + Config{ + Provider: "podman", + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + }, + }, + { + "With Provider set as env var and properties: Env var wins", + `provider=docker`, + map[string]string{ + "TESTCONTAINERS_PROVIDER": "podman", + }, + Config{ + Provider: "podman", + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/provider.go b/provider.go index d2347b7f3b..a97de022da 100644 --- a/provider.go +++ b/provider.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "os" "strings" "github.com/testcontainers/testcontainers-go/internal/config" @@ -94,6 +93,30 @@ type ContainerProvider interface { Config() TestcontainersConfig } +func (t ProviderType) UnderlyingProviderType() ProviderType { + // Provider set within code has precedence over all others + if t != ProviderDefault { + return t + } + + // Configuration of an explicit provider has the next priority + conf := config.Read() + switch conf.Provider { + case "docker": + return ProviderDocker + case "podman": + return ProviderPodman + } + + // Attempt to auto-detect Podman from the the Docker configuration + if strings.Contains(core.MustExtractDockerHost(context.Background()), "podman.sock") { + return ProviderPodman + } + + // When all else fails, default to Docker + return ProviderDocker +} + // GetProvider provides the provider implementation for a certain type func (t ProviderType) GetProvider(opts ...GenericProviderOption) (GenericProvider, error) { opt := &GenericProviderOptions{ @@ -104,12 +127,7 @@ func (t ProviderType) GetProvider(opts ...GenericProviderOption) (GenericProvide o.ApplyGenericTo(opt) } - pt := t - if pt == ProviderDefault && strings.Contains(os.Getenv("DOCKER_HOST"), "podman.sock") { - pt = ProviderPodman - } - - switch pt { + switch t.UnderlyingProviderType() { case ProviderDefault, ProviderDocker: providerOptions := append(Generic2DockerOptions(opts...), WithDefaultBridgeNetwork(Bridge)) provider, err := NewDockerProvider(providerOptions...) diff --git a/provider_test.go b/provider_test.go index 94206e46bf..10b1992162 100644 --- a/provider_test.go +++ b/provider_test.go @@ -2,13 +2,123 @@ package testcontainers import ( "context" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/internal/core" ) +func TestProviderTypeGetUnderlyingProviderType(t *testing.T) { + tests := []struct { + name string + providerType ProviderType + propertiesFile string // content of .testcontainers.properties + env map[string]string + expectedType ProviderType + }{ + { + name: "ProviderDocker always returns ProviderDocker", + providerType: ProviderDocker, + expectedType: ProviderDocker, + }, + { + name: "ProviderPodman always returns ProviderPodman", + providerType: ProviderPodman, + expectedType: ProviderPodman, + }, + { + name: "ProviderDefault with properties file set to docker", + providerType: ProviderDefault, + propertiesFile: "provider=docker", + expectedType: ProviderDocker, + }, + { + name: "ProviderDefault with properties file set to podman", + providerType: ProviderDefault, + propertiesFile: "provider=podman", + expectedType: ProviderPodman, + }, + { + name: "ProviderDefault with env var set to docker", + providerType: ProviderDefault, + env: map[string]string{ + "TESTCONTAINERS_PROVIDER": "docker", + }, + expectedType: ProviderDocker, + }, + { + name: "ProviderDefault with env var set to podman", + providerType: ProviderDefault, + env: map[string]string{ + "TESTCONTAINERS_PROVIDER": "podman", + }, + expectedType: ProviderPodman, + }, + { + name: "ProviderDefault with env var podman and properties docker - env wins", + providerType: ProviderDefault, + propertiesFile: "provider=docker", + env: map[string]string{ + "TESTCONTAINERS_PROVIDER": "podman", + }, + expectedType: ProviderPodman, + }, + { + name: "ProviderDocker with env var podman and properties podman - explicit provider wins", + providerType: ProviderDocker, + propertiesFile: "provider=podman", + env: map[string]string{ + "TESTCONTAINERS_PROVIDER": "podman", + }, + expectedType: ProviderDocker, + }, + { + name: "ProviderPodman with env var docker and properties docker - explicit provider wins", + providerType: ProviderPodman, + propertiesFile: "provider=docker", + env: map[string]string{ + "TESTCONTAINERS_PROVIDER": "docker", + }, + expectedType: ProviderPodman, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset config for each test to ensure clean state + config.Reset() + + // Create temp directory for HOME + tmpDir := t.TempDir() + t.Setenv("HOME", tmpDir) + t.Setenv("USERPROFILE", tmpDir) // Windows support + + // Set any additional environment variables + for k, v := range tt.env { + t.Setenv(k, v) + } + + // Create properties file if content is provided + if tt.propertiesFile != "" { + err := os.WriteFile( + filepath.Join(tmpDir, ".testcontainers.properties"), + []byte(tt.propertiesFile), + 0600, + ) + require.NoError(t, err, "Failed to create properties file") + } + + // Test UnderlyingProviderType + result := tt.providerType.UnderlyingProviderType() + require.Equal(t, tt.expectedType, result, "UnderlyingProviderType() returned unexpected type") + }) + } +} + func TestProviderTypeGetProviderAutodetect(t *testing.T) { dockerHost := core.MustExtractDockerHost(context.Background()) const podmanSocket = "unix://$XDG_RUNTIME_DIR/podman/podman.sock" From a4f67abd28158b7a94e110a421871b47df2f1995 Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Thu, 20 Nov 2025 11:59:26 -0500 Subject: [PATCH 2/5] Document that the provider can be set in the properties/env var --- docs/system_requirements/using_podman.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/system_requirements/using_podman.md b/docs/system_requirements/using_podman.md index a9692d39b7..3cf57bb2f0 100644 --- a/docs/system_requirements/using_podman.md +++ b/docs/system_requirements/using_podman.md @@ -82,6 +82,8 @@ In order to use Testcontainers then either } ``` +As an alternative to declaring the provider in the tests themselves, you can instead set the `TESTCONTAINERS_PROVIDER` environment variable to `podman` to force all tests to execute with the Podman provider. This may also be set in the .testcontainers.properties file as `provider=podman` in your home directory. + ## Fedora `DOCKER_HOST` environment variable must be set From f5edd607a729c0c5d8cfd5b45a4acd040543b6a9 Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Thu, 20 Nov 2025 12:15:17 -0500 Subject: [PATCH 3/5] Normalize string case comparisons --- provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider.go b/provider.go index a97de022da..dd06de405c 100644 --- a/provider.go +++ b/provider.go @@ -101,7 +101,7 @@ func (t ProviderType) UnderlyingProviderType() ProviderType { // Configuration of an explicit provider has the next priority conf := config.Read() - switch conf.Provider { + switch strings.ToLower(conf.Provider) { case "docker": return ProviderDocker case "podman": From 31c89db8f61abcddc3a6005adc582ac28feb9112 Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Wed, 28 Jan 2026 13:25:27 -0500 Subject: [PATCH 4/5] Fix linter issue --- provider_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider_test.go b/provider_test.go index 10b1992162..b99375c990 100644 --- a/provider_test.go +++ b/provider_test.go @@ -107,7 +107,7 @@ func TestProviderTypeGetUnderlyingProviderType(t *testing.T) { err := os.WriteFile( filepath.Join(tmpDir, ".testcontainers.properties"), []byte(tt.propertiesFile), - 0600, + 0o600, ) require.NoError(t, err, "Failed to create properties file") } From f77377ada36426161c235b1ad9d3f5820b441a72 Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Tue, 17 Feb 2026 12:21:35 -0500 Subject: [PATCH 5/5] Unset the TESTCONTAINERS_PROVIDER variable in unit tests --- provider_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/provider_test.go b/provider_test.go index b99375c990..2ff0879c48 100644 --- a/provider_test.go +++ b/provider_test.go @@ -95,7 +95,8 @@ func TestProviderTypeGetUnderlyingProviderType(t *testing.T) { // Create temp directory for HOME tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) - t.Setenv("USERPROFILE", tmpDir) // Windows support + t.Setenv("USERPROFILE", tmpDir) // Windows support + t.Setenv("TESTCONTAINERS_PROVIDER", "") // Ensure env var is not set before test // Set any additional environment variables for k, v := range tt.env {