From d99afd3ec0ad7ae3ebd9cbb8e2c3da53623d7980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreira=20Fernandes?= Date: Wed, 5 Aug 2026 11:13:15 +0100 Subject: [PATCH 1/6] feat: allow overriding the session ID --- internal/config/config.go | 12 ++++++++++++ internal/config/config_test.go | 5 +++++ internal/core/bootstrap.go | 30 +++++++++++++++++++++--------- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 262f40d675..1e8ff4ba95 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -52,6 +52,13 @@ type Config struct { // Environment variable: TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX HubImageNamePrefix string `properties:"hub.image.name.prefix,default="` + // SessionID is the ID of the testing session. + // Setting this value will preclude runs from creating more than one reaper. Therefore, + // changes to ryuk settings past its creation will be ignored. + // + // Environment variable: TESTCONTAINERS_SESSION_ID + SessionID string `properties:"session.id,default="` + // RyukDisabled is a flag to enable or disable the Garbage Collector. // Setting this to true will prevent testcontainers from automatically cleaning up // resources, which is particularly important in tests which timeout as they @@ -121,6 +128,11 @@ func read() Config { config.HubImageNamePrefix = hubImageNamePrefix } + sessionID := os.Getenv("TESTCONTAINERS_SESSION_ID") + if sessionID != "" { + config.SessionID = sessionID + } + ryukPrivilegedEnv := os.Getenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") if parseBool(ryukPrivilegedEnv) { config.RyukPrivileged = ryukPrivilegedEnv == "true" diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 591fcff11c..4a3dbee501 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -21,6 +21,7 @@ const ( func resetTestEnv(t *testing.T) { t.Helper() t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "") + t.Setenv("TESTCONTAINERS_SESSION_ID", "") t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "") t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "") t.Setenv("RYUK_VERBOSE", "") @@ -76,6 +77,7 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("USERPROFILE", "") // Windows support t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", defaultHubPrefix) + t.Setenv("TESTCONTAINERS_SESSION_ID", "foo") t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") t.Setenv("RYUK_CONNECTION_TIMEOUT", "12s") @@ -84,6 +86,7 @@ func TestReadTCConfig(t *testing.T) { expected := Config{ HubImageNamePrefix: defaultHubPrefix, + SessionID: "foo", RyukDisabled: true, RyukPrivileged: true, Host: "", // docker socket is empty at the properties file @@ -124,6 +127,7 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("USERPROFILE", tmpDir) // Windows support t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", defaultHubPrefix) + t.Setenv("TESTCONTAINERS_SESSION_ID", "foo") t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") t.Setenv("RYUK_VERBOSE", "true") t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") @@ -132,6 +136,7 @@ func TestReadTCConfig(t *testing.T) { config := read() expected := Config{ HubImageNamePrefix: defaultHubPrefix, + SessionID: "foo", RyukDisabled: true, RyukPrivileged: true, RyukVerbose: true, diff --git a/internal/core/bootstrap.go b/internal/core/bootstrap.go index d249d9be31..55ef4ba905 100644 --- a/internal/core/bootstrap.go +++ b/internal/core/bootstrap.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/shirou/gopsutil/v4/process" + "github.com/testcontainers/testcontainers-go/internal/config" ) // sessionID returns a unique session ID for the current test session. Because each Go package @@ -45,6 +46,11 @@ var processID string const sessionIDPlaceholder = "testcontainers-go:%d:%d" func init() { + cfg := config.Read() + if cfg.SessionID != "" { + sessionID = cfg.SessionID + } + processID = uuid.New().String() parentPid := os.Getppid() @@ -57,7 +63,9 @@ func init() { processes, err := process.Processes() if err != nil { - sessionID = uuid.New().String() + if sessionID == "" { + sessionID = uuid.New().String() + } projectPath = fallbackCwd return } @@ -75,7 +83,9 @@ func init() { t, err := p.CreateTime() if err != nil { - sessionID = uuid.New().String() + if sessionID == "" { + sessionID = uuid.New().String() + } return } @@ -83,14 +93,16 @@ func init() { break } - hasher := sha256.New() - _, err = fmt.Fprintf(hasher, sessionIDPlaceholder, parentPid, createTime) - if err != nil { - sessionID = uuid.New().String() - return - } + if sessionID == "" { + hasher := sha256.New() + _, err = fmt.Fprintf(hasher, sessionIDPlaceholder, parentPid, createTime) + if err != nil { + sessionID = uuid.New().String() + return + } - sessionID = hex.EncodeToString(hasher.Sum(nil)) + sessionID = hex.EncodeToString(hasher.Sum(nil)) + } } func ProcessID() string { From c329c04e7b3e603c4a4826c73f94df287a880641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreira=20Fernandes?= Date: Wed, 5 Aug 2026 11:13:15 +0100 Subject: [PATCH 2/6] chore: lint --- internal/core/bootstrap.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/core/bootstrap.go b/internal/core/bootstrap.go index 55ef4ba905..3ac4c287bf 100644 --- a/internal/core/bootstrap.go +++ b/internal/core/bootstrap.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/shirou/gopsutil/v4/process" + "github.com/testcontainers/testcontainers-go/internal/config" ) From 4f67e4da148a3c4e47da1e7382b8884fecb83fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreira=20Fernandes?= Date: Wed, 5 Aug 2026 11:13:15 +0100 Subject: [PATCH 3/6] refactor: have the config load from core/bootstrap --- container.go | 3 +- docker.go | 2 +- docker_client.go | 11 +++- generic.go | 3 +- internal/config/config.go | 3 + internal/config/config_test.go | 64 +++++++++++++++++++++- internal/core/{ => bootstrap}/bootstrap.go | 33 ++++------- internal/core/client.go | 5 +- network.go | 3 +- reaper_test.go | 2 +- testcontainers.go | 3 +- 11 files changed, 96 insertions(+), 36 deletions(-) rename internal/core/{ => bootstrap}/bootstrap.go (85%) diff --git a/container.go b/container.go index b7f0b653e7..b2a1706bfe 100644 --- a/container.go +++ b/container.go @@ -22,6 +22,7 @@ import ( "github.com/moby/patternmatcher/ignorefile" tcexec "github.com/testcontainers/testcontainers-go/exec" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/internal/core" "github.com/testcontainers/testcontainers-go/log" "github.com/testcontainers/testcontainers-go/wait" @@ -176,7 +177,7 @@ func (c *ContainerRequest) sessionID() string { return sessionID } - return core.SessionID() + return config.Read().SessionID } // containerOptions functional options for a container diff --git a/docker.go b/docker.go index 49cac317d0..1ce4da7f2f 100644 --- a/docker.go +++ b/docker.go @@ -1010,7 +1010,7 @@ func (c *DockerContainer) connectReaper(ctx context.Context) error { return nil } - reaper, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, c.provider.host), core.SessionID(), c.provider) + reaper, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, c.provider.host), c.provider.config.SessionID, c.provider) if err != nil { return fmt.Errorf("reaper: %w", err) } diff --git a/docker_client.go b/docker_client.go index ae4ff511dd..adebb1151c 100644 --- a/docker_client.go +++ b/docker_client.go @@ -9,7 +9,9 @@ import ( "github.com/moby/moby/client" "github.com/testcontainers/testcontainers-go/internal" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/internal/core" + "github.com/testcontainers/testcontainers-go/internal/core/bootstrap" "github.com/testcontainers/testcontainers-go/log" ) @@ -17,6 +19,8 @@ import ( // It implements the SystemAPIClient interface in order to cache the docker info and reuse it. type DockerClient struct { *client.Client // client is embedded into our own client + + config config.Config } var ( @@ -85,8 +89,8 @@ func (c *DockerClient) Info(ctx context.Context, options client.InfoOptions) (cl internal.Version, host, core.MustExtractDockerSocket(ctx), - core.SessionID(), - core.ProcessID(), + c.config.SessionID, + bootstrap.ProcessID(), ) return dockerInfo, nil @@ -123,8 +127,11 @@ func NewDockerClientWithOpts(ctx context.Context, opt ...client.Opt) (*DockerCli return nil, err } + tcConfig := config.Read() + tcClient := DockerClient{ Client: dockerClient, + config: tcConfig, } if _, err = tcClient.Info(ctx, client.InfoOptions{}); err != nil { diff --git a/generic.go b/generic.go index 57dbe76039..e493317784 100644 --- a/generic.go +++ b/generic.go @@ -7,6 +7,7 @@ import ( "maps" "sync" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/internal/core" "github.com/testcontainers/testcontainers-go/log" ) @@ -100,7 +101,7 @@ type GenericProvider interface { // reaper is enabled, otherwise this is excluded to prevent resources being // incorrectly reaped. func GenericLabels() map[string]string { - return core.DefaultLabels(core.SessionID()) + return core.DefaultLabels(config.Read().SessionID) } // AddGenericLabels adds the generic labels to target. diff --git a/internal/config/config.go b/internal/config/config.go index 1e8ff4ba95..3e5d79b751 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,6 +9,7 @@ import ( "time" "github.com/magiconair/properties" + "github.com/testcontainers/testcontainers-go/internal/core/bootstrap" ) const ReaperDefaultImage = "testcontainers/ryuk:0.14.0" @@ -131,6 +132,8 @@ func read() Config { sessionID := os.Getenv("TESTCONTAINERS_SESSION_ID") if sessionID != "" { config.SessionID = sessionID + } else if config.SessionID == "" { + config.SessionID = bootstrap.SessionID() } ryukPrivilegedEnv := os.Getenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 4a3dbee501..527c5fb765 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/internal/core/bootstrap" ) const ( @@ -43,6 +44,7 @@ func TestReadConfig(t *testing.T) { config := Read() expected := Config{ + SessionID: bootstrap.SessionID(), RyukDisabled: true, Host: "", // docker socket is empty at the properties file } @@ -67,7 +69,9 @@ func TestReadTCConfig(t *testing.T) { config := read() - expected := Config{} + expected := Config{ + SessionID: bootstrap.SessionID(), + } assert.Equal(t, expected, config) }) @@ -104,7 +108,9 @@ func TestReadTCConfig(t *testing.T) { config := read() - expected := Config{} + expected := Config{ + SessionID: bootstrap.SessionID(), + } assert.Equal(t, expected, config) }) @@ -116,7 +122,10 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("DOCKER_HOST", tcpDockerHost33293) config := read() - expected := Config{} // the config does not read DOCKER_HOST, that's why it's empty + expected := Config{ + Host: "", // the config does not read env var `DOCKER_HOST`, that's why `Host` it's empty + SessionID: bootstrap.SessionID(), + } assert.Equal(t, expected, config) }) @@ -151,6 +160,7 @@ func TestReadTCConfig(t *testing.T) { defaultRyukConnectionTimeout := 60 * time.Second defaultRyukReconnectionTimeout := 10 * time.Second defaultConfig := Config{ + SessionID: bootstrap.SessionID(), RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, } @@ -166,6 +176,7 @@ func TestReadTCConfig(t *testing.T) { "docker.host = " + tcpDockerHost33293, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), Host: tcpDockerHost33293, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -178,6 +189,7 @@ func TestReadTCConfig(t *testing.T) { `, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), Host: tcpDockerHost4711, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -192,6 +204,7 @@ func TestReadTCConfig(t *testing.T) { `, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), Host: tcpDockerHost1234, TLSVerify: 1, RyukConnectionTimeout: defaultRyukConnectionTimeout, @@ -203,6 +216,7 @@ func TestReadTCConfig(t *testing.T) { "", map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, }, @@ -214,6 +228,7 @@ func TestReadTCConfig(t *testing.T) { `, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), Host: tcpDockerHost1234, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -224,6 +239,7 @@ func TestReadTCConfig(t *testing.T) { "docker.host=" + tcpDockerHost33293, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), Host: tcpDockerHost33293, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -243,6 +259,7 @@ func TestReadTCConfig(t *testing.T) { docker.cert.path=/tmp/certs`, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), Host: tcpDockerHost1234, CertPath: "/tmp/certs", RyukConnectionTimeout: defaultRyukConnectionTimeout, @@ -254,6 +271,7 @@ func TestReadTCConfig(t *testing.T) { `ryuk.disabled=true`, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), RyukDisabled: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -264,6 +282,7 @@ func TestReadTCConfig(t *testing.T) { `ryuk.container.privileged=true`, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), RyukPrivileged: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -275,6 +294,7 @@ func TestReadTCConfig(t *testing.T) { ryuk.reconnection.timeout=13s`, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), RyukReconnectionTimeout: 13 * time.Second, RyukConnectionTimeout: 12 * time.Second, }, @@ -287,6 +307,7 @@ func TestReadTCConfig(t *testing.T) { "RYUK_CONNECTION_TIMEOUT": "12s", }, Config{ + SessionID: bootstrap.SessionID(), RyukReconnectionTimeout: 13 * time.Second, RyukConnectionTimeout: 12 * time.Second, }, @@ -300,6 +321,7 @@ func TestReadTCConfig(t *testing.T) { "RYUK_CONNECTION_TIMEOUT": "12s", }, Config{ + SessionID: bootstrap.SessionID(), RyukReconnectionTimeout: 13 * time.Second, RyukConnectionTimeout: 12 * time.Second, }, @@ -309,6 +331,7 @@ func TestReadTCConfig(t *testing.T) { `ryuk.verbose=true`, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), RyukVerbose: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -321,6 +344,7 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_DISABLED": "true", }, Config{ + SessionID: bootstrap.SessionID(), RyukDisabled: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -333,6 +357,7 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", }, Config{ + SessionID: bootstrap.SessionID(), RyukPrivileged: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -345,6 +370,7 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_DISABLED": "true", }, Config{ + SessionID: bootstrap.SessionID(), RyukDisabled: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -357,6 +383,7 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_DISABLED": "true", }, Config{ + SessionID: bootstrap.SessionID(), RyukDisabled: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -385,6 +412,7 @@ func TestReadTCConfig(t *testing.T) { "RYUK_VERBOSE": "true", }, Config{ + SessionID: bootstrap.SessionID(), RyukVerbose: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -397,6 +425,7 @@ func TestReadTCConfig(t *testing.T) { "RYUK_VERBOSE": "true", }, Config{ + SessionID: bootstrap.SessionID(), RyukVerbose: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -425,6 +454,7 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", }, Config{ + SessionID: bootstrap.SessionID(), RyukPrivileged: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -437,6 +467,7 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", }, Config{ + SessionID: bootstrap.SessionID(), RyukPrivileged: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -467,6 +498,7 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", }, Config{ + SessionID: bootstrap.SessionID(), RyukDisabled: true, RyukPrivileged: true, }, @@ -492,6 +524,7 @@ func TestReadTCConfig(t *testing.T) { `hub.image.name.prefix=` + defaultHubPrefix + `/props/`, map[string]string{}, Config{ + SessionID: bootstrap.SessionID(), HubImageNamePrefix: defaultHubPrefix + "/props/", RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -504,6 +537,7 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": defaultHubPrefix + "/env/", }, Config{ + SessionID: bootstrap.SessionID(), HubImageNamePrefix: defaultHubPrefix + "/env/", RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, @@ -516,11 +550,35 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": defaultHubPrefix + "/env/", }, Config{ + SessionID: bootstrap.SessionID(), HubImageNamePrefix: defaultHubPrefix + "/env/", RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, }, }, + // + { + "With Session ID set as a property", + `session.id=foo`, + map[string]string{}, + Config{ + SessionID: "foo", + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + }, + }, + { + "With Ryuk disabled using an env var and properties. Env var wins", + `session.id=bar`, + map[string]string{ + "TESTCONTAINERS_SESSION_ID": "foo", + }, + Config{ + SessionID: "foo", + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/core/bootstrap.go b/internal/core/bootstrap/bootstrap.go similarity index 85% rename from internal/core/bootstrap.go rename to internal/core/bootstrap/bootstrap.go index 3ac4c287bf..18758c7e13 100644 --- a/internal/core/bootstrap.go +++ b/internal/core/bootstrap/bootstrap.go @@ -1,4 +1,4 @@ -package core +package bootstrap import ( "crypto/sha256" @@ -8,8 +8,6 @@ import ( "github.com/google/uuid" "github.com/shirou/gopsutil/v4/process" - - "github.com/testcontainers/testcontainers-go/internal/config" ) // sessionID returns a unique session ID for the current test session. Because each Go package @@ -47,11 +45,6 @@ var processID string const sessionIDPlaceholder = "testcontainers-go:%d:%d" func init() { - cfg := config.Read() - if cfg.SessionID != "" { - sessionID = cfg.SessionID - } - processID = uuid.New().String() parentPid := os.Getppid() @@ -64,9 +57,7 @@ func init() { processes, err := process.Processes() if err != nil { - if sessionID == "" { - sessionID = uuid.New().String() - } + sessionID = uuid.New().String() projectPath = fallbackCwd return } @@ -84,9 +75,7 @@ func init() { t, err := p.CreateTime() if err != nil { - if sessionID == "" { - sessionID = uuid.New().String() - } + sessionID = uuid.New().String() return } @@ -94,16 +83,14 @@ func init() { break } - if sessionID == "" { - hasher := sha256.New() - _, err = fmt.Fprintf(hasher, sessionIDPlaceholder, parentPid, createTime) - if err != nil { - sessionID = uuid.New().String() - return - } - - sessionID = hex.EncodeToString(hasher.Sum(nil)) + hasher := sha256.New() + _, err = fmt.Fprintf(hasher, sessionIDPlaceholder, parentPid, createTime) + if err != nil { + sessionID = uuid.New().String() + return } + + sessionID = hex.EncodeToString(hasher.Sum(nil)) } func ProcessID() string { diff --git a/internal/core/client.go b/internal/core/client.go index 795e6640c6..dd3263d26b 100644 --- a/internal/core/client.go +++ b/internal/core/client.go @@ -8,6 +8,7 @@ import ( "github.com/testcontainers/testcontainers-go/internal" "github.com/testcontainers/testcontainers-go/internal/config" + "github.com/testcontainers/testcontainers-go/internal/core/bootstrap" ) // NewClient returns a new docker client extracting the docker host from the different alternatives @@ -35,8 +36,8 @@ func NewClient(ctx context.Context, ops ...client.Opt) (*client.Client, error) { opts = append(opts, client.WithHTTPHeaders( map[string]string{ - "x-tc-pp": ProjectPath(), - "x-tc-sid": SessionID(), + "x-tc-pp": bootstrap.ProjectPath(), + "x-tc-sid": tcConfig.SessionID, "User-Agent": "tc-go/" + internal.Version, }), ) diff --git a/network.go b/network.go index c5fa4eb526..a305c7b2f4 100644 --- a/network.go +++ b/network.go @@ -5,6 +5,7 @@ import ( "github.com/moby/moby/api/types/network" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/internal/core" ) @@ -56,5 +57,5 @@ func (r NetworkRequest) sessionID() string { return sessionID } - return core.SessionID() + return config.Read().SessionID } diff --git a/reaper_test.go b/reaper_test.go index bd435f0d71..52408e58a3 100644 --- a/reaper_test.go +++ b/reaper_test.go @@ -119,7 +119,7 @@ func testReaperRunning(t *testing.T) { t.Helper() ctx := context.Background() - sessionID := core.SessionID() + sessionID := config.Read().SessionID reaperContainer, err := spawner.lookupContainer(ctx, sessionID) require.NoError(t, err) require.NotNil(t, reaperContainer) diff --git a/testcontainers.go b/testcontainers.go index 77ba722c74..d3e1890735 100644 --- a/testcontainers.go +++ b/testcontainers.go @@ -3,6 +3,7 @@ package testcontainers import ( "context" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/internal/core" ) @@ -50,5 +51,5 @@ func MustExtractDockerSocket(ctx context.Context) string { // - identify the test session, aggregating the test execution of multiple packages in the same test session. // - tag the containers created by testcontainers-go, adding a label to the container with the session ID. func SessionID() string { - return core.SessionID() + return config.Read().SessionID } From 17da36ed3b7c98a2b4ed7b2f38e542a1f3018ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreira=20Fernandes?= Date: Wed, 5 Aug 2026 11:13:15 +0100 Subject: [PATCH 4/6] chore: lint --- internal/config/config.go | 1 + internal/config/config_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 3e5d79b751..d2b762eb9c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,6 +9,7 @@ import ( "time" "github.com/magiconair/properties" + "github.com/testcontainers/testcontainers-go/internal/core/bootstrap" ) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 527c5fb765..60bc98db9f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/internal/core/bootstrap" ) From c717c300d3b4a0c9c5a3f8b9df5b33d99cff0e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreira=20Fernandes?= Date: Wed, 5 Aug 2026 11:13:15 +0100 Subject: [PATCH 5/6] refactor: tidy --- docker_client.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker_client.go b/docker_client.go index adebb1151c..53c7da0391 100644 --- a/docker_client.go +++ b/docker_client.go @@ -127,11 +127,9 @@ func NewDockerClientWithOpts(ctx context.Context, opt ...client.Opt) (*DockerCli return nil, err } - tcConfig := config.Read() - tcClient := DockerClient{ Client: dockerClient, - config: tcConfig, + config: config.Read(), } if _, err = tcClient.Info(ctx, client.InfoOptions{}); err != nil { From d3faef8a342dfd5e3ada476b1ef5f780b385fa40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreira=20Fernandes?= Date: Wed, 5 Aug 2026 11:13:15 +0100 Subject: [PATCH 6/6] test: fix description --- internal/config/config_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 60bc98db9f..b86148d214 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -569,7 +569,7 @@ func TestReadTCConfig(t *testing.T) { }, }, { - "With Ryuk disabled using an env var and properties. Env var wins", + "With Session ID set using an env var and properties. Env var wins", `session.id=bar`, map[string]string{ "TESTCONTAINERS_SESSION_ID": "foo",