Skip to content

Commit c5ddb24

Browse files
authored
Merge branch 'main' into fix/reaper-handshake-error
2 parents d2b2bd6 + daa2901 commit c5ddb24

39 files changed

Lines changed: 435 additions & 106 deletions

docs/features/test_session_semantics.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,60 @@ After that, the `sessionID` will be used to:
3131
- identify the test session, aggregating the test execution of multiple packages in the same test session.
3232
- pass the `sessionID` to the container runtime, as an HTTP header to the daemon.
3333
- tag the containers created by _Testcontainers for Go_, adding a label to the container with this session ID.
34+
35+
## Overriding the session ID
36+
37+
Deriving the session ID from the parent process works when the processes taking part in a test session
38+
share a parent, which is the case for `go test` and for the IDE. When that is not true, the processes
39+
end up in different sessions: some build systems and CI setups run each test target in isolation, and
40+
the parent process they are given is not guaranteed to be stable or shared.
41+
42+
For those cases the session ID can be set explicitly, either with the `TESTCONTAINERS_SESSION_ID`
43+
**environment variable** or with the `session.id` **property**. When set, the value is used as-is,
44+
instead of being derived, so every process configured with the same value takes part in the same session.
45+
46+
!!!warning
47+
Setting this value will preclude runs from creating more than one reaper. Therefore, changes to Ryuk
48+
settings past its creation will be ignored.
49+
50+
The session ID is used to build the name of the reaper container (`reaper_<sessionID>`), so a configured
51+
value must produce a name the container runtime accepts. It must:
52+
53+
- not be empty;
54+
- contain only alphanumeric characters, dots (`.`), hyphens (`-`) and underscores (`_`);
55+
- be at most 121 characters long, so that the resulting container name stays within the 128 character
56+
limit imposed by the container runtime.
57+
58+
A configured value that does not satisfy these constraints is rejected when the configuration is read,
59+
reporting the offending value and the reason. The session IDs generated by the library always satisfy
60+
them, so this only applies to values coming from the configuration.
61+
62+
### Bazel
63+
64+
Bazel is a concrete example of a build system where the session ID needs to be set explicitly, because it
65+
does not provide the conditions the derivation relies on. Its
66+
[Test Encyclopedia](https://bazel.build/reference/test-encyclopedia) states that _"the current process id,
67+
process group id, session id, and parent process id are unspecified"_, so the parent process the library
68+
would derive the session ID from is not guaranteed to be stable or shared across test targets. Sharding
69+
makes this more visible, as the test runner is launched once per shard.
70+
71+
Two more details of the test environment matter:
72+
73+
- `HOME` is set to the value of `$TEST_TMPDIR`, a private directory for the test, so the
74+
`~/.testcontainers.properties` file is not picked up.
75+
- The test environment is sanitised: tests _"should not depend on the presence, absence, or value of any
76+
environment variable not listed"_ in the encyclopedia, so `TESTCONTAINERS_SESSION_ID` does not reach the
77+
test unless it is explicitly propagated.
78+
79+
That leaves the environment variable, declared with `--test_env`, as the way to share a session across test
80+
targets. The value can be taken from the invocation environment:
81+
82+
```shell
83+
bazel test //... --test_env=TESTCONTAINERS_SESSION_ID
84+
```
85+
86+
or set independently of it:
87+
88+
```shell
89+
bazel test //... --test_env=TESTCONTAINERS_SESSION_ID=my-session
90+
```

docs/modules/gcloud.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The Google Cloud module exposes the following Go packages:
2525
- [Pubsub](#pubsub): `github.com/testcontainers/testcontainers-go/modules/gcloud/pubsub`.
2626
- [Spanner](#spanner): `github.com/testcontainers/testcontainers-go/modules/gcloud/spanner`.
2727
!!!info
28-
By default, the all the emulators use `gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators` as the default Docker image, except for the BigQuery emulator, which uses `ghcr.io/goccy/bigquery-emulator:0.6.1`, and Spanner, which uses `gcr.io/cloud-spanner-emulator/emulator:1.4.0`.
28+
By default, the all the emulators use `gcr.io/google.com/cloudsdktool/cloud-sdk:emulators` as the default Docker image, except for the BigQuery emulator, which uses `ghcr.io/goccy/bigquery-emulator:0.6.1`, and Spanner, which uses `gcr.io/cloud-spanner-emulator/emulator:1.4.0`.
2929

3030
## BigQuery
3131

@@ -97,7 +97,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
9797
#### Image
9898
9999
Use the second argument in the `Run` function to set a valid Docker image.
100-
In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`.
100+
In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:emulators")`.
101101
102102
### Container Options
103103
@@ -136,7 +136,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
136136
#### Image
137137
138138
Use the second argument in the `Run` function to set a valid Docker image.
139-
In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`.
139+
In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:emulators")`.
140140
141141
### Container Options
142142
@@ -174,7 +174,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
174174
#### Image
175175
176176
Use the second argument in the `Run` function to set a valid Docker image.
177-
In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`.
177+
In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:emulators")`.
178178
179179
### Container Options
180180
@@ -220,7 +220,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
220220
#### Image
221221
222222
Use the second argument in the `Run` function to set a valid Docker image.
223-
In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`.
223+
In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:emulators")`.
224224
225225
### Container Options
226226
@@ -258,7 +258,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
258258
#### Image
259259
260260
Use the second argument in the `Run` function to set a valid Docker image.
261-
In example: `Run(context.Background(), "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators")`.
261+
In example: `Run(context.Background(), "gcr.io/cloud-spanner-emulator/emulator:1.4.0")`.
262262
263263
### Container Options
264264

docs/usage-metrics/core.csv

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,3 +1053,39 @@ date,version,count
10531053
2026-08-01,v0.41.0,604
10541054
2026-08-01,v0.42.0,1512
10551055
2026-08-01,v0.43.0,1356
1056+
2026-09-01,v0.13.0,118
1057+
2026-09-01,v0.14.0,27
1058+
2026-09-01,v0.15.0,78
1059+
2026-09-01,v0.16.0,8
1060+
2026-09-01,v0.17.0,43
1061+
2026-09-01,v0.18.0,34
1062+
2026-09-01,v0.19.0,56
1063+
2026-09-01,v0.20.0,0
1064+
2026-09-01,v0.20.1,58
1065+
2026-09-01,v0.21.0,29
1066+
2026-09-01,v0.22.0,26
1067+
2026-09-01,v0.23.0,123
1068+
2026-09-01,v0.24.0,1
1069+
2026-09-01,v0.24.1,17
1070+
2026-09-01,v0.25.0,66
1071+
2026-09-01,v0.26.0,131
1072+
2026-09-01,v0.27.0,178
1073+
2026-09-01,v0.28.0,122
1074+
2026-09-01,v0.29.0,1
1075+
2026-09-01,v0.29.1,102
1076+
2026-09-01,v0.30.0,134
1077+
2026-09-01,v0.31.0,344
1078+
2026-09-01,v0.32.0,206
1079+
2026-09-01,v0.33.0,396
1080+
2026-09-01,v0.34.0,474
1081+
2026-09-01,v0.34.1,5
1082+
2026-09-01,v0.35.0,645
1083+
2026-09-01,v0.36.0,219
1084+
2026-09-01,v0.37.0,746
1085+
2026-09-01,v0.38.0,525
1086+
2026-09-01,v0.39.0,433
1087+
2026-09-01,v0.40.0,1512
1088+
2026-09-01,v0.41.0,580
1089+
2026-09-01,v0.42.0,1600
1090+
2026-09-01,v0.43.0,992
1091+
2026-09-01,v0.44.0,1060

docs/usage-metrics/modules.csv

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,88 @@ date,module,count
214214
2026-08-01,vearch,0
215215
2026-08-01,weaviate,18
216216
2026-08-01,yugabytedb,2
217+
2026-09-01,activemq,0
218+
2026-09-01,aerospike,2
219+
2026-09-01,arangodb,2
220+
2026-09-01,artemis,2
221+
2026-09-01,azure,27
222+
2026-09-01,azurite,2
223+
2026-09-01,cassandra,19
224+
2026-09-01,chroma,8
225+
2026-09-01,clickhouse,94
226+
2026-09-01,cockroachdb,23
227+
2026-09-01,compose,327
228+
2026-09-01,consul,14
229+
2026-09-01,couchbase,6
230+
2026-09-01,couchdb,0
231+
2026-09-01,cratedb,0
232+
2026-09-01,databend,0
233+
2026-09-01,dex,1
234+
2026-09-01,dind,1
235+
2026-09-01,dockermcpgateway,5
236+
2026-09-01,dockermodelrunner,19
237+
2026-09-01,dolt,10
238+
2026-09-01,dynamodb,29
239+
2026-09-01,elasticsearch,42
240+
2026-09-01,etcd,14
241+
2026-09-01,fakegcsserver,0
242+
2026-09-01,firebird,0
243+
2026-09-01,forgejo,2
244+
2026-09-01,gcloud,39
245+
2026-09-01,grafana-lgtm,3
246+
2026-09-01,inbucket,3
247+
2026-09-01,influxdb,2
248+
2026-09-01,k3s,77
249+
2026-09-01,k6,2
250+
2026-09-01,kafka,263
251+
2026-09-01,kurrentdb,0
252+
2026-09-01,localstack,135
253+
2026-09-01,mailpit,0
254+
2026-09-01,mariadb,36
255+
2026-09-01,meilisearch,4
256+
2026-09-01,memcached,3
257+
2026-09-01,milvus,8
258+
2026-09-01,minio,215
259+
2026-09-01,mockserver,1
260+
2026-09-01,mongodb,380
261+
2026-09-01,mosquitto,0
262+
2026-09-01,mssql,42
263+
2026-09-01,mysql,415
264+
2026-09-01,nats,114
265+
2026-09-01,nebulagraph,0
266+
2026-09-01,neo4j,17
267+
2026-09-01,nginx,0
268+
2026-09-01,ollama,12
269+
2026-09-01,openfga,12
270+
2026-09-01,openldap,4
271+
2026-09-01,opensearch,20
272+
2026-09-01,orientdb,0
273+
2026-09-01,papercutsmtp,0
274+
2026-09-01,pinecone,1
275+
2026-09-01,postgres,4680
276+
2026-09-01,presto,0
277+
2026-09-01,pulsar,1
278+
2026-09-01,qdrant,16
279+
2026-09-01,questdb,0
280+
2026-09-01,rabbitmq,164
281+
2026-09-01,ravendb,0
282+
2026-09-01,redis,754
283+
2026-09-01,redpanda,88
284+
2026-09-01,registry,17
285+
2026-09-01,s3mock,0
286+
2026-09-01,scylladb,7
287+
2026-09-01,sftp,0
288+
2026-09-01,socat,19
289+
2026-09-01,solace,0
290+
2026-09-01,solr,0
291+
2026-09-01,surrealdb,1
292+
2026-09-01,tidb,1
293+
2026-09-01,timeplus,0
294+
2026-09-01,toxiproxy,15
295+
2026-09-01,trino,0
296+
2026-09-01,typesense,0
297+
2026-09-01,valkey,43
298+
2026-09-01,vault,31
299+
2026-09-01,vearch,0
300+
2026-09-01,weaviate,17
301+
2026-09-01,yugabytedb,2

internal/config/config.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package config
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"path/filepath"
8+
"regexp"
79
"strconv"
810
"sync"
911
"time"
@@ -15,11 +17,46 @@ import (
1517

1618
const ReaperDefaultImage = "testcontainers/ryuk:0.14.0"
1719

20+
// reaperNamePrefix is the prefix of the name of the reaper container, which is built
21+
// from the session ID. It is duplicated here, instead of imported, because the package
22+
// building that name depends on this one.
23+
const reaperNamePrefix = "reaper_"
24+
25+
// maxContainerNameLen is the maximum length of a container name.
26+
const maxContainerNameLen = 128
27+
1828
var (
1929
tcConfig Config
2030
tcConfigOnce = new(sync.Once)
31+
32+
// containerNameRegex matches the names accepted by the container runtime.
33+
containerNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`)
2134
)
2235

36+
// validateSessionID verifies that a user-provided session ID can be used to build the
37+
// resources created by the library, most notably the name of the reaper container.
38+
// It validates the resulting name, not the session ID on its own, so a session ID is
39+
// only rejected when it actually produces a name the container runtime refuses.
40+
// The session IDs generated by the library always satisfy these constraints, so this
41+
// validation only applies to the values coming from the user configuration.
42+
func validateSessionID(sessionID string) error {
43+
if sessionID == "" {
44+
return errors.New("must not be empty")
45+
}
46+
47+
reaperName := reaperNamePrefix + sessionID
48+
49+
if len(reaperName) > maxContainerNameLen {
50+
return fmt.Errorf("must be at most %d characters long, got %d", maxContainerNameLen-len(reaperNamePrefix), len(sessionID))
51+
}
52+
53+
if !containerNameRegex.MatchString(reaperName) {
54+
return errors.New("must contain only alphanumeric characters, dots, hyphens and underscores")
55+
}
56+
57+
return nil
58+
}
59+
2360
// testcontainersConfig {
2461

2562
// Config represents the configuration for Testcontainers.
@@ -131,9 +168,17 @@ func read() Config {
131168
}
132169

133170
sessionID := os.Getenv("TESTCONTAINERS_SESSION_ID")
134-
if sessionID != "" {
171+
switch {
172+
case sessionID != "":
173+
if err := validateSessionID(sessionID); err != nil {
174+
panic(fmt.Sprintf("invalid TESTCONTAINERS_SESSION_ID value %q: %s", sessionID, err))
175+
}
135176
config.SessionID = sessionID
136-
} else if config.SessionID == "" {
177+
case config.SessionID != "":
178+
if err := validateSessionID(config.SessionID); err != nil {
179+
panic(fmt.Sprintf("invalid session.id property value %q: %s", config.SessionID, err))
180+
}
181+
default:
137182
config.SessionID = bootstrap.SessionID()
138183
}
139184

0 commit comments

Comments
 (0)