Skip to content

Commit

Permalink
Merge branch 'main' into v1
Browse files Browse the repository at this point in the history
* main:
  fix: resource clean up for tests and examples (#2738)
  • Loading branch information
mdelapenya committed Sep 13, 2024
2 parents e1705ce + b60497e commit d328e09
Show file tree
Hide file tree
Showing 220 changed files with 3,630 additions and 4,168 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ TEST-*.xml
coverage.out
tcvenv

**/go.work
**/go.work

# VS Code settings
.vscode
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ linters:
- nonamedreturns
- testifylint
- errcheck
- nolintlint

linters-settings:
errorlint:
Expand Down
107 changes: 107 additions & 0 deletions cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package testcontainers

import (
"context"
"errors"
"fmt"
"reflect"
"time"
)

// terminateOptions is a type that holds the options for terminating a container.
type terminateOptions struct {
ctx context.Context
timeout *time.Duration
volumes []string
}

// TerminateOption is a type that represents an option for terminating a container.
type TerminateOption func(*terminateOptions)

// StopContext returns a TerminateOption that sets the context.
// Default: context.Background().
func StopContext(ctx context.Context) TerminateOption {
return func(c *terminateOptions) {
c.ctx = ctx
}
}

// StopTimeout returns a TerminateOption that sets the timeout.
// Default: See [Container.Stop].
func StopTimeout(timeout time.Duration) TerminateOption {
return func(c *terminateOptions) {
c.timeout = &timeout
}
}

// RemoveVolumes returns a TerminateOption that sets additional volumes to remove.
// This is useful when the container creates named volumes that should be removed
// which are not removed by default.
// Default: nil.
func RemoveVolumes(volumes ...string) TerminateOption {
return func(c *terminateOptions) {
c.volumes = volumes
}
}

// TerminateContainer calls [Container.Terminate] on the container if it is not nil.
//
// This should be called as a defer directly after [GenericContainer](...)
// or a modules Run(...) to ensure the container is terminated when the
// function ends.
func TerminateContainer(ctr StartedContainer, options ...TerminateOption) error {
if isNil(ctr) {
return nil
}

c := &terminateOptions{
ctx: context.Background(),
}

for _, opt := range options {
opt(c)
}

// TODO: Add a timeout when terminate supports it.
err := ctr.Terminate(c.ctx)
if !isCleanupSafe(err) {
return fmt.Errorf("terminate: %w", err)
}

// Remove additional volumes if any.
if len(c.volumes) == 0 {
return nil
}

client, err := NewDockerClientWithOpts(c.ctx)
if err != nil {
return fmt.Errorf("docker client: %w", err)
}

defer client.Close()

// Best effort to remove all volumes.
var errs []error
for _, volume := range c.volumes {
if errRemove := client.VolumeRemove(c.ctx, volume, true); errRemove != nil {
errs = append(errs, fmt.Errorf("volume remove %q: %w", volume, errRemove))
}
}

return errors.Join(errs...)
}

// isNil returns true if val is nil or an nil instance false otherwise.
func isNil(val any) bool {
if val == nil {
return true
}

valueOf := reflect.ValueOf(val)
switch valueOf.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
return valueOf.IsNil()
default:
return false
}
}
25 changes: 8 additions & 17 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,7 @@ func TestBuildImageWithContexts(t *testing.T) {
}

c, err := testcontainers.Run(ctx, req)

defer testcontainers.TerminateContainerOnEnd(t, ctx, c)
testcontainers.CleanupContainer(t, c)

if testCase.ExpectedError != "" {
require.EqualError(t, err, testCase.ExpectedError)
Expand All @@ -313,7 +312,7 @@ func TestGetLogsFromFailedContainer(t *testing.T) {
// }

c, err := testcontainers.Run(ctx, req)
testcontainers.TerminateContainerOnEnd(t, ctx, c)
testcontainers.CleanupContainer(t, c)
require.Error(t, err)
require.Contains(t, err.Error(), "container exited with code 0")

Expand Down Expand Up @@ -386,17 +385,13 @@ func TestImageSubstitutors(t *testing.T) {
}

ctr, err := testcontainers.Run(ctx, req)
testcontainers.CleanupContainer(t, ctr)
if test.expectedError != nil {
require.ErrorIs(t, err, test.expectedError)
return
}

if err != nil {
t.Fatal(err)
}
defer func() {
testcontainers.TerminateContainerOnEnd(t, ctx, ctr)
}()
require.NoError(t, err)

assert.Equal(t, test.expectedImage, ctr.Image)
})
Expand All @@ -419,17 +414,13 @@ func TestShouldStartContainersInParallel(t *testing.T) {
Started: true,
}
ctr, err := testcontainers.Run(ctx, req)
if err != nil {
t.Fatalf("could not start container: %v", err)
}
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)

// mappedPort {
port, err := ctr.MappedPort(ctx, nginxDefaultPort)
// }
if err != nil {
t.Fatalf("could not get mapped port: %v", err)
}

testcontainers.TerminateContainerOnEnd(t, ctx, ctr)
require.NoError(t, err)

