Skip to content

Commit 7c53c49

Browse files
committed
Merge branch 'main' into refactor/ollama-local
* main: chore(deps): bump sonarsource/sonarcloud-github-action (#2933) feat(termination)!: make container termination timeout configurable (#2926) chore(deps): bump slackapi/slack-github-action from 1.26.0 to 2.0.0 (#2934) chore(deps): bump github/codeql-action from 3.25.15 to 3.28.0 (#2932)
2 parents 52bd20f + 7ca837d commit 7c53c49

File tree

11 files changed

+195
-59
lines changed

11 files changed

+195
-59
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ jobs:
140140
merge-multiple: true
141141

142142
- name: Analyze with SonarCloud
143-
uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1
143+
uses: sonarsource/sonarcloud-github-action@02ef91109b2d589e757aefcfb2854c2783fd7b19 # v4.0.0
144144
env:
145145
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
146146
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

.github/workflows/codeql.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353

5454
# Initializes the CodeQL tools for scanning.
5555
- name: Initialize CodeQL
56-
uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
56+
uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
5757
with:
5858
languages: ${{ matrix.language }}
5959
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -67,7 +67,7 @@ jobs:
6767
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
6868
# If this step fails, then you should remove it and run the build manually (see below)
6969
- name: Autobuild
70-
uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
70+
uses: github/codeql-action/autobuild@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
7171

7272
# ℹ️ Command-line programs to run using the OS shell.
7373
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -80,6 +80,6 @@ jobs:
8080
# ./location_of_script_within_repo/buildscript.sh
8181

8282
- name: Perform CodeQL Analysis
83-
uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
83+
uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
8484
with:
8585
category: "/language:${{matrix.language}}"

.github/workflows/docker-moby-latest.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ jobs:
7070
- name: Notify to Slack on failures
7171
if: failure()
7272
id: slack
73-
uses: slackapi/slack-github-action@v1.26.0
73+
uses: slackapi/slack-github-action@v2.0.0
7474
with:
75+
payload-templated: true
7576
payload-file-path: "./payload-slack-content.json"
7677
env:
7778
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DOCKER_LATEST_WEBHOOK }}

.github/workflows/scorecards.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,6 @@ jobs:
5151

5252
# required for Code scanning alerts
5353
- name: "Upload SARIF results to code scanning"
54-
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
54+
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
5555
with:
5656
sarif_file: results.sarif

cleanup.go

+57-41
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,74 @@ import (
88
"time"
99
)
1010

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
11+
// TerminateOptions is a type that holds the options for terminating a container.
12+
type TerminateOptions struct {
13+
ctx context.Context
14+
stopTimeout *time.Duration
15+
volumes []string
1616
}
1717

1818
// TerminateOption is a type that represents an option for terminating a container.
19-
type TerminateOption func(*terminateOptions)
19+
type TerminateOption func(*TerminateOptions)
20+
21+
// NewTerminateOptions returns a fully initialised TerminateOptions.
22+
// Defaults: StopTimeout: 10 seconds.
23+
func NewTerminateOptions(ctx context.Context, opts ...TerminateOption) *TerminateOptions {
24+
timeout := time.Second * 10
25+
options := &TerminateOptions{
26+
stopTimeout: &timeout,
27+
ctx: ctx,
28+
}
29+
for _, opt := range opts {
30+
opt(options)
31+
}
32+
return options
33+
}
34+
35+
// Context returns the context to use during a Terminate.
36+
func (o *TerminateOptions) Context() context.Context {
37+
return o.ctx
38+
}
39+
40+
// StopTimeout returns the stop timeout to use during a Terminate.
41+
func (o *TerminateOptions) StopTimeout() *time.Duration {
42+
return o.stopTimeout
43+
}
44+
45+
// Cleanup performs any clean up needed
46+
func (o *TerminateOptions) Cleanup() error {
47+
// TODO: simplify this when when perform the client refactor.
48+
if len(o.volumes) == 0 {
49+
return nil
50+
}
51+
client, err := NewDockerClientWithOpts(o.ctx)
52+
if err != nil {
53+
return fmt.Errorf("docker client: %w", err)
54+
}
55+
defer client.Close()
56+
// Best effort to remove all volumes.
57+
var errs []error
58+
for _, volume := range o.volumes {
59+
if errRemove := client.VolumeRemove(o.ctx, volume, true); errRemove != nil {
60+
errs = append(errs, fmt.Errorf("volume remove %q: %w", volume, errRemove))
61+
}
62+
}
63+
return errors.Join(errs...)
64+
}
2065

