Skip to content

Commit dcef9bb

Browse files
committed
Merge branch 'main' into windows-fixes
* main: chore: use a much smaller image for testing (testcontainers#2795) fix: parallel containers clean race (testcontainers#2790) fix(registry): wait for (testcontainers#2793) fix: container timeout test (testcontainers#2792) docs: document redpanda options (testcontainers#2789) feat: support databend module (testcontainers#2779) chore: golangci-lint 1.61.0 (testcontainers#2787) fix(mssql): bump Docker image version (testcontainers#2786) fix: handle 127 error code for podman compatibility (testcontainers#2778) fix: do not override ImageBuildOptions.Labels when building from a Dockerfile (testcontainers#2775) feat(mongodb): Wait for mongodb module with a replicaset to finish (testcontainers#2777) fix(postgres): Apply default snapshot name if no name specified (testcontainers#2783) fix: resource clean up for tests and examples (testcontainers#2738)
2 parents 7676025 + 738e8fc commit dcef9bb

File tree

260 files changed

+4968
-4346
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

260 files changed

+4968
-4346
lines changed

.github/workflows/ci-test-go.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ jobs:
7373

7474
- name: golangci-lint
7575
if: ${{ inputs.platform == 'ubuntu-latest' }}
76-
uses: golangci/golangci-lint-action@9d1e0624a798bb64f6c3cea93db47765312263dc # v5
76+
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
7777
with:
7878
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
79-
version: v1.59.1
79+
version: v1.61.0
8080
# Optional: working directory, useful for monorepos
8181
working-directory: ${{ inputs.project-directory }}
8282
# Optional: golangci-lint command line arguments.

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
matrix:
9595
go-version: [1.22.x, 1.x]
9696
platform: [ubuntu-latest]
97-
module: [artemis, azurite, cassandra, chroma, clickhouse, cockroachdb, compose, consul, couchbase, dolt, elasticsearch, gcloud, grafana-lgtm, inbucket, influxdb, k3s, k6, kafka, localstack, mariadb, milvus, minio, mockserver, mongodb, mssql, mysql, nats, neo4j, ollama, openfga, openldap, opensearch, postgres, pulsar, qdrant, rabbitmq, redis, redpanda, registry, surrealdb, valkey, vault, vearch, weaviate]
97+
module: [artemis, azurite, cassandra, chroma, clickhouse, cockroachdb, compose, consul, couchbase, databend, dolt, elasticsearch, gcloud, grafana-lgtm, inbucket, influxdb, k3s, k6, kafka, localstack, mariadb, milvus, minio, mockserver, mongodb, mssql, mysql, nats, neo4j, ollama, openfga, openldap, opensearch, postgres, pulsar, qdrant, rabbitmq, redis, redpanda, registry, surrealdb, valkey, vault, vearch, weaviate]
9898
uses: ./.github/workflows/ci-test-go.yml
9999
with:
100100
go-version: ${{ matrix.go-version }}

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ TEST-*.xml
1414

1515
tcvenv
1616

17-
**/go.work
17+
**/go.work
18+
19+
# VS Code settings
20+
.vscode

.golangci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ linters:
88
- nonamedreturns
99
- testifylint
1010
- errcheck
11+
- nolintlint
1112

1213
linters-settings:
1314
errorlint:

.vscode/.testcontainers-go.code-workspace

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
"name": "module / couchbase",
5050
"path": "../modules/couchbase"
5151
},
52+
{
53+
"name": "module / databend",
54+
"path": "../modules/databend"
55+
},
5256
{
5357
"name": "module / dolt",
5458
"path": "../modules/dolt"

cleanup.go

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package testcontainers
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"reflect"
8+
"time"
9+
)
10+
11+
// terminateOptions is a type that holds the options for terminating a container.
12+
type terminateOptions struct {
13+
ctx context.Context
14+
timeout *time.Duration
15+
volumes []string
16+
}
17+
18+
// TerminateOption is a type that represents an option for terminating a container.
19+
type TerminateOption func(*terminateOptions)
20+
21+
// StopContext returns a TerminateOption that sets the context.
22+
// Default: context.Background().
23+
func StopContext(ctx context.Context) TerminateOption {
24+
return func(c *terminateOptions) {
25+
c.ctx = ctx
26+
}
27+
}
28+
29+
// StopTimeout returns a TerminateOption that sets the timeout.
30+
// Default: See [Container.Stop].
31+
func StopTimeout(timeout time.Duration) TerminateOption {
32+
return func(c *terminateOptions) {
33+
c.timeout = &timeout
34+
}
35+
}
36+
37+
// RemoveVolumes returns a TerminateOption that sets additional volumes to remove.
38+
// This is useful when the container creates named volumes that should be removed
39+
// which are not removed by default.
40+
// Default: nil.
41+
func RemoveVolumes(volumes ...string) TerminateOption {
42+
return func(c *terminateOptions) {
43+
c.volumes = volumes
44+
}
45+
}
46+
47+
// TerminateContainer calls [Container.Terminate] on the container if it is not nil.
48+
//
49+
// This should be called as a defer directly after [GenericContainer](...)
50+
// or a modules Run(...) to ensure the container is terminated when the
51+
// function ends.
52+
func TerminateContainer(container Container, options ...TerminateOption) error {
53+
if isNil(container) {
54+
return nil
55+
}
56+
57+
c := &terminateOptions{
58+
ctx: context.Background(),
59+
}
60+
61+
for _, opt := range options {
62+
opt(c)
63+
}
64+
65+
// TODO: Add a timeout when terminate supports it.
66+
err := container.Terminate(c.ctx)
67+
if !isCleanupSafe(err) {
68+
return fmt.Errorf("terminate: %w", err)
69+
}
70+
71+
// Remove additional volumes if any.
72+
if len(c.volumes) == 0 {
73+
return nil
74+
}
75+
76+
client, err := NewDockerClientWithOpts(c.ctx)
77+
if err != nil {
78+
return fmt.Errorf("docker client: %w", err)
79+
}
80+
81+
defer client.Close()
82+
83+
// Best effort to remove all volumes.
84+
var errs []error
85+
for _, volume := range c.volumes {
86+
if errRemove := client.VolumeRemove(c.ctx, volume, true); errRemove != nil {
87+
errs = append(errs, fmt.Errorf("volume remove %q: %w", volume, errRemove))
88+
}
89+
}
90+
91+
return errors.Join(errs...)
92+
}
93+
94+
// isNil returns true if val is nil or an nil instance false otherwise.
95+
func isNil(val any) bool {
96+
if val == nil {
97+
return true
98+
}
99+
100+
valueOf := reflect.ValueOf(val)
101+
switch valueOf.Kind() {
102+
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
103+
return valueOf.IsNil()
104+
default:
105+
return false
106+
}
107+
}

