Skip to content

Commit

Permalink
fix: add missing methods
Browse files Browse the repository at this point in the history
  • Loading branch information
mdelapenya committed Sep 6, 2024
1 parent a780739 commit 0589878
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 0 deletions.
79 changes: 79 additions & 0 deletions cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package testcontainers

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

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

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

// 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(container Container, options ...TerminateOption) error {
if isNil(container) {
return nil
}

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

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

// TODO: Add a timeout when terminate supports it.
err := container.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
}
}
57 changes: 57 additions & 0 deletions testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,66 @@ package testcontainers
import (
"context"
"fmt"
"regexp"
"testing"

"github.com/docker/docker/errdefs"
)

// errAlreadyInProgress is a regular expression that matches the error for a container
// removal that is already in progress.
var errAlreadyInProgress = regexp.MustCompile(`removal of container .* is already in progress`)

// causer is an interface that allows to get the cause of an error.
type causer interface {
Cause() error
}

// wrapErr is an interface that allows to unwrap an error.
type wrapErr interface {
Unwrap() error
}

// unwrapErrs is an interface that allows to unwrap multiple errors.
type unwrapErrs interface {
Unwrap() []error
}

// isCleanupSafe reports whether all errors in err's tree are one of the
// following, so can safely be ignored:
// - nil
// - not found
// - already in progress
func isCleanupSafe(err error) bool {
if err == nil {
return true
}

switch x := err.(type) { //nolint:errorlint // We need to check for interfaces.
case errdefs.ErrNotFound:
return true
case errdefs.ErrConflict:
// Terminating a container that is already terminating.
if errAlreadyInProgress.MatchString(err.Error()) {
return true
}
return false
case causer:
return isCleanupSafe(x.Cause())
case wrapErr:
return isCleanupSafe(x.Unwrap())
case unwrapErrs:
for _, e := range x.Unwrap() {
if !isCleanupSafe(e) {
return false
}
}
return true
default:
return false
}
}

// SkipIfProviderIsNotHealthy is a utility function capable of skipping tests
// if the provider is not healthy, or running at all.
// This is a function designed to be used in your test, when Docker is not mandatory for CI/CD.
Expand Down

0 comments on commit 0589878

Please sign in to comment.