From 1ddf78c0337593e064e5fd86c427fb264c96487e Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Thu, 20 Nov 2025 11:38:32 -0500 Subject: [PATCH] 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. --- docs/system_requirements/using_podman.md | 2 + internal/config/config.go | 10 ++ internal/config/config_test.go | 39 ++++++++ provider.go | 32 +++++-- provider_test.go | 111 +++++++++++++++++++++++ 5 files changed, 187 insertions(+), 7 deletions(-) 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 diff --git a/internal/config/config.go b/internal/config/config.go index 28e1189e48..f8607951d2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -131,6 +131,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="` } // } @@ -202,6 +207,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 b3566d7b86..6002410a68 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -30,6 +30,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) { @@ -42,6 +43,7 @@ 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() @@ -49,6 +51,7 @@ func TestReadConfig(t *testing.T) { SessionID: bootstrap.SessionID(), RyukDisabled: true, Host: "", // docker socket is empty at the properties file + Provider: "podman", } require.Equal(t, expected, config) @@ -87,6 +90,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() @@ -98,6 +102,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) @@ -569,6 +574,16 @@ 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 Session ID set using an env var and properties. Env var wins", `session.id=bar`, @@ -581,6 +596,30 @@ func TestReadTCConfig(t *testing.T) { 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 210f451fc6..7c439cbceb 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 strings.ToLower(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..2ff0879c48 100644 --- a/provider_test.go +++ b/provider_test.go @@ -2,13 +2,124 @@ 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 + t.Setenv("TESTCONTAINERS_PROVIDER", "") // Ensure env var is not set before test + + // 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), + 0o600, + ) + 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"