commons-test.mk

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ define go_install
66
endef
77

88
$(GOBIN)/golangci-lint:
9-
$(call go_install,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.1)
9+
$(call go_install,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0)
1010

1111
$(GOBIN)/gotestsum:
1212
$(call go_install,gotest.tools/gotestsum@latest)

container.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,14 @@ func (c *ContainerRequest) BuildOptions() (types.ImageBuildOptions, error) {
460460
}
461461

462462
if !c.ShouldKeepBuiltImage() {
463-
buildOptions.Labels = core.DefaultLabels(core.SessionID())
463+
dst := GenericLabels()
464+
if err = core.MergeCustomLabels(dst, c.Labels); err != nil {
465+
return types.ImageBuildOptions{}, err
466+
}
467+
if err = core.MergeCustomLabels(dst, buildOptions.Labels); err != nil {
468+
return types.ImageBuildOptions{}, err
469+
}
470+
buildOptions.Labels = dst
464471
}
465472

466473
// Do this as late as possible to ensure we don't leak the context on error/panic.
@@ -513,7 +520,7 @@ func (c *ContainerRequest) validateMounts() error {
513520

514521
c.HostConfigModifier(&hostConfig)
515522

516-
if hostConfig.Binds != nil && len(hostConfig.Binds) > 0 {
523+
if len(hostConfig.Binds) > 0 {
517524
for _, bind := range hostConfig.Binds {
518525
parts := strings.Split(bind, ":")
519526
if len(parts) != 2 {

container_test.go

+81-31
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"testing"
1212
"time"
1313

14+
"github.com/docker/docker/api/types"
1415
"github.com/docker/docker/api/types/container"
1516
"github.com/stretchr/testify/assert"
1617
"github.com/stretchr/testify/require"
@@ -290,8 +291,7 @@ func Test_BuildImageWithContexts(t *testing.T) {
290291
ContainerRequest: req,
291292
Started: true,
292293
})
293-
294-
defer terminateContainerOnEnd(t, ctx, c)
294+
testcontainers.CleanupContainer(t, c)
295295

296296
if testCase.ExpectedError != "" {
297297
require.EqualError(t, err, testCase.ExpectedError)
@@ -303,6 +303,64 @@ func Test_BuildImageWithContexts(t *testing.T) {
303303
}
304304
}
305305

306+
func TestCustomLabelsImage(t *testing.T) {
307+
const (
308+
myLabelName = "org.my.label"
309+
myLabelValue = "my-label-value"
310+
)
311+
312+
ctx := context.Background()
313+
req := testcontainers.GenericContainerRequest{
314+
ContainerRequest: testcontainers.ContainerRequest{
315+
Image: "alpine:latest",
316+
Labels: map[string]string{myLabelName: myLabelValue},
317+
},
318+
}
319+
320+
ctr, err := testcontainers.GenericContainer(ctx, req)
321+
322+
require.NoError(t, err)
323+
t.Cleanup(func() { assert.NoError(t, ctr.Terminate(ctx)) })
324+
325+
ctrJSON, err := ctr.Inspect(ctx)
326+
require.NoError(t, err)
327+
assert.Equal(t, myLabelValue, ctrJSON.Config.Labels[myLabelName])
328+
}
329+
330+
func TestCustomLabelsBuildOptionsModifier(t *testing.T) {
331+
const (
332+
myLabelName = "org.my.label"
333+
myLabelValue = "my-label-value"
334+
myBuildOptionLabel = "org.my.bo.label"
335+
myBuildOptionValue = "my-bo-label-value"
336+
)
337+
338+
ctx := context.Background()
339+
req := testcontainers.GenericContainerRequest{
340+
ContainerRequest: testcontainers.ContainerRequest{
341+
FromDockerfile: testcontainers.FromDockerfile{
342+
Context: "./testdata",
343+
Dockerfile: "Dockerfile",
344+
BuildOptionsModifier: func(opts *types.ImageBuildOptions) {
345+
opts.Labels = map[string]string{
346+
myBuildOptionLabel: myBuildOptionValue,
347+
}
348+
},
349+
},
350+
Labels: map[string]string{myLabelName: myLabelValue},
351+
},
352+
}
353+
354+
ctr, err := testcontainers.GenericContainer(ctx, req)
355+
testcontainers.CleanupContainer(t, ctr)
356+
require.NoError(t, err)
357+
358+
ctrJSON, err := ctr.Inspect(ctx)
359+
require.NoError(t, err)
360+
require.Equal(t, myLabelValue, ctrJSON.Config.Labels[myLabelName])
361+
require.Equal(t, myBuildOptionValue, ctrJSON.Config.Labels[myBuildOptionLabel])
362+
}
363+
306364
func Test_GetLogsFromFailedContainer(t *testing.T) {
307365
ctx := context.Background()
308366
// directDockerHubReference {
@@ -317,7 +375,7 @@ func Test_GetLogsFromFailedContainer(t *testing.T) {
317375
ContainerRequest: req,
318376
Started: true,
319377
})
320-
terminateContainerOnEnd(t, ctx, c)
378+
testcontainers.CleanupContainer(t, c)
321379
require.Error(t, err)
322380
require.Contains(t, err.Error(), "container exited with code 0")
323381

@@ -417,25 +475,21 @@ func TestImageSubstitutors(t *testing.T) {
417475
ImageSubstitutors: test.substitutors,
418476
}
419477

420-
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
478+
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
421479
ContainerRequest: req,
422480
Started: true,
423481
})
482+
testcontainers.CleanupContainer(t, ctr)
424483
if test.expectedError != nil {
425484
require.ErrorIs(t, err, test.expectedError)
426485
return
427486
}
428487

429-
if err != nil {
430-
t.Fatal(err)
431-
}
432-
defer func() {
433-
terminateContainerOnEnd(t, ctx, container)
434-
}()
488+
require.NoError(t, err)
435489

436490
// enforce the concrete type, as GenericContainer returns an interface,
437491
// which will be changed in future implementations of the library
438-
dockerContainer := container.(*testcontainers.DockerContainer)
492+
dockerContainer := ctr.(*testcontainers.DockerContainer)
439493
assert.Equal(t, test.expectedImage, dockerContainer.Image)
440494
})
441495
}
@@ -455,21 +509,17 @@ func TestShouldStartContainersInParallel(t *testing.T) {
455509
ExposedPorts: []string{nginxDefaultPort},
456510
WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second),
457511
}
458-
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
512+
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
459513
ContainerRequest: req,
460514
Started: true,
461515
})
462-
if err != nil {
463-
t.Fatalf("could not start container: %v", err)
464-
}
516+
testcontainers.CleanupContainer(t, ctr)
517+
require.NoError(t, err)
518+
465519
// mappedPort {
466-
port, err := container.MappedPort(ctx, nginxDefaultPort)
520+
port, err := ctr.MappedPort(ctx, nginxDefaultPort)
467521
// }
468-
if err != nil {
469-
t.Fatalf("could not get mapped port: %v", err)
470-
}
471-
472-
terminateContainerOnEnd(t, ctx, container)
522+
require.NoError(t, err)
473523