2166
// StopContext returns a TerminateOption that sets the context.
2267
// Default: context.Background().
2368
func StopContext(ctx context.Context) TerminateOption {
24-
return func(c *terminateOptions) {
69+
return func(c *TerminateOptions) {
2570
c.ctx = ctx
2671
}
2772
}
2873

2974
// StopTimeout returns a TerminateOption that sets the timeout.
3075
// Default: See [Container.Stop].
3176
func StopTimeout(timeout time.Duration) TerminateOption {
32-
return func(c *terminateOptions) {
33-
c.timeout = &timeout
77+
return func(c *TerminateOptions) {
78+
c.stopTimeout = &timeout
3479
}
3580
}
3681

@@ -39,7 +84,7 @@ func StopTimeout(timeout time.Duration) TerminateOption {
3984
// which are not removed by default.
4085
// Default: nil.
4186
func RemoveVolumes(volumes ...string) TerminateOption {
42-
return func(c *terminateOptions) {
87+
return func(c *TerminateOptions) {
4388
c.volumes = volumes
4489
}
4590
}
@@ -54,41 +99,12 @@ func TerminateContainer(container Container, options ...TerminateOption) error {
5499
return nil
55100
}
56101

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)
102+
err := container.Terminate(context.Background(), options...)
67103
if !isCleanupSafe(err) {
68104
return fmt.Errorf("terminate: %w", err)
69105
}
70106

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...)
107+
return nil
92108
}
93109

94110
// isNil returns true if val is nil or an nil instance false otherwise.

container.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type Container interface {
5050
Stop(context.Context, *time.Duration) error // stop the container
5151

5252
// Terminate stops and removes the container and its image if it was built and not flagged as kept.
53-
Terminate(ctx context.Context) error
53+
Terminate(ctx context.Context, opts ...TerminateOption) error
5454

5555
Logs(context.Context) (io.ReadCloser, error) // Get logs of the container
5656
FollowOutput(LogConsumer) // Deprecated: it will be removed in the next major release

docker.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -303,12 +303,11 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro
303303
// The following hooks are called in order:
304304
// - [ContainerLifecycleHooks.PreTerminates]
305305
// - [ContainerLifecycleHooks.PostTerminates]
306-
func (c *DockerContainer) Terminate(ctx context.Context) error {
307-
// ContainerRemove hardcodes stop timeout to 3 seconds which is too short
308-
// to ensure that child containers are stopped so we manually call stop.
309-
// TODO: make this configurable via a functional option.
310-
timeout := 10 * time.Second
311-
err := c.Stop(ctx, &timeout)
306+
//
307+
// Default: timeout is 10 seconds.
308+
func (c *DockerContainer) Terminate(ctx context.Context, opts ...TerminateOption) error {
309+
options := NewTerminateOptions(ctx, opts...)
310+
err := c.Stop(options.Context(), options.StopTimeout())
312311
if err != nil && !isCleanupSafe(err) {
313312
return fmt.Errorf("stop: %w", err)
314313
}
@@ -343,6 +342,10 @@ func (c *DockerContainer) Terminate(ctx context.Context) error {
343342
c.sessionID = ""
344343
c.isRunning = false
345344

345+
if err = options.Cleanup(); err != nil {
346+
errs = append(errs, err)
347+
}
348+
346349
return errors.Join(errs...)
347350
}
348351

docker_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,18 @@ func TestContainerStateAfterTermination(t *testing.T) {
281281
require.Nil(t, state, "expected nil container inspect.")
282282
})
283283

284+
t.Run("termination-timeout", func(t *testing.T) {
285+
ctx := context.Background()
286+
nginx, err := createContainerFn(ctx)
287+
require.NoError(t, err)
288+
289+
err = nginx.Start(ctx)
290+
require.NoError(t, err, "expected no error from container start.")
291+
292+
err = nginx.Terminate(ctx, StopTimeout(5*time.Microsecond))
293+
require.NoError(t, err)
294+
})
295+
284296
t.Run("Nil State after termination if raw as already set", func(t *testing.T) {
285297
ctx := context.Background()
286298
nginx, err := createContainerFn(ctx)
@@ -1077,6 +1089,38 @@ func TestContainerCreationWithVolumeAndFileWritingToIt(t *testing.T) {
10771089
{
10781090
HostFilePath: absPath,
10791091
ContainerFilePath: "/hello.sh",
1092+
FileMode: 700,
1093+
},
1094+
},
1095+
Mounts: Mounts(VolumeMount(volumeName, "/data")),
1096+
Cmd: []string{"bash", "/hello.sh"},
1097+
WaitingFor: wait.ForLog("done"),
1098+
},
1099+
Started: true,
1100+
})
1101+
CleanupContainer(t, bashC, RemoveVolumes(volumeName))
1102+
require.NoError(t, err)
1103+
}
1104+
1105+
func TestContainerCreationWithVolumeCleaning(t *testing.T) {
1106+
absPath, err := filepath.Abs(filepath.Join(".", "testdata", "hello.sh"))
1107+
require.NoError(t, err)
1108+
ctx, cnl := context.WithTimeout(context.Background(), 30*time.Second)
1109+
defer cnl()
1110+
1111+
// Create the volume.
1112+
volumeName := "volumeName"
1113+
1114+
// Create the container that writes into the mounted volume.
1115+
bashC, err := GenericContainer(ctx, GenericContainerRequest{
1116+
ProviderType: providerType,
1117+
ContainerRequest: ContainerRequest{
1118+
Image: "bash:5.2.26",
1119+
Files: []ContainerFile{
1120+
{
1121+
HostFilePath: absPath,
1122+
ContainerFilePath: "/hello.sh",
1123+
FileMode: 700,
10801124
},
10811125
},
10821126
Mounts: Mounts(VolumeMount(volumeName, "/data")),
@@ -1085,10 +1129,41 @@ func TestContainerCreationWithVolumeAndFileWritingToIt(t *testing.T) {
10851129
},
10861130
Started: true,
10871131
})
1132+
require.NoError(t, err)
1133+
err = bashC.Terminate(ctx, RemoveVolumes(volumeName))
10881134
CleanupContainer(t, bashC, RemoveVolumes(volumeName))
10891135
require.NoError(t, err)
10901136
}
10911137

1138+
func TestContainerTerminationOptions(t *testing.T) {
1139+
t.Run("volumes", func(t *testing.T) {
1140+
var options TerminateOptions
1141+
RemoveVolumes("vol1", "vol2")(&options)
1142+
require.Equal(t, TerminateOptions{
1143+
volumes: []string{"vol1", "vol2"},
1144+
}, options)
1145+
})
1146+
t.Run("stop-timeout", func(t *testing.T) {
1147+
var options TerminateOptions
1148+
timeout := 11 * time.Second
1149+
StopTimeout(timeout)(&options)
1150+
require.Equal(t, TerminateOptions{
1151+
stopTimeout: &timeout,
1152+
}, options)
1153+
})
1154+
1155+
t.Run("all", func(t *testing.T) {
1156+
var options TerminateOptions
1157+
timeout := 9 * time.Second
1158+
StopTimeout(timeout)(&options)
1159+
RemoveVolumes("vol1", "vol2")(&options)
1160+
require.Equal(t, TerminateOptions{
1161+
stopTimeout: &timeout,
1162+
volumes: []string{"vol1", "vol2"},
1163+
}, options)
1164+
})
1165+
}
1166+
10921167
func TestContainerWithTmpFs(t *testing.T) {
10931168
ctx := context.Background()
10941169
req := ContainerRequest{

docs/features/garbage_collector.md

+41
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,47 @@ The primary method is to use the `Terminate(context.Context)` function that is
1717
available when a container is created. Use `defer` to ensure that it is called
1818
on test completion.
1919

20+
The `Terminate` function can be customised with termination options to determine how a container is removed: termination timeout, and the ability to remove container volumes are supported at the moment. You can build the default options using the `testcontainers.NewTerminationOptions` function.
21+
22+
#### NewTerminateOptions
23+
24+
- Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
25+
26+
If you want to attach option to container termination, you can use the `testcontainers.NewTerminateOptions(ctx context.Context, opts ...TerminateOption) *TerminateOptions` option, which receives a TerminateOption as parameter, creating custom termination options to be passed on the container termination.
27+
28+
##### Terminate Options
29+
30+
###### [StopContext](../../cleanup.go)
31+
Sets the context for the Container termination.
32+
33+
- **Function**: `StopContext(ctx context.Context) TerminateOption`
34+
- **Default**: The context passed in `Terminate()`
35+
- **Usage**:
36+
```go
37+
err := container.Terminate(ctx,StopContext(context.Background()))
38+
```
39+
40+
###### [StopTimeout](../../cleanup.go)
41+
Sets the timeout for stopping the Container.
42+
43+
- **Function**: ` StopTimeout(timeout time.Duration) TerminateOption`
44+
- **Default**: 10 seconds
45+
- **Usage**:
46+
```go
47+
err := container.Terminate(ctx, StopTimeout(20 * time.Second))
48+
```
49+
50+
###### [RemoveVolumes](../../cleanup.go)
51+
Sets the volumes to be removed during Container termination.
52+
53+
- **Function**: ` RemoveVolumes(volumes ...string) TerminateOption`
54+
- **Default**: Empty (no volumes removed)
55+
- **Usage**:
56+
```go
57+
err := container.Terminate(ctx, RemoveVolumes("vol1", "vol2"))
58+
```
59+
60+
2061
!!!tip
2162

2263
Remember to `defer` as soon as possible so you won't forget. The best time

modules/etcd/etcd.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,18 @@ type EtcdContainer struct {
2929

3030
// Terminate terminates the etcd container, its child nodes, and the network in which the cluster is running
3131
// to communicate between the nodes.
32-
func (c *EtcdContainer) Terminate(ctx context.Context) error {
32+
func (c *EtcdContainer) Terminate(ctx context.Context, opts ...testcontainers.TerminateOption) error {
3333
var errs []error
3434

3535
// child nodes has no other children
3636
for i, child := range c.childNodes {
37-
if err := child.Terminate(ctx); err != nil {
37+
if err := child.Terminate(ctx, opts...); err != nil {
3838
errs = append(errs, fmt.Errorf("terminate child node(%d): %w", i, err))
3939
}
4040
}
4141

4242
if c.Container != nil {
43-
if err := c.Container.Terminate(ctx); err != nil {
43+
if err := c.Container.Terminate(ctx, opts...); err != nil {
4444
errs = append(errs, fmt.Errorf("terminate cluster node: %w", err))
4545
}
4646
}

0 commit comments

Comments
 (0)