t.Logf("Parallel container [iteration_%d] listening on %d\n", i, port.Int())
})
Expand Down
30 changes: 26 additions & 4 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,9 +624,14 @@ func (c *DockerContainer) State(ctx context.Context) (*types.ContainerState, err
// otherwise the engine default. A negative timeout value can be specified,
// meaning no timeout, i.e. no forceful termination is performed.
func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) error {
// Note we can't check isRunning here because we allow external creation
// without exposing the ability to fully initialize the container state.
// See: https://github.com/testcontainers/testcontainers-go/issues/2667
// TODO: Add a check for isRunning when the above issue is resolved.

err := c.stoppingHook(ctx)
if err != nil {
return err
return fmt.Errorf("stopping hook: %w", err)
}

var options container.StopOptions
Expand All @@ -643,21 +648,36 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro
defer cli.Close()

if err := cli.ContainerStop(ctx, c.ID, options); err != nil {
return err
return fmt.Errorf("container stop: %w", err)
}

c.isRunning = false

err = c.stoppedHook(ctx)
if err != nil {
return err
return fmt.Errorf("stopped hook: %w", err)
}

return nil
}

// Terminate is used to kill the container. It is usually triggered by as defer function.
// Terminate calls stops and then removes the container including its volumes.
// If its image was built it and all child images are also removed unless
// the [FromDockerfile.KeepImage] on the [ContainerRequest] was set to true.
//
// The following hooks are called in order:
// - [ContainerLifecycleHooks.PreTerminates]
// - [ContainerLifecycleHooks.PostTerminates]
func (c *DockerContainer) Terminate(ctx context.Context) error {
// ContainerRemove hardcodes stop timeout to 3 seconds which is too short
// to ensure that child containers are stopped so we manually call stop.
// TODO: make this configurable via a functional option.
timeout := 10 * time.Second
err := c.Stop(ctx, &timeout)
if err != nil && !isCleanupSafe(err) {
return fmt.Errorf("stop: %w", err)
}

select {
// close reaper if it was created
case c.terminationSignal <- true:
Expand All @@ -670,6 +690,8 @@ func (c *DockerContainer) Terminate(ctx context.Context) error {
}
defer cli.Close()

// TODO: Handle errors from ContainerRemove more correctly, e.g. should we
// run the terminated hook?
errs := []error{
c.terminatingHook(ctx),
cli.ContainerRemove(ctx, c.GetContainerID(), container.RemoveOptions{
Expand Down
15 changes: 6 additions & 9 deletions docker_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ func TestBuildContainerFromDockerfile(t *testing.T) {
}

redisC, err := Run(ctx, req)
CleanupContainer(t, redisC)
require.NoError(t, err)
TerminateContainerOnEnd(t, ctx, redisC)
}

// removeImageFromLocalCache removes the image from the local cache
Expand Down Expand Up @@ -67,8 +67,7 @@ func TestBuildContainerFromDockerfileWithDockerAuthConfig(t *testing.T) {
BuildArgs: map[string]*string{
"REGISTRY_HOST": &registryHost,
},
Repo: "localhost",
PrintBuildLog: true,
Repo: "localhost",
},
AlwaysPullImage: true, // make sure the authentication takes place
ExposedPorts: []string{"6379/tcp"},
Expand All @@ -77,8 +76,8 @@ func TestBuildContainerFromDockerfileWithDockerAuthConfig(t *testing.T) {
}

redisC, err := Run(ctx, req)
CleanupContainer(t, redisC)
require.NoError(t, err)
TerminateContainerOnEnd(t, ctx, redisC)
}

func TestBuildContainerFromDockerfileShouldFailWithWrongDockerAuthConfig(t *testing.T) {
Expand All @@ -104,8 +103,8 @@ func TestBuildContainerFromDockerfileShouldFailWithWrongDockerAuthConfig(t *test
}

redisC, err := Run(ctx, req)
CleanupContainer(t, redisC)
require.Error(t, err)
TerminateContainerOnEnd(t, ctx, redisC)
}

func TestCreateContainerFromPrivateRegistry(t *testing.T) {
Expand All @@ -124,8 +123,8 @@ func TestCreateContainerFromPrivateRegistry(t *testing.T) {
}

redisContainer, err := Run(ctx, req)
CleanupContainer(t, redisContainer)
require.NoError(t, err)
TerminateContainerOnEnd(t, ctx, redisContainer)
}

func prepareLocalRegistryWithAuth(t *testing.T) string {
Expand Down Expand Up @@ -157,6 +156,7 @@ func prepareLocalRegistryWithAuth(t *testing.T) string {
}

registryC, err := Run(ctx, req)
CleanupContainer(t, registryC)
require.NoError(t, err)

mappedPort, err := registryC.MappedPort(ctx, "5000/tcp")
Expand All @@ -169,9 +169,6 @@ func prepareLocalRegistryWithAuth(t *testing.T) string {
t.Cleanup(func() {
removeImageFromLocalCache(t, addr+"/redis:5.0-alpine")
})
t.Cleanup(func() {
require.NoError(t, registryC.Terminate(context.Background()))
})

_, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
Expand Down
Loading

0 comments on commit d328e09

Please sign in to comment.