474524
t.Logf("Parallel container [iteration_%d] listening on %d\n", i, port.Int())
475525
})
@@ -480,28 +530,28 @@ func ExampleGenericContainer_withSubstitutors() {
480530
ctx := context.Background()
481531

482532
// applyImageSubstitutors {
483-
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
533+
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
484534
ContainerRequest: testcontainers.ContainerRequest{
485535
Image: "alpine:latest",
486536
ImageSubstitutors: []testcontainers.ImageSubstitutor{dockerImageSubstitutor{}},
487537
},
488538
Started: true,
489539
})
490-
// }
491-
if err != nil {
492-
log.Fatalf("could not start container: %v", err)
493-
}
494-
495540
defer func() {
496-
err := container.Terminate(ctx)
497-
if err != nil {
498-
log.Fatalf("could not terminate container: %v", err)
541+
if err := testcontainers.TerminateContainer(ctr); err != nil {
542+
log.Printf("failed to terminate container: %s", err)
499543
}
500544
}()
501545

546+
// }
547+
if err != nil {
548+
log.Printf("could not start container: %v", err)
549+
return
550+
}
551+
502552
// enforce the concrete type, as GenericContainer returns an interface,
503553
// which will be changed in future implementations of the library
504-
dockerContainer := container.(*testcontainers.DockerContainer)
554+
dockerContainer := ctr.(*testcontainers.DockerContainer)
505555

506556
fmt.Println(dockerContainer.Image)
507557

0 commit comments

Comments
 (0)