Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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="`
}

// }
Expand Down Expand Up @@ -141,6 +146,11 @@ func read() Config {
config.RyukConnectionTimeout = timeout
}

providerEnv := os.Getenv("TESTCONTAINERS_PROVIDER")
if providerEnv != "" {
config.Provider = providerEnv
}

return config
}

Expand Down
39 changes: 39 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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()

Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
32 changes: 25 additions & 7 deletions provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"os"
"strings"

"github.com/testcontainers/testcontainers-go/internal/config"
Expand Down Expand Up @@ -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
}
Comment on lines +104 to +109
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify case handling for provider configuration values.

The string comparison for conf.Provider is case-sensitive. Ensure that the configuration parsing in internal/config/config.go normalizes the provider value to lowercase before storing it, otherwise users who set TESTCONTAINERS_PROVIDER=PODMAN (uppercase) might experience unexpected behavior.


🏁 Script executed:

#!/bin/bash
# Description: Check how config.Read() processes the Provider field and whether it normalizes case.

# Search for Provider field assignment and any case normalization logic
ast-grep --pattern $'Provider$$$=$$$'

# Also check for any strings.ToLower or similar normalization
rg -n -C3 'Provider.*=' internal/config/config.go

Length of output: 371


Now I need to identify which library provides the properties tag parsing and whether it handles case normalization.

Based on my verification, the original review comment correctly identifies a real case-sensitivity issue. The evidence shows:

  1. From the script output, the environment variable TESTCONTAINERS_PROVIDER is read directly via os.Getenv() and assigned to config.Provider without any strings.ToLower() call.

  2. The switch statement in provider.go (lines 104-109) performs case-sensitive string matching for "docker" and "podman".

  3. The magiconair/properties library used for the properties tag does not automatically normalize string values to lowercase—it preserves case as-is.

This means if a user sets TESTCONTAINERS_PROVIDER=PODMAN (uppercase), it won't match the case-sensitive "podman" check and will fall through to the default, causing unexpected behavior.

Add case normalization to environment variable assignment in internal/config/config.go.

At line 151, normalize the environment variable to lowercase before assigning it:

providerEnv := os.Getenv("TESTCONTAINERS_PROVIDER")
if providerEnv != "" {
    config.Provider = strings.ToLower(providerEnv)
}

This ensures the switch statement in provider.go (lines 104-109) will correctly match provider values regardless of the case used when setting the environment variable.


🏁 Script executed:

#!/bin/bash
# Find the library that handles properties tag parsing
cd /tmp && git clone https://github.com/testcontainers/testcontainers-go.git 2>/dev/null || true
cd testcontainers-go

# Search for imports in config.go that might handle properties tag
rg -n "import" internal/config/config.go

# Search for config.Read implementation
rg -n "func.*Read" internal/config/config.go -A 20

# Check go.mod for dependencies that might handle properties tags
rg -n "properties" go.mod

Length of output: 723


🏁 Script executed:

#!/bin/bash
# Search for github packages that provide properties tag parsing
curl -s "https://api.github.com/search/repositories?q=go+properties+tag+parser" | head -50

Length of output: 163056


Based on the web search results, the magiconair/properties library's string field handling simply returns the string value as-is: case isString(t): v, err = s, nil. This means the library does NOT normalize string values to lowercase.

Verification confirms the original review comment is correct. The case-sensitivity bug exists:

  1. Only GetBool does case-insensitive comparison, not string values
  2. From the earlier script results, the environment variable is assigned directly without normalization: config.Provider = providerEnv
  3. The switch statement in provider.go performs case-sensitive matching for "docker" and "podman"

Add case normalization to environment variable assignment in internal/config/config.go.

At line 151, normalize the environment variable to lowercase before assigning it:

providerEnv := os.Getenv("TESTCONTAINERS_PROVIDER")
if providerEnv != "" {
    config.Provider = strings.ToLower(providerEnv)
}

This ensures the switch statement in provider.go (lines 104-109) will correctly match provider values regardless of the case used when setting the environment variable. Without this fix, users setting TESTCONTAINERS_PROVIDER=PODMAN (uppercase, a common practice) will silently have their configuration ignored, with the provider defaulting to Docker instead.

🤖 Prompt for AI Agents
In internal/config/config.go around line 151, the environment variable
TESTCONTAINERS_PROVIDER is assigned verbatim which yields case-sensitive values
and causes provider matching in provider.go (lines 104-109) to fail; update the
assignment to normalize the env var to lowercase before setting config.Provider
(i.e., set config.Provider = strings.ToLower(providerEnv) when providerEnv is
non-empty) and ensure the strings package is imported in the file.


// 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{
Expand All @@ -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...)
Expand Down
110 changes: 110 additions & 0 deletions provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading