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 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..dd06de405c 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"