Skip to content

Commit

Permalink
Merge branch 'main' into windows-fixes
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
mdelapenya committed Sep 23, 2024
2 parents 7676025 + 738e8fc commit dcef9bb
Show file tree
Hide file tree
Showing 260 changed files with 4,968 additions and 4,346 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci-test-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ jobs:

- name: golangci-lint
if: ${{ inputs.platform == 'ubuntu-latest' }}
uses: golangci/golangci-lint-action@9d1e0624a798bb64f6c3cea93db47765312263dc # v5
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.59.1
version: v1.61.0
# Optional: working directory, useful for monorepos
working-directory: ${{ inputs.project-directory }}
# Optional: golangci-lint command line arguments.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ jobs:
matrix:
go-version: [1.22.x, 1.x]
platform: [ubuntu-latest]
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]
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]
uses: ./.github/workflows/ci-test-go.yml
with:
go-version: ${{ matrix.go-version }}
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ TEST-*.xml

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
4 changes: 4 additions & 0 deletions .vscode/.testcontainers-go.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
"name": "module / couchbase",
"path": "../modules/couchbase"
},
{
"name": "module / databend",
"path": "../modules/databend"
},
{
"name": "module / dolt",
"path": "../modules/dolt"
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(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
}
}
2 changes: 1 addition & 1 deletion commons-test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ define go_install
endef

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

$(GOBIN)/gotestsum:
$(call go_install,gotest.tools/gotestsum@latest)
Expand Down
11 changes: 9 additions & 2 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,14 @@ func (c *ContainerRequest) BuildOptions() (types.ImageBuildOptions, error) {
}

if !c.ShouldKeepBuiltImage() {
buildOptions.Labels = core.DefaultLabels(core.SessionID())
dst := GenericLabels()
if err = core.MergeCustomLabels(dst, c.Labels); err != nil {
return types.ImageBuildOptions{}, err
}
if err = core.MergeCustomLabels(dst, buildOptions.Labels); err != nil {
return types.ImageBuildOptions{}, err
}
buildOptions.Labels = dst
}

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

c.HostConfigModifier(&hostConfig)

if hostConfig.Binds != nil && len(hostConfig.Binds) > 0 {
if len(hostConfig.Binds) > 0 {
for _, bind := range hostConfig.Binds {
parts := strings.Split(bind, ":")
if len(parts) != 2 {
Expand Down
112 changes: 81 additions & 31 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"
"time"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -290,8 +291,7 @@ func Test_BuildImageWithContexts(t *testing.T) {
ContainerRequest: req,
Started: true,
})

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

if testCase.ExpectedError != "" {
require.EqualError(t, err, testCase.ExpectedError)
Expand All @@ -303,6 +303,64 @@ func Test_BuildImageWithContexts(t *testing.T) {
}
}

func TestCustomLabelsImage(t *testing.T) {
const (
myLabelName = "org.my.label"
myLabelValue = "my-label-value"
)

ctx := context.Background()
req := testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "alpine:latest",
Labels: map[string]string{myLabelName: myLabelValue},
},
}

ctr, err := testcontainers.GenericContainer(ctx, req)

require.NoError(t, err)
t.Cleanup(func() { assert.NoError(t, ctr.Terminate(ctx)) })

ctrJSON, err := ctr.Inspect(ctx)
require.NoError(t, err)
assert.Equal(t, myLabelValue, ctrJSON.Config.Labels[myLabelName])
}

func TestCustomLabelsBuildOptionsModifier(t *testing.T) {
const (
myLabelName = "org.my.label"
myLabelValue = "my-label-value"
myBuildOptionLabel = "org.my.bo.label"
myBuildOptionValue = "my-bo-label-value"
)

ctx := context.Background()
req := testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: "./testdata",
Dockerfile: "Dockerfile",
BuildOptionsModifier: func(opts *types.ImageBuildOptions) {
opts.Labels = map[string]string{
myBuildOptionLabel: myBuildOptionValue,
}
},
},
Labels: map[string]string{myLabelName: myLabelValue},
},
}

ctr, err := testcontainers.GenericContainer(ctx, req)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)

ctrJSON, err := ctr.Inspect(ctx)
require.NoError(t, err)
require.Equal(t, myLabelValue, ctrJSON.Config.Labels[myLabelName])
require.Equal(t, myBuildOptionValue, ctrJSON.Config.Labels[myBuildOptionLabel])
}

func Test_GetLogsFromFailedContainer(t *testing.T) {
ctx := context.Background()
// directDockerHubReference {
Expand All @@ -317,7 +375,7 @@ func Test_GetLogsFromFailedContainer(t *testing.T) {
ContainerRequest: req,
Started: true,
})
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 @@ -417,25 +475,21 @@ func TestImageSubstitutors(t *testing.T) {
ImageSubstitutors: test.substitutors,
}

container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
testcontainers.CleanupContainer(t, ctr)
if test.expectedError != nil {
require.ErrorIs(t, err, test.expectedError)
return
}

if err != nil {
t.Fatal(err)
}
defer func() {
terminateContainerOnEnd(t, ctx, container)
}()
require.NoError(t, err)

// enforce the concrete type, as GenericContainer returns an interface,
// which will be changed in future implementations of the library
dockerContainer := container.(*testcontainers.DockerContainer)
dockerContainer := ctr.(*testcontainers.DockerContainer)
assert.Equal(t, test.expectedImage, dockerContainer.Image)
})
}
Expand All @@ -455,21 +509,17 @@ func TestShouldStartContainersInParallel(t *testing.T) {
ExposedPorts: []string{nginxDefaultPort},
WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatalf("could not start container: %v", err)
}
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)

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

terminateContainerOnEnd(t, ctx, container)
require.NoError(t, err)

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

// applyImageSubstitutors {
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "alpine:latest",
ImageSubstitutors: []testcontainers.ImageSubstitutor{dockerImageSubstitutor{}},
},
Started: true,
})
// }
if err != nil {
log.Fatalf("could not start container: %v", err)
}

defer func() {
err := container.Terminate(ctx)
if err != nil {
log.Fatalf("could not terminate container: %v", err)
if err := testcontainers.TerminateContainer(ctr); err != nil {
log.Printf("failed to terminate container: %s", err)
}
}()

// }
if err != nil {
log.Printf("could not start container: %v", err)
return
}

// enforce the concrete type, as GenericContainer returns an interface,
// which will be changed in future implementations of the library
dockerContainer := container.(*testcontainers.DockerContainer)
dockerContainer := ctr.(*testcontainers.DockerContainer)

fmt.Println(dockerContainer.Image)

Expand Down
Loading

0 comments on commit dcef9bb

Please sign in to comment.