diff --git a/.github/workflows/ci-test-go.yml b/.github/workflows/ci-test-go.yml index c2c0747278..df544fdc61 100644 --- a/.github/workflows/ci-test-go.yml +++ b/.github/workflows/ci-test-go.yml @@ -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@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 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. @@ -85,13 +85,25 @@ jobs: # takes precedence over all other caching options. skip-cache: true + - name: generate + if: ${{ inputs.platform == 'ubuntu-latest' }} + working-directory: ./${{ inputs.project-directory }} + shell: bash + run: | + make generate + git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]] + - name: modVerify working-directory: ./${{ inputs.project-directory }} run: go mod verify - name: modTidy + if: ${{ inputs.platform == 'ubuntu-latest' }} working-directory: ./${{ inputs.project-directory }} - run: make tidy + shell: bash + run: | + make tidy + git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]] - name: ensure compilation working-directory: ./${{ inputs.project-directory }} @@ -107,11 +119,18 @@ jobs: timeout-minutes: 30 run: make test-unit + - name: Set sonar artifact name + # For the core library, where the project directory is '.', we'll use "core" as artifact name. + # For the modules, we'll remove the slashes, keeping the name of the module + if: ${{ github.ref_name == 'main' && github.repository_owner == 'testcontainers' && inputs.platform == 'ubuntu-latest' && inputs.run-tests && !inputs.rootless-docker && !inputs.ryuk-disabled }} + run: | + echo "ARTIFACT_NAME=$(basename ${{ inputs.project-directory == '.' && 'core' || inputs.project-directory }})-${{ inputs.go-version }}-${{ inputs.platform }}" >> $GITHUB_ENV + - name: Upload SonarCloud files - if: ${{ github.ref_name == 'main' && github.repository_owner == 'testcontainers' && inputs.run-tests && !inputs.rootless-docker }} - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 + if: ${{ github.ref_name == 'main' && github.repository_owner == 'testcontainers' && inputs.platform == 'ubuntu-latest' && inputs.run-tests && !inputs.rootless-docker && !inputs.ryuk-disabled }} + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: - name: sonarcloud + name: sonarcloud-${{ env.ARTIFACT_NAME }} path: | ./sonar-project.properties ${{ inputs.project-directory }}/TEST-unit.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f326e9c1da..10fb7f4949 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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, dynamodb, elasticsearch, etcd, gcloud, grafana-lgtm, inbucket, influxdb, k3s, k6, kafka, localstack, mariadb, meilisearch, milvus, minio, mockserver, mongodb, mssql, mysql, nats, neo4j, ollama, openfga, openldap, opensearch, postgres, pulsar, qdrant, rabbitmq, redis, redpanda, registry, surrealdb, valkey, vault, vearch, weaviate, yugabytedb] uses: ./.github/workflows/ci-test-go.yml with: go-version: ${{ matrix.go-version }} @@ -134,9 +134,10 @@ jobs: # Disabling shallow clone is recommended for improving relevancy of reporting fetch-depth: 0 - - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: sonarcloud + pattern: sonarcloud-* + merge-multiple: true - name: Analyze with SonarCloud uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1 diff --git a/.github/workflows/docker-moby-latest.yml b/.github/workflows/docker-moby-latest.yml index 39cb96df3d..bebb968652 100644 --- a/.github/workflows/docker-moby-latest.yml +++ b/.github/workflows/docker-moby-latest.yml @@ -10,6 +10,8 @@ jobs: strategy: matrix: rootless-docker: [true, false] + containerd-integration: [true, false] + name: "Core tests using latest moby/moby" runs-on: 'ubuntu-latest' continue-on-error: true @@ -17,14 +19,7 @@ jobs: - name: Set the Docker Install type run: | echo "docker_install_type=${{ matrix.rootless-docker == true && 'Rootless' || 'Rootful' }}" >> "$GITHUB_ENV" - - - name: Setup rootless Docker - if: ${{ matrix.rootless-docker }} - uses: ScribeMD/rootless-docker@6bd157a512c2fafa4e0243a8aa87d964eb890886 # v0.2.2 - - - name: Remove Docker root socket - if: ${{ matrix.rootless-docker }} - run: sudo rm -rf /var/run/docker.sock + echo "containerd_integration=${{ matrix.containerd-integration == true && 'containerd' || '' }}" >> "$GITHUB_ENV" - name: Check out code into the Go module directory uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 @@ -42,8 +37,18 @@ jobs: - name: modTidy run: go mod tidy - - name: Install Latest Docker - run: curl https://get.docker.com | CHANNEL=test sh + - name: Install Nightly Docker + uses: crazy-max/ghaction-setup-docker@master + with: + rootless: ${{ matrix.rootless-docker }} + version: type=image,tag=master + daemon-config: | + { + "debug": true, + "features": { + "containerd-snapshotter": ${{ matrix.containerd-integration }} + } + } - name: go test timeout-minutes: 30 @@ -56,6 +61,7 @@ jobs: { "tc_project": "testcontainers-go", "tc_docker_install_type": "${docker_install_type}", + "tc_containerd_integration": "${containerd_integration}", "tc_github_action_url": "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}", "tc_github_action_status": "FAILED", "tc_slack_channel_id": "${{ secrets.SLACK_DOCKER_LATEST_CHANNEL_ID }}" diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 29c2b72ee0..7bab45a5aa 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -16,6 +16,6 @@ jobs: pull-requests: write # for release-drafter/release-drafter to add label to PR runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5.19.0 + - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 # v5.19.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 82b14c7ad4..58de84c72e 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -43,7 +43,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: SARIF file path: results.sarif diff --git a/.gitignore b/.gitignore index 4b420b86b4..e529356359 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ TEST-*.xml tcvenv -**/go.work \ No newline at end of file +**/go.work + +# VS Code settings +.vscode diff --git a/.golangci.yml b/.golangci.yml index 1791b9caac..d708f003e3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,15 +1,21 @@ linters: enable: + - errcheck - errorlint - gci - gocritic - gofumpt - misspell - - nonamedreturns + - nolintlint + - nakedret + - perfsprint - testifylint - - errcheck + - thelper + - usestdlibvars linters-settings: + nakedret: + max-func-lines: 0 errorlint: # Check whether fmt.Errorf uses the %w verb for formatting errors. # See the https://github.com/polyfloyd/go-errorlint for caveats. @@ -29,16 +35,6 @@ linters-settings: disable: - float-compare - go-require - enable: - - bool-compare - - compares - - empty - - error-is-as - - error-nil - - expected-actual - - len - - require-error - - suite-dont-use-pkg - - suite-extra-assert-call + enable-all: true run: timeout: 5m diff --git a/.vscode/.testcontainers-go.code-workspace b/.vscode/.testcontainers-go.code-workspace index 156da3891d..bc7e7aead9 100644 --- a/.vscode/.testcontainers-go.code-workspace +++ b/.vscode/.testcontainers-go.code-workspace @@ -49,14 +49,26 @@ "name": "module / couchbase", "path": "../modules/couchbase" }, + { + "name": "module / databend", + "path": "../modules/databend" + }, { "name": "module / dolt", "path": "../modules/dolt" }, + { + "name": "module / dynamodb", + "path": "../modules/dynamodb" + }, { "name": "module / elasticsearch", "path": "../modules/elasticsearch" }, + { + "name": "module / etcd", + "path": "../modules/etcd" + }, { "name": "module / gcloud", "path": "../modules/gcloud" @@ -93,6 +105,10 @@ "name": "module / mariadb", "path": "../modules/mariadb" }, + { + "name": "module / meilisearch", + "path": "../modules/meilisearch" + }, { "name": "module / milvus", "path": "../modules/milvus" @@ -189,6 +205,10 @@ "name": "module / weaviate", "path": "../modules/weaviate" }, + { + "name": "module / yugabytedb", + "path": "../modules/yugabytedb" + }, { "name": "modulegen", "path": "../modulegen" diff --git a/cleanup.go b/cleanup.go new file mode 100644 index 0000000000..e2d52440b9 --- /dev/null +++ b/cleanup.go @@ -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 + } +} diff --git a/commons-test.mk b/commons-test.mk index 04d0a6e70c..91ed6a1244 100644 --- a/commons-test.mk +++ b/commons-test.mk @@ -6,18 +6,22 @@ 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) +$(GOBIN)/mockery: + $(call go_install,github.com/vektra/mockery/v2@v2.45) + .PHONY: install -install: $(GOBIN)/golangci-lint $(GOBIN)/gotestsum +install: $(GOBIN)/golangci-lint $(GOBIN)/gotestsum $(GOBIN)/mockery .PHONY: clean clean: rm $(GOBIN)/golangci-lint rm $(GOBIN)/gotestsum + rm $(GOBIN)/mockery .PHONY: dependencies-scan dependencies-scan: @@ -26,7 +30,11 @@ dependencies-scan: .PHONY: lint lint: $(GOBIN)/golangci-lint - golangci-lint run --out-format=github-actions --path-prefix=. --verbose -c $(ROOT_DIR)/.golangci.yml --fix + golangci-lint run --out-format=colored-line-number --path-prefix=. --verbose -c $(ROOT_DIR)/.golangci.yml --fix + +.PHONY: generate +generate: $(GOBIN)/mockery + go generate ./... .PHONY: test-% test-%: $(GOBIN)/gotestsum @@ -39,7 +47,8 @@ test-%: $(GOBIN)/gotestsum -- \ -v \ -coverprofile=coverage.out \ - -timeout=30m + -timeout=30m \ + -race .PHONY: tools tools: @@ -51,3 +60,6 @@ test-tools: $(GOBIN)/gotestsum .PHONY: tidy tidy: go mod tidy + +.PHONY: pre-commit +pre-commit: generate tidy lint diff --git a/container.go b/container.go index 8747335a28..90a7ba91cb 100644 --- a/container.go +++ b/container.go @@ -37,17 +37,17 @@ type DeprecatedContainer interface { // Container allows getting info about and controlling a single container instance type Container interface { - GetContainerID() string // get the container id from the provider - Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the lowest exposed port - PortEndpoint(context.Context, nat.Port, string) (string, error) // get proto://ip:port string for the given exposed port - Host(context.Context) (string, error) // get host where the container port is exposed - Inspect(context.Context) (*types.ContainerJSON, error) // get container info - MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port - Ports(context.Context) (nat.PortMap, error) // Deprecated: Use c.Inspect(ctx).NetworkSettings.Ports instead - SessionID() string // get session id - IsRunning() bool // IsRunning returns true if the container is running, false otherwise. - Start(context.Context) error // start the container - Stop(context.Context, *time.Duration) error // stop the container + GetContainerID() string // get the container id from the provider + Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the lowest exposed port + PortEndpoint(ctx context.Context, port nat.Port, proto string) (string, error) // get proto://ip:port string for the given exposed port + Host(context.Context) (string, error) // get host where the container port is exposed + Inspect(context.Context) (*types.ContainerJSON, error) // get container info + MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port + Ports(context.Context) (nat.PortMap, error) // Deprecated: Use c.Inspect(ctx).NetworkSettings.Ports instead + SessionID() string // get session id + IsRunning() bool // IsRunning returns true if the container is running, false otherwise. + Start(context.Context) error // start the container + Stop(context.Context, *time.Duration) error // stop the container // Terminate stops and removes the container and its image if it was built and not flagged as kept. Terminate(ctx context.Context) error @@ -74,7 +74,7 @@ type Container interface { type ImageBuildInfo interface { BuildOptions() (types.ImageBuildOptions, error) // converts the ImageBuildInfo to a types.ImageBuildOptions GetContext() (io.Reader, error) // the path to the build context - GetDockerfile() string // the relative path to the Dockerfile, including the fileitself + GetDockerfile() string // the relative path to the Dockerfile, including the file itself GetRepo() string // get repo label for image GetTag() string // get tag label for image ShouldPrintBuildLog() bool // allow build log to be printed to stdout @@ -167,6 +167,15 @@ type ContainerRequest struct { LogConsumerCfg *LogConsumerConfig // define the configuration for the log producer and its log consumers to follow the logs } +// sessionID returns the session ID for the container request. +func (c *ContainerRequest) sessionID() string { + if sessionID := c.Labels[core.LabelSessionID]; sessionID != "" { + return sessionID + } + + return core.SessionID() +} + // containerOptions functional options for a container type containerOptions struct { ImageName string @@ -277,34 +286,34 @@ func (c *ContainerRequest) GetBuildArgs() map[string]*string { return c.FromDockerfile.BuildArgs } -// GetDockerfile returns the Dockerfile from the ContainerRequest, defaults to "Dockerfile" +// GetDockerfile returns the Dockerfile from the ContainerRequest, defaults to "Dockerfile". +// Sets FromDockerfile.Dockerfile to the default if blank. func (c *ContainerRequest) GetDockerfile() string { - f := c.FromDockerfile.Dockerfile - if f == "" { - return "Dockerfile" + if c.FromDockerfile.Dockerfile == "" { + c.FromDockerfile.Dockerfile = "Dockerfile" } - return f + return c.FromDockerfile.Dockerfile } -// GetRepo returns the Repo label for image from the ContainerRequest, defaults to UUID +// GetRepo returns the Repo label for image from the ContainerRequest, defaults to UUID. +// Sets FromDockerfile.Repo to the default value if blank. func (c *ContainerRequest) GetRepo() string { - r := c.FromDockerfile.Repo - if r == "" { - return uuid.NewString() + if c.FromDockerfile.Repo == "" { + c.FromDockerfile.Repo = uuid.NewString() } - return strings.ToLower(r) + return strings.ToLower(c.FromDockerfile.Repo) } -// GetTag returns the Tag label for image from the ContainerRequest, defaults to UUID +// GetTag returns the Tag label for image from the ContainerRequest, defaults to UUID. +// Sets FromDockerfile.Tag to the default value if blank. func (c *ContainerRequest) GetTag() string { - t := c.FromDockerfile.Tag - if t == "" { - return uuid.NewString() + if c.FromDockerfile.Tag == "" { + c.FromDockerfile.Tag = uuid.NewString() } - return strings.ToLower(t) + return strings.ToLower(c.FromDockerfile.Tag) } // Deprecated: Testcontainers will detect registry credentials automatically, and it will be removed in the next major release. @@ -460,7 +469,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. @@ -513,7 +529,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 { diff --git a/container_file_test.go b/container_file_test.go index 31273c9966..12d2240604 100644 --- a/container_file_test.go +++ b/container_file_test.go @@ -3,23 +3,22 @@ package testcontainers import ( - "errors" "os" "path/filepath" "testing" + + "github.com/stretchr/testify/require" ) func TestContainerFileValidation(t *testing.T) { type ContainerFileValidationTestCase struct { Name string - ExpectedError error + ExpectedError string File ContainerFile } f, err := os.Open(filepath.Join(".", "testdata", "hello.sh")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) testTable := []ContainerFileValidationTestCase{ { @@ -38,7 +37,7 @@ func TestContainerFileValidation(t *testing.T) { }, { Name: "invalid container file", - ExpectedError: errors.New("either HostFilePath or Reader must be specified"), + ExpectedError: "either HostFilePath or Reader must be specified", File: ContainerFile{ HostFilePath: "", Reader: nil, @@ -47,7 +46,7 @@ func TestContainerFileValidation(t *testing.T) { }, { Name: "invalid container file", - ExpectedError: errors.New("ContainerFilePath must be specified"), + ExpectedError: "ContainerFilePath must be specified", File: ContainerFile{ HostFilePath: "/path/to/host", ContainerFilePath: "", @@ -58,15 +57,10 @@ func TestContainerFileValidation(t *testing.T) { for _, testCase := range testTable { t.Run(testCase.Name, func(t *testing.T) { err := testCase.File.validate() - switch { - case err == nil && testCase.ExpectedError == nil: - return - case err == nil && testCase.ExpectedError != nil: - t.Errorf("did not receive expected error: %s", testCase.ExpectedError.Error()) - case err != nil && testCase.ExpectedError == nil: - t.Errorf("received unexpected error: %s", err.Error()) - case err.Error() != testCase.ExpectedError.Error(): - t.Errorf("errors mismatch: %s != %s", err.Error(), testCase.ExpectedError.Error()) + if testCase.ExpectedError != "" { + require.EqualError(t, err, testCase.ExpectedError) + } else { + require.NoError(t, err) } }) } diff --git a/container_ignore_test.go b/container_ignore_test.go index ca89db4d89..505b9edd6d 100644 --- a/container_ignore_test.go +++ b/container_ignore_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseDockerIgnore(t *testing.T) { @@ -37,7 +38,7 @@ func TestParseDockerIgnore(t *testing.T) { for _, testCase := range testCases { exists, excluded, err := parseDockerIgnore(testCase.filePath) assert.Equal(t, testCase.exists, exists) - assert.Equal(t, testCase.expectedErr, err) + require.ErrorIs(t, testCase.expectedErr, err) assert.Equal(t, testCase.expectedExcluded, excluded) } } diff --git a/container_test.go b/container_test.go index 3cb14ac296..c3581a379e 100644 --- a/container_test.go +++ b/container_test.go @@ -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" @@ -22,14 +23,14 @@ import ( func Test_ContainerValidation(t *testing.T) { type ContainerValidationTestCase struct { Name string - ExpectedError error + ExpectedError string ContainerRequest testcontainers.ContainerRequest } testTable := []ContainerValidationTestCase{ { Name: "cannot set both context and image", - ExpectedError: errors.New("you cannot specify both an Image and Context in a ContainerRequest"), + ExpectedError: "you cannot specify both an Image and Context in a ContainerRequest", ContainerRequest: testcontainers.ContainerRequest{ FromDockerfile: testcontainers.FromDockerfile{ Context: ".", @@ -38,15 +39,13 @@ func Test_ContainerValidation(t *testing.T) { }, }, { - Name: "can set image without context", - ExpectedError: nil, + Name: "can set image without context", ContainerRequest: testcontainers.ContainerRequest{ Image: "redis:latest", }, }, { - Name: "can set context without image", - ExpectedError: nil, + Name: "can set context without image", ContainerRequest: testcontainers.ContainerRequest{ FromDockerfile: testcontainers.FromDockerfile{ Context: ".", @@ -54,8 +53,7 @@ func Test_ContainerValidation(t *testing.T) { }, }, { - Name: "Can mount same source to multiple targets", - ExpectedError: nil, + Name: "Can mount same source to multiple targets", ContainerRequest: testcontainers.ContainerRequest{ Image: "redis:latest", HostConfigModifier: func(hc *container.HostConfig) { @@ -65,7 +63,7 @@ func Test_ContainerValidation(t *testing.T) { }, { Name: "Cannot mount multiple sources to same target", - ExpectedError: errors.New("duplicate mount target detected: /data"), + ExpectedError: "duplicate mount target detected: /data", ContainerRequest: testcontainers.ContainerRequest{ Image: "redis:latest", HostConfigModifier: func(hc *container.HostConfig) { @@ -75,7 +73,7 @@ func Test_ContainerValidation(t *testing.T) { }, { Name: "Invalid bind mount", - ExpectedError: errors.New("invalid bind mount: /data:/data:/data"), + ExpectedError: "invalid bind mount: /data:/data:/data", ContainerRequest: testcontainers.ContainerRequest{ Image: "redis:latest", HostConfigModifier: func(hc *container.HostConfig) { @@ -88,15 +86,10 @@ func Test_ContainerValidation(t *testing.T) { for _, testCase := range testTable { t.Run(testCase.Name, func(t *testing.T) { err := testCase.ContainerRequest.Validate() - switch { - case err == nil && testCase.ExpectedError == nil: - return - case err == nil && testCase.ExpectedError != nil: - t.Errorf("did not receive expected error: %s", testCase.ExpectedError.Error()) - case err != nil && testCase.ExpectedError == nil: - t.Errorf("received unexpected error: %s", err.Error()) - case err.Error() != testCase.ExpectedError.Error(): - t.Errorf("errors mismatch: %s != %s", err.Error(), testCase.ExpectedError.Error()) + if testCase.ExpectedError != "" { + require.EqualError(t, err, testCase.ExpectedError) + } else { + require.NoError(t, err) } }) } @@ -136,9 +129,7 @@ func Test_GetDockerfile(t *testing.T) { for _, testCase := range testTable { t.Run(testCase.name, func(t *testing.T) { n := testCase.ContainerRequest.GetDockerfile() - if n != testCase.ExpectedDockerfileName { - t.Fatalf("expected Dockerfile name: %s, received: %s", testCase.ExpectedDockerfileName, n) - } + require.Equalf(t, n, testCase.ExpectedDockerfileName, "expected Dockerfile name: %s, received: %s", testCase.ExpectedDockerfileName, n) }) } } @@ -166,7 +157,7 @@ func Test_BuildImageWithContexts(t *testing.T) { }{ { Name: "Dockerfile", - Contents: `FROM docker.io/alpine + Contents: `FROM alpine CMD ["echo", "this is from the archive"]`, }, } @@ -215,7 +206,7 @@ func Test_BuildImageWithContexts(t *testing.T) { }, { Name: "Dockerfile", - Contents: `FROM docker.io/alpine + Contents: `FROM alpine WORKDIR /app COPY . . CMD ["sh", "./say_hi.sh"]`, @@ -290,8 +281,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) @@ -303,11 +293,69 @@ 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() { require.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 { req := testcontainers.ContainerRequest{ - Image: "docker.io/alpine", + Image: "alpine", Cmd: []string{"echo", "-n", "I was not expecting this"}, WaitingFor: wait.ForLog("I was expecting this").WithStartupTimeout(5 * time.Second), } @@ -317,9 +365,8 @@ func Test_GetLogsFromFailedContainer(t *testing.T) { ContainerRequest: req, Started: true, }) - terminateContainerOnEnd(t, ctx, c) - require.Error(t, err) - require.Contains(t, err.Error(), "container exited with code 0") + testcontainers.CleanupContainer(t, c) + require.ErrorContains(t, err, "container exited with code 0") logs, logErr := c.Logs(ctx) require.NoError(t, logErr) @@ -335,11 +382,11 @@ func Test_GetLogsFromFailedContainer(t *testing.T) { type dockerImageSubstitutor struct{} func (s dockerImageSubstitutor) Description() string { - return "DockerImageSubstitutor (prepends docker.io)" + return "DockerImageSubstitutor (prepends registry.hub.docker.com)" } func (s dockerImageSubstitutor) Substitute(image string) (string, error) { - return "docker.io/" + image, nil + return "registry.hub.docker.com/library/" + image, nil } // } @@ -398,7 +445,7 @@ func TestImageSubstitutors(t *testing.T) { name: "Prepend namespace", image: "alpine", substitutors: []testcontainers.ImageSubstitutor{dockerImageSubstitutor{}}, - expectedImage: "docker.io/alpine", + expectedImage: "registry.hub.docker.com/library/alpine", }, { name: "Substitution with error", @@ -417,25 +464,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) }) } @@ -455,21 +498,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()) }) @@ -480,30 +519,30 @@ 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) - // Output: docker.io/alpine:latest + // Output: registry.hub.docker.com/library/alpine:latest } diff --git a/docker.go b/docker.go index 5f6c415627..296fe6743c 100644 --- a/docker.go +++ b/docker.go @@ -5,7 +5,6 @@ import ( "bufio" "context" "encoding/base64" - "encoding/binary" "encoding/json" "errors" "fmt" @@ -16,7 +15,6 @@ import ( "os" "path/filepath" "regexp" - "strings" "sync" "time" @@ -30,6 +28,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/stdcopy" "github.com/docker/go-connections/nat" "github.com/moby/term" specs "github.com/opencontainers/image-spec/specs-go/v1" @@ -48,11 +47,21 @@ const ( Podman = "podman" ReaperDefault = "reaper_default" // Default network name when bridge is not available packagePath = "github.com/testcontainers/testcontainers-go" - - logStoppedForOutOfSyncMessage = "Stopping log consumer: Headers out of sync" ) -var createContainerFailDueToNameConflictRegex = regexp.MustCompile("Conflict. The container name .* is already in use by container .*") +var ( + // createContainerFailDueToNameConflictRegex is a regular expression that matches the container is already in use error. + createContainerFailDueToNameConflictRegex = regexp.MustCompile("Conflict. The container name .* is already in use by container .*") + + // minLogProductionTimeout is the minimum log production timeout. + minLogProductionTimeout = time.Duration(5 * time.Second) + + // maxLogProductionTimeout is the maximum log production timeout. + maxLogProductionTimeout = time.Duration(60 * time.Second) + + // errLogProductionStop is the cause for stopping log production. + errLogProductionStop = errors.New("log production stopped") +) // DockerContainer represents a container started using Docker type DockerContainer struct { @@ -65,23 +74,19 @@ type DockerContainer struct { isRunning bool imageWasBuilt bool // keepBuiltImage makes Terminate not remove the image if imageWasBuilt. - keepBuiltImage bool - provider *DockerProvider - sessionID string - terminationSignal chan bool - consumers []LogConsumer - logProductionError chan error + keepBuiltImage bool + provider *DockerProvider + sessionID string + terminationSignal chan bool + consumers []LogConsumer // TODO: Remove locking and wait group once the deprecated StartLogProducer and // StopLogProducer have been removed and hence logging can only be started and // stopped once. - // logProductionWaitGroup is used to signal when the log production has stopped. - // This allows stopLogProduction to safely set logProductionStop to nil. - // See simplification in https://go.dev/play/p/x0pOElF2Vjf - logProductionWaitGroup sync.WaitGroup - - logProductionStop chan struct{} + // logProductionCancel is used to signal the log production to stop. + logProductionCancel context.CancelCauseFunc + logProductionCtx context.Context logProductionTimeout *time.Duration logger Logging @@ -147,7 +152,7 @@ func (c *DockerContainer) PortEndpoint(ctx context.Context, port nat.Port, proto protoFull := "" if proto != "" { - protoFull = fmt.Sprintf("%s://", proto) + protoFull = proto + "://" } return fmt.Sprintf("%s%s:%s", protoFull, host, outerPort.Port()), nil @@ -178,7 +183,7 @@ func (c *DockerContainer) Inspect(ctx context.Context) (*types.ContainerJSON, er func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Port, error) { inspect, err := c.Inspect(ctx) if err != nil { - return "", err + return "", fmt.Errorf("inspect: %w", err) } if inspect.ContainerJSONBase.HostConfig.NetworkMode == "host" { return port, nil @@ -199,7 +204,7 @@ func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Po return nat.NewPort(k.Proto(), p[0].HostPort) } - return "", errors.New("port not found") + return "", errdefs.NotFound(fmt.Errorf("port %q not found", port)) } // Deprecated: use c.Inspect(ctx).NetworkSettings.Ports instead. @@ -259,9 +264,13 @@ func (c *DockerContainer) Start(ctx context.Context) error { // // If the container is already stopped, the method is a no-op. 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 @@ -272,30 +281,48 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro } if err := c.provider.client.ContainerStop(ctx, c.ID, options); err != nil { - return err + return fmt.Errorf("container stop: %w", err) } + defer c.provider.Close() 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 + // Close reaper connection if it was attached. case c.terminationSignal <- true: default: } defer c.provider.client.Close() + // TODO: Handle errors from ContainerRemove more correctly, e.g. should we + // run the terminated hook? errs := []error{ c.terminatingHook(ctx), c.provider.client.ContainerRemove(ctx, c.GetContainerID(), container.RemoveOptions{ @@ -667,6 +694,29 @@ func (c *DockerContainer) copyToContainer(ctx context.Context, fileContent func( return nil } +// logConsumerWriter is a writer that writes to a LogConsumer. +type logConsumerWriter struct { + log Log + consumers []LogConsumer +} + +// newLogConsumerWriter creates a new logConsumerWriter for logType that sends messages to all consumers. +func newLogConsumerWriter(logType string, consumers []LogConsumer) *logConsumerWriter { + return &logConsumerWriter{ + log: Log{LogType: logType}, + consumers: consumers, + } +} + +// Write writes the p content to all consumers. +func (lw logConsumerWriter) Write(p []byte) (int, error) { + lw.log.Content = p + for _, consumer := range lw.consumers { + consumer.Accept(lw.log) + } + return len(p), nil +} + type LogProductionOption func(*DockerContainer) // WithLogProductionTimeout is a functional option that sets the timeout for the log production. @@ -684,124 +734,107 @@ func (c *DockerContainer) StartLogProducer(ctx context.Context, opts ...LogProdu // startLogProduction will start a concurrent process that will continuously read logs // from the container and will send them to each added LogConsumer. +// // Default log production timeout is 5s. It is used to set the context timeout -// which means that each log-reading loop will last at least the specified timeout -// and that it cannot be cancelled earlier. +// which means that each log-reading loop will last at up to the specified timeout. +// // Use functional option WithLogProductionTimeout() to override default timeout. If it's // lower than 5s and greater than 60s it will be set to 5s or 60s respectively. func (c *DockerContainer) startLogProduction(ctx context.Context, opts ...LogProductionOption) error { - c.logProductionStop = make(chan struct{}, 1) // buffered channel to avoid blocking - c.logProductionWaitGroup.Add(1) - for _, opt := range opts { opt(c) } - minLogProductionTimeout := time.Duration(5 * time.Second) - maxLogProductionTimeout := time.Duration(60 * time.Second) - - if c.logProductionTimeout == nil { + // Validate the log production timeout. + switch { + case c.logProductionTimeout == nil: c.logProductionTimeout = &minLogProductionTimeout - } - - if *c.logProductionTimeout < minLogProductionTimeout { + case *c.logProductionTimeout < minLogProductionTimeout: c.logProductionTimeout = &minLogProductionTimeout - } - - if *c.logProductionTimeout > maxLogProductionTimeout { + case *c.logProductionTimeout > maxLogProductionTimeout: c.logProductionTimeout = &maxLogProductionTimeout } - c.logProductionError = make(chan error, 1) + // Setup the log writers. + stdout := newLogConsumerWriter(StdoutLog, c.consumers) + stderr := newLogConsumerWriter(StderrLog, c.consumers) - go func() { - defer func() { - close(c.logProductionError) - c.logProductionWaitGroup.Done() - }() + // Setup the log production context which will be used to stop the log production. + c.logProductionCtx, c.logProductionCancel = context.WithCancelCause(ctx) - since := "" - // if the socket is closed we will make additional logs request with updated Since timestamp - BEGIN: - options := container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: true, - Since: since, - } + // We capture context cancel function to avoid data race with multiple + // calls to startLogProduction. + go func(cancel context.CancelCauseFunc) { + // Ensure the context is cancelled when log productions completes + // so that GetLogProductionErrorChannel functions correctly. + defer cancel(nil) - ctx, cancel := context.WithTimeout(ctx, *c.logProductionTimeout) - defer cancel() + c.logProducer(stdout, stderr) + }(c.logProductionCancel) - r, err := c.provider.client.ContainerLogs(ctx, c.GetContainerID(), options) - if err != nil { - c.logProductionError <- err - return - } - defer c.provider.Close() + return nil +} - for { - select { - case <-c.logProductionStop: - c.logProductionError <- r.Close() - return - default: - } - h := make([]byte, 8) - _, err := io.ReadFull(r, h) - if err != nil { - switch { - case err == io.EOF: - // No more logs coming - case errors.Is(err, net.ErrClosed): - now := time.Now() - since = fmt.Sprintf("%d.%09d", now.Unix(), int64(now.Nanosecond())) - goto BEGIN - case errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled): - // Probably safe to continue here - continue - default: - _, _ = fmt.Fprintf(os.Stderr, "container log error: %+v. %s", err, logStoppedForOutOfSyncMessage) - // if we would continue here, the next header-read will result into random data... - } - return - } +// logProducer read logs from the container and writes them to stdout, stderr until either: +// - logProductionCtx is done +// - A fatal error occurs +// - No more logs are available +func (c *DockerContainer) logProducer(stdout, stderr io.Writer) { + // Clean up idle client connections. + defer c.provider.Close() - count := binary.BigEndian.Uint32(h[4:]) - if count == 0 { - continue - } - logType := h[0] - if logType > 2 { - _, _ = fmt.Fprintf(os.Stderr, "received invalid log type: %d", logType) - // sometimes docker returns logType = 3 which is an undocumented log type, so treat it as stdout - logType = 1 - } + // Setup the log options, start from the beginning. + options := &container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + } - // a map of the log type --> int representation in the header, notice the first is blank, this is stdin, but the go docker client doesn't allow following that in logs - logTypes := []string{"", StdoutLog, StderrLog} + // Use a separate method so that timeout cancel function is + // called correctly. + for c.copyLogsTimeout(stdout, stderr, options) { + } +} - b := make([]byte, count) - _, err = io.ReadFull(r, b) - if err != nil { - // TODO: add-logger: use logger to log out this error - _, _ = fmt.Fprintf(os.Stderr, "error occurred reading log with known length %s", err.Error()) - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - // Probably safe to continue here - continue - } - // we can not continue here as the next read most likely will not be the next header - _, _ = fmt.Fprintln(os.Stderr, logStoppedForOutOfSyncMessage) - return - } - for _, c := range c.consumers { - c.Accept(Log{ - LogType: logTypes[logType], - Content: b, - }) - } - } - }() +// copyLogsTimeout copies logs from the container to stdout and stderr with a timeout. +// It returns true if the log production should be retried, false otherwise. +func (c *DockerContainer) copyLogsTimeout(stdout, stderr io.Writer, options *container.LogsOptions) bool { + timeoutCtx, cancel := context.WithTimeout(c.logProductionCtx, *c.logProductionTimeout) + defer cancel() + + err := c.copyLogs(timeoutCtx, stdout, stderr, *options) + switch { + case err == nil: + // No more logs available. + return false + case c.logProductionCtx.Err() != nil: + // Log production was stopped or caller context is done. + return false + case timeoutCtx.Err() != nil, errors.Is(err, net.ErrClosed): + // Timeout or client connection closed, retry. + default: + // Unexpected error, retry. + Logger.Printf("Unexpected error reading logs: %v", err) + } + + // Retry from the last log received. + now := time.Now() + options.Since = fmt.Sprintf("%d.%09d", now.Unix(), int64(now.Nanosecond())) + + return true +} + +// copyLogs copies logs from the container to stdout and stderr. +func (c *DockerContainer) copyLogs(ctx context.Context, stdout, stderr io.Writer, options container.LogsOptions) error { + rc, err := c.provider.client.ContainerLogs(ctx, c.GetContainerID(), options) + if err != nil { + return fmt.Errorf("container logs: %w", err) + } + defer rc.Close() + + if _, err = stdcopy.StdCopy(stdout, stderr, rc); err != nil { + return fmt.Errorf("stdcopy: %w", err) + } return nil } @@ -814,18 +847,25 @@ func (c *DockerContainer) StopLogProducer() error { // stopLogProduction will stop the concurrent process that is reading logs // and sending them to each added LogConsumer func (c *DockerContainer) stopLogProduction() error { - // signal the log production to stop - c.logProductionStop <- struct{}{} + if c.logProductionCancel == nil { + return nil + } - c.logProductionWaitGroup.Wait() + // Signal the log production to stop. + c.logProductionCancel(errLogProductionStop) - if err := <-c.logProductionError; err != nil { - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - // Returning context errors is not useful for the consumer. + if err := context.Cause(c.logProductionCtx); err != nil { + switch { + case errors.Is(err, errLogProductionStop): + // Log production was stopped. + return nil + case errors.Is(err, context.DeadlineExceeded), + errors.Is(err, context.Canceled): + // Parent context is done. return nil + default: + return err } - - return err } return nil @@ -834,7 +874,44 @@ func (c *DockerContainer) stopLogProduction() error { // GetLogProductionErrorChannel exposes the only way for the consumer // to be able to listen to errors and react to them. func (c *DockerContainer) GetLogProductionErrorChannel() <-chan error { - return c.logProductionError + if c.logProductionCtx == nil { + return nil + } + + errCh := make(chan error, 1) + go func(ctx context.Context) { + <-ctx.Done() + errCh <- context.Cause(ctx) + close(errCh) + }(c.logProductionCtx) + + return errCh +} + +// connectReaper connects the reaper to the container if it is needed. +func (c *DockerContainer) connectReaper(ctx context.Context) error { + if c.provider.config.RyukDisabled || isReaperImage(c.Image) { + // Reaper is disabled or we are the reaper container. + return nil + } + + reaper, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, c.provider.host), core.SessionID(), c.provider) + if err != nil { + return fmt.Errorf("reaper: %w", err) + } + + if c.terminationSignal, err = reaper.Connect(); err != nil { + return fmt.Errorf("reaper connect: %w", err) + } + + return nil +} + +// cleanupTermSignal triggers the termination signal if it was created and an error occurred. +func (c *DockerContainer) cleanupTermSignal(err error) { + if c.terminationSignal != nil && err != nil { + c.terminationSignal <- true + } } // DockerNetwork represents a network started using Docker @@ -870,6 +947,7 @@ type DockerProvider struct { host string hostCache string config config.Config + mtx sync.Mutex } // Client gets the docker client used by the provider @@ -944,35 +1022,30 @@ func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (st } // CreateContainer fulfils a request for a container without starting it -func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerRequest) (Container, error) { - var err error - +func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerRequest) (con Container, err error) { // defer the close of the Docker client connection the soonest defer p.Close() - // Make sure that bridge network exists - // In case it is disabled we will create reaper_default network - if p.DefaultNetwork == "" { - p.DefaultNetwork, err = p.getDefaultNetwork(ctx, p.client) - if err != nil { - return nil, err - } + var defaultNetwork string + defaultNetwork, err = p.ensureDefaultNetwork(ctx) + if err != nil { + return nil, fmt.Errorf("ensure default network: %w", err) } // If default network is not bridge make sure it is attached to the request // as container won't be attached to it automatically // in case of Podman the bridge network is called 'podman' as 'bridge' would conflict - if p.DefaultNetwork != p.defaultBridgeNetworkName { + if defaultNetwork != p.defaultBridgeNetworkName { isAttached := false for _, net := range req.Networks { - if net == p.DefaultNetwork { + if net == defaultNetwork { isAttached = true break } } if !isAttached { - req.Networks = append(req.Networks, p.DefaultNetwork) + req.Networks = append(req.Networks, defaultNetwork) } } @@ -987,27 +1060,6 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque req.Labels = make(map[string]string) } - var termSignal chan bool - // the reaper does not need to start a reaper for itself - isReaperContainer := strings.HasSuffix(imageName, config.ReaperDefaultImage) - if !p.config.RyukDisabled && !isReaperContainer { - r, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), core.SessionID(), p) - if err != nil { - return nil, fmt.Errorf("%w: creating reaper failed", err) - } - termSignal, err = r.Connect() - if err != nil { - return nil, fmt.Errorf("%w: connecting to reaper failed", err) - } - } - - // Cleanup on error, otherwise set termSignal to nil before successful return. - defer func() { - if termSignal != nil { - termSignal <- true - } - }() - if err = req.Validate(); err != nil { return nil, err } @@ -1017,11 +1069,29 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque var platform *specs.Platform + defaultHooks := []ContainerLifecycleHooks{ + DefaultLoggingHook(p.Logger), + } + + origLifecycleHooks := req.LifecycleHooks + req.LifecycleHooks = []ContainerLifecycleHooks{ + combineContainerHooks(defaultHooks, req.LifecycleHooks), + } + if req.ShouldBuildImage() { + if err = req.buildingHook(ctx); err != nil { + return nil, err + } + imageName, err = p.BuildImage(ctx, &req) if err != nil { return nil, err } + + req.Image = imageName + if err = req.builtHook(ctx); err != nil { + return nil, err + } } else { for _, is := range req.ImageSubstitutors { modifiedTag, err := is.Substitute(imageName) @@ -1071,11 +1141,10 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque } } - if !isReaperContainer { - // add the labels that the reaper will use to terminate the container to the request - for k, v := range core.DefaultLabels(core.SessionID()) { - req.Labels[k] = v - } + if !isReaperImage(imageName) { + // Add the labels that identify this as a testcontainers container and + // allow the reaper to terminate it if requested. + AddGenericLabels(req.Labels) } dockerInput := &container.Config{ @@ -1098,13 +1167,12 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque networkingConfig := &network.NetworkingConfig{} // default hooks include logger hook and pre-create hook - defaultHooks := []ContainerLifecycleHooks{ - DefaultLoggingHook(p.Logger), + defaultHooks = append(defaultHooks, defaultPreCreateHook(p, dockerInput, hostConfig, networkingConfig), defaultCopyFileToContainerHook(req.Files), defaultLogConsumersHook(req.LogConsumerCfg), defaultReadinessHook(), - } + ) // in the case the container needs to access a local port // we need to forward the local port to the container @@ -1117,10 +1185,25 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque return nil, fmt.Errorf("expose host ports: %w", err) } + defer func() { + if err != nil && con == nil { + // Container setup failed so ensure we clean up the sshd container too. + ctr := &DockerContainer{ + provider: p, + logger: p.Logger, + lifecycleHooks: []ContainerLifecycleHooks{sshdForwardPortsHook}, + } + err = errors.Join(ctr.terminatingHook(ctx)) + } + }() + defaultHooks = append(defaultHooks, sshdForwardPortsHook) } - req.LifecycleHooks = []ContainerLifecycleHooks{combineContainerHooks(defaultHooks, req.LifecycleHooks)} + // Combine with the original LifecycleHooks to avoid duplicate logging hooks. + req.LifecycleHooks = []ContainerLifecycleHooks{ + combineContainerHooks(defaultHooks, origLifecycleHooks), + } err = req.creatingHook(ctx) if err != nil { @@ -1150,29 +1233,35 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque } } - c := &DockerContainer{ - ID: resp.ID, - WaitingFor: req.WaitingFor, - Image: imageName, - imageWasBuilt: req.ShouldBuildImage(), - keepBuiltImage: req.ShouldKeepBuiltImage(), - sessionID: core.SessionID(), - exposedPorts: req.ExposedPorts, - provider: p, - terminationSignal: termSignal, - logger: p.Logger, - lifecycleHooks: req.LifecycleHooks, + // This should match the fields set in ContainerFromDockerResponse. + ctr := &DockerContainer{ + ID: resp.ID, + WaitingFor: req.WaitingFor, + Image: imageName, + imageWasBuilt: req.ShouldBuildImage(), + keepBuiltImage: req.ShouldKeepBuiltImage(), + sessionID: req.sessionID(), + exposedPorts: req.ExposedPorts, + provider: p, + logger: p.Logger, + lifecycleHooks: req.LifecycleHooks, } - err = c.createdHook(ctx) - if err != nil { - return nil, err + if err = ctr.connectReaper(ctx); err != nil { + return ctr, err // No wrap as it would stutter. } - // Disable cleanup on success - termSignal = nil + // Wrapped so the returned error is passed to the cleanup function. + defer func(ctr *DockerContainer) { + ctr.cleanupTermSignal(err) + }(ctr) - return c, nil + if err = ctr.createdHook(ctx); err != nil { + // Return the container to allow caller to clean up. + return ctr, fmt.Errorf("created hook: %w", err) + } + + return ctr, nil } func (p *DockerProvider) findContainerByName(ctx context.Context, name string) (*types.Container, error) { @@ -1184,7 +1273,7 @@ func (p *DockerProvider) findContainerByName(ctx context.Context, name string) ( filter := filters.NewArgs(filters.Arg("name", fmt.Sprintf("^%s$", name))) containers, err := p.client.ContainerList(ctx, container.ListOptions{Filters: filter}) if err != nil { - return nil, err + return nil, fmt.Errorf("container list: %w", err) } defer p.Close() @@ -1220,7 +1309,7 @@ func (p *DockerProvider) waitContainerCreation(ctx context.Context, name string) ) } -func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req ContainerRequest) (Container, error) { +func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req ContainerRequest) (con Container, err error) { c, err := p.findContainerByName(ctx, req.Name) if err != nil { return nil, err @@ -1239,18 +1328,26 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain } } - sessionID := core.SessionID() + sessionID := req.sessionID() var termSignal chan bool if !p.config.RyukDisabled { - r, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p) + r, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p) if err != nil { return nil, fmt.Errorf("reaper: %w", err) } - termSignal, err = r.Connect() + + termSignal, err := r.Connect() if err != nil { - return nil, fmt.Errorf("%w: connecting to reaper failed", err) + return nil, fmt.Errorf("reaper connect: %w", err) } + + // Cleanup on error. + defer func() { + if err != nil { + termSignal <- true + } + }() } // default hooks include logger hook and pre-create hook @@ -1372,10 +1469,13 @@ func (p *DockerProvider) Config() TestcontainersConfig { // Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel // You can use the "TESTCONTAINERS_HOST_OVERRIDE" env variable to set this yourself func (p *DockerProvider) DaemonHost(ctx context.Context) (string, error) { - return daemonHost(ctx, p) + p.mtx.Lock() + defer p.mtx.Unlock() + + return p.daemonHostLocked(ctx) } -func daemonHost(ctx context.Context, p *DockerProvider) (string, error) { +func (p *DockerProvider) daemonHostLocked(ctx context.Context) (string, error) { if p.hostCache != "" { return p.hostCache, nil } @@ -1418,18 +1518,12 @@ func daemonHost(ctx context.Context, p *DockerProvider) (string, error) { // Deprecated: use network.New instead // CreateNetwork returns the object representing a new network identified by its name -func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) (Network, error) { - var err error - +func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) (net Network, err error) { // defer the close of the Docker client connection the soonest defer p.Close() - // Make sure that bridge network exists - // In case it is disabled we will create reaper_default network - if p.DefaultNetwork == "" { - if p.DefaultNetwork, err = p.getDefaultNetwork(ctx, p.client); err != nil { - return nil, err - } + if _, err = p.ensureDefaultNetwork(ctx); err != nil { + return nil, fmt.Errorf("ensure default network: %w", err) } if req.Labels == nil { @@ -1445,35 +1539,34 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) IPAM: req.IPAM, } - sessionID := core.SessionID() + sessionID := req.sessionID() var termSignal chan bool if !p.config.RyukDisabled { - r, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p) + r, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p) if err != nil { - return nil, fmt.Errorf("%w: creating network reaper failed", err) + return nil, fmt.Errorf("reaper: %w", err) } - termSignal, err = r.Connect() + + termSignal, err := r.Connect() if err != nil { - return nil, fmt.Errorf("%w: connecting to network reaper failed", err) + return nil, fmt.Errorf("reaper connect: %w", err) } - } - // add the labels that the reaper will use to terminate the network to the request - for k, v := range core.DefaultLabels(sessionID) { - req.Labels[k] = v + // Cleanup on error. + defer func() { + if err != nil { + termSignal <- true + } + }() } - // Cleanup on error, otherwise set termSignal to nil before successful return. - defer func() { - if termSignal != nil { - termSignal <- true - } - }() + // add the labels that the reaper will use to terminate the network to the request + core.AddDefaultLabels(sessionID, req.Labels) response, err := p.client.NetworkCreate(ctx, req.Name, nc) if err != nil { - return &DockerNetwork{}, err + return &DockerNetwork{}, fmt.Errorf("create network: %w", err) } n := &DockerNetwork{ @@ -1484,9 +1577,6 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) provider: p, } - // Disable cleanup on success - termSignal = nil - return n, nil } @@ -1504,14 +1594,12 @@ func (p *DockerProvider) GetNetwork(ctx context.Context, req NetworkRequest) (ne func (p *DockerProvider) GetGatewayIP(ctx context.Context) (string, error) { // Use a default network as defined in the DockerProvider - if p.DefaultNetwork == "" { - var err error - p.DefaultNetwork, err = p.getDefaultNetwork(ctx, p.client) - if err != nil { - return "", err - } + defaultNetwork, err := p.ensureDefaultNetwork(ctx) + if err != nil { + return "", fmt.Errorf("ensure default network: %w", err) } - nw, err := p.GetNetwork(ctx, NetworkRequest{Name: p.DefaultNetwork}) + + nw, err := p.GetNetwork(ctx, NetworkRequest{Name: defaultNetwork}) if err != nil { return "", err } @@ -1530,73 +1618,94 @@ func (p *DockerProvider) GetGatewayIP(ctx context.Context) (string, error) { return ip, nil } -func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APIClient) (string, error) { - // Get list of available networks - networkResources, err := cli.NetworkList(ctx, network.ListOptions{}) - if err != nil { - return "", err - } +// ensureDefaultNetwork ensures that defaultNetwork is set and creates +// it if it does not exist, returning its value. +// It is safe to call this method concurrently. +func (p *DockerProvider) ensureDefaultNetwork(ctx context.Context) (string, error) { + p.mtx.Lock() + defer p.mtx.Unlock() - reaperNetwork := ReaperDefault + if p.defaultNetwork != "" { + // Already set. + return p.defaultNetwork, nil + } - reaperNetworkExists := false + networkResources, err := p.client.NetworkList(ctx, network.ListOptions{}) + if err != nil { + return "", fmt.Errorf("network list: %w", err) + } + // TODO: remove once we have docker context support via #2810 + // Prefer the default bridge network if it exists. + // This makes the results stable as network list order is not guaranteed. for _, net := range networkResources { - if net.Name == p.defaultBridgeNetworkName { - return p.defaultBridgeNetworkName, nil + switch net.Name { + case p.defaultBridgeNetworkName: + p.defaultNetwork = p.defaultBridgeNetworkName + return p.defaultNetwork, nil + case ReaperDefault: + p.defaultNetwork = ReaperDefault } + } - if net.Name == reaperNetwork { - reaperNetworkExists = true - } + if p.defaultNetwork != "" { + return p.defaultNetwork, nil } - // Create a bridge network for the container communications - if !reaperNetworkExists { - _, err = cli.NetworkCreate(ctx, reaperNetwork, network.CreateOptions{ - Driver: Bridge, - Attachable: true, - Labels: core.DefaultLabels(core.SessionID()), - }) - if err != nil { - return "", err - } + // Create a bridge network for the container communications. + _, err = p.client.NetworkCreate(ctx, ReaperDefault, network.CreateOptions{ + Driver: Bridge, + Attachable: true, + Labels: GenericLabels(), + }) + // If the network already exists, we can ignore the error as that can + // happen if we are running multiple tests in parallel and we only + // need to ensure that the network exists. + if err != nil && !errdefs.IsConflict(err) { + return "", fmt.Errorf("network create: %w", err) } - return reaperNetwork, nil + p.defaultNetwork = ReaperDefault + + return p.defaultNetwork, nil } -// containerFromDockerResponse builds a Docker container struct from the response of the Docker API -func containerFromDockerResponse(ctx context.Context, response types.Container) (*DockerContainer, error) { - provider, err := NewDockerProvider() - if err != nil { - return nil, err +// ContainerFromType builds a Docker container struct from the response of the Docker API +func (p *DockerProvider) ContainerFromType(ctx context.Context, response types.Container) (ctr *DockerContainer, err error) { + exposedPorts := make([]string, len(response.Ports)) + for i, port := range response.Ports { + exposedPorts[i] = fmt.Sprintf("%d/%s", port.PublicPort, port.Type) } - ctr := DockerContainer{} - - ctr.ID = response.ID - ctr.WaitingFor = nil - ctr.Image = response.Image - ctr.imageWasBuilt = false - - ctr.logger = provider.Logger - ctr.lifecycleHooks = []ContainerLifecycleHooks{ - DefaultLoggingHook(ctr.logger), + // This should match the fields set in CreateContainer. + ctr = &DockerContainer{ + ID: response.ID, + Image: response.Image, + imageWasBuilt: false, + sessionID: response.Labels[core.LabelSessionID], + isRunning: response.State == "running", + exposedPorts: exposedPorts, + provider: p, + logger: p.Logger, + lifecycleHooks: []ContainerLifecycleHooks{ + DefaultLoggingHook(p.Logger), + }, } - ctr.provider = provider - ctr.sessionID = core.SessionID() - ctr.consumers = []LogConsumer{} - ctr.isRunning = response.State == "running" + if err = ctr.connectReaper(ctx); err != nil { + return nil, err + } - // the termination signal should be obtained from the reaper - ctr.terminationSignal = nil + // Wrapped so the returned error is passed to the cleanup function. + defer func(ctr *DockerContainer) { + ctr.cleanupTermSignal(err) + }(ctr) // populate the raw representation of the container jsonRaw, err := ctr.inspectRawContainer(ctx) if err != nil { - return nil, err + // Return the container to allow caller to clean up. + return ctr, fmt.Errorf("inspect raw container: %w", err) } // the health status of the container, if any @@ -1604,7 +1713,7 @@ func containerFromDockerResponse(ctx context.Context, response types.Container) ctr.healthStatus = health.Status } - return &ctr, nil + return ctr, nil } // ListImages list images from the provider. If an image has multiple Tags, each tag is reported diff --git a/docker_auth.go b/docker_auth.go index 99e2d2fdba..58b3ef2637 100644 --- a/docker_auth.go +++ b/docker_auth.go @@ -8,7 +8,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "net/url" "os" "sync" @@ -22,6 +21,9 @@ import ( // defaultRegistryFn is variable overwritten in tests to check for behaviour with different default values. var defaultRegistryFn = defaultRegistry +// getRegistryCredentials is a variable overwritten in tests to mock the dockercfg.GetRegistryCredentials function. +var getRegistryCredentials = dockercfg.GetRegistryCredentials + // DockerImageAuth returns the auth config for the given Docker image, extracting first its Docker registry. // Finally, it will use the credential helpers to extract the information from the docker config file // for that registry, if it exists. @@ -112,9 +114,28 @@ type credentials struct { var creds = &credentialsCache{entries: map[string]credentials{}} -// Get returns the username and password for the given hostname +// AuthConfig updates the details in authConfig for the given hostname +// as determined by the details in configKey. +func (c *credentialsCache) AuthConfig(hostname, configKey string, authConfig *registry.AuthConfig) error { + u, p, err := creds.get(hostname, configKey) + if err != nil { + return err + } + + if u != "" { + authConfig.Username = u + authConfig.Password = p + } else { + authConfig.IdentityToken = p + } + + return nil +} + +// get returns the username and password for the given hostname // as determined by the details in configPath. -func (c *credentialsCache) Get(hostname, configKey string) (string, string, error) { +// If the username is empty, the password is an identity token. +func (c *credentialsCache) get(hostname, configKey string) (string, string, error) { key := configKey + ":" + hostname c.mtx.RLock() entry, ok := c.entries[key] @@ -125,7 +146,7 @@ func (c *credentialsCache) Get(hostname, configKey string) (string, string, erro } // No entry found, request and cache. - user, password, err := dockercfg.GetRegistryCredentials(hostname) + user, password, err := getRegistryCredentials(hostname) if err != nil { return "", "", fmt.Errorf("getting credentials for %s: %w", hostname, err) } @@ -137,24 +158,12 @@ func (c *credentialsCache) Get(hostname, configKey string) (string, string, erro return user, password, nil } -// configFileKey returns a key to use for caching credentials based on +// configKey returns a key to use for caching credentials based on // the contents of the currently active config. -func configFileKey() (string, error) { - configPath, err := dockercfg.ConfigPath() - if err != nil { - return "", err - } - - f, err := os.Open(configPath) - if err != nil { - return "", fmt.Errorf("open config file: %w", err) - } - - defer f.Close() - +func configKey(cfg *dockercfg.Config) (string, error) { h := md5.New() - if _, err := io.Copy(h, f); err != nil { - return "", fmt.Errorf("copying config file: %w", err) + if err := json.NewEncoder(h).Encode(cfg); err != nil { + return "", fmt.Errorf("encode config: %w", err) } return hex.EncodeToString(h.Sum(nil)), nil @@ -165,10 +174,14 @@ func configFileKey() (string, error) { func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { cfg, err := getDockerConfig() if err != nil { + if errors.Is(err, os.ErrNotExist) { + return map[string]registry.AuthConfig{}, nil + } + return nil, err } - configKey, err := configFileKey() + key, err := configKey(cfg) if err != nil { return nil, err } @@ -195,14 +208,10 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { switch { case ac.Username == "" && ac.Password == "": // Look up credentials from the credential store. - u, p, err := creds.Get(k, configKey) - if err != nil { + if err := creds.AuthConfig(k, key, &ac); err != nil { results <- authConfigResult{err: err} return } - - ac.Username = u - ac.Password = p case ac.Auth == "": // Create auth from the username and password encoding. ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password)) @@ -212,25 +221,19 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { }(k, v) } - // in the case where the auth field in the .docker/conf.json is empty, and the user has credential helpers registered - // the auth comes from there + // In the case where the auth field in the .docker/conf.json is empty, and the user has + // credential helpers registered the auth comes from there. for k := range cfg.CredentialHelpers { go func(k string) { defer wg.Done() - u, p, err := creds.Get(k, configKey) - if err != nil { + var ac registry.AuthConfig + if err := creds.AuthConfig(k, key, &ac); err != nil { results <- authConfigResult{err: err} return } - results <- authConfigResult{ - key: k, - cfg: registry.AuthConfig{ - Username: u, - Password: p, - }, - } + results <- authConfigResult{key: k, cfg: ac} }(k) } @@ -260,20 +263,20 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { // 1. the DOCKER_AUTH_CONFIG environment variable, unmarshalling it into a dockercfg.Config // 2. the DOCKER_CONFIG environment variable, as the path to the config file // 3. else it will load the default config file, which is ~/.docker/config.json -func getDockerConfig() (dockercfg.Config, error) { - dockerAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG") - if dockerAuthConfig != "" { - cfg := dockercfg.Config{} - err := json.Unmarshal([]byte(dockerAuthConfig), &cfg) - if err == nil { - return cfg, nil +func getDockerConfig() (*dockercfg.Config, error) { + if env := os.Getenv("DOCKER_AUTH_CONFIG"); env != "" { + var cfg dockercfg.Config + if err := json.Unmarshal([]byte(env), &cfg); err != nil { + return nil, fmt.Errorf("unmarshal DOCKER_AUTH_CONFIG: %w", err) } + + return &cfg, nil } cfg, err := dockercfg.LoadDefaultConfig() if err != nil { - return cfg, err + return nil, fmt.Errorf("load default config: %w", err) } - return cfg, nil + return &cfg, nil } diff --git a/docker_auth_test.go b/docker_auth_test.go index 4e55d2b9bf..5d397d53c8 100644 --- a/docker_auth_test.go +++ b/docker_auth_test.go @@ -14,100 +14,99 @@ import ( "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/client" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go/internal/core" "github.com/testcontainers/testcontainers-go/wait" ) -const exampleAuth = "https://example-auth.com" - -var testDockerConfigDirPath = filepath.Join("testdata", ".docker") - -var indexDockerIO = core.IndexDockerIO - -func TestGetDockerConfig(t *testing.T) { - const expectedErrorMessage = "Expected to find %s in auth configs" - - // Verify that the default docker config file exists before any test in this suite runs. - // Then, we can safely run the tests that rely on it. - defaultCfg, err := dockercfg.LoadDefaultConfig() - require.NoError(t, err) - require.NotEmpty(t, defaultCfg) +const ( + exampleAuth = "https://example-auth.com" + privateRegistry = "https://my.private.registry" + exampleRegistry = "https://example.com" +) - t.Run("without DOCKER_CONFIG env var retrieves default", func(t *testing.T) { - t.Setenv("DOCKER_CONFIG", "") +func Test_getDockerConfig(t *testing.T) { + expectedConfig := &dockercfg.Config{ + AuthConfigs: map[string]dockercfg.AuthConfig{ + core.IndexDockerIO: {}, + exampleRegistry: {}, + privateRegistry: {}, + }, + CredentialsStore: "desktop", + } + t.Run("HOME/valid", func(t *testing.T) { + testDockerConfigHome(t, "testdata") cfg, err := getDockerConfig() require.NoError(t, err) - require.NotEmpty(t, cfg) + require.Equal(t, expectedConfig, cfg) + }) + + t.Run("HOME/not-found", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-found") - assert.Equal(t, defaultCfg, cfg) + cfg, err := getDockerConfig() + require.ErrorIs(t, err, os.ErrNotExist) + require.Nil(t, cfg) }) - t.Run("with DOCKER_CONFIG env var pointing to a non-existing file raises error", func(t *testing.T) { - t.Setenv("DOCKER_CONFIG", filepath.Join(testDockerConfigDirPath, "non-existing")) + t.Run("HOME/invalid-config", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "invalid-config") cfg, err := getDockerConfig() - require.Error(t, err) - require.Empty(t, cfg) + require.ErrorContains(t, err, "json: cannot unmarshal array") + require.Nil(t, cfg) }) - t.Run("with DOCKER_CONFIG env var", func(t *testing.T) { - t.Setenv("DOCKER_CONFIG", testDockerConfigDirPath) + t.Run("DOCKER_AUTH_CONFIG/valid", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-found") + t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig) cfg, err := getDockerConfig() require.NoError(t, err) - require.NotEmpty(t, cfg) - - assert.Len(t, cfg.AuthConfigs, 3) + require.Equal(t, expectedConfig, cfg) + }) - authCfgs := cfg.AuthConfigs + t.Run("DOCKER_AUTH_CONFIG/invalid-config", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-found") + t.Setenv("DOCKER_AUTH_CONFIG", `{"auths": []}`) - if _, ok := authCfgs[indexDockerIO]; !ok { - t.Errorf(expectedErrorMessage, indexDockerIO) - } - if _, ok := authCfgs["https://example.com"]; !ok { - t.Errorf(expectedErrorMessage, "https://example.com") - } - if _, ok := authCfgs["https://my.private.registry"]; !ok { - t.Errorf(expectedErrorMessage, "https://my.private.registry") - } + cfg, err := getDockerConfig() + require.ErrorContains(t, err, "json: cannot unmarshal array") + require.Nil(t, cfg) }) - t.Run("DOCKER_AUTH_CONFIG env var takes precedence", func(t *testing.T) { - setAuthConfig(t, exampleAuth, "", "") - t.Setenv("DOCKER_CONFIG", testDockerConfigDirPath) + t.Run("DOCKER_CONFIG/valid", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-found") + t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", ".docker")) cfg, err := getDockerConfig() require.NoError(t, err) - require.NotEmpty(t, cfg) - - assert.Len(t, cfg.AuthConfigs, 1) + require.Equal(t, expectedConfig, cfg) + }) - authCfgs := cfg.AuthConfigs + t.Run("DOCKER_CONFIG/invalid-config", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-found") + t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", "invalid-config", ".docker")) - if _, ok := authCfgs[indexDockerIO]; ok { - t.Errorf("Not expected to find %s in auth configs", indexDockerIO) - } - if _, ok := authCfgs[exampleAuth]; !ok { - t.Errorf(expectedErrorMessage, exampleAuth) - } + cfg, err := getDockerConfig() + require.ErrorContains(t, err, "json: cannot unmarshal array") + require.Nil(t, cfg) }) +} +func TestDockerImageAuth(t *testing.T) { t.Run("retrieve auth with DOCKER_AUTH_CONFIG env var", func(t *testing.T) { username, password := "gopher", "secret" creds := setAuthConfig(t, exampleAuth, username, password) registry, cfg, err := DockerImageAuth(context.Background(), exampleAuth+"/my/image:latest") require.NoError(t, err) - require.NotEmpty(t, cfg) - - assert.Equal(t, exampleAuth, registry) - assert.Equal(t, username, cfg.Username) - assert.Equal(t, password, cfg.Password) - assert.Equal(t, creds, cfg.Auth) + require.Equal(t, exampleAuth, registry) + require.Equal(t, username, cfg.Username) + require.Equal(t, password, cfg.Password) + require.Equal(t, creds, cfg.Auth) }) t.Run("match registry authentication by host", func(t *testing.T) { @@ -117,12 +116,10 @@ func TestGetDockerConfig(t *testing.T) { registry, cfg, err := DockerImageAuth(context.Background(), imageReg+imagePath) require.NoError(t, err) - require.NotEmpty(t, cfg) - - assert.Equal(t, imageReg, registry) - assert.Equal(t, "gopher", cfg.Username) - assert.Equal(t, "secret", cfg.Password) - assert.Equal(t, base64, cfg.Auth) + require.Equal(t, imageReg, registry) + require.Equal(t, "gopher", cfg.Username) + require.Equal(t, "secret", cfg.Password) + require.Equal(t, base64, cfg.Auth) }) t.Run("fail to match registry authentication due to invalid host", func(t *testing.T) { @@ -135,8 +132,7 @@ func TestGetDockerConfig(t *testing.T) { registry, cfg, err := DockerImageAuth(context.Background(), imageReg+imagePath) require.ErrorIs(t, err, dockercfg.ErrCredentialsNotFound) require.Empty(t, cfg) - - assert.Equal(t, imageReg, registry) + require.Equal(t, imageReg, registry) }) t.Run("fail to match registry authentication by host with empty URL scheme creds and missing default", func(t *testing.T) { @@ -156,8 +152,7 @@ func TestGetDockerConfig(t *testing.T) { registry, cfg, err := DockerImageAuth(context.Background(), imageReg+imagePath) require.ErrorIs(t, err, dockercfg.ErrCredentialsNotFound) require.Empty(t, cfg) - - assert.Equal(t, imageReg, registry) + require.Equal(t, imageReg, registry) }) } @@ -173,12 +168,13 @@ func TestBuildContainerFromDockerfile(t *testing.T) { } redisC, err := prepareRedisImage(ctx, req) + CleanupContainer(t, redisC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, redisC) } // removeImageFromLocalCache removes the image from the local cache func removeImageFromLocalCache(t *testing.T, img string) { + t.Helper() ctx := context.Background() testcontainersClient, err := NewDockerClientWithOpts(ctx, client.WithVersion(daemonMaxVersion)) @@ -211,8 +207,7 @@ func TestBuildContainerFromDockerfileWithDockerAuthConfig(t *testing.T) { BuildArgs: map[string]*string{ "REGISTRY_HOST": ®istryHost, }, - Repo: "localhost", - PrintBuildLog: true, + Repo: "localhost", }, AlwaysPullImage: true, // make sure the authentication takes place ExposedPorts: []string{"6379/tcp"}, @@ -220,7 +215,7 @@ func TestBuildContainerFromDockerfileWithDockerAuthConfig(t *testing.T) { } redisC, err := prepareRedisImage(ctx, req) - terminateContainerOnEnd(t, ctx, redisC) + CleanupContainer(t, redisC) require.NoError(t, err) } @@ -246,7 +241,7 @@ func TestBuildContainerFromDockerfileShouldFailWithWrongDockerAuthConfig(t *test } redisC, err := prepareRedisImage(ctx, req) - terminateContainerOnEnd(t, ctx, redisC) + CleanupContainer(t, redisC) require.Error(t, err) } @@ -268,11 +263,12 @@ func TestCreateContainerFromPrivateRegistry(t *testing.T) { ContainerRequest: req, Started: true, }) - terminateContainerOnEnd(t, ctx, redisContainer) + CleanupContainer(t, redisContainer) require.NoError(t, err) } func prepareLocalRegistryWithAuth(t *testing.T) string { + t.Helper() ctx := context.Background() wd, err := os.Getwd() require.NoError(t, err) @@ -288,15 +284,15 @@ func prepareLocalRegistryWithAuth(t *testing.T) string { }, Files: []ContainerFile{ { - HostFilePath: fmt.Sprintf("%s/testdata/auth", wd), + HostFilePath: wd + "/testdata/auth", ContainerFilePath: "/auth", }, { - HostFilePath: fmt.Sprintf("%s/testdata/data", wd), + HostFilePath: wd + "/testdata/data", ContainerFilePath: "/data", }, }, - WaitingFor: wait.ForExposedPort(), + WaitingFor: wait.ForHTTP("/").WithPort("5000/tcp"), } // } @@ -307,6 +303,7 @@ func prepareLocalRegistryWithAuth(t *testing.T) string { } registryC, err := GenericContainer(ctx, genContainerReq) + CleanupContainer(t, registryC) require.NoError(t, err) mappedPort, err := registryC.MappedPort(ctx, "5000/tcp") @@ -319,12 +316,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) return addr } @@ -374,6 +365,7 @@ func setAuthConfig(t *testing.T, host, username, password string) string { // which can be used to connect to the local registry. // This avoids the issues with localhost on WSL. func localAddress(t *testing.T) string { + t.Helper() if os.Getenv("WSL_DISTRO_NAME") == "" { return "localhost" } @@ -390,28 +382,137 @@ func localAddress(t *testing.T) string { //go:embed testdata/.docker/config.json var dockerConfig string +// reset resets the credentials cache. +func (c *credentialsCache) reset() { + c.mtx.Lock() + defer c.mtx.Unlock() + c.entries = make(map[string]credentials) +} + func Test_getDockerAuthConfigs(t *testing.T) { - t.Run("file", func(t *testing.T) { - got, err := getDockerAuthConfigs() + t.Run("HOME/valid", func(t *testing.T) { + testDockerConfigHome(t, "testdata") + + requireValidAuthConfig(t) + }) + + t.Run("HOME/not-found", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-exist") + + authConfigs, err := getDockerAuthConfigs() require.NoError(t, err) - require.NotNil(t, got) + require.NotNil(t, authConfigs) + require.Empty(t, authConfigs) }) - t.Run("env", func(t *testing.T) { + t.Run("HOME/invalid-config", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "invalid-config") + + authConfigs, err := getDockerAuthConfigs() + require.ErrorContains(t, err, "json: cannot unmarshal array") + require.Nil(t, authConfigs) + }) + + t.Run("DOCKER_AUTH_CONFIG/valid", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-exist") t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig) - got, err := getDockerAuthConfigs() - require.NoError(t, err) + requireValidAuthConfig(t) + }) - // We can only check the keys as the values are not deterministic. - expected := map[string]registry.AuthConfig{ - "https://index.docker.io/v1/": {}, - "https://example.com": {}, - "https://my.private.registry": {}, - } - for k := range got { - got[k] = registry.AuthConfig{} + t.Run("DOCKER_AUTH_CONFIG/invalid-config", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-exist") + t.Setenv("DOCKER_AUTH_CONFIG", `{"auths": []}`) + + authConfigs, err := getDockerAuthConfigs() + require.ErrorContains(t, err, "json: cannot unmarshal array") + require.Nil(t, authConfigs) + }) + + t.Run("DOCKER_AUTH_CONFIG/identity-token", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-exist") + + // Reset the credentials cache to ensure our mocked method is called. + creds.reset() + + // Mock getRegistryCredentials to return identity-token for index.docker.io. + old := getRegistryCredentials + t.Cleanup(func() { + getRegistryCredentials = old + creds.reset() // Ensure our mocked results aren't cached. + }) + getRegistryCredentials = func(hostname string) (string, string, error) { + switch hostname { + case core.IndexDockerIO: + return "", "identity-token", nil + default: + return "username", "password", nil + } } - require.Equal(t, expected, got) + t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig) + + authConfigs, err := getDockerAuthConfigs() + require.NoError(t, err) + require.Equal(t, map[string]registry.AuthConfig{ + core.IndexDockerIO: { + IdentityToken: "identity-token", + }, + privateRegistry: { + Username: "username", + Password: "password", + }, + exampleRegistry: { + Username: "username", + Password: "password", + }, + }, authConfigs) }) + + t.Run("DOCKER_CONFIG/valid", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-found") + t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", ".docker")) + + requireValidAuthConfig(t) + }) + + t.Run("DOCKER_CONFIG/invalid-config", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-found") + t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", "invalid-config", ".docker")) + + cfg, err := getDockerConfig() + require.ErrorContains(t, err, "json: cannot unmarshal array") + require.Nil(t, cfg) + }) +} + +// requireValidAuthConfig checks that the given authConfigs map contains the expected keys. +func requireValidAuthConfig(t *testing.T) { + t.Helper() + + authConfigs, err := getDockerAuthConfigs() + require.NoError(t, err) + + // We can only check the keys as the values are not deterministic as they depend + // on users environment. + expected := map[string]registry.AuthConfig{ + core.IndexDockerIO: {}, + exampleRegistry: {}, + privateRegistry: {}, + } + for k := range authConfigs { + authConfigs[k] = registry.AuthConfig{} + } + require.Equal(t, expected, authConfigs) +} + +// testDockerConfigHome sets the user's home directory to the given path +// and unsets the DOCKER_CONFIG and DOCKER_AUTH_CONFIG environment variables. +func testDockerConfigHome(t *testing.T, dirs ...string) { + t.Helper() + + dir := filepath.Join(dirs...) + t.Setenv("DOCKER_AUTH_CONFIG", "") + t.Setenv("DOCKER_CONFIG", "") + t.Setenv("HOME", dir) + t.Setenv("USERPROFILE", dir) // Windows } diff --git a/docker_exec_test.go b/docker_exec_test.go index 11f187c226..65f9e71e07 100644 --- a/docker_exec_test.go +++ b/docker_exec_test.go @@ -51,19 +51,18 @@ func TestExecWithOptions(t *testing.T) { Image: nginxAlpineImage, } - container, err := GenericContainer(ctx, GenericContainerRequest{ + ctr, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, Started: true, }) - + CleanupContainer(t, ctr) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, container) // always append the multiplexed option for having the output // in a readable format tt.opts = append(tt.opts, tcexec.Multiplexed()) - code, reader, err := container.Exec(ctx, tt.cmds, tt.opts...) + code, reader, err := ctr.Exec(ctx, tt.cmds, tt.opts...) require.NoError(t, err) require.Zero(t, code) require.NotNil(t, reader) @@ -84,15 +83,14 @@ func TestExecWithMultiplexedResponse(t *testing.T) { Image: nginxAlpineImage, } - container, err := GenericContainer(ctx, GenericContainerRequest{ + ctr, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, Started: true, }) - + CleanupContainer(t, ctr) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, container) - code, reader, err := container.Exec(ctx, []string{"sh", "-c", "echo stdout; echo stderr >&2"}, tcexec.Multiplexed()) + code, reader, err := ctr.Exec(ctx, []string{"sh", "-c", "echo stdout; echo stderr >&2"}, tcexec.Multiplexed()) require.NoError(t, err) require.Zero(t, code) require.NotNil(t, reader) @@ -112,15 +110,14 @@ func TestExecWithNonMultiplexedResponse(t *testing.T) { Image: nginxAlpineImage, } - container, err := GenericContainer(ctx, GenericContainerRequest{ + ctr, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, Started: true, }) - + CleanupContainer(t, ctr) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, container) - code, reader, err := container.Exec(ctx, []string{"sh", "-c", "echo stdout; echo stderr >&2"}) + code, reader, err := ctr.Exec(ctx, []string{"sh", "-c", "echo stdout; echo stderr >&2"}) require.NoError(t, err) require.Zero(t, code) require.NotNil(t, reader) diff --git a/docker_files_test.go b/docker_files_test.go index 6fcfc92a0b..6b32168081 100644 --- a/docker_files_test.go +++ b/docker_files_test.go @@ -13,24 +13,22 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) +const testBashImage string = "bash:5.2.26" + func TestCopyFileToContainer(t *testing.T) { ctx, cnl := context.WithTimeout(context.Background(), 30*time.Second) defer cnl() // copyFileOnCreate { absPath, err := filepath.Abs(filepath.Join(".", "testdata", "hello.sh")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) r, err := os.Open(absPath) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ - Image: "docker.io/bash", + Image: testBashImage, Files: []testcontainers.ContainerFile{ { Reader: r, @@ -45,9 +43,8 @@ func TestCopyFileToContainer(t *testing.T) { Started: true, }) // } - + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - require.NoError(t, container.Terminate(ctx)) } func TestCopyFileToRunningContainer(t *testing.T) { @@ -57,17 +54,13 @@ func TestCopyFileToRunningContainer(t *testing.T) { // Not using the assertations here to avoid leaking the library into the example // copyFileAfterCreate { waitForPath, err := filepath.Abs(filepath.Join(".", "testdata", "waitForHello.sh")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) helloPath, err := filepath.Abs(filepath.Join(".", "testdata", "hello.sh")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ - Image: "docker.io/bash:5.2.26", + Image: testBashImage, Files: []testcontainers.ContainerFile{ { HostFilePath: waitForPath, @@ -79,20 +72,17 @@ func TestCopyFileToRunningContainer(t *testing.T) { }, Started: true, }) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - err = container.CopyFileToContainer(ctx, helloPath, "/scripts/hello.sh", 0o700) + err = ctr.CopyFileToContainer(ctx, helloPath, "/scripts/hello.sh", 0o700) // } require.NoError(t, err) // Give some time to the wait script to catch the hello script being created - err = wait.ForLog("done").WithStartupTimeout(2*time.Second).WaitUntilReady(ctx, container) + err = wait.ForLog("done").WithStartupTimeout(2*time.Second).WaitUntilReady(ctx, ctr) require.NoError(t, err) - - require.NoError(t, container.Terminate(ctx)) } func TestCopyDirectoryToContainer(t *testing.T) { @@ -102,13 +92,11 @@ func TestCopyDirectoryToContainer(t *testing.T) { // Not using the assertations here to avoid leaking the library into the example // copyDirectoryToContainer { dataDirectory, err := filepath.Abs(filepath.Join(".", "testdata")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ - Image: "docker.io/bash", + Image: testBashImage, Files: []testcontainers.ContainerFile{ { HostFilePath: dataDirectory, @@ -125,9 +113,8 @@ func TestCopyDirectoryToContainer(t *testing.T) { Started: true, }) // } - + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - require.NoError(t, container.Terminate(ctx)) } func TestCopyDirectoryToRunningContainerAsFile(t *testing.T) { @@ -136,17 +123,13 @@ func TestCopyDirectoryToRunningContainerAsFile(t *testing.T) { // copyDirectoryToRunningContainerAsFile { dataDirectory, err := filepath.Abs(filepath.Join(".", "testdata")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) waitForPath, err := filepath.Abs(filepath.Join(dataDirectory, "waitForHello.sh")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ - Image: "docker.io/bash", + Image: testBashImage, Files: []testcontainers.ContainerFile{ { HostFilePath: waitForPath, @@ -158,25 +141,17 @@ func TestCopyDirectoryToRunningContainerAsFile(t *testing.T) { }, Started: true, }) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // as the container is started, we can create the directory first - _, _, err = container.Exec(ctx, []string{"mkdir", "-p", "/scripts"}) - if err != nil { - t.Fatal(err) - } + _, _, err = ctr.Exec(ctx, []string{"mkdir", "-p", "/scripts"}) + require.NoError(t, err) // because the container path is a directory, it will use the copy dir method as fallback - err = container.CopyFileToContainer(ctx, dataDirectory, "/scripts", 0o700) - if err != nil { - t.Fatal(err) - } - // } - + err = ctr.CopyFileToContainer(ctx, dataDirectory, "/scripts", 0o700) require.NoError(t, err) - require.NoError(t, container.Terminate(ctx)) + // } } func TestCopyDirectoryToRunningContainerAsDir(t *testing.T) { @@ -186,17 +161,13 @@ func TestCopyDirectoryToRunningContainerAsDir(t *testing.T) { // Not using the assertations here to avoid leaking the library into the example // copyDirectoryToRunningContainerAsDir { waitForPath, err := filepath.Abs(filepath.Join(".", "testdata", "waitForHello.sh")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) dataDirectory, err := filepath.Abs(filepath.Join(".", "testdata")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ - Image: "docker.io/bash", + Image: testBashImage, Files: []testcontainers.ContainerFile{ { HostFilePath: waitForPath, @@ -208,22 +179,14 @@ func TestCopyDirectoryToRunningContainerAsDir(t *testing.T) { }, Started: true, }) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // as the container is started, we can create the directory first - _, _, err = container.Exec(ctx, []string{"mkdir", "-p", "/scripts"}) - if err != nil { - t.Fatal(err) - } - - err = container.CopyDirToContainer(ctx, dataDirectory, "/scripts", 0o700) - if err != nil { - t.Fatal(err) - } - // } + _, _, err = ctr.Exec(ctx, []string{"mkdir", "-p", "/scripts"}) + require.NoError(t, err) + err = ctr.CopyDirToContainer(ctx, dataDirectory, "/scripts", 0o700) require.NoError(t, err) - require.NoError(t, container.Terminate(ctx)) + // } } diff --git a/docker_mounts.go b/docker_mounts.go index aed3010361..d8af3fae3e 100644 --- a/docker_mounts.go +++ b/docker_mounts.go @@ -126,9 +126,7 @@ func mapToDockerMounts(containerMounts ContainerMounts) []mount.Mount { Labels: make(map[string]string), } } - for k, v := range GenericLabels() { - containerMount.VolumeOptions.Labels[k] = v - } + AddGenericLabels(containerMount.VolumeOptions.Labels) } mounts = append(mounts, containerMount) diff --git a/docker_test.go b/docker_test.go index 402d944dce..3fa686632f 100644 --- a/docker_test.go +++ b/docker_test.go @@ -29,10 +29,10 @@ import ( ) const ( - mysqlImage = "docker.io/mysql:8.0.36" - nginxDelayedImage = "docker.io/menedev/delayed-nginx:1.15.2" - nginxImage = "docker.io/nginx" - nginxAlpineImage = "docker.io/nginx:alpine" + mysqlImage = "mysql:8.0.36" + nginxDelayedImage = "menedev/delayed-nginx:1.15.2" + nginxImage = "nginx" + nginxAlpineImage = "nginx:alpine" nginxDefaultPort = "80/tcp" nginxHighPort = "8080/tcp" daemonMaxVersion = "1.41" @@ -55,9 +55,7 @@ func TestContainerWithHostNetworkOptions(t *testing.T) { SkipIfDockerDesktop(t, ctx) absPath, err := filepath.Abs(filepath.Join("testdata", "nginx-highport.conf")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) gcr := GenericContainerRequest{ ProviderType: providerType, @@ -82,23 +80,14 @@ func TestContainerWithHostNetworkOptions(t *testing.T) { } nginxC, err := GenericContainer(ctx, gcr) + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) - // host, err := nginxC.Host(ctx) - // if err != nil { - // t.Errorf("Expected host %s. Got '%d'.", host, err) - // } - // endpoint, err := nginxC.PortEndpoint(ctx, nginxHighPort, "http") - if err != nil { - t.Errorf("Expected server endpoint. Got '%v'.", err) - } + require.NoErrorf(t, err, "Expected server endpoint") _, err = http.Get(endpoint) - if err != nil { - t.Errorf("Expected OK response. Got '%d'.", err) - } + require.NoErrorf(t, err, "Expected OK response") } func TestContainerWithHostNetworkOptions_UseExposePortsFromImageConfigs(t *testing.T) { @@ -113,26 +102,17 @@ func TestContainerWithHostNetworkOptions_UseExposePortsFromImageConfigs(t *testi } nginxC, err := GenericContainer(ctx, gcr) - if err != nil { - t.Fatal(err) - } - - terminateContainerOnEnd(t, ctx, nginxC) + CleanupContainer(t, nginxC) + require.NoError(t, err) endpoint, err := nginxC.Endpoint(ctx, "http") - if err != nil { - t.Errorf("Expected server endpoint. Got '%v'.", err) - } + require.NoErrorf(t, err, "Expected server endpoint") resp, err := http.Get(endpoint) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) - } + require.Equalf(t, http.StatusOK, resp.StatusCode, "Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) } func TestContainerWithNetworkModeAndNetworkTogether(t *testing.T) { @@ -158,11 +138,11 @@ func TestContainerWithNetworkModeAndNetworkTogether(t *testing.T) { } nginx, err := GenericContainer(ctx, gcr) + CleanupContainer(t, nginx) if err != nil { // Error when NetworkMode = host and Network = []string{"bridge"} t.Logf("Can't use Network and NetworkMode together, %s\n", err) } - terminateContainerOnEnd(t, ctx, nginx) } func TestContainerWithHostNetwork(t *testing.T) { @@ -174,9 +154,7 @@ func TestContainerWithHostNetwork(t *testing.T) { SkipIfDockerDesktop(t, ctx) absPath, err := filepath.Abs(filepath.Join("testdata", "nginx-highport.conf")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) gcr := GenericContainerRequest{ ProviderType: providerType, @@ -197,30 +175,21 @@ func TestContainerWithHostNetwork(t *testing.T) { } nginxC, err := GenericContainer(ctx, gcr) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) portEndpoint, err := nginxC.PortEndpoint(ctx, nginxHighPort, "http") - if err != nil { - t.Errorf("Expected port endpoint %s. Got '%d'.", portEndpoint, err) - } + require.NoErrorf(t, err, "Expected port endpoint %s", portEndpoint) t.Log(portEndpoint) _, err = http.Get(portEndpoint) - if err != nil { - t.Errorf("Expected OK response. Got '%v'.", err) - } + require.NoErrorf(t, err, "Expected OK response") host, err := nginxC.Host(ctx) - if err != nil { - t.Errorf("Expected host %s. Got '%d'.", host, err) - } + require.NoErrorf(t, err, "Expected host %s", host) _, err = http.Get("http://" + host + ":8080") - if err != nil { - t.Errorf("Expected OK response. Got '%v'.", err) - } + assert.NoErrorf(t, err, "Expected OK response") } func TestContainerReturnItsContainerID(t *testing.T) { @@ -234,13 +203,19 @@ func TestContainerReturnItsContainerID(t *testing.T) { }, }, }) - + CleanupContainer(t, nginxA) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxA) - if nginxA.GetContainerID() == "" { - t.Errorf("expected a containerID but we got an empty string.") - } + assert.NotEmptyf(t, nginxA.GetContainerID(), "expected a containerID but we got an empty string.") +} + +// testLogConsumer is a simple implementation of LogConsumer that logs to the test output. +type testLogConsumer struct { + t *testing.T +} + +func (l *testLogConsumer) Accept(log Log) { + l.t.Log(log.LogType + ": " + strings.TrimSpace(string(log.Content))) } func TestContainerTerminationResetsState(t *testing.T) { @@ -253,24 +228,22 @@ func TestContainerTerminationResetsState(t *testing.T) { ExposedPorts: []string{ nginxDefaultPort, }, + LogConsumerCfg: &LogConsumerConfig{ + Consumers: []LogConsumer{&testLogConsumer{t: t}}, + }, }, Started: true, }) - if err != nil { - t.Fatal(err) - } + CleanupContainer(t, nginxA) + require.NoError(t, err) err = nginxA.Terminate(ctx) - if err != nil { - t.Fatal(err) - } - if nginxA.SessionID() != "" { - t.Fatal("Internal state must be reset.") - } + require.NoError(t, err) + require.Empty(t, nginxA.SessionID()) + inspect, err := nginxA.Inspect(ctx) - if err == nil || inspect != nil { - t.Fatal("expected error from container inspect.") - } + require.Error(t, err) + require.Nil(t, inspect) } func TestContainerStateAfterTermination(t *testing.T) { @@ -282,6 +255,9 @@ func TestContainerStateAfterTermination(t *testing.T) { ExposedPorts: []string{ nginxDefaultPort, }, + LogConsumerCfg: &LogConsumerConfig{ + Consumers: []LogConsumer{&testLogConsumer{t: t}}, + }, }, Started: true, }) @@ -290,44 +266,36 @@ func TestContainerStateAfterTermination(t *testing.T) { t.Run("Nil State after termination", func(t *testing.T) { ctx := context.Background() nginx, err := createContainerFn(ctx) - if err != nil { - t.Fatal(err) - } + CleanupContainer(t, nginx) + require.NoError(t, err) // terminate the container before the raw state is set err = nginx.Terminate(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) state, err := nginx.State(ctx) require.Error(t, err, "expected error from container inspect.") - assert.Nil(t, state, "expected nil container inspect.") + require.Nil(t, state, "expected nil container inspect.") }) t.Run("Nil State after termination if raw as already set", func(t *testing.T) { ctx := context.Background() nginx, err := createContainerFn(ctx) - if err != nil { - t.Fatal(err) - } + CleanupContainer(t, nginx) + require.NoError(t, err) state, err := nginx.State(ctx) require.NoError(t, err, "unexpected error from container inspect before container termination.") - - assert.NotNil(t, state, "unexpected nil container inspect before container termination.") + require.NotNil(t, state, "unexpected nil container inspect before container termination.") // terminate the container before the raw state is set err = nginx.Terminate(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) state, err = nginx.State(ctx) require.Error(t, err, "expected error from container inspect after container termination.") - - assert.Nil(t, state, "unexpected nil container inspect after container termination.") + require.Nil(t, state, "unexpected nil container inspect after container termination.") }) } @@ -335,9 +303,7 @@ func TestContainerTerminationRemovesDockerImage(t *testing.T) { t.Run("if not built from Dockerfile", func(t *testing.T) { ctx := context.Background() dockerClient, err := NewDockerClientWithOpts(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer dockerClient.Close() ctr, err := GenericContainer(ctx, GenericContainerRequest{ @@ -350,25 +316,20 @@ func TestContainerTerminationRemovesDockerImage(t *testing.T) { }, Started: true, }) - if err != nil { - t.Fatal(err) - } + CleanupContainer(t, ctr) + require.NoError(t, err) + err = ctr.Terminate(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + _, _, err = dockerClient.ImageInspectWithRaw(ctx, nginxAlpineImage) - if err != nil { - t.Fatal("nginx image should not have been removed") - } + require.NoErrorf(t, err, "nginx image should not have been removed") }) t.Run("if built from Dockerfile", func(t *testing.T) { ctx := context.Background() dockerClient, err := NewDockerClientWithOpts(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer dockerClient.Close() req := ContainerRequest{ @@ -383,25 +344,18 @@ func TestContainerTerminationRemovesDockerImage(t *testing.T) { ContainerRequest: req, Started: true, }) - if err != nil { - t.Fatal(err) - } + CleanupContainer(t, ctr) + require.NoError(t, err) containerID := ctr.GetContainerID() resp, err := dockerClient.ContainerInspect(ctx, containerID) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) imageID := resp.Config.Image err = ctr.Terminate(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, _, err = dockerClient.ImageInspectWithRaw(ctx, imageID) - if err == nil { - t.Fatal("custom built image should have been removed", err) - } + require.Errorf(t, err, "custom built image should have been removed") }) } @@ -418,9 +372,8 @@ func TestTwoContainersExposingTheSamePort(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxA) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxA) nginxB, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, @@ -433,37 +386,26 @@ func TestTwoContainersExposingTheSamePort(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxB) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxB) endpointA, err := nginxA.PortEndpoint(ctx, nginxDefaultPort, "http") require.NoError(t, err) resp, err := http.Get(endpointA) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) - } + require.Equalf(t, http.StatusOK, resp.StatusCode, "Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) endpointB, err := nginxB.PortEndpoint(ctx, nginxDefaultPort, "http") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) resp, err = http.Get(endpointB) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) - } + require.Equalf(t, http.StatusOK, resp.StatusCode, "Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) } func TestContainerCreation(t *testing.T) { @@ -480,41 +422,25 @@ func TestContainerCreation(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) endpoint, err := nginxC.PortEndpoint(ctx, nginxDefaultPort, "http") require.NoError(t, err) resp, err := http.Get(endpoint) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) - } + require.Equalf(t, http.StatusOK, resp.StatusCode, "Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) networkIP, err := nginxC.ContainerIP(ctx) - if err != nil { - t.Fatal(err) - } - if len(networkIP) == 0 { - t.Errorf("Expected an IP address, got %v", networkIP) - } + require.NoError(t, err) + require.NotEmptyf(t, networkIP, "Expected an IP address, got %v", networkIP) networkAliases, err := nginxC.NetworkAliases(ctx) - if err != nil { - t.Fatal(err) - } - if len(networkAliases) != 1 { - fmt.Printf("%v", networkAliases) - t.Errorf("Expected number of connected networks %d. Got %d.", 0, len(networkAliases)) - } - - if len(networkAliases["bridge"]) != 0 { - t.Errorf("Expected number of aliases for 'bridge' network %d. Got %d.", 0, len(networkAliases["bridge"])) - } + require.NoError(t, err) + require.Lenf(t, networkAliases, 1, "Expected number of connected networks %d. Got %d.", 0, len(networkAliases)) + require.Contains(t, networkAliases, "bridge") + assert.Emptyf(t, networkAliases["bridge"], "Expected number of aliases for 'bridge' network %d. Got %d.", 0, len(networkAliases["bridge"])) } func TestContainerCreationWithName(t *testing.T) { @@ -536,50 +462,33 @@ func TestContainerCreationWithName(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) inspect, err := nginxC.Inspect(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) name := inspect.Name - if name != expectedName { - t.Errorf("Expected container name '%s'. Got '%s'.", expectedName, name) - } + assert.Equalf(t, expectedName, name, "Expected container name '%s'. Got '%s'.", expectedName, name) networks, err := nginxC.Networks(ctx) - if err != nil { - t.Fatal(err) - } - if len(networks) != 1 { - t.Errorf("Expected networks 1. Got '%d'.", len(networks)) - } + require.NoError(t, err) + require.Lenf(t, networks, 1, "Expected networks 1. Got '%d'.", len(networks)) network := networks[0] switch providerType { case ProviderDocker: - if network != Bridge { - t.Errorf("Expected network name '%s'. Got '%s'.", Bridge, network) - } + assert.Equalf(t, Bridge, network, "Expected network name '%s'. Got '%s'.", Bridge, network) case ProviderPodman: - if network != Podman { - t.Errorf("Expected network name '%s'. Got '%s'.", Podman, network) - } + assert.Equalf(t, Podman, network, "Expected network name '%s'. Got '%s'.", Podman, network) } endpoint, err := nginxC.PortEndpoint(ctx, nginxDefaultPort, "http") require.NoError(t, err) resp, err := http.Get(endpoint) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) - } + require.Equalf(t, http.StatusOK, resp.StatusCode, "Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) } func TestContainerCreationAndWaitForListeningPortLongEnough(t *testing.T) { @@ -597,23 +506,16 @@ func TestContainerCreationAndWaitForListeningPortLongEnough(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) origin, err := nginxC.PortEndpoint(ctx, nginxDefaultPort, "http") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) resp, err := http.Get(origin) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) - } + require.Equalf(t, http.StatusOK, resp.StatusCode, "Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) } func TestContainerCreationTimesOut(t *testing.T) { @@ -630,12 +532,9 @@ func TestContainerCreationTimesOut(t *testing.T) { }, Started: true, }) + CleanupContainer(t, nginxC) - terminateContainerOnEnd(t, ctx, nginxC) - - if err == nil { - t.Error("Expected timeout") - } + assert.Errorf(t, err, "Expected timeout") } func TestContainerRespondsWithHttp200ForIndex(t *testing.T) { @@ -652,23 +551,16 @@ func TestContainerRespondsWithHttp200ForIndex(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) origin, err := nginxC.PortEndpoint(ctx, nginxDefaultPort, "http") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) resp, err := http.Get(origin) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) - } + require.Equalf(t, http.StatusOK, resp.StatusCode, "Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) } func TestContainerCreationTimesOutWithHttp(t *testing.T) { @@ -681,15 +573,12 @@ func TestContainerCreationTimesOutWithHttp(t *testing.T) { ExposedPorts: []string{ nginxDefaultPort, }, - WaitingFor: wait.ForHTTP("/").WithStartupTimeout(1 * time.Second), + WaitingFor: wait.ForHTTP("/").WithStartupTimeout(time.Millisecond * 500), }, Started: true, }) - terminateContainerOnEnd(t, ctx, nginxC) - - if err == nil { - t.Error("Expected timeout") - } + CleanupContainer(t, nginxC) + require.Error(t, err, "expected timeout") } func TestContainerCreationWaitsForLogContextTimeout(t *testing.T) { @@ -708,11 +597,8 @@ func TestContainerCreationWaitsForLogContextTimeout(t *testing.T) { ContainerRequest: req, Started: true, }) - if err == nil { - t.Error("Expected timeout") - } - - terminateContainerOnEnd(t, ctx, c) + CleanupContainer(t, c) + assert.Errorf(t, err, "Expected timeout") } func TestContainerCreationWaitsForLog(t *testing.T) { @@ -731,9 +617,8 @@ func TestContainerCreationWaitsForLog(t *testing.T) { ContainerRequest: req, Started: true, }) - + CleanupContainer(t, mysqlC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, mysqlC) } func Test_BuildContainerFromDockerfileWithBuildArgs(t *testing.T) { @@ -761,9 +646,8 @@ func Test_BuildContainerFromDockerfileWithBuildArgs(t *testing.T) { } c, err := GenericContainer(ctx, genContainerReq) - + CleanupContainer(t, c) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) ep, err := c.Endpoint(ctx, "http") require.NoError(t, err) @@ -779,13 +663,16 @@ func Test_BuildContainerFromDockerfileWithBuildArgs(t *testing.T) { } func Test_BuildContainerFromDockerfileWithBuildLog(t *testing.T) { - rescueStdout := os.Stderr - r, w, _ := os.Pipe() + r, w, err := os.Pipe() + require.NoError(t, err) + + oldStderr := os.Stderr os.Stderr = w + t.Cleanup(func() { + os.Stderr = oldStderr + }) - t.Log("getting ctx") ctx := context.Background() - t.Log("got ctx, creating container request") // fromDockerfile { req := ContainerRequest{ @@ -804,18 +691,18 @@ func Test_BuildContainerFromDockerfileWithBuildLog(t *testing.T) { } c, err := GenericContainer(ctx, genContainerReq) + CleanupContainer(t, c) + require.NoError(t, err) + err = w.Close() require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) - _ = w.Close() - out, _ := io.ReadAll(r) - os.Stdout = rescueStdout - temp := strings.Split(string(out), "\n") + out, err := io.ReadAll(r) + require.NoError(t, err) - if !regexp.MustCompile(`^Step\s*1/\d+\s*:\s*FROM docker.io/alpine$`).MatchString(temp[0]) { - t.Errorf("Expected stdout firstline to be %s. Got '%s'.", "Step 1/* : FROM docker.io/alpine", temp[0]) - } + temp := strings.Split(string(out), "\n") + require.NotEmpty(t, temp) + assert.Regexpf(t, `^Step\s*1/\d+\s*:\s*FROM alpine$`, temp[0], "Expected stdout first line to be %s. Got '%s'.", "Step 1/* : FROM alpine", temp[0]) } func TestContainerCreationWaitsForLogAndPortContextTimeout(t *testing.T) { @@ -837,11 +724,8 @@ func TestContainerCreationWaitsForLogAndPortContextTimeout(t *testing.T) { ContainerRequest: req, Started: true, }) - if err == nil { - t.Fatal("Expected timeout") - } - - terminateContainerOnEnd(t, ctx, c) + CleanupContainer(t, c) + require.Errorf(t, err, "Expected timeout") } func TestContainerCreationWaitingForHostPort(t *testing.T) { @@ -858,9 +742,8 @@ func TestContainerCreationWaitingForHostPort(t *testing.T) { ContainerRequest: req, Started: true, }) - + CleanupContainer(t, nginx) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginx) } func TestContainerCreationWaitingForHostPortWithoutBashThrowsAnError(t *testing.T) { @@ -875,9 +758,8 @@ func TestContainerCreationWaitingForHostPortWithoutBashThrowsAnError(t *testing. ContainerRequest: req, Started: true, }) - + CleanupContainer(t, nginx) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginx) } func TestCMD(t *testing.T) { @@ -890,7 +772,7 @@ func TestCMD(t *testing.T) { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/alpine", + Image: "alpine", WaitingFor: wait.ForAll( wait.ForLog("command override!"), ), @@ -902,9 +784,8 @@ func TestCMD(t *testing.T) { ContainerRequest: req, Started: true, }) - + CleanupContainer(t, c) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) } func TestEntrypoint(t *testing.T) { @@ -917,7 +798,7 @@ func TestEntrypoint(t *testing.T) { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/alpine", + Image: "alpine", WaitingFor: wait.ForAll( wait.ForLog("entrypoint override!"), ), @@ -929,9 +810,8 @@ func TestEntrypoint(t *testing.T) { ContainerRequest: req, Started: true, }) - + CleanupContainer(t, c) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) } func TestWorkingDir(t *testing.T) { @@ -944,7 +824,7 @@ func TestWorkingDir(t *testing.T) { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/alpine", + Image: "alpine", WaitingFor: wait.ForAll( wait.ForLog("/var/tmp/test"), ), @@ -957,31 +837,35 @@ func TestWorkingDir(t *testing.T) { ContainerRequest: req, Started: true, }) - + CleanupContainer(t, c) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) } func ExampleDockerProvider_CreateContainer() { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/nginx:alpine", + Image: "nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, Started: true, }) defer func() { - if err := nginxC.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := TerminateContainer(nginxC); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to create container: %s", err) + return + } state, err := nginxC.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -993,55 +877,74 @@ func ExampleDockerProvider_CreateContainer() { func ExampleContainer_Host() { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/nginx:alpine", + Image: "nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, Started: true, }) defer func() { - if err := nginxC.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := TerminateContainer(nginxC); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to create container: %s", err) + return + } // containerHost { - ip, _ := nginxC.Host(ctx) + ip, err := nginxC.Host(ctx) + if err != nil { + log.Printf("failed to create container: %s", err) + return + } // } - println(ip) + fmt.Println(ip) state, err := nginxC.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) // Output: + // localhost // true } func ExampleContainer_Start() { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/nginx:alpine", + Image: "nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, }) defer func() { - if err := nginxC.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := TerminateContainer(nginxC); err != nil { + log.Printf("failed to terminate container: %s", err) } }() - _ = nginxC.Start(ctx) + if err != nil { + log.Printf("failed to create container: %s", err) + return + } + + if err = nginxC.Start(ctx); err != nil { + log.Printf("failed to start container: %s", err) + return + } state, err := nginxC.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -1053,23 +956,28 @@ func ExampleContainer_Start() { func ExampleContainer_Stop() { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/nginx:alpine", + Image: "nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, }) defer func() { - if err := nginxC.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := TerminateContainer(nginxC); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to create and start container: %s", err) + return + } + fmt.Println("Container has been started") timeout := 10 * time.Second - err := nginxC.Stop(ctx, &timeout) - if err != nil { - log.Fatalf("failed to stop container: %s", err) // nolint:gocritic + if err = nginxC.Stop(ctx, &timeout); err != nil { + log.Printf("failed to terminate container: %s", err) + return } fmt.Println("Container has been stopped") @@ -1082,19 +990,24 @@ func ExampleContainer_Stop() { func ExampleContainer_MappedPort() { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/nginx:alpine", + Image: "nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, Started: true, }) defer func() { - if err := nginxC.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := TerminateContainer(nginxC); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to create and start container: %s", err) + return + } + // buildingAddresses { ip, _ := nginxC.Host(ctx) port, _ := nginxC.MappedPort(ctx, "80") @@ -1103,7 +1016,8 @@ func ExampleContainer_MappedPort() { state, err := nginxC.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -1114,9 +1028,7 @@ func ExampleContainer_MappedPort() { func TestContainerCreationWithVolumeAndFileWritingToIt(t *testing.T) { absPath, err := filepath.Abs(filepath.Join(".", "testdata", "hello.sh")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) ctx, cnl := context.WithTimeout(context.Background(), 30*time.Second) defer cnl() @@ -1127,7 +1039,7 @@ func TestContainerCreationWithVolumeAndFileWritingToIt(t *testing.T) { bashC, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ - Image: "docker.io/bash", + Image: "bash:5.2.26", Files: []ContainerFile{ { HostFilePath: absPath, @@ -1140,15 +1052,14 @@ func TestContainerCreationWithVolumeAndFileWritingToIt(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, bashC, RemoveVolumes(volumeName)) require.NoError(t, err) - require.NoError(t, bashC.Terminate(ctx)) } func TestContainerWithTmpFs(t *testing.T) { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/busybox", + Image: "busybox", Cmd: []string{"sleep", "10"}, Tmpfs: map[string]string{"/testtmpfs": "rw"}, } @@ -1158,26 +1069,19 @@ func TestContainerWithTmpFs(t *testing.T) { ContainerRequest: req, Started: true, }) - + CleanupContainer(t, ctr) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, ctr) path := "/testtmpfs/test.file" // exec_reader_example { c, reader, err := ctr.Exec(ctx, []string{"ls", path}) - if err != nil { - t.Fatal(err) - } - if c != 1 { - t.Fatalf("File %s should not have existed, expected return code 1, got %v", path, c) - } + require.NoError(t, err) + require.Equalf(t, 1, c, "File %s should not have existed, expected return code 1, got %v", path, c) buf := new(strings.Builder) _, err = io.Copy(buf, reader) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // See the logs from the command execution. t.Log(buf.String()) @@ -1185,36 +1089,27 @@ func TestContainerWithTmpFs(t *testing.T) { // exec_example { c, _, err = ctr.Exec(ctx, []string{"touch", path}) - if err != nil { - t.Fatal(err) - } - if c != 0 { - t.Fatalf("File %s should have been created successfully, expected return code 0, got %v", path, c) - } + require.NoError(t, err) + require.Zerof(t, c, "File %s should have been created successfully, expected return code 0, got %v", path, c) // } c, _, err = ctr.Exec(ctx, []string{"ls", path}) - if err != nil { - t.Fatal(err) - } - if c != 0 { - t.Fatalf("File %s should exist, expected return code 0, got %v", path, c) - } + require.NoError(t, err) + require.Zerof(t, c, "File %s should exist, expected return code 0, got %v", path, c) } func TestContainerNonExistentImage(t *testing.T) { t.Run("if the image not found don't propagate the error", func(t *testing.T) { - _, err := GenericContainer(context.Background(), GenericContainerRequest{ + ctr, err := GenericContainer(context.Background(), GenericContainerRequest{ ContainerRequest: ContainerRequest{ Image: "postgres:nonexistent-version", }, Started: true, }) + CleanupContainer(t, ctr) var nf errdefs.ErrNotFound - if !errors.As(err, &nf) { - t.Fatalf("the error should have been an errdefs.ErrNotFound: %v", err) - } + require.ErrorAsf(t, err, &nf, "the error should have been an errdefs.ErrNotFound: %v", err) }) t.Run("the context cancellation is propagated to container creation", func(t *testing.T) { @@ -1223,16 +1118,13 @@ func TestContainerNonExistentImage(t *testing.T) { c, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ - Image: "docker.io/postgres:12", + Image: "postgres:12", WaitingFor: wait.ForLog("log"), }, Started: true, }) - if !errors.Is(err, ctx.Err()) { - t.Fatalf("err should be a ctx cancelled error %v", err) - } - - terminateContainerOnEnd(t, context.Background(), c) // use non-cancelled context + CleanupContainer(t, c) + require.ErrorIsf(t, err, ctx.Err(), "err should be a ctx cancelled error %v", err) }) } @@ -1248,14 +1140,12 @@ func TestContainerCustomPlatformImage(t *testing.T) { c, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ - Image: "docker.io/redis:latest", + Image: "redis:latest", ImagePlatform: nonExistentPlatform, }, Started: false, }) - - terminateContainerOnEnd(t, ctx, c) - + CleanupContainer(t, c) require.Error(t, err) }) @@ -1266,14 +1156,13 @@ func TestContainerCustomPlatformImage(t *testing.T) { c, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ - Image: "docker.io/mysql:8.0.36", + Image: "mysql:8.0.36", ImagePlatform: "linux/amd64", }, Started: false, }) - + CleanupContainer(t, c) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) dockerCli, err := NewDockerClientWithOpts(ctx) require.NoError(t, err) @@ -1303,13 +1192,11 @@ func TestContainerWithCustomHostname(t *testing.T) { ContainerRequest: req, Started: true, }) - + CleanupContainer(t, ctr) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, ctr) - if actualHostname := readHostname(t, ctr.GetContainerID()); actualHostname != hostname { - t.Fatalf("expected hostname %s, got %s", hostname, actualHostname) - } + actualHostname := readHostname(t, ctr.GetContainerID()) + require.Equalf(t, actualHostname, hostname, "expected hostname %s, got %s", hostname, actualHostname) } func TestContainerInspect_RawInspectIsCleanedOnStop(t *testing.T) { @@ -1319,28 +1206,25 @@ func TestContainerInspect_RawInspectIsCleanedOnStop(t *testing.T) { }, Started: true, }) + CleanupContainer(t, ctr) require.NoError(t, err) - terminateContainerOnEnd(t, context.Background(), ctr) inspect, err := ctr.Inspect(context.Background()) require.NoError(t, err) - assert.NotEmpty(t, inspect.ID) + require.NotEmpty(t, inspect.ID) require.NoError(t, ctr.Stop(context.Background(), nil)) } func readHostname(tb testing.TB, containerId string) string { + tb.Helper() containerClient, err := NewDockerClientWithOpts(context.Background()) - if err != nil { - tb.Fatalf("Failed to create Docker client: %v", err) - } + require.NoErrorf(tb, err, "Failed to create Docker client") defer containerClient.Close() containerDetails, err := containerClient.ContainerInspect(context.Background(), containerId) - if err != nil { - tb.Fatalf("Failed to inspect container: %v", err) - } + require.NoErrorf(tb, err, "Failed to inspect container") return containerDetails.Config.Hostname } @@ -1373,18 +1257,13 @@ func TestDockerContainerCopyFileToContainer(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) _ = nginxC.CopyFileToContainer(ctx, filepath.Join(".", "testdata", "hello.sh"), tc.copiedFileName, 700) c, _, err := nginxC.Exec(ctx, []string{"bash", tc.copiedFileName}) - if err != nil { - t.Fatal(err) - } - if c != 0 { - t.Fatalf("File %s should exist, expected return code 0, got %v", tc.copiedFileName, c) - } + require.NoError(t, err) + require.Zerof(t, c, "File %s should exist, expected return code 0, got %v", tc.copiedFileName, c) }) } } @@ -1401,19 +1280,16 @@ func TestDockerContainerCopyDirToContainer(t *testing.T) { }, Started: true, }) - - p := filepath.Join(".", "testdata", "Dokerfile") + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) + p := filepath.Join(".", "testdata", "Dokerfile") err = nginxC.CopyDirToContainer(ctx, p, "/tmp/testdata/Dockerfile", 700) require.Error(t, err) // copying a file using the directory method will raise an error p = filepath.Join(".", "testdata") err = nginxC.CopyDirToContainer(ctx, p, "/tmp/testdata", 700) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assertExtractedFiles(t, ctx, nginxC, p, "/tmp/testdata/") } @@ -1462,10 +1338,10 @@ func TestDockerCreateContainerWithFiles(t *testing.T) { }, Started: false, }) - terminateContainerOnEnd(t, ctx, nginxC) + CleanupContainer(t, nginxC) if err != nil { - require.Contains(t, err.Error(), tc.errMsg) + require.ErrorContains(t, err, tc.errMsg) } else { for _, f := range tc.files { require.NoError(t, err) @@ -1547,7 +1423,7 @@ func TestDockerCreateContainerWithDirs(t *testing.T) { }, Started: false, }) - terminateContainerOnEnd(t, ctx, nginxC) + CleanupContainer(t, nginxC) require.Equal(t, (err != nil), tc.hasError) if err == nil { @@ -1587,34 +1463,23 @@ func TestDockerContainerCopyToContainer(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) fileContent, err := os.ReadFile(filepath.Join(".", "testdata", "hello.sh")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) err = nginxC.CopyToContainer(ctx, fileContent, tc.copiedFileName, 700) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) c, _, err := nginxC.Exec(ctx, []string{"bash", tc.copiedFileName}) - if err != nil { - t.Fatal(err) - } - if c != 0 { - t.Fatalf("File %s should exist, expected return code 0, got %v", tc.copiedFileName, c) - } + require.NoError(t, err) + require.Zerof(t, c, "File %s should exist, expected return code 0, got %v", tc.copiedFileName, c) }) } } func TestDockerContainerCopyFileFromContainer(t *testing.T) { fileContent, err := os.ReadFile(filepath.Join(".", "testdata", "hello.sh")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) ctx := context.Background() nginxC, err := GenericContainer(ctx, GenericContainerRequest{ @@ -1626,30 +1491,21 @@ func TestDockerContainerCopyFileFromContainer(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) copiedFileName := "hello_copy.sh" _ = nginxC.CopyFileToContainer(ctx, filepath.Join(".", "testdata", "hello.sh"), "/"+copiedFileName, 700) c, _, err := nginxC.Exec(ctx, []string{"bash", copiedFileName}) - if err != nil { - t.Fatal(err) - } - if c != 0 { - t.Fatalf("File %s should exist, expected return code 0, got %v", copiedFileName, c) - } + require.NoError(t, err) + require.Zerof(t, c, "File %s should exist, expected return code 0, got %v", copiedFileName, c) reader, err := nginxC.CopyFileFromContainer(ctx, "/"+copiedFileName) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer reader.Close() fileContentFromContainer, err := io.ReadAll(reader) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, fileContent, fileContentFromContainer) } @@ -1665,31 +1521,22 @@ func TestDockerContainerCopyEmptyFileFromContainer(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) copiedFileName := "hello_copy.sh" _ = nginxC.CopyFileToContainer(ctx, filepath.Join(".", "testdata", "empty.sh"), "/"+copiedFileName, 700) c, _, err := nginxC.Exec(ctx, []string{"bash", copiedFileName}) - if err != nil { - t.Fatal(err) - } - if c != 0 { - t.Fatalf("File %s should exist, expected return code 0, got %v", copiedFileName, c) - } + require.NoError(t, err) + require.Zerof(t, c, "File %s should exist, expected return code 0, got %v", copiedFileName, c) reader, err := nginxC.CopyFileFromContainer(ctx, "/"+copiedFileName) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer reader.Close() fileContentFromContainer, err := io.ReadAll(reader) - if err != nil { - t.Fatal(err) - } - assert.Empty(t, fileContentFromContainer) + require.NoError(t, err) + require.Empty(t, fileContentFromContainer) } func TestDockerContainerResources(t *testing.T) { @@ -1729,9 +1576,8 @@ func TestDockerContainerResources(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxC) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) c, err := NewDockerClientWithOpts(ctx) require.NoError(t, err) @@ -1766,8 +1612,8 @@ func TestContainerCapAdd(t *testing.T) { }, Started: true, }) + CleanupContainer(t, nginx) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginx) dockerClient, err := NewDockerClientWithOpts(ctx) require.NoError(t, err) @@ -1799,17 +1645,14 @@ func TestContainerRunningCheckingStatusCode(t *testing.T) { ContainerRequest: req, Started: true, }) - if err != nil { - t.Fatal(err) - } - - terminateContainerOnEnd(t, ctx, influx) + CleanupContainer(t, influx) + require.NoError(t, err) } func TestContainerWithUserID(t *testing.T) { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/alpine:latest", + Image: "alpine:latest", User: "60125", Cmd: []string{"sh", "-c", "id -u"}, WaitingFor: wait.ForExit(), @@ -1819,19 +1662,14 @@ func TestContainerWithUserID(t *testing.T) { ContainerRequest: req, Started: true, }) - + CleanupContainer(t, ctr) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, ctr) r, err := ctr.Logs(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer r.Close() b, err := io.ReadAll(r) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) actual := regexp.MustCompile(`\D+`).ReplaceAllString(string(b), "") assert.Equal(t, req.User, actual) } @@ -1839,7 +1677,7 @@ func TestContainerWithUserID(t *testing.T) { func TestContainerWithNoUserID(t *testing.T) { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/alpine:latest", + Image: "alpine:latest", Cmd: []string{"sh", "-c", "id -u"}, WaitingFor: wait.ForExit(), } @@ -1848,19 +1686,14 @@ func TestContainerWithNoUserID(t *testing.T) { ContainerRequest: req, Started: true, }) - + CleanupContainer(t, ctr) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, ctr) r, err := ctr.Logs(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer r.Close() b, err := io.ReadAll(r) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) actual := regexp.MustCompile(`\D+`).ReplaceAllString(string(b), "") assert.Equal(t, "0", actual) } @@ -1869,18 +1702,17 @@ func TestGetGatewayIP(t *testing.T) { // When using docker compose with DinD mode, and using host port or http wait strategy // It's need to invoke GetGatewayIP for get the host provider, err := providerType.GetProvider(WithLogger(TestLogger(t))) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer provider.Close() - ip, err := provider.(*DockerProvider).GetGatewayIP(context.Background()) - if err != nil { - t.Fatal(err) - } - if ip == "" { - t.Fatal("could not get gateway ip") + dockerProvider, ok := provider.(*DockerProvider) + if !ok { + t.Skip("provider is not a DockerProvider") } + + ip, err := dockerProvider.GetGatewayIP(context.Background()) + require.NoError(t, err) + require.NotEmpty(t, ip) } func TestNetworkModeWithContainerReference(t *testing.T) { @@ -1892,9 +1724,8 @@ func TestNetworkModeWithContainerReference(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxA) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxA) networkMode := fmt.Sprintf("container:%v", nginxA.GetContainerID()) nginxB, err := GenericContainer(ctx, GenericContainerRequest{ @@ -1907,13 +1738,13 @@ func TestNetworkModeWithContainerReference(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxB) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxB) } // creates a temporary dir in which the files will be extracted. Then it will compare the bytes of each file in the source with the bytes from the copied-from-container file func assertExtractedFiles(t *testing.T, ctx context.Context, container Container, hostFilePath string, containerFilePath string) { + t.Helper() // create all copied files into a temporary dir tmpDir := t.TempDir() @@ -1957,16 +1788,6 @@ func assertExtractedFiles(t *testing.T, ctx context.Context, container Container } } -func terminateContainerOnEnd(tb testing.TB, ctx context.Context, ctr Container) { - tb.Helper() - if ctr == nil { - return - } - tb.Cleanup(func() { - require.NoError(tb, ctr.Terminate(ctx)) - }) -} - func TestDockerProviderFindContainerByName(t *testing.T) { ctx := context.Background() provider, err := NewDockerProvider(WithLogger(TestLogger(t))) @@ -1982,11 +1803,12 @@ func TestDockerProviderFindContainerByName(t *testing.T) { }, Started: true, }) + CleanupContainer(t, c1) require.NoError(t, err) c1Inspect, err := c1.Inspect(ctx) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c1) + CleanupContainer(t, c1) c1Name := c1Inspect.Name @@ -1999,8 +1821,8 @@ func TestDockerProviderFindContainerByName(t *testing.T) { }, Started: true, }) + CleanupContainer(t, c2) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c2) c, err := provider.findContainerByName(ctx, "test") require.NoError(t, err) @@ -2035,8 +1857,8 @@ func TestImageBuiltFromDockerfile_KeepBuiltImage(t *testing.T) { }, }, }) + CleanupContainer(t, c) require.NoError(t, err, "create container should not fail") - defer func() { _ = c.Terminate(context.Background()) }() // Get the image ID. containerInspect, err := c.Inspect(ctx) require.NoError(t, err, "container inspect should not fail") @@ -2058,7 +1880,7 @@ func TestImageBuiltFromDockerfile_KeepBuiltImage(t *testing.T) { if tt.keepBuiltImage { require.NoError(t, err, "image should still exist") } else { - require.Error(t, err, "image should not exist anymore") + require.Error(t, err, "image should not exist any more") } }) } @@ -2295,15 +2117,11 @@ func TestCustomPrefixTrailingSlashIsProperlyRemovedIfPresent(t *testing.T) { ContainerRequest: req, Started: true, }) - if err != nil { - t.Fatal(err) - } - defer func() { - terminateContainerOnEnd(t, ctx, c) - }() + CleanupContainer(t, c) + require.NoError(t, err) // enforce the concrete type, as GenericContainer returns an interface, // which will be changed in future implementations of the library dockerContainer := c.(*DockerContainer) - assert.Equal(t, fmt.Sprintf("%s%s", hubPrefixWithTrailingSlash, dockerImage), dockerContainer.Image) + require.Equal(t, fmt.Sprintf("%s%s", hubPrefixWithTrailingSlash, dockerImage), dockerContainer.Image) } diff --git a/docs/features/common_functional_options.md b/docs/features/common_functional_options.md index d559a3ee7f..18d0e4b007 100644 --- a/docs/features/common_functional_options.md +++ b/docs/features/common_functional_options.md @@ -70,7 +70,8 @@ useful context instead of appearing out of band. ```golang func TestHandler(t *testing.T) { logger := TestLogger(t) - _, err := postgresModule.Run(ctx, "postgres:15-alpine", testcontainers.WithLogger(logger)) + ctr, err := postgresModule.Run(ctx, "postgres:15-alpine", testcontainers.WithLogger(logger)) + CleanupContainer(t, ctr) require.NoError(t, err) // Do something with container. } diff --git a/docs/features/configuration.md b/docs/features/configuration.md index ee5b6a4d69..8da214e977 100644 --- a/docs/features/configuration.md +++ b/docs/features/configuration.md @@ -50,9 +50,9 @@ Please read more about customizing images in the [Image name substitution](image 1. If your environment already implements automatic cleanup of containers after the execution, but does not allow starting privileged containers, you can turn off the Ryuk container by setting `TESTCONTAINERS_RYUK_DISABLED` **environment variable** , or the `ryuk.disabled` **property** to `true`. -1. You can specify the connection timeout for Ryuk by setting the `TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT` **environment variable**, or the `ryuk.connection.timeout` **property**. The default value is 1 minute. -1. You can specify the reconnection timeout for Ryuk by setting the `TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT` **environment variable**, or the `ryuk.reconnection.timeout` **property**. The default value is 10 seconds. -1. You can configure Ryuk to run in verbose mode by setting any of the `ryuk.verbose` **property** or the `TESTCONTAINERS_RYUK_VERBOSE` **environment variable**. The default value is `false`. +1. You can specify the connection timeout for Ryuk by setting the `RYUK_CONNECTION_TIMEOUT` **environment variable**, or the `ryuk.connection.timeout` **property**. The default value is 1 minute. +1. You can specify the reconnection timeout for Ryuk by setting the `RYUK_RECONNECTION_TIMEOUT` **environment variable**, or the `ryuk.reconnection.timeout` **property**. The default value is 10 seconds. +1. You can configure Ryuk to run in verbose mode by setting any of the `ryuk.verbose` **property** or the `RYUK_VERBOSE` **environment variable**. The default value is `false`. !!!info For more information about Ryuk, see [Garbage Collector](garbage_collector.md). @@ -62,6 +62,12 @@ but does not allow starting privileged containers, you can turn off the Ryuk con This is because the Compose module may take longer to start all the services. Besides, the `ryuk.reconnection.timeout` should be increased to at least 30 seconds. For further information, please check [https://github.com/testcontainers/testcontainers-go/pull/2485](https://github.com/testcontainers/testcontainers-go/pull/2485). +!!!warn + The following environment variables for configuring Ryuk have been deprecated: + `TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT`, `TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT` and + `TESTCONTAINERS_RYUK_VERBOSE` have been replaced by `RYUK_CONNECTION_TIMEOUT` + `RYUK_RECONNECTION_TIMEOUT` and `RYUK_VERBOSE` respectively. + ## Docker host detection _Testcontainers for Go_ will attempt to detect the Docker environment and configure everything to work automatically. diff --git a/docs/features/creating_container.md b/docs/features/creating_container.md index 30264a05da..35e53d003e 100644 --- a/docs/features/creating_container.md +++ b/docs/features/creating_container.md @@ -11,7 +11,15 @@ up with Testcontainers and integrate into your tests: `testcontainers.GenericContainer` defines the container that should be run, similar to the `docker run` command. -The following test creates an NGINX container and validates that it returns 200 for the status code: +The following test creates an NGINX container on both the `bridge` (docker default +network) and the `foo` network and validates that it returns 200 for the status code. + +It also demonstrates how to use `CleanupContainer` ensures that nginx container +is removed when the test ends even if the underlying `GenericContainer` errored +as well as the `CleanupNetwork` which does the same for networks. + +The alternatives for these outside of tests as a `defer` are `TerminateContainer` +and `Network.Remove` which can be seen in the examples. ```go package main @@ -32,33 +40,38 @@ type nginxContainer struct { } -func setupNginx(ctx context.Context) (*nginxContainer, error) { +func setupNginx(ctx context.Context, networkName string) (*nginxContainer, error) { req := testcontainers.ContainerRequest{ Image: "nginx", ExposedPorts: []string{"80/tcp"}, + Networks: []string{"bridge", networkName}, WaitingFor: wait.ForHTTP("/"), } container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) + var nginxC *nginxContainer + if container != nil { + nginxC = &nginxContainer{Container: c} + } if err != nil { - return nil, err + return nginxC, err } ip, err := container.Host(ctx) if err != nil { - return nil, err + return nginxC, err } mappedPort, err := container.MappedPort(ctx, "80") if err != nil { - return nil, err + return nginxC, err } - uri := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port()) + nginxC.URI = fmt.Sprintf("http://%s:%s", ip, mappedPort.Port()) - return &nginxContainer{Container: container, URI: uri}, nil + return nginxC, nil } func TestIntegrationNginxLatestReturn(t *testing.T) { @@ -68,31 +81,33 @@ func TestIntegrationNginxLatestReturn(t *testing.T) { ctx := context.Background() - nginxC, err := setupNginx(ctx) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := nginxC.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } + networkName := "foo" + net, err := provider.CreateNetwork(ctx, NetworkRequest{ + Name: networkName, }) + require.NoError(t, err) + CleanupNetwork(t, net) + + nginxC, err := setupNginx(ctx, networkName) + testcontainers.CleanupContainer(t, nginxC) + require.NoError(t, err) resp, err := http.Get(nginxC.URI) - if resp.StatusCode != http.StatusOK { - t.Fatalf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) - } + require.Equal(t, http.StatusOK, resp.StatusCode) } ``` + + + ### Lifecycle hooks _Testcontainers for Go_ allows you to define your own lifecycle hooks for better control over your containers. You just need to define functions that return an error and receive the Go context as first argument, and a `ContainerRequest` for the `Creating` hook, and a `Container` for the rest of them as second argument. You'll be able to pass multiple lifecycle hooks at the `ContainerRequest` as an array of `testcontainers.ContainerLifecycleHooks`. The `testcontainers.ContainerLifecycleHooks` struct defines the following lifecycle hooks, each of them backed by an array of functions representing the hooks: +* `PreBuilds` - hooks that are executed before the image is built. This hook is only available when creating a container from a Dockerfile +* `PostBuilds` - hooks that are executed after the image is built. This hook is only available when creating a container from a Dockerfile * `PreCreates` - hooks that are executed before the container is created * `PostCreates` - hooks that are executed after the container is created * `PreStarts` - hooks that are executed before the container is started @@ -145,8 +160,8 @@ The aforementioned `GenericContainer` function and the `ContainerRequest` struct ## Reusable container -With `Reuse` option you can reuse an existing container. Reusing will work only if you pass an -existing container name via 'req.Name' field. If the name is not in a list of existing containers, +With `Reuse` option you can reuse an existing container. Reusing will work only if you pass an +existing container name via 'req.Name' field. If the name is not in a list of existing containers, the function will create a new generic container. If `Reuse` is true and `Name` is empty, you will get error. The following test creates an NGINX container, adds a file into it and then reuses the container again for checking the file: @@ -178,16 +193,22 @@ func main() { }, Started: true, }) + defer func() { + if err := testcontainers.TerminateContainer(n1); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() if err != nil { - log.Fatal(err) + log.Print(err) + return } - defer n1.Terminate(ctx) copiedFileName := "hello_copy.sh" err = n1.CopyFileToContainer(ctx, "./testdata/hello.sh", "/"+copiedFileName, 700) if err != nil { - log.Fatal(err) + log.Print(err) + return } n2, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ @@ -200,13 +221,20 @@ func main() { Started: true, Reuse: true, }) + defer func() { + if err := testcontainers.TerminateContainer(n2); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() if err != nil { - log.Fatal(err) + log.Print(err) + return } c, _, err := n2.Exec(ctx, []string{"bash", copiedFileName}) if err != nil { - log.Fatal(err) + log.Print(err) + return } fmt.Println(c) } @@ -256,10 +284,20 @@ func main() { } res, err := testcontainers.ParallelContainers(ctx, requests, testcontainers.ParallelContainersOptions{}) + for _, c := range res { + c := c + defer func() { + if err := testcontainers.TerminateContainer(c); err != nil { + log.Printf("failed to terminate container: %s", c) + } + }() + } + if err != nil { e, ok := err.(testcontainers.ParallelContainersError) if !ok { - log.Fatalf("unknown error: %v", err) + log.Printf("unknown error: %v", err) + return } for _, pe := range e.Errors { @@ -267,14 +305,5 @@ func main() { } return } - - for _, c := range res { - c := c - defer func() { - if err := c.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", c) - } - }() - } } ``` diff --git a/docs/features/image_name_substitution.md b/docs/features/image_name_substitution.md index 08e9e1d6a0..4272d35c1e 100644 --- a/docs/features/image_name_substitution.md +++ b/docs/features/image_name_substitution.md @@ -45,7 +45,7 @@ _Testcontainers for Go_ will automatically apply the prefix to every image that _Testcontainers for Go_ will not apply the prefix to: * non-Hub image names (e.g. where another registry is set) -* Docker Hub image names where the hub registry is explicitly part of the name (i.e. anything with a `docker.io` or `registry.hub.docker.com` host part) +* Docker Hub image names where the hub registry is explicitly part of the name (i.e. anything with a `registry.hub.docker.com` host part) ## Developing a custom function for transforming image names on the fly @@ -68,7 +68,7 @@ You can implement a custom image name substitutor by: * implementing the `ImageNameSubstitutor` interface, exposed by the `testcontainers` package. * configuring _Testcontainers for Go_ to use your custom implementation, defined at the `ContainerRequest` level. -The following is an example image substitutor implementation prepending the `docker.io/` prefix, used in the tests: +The following is an example image substitutor implementation prepending the `registry.hub.docker.com/library/` prefix, used in the tests: [Image Substitutor Interface](../../options.go) inside_block:imageSubstitutor diff --git a/docs/features/wait/exit.md b/docs/features/wait/exit.md index 3487cb2d21..bcb1aaca36 100644 --- a/docs/features/wait/exit.md +++ b/docs/features/wait/exit.md @@ -9,7 +9,7 @@ The exit wait strategy will check that the container is not in the running state ```golang req := ContainerRequest{ - Image: "docker.io/alpine:latest", + Image: "alpine:latest", WaitingFor: wait.ForExit(), } ``` diff --git a/docs/features/wait/health.md b/docs/features/wait/health.md index d4756f47bd..f724a11010 100644 --- a/docs/features/wait/health.md +++ b/docs/features/wait/health.md @@ -7,7 +7,7 @@ The health wait strategy will check that the container is in the healthy state a ```golang req := ContainerRequest{ - Image: "docker.io/alpine:latest", + Image: "alpine:latest", WaitingFor: wait.ForHealthCheck(), } ``` diff --git a/docs/features/wait/host_port.md b/docs/features/wait/host_port.md index 1b6090b584..10531e5e64 100644 --- a/docs/features/wait/host_port.md +++ b/docs/features/wait/host_port.md @@ -14,7 +14,7 @@ Variations on the HostPort wait strategy are supported, including: ```golang req := ContainerRequest{ - Image: "docker.io/nginx:alpine", + Image: "nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForListeningPort("80/tcp"), } @@ -26,7 +26,7 @@ The wait strategy will use the lowest exposed port from the container configurat ```golang req := ContainerRequest{ - Image: "docker.io/nginx:alpine", + Image: "nginx:alpine", WaitingFor: wait.ForExposedPort(), } ``` @@ -35,7 +35,7 @@ Said that, it could be the case that the container request included ports to be ```golang req := ContainerRequest{ - Image: "docker.io/nginx:alpine", + Image: "nginx:alpine", ExposedPorts: []string{"80/tcp", "9080/tcp"}, WaitingFor: wait.ForExposedPort(), } @@ -55,7 +55,7 @@ In this case, the `wait.ForExposedPort.SkipInternalCheck` can be used to skip th ```golang req := ContainerRequest{ - Image: "docker.io/nginx:alpine", + Image: "nginx:alpine", ExposedPorts: []string{"80/tcp", "9080/tcp"}, WaitingFor: wait.ForExposedPort().SkipInternalCheck(), } diff --git a/docs/features/wait/log.md b/docs/features/wait/log.md index 66c418b284..f1d40ff360 100644 --- a/docs/features/wait/log.md +++ b/docs/features/wait/log.md @@ -10,7 +10,7 @@ The Log wait strategy will check if a string occurs in the container logs for a ```golang req := ContainerRequest{ - Image: "docker.io/mysql:8.0.36", + Image: "mysql:8.0.36", ExposedPorts: []string{"3306/tcp", "33060/tcp"}, Env: map[string]string{ "MYSQL_ROOT_PASSWORD": "password", @@ -24,7 +24,7 @@ Using a regular expression: ```golang req := ContainerRequest{ - Image: "docker.io/mysql:8.0.36", + Image: "mysql:8.0.36", ExposedPorts: []string{"3306/tcp", "33060/tcp"}, Env: map[string]string{ "MYSQL_ROOT_PASSWORD": "password", diff --git a/docs/features/wait/multi.md b/docs/features/wait/multi.md index bfd053955b..d5f809d6c2 100644 --- a/docs/features/wait/multi.md +++ b/docs/features/wait/multi.md @@ -9,7 +9,7 @@ Available Options: ```golang req := ContainerRequest{ - Image: "docker.io/mysql:8.0.36", + Image: "mysql:8.0.36", ExposedPorts: []string{"3306/tcp", "33060/tcp"}, Env: map[string]string{ "MYSQL_ROOT_PASSWORD": "password", diff --git a/docs/modules/artemis.md b/docs/modules/artemis.md index 395f1b304b..da13e2178a 100644 --- a/docs/modules/artemis.md +++ b/docs/modules/artemis.md @@ -50,7 +50,7 @@ When starting the Artemis container, you can pass options in a variadic way to c #### Image If you need to set a different Artemis Docker image, you can set a valid Docker image as the second argument in the `Run` function. -E.g. `Run(context.Background(), "docker.io/apache/activemq-artemis:2.30.0")`. +E.g. `Run(context.Background(), "apache/activemq-artemis:2.30.0")`. {% include "../features/common_functional_options.md" %} diff --git a/docs/modules/consul.md b/docs/modules/consul.md index 813799e28a..b2b77921df 100644 --- a/docs/modules/consul.md +++ b/docs/modules/consul.md @@ -46,7 +46,7 @@ When starting the Consul container, you can pass options in a variadic way to co #### Image If you need to set a different Consul Docker image, you can set a valid Docker image as the second argument in the `Run` function. -E.g. `Run(context.Background(), "docker.io/hashicorp/consul:1.15")`. +E.g. `Run(context.Background(), "hashicorp/consul:1.15")`. {% include "../features/common_functional_options.md" %} diff --git a/docs/modules/couchbase.md b/docs/modules/couchbase.md index ac07889bfa..43b0c85007 100644 --- a/docs/modules/couchbase.md +++ b/docs/modules/couchbase.md @@ -73,7 +73,7 @@ When starting the Couchbase container, you can pass options in a variadic way to #### Image If you need to set a different Couchbase Docker image, you can set a valid Docker image as the second argument in the `Run` function. -E.g. `Run(context.Background(), "docker.io/couchbase:6.5.1")`. +E.g. `Run(context.Background(), "couchbase:6.5.1")`. You can find the Docker images that are currently tested in this module, for the Enterprise and Community editions, in the following list: diff --git a/docs/modules/databend.md b/docs/modules/databend.md new file mode 100644 index 0000000000..0e3e2fe438 --- /dev/null +++ b/docs/modules/databend.md @@ -0,0 +1,72 @@ +# Databend + +Since testcontainers-go :material-tag: v0.34.0 + +## Introduction + +The Testcontainers module for Databend. + +## Adding this module to your project dependencies + +Please run the following command to add the Databend module to your Go dependencies: + +``` +go get github.com/testcontainers/testcontainers-go/modules/databend +``` + +## Usage example + + +[Creating a Databend container](../../modules/databend/examples_test.go) inside_block:runDatabendContainer + + +## Module Reference + +### Run function + +- Since testcontainers-go :material-tag: v0.34.0 + +The Databend module exposes one entrypoint function to create the Databend container, and this function receives three parameters: + +```golang +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*DatabendContainer, error) +``` + +- `context.Context`, the Go context. +- `string`, the Docker image to use. +- `testcontainers.ContainerCustomizer`, a variadic argument for passing options. + +### Container Options + +When starting the Databend container, you can pass options in a variadic way to configure it. + +#### Image + +If you need to set a different Databend Docker image, you can set a valid Docker image as the second argument in the `Run` function. +E.g. `Run(context.Background(), "datafuselabs/databend:v1.2.615")`. + +{% include "../features/common_functional_options.md" %} + +#### Set username, password + +If you need to set a different user/password/database, you can use `WithUsername`, `WithPassword` options. + +!!!info +The default values for the username is `databend`, for password is `databend` and for the default database name is `default`. + +### Container Methods + +The Databend container exposes the following methods: + +#### ConnectionString + +This method returns the connection string to connect to the Databend container, using the default `8000` port. +It's possible to pass extra parameters to the connection string, e.g. `sslmode=disable`. + + +[Get connection string](../../modules/databend/databend_test.go) inside_block:connectionString + + +#### MustGetConnectionString + +`MustConnectionString` panics if the address cannot be determined. diff --git a/docs/modules/dynamodb.md b/docs/modules/dynamodb.md new file mode 100644 index 0000000000..b7b53b64e9 --- /dev/null +++ b/docs/modules/dynamodb.md @@ -0,0 +1,77 @@ +# DynamoDB + +Since testcontainers-go :material-tag: v0.34.0 + +## Introduction + +The Testcontainers module for DynamoDB. + +## Adding this module to your project dependencies + +Please run the following command to add the DynamoDB module to your Go dependencies: + +``` +go get github.com/testcontainers/testcontainers-go/modules/dynamodb +``` + +## Usage example + + +[Creating a DynamoDB container](../../modules/dynamodb/examples_test.go) inside_block:runDynamoDBContainer + + +## Module Reference + +### Run function + +- Since testcontainers-go :material-tag: v0.34.0 + +The DynamoDB module exposes one entrypoint function to create the DynamoDB container, and this function receives three parameters: + +```golang +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*DynamoDBContainer, error) +``` + +- `context.Context`, the Go context. +- `string`, the Docker image to use. +- `testcontainers.ContainerCustomizer`, a variadic argument for passing options. + +### Container Options + +When starting the DynamoDB container, you can pass options in a variadic way to configure it. + +#### Image + +If you need to set a different DynamoDB Docker image, you can set a valid Docker image as the second argument in the `Run` function. +E.g. `Run(context.Background(), "amazon/dynamodb-local:2.2.1")`. + +{% include "../features/common_functional_options.md" %} + +#### WithSharedDB + +- Since testcontainers-go :material-tag: v0.34.0 + +The `WithSharedDB` option tells the DynamoDB container to use a single database file. At the same time, it marks the container as reusable, which causes that successive calls to the `Run` function will return the same container instance, and therefore, the same database file. + +#### WithDisableTelemetry + +- Since testcontainers-go :material-tag: v0.34.0 + +You can turn off telemetry when starting the DynamoDB container, using the option `WithDisableTelemetry`. + +### Container Methods + +The DynamoDB container exposes the following methods: + +#### ConnectionString + +- Since testcontainers-go :material-tag: v0.34.0 + +The `ConnectionString` method returns the connection string to the DynamoDB container. This connection string can be used to connect to the DynamoDB container from your application, +using the AWS SDK or any other DynamoDB client of your choice. + + +[Creating a client](../../modules/dynamodb/dynamodb_test.go) inside_block:createClient + + +The above example uses `github.com/aws/aws-sdk-go-v2/service/dynamodb` to create a client and connect to the DynamoDB container. diff --git a/docs/modules/etcd.md b/docs/modules/etcd.md new file mode 100644 index 0000000000..ffde87ac6b --- /dev/null +++ b/docs/modules/etcd.md @@ -0,0 +1,95 @@ +# etcd + +Since testcontainers-go :material-tag: v0.34.0 + +## Introduction + +The Testcontainers module for etcd. + +## Adding this module to your project dependencies + +Please run the following command to add the etcd module to your Go dependencies: + +``` +go get github.com/testcontainers/testcontainers-go/modules/etcd +``` + +## Usage example + + +[Creating a etcd container](../../modules/etcd/examples_test.go) inside_block:runetcdContainer + + +## Module Reference + +### Run function + +- Since testcontainers-go :material-tag: v0.34.0 + +The etcd module exposes one entrypoint function to create the etcd container, and this function receives three parameters: + +```golang +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*etcdContainer, error) +``` + +- `context.Context`, the Go context. +- `string`, the Docker image to use. +- `testcontainers.ContainerCustomizer`, a variadic argument for passing options. + +### Container Options + +When starting the etcd container, you can pass options in a variadic way to configure it. + +#### Image + +If you need to set a different etcd Docker image, you can set a valid Docker image as the second argument in the `Run` function. +E.g. `Run(context.Background(), "bitnami/etcd:latest")`. + +{% include "../features/common_functional_options.md" %} + +#### WithAdditionalArgs + +- Since testcontainers-go :material-tag: v0.34.0 + +You can pass additional arguments to the etcd container by using the `WithAdditionalArgs` option. The arguments are passed to the CMD of the etcd container. + +#### WithDataDir + +- Since testcontainers-go :material-tag: v0.34.0 + +You can set the data directory for the etcd container by using the `WithDataDir` boolean option. The data directory where the etcd data is stored is `/data.etcd`. + +#### WithNodes + +- Since testcontainers-go :material-tag: v0.34.0 + +You can set the number of nodes for the etcd cluster by using the `WithNodes` option, passing the node names for each of the nodes. Single-node clusters are not allowed, +for that reason the functional option receives three string arguments: the first node, the second node, and a variadic argument for the rest of the nodes. +The module starts a container for each node, having the first node a reference to the other nodes. E.g. `WithNodes("etcd-1", "etcd-2")`, `WithNodes("etcd-1", "etcd-2", "etcd-3")` and so on. + +The module creates a Docker network for the etcd cluster, and the nodes are connected to this network, so that they can communicate with each other through the network. + +#### WithClusterToken + +- Since testcontainers-go :material-tag: v0.34.0 + +Sets the cluster token for the etcd cluster. The cluster token is used to identify the etcd cluster. The default value is `mys3cr3ttok3n`. +The etcd container holds a reference to the cluster token, so you can use it with e.g. `ctr.ClusterToken`. + +### Container Methods + +- Since testcontainers-go :material-tag: v0.34.0 + +The etcd container exposes the following methods: + +#### ClientEndpoint + +- Since testcontainers-go :material-tag: v0.34.0 + +Returns the client endpoint for the etcd container and an error, if any. In the case of a cluster, it returns the client endpoint for the first node. + +#### PeerEndpoint + +- Since testcontainers-go :material-tag: v0.34.0 + +Returns the peer endpoint for the etcd container and an error, if any. In the case of a cluster, it returns the peer endpoint for the first node. diff --git a/docs/modules/index.md b/docs/modules/index.md index 629787fedf..8db41723fa 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -72,7 +72,7 @@ We have provided a command line tool to generate the scaffolding for the code of | Flag | Short | Type | Required | Description | |---------|-------|--------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------| | --name | -n | string | Yes | Name of the module, use camel-case when needed. Only alphanumerical characters are allowed (leading character must be a letter). | -| --image | -i | string | Yes | Fully-qualified name of the Docker image to be used in the examples and tests (i.e. 'docker.io/org/project:tag') | +| --image | -i | string | Yes | Fully-qualified name of the Docker image to be used in the examples and tests (i.e. 'org/project:tag') | | --title | -t | string | No | A variant of the name supporting mixed casing (i.e. 'MongoDB'). Only alphanumerical characters are allowed (leading character must be a letter). | diff --git a/docs/modules/k3s.md b/docs/modules/k3s.md index 6ada392534..617c1d4ffe 100644 --- a/docs/modules/k3s.md +++ b/docs/modules/k3s.md @@ -52,7 +52,7 @@ When starting the K3s container, you can pass options in a variadic way to confi #### Image If you need to set a different K3s Docker image, you can set a valid Docker image as the second argument in the `Run` function. -E.g. `Run(context.Background(), "docker.io/rancher/k3s:v1.27.1-k3s1")`. +E.g. `Run(context.Background(), "rancher/k3s:v1.27.1-k3s1")`. {% include "../features/common_functional_options.md" %} diff --git a/docs/modules/localstack.md b/docs/modules/localstack.md index cf209162af..77cc522907 100644 --- a/docs/modules/localstack.md +++ b/docs/modules/localstack.md @@ -103,6 +103,7 @@ For further reference on the SDK v1, please check out the AWS docs [here](https: ### Using the AWS SDK v2 +[EndpointResolver](../../modules/localstack/v2/s3_test.go) inside_block:awsResolverV2 [AWS SDK v2](../../modules/localstack/v2/s3_test.go) inside_block:awsSDKClientV2 diff --git a/docs/modules/meilisearch.md b/docs/modules/meilisearch.md new file mode 100644 index 0000000000..d8fe865be0 --- /dev/null +++ b/docs/modules/meilisearch.md @@ -0,0 +1,77 @@ +# Meilisearch + +Since testcontainers-go :material-tag: v0.34.0 + +## Introduction + +The Testcontainers module for Meilisearch. + +## Adding this module to your project dependencies + +Please run the following command to add the Meilisearch module to your Go dependencies: + +``` +go get github.com/testcontainers/testcontainers-go/modules/meilisearch +``` + +## Usage example + + +[Creating a Meilisearch container](../../modules/meilisearch/examples_test.go) inside_block:runMeilisearchContainer + + +## Module Reference + +### Run function + +- Since testcontainers-go :material-tag: v0.34.0 + +The Meilisearch module exposes one entrypoint function to create the Meilisearch container, and this function receives three parameters: + +```golang +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*MeilisearchContainer, error) +``` + +- `context.Context`, the Go context. +- `string`, the Docker image to use. +- `testcontainers.ContainerCustomizer`, a variadic argument for passing options. + +### Container Options + +When starting the Meilisearch container, you can pass options in a variadic way to configure it. + +#### Image + +If you need to set a different Meilisearch Docker image, you can set a valid Docker image as the second argument in the `Run` function. +E.g. `Run(context.Background(), "getmeili/meilisearch:v1.10.3")`. + +{% include "../features/common_functional_options.md" %} + +#### Master Key + +- Since testcontainers-go :material-tag: v0.34.0 + +If you need to set a master key, you can use the `WithMasterKey(key string)` option. Otherwise, the default will be used which is `just-a-master-key-for-test`, which is exported on the container fields. + +#### Dump Data + +- Since testcontainers-go :material-tag: v0.34.0 + +If you need to dump data in Meilisearch upon initialization for testing, you can use `WithDumpData(filepath string)` option where `filepath` can be an absolute path or relative path to a `dump` file. Please refer to the official Meilisearch documentation about dump files [here](https://www.meilisearch.com/docs/learn/advanced/snapshots_vs_dumps#dumps). + +### Container Methods + +The Meilisearch container exposes the following methods: + +#### Address + +- Since testcontainers-go :material-tag: v0.34.0 + +The `Address` method retrieves the address of the Meilisearch container. +It will use http as protocol, as TLS is not supported at the moment. + +#### MasterKey + +- Since testcontainers-go :material-tag: v0.34.0 + +The `MasterKey` method retrieves the master key of the Meilisearch container. diff --git a/docs/modules/mysql.md b/docs/modules/mysql.md index ca81ba3974..5ef38d56da 100644 --- a/docs/modules/mysql.md +++ b/docs/modules/mysql.md @@ -52,12 +52,6 @@ When starting the MySQL container, you can pass options in a variadic way to con If you need to set a different MySQL Docker image, you can set a valid Docker image as the second argument in the `Run` function. E.g. `Run(context.Background(), "mysql:8.0.36")`. -By default, the container will use the following Docker image: - - -[Default Docker image](../../modules/mysql/mysql.go) inside_block:defaultImage - - {% include "../features/common_functional_options.md" %} #### Set username, password and database name diff --git a/docs/modules/neo4j.md b/docs/modules/neo4j.md index aadda970e1..e0188e015d 100644 --- a/docs/modules/neo4j.md +++ b/docs/modules/neo4j.md @@ -56,13 +56,7 @@ When starting the Neo4j container, you can pass options in a variadic way to con #### Image If you need to set a different Neo4j Docker image, you can set a valid Docker image as the second argument in the `Run` function. -E.g. `Run(context.Background(), "docker.io/neo4j:4.4")`. - -By default, the container will use the following Docker image: - - -[Default Docker image](../../modules/neo4j/neo4j.go) inside_block:defaultImage - +E.g. `Run(context.Background(), "neo4j:4.4")`. {% include "../features/common_functional_options.md" %} diff --git a/docs/modules/postgres.md b/docs/modules/postgres.md index 82b1e04351..930de50c15 100644 --- a/docs/modules/postgres.md +++ b/docs/modules/postgres.md @@ -49,7 +49,7 @@ When starting the Postgres container, you can pass options in a variadic way to #### Image If you need to set a different Postgres Docker image, you can set a valid Docker image as the second argument in the `Run` function. -E.g. `Run(context.Background(), "docker.io/postgres:16-alpine")`. +E.g. `Run(context.Background(), "postgres:16-alpine")`. {% include "../features/common_functional_options.md" %} diff --git a/docs/modules/pulsar.md b/docs/modules/pulsar.md index d8c3db5735..99f93b2222 100644 --- a/docs/modules/pulsar.md +++ b/docs/modules/pulsar.md @@ -52,7 +52,7 @@ When starting the Pulsar container, you can pass options in a variadic way to co #### Image If you need to set a different Pulsar Docker image, you can set a valid Docker image as the second argument in the `Run` function. -E.g. `Run(context.Background(), "docker.io/apachepulsar/pulsar:2.10.2")`. +E.g. `Run(context.Background(), "apachepulsar/pulsar:2.10.2")`. {% include "../features/common_functional_options.md" %} diff --git a/docs/modules/redis.md b/docs/modules/redis.md index 51e111dfff..e6bbd90851 100644 --- a/docs/modules/redis.md +++ b/docs/modules/redis.md @@ -49,7 +49,7 @@ When starting the Redis container, you can pass options in a variadic way to con #### Image If you need to set a different Redis Docker image, you can set a valid Docker image as the second argument in the `Run` function. -E.g. `Run(context.Background(), "docker.io/redis:7")`. +E.g. `Run(context.Background(), "redis:7")`. {% include "../features/common_functional_options.md" %} diff --git a/docs/modules/redpanda.md b/docs/modules/redpanda.md index f923b8be09..028dbaf95f 100644 --- a/docs/modules/redpanda.md +++ b/docs/modules/redpanda.md @@ -61,6 +61,8 @@ If you need to enable TLS use `WithTLS` with a valid PEM encoded certificate and #### Additional Listener +- Since testcontainers-go :material-tag: v0.28.0 + There are scenarios where additional listeners are needed, for example if you want to consume/from another container in the same network @@ -79,12 +81,77 @@ Produce messages using the new registered listener [Produce/consume via registered listener](../../modules/redpanda/redpanda_test.go) inside_block:withListenerExec +#### Adding Service Accounts + +- Since testcontainers-go :material-tag: v0.20.0 + +It's possible to add service accounts to the Redpanda container using the `WithNewServiceAccount` option, setting the service account name and its password. +E.g. `WithNewServiceAccount("service-account", "password")`. + +#### Adding Super Users + +- Since testcontainers-go :material-tag: v0.20.0 + +When a super user is needed, you can use the `WithSuperusers` option, passing a variadic list of super users. +E.g. `WithSuperusers("superuser-1", "superuser-2")`. + +#### Enabling SASL + +- Since testcontainers-go :material-tag: v0.20.0 + +The `WithEnableSASL()` option enables SASL scram sha authentication. By default, no authentication (plaintext) is used. +When setting an authentication method, make sure to add users as well and authorize them using the `WithSuperusers()` option. + +#### WithEnableKafkaAuthorization + +- Since testcontainers-go :material-tag: v0.20.0 + +The `WithEnableKafkaAuthorization` enables authorization for connections on the Kafka API. + +#### WithEnableWasmTransform + +- Since testcontainers-go :material-tag: v0.28.0 + +The `WithEnableWasmTransform` enables wasm transform. + +!!!warning + Should not be used with RP versions before 23.3 + +#### WithEnableSchemaRegistryHTTPBasicAuth + +- Since testcontainers-go :material-tag: v0.20.0 + +The `WithEnableSchemaRegistryHTTPBasicAuth` enables HTTP basic authentication for the Schema Registry. + +#### WithAutoCreateTopics + +- Since testcontainers-go :material-tag: v0.22.0 + +The `WithAutoCreateTopics` option enables the auto-creation of topics. + +#### WithTLS + +- Since testcontainers-go :material-tag: v0.24.0 + +The `WithTLS` option enables TLS encryption. It requires a valid PEM encoded certificate and key, passed as byte slices. +E.g. `WithTLS([]byte(cert), []byte(key))`. + +#### WithBootstrapConfig + +- Since testcontainers-go :material-tag: v0.33.0 + +`WithBootstrapConfig` adds an arbitrary config key-value pair to the Redpanda container. Per the name, this config will be interpolated into the generated bootstrap +config file, which is particularly useful for configs requiring a restart when otherwise applied to a running Redpanda instance. +E.g. `WithBootstrapConfig("config_key", config_value)`, where `config_value` is of type `any`. + ### Container Methods The Redpanda container exposes the following methods: #### KafkaSeedBroker +- Since testcontainers-go :material-tag: v0.20.0 + KafkaSeedBroker returns the seed broker that should be used for connecting to the Kafka API with your Kafka client. It'll be returned in the format: "host:port" - for example: "localhost:55687". @@ -95,6 +162,8 @@ to the Kafka API with your Kafka client. It'll be returned in the format: #### SchemaRegistryAddress +- Since testcontainers-go :material-tag: v0.20.0 + SchemaRegistryAddress returns the address to the schema registry API. This is an HTTP-based API and thus the returned format will be: http://host:port. @@ -105,6 +174,8 @@ is an HTTP-based API and thus the returned format will be: http://host:port. #### AdminAPIAddress +- Since testcontainers-go :material-tag: v0.20.0 + AdminAPIAddress returns the address to the Redpanda Admin API. This is an HTTP-based API and thus the returned format will be: http://host:port. diff --git a/docs/modules/yugabytedb.md b/docs/modules/yugabytedb.md new file mode 100644 index 0000000000..9922c48574 --- /dev/null +++ b/docs/modules/yugabytedb.md @@ -0,0 +1,94 @@ +# YugabyteDB + +Since testcontainers-go :material-tag: v0.34.0 + +## Introduction + +The Testcontainers module for yugabyteDB. + +## Adding this module to your project dependencies + +Please run the following command to add the yugabyteDB module to your Go dependencies: + +``` +go get github.com/testcontainers/testcontainers-go/modules/yugabytedb +``` + +## Usage example + + +[Creating a yugabyteDB container](../../modules/yugabytedb/examples_test.go) inside_block:runyugabyteDBContainer + + +## Module Reference + +### Run function + +The yugabyteDB module exposes one entrypoint function to create the yugabyteDB container, and this function receives three parameters: + +```golang +func Run( + ctx context.Context, + img string, + opts ...testcontainers.ContainerCustomizer, +) (*Container, error) +``` + +- `context.Context`, the Go context. +- `string`, the Docker image to use. +- `testcontainers.ContainerCustomizer`, a variadic argument for passing options. + +### Container Options + +When starting the yugabyteDB container, you can pass options in a variadic way to configure it. + +#### Image + +If you need to set a different yugabyteDB Docker image, you can set a valid Docker image as the second argument in the `Run` function. +E.g. `Run(context.Background(), "yugabytedb/yugabyte")`. + +{% include "../features/common_functional_options.md" %} + +#### Initial Database + +By default the yugabyteDB container will start with a database named `yugabyte` and the default credentials `yugabyte` and `yugabyte`. + +If you need to set a different database, and its credentials, you can use the `WithDatabaseName(dbName string)`, `WithDatabaseUser(dbUser string)` and `WithDatabasePassword(dbPassword string)` options. + +#### Initial Cluster Configuration + +By default the yugabyteDB container will start with a cluster keyspace named `yugabyte` and the default credentials `yugabyte` and `yugabyte`. + +If you need to set a different cluster keyspace, and its credentials, you can use the `WithKeyspace(keyspace string)`, `WithUser(user string)` and `WithPassword(password string)` options. + +### Container Methods + +The yugabyteDB container exposes the following methods: + +#### YSQLConnectionString + +This method returns the connection string for the yugabyteDB container when using +the YSQL query language. +The connection string can then be used to connect to the yugabyteDB container using +a standard PostgreSQL client. + + +[Create a postgres client using the connection string](../../modules/yugabytedb/examples_test.go) block:ExampleContainer_YSQLConnectionString + + +### Usage examples + +#### Usage with YSQL and gocql + +To use the YCQL query language, you need to configure the cluster +with the keyspace, user, and password. + +By default, the yugabyteDB container will start with a cluster keyspace named `yugabyte` and the default credentials `yugabyte` and `yugabyte` but you can change it using the `WithKeyspace`, `WithUser` and `WithPassword` options. + +In order to get the appropriate host and port to connect to the yugabyteDB container, +you can use the `GetHost` and `GetMappedPort` methods on the Container struct. +See the examples below: + + +[Create a yugabyteDB client using the cluster configuration](../../modules/yugabytedb/yugabytedb_test.go) block:TestYugabyteDB_YCQL + \ No newline at end of file diff --git a/docs/quickstart.md b/docs/quickstart.md index 5660e35757..ed6bbfcd4a 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -9,7 +9,7 @@ Please read the [system requirements](../system_requirements/) page before you s ## 2. Install _Testcontainers for Go_ -We use [gomod](https://blog.golang.org/using-go-modules) and you can get it installed via: +We use [go mod](https://blog.golang.org/using-go-modules) and you can get it installed via: ``` go get github.com/testcontainers/testcontainers-go @@ -22,6 +22,8 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) @@ -37,14 +39,8 @@ func TestWithRedis(t *testing.T) { ContainerRequest: req, Started: true, }) - if err != nil { - t.Fatalf("Could not start redis: %s", err) - } - defer func() { - if err := redisC.Terminate(ctx); err != nil { - t.Fatalf("Could not stop redis: %s", err) - } - }() + testcontainers.CleanupContainer(t, redisC) + require.NoError(t, err) } ``` @@ -75,7 +71,8 @@ start, leaving to you the decision about when to start it. All the containers must be removed at some point, otherwise they will run until the host is overloaded. One of the ways we have to clean up is by deferring the -terminated function: `defer redisC.Terminate(ctx)`. +terminated function: `defer testcontainers.TerminateContainer(redisC)` which +automatically handles nil container so is safe to use even in the error case. !!!tip diff --git a/docs/system_requirements/using_podman.md b/docs/system_requirements/using_podman.md index cf65792bbd..4143306901 100644 --- a/docs/system_requirements/using_podman.md +++ b/docs/system_requirements/using_podman.md @@ -27,7 +27,7 @@ func TestSomething(t *testing.T) { req := tc.GenericContainerRequest{ ProviderType: tc.ProviderPodman, ContainerRequest: tc.ContainerRequest{ - Image: "docker.io/nginx:alpine" + Image: "nginx:alpine" }, } diff --git a/examples/nginx/go.mod b/examples/nginx/go.mod index a6f67e793c..a31fbd366e 100644 --- a/examples/nginx/go.mod +++ b/examples/nginx/go.mod @@ -2,7 +2,10 @@ module github.com/testcontainers/testcontainers-go/examples/nginx go 1.22 -require github.com/testcontainers/testcontainers-go v0.33.0 +require ( + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 +) replace github.com/testcontainers/testcontainers-go => ../.. @@ -14,7 +17,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -26,6 +30,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -37,6 +42,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -48,9 +54,10 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/examples/nginx/go.sum b/examples/nginx/go.sum index 85338720c8..447eec4038 100644 --- a/examples/nginx/go.sum +++ b/examples/nginx/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,6 +53,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -78,6 +83,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -89,6 +96,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -122,8 +131,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -145,14 +154,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -172,6 +181,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/nginx/nginx.go b/examples/nginx/nginx.go index fd0e530ea6..6a5718c3e0 100644 --- a/examples/nginx/nginx.go +++ b/examples/nginx/nginx.go @@ -24,21 +24,24 @@ func startContainer(ctx context.Context) (*nginxContainer, error) { ContainerRequest: req, Started: true, }) + var nginxC *nginxContainer + if container != nil { + nginxC = &nginxContainer{Container: container} + } if err != nil { - return nil, err + return nginxC, err } ip, err := container.Host(ctx) if err != nil { - return nil, err + return nginxC, err } mappedPort, err := container.MappedPort(ctx, "80") if err != nil { - return nil, err + return nginxC, err } - uri := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port()) - - return &nginxContainer{Container: container, URI: uri}, nil + nginxC.URI = fmt.Sprintf("http://%s:%s", ip, mappedPort.Port()) + return nginxC, nil } diff --git a/examples/nginx/nginx_test.go b/examples/nginx/nginx_test.go index 3d7b8ada48..fe662daf07 100644 --- a/examples/nginx/nginx_test.go +++ b/examples/nginx/nginx_test.go @@ -4,6 +4,10 @@ import ( "context" "net/http" "testing" + + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" ) func TestIntegrationNginxLatestReturn(t *testing.T) { @@ -14,23 +18,10 @@ func TestIntegrationNginxLatestReturn(t *testing.T) { ctx := context.Background() nginxC, err := startContainer(ctx) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := nginxC.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, nginxC) + require.NoError(t, err) resp, err := http.Get(nginxC.URI) - if err != nil { - t.Fatal(err) - } - - if resp.StatusCode != http.StatusOK { - t.Fatalf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) - } + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) } diff --git a/examples/toxiproxy/go.mod b/examples/toxiproxy/go.mod index 04be7036c1..644c7a9a0d 100644 --- a/examples/toxiproxy/go.mod +++ b/examples/toxiproxy/go.mod @@ -6,7 +6,8 @@ require ( github.com/Shopify/toxiproxy/v2 v2.8.0 github.com/go-redis/redis/v8 v8.11.5 github.com/google/uuid v1.6.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -18,7 +19,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -30,6 +32,7 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -41,6 +44,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -52,11 +56,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/examples/toxiproxy/go.sum b/examples/toxiproxy/go.sum index c62c0ac532..dbbd37fcaf 100644 --- a/examples/toxiproxy/go.sum +++ b/examples/toxiproxy/go.sum @@ -18,8 +18,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -62,6 +63,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -94,6 +99,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -105,6 +112,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -138,8 +147,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -161,14 +170,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -188,6 +197,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/examples/toxiproxy/redis.go b/examples/toxiproxy/redis.go index ead526773d..c66e52550f 100644 --- a/examples/toxiproxy/redis.go +++ b/examples/toxiproxy/redis.go @@ -27,9 +27,9 @@ func setupRedis(ctx context.Context, network string, networkAlias []string) (*re ContainerRequest: req, Started: true, }) - if err != nil { - return nil, err + var nginxC *redisContainer + if container != nil { + nginxC = &redisContainer{Container: container} } - - return &redisContainer{Container: container}, nil + return nginxC, err } diff --git a/examples/toxiproxy/toxiproxy.go b/examples/toxiproxy/toxiproxy.go index e7903a9f99..1a226e8c61 100644 --- a/examples/toxiproxy/toxiproxy.go +++ b/examples/toxiproxy/toxiproxy.go @@ -31,21 +31,25 @@ func startContainer(ctx context.Context, network string, networkAlias []string) ContainerRequest: req, Started: true, }) + var toxiC *toxiproxyContainer + if container != nil { + toxiC = &toxiproxyContainer{Container: container} + } if err != nil { - return nil, err + return toxiC, err } mappedPort, err := container.MappedPort(ctx, "8474") if err != nil { - return nil, err + return toxiC, err } hostIP, err := container.Host(ctx) if err != nil { - return nil, err + return toxiC, err } - uri := fmt.Sprintf("%s:%s", hostIP, mappedPort.Port()) + toxiC.URI = fmt.Sprintf("%s:%s", hostIP, mappedPort.Port()) - return &toxiproxyContainer{Container: container, URI: uri}, nil + return toxiC, nil } diff --git a/examples/toxiproxy/toxiproxy_test.go b/examples/toxiproxy/toxiproxy_test.go index 0cb3f05320..c372d739b8 100644 --- a/examples/toxiproxy/toxiproxy_test.go +++ b/examples/toxiproxy/toxiproxy_test.go @@ -9,7 +9,9 @@ import ( toxiproxy "github.com/Shopify/toxiproxy/v2/client" "github.com/go-redis/redis/v8" "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/network" ) @@ -17,63 +19,37 @@ func TestToxiproxy(t *testing.T) { ctx := context.Background() newNetwork, err := network.New(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + testcontainers.CleanupNetwork(t, newNetwork) networkName := newNetwork.Name toxiproxyContainer, err := startContainer(ctx, networkName, []string{"toxiproxy"}) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, toxiproxyContainer) + require.NoError(t, err) redisContainer, err := setupRedis(ctx, networkName, []string{"redis"}) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := toxiproxyContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - if err := redisContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - if err := newNetwork.Remove(ctx); err != nil { - t.Fatalf("failed to terminate network: %s", err) - } - }) + testcontainers.CleanupContainer(t, redisContainer) + require.NoError(t, err) toxiproxyClient := toxiproxy.NewClient(toxiproxyContainer.URI) proxy, err := toxiproxyClient.CreateProxy("redis", "0.0.0.0:8666", "redis:6379") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) toxiproxyProxyPort, err := toxiproxyContainer.MappedPort(ctx, "8666") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) toxiproxyProxyHostIP, err := toxiproxyContainer.Host(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) redisUri := fmt.Sprintf("redis://%s:%s?read_timeout=2s", toxiproxyProxyHostIP, toxiproxyProxyPort.Port()) options, err := redis.ParseURL(redisUri) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) redisClient := redis.NewClient(options) + defer func() { - err := flushRedis(ctx, *redisClient) - if err != nil { - t.Fatal(err) - } + require.NoError(t, flushRedis(ctx, *redisClient)) }() // Set data @@ -81,28 +57,18 @@ func TestToxiproxy(t *testing.T) { value := "Cabbage Biscuits" ttl, _ := time.ParseDuration("2h") err = redisClient.Set(ctx, key, value, ttl).Err() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = proxy.AddToxic("latency_down", "latency", "downstream", 1.0, toxiproxy.Attributes{ "latency": 1000, "jitter": 100, }) - if err != nil { - return - } + require.NoError(t, err) // Get data savedValue, err := redisClient.Get(ctx, key).Result() - if err != nil { - t.Fatal(err) - } - - // perform assertions - if savedValue != value { - t.Fatalf("Expected value %s. Got %s.", savedValue, value) - } + require.NoError(t, err) + require.Equal(t, value, savedValue) } func flushRedis(ctx context.Context, client redis.Client) error { diff --git a/exec/processor.go b/exec/processor.go index 2b79583609..9c852fb5aa 100644 --- a/exec/processor.go +++ b/exec/processor.go @@ -2,7 +2,9 @@ package exec import ( "bytes" + "fmt" "io" + "sync" "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/stdcopy" @@ -60,6 +62,43 @@ func WithEnv(env []string) ProcessOption { }) } +// safeBuffer is a goroutine safe buffer. +type safeBuffer struct { + mtx sync.Mutex + buf bytes.Buffer + err error +} + +// Error sets an error for the next read. +func (sb *safeBuffer) Error(err error) { + sb.mtx.Lock() + defer sb.mtx.Unlock() + + sb.err = err +} + +// Write writes p to the buffer. +// It is safe for concurrent use by multiple goroutines. +func (sb *safeBuffer) Write(p []byte) (n int, err error) { + sb.mtx.Lock() + defer sb.mtx.Unlock() + + return sb.buf.Write(p) +} + +// Read reads up to len(p) bytes into p from the buffer. +// It is safe for concurrent use by multiple goroutines. +func (sb *safeBuffer) Read(p []byte) (n int, err error) { + sb.mtx.Lock() + defer sb.mtx.Unlock() + + if sb.err != nil { + return 0, sb.err + } + + return sb.buf.Read(p) +} + // Multiplexed returns a [ProcessOption] that configures the command execution // to combine stdout and stderr into a single stream without Docker's multiplexing headers. func Multiplexed() ProcessOption { @@ -73,13 +112,14 @@ func Multiplexed() ProcessOption { done := make(chan struct{}) - var outBuff bytes.Buffer - var errBuff bytes.Buffer + var outBuff safeBuffer + var errBuff safeBuffer go func() { + defer close(done) if _, err := stdcopy.StdCopy(&outBuff, &errBuff, opts.Reader); err != nil { + outBuff.Error(fmt.Errorf("copying output: %w", err)) return } - close(done) }() <-done diff --git a/file_test.go b/file_test.go index 1128304df7..edf12af3b3 100644 --- a/file_test.go +++ b/file_test.go @@ -6,6 +6,7 @@ import ( "archive/tar" "bytes" "compress/gzip" + "errors" "fmt" "io" "os" @@ -37,7 +38,7 @@ func Test_IsDir(t *testing.T) { { filepath: "foobar.doc", expected: false, - err: fmt.Errorf("does not exist"), + err: errors.New("does not exist"), }, } @@ -78,34 +79,24 @@ func Test_TarDir(t *testing.T) { } buff, err := tarDir(src, 0o755) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) tmpDir := filepath.Join(t.TempDir(), "subfolder") err = untar(tmpDir, bytes.NewReader(buff.Bytes())) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) srcFiles, err := os.ReadDir(src) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for _, srcFile := range srcFiles { if srcFile.IsDir() { continue } srcBytes, err := os.ReadFile(filepath.Join(src, srcFile.Name())) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) untarBytes, err := os.ReadFile(filepath.Join(tmpDir, "testdata", srcFile.Name())) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, srcBytes, untarBytes) } }) @@ -114,28 +105,20 @@ func Test_TarDir(t *testing.T) { func Test_TarFile(t *testing.T) { b, err := os.ReadFile(filepath.Join(".", "testdata", "Dockerfile")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) buff, err := tarFile("Docker.file", func(tw io.Writer) error { _, err := tw.Write(b) return err }, int64(len(b)), 0o755) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) tmpDir := t.TempDir() err = untar(tmpDir, bytes.NewReader(buff.Bytes())) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) untarBytes, err := os.ReadFile(filepath.Join(tmpDir, "Docker.file")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, b, untarBytes) } diff --git a/from_dockerfile_test.go b/from_dockerfile_test.go index fc1d4052ea..75f80537d2 100644 --- a/from_dockerfile_test.go +++ b/from_dockerfile_test.go @@ -12,15 +12,12 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/image" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBuildImageFromDockerfile(t *testing.T) { provider, err := NewDockerProvider() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer provider.Close() cli := provider.Client() @@ -38,7 +35,7 @@ func TestBuildImageFromDockerfile(t *testing.T) { // } }) require.NoError(t, err) - assert.Equal(t, "test-repo:test-tag", tag) + require.Equal(t, "test-repo:test-tag", tag) _, _, err = cli.ImageInspectWithRaw(ctx, tag) require.NoError(t, err) @@ -48,17 +45,13 @@ func TestBuildImageFromDockerfile(t *testing.T) { Force: true, PruneChildren: true, }) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) }) } func TestBuildImageFromDockerfile_NoRepo(t *testing.T) { provider, err := NewDockerProvider() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer provider.Close() cli := provider.Client() @@ -73,7 +66,7 @@ func TestBuildImageFromDockerfile_NoRepo(t *testing.T) { }, }) require.NoError(t, err) - assert.True(t, strings.HasPrefix(tag, "test-repo:")) + require.True(t, strings.HasPrefix(tag, "test-repo:")) _, _, err = cli.ImageInspectWithRaw(ctx, tag) require.NoError(t, err) @@ -83,9 +76,7 @@ func TestBuildImageFromDockerfile_NoRepo(t *testing.T) { Force: true, PruneChildren: true, }) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) }) } @@ -102,20 +93,18 @@ func TestBuildImageFromDockerfile_BuildError(t *testing.T) { Context: filepath.Join(".", "testdata"), }, } - _, err = GenericContainer(ctx, GenericContainerRequest{ + ctr, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, }) - + CleanupContainer(t, ctr) require.EqualError(t, err, `create container: build image: The command '/bin/sh -c exit 1' returned a non-zero code: 1`) } func TestBuildImageFromDockerfile_NoTag(t *testing.T) { provider, err := NewDockerProvider() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer provider.Close() cli := provider.Client() @@ -130,7 +119,7 @@ func TestBuildImageFromDockerfile_NoTag(t *testing.T) { }, }) require.NoError(t, err) - assert.True(t, strings.HasSuffix(tag, ":test-tag")) + require.True(t, strings.HasSuffix(tag, ":test-tag")) _, _, err = cli.ImageInspectWithRaw(ctx, tag) require.NoError(t, err) @@ -140,9 +129,7 @@ func TestBuildImageFromDockerfile_NoTag(t *testing.T) { Force: true, PruneChildren: true, }) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) }) } @@ -153,10 +140,9 @@ func TestBuildImageFromDockerfile_Target(t *testing.T) { c, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: ContainerRequest{ FromDockerfile: FromDockerfile{ - Context: "testdata", - Dockerfile: "target.Dockerfile", - PrintBuildLog: true, - KeepImage: false, + Context: "testdata", + Dockerfile: "target.Dockerfile", + KeepImage: false, BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) { buildOptions.Target = fmt.Sprintf("target%d", i) }, @@ -164,6 +150,7 @@ func TestBuildImageFromDockerfile_Target(t *testing.T) { }, Started: true, }) + CleanupContainer(t, c) require.NoError(t, err) r, err := c.Logs(ctx) @@ -171,12 +158,7 @@ func TestBuildImageFromDockerfile_Target(t *testing.T) { logs, err := io.ReadAll(r) require.NoError(t, err) - - assert.Equal(t, fmt.Sprintf("target%d\n\n", i), string(logs)) - - t.Cleanup(func() { - require.NoError(t, c.Terminate(ctx)) - }) + require.Equal(t, fmt.Sprintf("target%d\n\n", i), string(logs)) } } @@ -187,10 +169,9 @@ func ExampleGenericContainer_buildFromDockerfile() { c, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: ContainerRequest{ FromDockerfile: FromDockerfile{ - Context: "testdata", - Dockerfile: "target.Dockerfile", - PrintBuildLog: true, - KeepImage: false, + Context: "testdata", + Dockerfile: "target.Dockerfile", + KeepImage: false, BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) { buildOptions.Target = "target2" }, @@ -199,18 +180,26 @@ func ExampleGenericContainer_buildFromDockerfile() { Started: true, }) // } + defer func() { + if err := TerminateContainer(c); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() if err != nil { - log.Fatalf("failed to start container: %v", err) + log.Printf("failed to start container: %v", err) + return } r, err := c.Logs(ctx) if err != nil { - log.Fatalf("failed to get logs: %v", err) + log.Printf("failed to get logs: %v", err) + return } logs, err := io.ReadAll(r) if err != nil { - log.Fatalf("failed to read logs: %v", err) + log.Printf("failed to read logs: %v", err) + return } fmt.Println(string(logs)) @@ -223,13 +212,12 @@ func TestBuildImageFromDockerfile_TargetDoesNotExist(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - _, err := GenericContainer(ctx, GenericContainerRequest{ + ctr, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: ContainerRequest{ FromDockerfile: FromDockerfile{ - Context: "testdata", - Dockerfile: "target.Dockerfile", - PrintBuildLog: true, - KeepImage: false, + Context: "testdata", + Dockerfile: "target.Dockerfile", + KeepImage: false, BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) { buildOptions.Target = "target-foo" }, @@ -237,5 +225,6 @@ func TestBuildImageFromDockerfile_TargetDoesNotExist(t *testing.T) { }, Started: true, }) + CleanupContainer(t, ctr) require.Error(t, err) } diff --git a/generate.go b/generate.go new file mode 100644 index 0000000000..19ae49695d --- /dev/null +++ b/generate.go @@ -0,0 +1,3 @@ +package testcontainers + +//go:generate mockery diff --git a/generic.go b/generic.go index 9052287b51..fd13a607de 100644 --- a/generic.go +++ b/generic.go @@ -101,7 +101,17 @@ type GenericProvider interface { ImageProvider } -// GenericLabels returns a map of labels that can be used to identify containers created by this library +// GenericLabels returns a map of labels that can be used to identify resources +// created by this library. This includes the standard LabelSessionID if the +// reaper is enabled, otherwise this is excluded to prevent resources being +// incorrectly reaped. func GenericLabels() map[string]string { return core.DefaultLabels(core.SessionID()) } + +// AddGenericLabels adds the generic labels to target. +func AddGenericLabels(target map[string]string) { + for k, v := range GenericLabels() { + target[k] = v + } +} diff --git a/generic_test.go b/generic_test.go index 9116fd0f65..7c0de2a246 100644 --- a/generic_test.go +++ b/generic_test.go @@ -5,7 +5,6 @@ import ( "errors" "os" "os/exec" - "strings" "sync" "testing" "time" @@ -38,7 +37,7 @@ func TestGenericReusableContainer(t *testing.T) { }) require.NoError(t, err) require.True(t, n1.IsRunning()) - terminateContainerOnEnd(t, ctx, n1) + CleanupContainer(t, n1) copiedFileName := "hello_copy.sh" err = n1.CopyFileToContainer(ctx, "./testdata/hello.sh", "/"+copiedFileName, 700) @@ -123,7 +122,7 @@ func TestGenericContainerShouldReturnRefOnError(t *testing.T) { }) require.Error(t, err) require.NotNil(t, c) - terminateContainerOnEnd(t, context.Background(), c) + CleanupContainer(t, c) } func TestGenericReusableContainerInSubprocess(t *testing.T) { @@ -138,8 +137,8 @@ func TestGenericReusableContainerInSubprocess(t *testing.T) { t.Log(output) // check is reuse container with WaitingFor work correctly. - require.True(t, strings.Contains(output, "⏳ Waiting for container id")) - require.True(t, strings.Contains(output, "🔔 Container is ready")) + require.Contains(t, output, "⏳ Waiting for container id") + require.Contains(t, output, "🔔 Container is ready") }() } @@ -157,13 +156,18 @@ func TestGenericReusableContainerInSubprocess(t *testing.T) { require.NoError(t, err) require.Len(t, ctrs, 1) - nginxC, err := containerFromDockerResponse(context.Background(), ctrs[0]) + provider, err := NewDockerProvider() require.NoError(t, err) - terminateContainerOnEnd(t, context.Background(), nginxC) + provider.SetClient(cli) + + nginxC, err := provider.ContainerFromType(context.Background(), ctrs[0]) + CleanupContainer(t, nginxC) + require.NoError(t, err) } func createReuseContainerInSubprocess(t *testing.T) string { + t.Helper() // force verbosity in subprocesses, so that the output is printed cmd := exec.Command(os.Args[0], "-test.run=TestHelperContainerStarterProcess", "-test.v=true") cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1") diff --git a/go.mod b/go.mod index 8e9b20a12a..78b747b76e 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( dario.cat/mergo v1.0.0 github.com/cenkalti/backoff/v4 v4.2.1 github.com/containerd/platforms v0.2.1 - github.com/cpuguy83/dockercfg v0.3.1 + github.com/cpuguy83/dockercfg v0.3.2 github.com/docker/docker v27.1.1+incompatible github.com/docker/go-connections v0.5.0 github.com/google/uuid v1.6.0 @@ -16,8 +16,8 @@ require ( github.com/opencontainers/image-spec v1.1.0 github.com/shirou/gopsutil/v3 v3.23.12 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.22.0 - golang.org/x/sys v0.21.0 + golang.org/x/crypto v0.28.0 + golang.org/x/sys v0.26.0 ) require ( diff --git a/go.sum b/go.sum index ef44f5f91c..38497f9611 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -137,8 +137,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -160,14 +160,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/image_substitutors_test.go b/image_substitutors_test.go index 8054ebf96c..c9d6aee244 100644 --- a/image_substitutors_test.go +++ b/image_substitutors_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/internal/config" ) @@ -12,25 +14,17 @@ func TestCustomHubSubstitutor(t *testing.T) { s := NewCustomHubSubstitutor("quay.io") img, err := s.Substitute("foo/foo:latest") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if img != "quay.io/foo/foo:latest" { - t.Errorf("expected quay.io/foo/foo:latest, got %s", img) - } + require.Equalf(t, "quay.io/foo/foo:latest", img, "expected quay.io/foo/foo:latest, got %s", img) }) t.Run("should not substitute the image if it is already using the provided hub", func(t *testing.T) { s := NewCustomHubSubstitutor("quay.io") img, err := s.Substitute("quay.io/foo/foo:latest") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if img != "quay.io/foo/foo:latest" { - t.Errorf("expected quay.io/foo/foo:latest, got %s", img) - } + require.Equalf(t, "quay.io/foo/foo:latest", img, "expected quay.io/foo/foo:latest, got %s", img) }) t.Run("should not substitute the image if hub image name prefix config exist", func(t *testing.T) { t.Cleanup(config.Reset) @@ -39,13 +33,9 @@ func TestCustomHubSubstitutor(t *testing.T) { s := NewCustomHubSubstitutor("quay.io") img, err := s.Substitute("foo/foo:latest") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if img != "foo/foo:latest" { - t.Errorf("expected foo/foo:latest, got %s", img) - } + require.Equalf(t, "foo/foo:latest", img, "expected foo/foo:latest, got %s", img) }) } @@ -55,38 +45,26 @@ func TestPrependHubRegistrySubstitutor(t *testing.T) { s := newPrependHubRegistry("my-registry") img, err := s.Substitute("foo:latest") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if img != "my-registry/foo:latest" { - t.Errorf("expected my-registry/foo, got %s", img) - } + require.Equalf(t, "my-registry/foo:latest", img, "expected my-registry/foo, got %s", img) }) t.Run("image with user", func(t *testing.T) { s := newPrependHubRegistry("my-registry") img, err := s.Substitute("user/foo:latest") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if img != "my-registry/user/foo:latest" { - t.Errorf("expected my-registry/foo, got %s", img) - } + require.Equalf(t, "my-registry/user/foo:latest", img, "expected my-registry/foo, got %s", img) }) t.Run("image with organization and user", func(t *testing.T) { s := newPrependHubRegistry("my-registry") img, err := s.Substitute("org/user/foo:latest") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if img != "my-registry/org/user/foo:latest" { - t.Errorf("expected my-registry/org/foo:latest, got %s", img) - } + require.Equalf(t, "my-registry/org/user/foo:latest", img, "expected my-registry/org/foo:latest, got %s", img) }) }) @@ -95,39 +73,27 @@ func TestPrependHubRegistrySubstitutor(t *testing.T) { s := newPrependHubRegistry("my-registry") img, err := s.Substitute("quay.io/foo:latest") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if img != "quay.io/foo:latest" { - t.Errorf("expected quay.io/foo:latest, got %s", img) - } + require.Equalf(t, "quay.io/foo:latest", img, "expected quay.io/foo:latest, got %s", img) }) - t.Run("explicitly including docker.io", func(t *testing.T) { + t.Run("explicitly including registry.hub.docker.com/library", func(t *testing.T) { s := newPrependHubRegistry("my-registry") - img, err := s.Substitute("docker.io/foo:latest") - if err != nil { - t.Fatal(err) - } + img, err := s.Substitute("registry.hub.docker.com/library/foo:latest") + require.NoError(t, err) - if img != "docker.io/foo:latest" { - t.Errorf("expected docker.io/foo:latest, got %s", img) - } + require.Equalf(t, "registry.hub.docker.com/library/foo:latest", img, "expected registry.hub.docker.com/library/foo:latest, got %s", img) }) t.Run("explicitly including registry.hub.docker.com", func(t *testing.T) { s := newPrependHubRegistry("my-registry") img, err := s.Substitute("registry.hub.docker.com/foo:latest") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if img != "registry.hub.docker.com/foo:latest" { - t.Errorf("expected registry.hub.docker.com/foo:latest, got %s", img) - } + require.Equalf(t, "registry.hub.docker.com/foo:latest", img, "expected registry.hub.docker.com/foo:latest, got %s", img) }) }) } @@ -149,17 +115,12 @@ func TestSubstituteBuiltImage(t *testing.T) { t.Run("should not use the properties prefix on built images", func(t *testing.T) { config.Reset() c, err := GenericContainer(context.Background(), req) - if err != nil { - t.Fatal(err) - } + CleanupContainer(t, c) + require.NoError(t, err) json, err := c.Inspect(context.Background()) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if json.Config.Image != "my-registry/my-repo:my-image" { - t.Errorf("expected my-registry/my-repo:my-image, got %s", json.Config.Image) - } + require.Equalf(t, "my-registry/my-repo:my-image", json.Config.Image, "expected my-registry/my-repo:my-image, got %s", json.Config.Image) }) } diff --git a/image_test.go b/image_test.go index 795a521b29..b5a95640a8 100644 --- a/image_test.go +++ b/image_test.go @@ -6,6 +6,8 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/internal/core" ) @@ -13,9 +15,7 @@ func TestImageList(t *testing.T) { t.Setenv("DOCKER_HOST", core.MustExtractDockerHost(context.Background())) provider, err := ProviderDocker.GetProvider() - if err != nil { - t.Fatalf("failed to get provider %v", err) - } + require.NoErrorf(t, err, "failed to get provider") defer func() { _ = provider.Close() @@ -25,23 +25,14 @@ func TestImageList(t *testing.T) { Image: "redis:latest", } - container, err := provider.CreateContainer(context.Background(), req) - if err != nil { - t.Fatalf("creating test container %v", err) - } - - defer func() { - _ = container.Terminate(context.Background()) - }() + ctr, err := provider.CreateContainer(context.Background(), req) + CleanupContainer(t, ctr) + require.NoErrorf(t, err, "creating test container") images, err := provider.ListImages(context.Background()) - if err != nil { - t.Fatalf("listing images %v", err) - } + require.NoErrorf(t, err, "listing images") - if len(images) == 0 { - t.Fatal("no images retrieved") - } + require.NotEmptyf(t, images, "no images retrieved") // look if the list contains the container image for _, img := range images { @@ -57,9 +48,7 @@ func TestSaveImages(t *testing.T) { t.Setenv("DOCKER_HOST", core.MustExtractDockerHost(context.Background())) provider, err := ProviderDocker.GetProvider() - if err != nil { - t.Fatalf("failed to get provider %v", err) - } + require.NoErrorf(t, err, "failed to get provider") defer func() { _ = provider.Close() @@ -69,27 +58,16 @@ func TestSaveImages(t *testing.T) { Image: "redis:latest", } - container, err := provider.CreateContainer(context.Background(), req) - if err != nil { - t.Fatalf("creating test container %v", err) - } - - defer func() { - _ = container.Terminate(context.Background()) - }() + ctr, err := provider.CreateContainer(context.Background(), req) + CleanupContainer(t, ctr) + require.NoErrorf(t, err, "creating test container") output := filepath.Join(t.TempDir(), "images.tar") err = provider.SaveImages(context.Background(), output, req.Image) - if err != nil { - t.Fatalf("saving image %q: %v", req.Image, err) - } + require.NoErrorf(t, err, "saving image %q", req.Image) info, err := os.Stat(output) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if info.Size() == 0 { - t.Fatalf("output file is empty") - } + require.NotZerof(t, info.Size(), "output file is empty") } diff --git a/internal/config/config.go b/internal/config/config.go index a172fa3a16..85be6acd86 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,7 +11,7 @@ import ( "github.com/magiconair/properties" ) -const ReaperDefaultImage = "testcontainers/ryuk:0.9.0" +const ReaperDefaultImage = "testcontainers/ryuk:0.11.0" var ( tcConfig Config @@ -68,17 +68,17 @@ type Config struct { // RyukReconnectionTimeout is the time to wait before attempting to reconnect to the Garbage Collector container. // - // Environment variable: TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT + // Environment variable: RYUK_RECONNECTION_TIMEOUT RyukReconnectionTimeout time.Duration `properties:"ryuk.reconnection.timeout,default=10s"` // RyukConnectionTimeout is the time to wait before timing out when connecting to the Garbage Collector container. // - // Environment variable: TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT + // Environment variable: RYUK_CONNECTION_TIMEOUT RyukConnectionTimeout time.Duration `properties:"ryuk.connection.timeout,default=1m"` // RyukVerbose is a flag to enable or disable verbose logging for the Garbage Collector. // - // Environment variable: TESTCONTAINERS_RYUK_VERBOSE + // Environment variable: RYUK_VERBOSE RyukVerbose bool `properties:"ryuk.verbose,default=false"` // TestcontainersHost is the address of the Testcontainers host. @@ -126,17 +126,17 @@ func read() Config { config.RyukPrivileged = ryukPrivilegedEnv == "true" } - ryukVerboseEnv := os.Getenv("TESTCONTAINERS_RYUK_VERBOSE") + ryukVerboseEnv := readTestcontainersEnv("RYUK_VERBOSE") if parseBool(ryukVerboseEnv) { config.RyukVerbose = ryukVerboseEnv == "true" } - ryukReconnectionTimeoutEnv := os.Getenv("TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT") + ryukReconnectionTimeoutEnv := readTestcontainersEnv("RYUK_RECONNECTION_TIMEOUT") if timeout, err := time.ParseDuration(ryukReconnectionTimeoutEnv); err == nil { config.RyukReconnectionTimeout = timeout } - ryukConnectionTimeoutEnv := os.Getenv("TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT") + ryukConnectionTimeoutEnv := readTestcontainersEnv("RYUK_CONNECTION_TIMEOUT") if timeout, err := time.ParseDuration(ryukConnectionTimeoutEnv); err == nil { config.RyukConnectionTimeout = timeout } @@ -168,3 +168,18 @@ func parseBool(input string) bool { _, err := strconv.ParseBool(input) return err == nil } + +// readTestcontainersEnv reads the environment variable with the given name. +// It checks for the environment variable with the given name first, and then +// checks for the environment variable with the given name prefixed with "TESTCONTAINERS_". +func readTestcontainersEnv(envVar string) string { + value := os.Getenv(envVar) + if value != "" { + return value + } + + // TODO: remove this prefix after the next major release + const prefix string = "TESTCONTAINERS_" + + return os.Getenv(prefix + envVar) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index efd2e054e6..591fcff11c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,13 +1,13 @@ package config import ( - "fmt" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -19,12 +19,13 @@ const ( // unset environment variables to avoid side effects // execute this function before each test func resetTestEnv(t *testing.T) { + t.Helper() t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "") t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "") t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "") - t.Setenv("TESTCONTAINERS_RYUK_VERBOSE", "") - t.Setenv("TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT", "") - t.Setenv("TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT", "") + t.Setenv("RYUK_VERBOSE", "") + t.Setenv("RYUK_RECONNECTION_TIMEOUT", "") + t.Setenv("RYUK_CONNECTION_TIMEOUT", "") } func TestReadConfig(t *testing.T) { @@ -45,7 +46,7 @@ func TestReadConfig(t *testing.T) { Host: "", // docker socket is empty at the properties file } - assert.Equal(t, expected, config) + require.Equal(t, expected, config) t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "false") @@ -76,8 +77,8 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", defaultHubPrefix) t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") - t.Setenv("TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT", "13s") - t.Setenv("TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT", "12s") + t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") + t.Setenv("RYUK_CONNECTION_TIMEOUT", "12s") config := read() @@ -124,9 +125,9 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", defaultHubPrefix) t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") - t.Setenv("TESTCONTAINERS_RYUK_VERBOSE", "true") - t.Setenv("TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT", "13s") - t.Setenv("TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT", "12s") + t.Setenv("RYUK_VERBOSE", "true") + t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") + t.Setenv("RYUK_CONNECTION_TIMEOUT", "12s") config := read() expected := Config{ @@ -277,8 +278,8 @@ func TestReadTCConfig(t *testing.T) { "With Ryuk container timeouts configured using env vars", ``, map[string]string{ - "TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT": "13s", - "TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT": "12s", + "RYUK_RECONNECTION_TIMEOUT": "13s", + "RYUK_CONNECTION_TIMEOUT": "12s", }, Config{ RyukReconnectionTimeout: 13 * time.Second, @@ -290,8 +291,8 @@ func TestReadTCConfig(t *testing.T) { `ryuk.connection.timeout=22s ryuk.reconnection.timeout=23s`, map[string]string{ - "TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT": "13s", - "TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT": "12s", + "RYUK_RECONNECTION_TIMEOUT": "13s", + "RYUK_CONNECTION_TIMEOUT": "12s", }, Config{ RyukReconnectionTimeout: 13 * time.Second, @@ -376,7 +377,7 @@ func TestReadTCConfig(t *testing.T) { "With Ryuk verbose using an env var and properties. Env var wins (0)", `ryuk.verbose=true`, map[string]string{ - "TESTCONTAINERS_RYUK_VERBOSE": "true", + "RYUK_VERBOSE": "true", }, Config{ RyukVerbose: true, @@ -388,7 +389,7 @@ func TestReadTCConfig(t *testing.T) { "With Ryuk verbose using an env var and properties. Env var wins (1)", `ryuk.verbose=false`, map[string]string{ - "TESTCONTAINERS_RYUK_VERBOSE": "true", + "RYUK_VERBOSE": "true", }, Config{ RyukVerbose: true, @@ -400,7 +401,7 @@ func TestReadTCConfig(t *testing.T) { "With Ryuk verbose using an env var and properties. Env var wins (2)", `ryuk.verbose=true`, map[string]string{ - "TESTCONTAINERS_RYUK_VERBOSE": "false", + "RYUK_VERBOSE": "false", }, defaultConfig, }, @@ -408,7 +409,7 @@ func TestReadTCConfig(t *testing.T) { "With Ryuk verbose using an env var and properties. Env var wins (3)", `ryuk.verbose=false`, map[string]string{ - "TESTCONTAINERS_RYUK_VERBOSE": "false", + "RYUK_VERBOSE": "false", }, defaultConfig, }, @@ -517,17 +518,15 @@ func TestReadTCConfig(t *testing.T) { }, } for _, tt := range tests { - t.Run(fmt.Sprintf(tt.name), func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) t.Setenv("USERPROFILE", tmpDir) // Windows support for k, v := range tt.env { t.Setenv(k, v) } - if err := os.WriteFile(filepath.Join(tmpDir, ".testcontainers.properties"), []byte(tt.content), 0o600); err != nil { - t.Errorf("Failed to create the file: %v", err) - return - } + err := os.WriteFile(filepath.Join(tmpDir, ".testcontainers.properties"), []byte(tt.content), 0o600) + require.NoErrorf(t, err, "Failed to create the file") // config := read() diff --git a/internal/core/bootstrap.go b/internal/core/bootstrap.go index cf06dde7e2..1c45297704 100644 --- a/internal/core/bootstrap.go +++ b/internal/core/bootstrap.go @@ -2,6 +2,7 @@ package core import ( "crypto/sha256" + "encoding/hex" "fmt" "os" @@ -89,7 +90,7 @@ func init() { return } - sessionID = fmt.Sprintf("%x", hasher.Sum(nil)) + sessionID = hex.EncodeToString(hasher.Sum(nil)) } func ProcessID() string { diff --git a/internal/core/docker_host_test.go b/internal/core/docker_host_test.go index eceee573fc..6faac45776 100644 --- a/internal/core/docker_host_test.go +++ b/internal/core/docker_host_test.go @@ -2,7 +2,7 @@ package core import ( "context" - "fmt" + "errors" "os" "path/filepath" "testing" @@ -46,10 +46,11 @@ func testCallbackCheckPassing(_ context.Context, _ string) error { } func testCallbackCheckError(_ context.Context, _ string) error { - return fmt.Errorf("could not check the Docker host") + return errors.New("could not check the Docker host") } func mockCallbackCheck(t *testing.T, fn func(_ context.Context, _ string) error) { + t.Helper() oldCheck := dockerHostCheck dockerHostCheck = fn t.Cleanup(func() { @@ -73,7 +74,7 @@ func TestExtractDockerHost(t *testing.T) { host := MustExtractDockerHost(context.Background()) - assert.Equal(t, expected, host) + require.Equal(t, expected, host) t.Setenv("DOCKER_HOST", "/path/to/another/docker.sock") @@ -192,7 +193,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := testcontainersHostFromProperties(context.Background()) require.ErrorIs(t, err, ErrTestcontainersHostNotSetInProperties) - assert.Empty(t, socket) + require.Empty(t, socket) }) t.Run("DOCKER_HOST is set", func(t *testing.T) { @@ -212,7 +213,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerHostFromEnv(context.Background()) require.ErrorIs(t, err, ErrDockerHostNotSet) - assert.Empty(t, socket) + require.Empty(t, socket) }) t.Run("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE is set", func(t *testing.T) { @@ -236,7 +237,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerSocketOverridePath() require.ErrorIs(t, err, ErrDockerSocketOverrideNotSet) - assert.Empty(t, socket) + require.Empty(t, socket) }) t.Run("Context sets the Docker socket", func(t *testing.T) { @@ -252,7 +253,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerHostFromContext(context.WithValue(ctx, DockerHostContextKey, "path-to-docker-sock")) require.Error(t, err) - assert.Empty(t, socket) + require.Empty(t, socket) }) t.Run("Context sets a malformed schema for the Docker socket", func(t *testing.T) { @@ -260,7 +261,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerHostFromContext(context.WithValue(ctx, DockerHostContextKey, "http://example.com/docker.sock")) require.ErrorIs(t, err, ErrNoUnixSchema) - assert.Empty(t, socket) + require.Empty(t, socket) }) t.Run("Docker socket exists", func(t *testing.T) { @@ -289,7 +290,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerHostFromProperties(context.Background()) require.ErrorIs(t, err, ErrDockerSocketNotSetInProperties) - assert.Empty(t, socket) + require.Empty(t, socket) }) t.Run("Docker socket does not exist", func(t *testing.T) { @@ -297,7 +298,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerSocketPath(context.Background()) require.ErrorIs(t, err, ErrSocketNotFoundInPath) - assert.Empty(t, socket) + require.Empty(t, socket) }) }) } @@ -515,10 +516,12 @@ func createTmpDockerSocket(parent string) error { // setupDockerHostNotFound sets up the environment for the test case where the DOCKER_HOST environment variable is // already set (e.g. rootless docker) therefore we need to unset it before the test func setupDockerHostNotFound(t *testing.T) { + t.Helper() t.Setenv("DOCKER_HOST", "") } func setupDockerSocket(t *testing.T) string { + t.Helper() t.Cleanup(func() { DockerSocketPath = originalDockerSocketPath DockerSocketPathWithSchema = originalDockerSocketPathWithSchema @@ -536,6 +539,7 @@ func setupDockerSocket(t *testing.T) string { } func setupDockerSocketNotFound(t *testing.T) { + t.Helper() t.Cleanup(func() { DockerSocketPath = originalDockerSocketPath DockerSocketPathWithSchema = originalDockerSocketPathWithSchema @@ -548,6 +552,7 @@ func setupDockerSocketNotFound(t *testing.T) { } func setupTestcontainersProperties(t *testing.T, content string) { + t.Helper() t.Cleanup(func() { // reset the properties file after the test config.Reset() @@ -562,8 +567,6 @@ func setupTestcontainersProperties(t *testing.T, content string) { t.Setenv("HOME", homeDir) t.Setenv("USERPROFILE", homeDir) // Windows support - if err := os.WriteFile(filepath.Join(homeDir, ".testcontainers.properties"), []byte(content), 0o600); err != nil { - t.Errorf("Failed to create the file: %v", err) - return - } + err = os.WriteFile(filepath.Join(homeDir, ".testcontainers.properties"), []byte(content), 0o600) + require.NoErrorf(t, err, "Failed to create the file") } diff --git a/internal/core/docker_rootless.go b/internal/core/docker_rootless.go index b8e0f6e17e..70cdebf240 100644 --- a/internal/core/docker_rootless.go +++ b/internal/core/docker_rootless.go @@ -3,11 +3,11 @@ package core import ( "context" "errors" - "fmt" "net/url" "os" "path/filepath" "runtime" + "strconv" ) var ( @@ -144,7 +144,7 @@ func rootlessSocketPathFromHomeDesktopDir() (string, error) { // rootlessSocketPathFromRunDir returns the path to the rootless Docker socket from the /run/user//docker.sock file. func rootlessSocketPathFromRunDir() (string, error) { uid := os.Getuid() - f := filepath.Join(baseRunDir, "user", fmt.Sprintf("%d", uid), "docker.sock") + f := filepath.Join(baseRunDir, "user", strconv.Itoa(uid), "docker.sock") if fileExists(f) { return f, nil } diff --git a/internal/core/docker_rootless_test.go b/internal/core/docker_rootless_test.go index 7897f35783..d6a338acdb 100644 --- a/internal/core/docker_rootless_test.go +++ b/internal/core/docker_rootless_test.go @@ -2,9 +2,9 @@ package core import ( "context" - "fmt" "os" "path/filepath" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -70,7 +70,7 @@ func TestRootlessDockerSocketPathNotSupportedOnWindows(t *testing.T) { t.Setenv("GOOS", "windows") socketPath, err := rootlessDockerSocketPath(context.Background()) require.ErrorIs(t, err, ErrRootlessDockerNotSupportedWindows) - assert.Empty(t, socketPath) + require.Empty(t, socketPath) } func TestRootlessDockerSocketPath(t *testing.T) { @@ -161,7 +161,7 @@ func TestRootlessDockerSocketPath(t *testing.T) { }) uid := os.Getuid() - runDir := filepath.Join(tmpDir, "user", fmt.Sprintf("%d", uid)) + runDir := filepath.Join(tmpDir, "user", strconv.Itoa(uid)) err = createTmpDockerSocket(runDir) require.NoError(t, err) @@ -179,11 +179,12 @@ func TestRootlessDockerSocketPath(t *testing.T) { socketPath, err := rootlessDockerSocketPath(context.Background()) require.ErrorIs(t, err, ErrRootlessDockerNotFoundXDGRuntimeDir) - assert.Empty(t, socketPath) + require.Empty(t, socketPath) }) } func setupRootlessNotFound(t *testing.T) { + t.Helper() t.Cleanup(func() { baseRunDir = originalBaseRunDir os.Setenv("XDG_RUNTIME_DIR", originalXDGRuntimeDir) @@ -207,7 +208,7 @@ func setupRootlessNotFound(t *testing.T) { baseRunDir = tmpDir uid := os.Getuid() - runDir := filepath.Join(tmpDir, "run", "user", fmt.Sprintf("%d", uid)) + runDir := filepath.Join(tmpDir, "run", "user", strconv.Itoa(uid)) err = createTmpDir(runDir) require.NoError(t, err) } diff --git a/internal/core/images_test.go b/internal/core/images_test.go index 760a5cb857..509a117c80 100644 --- a/internal/core/images_test.go +++ b/internal/core/images_test.go @@ -67,7 +67,7 @@ func TestExtractImagesFromDockerfile(t *testing.T) { images, err := ExtractImagesFromDockerfile(tt.dockerfile, tt.buildArgs) if tt.expectedError { require.Error(t, err) - assert.Empty(t, images) + require.Empty(t, images) } else { require.NoError(t, err) assert.Equal(t, tt.expected, images) diff --git a/internal/core/labels.go b/internal/core/labels.go index 58b054ab95..0814924234 100644 --- a/internal/core/labels.go +++ b/internal/core/labels.go @@ -1,23 +1,73 @@ package core import ( + "errors" + "fmt" + "strings" + "github.com/testcontainers/testcontainers-go/internal" + "github.com/testcontainers/testcontainers-go/internal/config" ) const ( - LabelBase = "org.testcontainers" - LabelLang = LabelBase + ".lang" - LabelReaper = LabelBase + ".reaper" - LabelRyuk = LabelBase + ".ryuk" + // LabelBase is the base label for all testcontainers labels. + LabelBase = "org.testcontainers" + + // LabelLang specifies the language which created the test container. + LabelLang = LabelBase + ".lang" + + // LabelReaper identifies the container as a reaper. + LabelReaper = LabelBase + ".reaper" + + // LabelRyuk identifies the container as a ryuk. + LabelRyuk = LabelBase + ".ryuk" + + // LabelSessionID specifies the session ID of the container. LabelSessionID = LabelBase + ".sessionId" - LabelVersion = LabelBase + ".version" + + // LabelVersion specifies the version of testcontainers which created the container. + LabelVersion = LabelBase + ".version" + + // LabelReap specifies the container should be reaped by the reaper. + LabelReap = LabelBase + ".reap" ) +// DefaultLabels returns the standard set of labels which +// includes LabelSessionID if the reaper is enabled. func DefaultLabels(sessionID string) map[string]string { - return map[string]string{ + labels := map[string]string{ LabelBase: "true", LabelLang: "go", - LabelSessionID: sessionID, LabelVersion: internal.Version, + LabelSessionID: sessionID, + } + + if !config.Read().RyukDisabled { + labels[LabelReap] = "true" + } + + return labels +} + +// AddDefaultLabels adds the default labels for sessionID to target. +func AddDefaultLabels(sessionID string, target map[string]string) { + for k, v := range DefaultLabels(sessionID) { + target[k] = v + } +} + +// MergeCustomLabels sets labels from src to dst. +// If a key in src has [LabelBase] prefix returns an error. +// If dst is nil returns an error. +func MergeCustomLabels(dst, src map[string]string) error { + if dst == nil { + return errors.New("destination map is nil") + } + for key, value := range src { + if strings.HasPrefix(key, LabelBase) { + return fmt.Errorf("key %q has %q prefix", key, LabelBase) + } + dst[key] = value } + return nil } diff --git a/internal/core/labels_test.go b/internal/core/labels_test.go new file mode 100644 index 0000000000..e382a0ad48 --- /dev/null +++ b/internal/core/labels_test.go @@ -0,0 +1,34 @@ +package core + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMergeCustomLabels(t *testing.T) { + t.Run("success", func(t *testing.T) { + dst := map[string]string{"A": "1", "B": "2"} + src := map[string]string{"B": "X", "C": "3"} + + err := MergeCustomLabels(dst, src) + require.NoError(t, err) + require.Equal(t, map[string]string{"A": "1", "B": "X", "C": "3"}, dst) + }) + + t.Run("invalid-prefix", func(t *testing.T) { + dst := map[string]string{"A": "1", "B": "2"} + src := map[string]string{"B": "X", LabelLang: "go"} + + err := MergeCustomLabels(dst, src) + + require.EqualError(t, err, `key "org.testcontainers.lang" has "org.testcontainers" prefix`) + require.Equal(t, map[string]string{"A": "1", "B": "X"}, dst) + }) + + t.Run("nil-destination", func(t *testing.T) { + src := map[string]string{"A": "1"} + err := MergeCustomLabels(nil, src) + require.Error(t, err) + }) +} diff --git a/internal/version.go b/internal/version.go index 0c688d5e3d..6e8cb510c0 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,4 +1,4 @@ package internal // Version is the next development version of the application -const Version = "0.34.0" +const Version = "0.35.0" diff --git a/lifecycle.go b/lifecycle.go index 40360a4c0b..63446f715d 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "reflect" "strings" "time" @@ -33,12 +34,14 @@ type ContainerRequestHook func(ctx context.Context, req ContainerRequest) error // - Terminating // - Terminated // For that, it will receive a Container, modify it and return an error if needed. -type ContainerHook func(ctx context.Context, container Container) error +type ContainerHook func(ctx context.Context, ctr Container) error // ContainerLifecycleHooks is a struct that contains all the hooks that can be used // to modify the container lifecycle. All the container lifecycle hooks except the PreCreates hooks // will be passed to the container once it's created type ContainerLifecycleHooks struct { + PreBuilds []ContainerRequestHook + PostBuilds []ContainerRequestHook PreCreates []ContainerRequestHook PostCreates []ContainerHook PreStarts []ContainerHook @@ -57,6 +60,18 @@ var DefaultLoggingHook = func(logger Logging) ContainerLifecycleHooks { } return ContainerLifecycleHooks{ + PreBuilds: []ContainerRequestHook{ + func(ctx context.Context, req ContainerRequest) error { + logger.Printf("🐳 Building image %s:%s", req.GetRepo(), req.GetTag()) + return nil + }, + }, + PostBuilds: []ContainerRequestHook{ + func(ctx context.Context, req ContainerRequest) error { + logger.Printf("✅ Built image %s", req.Image) + return nil + }, + }, PreCreates: []ContainerRequestHook{ func(ctx context.Context, req ContainerRequest) error { logger.Printf("🐳 Creating container for image %s", req.Image) @@ -190,7 +205,6 @@ var defaultLogConsumersHook = func(cfg *LogConsumerConfig) ContainerLifecycleHoo } dockerContainer := c.(*DockerContainer) - return dockerContainer.stopLogProduction() }, }, @@ -285,11 +299,34 @@ var defaultReadinessHook = func() ContainerLifecycleHooks { } } +// buildingHook is a hook that will be called before a container image is built. +func (req ContainerRequest) buildingHook(ctx context.Context) error { + return req.applyLifecycleHooks(func(lifecycleHooks ContainerLifecycleHooks) error { + return lifecycleHooks.Building(ctx)(req) + }) +} + +// builtHook is a hook that will be called after a container image is built. +func (req ContainerRequest) builtHook(ctx context.Context) error { + return req.applyLifecycleHooks(func(lifecycleHooks ContainerLifecycleHooks) error { + return lifecycleHooks.Built(ctx)(req) + }) +} + // creatingHook is a hook that will be called before a container is created. func (req ContainerRequest) creatingHook(ctx context.Context) error { - errs := make([]error, len(req.LifecycleHooks)) - for i, lifecycleHooks := range req.LifecycleHooks { - errs[i] = lifecycleHooks.Creating(ctx)(req) + return req.applyLifecycleHooks(func(lifecycleHooks ContainerLifecycleHooks) error { + return lifecycleHooks.Creating(ctx)(req) + }) +} + +// applyLifecycleHooks calls hook on all LifecycleHooks. +func (req ContainerRequest) applyLifecycleHooks(hook func(lifecycleHooks ContainerLifecycleHooks) error) error { + var errs []error + for _, lifecycleHooks := range req.LifecycleHooks { + if err := hook(lifecycleHooks); err != nil { + errs = append(errs, err) + } } return errors.Join(errs...) @@ -371,9 +408,11 @@ func (c *DockerContainer) terminatedHook(ctx context.Context) error { // applyLifecycleHooks applies all lifecycle hooks reporting the container logs on error if logError is true. func (c *DockerContainer) applyLifecycleHooks(ctx context.Context, logError bool, hooks func(lifecycleHooks ContainerLifecycleHooks) []ContainerHook) error { - errs := make([]error, len(c.lifecycleHooks)) - for i, lifecycleHooks := range c.lifecycleHooks { - errs[i] = containerHookFn(ctx, hooks(lifecycleHooks))(c) + var errs []error + for _, lifecycleHooks := range c.lifecycleHooks { + if err := containerHookFn(ctx, hooks(lifecycleHooks))(c); err != nil { + errs = append(errs, err) + } } if err := errors.Join(errs...); err != nil { @@ -395,10 +434,26 @@ func (c *DockerContainer) applyLifecycleHooks(ctx context.Context, logError bool return nil } +// Building is a hook that will be called before a container image is built. +func (c ContainerLifecycleHooks) Building(ctx context.Context) func(req ContainerRequest) error { + return containerRequestHook(ctx, c.PreBuilds) +} + +// Building is a hook that will be called before a container image is built. +func (c ContainerLifecycleHooks) Built(ctx context.Context) func(req ContainerRequest) error { + return containerRequestHook(ctx, c.PostBuilds) +} + // Creating is a hook that will be called before a container is created. func (c ContainerLifecycleHooks) Creating(ctx context.Context) func(req ContainerRequest) error { + return containerRequestHook(ctx, c.PreCreates) +} + +// containerRequestHook returns a function that will iterate over all +// the hooks and call them one by one until there is an error. +func containerRequestHook(ctx context.Context, hooks []ContainerRequestHook) func(req ContainerRequest) error { return func(req ContainerRequest) error { - for _, hook := range c.PreCreates { + for _, hook := range hooks { if err := hook(ctx, req); err != nil { return err } @@ -411,10 +466,12 @@ func (c ContainerLifecycleHooks) Creating(ctx context.Context) func(req Containe // containerHookFn is a helper function that will create a function to be returned by all the different // container lifecycle hooks. The created function will iterate over all the hooks and call them one by one. func containerHookFn(ctx context.Context, containerHook []ContainerHook) func(container Container) error { - return func(container Container) error { - errs := make([]error, len(containerHook)) - for i, hook := range containerHook { - errs[i] = hook(ctx, container) + return func(ctr Container) error { + var errs []error + for _, hook := range containerHook { + if err := hook(ctx, ctr); err != nil { + errs = append(errs, err) + } } return errors.Join(errs...) @@ -533,65 +590,50 @@ func (p *DockerProvider) preCreateContainerHook(ctx context.Context, req Contain return nil } -// combineContainerHooks it returns just one ContainerLifecycle hook, as the result of combining -// the default hooks with the user-defined hooks. The function will loop over all the default hooks, -// storing each of the hooks in a slice, and then it will loop over all the user-defined hooks, -// appending or prepending them to the slice of hooks. The order of hooks is the following: -// - for Pre-hooks, always run the default hooks first, then append the user-defined hooks -// - for Post-hooks, always run the user-defined hooks first, then the default hooks +// combineContainerHooks returns a ContainerLifecycle hook as the result +// of combining the default hooks with the user-defined hooks. +// +// The order of hooks is the following: +// - Pre-hooks run the default hooks first then the user-defined hooks +// - Post-hooks run the user-defined hooks first then the default hooks func combineContainerHooks(defaultHooks, userDefinedHooks []ContainerLifecycleHooks) ContainerLifecycleHooks { - preCreates := []ContainerRequestHook{} - postCreates := []ContainerHook{} - preStarts := []ContainerHook{} - postStarts := []ContainerHook{} - postReadies := []ContainerHook{} - preStops := []ContainerHook{} - postStops := []ContainerHook{} - preTerminates := []ContainerHook{} - postTerminates := []ContainerHook{} - + // We use reflection here to ensure that any new hooks are handled. + var hooks ContainerLifecycleHooks + hooksVal := reflect.ValueOf(&hooks).Elem() + hooksType := reflect.TypeOf(hooks) for _, defaultHook := range defaultHooks { - preCreates = append(preCreates, defaultHook.PreCreates...) - preStarts = append(preStarts, defaultHook.PreStarts...) - preStops = append(preStops, defaultHook.PreStops...) - preTerminates = append(preTerminates, defaultHook.PreTerminates...) + defaultVal := reflect.ValueOf(defaultHook) + for i := 0; i < hooksType.NumField(); i++ { + if strings.HasPrefix(hooksType.Field(i).Name, "Pre") { + field := hooksVal.Field(i) + field.Set(reflect.AppendSlice(field, defaultVal.Field(i))) + } + } } - // append the user-defined hooks after the default pre-hooks - // and because the post hooks are still empty, the user-defined post-hooks - // will be the first ones to be executed + // Append the user-defined hooks after the default pre-hooks + // and because the post hooks are still empty, the user-defined + // post-hooks will be the first ones to be executed. for _, userDefinedHook := range userDefinedHooks { - preCreates = append(preCreates, userDefinedHook.PreCreates...) - postCreates = append(postCreates, userDefinedHook.PostCreates...) - preStarts = append(preStarts, userDefinedHook.PreStarts...) - postStarts = append(postStarts, userDefinedHook.PostStarts...) - postReadies = append(postReadies, userDefinedHook.PostReadies...) - preStops = append(preStops, userDefinedHook.PreStops...) - postStops = append(postStops, userDefinedHook.PostStops...) - preTerminates = append(preTerminates, userDefinedHook.PreTerminates...) - postTerminates = append(postTerminates, userDefinedHook.PostTerminates...) + userVal := reflect.ValueOf(userDefinedHook) + for i := 0; i < hooksType.NumField(); i++ { + field := hooksVal.Field(i) + field.Set(reflect.AppendSlice(field, userVal.Field(i))) + } } - // finally, append the default post-hooks + // Finally, append the default post-hooks. for _, defaultHook := range defaultHooks { - postCreates = append(postCreates, defaultHook.PostCreates...) - postStarts = append(postStarts, defaultHook.PostStarts...) - postReadies = append(postReadies, defaultHook.PostReadies...) - postStops = append(postStops, defaultHook.PostStops...) - postTerminates = append(postTerminates, defaultHook.PostTerminates...) + defaultVal := reflect.ValueOf(defaultHook) + for i := 0; i < hooksType.NumField(); i++ { + if strings.HasPrefix(hooksType.Field(i).Name, "Post") { + field := hooksVal.Field(i) + field.Set(reflect.AppendSlice(field, defaultVal.Field(i))) + } + } } - return ContainerLifecycleHooks{ - PreCreates: preCreates, - PostCreates: postCreates, - PreStarts: preStarts, - PostStarts: postStarts, - PostReadies: postReadies, - PreStops: preStops, - PostStops: postStops, - PreTerminates: preTerminates, - PostTerminates: postTerminates, - } + return hooks } func mergePortBindings(configPortMap, exposedPortMap nat.PortMap, exposedPorts []string) nat.PortMap { diff --git a/lifecycle_test.go b/lifecycle_test.go index f4b0a2ae37..91102ccf82 100644 --- a/lifecycle_test.go +++ b/lifecycle_test.go @@ -3,7 +3,10 @@ package testcontainers import ( "bufio" "context" + "errors" "fmt" + "io" + "reflect" "strings" "testing" "time" @@ -210,12 +213,7 @@ func TestPreCreateModifierHook(t *testing.T) { Name: networkName, }) require.NoError(t, err) - defer func() { - err := net.Remove(ctx) - if err != nil { - t.Logf("failed to remove network %s: %s\n", networkName, err) - } - }() + CleanupNetwork(t, net) dockerNetwork, err := provider.GetNetwork(ctx, NetworkRequest{ Name: networkName, @@ -262,12 +260,7 @@ func TestPreCreateModifierHook(t *testing.T) { Name: networkName, }) require.NoError(t, err) - defer func() { - err := net.Remove(ctx) - if err != nil { - t.Logf("failed to remove network %s: %s\n", networkName, err) - } - }() + CleanupNetwork(t, net) dockerNetwork, err := provider.GetNetwork(ctx, NetworkRequest{ Name: networkName, @@ -291,7 +284,7 @@ func TestPreCreateModifierHook(t *testing.T) { // assertions - assert.Empty( + require.Empty( t, inputNetworkingConfig.EndpointsConfig[networkName].Aliases, "Networking config's aliases should be empty", @@ -549,91 +542,91 @@ func TestLifecycleHooks(t *testing.T) { { PreCreates: []ContainerRequestHook{ func(ctx context.Context, req ContainerRequest) error { - prints = append(prints, fmt.Sprintf("pre-create hook 1: %#v", req)) + prints = append(prints, "pre-create hook 1") return nil }, func(ctx context.Context, req ContainerRequest) error { - prints = append(prints, fmt.Sprintf("pre-create hook 2: %#v", req)) + prints = append(prints, "pre-create hook 2") return nil }, }, PostCreates: []ContainerHook{ func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-create hook 1: %#v", c)) + prints = append(prints, "post-create hook 1") return nil }, func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-create hook 2: %#v", c)) + prints = append(prints, "post-create hook 2") return nil }, }, PreStarts: []ContainerHook{ func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("pre-start hook 1: %#v", c)) + prints = append(prints, "pre-start hook 1") return nil }, func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("pre-start hook 2: %#v", c)) + prints = append(prints, "pre-start hook 2") return nil }, }, PostStarts: []ContainerHook{ func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-start hook 1: %#v", c)) + prints = append(prints, "post-start hook 1") return nil }, func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-start hook 2: %#v", c)) + prints = append(prints, "post-start hook 2") return nil }, }, PostReadies: []ContainerHook{ func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-ready hook 1: %#v", c)) + prints = append(prints, "post-ready hook 1") return nil }, func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-ready hook 2: %#v", c)) + prints = append(prints, "post-ready hook 2") return nil }, }, PreStops: []ContainerHook{ func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("pre-stop hook 1: %#v", c)) + prints = append(prints, "pre-stop hook 1") return nil }, func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("pre-stop hook 2: %#v", c)) + prints = append(prints, "pre-stop hook 2") return nil }, }, PostStops: []ContainerHook{ func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-stop hook 1: %#v", c)) + prints = append(prints, "post-stop hook 1") return nil }, func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-stop hook 2: %#v", c)) + prints = append(prints, "post-stop hook 2") return nil }, }, PreTerminates: []ContainerHook{ func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("pre-terminate hook 1: %#v", c)) + prints = append(prints, "pre-terminate hook 1") return nil }, func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("pre-terminate hook 2: %#v", c)) + prints = append(prints, "pre-terminate hook 2") return nil }, }, PostTerminates: []ContainerHook{ func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-terminate hook 1: %#v", c)) + prints = append(prints, "post-terminate hook 1") return nil }, func(ctx context.Context, c Container) error { - prints = append(prints, fmt.Sprintf("post-terminate hook 2: %#v", c)) + prints = append(prints, "post-terminate hook 2") return nil }, }, @@ -651,6 +644,7 @@ func TestLifecycleHooks(t *testing.T) { Reuse: tt.reuse, Started: true, }) + CleanupContainer(t, c) require.NoError(t, err) require.NotNil(t, c) @@ -664,7 +658,7 @@ func TestLifecycleHooks(t *testing.T) { err = c.Terminate(ctx) require.NoError(t, err) - lifecycleHooksIsHonouredFn(t, ctx, prints) + lifecycleHooksIsHonouredFn(t, prints) }) } } @@ -698,6 +692,7 @@ func TestLifecycleHooks_WithDefaultLogger(t *testing.T) { ContainerRequest: req, Started: true, }) + CleanupContainer(t, c) require.NoError(t, err) require.NotNil(t, c) @@ -711,7 +706,8 @@ func TestLifecycleHooks_WithDefaultLogger(t *testing.T) { err = c.Terminate(ctx) require.NoError(t, err) - require.Len(t, dl.data, 12) + // Includes two additional entries for stop when terminate is called. + require.Len(t, dl.data, 14) } func TestCombineLifecycleHooks(t *testing.T) { @@ -788,7 +784,7 @@ func TestCombineLifecycleHooks(t *testing.T) { // There are 5 lifecycles (create, start, ready, stop, terminate), // but ready has only half of the hooks (it only has post), so we have 90 hooks in total. - assert.Len(t, prints, 90) + require.Len(t, prints, 90) // The order of the hooks is: // - pre-X hooks: first default (2*2), then user-defined (3*2) @@ -864,6 +860,7 @@ func TestLifecycleHooks_WithMultipleHooks(t *testing.T) { ContainerRequest: req, Started: true, }) + CleanupContainer(t, c) require.NoError(t, err) require.NotNil(t, c) @@ -877,7 +874,8 @@ func TestLifecycleHooks_WithMultipleHooks(t *testing.T) { err = c.Terminate(ctx) require.NoError(t, err) - require.Len(t, dl.data, 24) + // Includes four additional entries for stop (twice) when terminate is called. + require.Len(t, dl.data, 28) } type linesTestLogger struct { @@ -892,7 +890,7 @@ func TestPrintContainerLogsOnError(t *testing.T) { ctx := context.Background() req := ContainerRequest{ - Image: "docker.io/alpine", + Image: "alpine", Cmd: []string{"echo", "-n", "I am expecting this"}, WaitingFor: wait.ForLog("I was expecting that").WithStartupTimeout(5 * time.Second), } @@ -901,35 +899,28 @@ func TestPrintContainerLogsOnError(t *testing.T) { data: []string{}, } - container, err := GenericContainer(ctx, GenericContainerRequest{ + ctr, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Logger: &arrayOfLinesLogger, Started: true, }) + CleanupContainer(t, ctr) // it should fail because the waiting for condition is not met - if err == nil { - t.Fatal(err) - } - terminateContainerOnEnd(t, ctx, container) + require.Error(t, err) - containerLogs, err := container.Logs(ctx) - if err != nil { - t.Fatal(err) - } + containerLogs, err := ctr.Logs(ctx) + require.NoError(t, err) defer containerLogs.Close() // read container logs line by line, checking that each line is present in the stdout rd := bufio.NewReader(containerLogs) for { line, err := rd.ReadString('\n') - if err != nil { - if err.Error() == "EOF" { - break - } - - t.Fatal("Read Error:", err) + if errors.Is(err, io.EOF) { + break } + require.NoErrorf(t, err, "Read Error") // the last line of the array should contain the line of interest, // but we are checking all the lines to make sure that is present @@ -944,42 +935,142 @@ func TestPrintContainerLogsOnError(t *testing.T) { } } -func lifecycleHooksIsHonouredFn(t *testing.T, ctx context.Context, prints []string) { - require.Len(t, prints, 24) - - assert.True(t, strings.HasPrefix(prints[0], "pre-create hook 1: ")) - assert.True(t, strings.HasPrefix(prints[1], "pre-create hook 2: ")) - - assert.True(t, strings.HasPrefix(prints[2], "post-create hook 1: ")) - assert.True(t, strings.HasPrefix(prints[3], "post-create hook 2: ")) - - assert.True(t, strings.HasPrefix(prints[4], "pre-start hook 1: ")) - assert.True(t, strings.HasPrefix(prints[5], "pre-start hook 2: ")) - - assert.True(t, strings.HasPrefix(prints[6], "post-start hook 1: ")) - assert.True(t, strings.HasPrefix(prints[7], "post-start hook 2: ")) - - assert.True(t, strings.HasPrefix(prints[8], "post-ready hook 1: ")) - assert.True(t, strings.HasPrefix(prints[9], "post-ready hook 2: ")) - - assert.True(t, strings.HasPrefix(prints[10], "pre-stop hook 1: ")) - assert.True(t, strings.HasPrefix(prints[11], "pre-stop hook 2: ")) - - assert.True(t, strings.HasPrefix(prints[12], "post-stop hook 1: ")) - assert.True(t, strings.HasPrefix(prints[13], "post-stop hook 2: ")) - - assert.True(t, strings.HasPrefix(prints[14], "pre-start hook 1: ")) - assert.True(t, strings.HasPrefix(prints[15], "pre-start hook 2: ")) +func lifecycleHooksIsHonouredFn(t *testing.T, prints []string) { + t.Helper() + + expects := []string{ + "pre-create hook 1", + "pre-create hook 2", + "post-create hook 1", + "post-create hook 2", + "pre-start hook 1", + "pre-start hook 2", + "post-start hook 1", + "post-start hook 2", + "post-ready hook 1", + "post-ready hook 2", + "pre-stop hook 1", + "pre-stop hook 2", + "post-stop hook 1", + "post-stop hook 2", + "pre-start hook 1", + "pre-start hook 2", + "post-start hook 1", + "post-start hook 2", + "post-ready hook 1", + "post-ready hook 2", + // Terminate currently calls stop to ensure that child containers are stopped. + "pre-stop hook 1", + "pre-stop hook 2", + "post-stop hook 1", + "post-stop hook 2", + "pre-terminate hook 1", + "pre-terminate hook 2", + "post-terminate hook 1", + "post-terminate hook 2", + } - assert.True(t, strings.HasPrefix(prints[16], "post-start hook 1: ")) - assert.True(t, strings.HasPrefix(prints[17], "post-start hook 2: ")) + require.Equal(t, expects, prints) +} - assert.True(t, strings.HasPrefix(prints[18], "post-ready hook 1: ")) - assert.True(t, strings.HasPrefix(prints[19], "post-ready hook 2: ")) +func Test_combineContainerHooks(t *testing.T) { + var funcID string + defaultContainerRequestHook := func(ctx context.Context, req ContainerRequest) error { + funcID = "defaultContainerRequestHook" + return nil + } + userContainerRequestHook := func(ctx context.Context, req ContainerRequest) error { + funcID = "userContainerRequestHook" + return nil + } + defaultContainerHook := func(ctx context.Context, container Container) error { + funcID = "defaultContainerHook" + return nil + } + userContainerHook := func(ctx context.Context, container Container) error { + funcID = "userContainerHook" + return nil + } - assert.True(t, strings.HasPrefix(prints[20], "pre-terminate hook 1: ")) - assert.True(t, strings.HasPrefix(prints[21], "pre-terminate hook 2: ")) + defaultHooks := []ContainerLifecycleHooks{ + { + PreBuilds: []ContainerRequestHook{defaultContainerRequestHook}, + PostBuilds: []ContainerRequestHook{defaultContainerRequestHook}, + PreCreates: []ContainerRequestHook{defaultContainerRequestHook}, + PostCreates: []ContainerHook{defaultContainerHook}, + PreStarts: []ContainerHook{defaultContainerHook}, + PostStarts: []ContainerHook{defaultContainerHook}, + PostReadies: []ContainerHook{defaultContainerHook}, + PreStops: []ContainerHook{defaultContainerHook}, + PostStops: []ContainerHook{defaultContainerHook}, + PreTerminates: []ContainerHook{defaultContainerHook}, + PostTerminates: []ContainerHook{defaultContainerHook}, + }, + } + userDefinedHooks := []ContainerLifecycleHooks{ + { + PreBuilds: []ContainerRequestHook{userContainerRequestHook}, + PostBuilds: []ContainerRequestHook{userContainerRequestHook}, + PreCreates: []ContainerRequestHook{userContainerRequestHook}, + PostCreates: []ContainerHook{userContainerHook}, + PreStarts: []ContainerHook{userContainerHook}, + PostStarts: []ContainerHook{userContainerHook}, + PostReadies: []ContainerHook{userContainerHook}, + PreStops: []ContainerHook{userContainerHook}, + PostStops: []ContainerHook{userContainerHook}, + PreTerminates: []ContainerHook{userContainerHook}, + PostTerminates: []ContainerHook{userContainerHook}, + }, + } + expects := ContainerLifecycleHooks{ + PreBuilds: []ContainerRequestHook{defaultContainerRequestHook, userContainerRequestHook}, + PostBuilds: []ContainerRequestHook{userContainerRequestHook, defaultContainerRequestHook}, + PreCreates: []ContainerRequestHook{defaultContainerRequestHook, userContainerRequestHook}, + PostCreates: []ContainerHook{userContainerHook, defaultContainerHook}, + PreStarts: []ContainerHook{defaultContainerHook, userContainerHook}, + PostStarts: []ContainerHook{userContainerHook, defaultContainerHook}, + PostReadies: []ContainerHook{userContainerHook, defaultContainerHook}, + PreStops: []ContainerHook{defaultContainerHook, userContainerHook}, + PostStops: []ContainerHook{userContainerHook, defaultContainerHook}, + PreTerminates: []ContainerHook{defaultContainerHook, userContainerHook}, + PostTerminates: []ContainerHook{userContainerHook, defaultContainerHook}, + } - assert.True(t, strings.HasPrefix(prints[22], "post-terminate hook 1: ")) - assert.True(t, strings.HasPrefix(prints[23], "post-terminate hook 2: ")) + ctx := context.Background() + ctxVal := reflect.ValueOf(ctx) + var req ContainerRequest + reqVal := reflect.ValueOf(req) + container := &DockerContainer{} + containerVal := reflect.ValueOf(container) + + got := combineContainerHooks(defaultHooks, userDefinedHooks) + + // Compare for equal. This can't be done with deep equals as functions + // are not comparable so we us the unique value stored in funcID when + // the function is called to determine if they are the same. + gotVal := reflect.ValueOf(got) + gotType := reflect.TypeOf(got) + expectedVal := reflect.ValueOf(expects) + for i := 0; i < gotVal.NumField(); i++ { + fieldName := gotType.Field(i).Name + gotField := gotVal.Field(i) + expectedField := expectedVal.Field(i) + require.Equalf(t, expectedField.Len(), 2, "field %q not setup len expected %d got %d", fieldName, 2, expectedField.Len()) //nolint:testifylint // False positive. + require.Equalf(t, expectedField.Len(), gotField.Len(), "field %q len expected %d got %d", fieldName, gotField.Len(), expectedField.Len()) + for j := 0; j < gotField.Len(); j++ { + gotIndex := gotField.Index(j) + expectedIndex := expectedField.Index(j) + var gotID string + if gotIndex.Type().Name() == "ContainerRequestHook" { + gotIndex.Call([]reflect.Value{ctxVal, reqVal}) + gotID = funcID + expectedIndex.Call([]reflect.Value{ctxVal, reqVal}) + } else { + gotIndex.Call([]reflect.Value{ctxVal, containerVal}) + gotID = funcID + expectedIndex.Call([]reflect.Value{ctxVal, containerVal}) + } + require.Equalf(t, funcID, gotID, "field %q[%d] func expected %s got %s", fieldName, j, funcID, gotID) + } + } } diff --git a/logconsumer_test.go b/logconsumer_test.go index 6265f0a578..dae1ea0b5a 100644 --- a/logconsumer_test.go +++ b/logconsumer_test.go @@ -92,6 +92,7 @@ func Test_LogConsumerGetsCalled(t *testing.T) { } c, err := GenericContainer(ctx, gReq) + CleanupContainer(t, c) require.NoError(t, err) ep, err := c.Endpoint(ctx, "http") @@ -112,9 +113,7 @@ func Test_LogConsumerGetsCalled(t *testing.T) { t.Fatal("never received final log message") } - assert.Equal(t, []string{"ready\n", "echo hello\n", "echo there\n"}, g.Msgs()) - - terminateContainerOnEnd(t, ctx, c) + require.Equal(t, []string{"ready\n", "echo hello\n", "echo there\n"}, g.Msgs()) } type TestLogTypeConsumer struct { @@ -157,8 +156,8 @@ func Test_ShouldRecognizeLogTypes(t *testing.T) { } c, err := GenericContainer(ctx, gReq) + CleanupContainer(t, c) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) ep, err := c.Endpoint(ctx, "http") require.NoError(t, err) @@ -212,6 +211,7 @@ func Test_MultipleLogConsumers(t *testing.T) { } c, err := GenericContainer(ctx, gReq) + CleanupContainer(t, c) require.NoError(t, err) ep, err := c.Endpoint(ctx, "http") @@ -226,9 +226,9 @@ func Test_MultipleLogConsumers(t *testing.T) { <-first.Done <-second.Done - assert.Equal(t, []string{"ready\n", "echo mlem\n"}, first.Msgs()) - assert.Equal(t, []string{"ready\n", "echo mlem\n"}, second.Msgs()) - require.NoError(t, c.Terminate(ctx)) + expected := []string{"ready\n", "echo mlem\n"} + require.Equal(t, expected, first.Msgs()) + require.Equal(t, expected, second.Msgs()) } func TestContainerLogWithErrClosed(t *testing.T) { @@ -251,16 +251,15 @@ func TestContainerLogWithErrClosed(t *testing.T) { dind, err := GenericContainer(ctx, GenericContainerRequest{ Started: true, ContainerRequest: ContainerRequest{ - Image: "docker.io/docker:dind", + Image: "docker:dind", ExposedPorts: []string{"2375/tcp"}, Env: map[string]string{"DOCKER_TLS_CERTDIR": ""}, WaitingFor: wait.ForListeningPort("2375/tcp"), Privileged: true, }, }) - + CleanupContainer(t, dind) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, dind) var remoteDocker string @@ -279,16 +278,12 @@ func TestContainerLogWithErrClosed(t *testing.T) { time.Sleep(10 * time.Millisecond) t.Log("retrying get endpoint") } - if err != nil { - t.Fatal("get endpoint:", err) - } + require.NoErrorf(t, err, "get endpoint") opts := []client.Opt{client.WithHost(remoteDocker), client.WithAPIVersionNegotiation()} dockerClient, err := NewDockerClientWithOpts(ctx, opts...) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer dockerClient.Close() provider := &DockerProvider{ @@ -314,18 +309,13 @@ func TestContainerLogWithErrClosed(t *testing.T) { Consumers: []LogConsumer{&consumer}, }, }) - if err != nil { - t.Fatal(err) - } - if err := nginx.Start(ctx); err != nil { - t.Fatal(err) - } - terminateContainerOnEnd(t, ctx, nginx) + require.NoError(t, err) + err = nginx.Start(ctx) + require.NoError(t, err) + CleanupContainer(t, nginx) port, err := nginx.MappedPort(ctx, "80/tcp") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // Gather the initial container logs time.Sleep(time.Second * 1) @@ -333,17 +323,14 @@ func TestContainerLogWithErrClosed(t *testing.T) { hitNginx := func() { i, _, err := dind.Exec(ctx, []string{"wget", "--spider", "localhost:" + port.Port()}) - if err != nil || i > 0 { - t.Fatalf("Can't make request to nginx container from dind container") - } + require.NoError(t, err, "Can't make request to nginx container from dind container") + require.Zerof(t, i, "Can't make request to nginx container from dind container") } hitNginx() time.Sleep(time.Second * 1) msgs := consumer.Msgs() - if len(msgs)-existingLogs != 1 { - t.Fatalf("logConsumer should have 1 new log message, instead has: %v", msgs[existingLogs:]) - } + require.Equalf(t, 1, len(msgs)-existingLogs, "logConsumer should have 1 new log message, instead has: %v", msgs[existingLogs:]) existingLogs = len(consumer.Msgs()) iptableArgs := []string{ @@ -352,25 +339,21 @@ func TestContainerLogWithErrClosed(t *testing.T) { } // Simulate a transient closed connection to the docker daemon i, _, err := dind.Exec(ctx, append([]string{"iptables", "-A"}, iptableArgs...)) - if err != nil || i > 0 { - t.Fatalf("Failed to close connection to dind daemon: i(%d), err %v", i, err) - } + require.NoErrorf(t, err, "Failed to close connection to dind daemon: i(%d), err %v", i, err) + require.Zerof(t, i, "Failed to close connection to dind daemon: i(%d), err %v", i, err) i, _, err = dind.Exec(ctx, append([]string{"iptables", "-D"}, iptableArgs...)) - if err != nil || i > 0 { - t.Fatalf("Failed to re-open connection to dind daemon: i(%d), err %v", i, err) - } + require.NoErrorf(t, err, "Failed to re-open connection to dind daemon: i(%d), err %v", i, err) + require.Zerof(t, i, "Failed to re-open connection to dind daemon: i(%d), err %v", i, err) time.Sleep(time.Second * 3) hitNginx() hitNginx() time.Sleep(time.Second * 1) msgs = consumer.Msgs() - if len(msgs)-existingLogs != 2 { - t.Fatalf( - "LogConsumer should have 2 new log messages after detecting closed connection and"+ - " re-requesting logs. Instead has:\n%s", msgs[existingLogs:], - ) - } + require.Equalf(t, 2, len(msgs)-existingLogs, + "LogConsumer should have 2 new log messages after detecting closed connection and"+ + " re-requesting logs. Instead has:\n%s", msgs[existingLogs:], + ) } func TestContainerLogsShouldBeWithoutStreamHeader(t *testing.T) { @@ -380,23 +363,18 @@ func TestContainerLogsShouldBeWithoutStreamHeader(t *testing.T) { Cmd: []string{"sh", "-c", "id -u"}, WaitingFor: wait.ForExit(), } - container, err := GenericContainer(ctx, GenericContainerRequest{ + ctr, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, Started: true, }) - if err != nil { - t.Fatal(err) - } - terminateContainerOnEnd(t, ctx, container) - r, err := container.Logs(ctx) - if err != nil { - t.Fatal(err) - } + CleanupContainer(t, ctr) + require.NoError(t, err) + + r, err := ctr.Logs(ctx) + require.NoError(t, err) defer r.Close() b, err := io.ReadAll(r) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) assert.Equal(t, "0", strings.TrimSpace(string(b))) } @@ -429,6 +407,7 @@ func TestContainerLogsEnableAtStart(t *testing.T) { } c, err := GenericContainer(ctx, gReq) + CleanupContainer(t, c) require.NoError(t, err) ep, err := c.Endpoint(ctx, "http") @@ -448,9 +427,7 @@ func TestContainerLogsEnableAtStart(t *testing.T) { case <-time.After(10 * time.Second): t.Fatal("never received final log message") } - assert.Equal(t, []string{"ready\n", "echo hello\n", "echo there\n"}, g.Msgs()) - - terminateContainerOnEnd(t, ctx, c) + require.Equal(t, []string{"ready\n", "echo hello\n", "echo there\n"}, g.Msgs()) } func Test_StartLogProductionStillStartsWithTooLowTimeout(t *testing.T) { @@ -481,8 +458,8 @@ func Test_StartLogProductionStillStartsWithTooLowTimeout(t *testing.T) { } c, err := GenericContainer(ctx, gReq) + CleanupContainer(t, c) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) } func Test_StartLogProductionStillStartsWithTooHighTimeout(t *testing.T) { @@ -513,22 +490,44 @@ func Test_StartLogProductionStillStartsWithTooHighTimeout(t *testing.T) { } c, err := GenericContainer(ctx, gReq) + CleanupContainer(t, c) require.NoError(t, err) require.NotNil(t, c) - // because the log production timeout is too high, the container should have already been terminated - // so no need to terminate it again with "terminateContainerOnEnd(t, ctx, c)" dc := c.(*DockerContainer) require.NoError(t, dc.stopLogProduction()) +} + +// bufLogger is a Logging implementation that writes to a bytes.Buffer. +type bufLogger struct { + mtx sync.Mutex + buf bytes.Buffer +} + +// Printf implements Logging. +func (l *bufLogger) Printf(format string, v ...any) { + l.mtx.Lock() + defer l.mtx.Unlock() - terminateContainerOnEnd(t, ctx, c) + fmt.Fprintf(&l.buf, format, v...) +} + +// String returns the contents of the buffer as a string. +func (l *bufLogger) String() string { + l.mtx.Lock() + defer l.mtx.Unlock() + + return l.buf.String() } func Test_MultiContainerLogConsumer_CancelledContext(t *testing.T) { - // Redirect stderr to a buffer - oldStderr := os.Stderr - r, w, _ := os.Pipe() - os.Stderr = w + // Capture global logger. + logger := &bufLogger{} + Logger = logger + oldLogger := Logger + t.Cleanup(func() { + Logger = oldLogger + }) // Context with cancellation functionality for simulating user interruption ctx, cancel := context.WithCancel(context.Background()) @@ -558,6 +557,7 @@ func Test_MultiContainerLogConsumer_CancelledContext(t *testing.T) { } c, err := GenericContainer(ctx, genericReq1) + CleanupContainer(t, c) require.NoError(t, err) ep1, err := c.Endpoint(ctx, "http") @@ -593,6 +593,7 @@ func Test_MultiContainerLogConsumer_CancelledContext(t *testing.T) { } c2, err := GenericContainer(ctx, genericReq2) + CleanupContainer(t, c2) require.NoError(t, err) ep2, err := c2.Endpoint(ctx, "http") @@ -604,39 +605,15 @@ func Test_MultiContainerLogConsumer_CancelledContext(t *testing.T) { _, err = http.Get(ep2 + "/stdout?echo=there2") require.NoError(t, err) - // Handling the termination of the containers - defer func() { - shutdownCtx, shutdownCancel := context.WithTimeout( - context.Background(), 10*time.Second, - ) - defer shutdownCancel() - _ = c.Terminate(shutdownCtx) - _ = c2.Terminate(shutdownCtx) - }() - // Deliberately calling context cancel cancel() // We check log size due to context cancellation causing // varying message counts, leading to test failure. - assert.GreaterOrEqual(t, len(first.Msgs()), 2) - assert.GreaterOrEqual(t, len(second.Msgs()), 2) - - // Restore stderr - w.Close() - os.Stderr = oldStderr - - // Read the stderr output from the buffer - var buf bytes.Buffer - _, _ = buf.ReadFrom(r) - - // Check the stderr message - actual := buf.String() + require.GreaterOrEqual(t, len(first.Msgs()), 2) + require.GreaterOrEqual(t, len(second.Msgs()), 2) - // The context cancel shouldn't cause the system to throw a - // logStoppedForOutOfSyncMessage, as it hangs the system with - // the multiple containers. - assert.False(t, strings.Contains(actual, logStoppedForOutOfSyncMessage)) + require.NotContains(t, logger.String(), "Unexpected error reading logs") } // FooLogConsumer is a test log consumer that accepts logs from the @@ -689,7 +666,7 @@ func TestRestartContainerWithLogConsumer(t *testing.T) { logConsumer := NewFooLogConsumer(t) ctx := context.Background() - container, err := GenericContainer(ctx, GenericContainerRequest{ + ctr, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: ContainerRequest{ Image: "hello-world", AlwaysPullImage: true, @@ -699,24 +676,24 @@ func TestRestartContainerWithLogConsumer(t *testing.T) { }, Started: false, }) - terminateContainerOnEnd(t, ctx, container) + CleanupContainer(t, ctr) require.NoError(t, err) // Start and confirm that the log consumer receives the log message. - err = container.Start(ctx) + err = ctr.Start(ctx) require.NoError(t, err) logConsumer.AssertRead() // Stop the container and clear any pending message. d := 5 * time.Second - err = container.Stop(ctx, &d) + err = ctr.Stop(ctx, &d) require.NoError(t, err) logConsumer.SlurpOne() // Restart the container and confirm that the log consumer receives new log messages. - err = container.Start(ctx) + err = ctr.Start(ctx) require.NoError(t, err) // First message is from the first start. diff --git a/mkdocs.yml b/mkdocs.yml index d48a9dff17..2d80a5b420 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,8 +74,11 @@ nav: - modules/cockroachdb.md - modules/consul.md - modules/couchbase.md + - modules/databend.md - modules/dolt.md + - modules/dynamodb.md - modules/elasticsearch.md + - modules/etcd.md - modules/gcloud.md - modules/grafana-lgtm.md - modules/inbucket.md @@ -85,6 +88,7 @@ nav: - modules/kafka.md - modules/localstack.md - modules/mariadb.md + - modules/meilisearch.md - modules/milvus.md - modules/minio.md - modules/mockserver.md @@ -109,6 +113,7 @@ nav: - modules/vault.md - modules/vearch.md - modules/weaviate.md + - modules/yugabytedb.md - Examples: - examples/index.md - examples/nginx.md @@ -135,4 +140,4 @@ nav: - Getting help: getting_help.md edit_uri: edit/main/docs/ extra: - latest_version: v0.33.0 + latest_version: v0.34.0 diff --git a/modulegen/_template/ci.yml.tmpl b/modulegen/_template/ci.yml.tmpl index e4fd047b24..46fc3e3906 100644 --- a/modulegen/_template/ci.yml.tmpl +++ b/modulegen/_template/ci.yml.tmpl @@ -129,7 +129,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: # Disabling shallow clone is recommended for improving relevancy of reporting fetch-depth: 0 diff --git a/modulegen/_template/examples_test.go.tmpl b/modulegen/_template/examples_test.go.tmpl index b81cf22c58..ca55c61e44 100644 --- a/modulegen/_template/examples_test.go.tmpl +++ b/modulegen/_template/examples_test.go.tmpl @@ -1,33 +1,33 @@ -{{ $entrypoint := Entrypoint }}{{ $image := Image }}{{ $lower := ToLower }}{{ $title := Title }}package {{ $lower }}_test +{{ $entrypoint := Entrypoint }}{{ $image := Image }}{{ $lower := ToLower }}package {{ $lower }}_test import ( "context" "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/{{ ParentDir }}/{{ $lower }}" ) func Example{{ $entrypoint }}() { - // run{{ $title }}Container { ctx := context.Background() {{ $lower }}Container, err := {{ $lower }}.{{ $entrypoint }}(ctx, "{{ $image }}") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := {{ $lower }}Container.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer({{ $lower }}Container); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := {{ $lower }}Container.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modulegen/_template/module.go.tmpl b/modulegen/_template/module.go.tmpl index fe988afada..585e853fba 100644 --- a/modulegen/_template/module.go.tmpl +++ b/modulegen/_template/module.go.tmpl @@ -7,13 +7,13 @@ import ( "github.com/testcontainers/testcontainers-go" ) -// {{ $containerName }} represents the {{ $title }} container type used in the module -type {{ $containerName }} struct { +// Container represents the {{ $title }} container type used in the module +type Container struct { testcontainers.Container } // {{ $entrypoint }} creates an instance of the {{ $title }} container type -func {{ $entrypoint }}(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*{{ $containerName }}, error) { +func {{ $entrypoint }}(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { req := testcontainers.ContainerRequest{ Image: img, } @@ -30,9 +30,14 @@ func {{ $entrypoint }}(ctx context.Context, img string, opts ...testcontainers.C } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *Container + if container != nil { + c = &Container{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &{{ $containerName }}{Container: container}, nil + return c, nil } diff --git a/modulegen/_template/module.md.tmpl b/modulegen/_template/module.md.tmpl index ac29fb3337..91945bd254 100644 --- a/modulegen/_template/module.md.tmpl +++ b/modulegen/_template/module.md.tmpl @@ -1,4 +1,4 @@ -{{ $lower := ToLower }}{{ $title := Title }}# {{ $title }} +{{ $entrypoint := Entrypoint }}{{ $lower := ToLower }}{{ $title := Title }}# {{ $title }} Not available until the next release of testcontainers-go :material-tag: main @@ -17,7 +17,7 @@ go get github.com/testcontainers/testcontainers-go/{{ ParentDir }}/{{ $lower }} ## Usage example -[Creating a {{ $title }} container](../../{{ ParentDir }}/{{ $lower }}/examples_test.go) inside_block:run{{ $title }}Container +[Creating a {{ $title }} container](../../{{ ParentDir }}/{{ $lower }}/examples_test.go) inside_block:Example{{ $entrypoint }} ## Module Reference diff --git a/modulegen/_template/module_test.go.tmpl b/modulegen/_template/module_test.go.tmpl index 2f7774ad7a..1850e568c9 100644 --- a/modulegen/_template/module_test.go.tmpl +++ b/modulegen/_template/module_test.go.tmpl @@ -4,23 +4,18 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/{{ ParentDir }}/{{ $lower }}" ) func Test{{ $title }}(t *testing.T) { ctx := context.Background() - container, err := {{ $lower }}.{{ $entrypoint }}(ctx, "{{ $image }}") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := {{ $lower }}.{{ $entrypoint }}(ctx, "{{ $image }}") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions } diff --git a/modulegen/context_test.go b/modulegen/context_test.go index fc56ea3c5d..4023e2ed88 100644 --- a/modulegen/context_test.go +++ b/modulegen/context_test.go @@ -11,6 +11,7 @@ import ( ) func getTestRootContext(t *testing.T) context.Context { + t.Helper() current, err := os.Getwd() require.NoError(t, err) return context.New(filepath.Dir(current)) diff --git a/modulegen/internal/context/types.go b/modulegen/internal/context/types.go index 0792c249df..61d0e6217e 100644 --- a/modulegen/internal/context/types.go +++ b/modulegen/internal/context/types.go @@ -4,8 +4,6 @@ import ( "fmt" "regexp" "strings" - "unicode" - "unicode/utf8" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -22,16 +20,7 @@ type TestcontainersModule struct { // ContainerName returns the name of the container, which is the lower-cased title of the example // If the title is set, it will be used instead of the name func (m *TestcontainersModule) ContainerName() string { - name := m.Lower() - - if m.IsModule { - name = m.Title() - } else if m.TitleName != "" { - r, n := utf8.DecodeRuneInString(m.TitleName) - name = string(unicode.ToLower(r)) + m.TitleName[n:] - } - - return name + "Container" + return "Container" } // Entrypoint returns the name of the entrypoint function, which is the lower-cased title of the example diff --git a/modulegen/main_test.go b/modulegen/main_test.go index 2c1ddbd8e9..d90c0da5be 100644 --- a/modulegen/main_test.go +++ b/modulegen/main_test.go @@ -17,11 +17,10 @@ import ( func TestModule(t *testing.T) { tests := []struct { - name string - module context.TestcontainersModule - expectedContainerName string - expectedEntrypoint string - expectedTitle string + name string + module context.TestcontainersModule + expectedEntrypoint string + expectedTitle string }{ { name: "Module with title", @@ -31,9 +30,8 @@ func TestModule(t *testing.T) { Image: "mongodb:latest", TitleName: "MongoDB", }, - expectedContainerName: "MongoDBContainer", - expectedEntrypoint: "Run", - expectedTitle: "MongoDB", + expectedEntrypoint: "Run", + expectedTitle: "MongoDB", }, { name: "Module without title", @@ -42,9 +40,8 @@ func TestModule(t *testing.T) { IsModule: true, Image: "mongodb:latest", }, - expectedContainerName: "MongodbContainer", - expectedEntrypoint: "Run", - expectedTitle: "Mongodb", + expectedEntrypoint: "Run", + expectedTitle: "Mongodb", }, { name: "Example with title", @@ -54,9 +51,8 @@ func TestModule(t *testing.T) { Image: "mongodb:latest", TitleName: "MongoDB", }, - expectedContainerName: "mongoDBContainer", - expectedEntrypoint: "run", - expectedTitle: "MongoDB", + expectedEntrypoint: "run", + expectedTitle: "MongoDB", }, { name: "Example without title", @@ -65,9 +61,9 @@ func TestModule(t *testing.T) { IsModule: false, Image: "mongodb:latest", }, - expectedContainerName: "mongodbContainer", - expectedEntrypoint: "run", - expectedTitle: "Mongodb", + + expectedEntrypoint: "run", + expectedTitle: "Mongodb", }, } @@ -77,7 +73,7 @@ func TestModule(t *testing.T) { assert.Equal(t, "mongodb", module.Lower()) assert.Equal(t, test.expectedTitle, module.Title()) - assert.Equal(t, test.expectedContainerName, module.ContainerName()) + assert.Equal(t, "Container", module.ContainerName()) assert.Equal(t, test.expectedEntrypoint, module.Entrypoint()) }) } @@ -148,7 +144,11 @@ func TestModule_Validate(outer *testing.T) { for _, test := range tests { outer.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expectedErr, test.module.Validate()) + if test.expectedErr != nil { + require.EqualError(t, test.module.Validate(), test.expectedErr.Error()) + } else { + require.NoError(t, test.module.Validate()) + } }) } } @@ -187,7 +187,7 @@ func TestGenerateWrongModuleName(t *testing.T) { for _, test := range tests { module := context.TestcontainersModule{ Name: test.name, - Image: "docker.io/example/" + test.name + ":latest", + Image: "example/" + test.name + ":latest", } err = internal.GenerateFiles(tmpCtx, module) @@ -231,7 +231,7 @@ func TestGenerateWrongModuleTitle(t *testing.T) { module := context.TestcontainersModule{ Name: "foo", TitleName: test.title, - Image: "docker.io/example/foo:latest", + Image: "example/foo:latest", } err = internal.GenerateFiles(tmpCtx, module) @@ -266,7 +266,7 @@ func TestGenerate(t *testing.T) { Name: "foodb4tw", TitleName: "FooDB4TheWin", IsModule: false, - Image: "docker.io/example/foodb:latest", + Image: "example/foodb:latest", } moduleNameLower := module.Lower() @@ -277,7 +277,7 @@ func TestGenerate(t *testing.T) { moduleDirFileInfo, err := os.Stat(moduleDirPath) require.NoError(t, err) // error nil implies the file exist - assert.True(t, moduleDirFileInfo.IsDir()) + require.True(t, moduleDirFileInfo.IsDir()) moduleDocFile := filepath.Join(examplesDocTmp, moduleNameLower+".md") _, err = os.Stat(moduleDocFile) @@ -322,7 +322,7 @@ func TestGenerateModule(t *testing.T) { Name: "foodb", TitleName: "FooDB", IsModule: true, - Image: "docker.io/example/foodb:latest", + Image: "example/foodb:latest", } moduleNameLower := module.Lower() @@ -333,7 +333,7 @@ func TestGenerateModule(t *testing.T) { moduleDirFileInfo, err := os.Stat(moduleDirPath) require.NoError(t, err) // error nil implies the file exist - assert.True(t, moduleDirFileInfo.IsDir()) + require.True(t, moduleDirFileInfo.IsDir()) moduleDocFile := filepath.Join(modulesDocTmp, moduleNameLower+".md") _, err = os.Stat(moduleDocFile) @@ -357,11 +357,13 @@ func TestGenerateModule(t *testing.T) { // assert content module file in the docs func assertModuleDocContent(t *testing.T, module context.TestcontainersModule, moduleDocFile string) { + t.Helper() content, err := os.ReadFile(moduleDocFile) require.NoError(t, err) lower := module.Lower() title := module.Title() + entrypoint := module.Entrypoint() data := sanitiseContent(content) assert.Equal(t, "# "+title, data[0]) @@ -372,7 +374,7 @@ func assertModuleDocContent(t *testing.T, module context.TestcontainersModule, m assert.Equal(t, "Please run the following command to add the "+title+" module to your Go dependencies:", data[10]) assert.Equal(t, "go get github.com/testcontainers/testcontainers-go/"+module.ParentDir()+"/"+lower, data[13]) assert.Equal(t, "", data[18]) - assert.Equal(t, "[Creating a "+title+" container](../../"+module.ParentDir()+"/"+lower+"/examples_test.go) inside_block:run"+title+"Container", data[19]) + assert.Equal(t, "[Creating a "+title+" container](../../"+module.ParentDir()+"/"+lower+"/examples_test.go) inside_block:Example"+entrypoint, data[19]) assert.Equal(t, "", data[20]) assert.Equal(t, "The "+title+" module exposes one entrypoint function to create the "+title+" container, and this function receives three parameters:", data[31]) assert.True(t, strings.HasSuffix(data[34], "(*"+title+"Container, error)")) @@ -382,18 +384,18 @@ func assertModuleDocContent(t *testing.T, module context.TestcontainersModule, m // assert content module test func assertExamplesTestContent(t *testing.T, module context.TestcontainersModule, examplesTestFile string) { + t.Helper() content, err := os.ReadFile(examplesTestFile) require.NoError(t, err) lower := module.Lower() entrypoint := module.Entrypoint() - title := module.Title() data := sanitiseContent(content) assert.Equal(t, "package "+lower+"_test", data[0]) - assert.Equal(t, "\t\"github.com/testcontainers/testcontainers-go/modules/"+lower+"\"", data[7]) - assert.Equal(t, "func Example"+entrypoint+"() {", data[10]) - assert.Equal(t, "\t// run"+title+"Container {", data[11]) + assert.Equal(t, "\t\"github.com/testcontainers/testcontainers-go\"", data[7]) + assert.Equal(t, "\t\"github.com/testcontainers/testcontainers-go/modules/"+lower+"\"", data[8]) + assert.Equal(t, "func Example"+entrypoint+"() {", data[11]) assert.Equal(t, "\t"+lower+"Container, err := "+lower+"."+entrypoint+"(ctx, \""+module.Image+"\")", data[14]) assert.Equal(t, "\tfmt.Println(state.Running)", data[32]) assert.Equal(t, "\t// Output:", data[34]) @@ -402,17 +404,19 @@ func assertExamplesTestContent(t *testing.T, module context.TestcontainersModule // assert content module test func assertModuleTestContent(t *testing.T, module context.TestcontainersModule, exampleTestFile string) { + t.Helper() content, err := os.ReadFile(exampleTestFile) require.NoError(t, err) data := sanitiseContent(content) assert.Equal(t, "package "+module.Lower()+"_test", data[0]) - assert.Equal(t, "func Test"+module.Title()+"(t *testing.T) {", data[9]) - assert.Equal(t, "\tcontainer, err := "+module.Lower()+"."+module.Entrypoint()+"(ctx, \""+module.Image+"\")", data[12]) + assert.Equal(t, "func Test"+module.Title()+"(t *testing.T) {", data[12]) + assert.Equal(t, "\tctr, err := "+module.Lower()+"."+module.Entrypoint()+"(ctx, \""+module.Image+"\")", data[15]) } // assert content module func assertModuleContent(t *testing.T, module context.TestcontainersModule, exampleFile string) { + t.Helper() content, err := os.ReadFile(exampleFile) require.NoError(t, err) @@ -422,19 +426,22 @@ func assertModuleContent(t *testing.T, module context.TestcontainersModule, exam entrypoint := module.Entrypoint() data := sanitiseContent(content) - assert.Equal(t, "package "+lower, data[0]) - assert.Equal(t, "// "+containerName+" represents the "+exampleName+" container type used in the module", data[9]) - assert.Equal(t, "type "+containerName+" struct {", data[10]) - assert.Equal(t, "// "+entrypoint+" creates an instance of the "+exampleName+" container type", data[14]) - assert.Equal(t, "func "+entrypoint+"(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*"+containerName+", error) {", data[15]) - assert.Equal(t, "\t\tImage: img,", data[17]) - assert.Equal(t, "\t\tif err := opt.Customize(&genericContainerReq); err != nil {", data[26]) - assert.Equal(t, "\t\t\treturn nil, fmt.Errorf(\"customize: %w\", err)", data[27]) - assert.Equal(t, "\treturn &"+containerName+"{Container: container}, nil", data[36]) + require.Equal(t, "package "+lower, data[0]) + require.Equal(t, "// Container represents the "+exampleName+" container type used in the module", data[9]) + require.Equal(t, "type "+containerName+" struct {", data[10]) + require.Equal(t, "// "+entrypoint+" creates an instance of the "+exampleName+" container type", data[14]) + require.Equal(t, "func "+entrypoint+"(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*"+containerName+", error) {", data[15]) + require.Equal(t, "\t\tImage: img,", data[17]) + require.Equal(t, "\t\tif err := opt.Customize(&genericContainerReq); err != nil {", data[26]) + require.Equal(t, "\t\t\treturn nil, fmt.Errorf(\"customize: %w\", err)", data[27]) + require.Equal(t, "\tvar c *"+containerName, data[32]) + require.Equal(t, "\t\tc = &"+containerName+"{Container: container}", data[34]) + require.Equal(t, "\treturn c, nil", data[41]) } // assert content GitHub workflow for the module func assertModuleGithubWorkflowContent(t *testing.T, moduleWorkflowFile string) { + t.Helper() content, err := os.ReadFile(moduleWorkflowFile) require.NoError(t, err) @@ -452,6 +459,7 @@ func assertModuleGithubWorkflowContent(t *testing.T, moduleWorkflowFile string) // assert content go.mod func assertGoModContent(t *testing.T, module context.TestcontainersModule, tcVersion string, goModFile string) { + t.Helper() content, err := os.ReadFile(goModFile) require.NoError(t, err) @@ -463,6 +471,7 @@ func assertGoModContent(t *testing.T, module context.TestcontainersModule, tcVer // assert content Makefile func assertMakefileContent(t *testing.T, module context.TestcontainersModule, makefile string) { + t.Helper() content, err := os.ReadFile(makefile) require.NoError(t, err) @@ -472,6 +481,7 @@ func assertMakefileContent(t *testing.T, module context.TestcontainersModule, ma // assert content in the nav items from mkdocs.yml func assertMkdocsNavItems(t *testing.T, module context.TestcontainersModule, originalConfig *mkdocs.Config, tmpCtx context.Context) { + t.Helper() config, err := mkdocs.ReadConfig(tmpCtx.MkdocsConfigFile()) require.NoError(t, err) @@ -484,7 +494,7 @@ func assertMkdocsNavItems(t *testing.T, module context.TestcontainersModule, ori expectedEntries = originalConfig.Nav[3].Modules } - assert.Len(t, navItems, len(expectedEntries)+1) + require.Len(t, navItems, len(expectedEntries)+1) // the module should be in the nav found := false diff --git a/modulegen/mkdocs_test.go b/modulegen/mkdocs_test.go index 5fcf7c93ba..96391769de 100644 --- a/modulegen/mkdocs_test.go +++ b/modulegen/mkdocs_test.go @@ -40,9 +40,9 @@ func TestReadMkDocsConfig(t *testing.T) { require.NoError(t, err) require.NotNil(t, config) - assert.Equal(t, "Testcontainers for Go", config.SiteName) - assert.Equal(t, "https://github.com/testcontainers/testcontainers-go", config.RepoURL) - assert.Equal(t, "edit/main/docs/", config.EditURI) + require.Equal(t, "Testcontainers for Go", config.SiteName) + require.Equal(t, "https://github.com/testcontainers/testcontainers-go", config.RepoURL) + require.Equal(t, "edit/main/docs/", config.EditURI) // theme theme := config.Theme @@ -51,9 +51,9 @@ func TestReadMkDocsConfig(t *testing.T) { // nav bar nav := config.Nav assert.Equal(t, "index.md", nav[0].Home) - assert.NotEmpty(t, nav[2].Features) - assert.NotEmpty(t, nav[3].Modules) - assert.NotEmpty(t, nav[4].Examples) + require.NotEmpty(t, nav[2].Features) + require.NotEmpty(t, nav[3].Modules) + require.NotEmpty(t, nav[4].Examples) } func TestNavItems(t *testing.T) { @@ -64,7 +64,7 @@ func TestNavItems(t *testing.T) { require.NoError(t, err) // we have to remove the index.md file from the examples docs - assert.Len(t, examples, len(examplesDocs)-1) + require.Len(t, examples, len(examplesDocs)-1) // all example modules exist in the documentation for _, example := range examples { @@ -82,6 +82,7 @@ func TestNavItems(t *testing.T) { } func copyInitialMkdocsConfig(t *testing.T, tmpCtx context.Context) error { + t.Helper() ctx := getTestRootContext(t) return mkdocs.CopyConfig(ctx.MkdocsConfigFile(), tmpCtx.MkdocsConfigFile()) } diff --git a/modules/artemis/artemis.go b/modules/artemis/artemis.go index 9edfcaa527..e74b542cd7 100644 --- a/modules/artemis/artemis.go +++ b/modules/artemis/artemis.go @@ -81,7 +81,7 @@ func WithExtraArgs(args string) testcontainers.CustomizeRequestOption { // Deprecated: use Run instead. // RunContainer creates an instance of the Artemis container type. func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*Container, error) { - return Run(ctx, "docker.io/apache/activemq-artemis:2.30.0-alpine", opts...) + return Run(ctx, "apache/activemq-artemis:2.30.0-alpine", opts...) } // Run creates an instance of the Artemis container type with a given image @@ -109,12 +109,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, req) + var c *Container + if container != nil { + c = &Container{Container: container} + } if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - user := req.Env["ARTEMIS_USER"] - password := req.Env["ARTEMIS_PASSWORD"] + c.user = req.Env["ARTEMIS_USER"] + c.password = req.Env["ARTEMIS_PASSWORD"] - return &Container{Container: container, user: user, password: password}, nil + return c, nil } diff --git a/modules/artemis/artemis_test.go b/modules/artemis/artemis_test.go index c2767790ad..70dcab9440 100644 --- a/modules/artemis/artemis_test.go +++ b/modules/artemis/artemis_test.go @@ -57,6 +57,7 @@ func TestArtemis(t *testing.T) { user: "artemis", pass: "artemis", hook: func(t *testing.T, container *artemis.Container) { + t.Helper() expectQueue(t, container, "ArgsTestQueue") }, }, @@ -64,30 +65,30 @@ func TestArtemis(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - container, err := artemis.Run(ctx, "docker.io/apache/activemq-artemis:2.30.0-alpine", test.opts...) + ctr, err := artemis.Run(ctx, "apache/activemq-artemis:2.30.0-alpine", test.opts...) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, container.Terminate(ctx), "failed to terminate container") }) // consoleURL { - u, err := container.ConsoleURL(ctx) + u, err := ctr.ConsoleURL(ctx) // } require.NoError(t, err) res, err := http.Get(u) require.NoError(t, err, "failed to access console") res.Body.Close() - assert.Equal(t, http.StatusOK, res.StatusCode, "failed to access console") + require.Equal(t, http.StatusOK, res.StatusCode, "failed to access console") if test.user != "" { - assert.Equal(t, test.user, container.User(), "unexpected user") + assert.Equal(t, test.user, ctr.User(), "unexpected user") } if test.pass != "" { - assert.Equal(t, test.pass, container.Password(), "unexpected password") + assert.Equal(t, test.pass, ctr.Password(), "unexpected password") } // brokerEndpoint { - host, err := container.BrokerEndpoint(ctx) + host, err := ctr.BrokerEndpoint(ctx) // } require.NoError(t, err) @@ -116,7 +117,7 @@ func TestArtemis(t *testing.T) { } if test.hook != nil { - test.hook(t, container) + test.hook(t, ctr) } }) } diff --git a/modules/artemis/examples_test.go b/modules/artemis/examples_test.go index e1c21f3afc..04e973a013 100644 --- a/modules/artemis/examples_test.go +++ b/modules/artemis/examples_test.go @@ -7,6 +7,7 @@ import ( "github.com/go-stomp/stomp/v3" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/artemis" ) @@ -15,22 +16,24 @@ func ExampleRun() { ctx := context.Background() artemisContainer, err := artemis.Run(ctx, - "docker.io/apache/activemq-artemis:2.30.0", + "apache/activemq-artemis:2.30.0", artemis.WithCredentials("test", "test"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - if err := artemisContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(artemisContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := artemisContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -39,7 +42,8 @@ func ExampleRun() { // Get broker endpoint. host, err := artemisContainer.BrokerEndpoint(ctx) if err != nil { - log.Fatalf("failed to get broker endpoint: %s", err) + log.Printf("failed to get broker endpoint: %s", err) + return } // containerUser { @@ -52,11 +56,12 @@ func ExampleRun() { // Connect to Artemis via STOMP. conn, err := stomp.Dial("tcp", host, stomp.ConnOpt.Login(user, pass)) if err != nil { - log.Fatalf("failed to connect to Artemis: %s", err) + log.Printf("failed to connect to Artemis: %s", err) + return } defer func() { if err := conn.Disconnect(); err != nil { - log.Fatalf("failed to disconnect from Artemis: %s", err) + log.Printf("failed to disconnect from Artemis: %s", err) } }() // } diff --git a/modules/artemis/go.mod b/modules/artemis/go.mod index adb9dddf08..68ed4a2090 100644 --- a/modules/artemis/go.mod +++ b/modules/artemis/go.mod @@ -6,7 +6,7 @@ require ( github.com/docker/go-connections v0.5.0 github.com/go-stomp/stomp/v3 v3.0.5 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -17,7 +17,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -52,9 +52,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/artemis/go.sum b/modules/artemis/go.sum index 69f6d52997..8c89aa58bb 100644 --- a/modules/artemis/go.sum +++ b/modules/artemis/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -101,6 +101,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -135,8 +137,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -164,15 +166,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/azurite/azurite.go b/modules/azurite/azurite.go index 1dcdae4709..c3172fd58a 100644 --- a/modules/azurite/azurite.go +++ b/modules/azurite/azurite.go @@ -121,9 +121,14 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *AzuriteContainer + if container != nil { + c = &AzuriteContainer{Container: container, Settings: settings} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &AzuriteContainer{Container: container, Settings: settings}, nil + return c, nil } diff --git a/modules/azurite/azurite_test.go b/modules/azurite/azurite_test.go index 8fe5946e2f..618fc28b0b 100644 --- a/modules/azurite/azurite_test.go +++ b/modules/azurite/azurite_test.go @@ -4,23 +4,18 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/azurite" ) func TestAzurite(t *testing.T) { ctx := context.Background() - container, err := azurite.Run(ctx, "mcr.microsoft.com/azure-storage/azurite:3.23.0") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := azurite.Run(ctx, "mcr.microsoft.com/azure-storage/azurite:3.23.0") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions } diff --git a/modules/azurite/examples_test.go b/modules/azurite/examples_test.go index d2fc9965f0..567685891f 100644 --- a/modules/azurite/examples_test.go +++ b/modules/azurite/examples_test.go @@ -12,6 +12,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/azurite" ) @@ -23,21 +24,21 @@ func ExampleRun() { ctx, "mcr.microsoft.com/azure-storage/azurite:3.28.0", ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := azuriteContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(azuriteContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := azuriteContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -57,20 +58,21 @@ func ExampleRun_blobOperations() { "mcr.microsoft.com/azure-storage/azurite:3.28.0", azurite.WithInMemoryPersistence(64), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := azuriteContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(azuriteContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // using the built-in shared key credential type cred, err := azblob.NewSharedKeyCredential(azurite.AccountName, azurite.AccountKey) if err != nil { - log.Fatalf("failed to create shared key credential: %s", err) // nolint:gocritic + log.Printf("failed to create shared key credential: %s", err) + return } // create an azblob.Client for the specified storage account that uses the above credentials @@ -78,14 +80,16 @@ func ExampleRun_blobOperations() { client, err := azblob.NewClientWithSharedKeyCredential(blobServiceURL, cred, nil) if err != nil { - log.Fatalf("failed to create client: %s", err) // nolint:gocritic + log.Printf("failed to create client: %s", err) + return } // ===== 1. Create a container ===== containerName := "testcontainer" _, err = client.CreateContainer(context.TODO(), containerName, nil) if err != nil { - log.Fatalf("failed to create container: %s", err) + log.Printf("failed to create container: %s", err) + return } // ===== 2. Upload and Download a block blob ===== @@ -101,13 +105,15 @@ func ExampleRun_blobOperations() { Tags: map[string]string{"Year": "2022"}, }) if err != nil { - log.Fatalf("failed to upload blob: %s", err) + log.Printf("failed to upload blob: %s", err) + return } // Download the blob's contents and ensure that the download worked properly blobDownloadResponse, err := client.DownloadStream(context.TODO(), containerName, blobName, nil) if err != nil { - log.Fatalf("failed to download blob: %s", err) // nolint:gocritic + log.Printf("failed to download blob: %s", err) + return } // Use the bytes.Buffer object to read the downloaded data. @@ -115,7 +121,8 @@ func ExampleRun_blobOperations() { reader := blobDownloadResponse.Body downloadData, err := io.ReadAll(reader) if err != nil { - log.Fatalf("failed to read downloaded data: %s", err) // nolint:gocritic + log.Printf("failed to read downloaded data: %s", err) + return } fmt.Println(string(downloadData)) @@ -133,7 +140,8 @@ func ExampleRun_blobOperations() { for pager.More() { resp, err := pager.NextPage(context.TODO()) if err != nil { - log.Fatalf("failed to list blobs: %s", err) + log.Printf("failed to list blobs: %s", err) + return } fmt.Println(len(resp.Segment.BlobItems)) @@ -142,13 +150,15 @@ func ExampleRun_blobOperations() { // Delete the blob. _, err = client.DeleteBlob(context.TODO(), containerName, blobName, nil) if err != nil { - log.Fatalf("failed to delete blob: %s", err) + log.Printf("failed to delete blob: %s", err) + return } // Delete the container. _, err = client.DeleteContainer(context.TODO(), containerName, nil) if err != nil { - log.Fatalf("failed to delete container: %s", err) + log.Printf("failed to delete container: %s", err) + return } // } @@ -169,20 +179,21 @@ func ExampleRun_queueOperations() { "mcr.microsoft.com/azure-storage/azurite:3.28.0", azurite.WithInMemoryPersistence(64), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := azuriteContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(azuriteContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // using the built-in shared key credential type cred, err := azqueue.NewSharedKeyCredential(azurite.AccountName, azurite.AccountKey) if err != nil { - log.Fatalf("failed to create shared key credential: %s", err) // nolint:gocritic + log.Printf("failed to create shared key credential: %s", err) + return } // create an azqueue.Client for the specified storage account that uses the above credentials @@ -190,7 +201,8 @@ func ExampleRun_queueOperations() { client, err := azqueue.NewServiceClientWithSharedKeyCredential(queueServiceURL, cred, nil) if err != nil { - log.Fatalf("failed to create client: %s", err) + log.Printf("failed to create client: %s", err) + return } queueName := "testqueue" @@ -199,7 +211,8 @@ func ExampleRun_queueOperations() { Metadata: map[string]*string{"hello": to.Ptr("world")}, }) if err != nil { - log.Fatalf("failed to create queue: %s", err) + log.Printf("failed to create queue: %s", err) + return } pager := client.NewListQueuesPager(&azqueue.ListQueuesOptions{ @@ -210,7 +223,8 @@ func ExampleRun_queueOperations() { for pager.More() { resp, err := pager.NextPage(context.Background()) if err != nil { - log.Fatalf("failed to list queues: %s", err) + log.Printf("failed to list queues: %s", err) + return } fmt.Println(len(resp.Queues)) @@ -220,7 +234,8 @@ func ExampleRun_queueOperations() { // delete the queue _, err = client.DeleteQueue(context.TODO(), queueName, &azqueue.DeleteOptions{}) if err != nil { - log.Fatalf("failed to delete queue: %s", err) + log.Printf("failed to delete queue: %s", err) + return } // } @@ -241,20 +256,21 @@ func ExampleRun_tableOperations() { "mcr.microsoft.com/azure-storage/azurite:3.28.0", azurite.WithInMemoryPersistence(64), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := azuriteContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(azuriteContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // using the built-in shared key credential type cred, err := aztables.NewSharedKeyCredential(azurite.AccountName, azurite.AccountKey) if err != nil { - log.Fatalf("failed to create shared key credential: %s", err) // nolint:gocritic + log.Printf("failed to create shared key credential: %s", err) + return } // create an aztables.Client for the specified storage account that uses the above credentials @@ -262,14 +278,16 @@ func ExampleRun_tableOperations() { client, err := aztables.NewServiceClientWithSharedKey(tablesServiceURL, cred, nil) if err != nil { - log.Fatalf("failed to create client: %s", err) + log.Printf("failed to create client: %s", err) + return } tableName := "fromServiceClient" // Create a table _, err = client.CreateTable(context.TODO(), tableName, nil) if err != nil { - log.Fatalf("failed to create table: %s", err) + log.Printf("failed to create table: %s", err) + return } // List tables @@ -277,7 +295,8 @@ func ExampleRun_tableOperations() { for pager.More() { resp, err := pager.NextPage(context.Background()) if err != nil { - log.Fatalf("failed to list tables: %s", err) + log.Printf("failed to list tables: %s", err) + return } fmt.Println(len(resp.Tables)) @@ -287,7 +306,8 @@ func ExampleRun_tableOperations() { // Delete a table _, err = client.DeleteTable(context.TODO(), tableName, nil) if err != nil { - panic(err) + fmt.Println(err) + return } // } diff --git a/modules/azurite/go.mod b/modules/azurite/go.mod index 826a457a9a..451d3fef9c 100644 --- a/modules/azurite/go.mod +++ b/modules/azurite/go.mod @@ -8,7 +8,8 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.0 github.com/docker/go-connections v0.5.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -20,7 +21,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect @@ -31,6 +33,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -42,6 +45,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -53,12 +57,13 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/azurite/go.sum b/modules/azurite/go.sum index 90f8568088..abcbc8cec7 100644 --- a/modules/azurite/go.sum +++ b/modules/azurite/go.sum @@ -30,8 +30,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -73,6 +74,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -103,6 +108,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -114,6 +121,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -147,8 +156,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -170,14 +179,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -197,6 +206,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/cassandra/cassandra.go b/modules/cassandra/cassandra.go index c5dbfc61d6..e63d1c7e97 100644 --- a/modules/cassandra/cassandra.go +++ b/modules/cassandra/cassandra.go @@ -2,6 +2,7 @@ package cassandra import ( "context" + "fmt" "io" "path/filepath" "strings" @@ -114,9 +115,14 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *CassandraContainer + if container != nil { + c = &CassandraContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &CassandraContainer{Container: container}, nil + return c, nil } diff --git a/modules/cassandra/cassandra_test.go b/modules/cassandra/cassandra_test.go index a878db4f6f..f4979dff5f 100644 --- a/modules/cassandra/cassandra_test.go +++ b/modules/cassandra/cassandra_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/cassandra" ) @@ -20,26 +21,18 @@ type Test struct { func TestCassandra(t *testing.T) { ctx := context.Background() - container, err := cassandra.Run(ctx, "cassandra:4.1.3") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) + ctr, err := cassandra.Run(ctx, "cassandra:4.1.3") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // connectionString { - connectionHost, err := container.ConnectionHost(ctx) + connectionHost, err := ctr.ConnectionHost(ctx) // } require.NoError(t, err) cluster := gocql.NewCluster(connectionHost) session, err := cluster.CreateSession() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer session.Close() // perform assertions @@ -60,24 +53,16 @@ func TestCassandra(t *testing.T) { func TestCassandraWithConfigFile(t *testing.T) { ctx := context.Background() - container, err := cassandra.Run(ctx, "cassandra:4.1.3", cassandra.WithConfigFile(filepath.Join("testdata", "config.yaml"))) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) + ctr, err := cassandra.Run(ctx, "cassandra:4.1.3", cassandra.WithConfigFile(filepath.Join("testdata", "config.yaml"))) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - connectionHost, err := container.ConnectionHost(ctx) + connectionHost, err := ctr.ConnectionHost(ctx) require.NoError(t, err) cluster := gocql.NewCluster(connectionHost) session, err := cluster.CreateSession() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer session.Close() var result string @@ -91,27 +76,19 @@ func TestCassandraWithInitScripts(t *testing.T) { ctx := context.Background() // withInitScripts { - container, err := cassandra.Run(ctx, "cassandra:4.1.3", cassandra.WithInitScripts(filepath.Join("testdata", "init.cql"))) + ctr, err := cassandra.Run(ctx, "cassandra:4.1.3", cassandra.WithInitScripts(filepath.Join("testdata", "init.cql"))) // } - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // connectionHost { - connectionHost, err := container.ConnectionHost(ctx) + connectionHost, err := ctr.ConnectionHost(ctx) // } require.NoError(t, err) cluster := gocql.NewCluster(connectionHost) session, err := cluster.CreateSession() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer session.Close() var test Test @@ -123,24 +100,16 @@ func TestCassandraWithInitScripts(t *testing.T) { t.Run("with init bash script", func(t *testing.T) { ctx := context.Background() - container, err := cassandra.Run(ctx, "cassandra:4.1.3", cassandra.WithInitScripts(filepath.Join("testdata", "init.sh"))) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) + ctr, err := cassandra.Run(ctx, "cassandra:4.1.3", cassandra.WithInitScripts(filepath.Join("testdata", "init.sh"))) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - connectionHost, err := container.ConnectionHost(ctx) + connectionHost, err := ctr.ConnectionHost(ctx) require.NoError(t, err) cluster := gocql.NewCluster(connectionHost) session, err := cluster.CreateSession() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer session.Close() var test Test diff --git a/modules/cassandra/examples_test.go b/modules/cassandra/examples_test.go index f80cb3f666..68a80589ea 100644 --- a/modules/cassandra/examples_test.go +++ b/modules/cassandra/examples_test.go @@ -8,6 +8,7 @@ import ( "github.com/gocql/gocql" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/cassandra" ) @@ -20,41 +21,44 @@ func ExampleRun() { cassandra.WithInitScripts(filepath.Join("testdata", "init.cql")), cassandra.WithConfigFile(filepath.Join("testdata", "config.yaml")), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := cassandraContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(cassandraContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := cassandraContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) connectionHost, err := cassandraContainer.ConnectionHost(ctx) if err != nil { - log.Fatalf("failed to get connection host: %s", err) + log.Printf("failed to get connection host: %s", err) + return } cluster := gocql.NewCluster(connectionHost) session, err := cluster.CreateSession() if err != nil { - log.Fatalf("failed to create session: %s", err) + log.Printf("failed to create session: %s", err) + return } defer session.Close() var version string err = session.Query("SELECT release_version FROM system.local").Scan(&version) if err != nil { - log.Fatalf("failed to query: %s", err) + log.Printf("failed to query: %s", err) + return } fmt.Println(version) diff --git a/modules/cassandra/go.mod b/modules/cassandra/go.mod index fce3c2d919..8b0cf9a058 100644 --- a/modules/cassandra/go.mod +++ b/modules/cassandra/go.mod @@ -6,7 +6,7 @@ require ( github.com/docker/go-connections v0.5.0 github.com/gocql/gocql v1.6.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -17,7 +17,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -54,9 +54,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/modules/cassandra/go.sum b/modules/cassandra/go.sum index 775e48ecf5..4937474970 100644 --- a/modules/cassandra/go.sum +++ b/modules/cassandra/go.sum @@ -18,8 +18,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -107,6 +107,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -141,8 +143,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -164,14 +166,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/chroma/chroma.go b/modules/chroma/chroma.go index d0d633f390..e1c3d6e3bc 100644 --- a/modules/chroma/chroma.go +++ b/modules/chroma/chroma.go @@ -2,6 +2,7 @@ package chroma import ( "context" + "errors" "fmt" "github.com/testcontainers/testcontainers-go" @@ -45,11 +46,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *ChromaContainer + if container != nil { + c = &ChromaContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &ChromaContainer{Container: container}, nil + return c, nil } // RESTEndpoint returns the REST endpoint of the Chroma container @@ -61,7 +67,7 @@ func (c *ChromaContainer) RESTEndpoint(ctx context.Context) (string, error) { host, err := c.Host(ctx) if err != nil { - return "", fmt.Errorf("failed to get container host") + return "", errors.New("failed to get container host") } return fmt.Sprintf("http://%s:%s", host, containerPort.Port()), nil diff --git a/modules/chroma/chroma_test.go b/modules/chroma/chroma_test.go index 0e33d059f7..5d975c3602 100644 --- a/modules/chroma/chroma_test.go +++ b/modules/chroma/chroma_test.go @@ -8,55 +8,38 @@ import ( chromago "github.com/amikos-tech/chroma-go" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/chroma" ) func TestChroma(t *testing.T) { ctx := context.Background() - container, err := chroma.Run(ctx, "chromadb/chroma:0.4.24") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := chroma.Run(ctx, "chromadb/chroma:0.4.24") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) t.Run("REST Endpoint retrieve docs site", func(tt *testing.T) { // restEndpoint { - restEndpoint, err := container.RESTEndpoint(ctx) + restEndpoint, err := ctr.RESTEndpoint(ctx) // } - if err != nil { - tt.Fatalf("failed to get REST endpoint: %s", err) - } + require.NoErrorf(tt, err, "failed to get REST endpoint") cli := &http.Client{} resp, err := cli.Get(restEndpoint + "/docs") - if err != nil { - tt.Fatalf("failed to perform GET request: %s", err) - } + require.NoErrorf(tt, err, "failed to perform GET request") defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - tt.Fatalf("unexpected status code: %d", resp.StatusCode) - } + require.Equalf(tt, http.StatusOK, resp.StatusCode, "unexpected status code: %d", resp.StatusCode) }) t.Run("GetClient", func(tt *testing.T) { // restEndpoint { - endpoint, err := container.RESTEndpoint(context.Background()) - if err != nil { - tt.Fatalf("failed to get REST endpoint: %s", err) // nolint:gocritic - } + endpoint, err := ctr.RESTEndpoint(context.Background()) + require.NoErrorf(tt, err, "failed to get REST endpoint") chromaClient, err := chromago.NewClient(endpoint) // } - if err != nil { - tt.Fatalf("failed to create client: %s", err) - } + require.NoErrorf(tt, err, "failed to create client") hb, err := chromaClient.Heartbeat(context.TODO()) require.NoError(tt, err) diff --git a/modules/chroma/examples_test.go b/modules/chroma/examples_test.go index 1828c1eef4..a44125b242 100644 --- a/modules/chroma/examples_test.go +++ b/modules/chroma/examples_test.go @@ -17,21 +17,21 @@ func ExampleRun() { ctx := context.Background() chromaContainer, err := chroma.Run(ctx, "chromadb/chroma:0.4.24") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := chromaContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(chromaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := chromaContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -45,24 +45,25 @@ func ExampleChromaContainer_connectWithClient() { ctx := context.Background() chromaContainer, err := chroma.Run(ctx, "chromadb/chroma:0.4.24") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := chromaContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(chromaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } endpoint, err := chromaContainer.RESTEndpoint(context.Background()) if err != nil { - log.Fatalf("failed to get REST endpoint: %s", err) // nolint:gocritic + log.Printf("failed to get REST endpoint: %s", err) + return } chromaClient, err := chromago.NewClient(endpoint) if err != nil { - log.Fatalf("failed to get client: %s", err) // nolint:gocritic + log.Printf("failed to get client: %s", err) + return } hbs, errHb := chromaClient.Heartbeat(context.Background()) @@ -82,32 +83,35 @@ func ExampleChromaContainer_collections() { ctx := context.Background() chromaContainer, err := chroma.Run(ctx, "chromadb/chroma:0.4.24", testcontainers.WithEnv(map[string]string{"ALLOW_RESET": "true"})) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := chromaContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(chromaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // getClient { // create the client connection and confirm that we can access the server with it endpoint, err := chromaContainer.RESTEndpoint(context.Background()) if err != nil { - log.Fatalf("failed to get REST endpoint: %s", err) // nolint:gocritic + log.Printf("failed to get REST endpoint: %s", err) + return } chromaClient, err := chromago.NewClient(endpoint) // } if err != nil { - log.Fatalf("failed to get client: %s", err) // nolint:gocritic + log.Printf("failed to get client: %s", err) + return } // reset { reset, err := chromaClient.Reset(context.Background()) // } if err != nil { - log.Fatalf("failed to reset: %s", err) // nolint:gocritic + log.Printf("failed to reset: %s", err) + return } fmt.Printf("Reset successful: %v\n", reset) @@ -116,7 +120,8 @@ func ExampleChromaContainer_collections() { col, err := chromaClient.CreateCollection(context.Background(), "test-collection", map[string]any{}, true, types.NewConsistentHashEmbeddingFunction(), types.L2) // } if err != nil { - log.Fatalf("failed to create collection: %s", err) // nolint:gocritic + log.Printf("failed to create collection: %s", err) + return } fmt.Println("Collection created:", col.Name) @@ -132,7 +137,8 @@ func ExampleChromaContainer_collections() { ) // } if err != nil { - log.Fatalf("failed to add data to collection: %s", err) // nolint:gocritic + log.Printf("failed to add data to collection: %s", err) + return } fmt.Println(col1.Count(context.Background())) @@ -147,7 +153,8 @@ func ExampleChromaContainer_collections() { ) // } if err != nil { - log.Fatalf("failed to query collection: %s", err) // nolint:gocritic + log.Printf("failed to query collection: %s", err) + return } fmt.Printf("Result of query: %v\n", queryResults) @@ -156,7 +163,8 @@ func ExampleChromaContainer_collections() { cols, err := chromaClient.ListCollections(context.Background()) // } if err != nil { - log.Fatalf("failed to list collections: %s", err) // nolint:gocritic + log.Printf("failed to list collections: %s", err) + return } fmt.Println(len(cols)) @@ -165,7 +173,8 @@ func ExampleChromaContainer_collections() { _, err = chromaClient.DeleteCollection(context.Background(), "test-collection") // } if err != nil { - log.Fatalf("failed to delete collection: %s", err) // nolint:gocritic + log.Printf("failed to delete collection: %s", err) + return } fmt.Println(err) diff --git a/modules/chroma/go.mod b/modules/chroma/go.mod index 2c03baa164..aece8e62e9 100644 --- a/modules/chroma/go.mod +++ b/modules/chroma/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/amikos-tech/chroma-go v0.1.2 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -17,7 +17,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -55,9 +55,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/chroma/go.sum b/modules/chroma/go.sum index e7d70f0539..c7325de7ae 100644 --- a/modules/chroma/go.sum +++ b/modules/chroma/go.sum @@ -18,8 +18,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -103,6 +103,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -136,8 +138,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -159,14 +161,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/clickhouse/clickhouse.go b/modules/clickhouse/clickhouse.go index 88b8b82d4b..43b41110b8 100644 --- a/modules/clickhouse/clickhouse.go +++ b/modules/clickhouse/clickhouse.go @@ -249,13 +249,17 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *ClickHouseContainer + if container != nil { + c = &ClickHouseContainer{Container: container} + } if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - user := req.Env["CLICKHOUSE_USER"] - password := req.Env["CLICKHOUSE_PASSWORD"] - dbName := req.Env["CLICKHOUSE_DB"] + c.User = req.Env["CLICKHOUSE_USER"] + c.Password = req.Env["CLICKHOUSE_PASSWORD"] + c.DbName = req.Env["CLICKHOUSE_DB"] - return &ClickHouseContainer{Container: container, DbName: dbName, Password: password, User: user}, nil + return c, nil } diff --git a/modules/clickhouse/clickhouse_test.go b/modules/clickhouse/clickhouse_test.go index a581e8d4c5..69d05a2abc 100644 --- a/modules/clickhouse/clickhouse_test.go +++ b/modules/clickhouse/clickhouse_test.go @@ -10,7 +10,6 @@ import ( "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "github.com/cenkalti/backoff/v4" "github.com/docker/go-connections/nat" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" @@ -31,29 +30,23 @@ type Test struct { func TestClickHouseDefaultConfig(t *testing.T) { ctx := context.Background() - container, err := clickhouse.Run(ctx, "clickhouse/clickhouse-server:23.3.8.21-alpine") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) + ctr, err := clickhouse.Run(ctx, "clickhouse/clickhouse-server:23.3.8.21-alpine") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - connectionHost, err := container.ConnectionHost(ctx) + connectionHost, err := ctr.ConnectionHost(ctx) require.NoError(t, err) conn, err := ch.Open(&ch.Options{ Addr: []string{connectionHost}, Auth: ch.Auth{ - Database: container.DbName, - Username: container.User, - Password: container.Password, + Database: ctr.DbName, + Username: ctr.User, + Password: ctr.Password, }, }) require.NoError(t, err) - assert.NotNil(t, conn) + require.NotNil(t, conn) defer conn.Close() err = conn.Ping(context.Background()) @@ -63,23 +56,17 @@ func TestClickHouseDefaultConfig(t *testing.T) { func TestClickHouseConnectionHost(t *testing.T) { ctx := context.Background() - container, err := clickhouse.Run(ctx, + ctr, err := clickhouse.Run(ctx, "clickhouse/clickhouse-server:23.3.8.21-alpine", clickhouse.WithUsername(user), clickhouse.WithPassword(password), clickhouse.WithDatabase(dbname), ) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // connectionHost { - connectionHost, err := container.ConnectionHost(ctx) + connectionHost, err := ctr.ConnectionHost(ctx) // } require.NoError(t, err) @@ -92,74 +79,63 @@ func TestClickHouseConnectionHost(t *testing.T) { }, }) require.NoError(t, err) - assert.NotNil(t, conn) + require.NotNil(t, conn) defer conn.Close() // perform assertions data, err := performCRUD(t, conn) require.NoError(t, err) - assert.Len(t, data, 1) + require.Len(t, data, 1) } func TestClickHouseDSN(t *testing.T) { ctx := context.Background() - container, err := clickhouse.Run(ctx, + ctr, err := clickhouse.Run(ctx, "clickhouse/clickhouse-server:23.3.8.21-alpine", clickhouse.WithUsername(user), clickhouse.WithPassword(password), clickhouse.WithDatabase(dbname), ) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // connectionString { - connectionString, err := container.ConnectionString(ctx, "debug=true") + connectionString, err := ctr.ConnectionString(ctx, "debug=true") // } require.NoError(t, err) opts, err := ch.ParseDSN(connectionString) require.NoError(t, err) + opts.Debugf = t.Logf conn, err := ch.Open(opts) require.NoError(t, err) - assert.NotNil(t, conn) + require.NotNil(t, conn) defer conn.Close() // perform assertions data, err := performCRUD(t, conn) require.NoError(t, err) - assert.Len(t, data, 1) + require.Len(t, data, 1) } func TestClickHouseWithInitScripts(t *testing.T) { ctx := context.Background() // withInitScripts { - container, err := clickhouse.Run(ctx, + ctr, err := clickhouse.Run(ctx, "clickhouse/clickhouse-server:23.3.8.21-alpine", clickhouse.WithUsername(user), clickhouse.WithPassword(password), clickhouse.WithDatabase(dbname), clickhouse.WithInitScripts(filepath.Join("testdata", "init-db.sh")), ) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // } - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) - - connectionHost, err := container.ConnectionHost(ctx) + connectionHost, err := ctr.ConnectionHost(ctx) require.NoError(t, err) conn, err := ch.Open(&ch.Options{ @@ -171,13 +147,13 @@ func TestClickHouseWithInitScripts(t *testing.T) { }, }) require.NoError(t, err) - assert.NotNil(t, conn) + require.NotNil(t, conn) defer conn.Close() // perform assertions data, err := getAllRows(conn) require.NoError(t, err) - assert.Len(t, data, 1) + require.Len(t, data, 1) } func TestClickHouseWithConfigFile(t *testing.T) { @@ -192,23 +168,17 @@ func TestClickHouseWithConfigFile(t *testing.T) { } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { - container, err := clickhouse.Run(ctx, + ctr, err := clickhouse.Run(ctx, "clickhouse/clickhouse-server:23.3.8.21-alpine", clickhouse.WithUsername(user), clickhouse.WithPassword(""), clickhouse.WithDatabase(dbname), tC.configOption, ) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - connectionHost, err := container.ConnectionHost(ctx) + connectionHost, err := ctr.ConnectionHost(ctx) require.NoError(t, err) conn, err := ch.Open(&ch.Options{ @@ -220,13 +190,13 @@ func TestClickHouseWithConfigFile(t *testing.T) { }, }) require.NoError(t, err) - assert.NotNil(t, conn) + require.NotNil(t, conn) defer conn.Close() // perform assertions data, err := performCRUD(t, conn) require.NoError(t, err) - assert.Len(t, data, 1) + require.Len(t, data, 1) }) } } @@ -245,34 +215,24 @@ func TestClickHouseWithZookeeper(t *testing.T) { }, Started: true, }) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, zkcontainer) + require.NoError(t, err) ipaddr, err := zkcontainer.ContainerIP(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - container, err := clickhouse.Run(ctx, + ctr, err := clickhouse.Run(ctx, "clickhouse/clickhouse-server:23.3.8.21-alpine", clickhouse.WithUsername(user), clickhouse.WithPassword(password), clickhouse.WithDatabase(dbname), clickhouse.WithZookeeper(ipaddr, zkPort.Port()), ) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // } - // Clean up the container after the test is complete - t.Cleanup(func() { - require.NoError(t, container.Terminate(ctx)) - require.NoError(t, zkcontainer.Terminate(ctx)) - }) - - connectionHost, err := container.ConnectionHost(ctx) + connectionHost, err := ctr.ConnectionHost(ctx) require.NoError(t, err) conn, err := ch.Open(&ch.Options{ @@ -284,16 +244,17 @@ func TestClickHouseWithZookeeper(t *testing.T) { }, }) require.NoError(t, err) - assert.NotNil(t, conn) + require.NotNil(t, conn) defer conn.Close() // perform assertions data, err := performReplicatedCRUD(t, conn) require.NoError(t, err) - assert.Len(t, data, 1) + require.Len(t, data, 1) } func performReplicatedCRUD(t *testing.T, conn driver.Conn) ([]Test, error) { + t.Helper() return backoff.RetryNotifyWithData( func() ([]Test, error) { err := conn.Exec(context.Background(), "CREATE TABLE replicated_test_table (id UInt64) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/mdb.data_transfer_cp_cdc', '{replica}') PRIMARY KEY (id) ORDER BY (id) SETTINGS index_granularity = 8192;") @@ -332,6 +293,7 @@ func performReplicatedCRUD(t *testing.T, conn driver.Conn) ([]Test, error) { } func performCRUD(t *testing.T, conn driver.Conn) ([]Test, error) { + t.Helper() return backoff.RetryNotifyWithData( func() ([]Test, error) { err := conn.Exec(context.Background(), "create table if not exists test_table (id UInt64) engine = MergeTree PRIMARY KEY (id) ORDER BY (id) SETTINGS index_granularity = 8192;") diff --git a/modules/clickhouse/examples_test.go b/modules/clickhouse/examples_test.go index d63c440541..bc031f134d 100644 --- a/modules/clickhouse/examples_test.go +++ b/modules/clickhouse/examples_test.go @@ -9,6 +9,7 @@ import ( ch "github.com/ClickHouse/clickhouse-go/v2" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/clickhouse" ) @@ -28,31 +29,35 @@ func ExampleRun() { clickhouse.WithInitScripts(filepath.Join("testdata", "init-db.sh")), clickhouse.WithConfigFile(filepath.Join("testdata", "config.xml")), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - if err := clickHouseContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(clickHouseContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := clickHouseContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) connectionString, err := clickHouseContainer.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) + log.Printf("failed to get connection string: %s", err) + return } opts, err := ch.ParseDSN(connectionString) if err != nil { - log.Fatalf("failed to parse DSN: %s", err) + log.Printf("failed to parse DSN: %s", err) + return } fmt.Println(strings.HasPrefix(opts.ClientInfo.String(), "clickhouse-go/")) diff --git a/modules/clickhouse/go.mod b/modules/clickhouse/go.mod index 5542929f72..2c14eff644 100644 --- a/modules/clickhouse/go.mod +++ b/modules/clickhouse/go.mod @@ -7,7 +7,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 github.com/docker/go-connections v0.5.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -19,7 +19,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -60,9 +60,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/clickhouse/go.sum b/modules/clickhouse/go.sum index 2f09c4daae..f451caeaf6 100644 --- a/modules/clickhouse/go.sum +++ b/modules/clickhouse/go.sum @@ -20,8 +20,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -122,6 +122,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -163,8 +165,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -191,17 +193,17 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/cockroachdb/certs.go b/modules/cockroachdb/certs.go index dab738192a..afa12fcd1a 100644 --- a/modules/cockroachdb/certs.go +++ b/modules/cockroachdb/certs.go @@ -2,7 +2,7 @@ package cockroachdb import ( "crypto/x509" - "fmt" + "errors" "net" "time" @@ -28,7 +28,7 @@ func NewTLSConfig() (*TLSConfig, error) { ValidFor: time.Hour, }) if caCert == nil { - return nil, fmt.Errorf("failed to generate CA certificate") + return nil, errors.New("failed to generate CA certificate") } // } @@ -42,7 +42,7 @@ func NewTLSConfig() (*TLSConfig, error) { Parent: caCert, // using the CA certificate as parent }) if nodeCert == nil { - return nil, fmt.Errorf("failed to generate node certificate") + return nil, errors.New("failed to generate node certificate") } // } @@ -54,7 +54,7 @@ func NewTLSConfig() (*TLSConfig, error) { Parent: caCert, // using the CA certificate as parent }) if clientCert == nil { - return nil, fmt.Errorf("failed to generate client certificate") + return nil, errors.New("failed to generate client certificate") } return &TLSConfig{ diff --git a/modules/cockroachdb/cockroachdb.go b/modules/cockroachdb/cockroachdb.go index e53ef08c6a..092efa4e2a 100644 --- a/modules/cockroachdb/cockroachdb.go +++ b/modules/cockroachdb/cockroachdb.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/pem" + "errors" "fmt" "net" "net/url" @@ -18,7 +19,7 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -var ErrTLSNotEnabled = fmt.Errorf("tls not enabled") +var ErrTLSNotEnabled = errors.New("tls not enabled") const ( certsDir = "/tmp" @@ -118,10 +119,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, req) + var c *CockroachDBContainer + if container != nil { + c = &CockroachDBContainer{Container: container, opts: o} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &CockroachDBContainer{Container: container, opts: o}, nil + + return c, nil } type modiferFunc func(*testcontainers.GenericContainerRequest, options) error @@ -138,7 +145,7 @@ func addCmd(req *testcontainers.GenericContainerRequest, opts options) error { return fmt.Errorf("unsupported user %s with TLS, use %s", opts.User, defaultUser) } if opts.Password != "" { - return fmt.Errorf("cannot use password authentication with TLS") + return errors.New("cannot use password authentication with TLS") } } diff --git a/modules/cockroachdb/cockroachdb_test.go b/modules/cockroachdb/cockroachdb_test.go index 1f5a6df0ad..cc355e9168 100644 --- a/modules/cockroachdb/cockroachdb_test.go +++ b/modules/cockroachdb/cockroachdb_test.go @@ -63,15 +63,11 @@ type AuthNSuite struct { func (suite *AuthNSuite) TestConnectionString() { ctx := context.Background() - container, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + ctr, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + testcontainers.CleanupContainer(suite.T(), ctr) suite.Require().NoError(err) - suite.T().Cleanup(func() { - err := container.Terminate(ctx) - suite.Require().NoError(err) - }) - - connStr, err := removePort(container.MustConnectionString(ctx)) + connStr, err := removePort(ctr.MustConnectionString(ctx)) suite.Require().NoError(err) suite.Equal(suite.url, connStr) @@ -101,15 +97,11 @@ func (suite *AuthNSuite) TestPing() { opts := suite.opts opts = append(opts, input.opts...) - container, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", opts...) + ctr, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", opts...) + testcontainers.CleanupContainer(suite.T(), ctr) suite.Require().NoError(err) - suite.T().Cleanup(func() { - err := container.Terminate(ctx) - suite.Require().NoError(err) - }) - - conn, err := conn(ctx, container) + conn, err := conn(ctx, ctr) suite.Require().NoError(err) defer conn.Close(ctx) @@ -122,15 +114,11 @@ func (suite *AuthNSuite) TestPing() { func (suite *AuthNSuite) TestQuery() { ctx := context.Background() - container, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + ctr, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + testcontainers.CleanupContainer(suite.T(), ctr) suite.Require().NoError(err) - suite.T().Cleanup(func() { - err := container.Terminate(ctx) - suite.Require().NoError(err) - }) - - conn, err := conn(ctx, container) + conn, err := conn(ctx, ctr) suite.Require().NoError(err) defer conn.Close(ctx) @@ -155,15 +143,9 @@ func (suite *AuthNSuite) TestWithWaitStrategyAndDeadline() { // This will never match a log statement suite.opts = append(suite.opts, testcontainers.WithWaitStrategyAndDeadline(time.Millisecond*250, wait.ForLog("Won't Exist In Logs"))) - container, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) - + ctr, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + testcontainers.CleanupContainer(suite.T(), ctr) suite.Require().ErrorIs(err, context.DeadlineExceeded) - suite.T().Cleanup(func() { - if container != nil { - err := container.Terminate(ctx) - suite.Require().NoError(err) - } - }) }) suite.Run("Expected Failure To Run But Would Succeed ", func() { @@ -171,15 +153,9 @@ func (suite *AuthNSuite) TestWithWaitStrategyAndDeadline() { // This will timeout as we didn't give enough time for intialization, but would have succeeded otherwise suite.opts = append(suite.opts, testcontainers.WithWaitStrategyAndDeadline(time.Millisecond*20, wait.ForLog(nodeStartUpCompleted))) - container, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) - + ctr, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + testcontainers.CleanupContainer(suite.T(), ctr) suite.Require().ErrorIs(err, context.DeadlineExceeded) - suite.T().Cleanup(func() { - if container != nil { - err := container.Terminate(ctx) - suite.Require().NoError(err) - } - }) }) suite.Run("Succeeds And Executes Commands", func() { @@ -187,21 +163,16 @@ func (suite *AuthNSuite) TestWithWaitStrategyAndDeadline() { // This will succeed suite.opts = append(suite.opts, testcontainers.WithWaitStrategyAndDeadline(time.Second*60, wait.ForLog(nodeStartUpCompleted))) - container, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + ctr, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + testcontainers.CleanupContainer(suite.T(), ctr) suite.Require().NoError(err) - conn, err := conn(ctx, container) + conn, err := conn(ctx, ctr) suite.Require().NoError(err) defer conn.Close(ctx) _, err = conn.Exec(ctx, "CREATE TABLE test (id INT PRIMARY KEY)") suite.Require().NoError(err) - suite.T().Cleanup(func() { - if container != nil { - err := container.Terminate(ctx) - suite.Require().NoError(err) - } - }) }) suite.Run("Succeeds And Executes Commands Waiting on HTTP Endpoint", func() { @@ -209,21 +180,16 @@ func (suite *AuthNSuite) TestWithWaitStrategyAndDeadline() { // This will succeed suite.opts = append(suite.opts, testcontainers.WithWaitStrategyAndDeadline(time.Second*60, wait.ForHTTP("/health").WithPort("8080/tcp"))) - container, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + ctr, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1", suite.opts...) + testcontainers.CleanupContainer(suite.T(), ctr) suite.Require().NoError(err) - conn, err := conn(ctx, container) + conn, err := conn(ctx, ctr) suite.Require().NoError(err) defer conn.Close(ctx) _, err = conn.Exec(ctx, "CREATE TABLE test (id INT PRIMARY KEY)") suite.Require().NoError(err) - suite.T().Cleanup(func() { - if container != nil { - err := container.Terminate(ctx) - suite.Require().NoError(err) - } - }) }) } diff --git a/modules/cockroachdb/examples_test.go b/modules/cockroachdb/examples_test.go index b427846d4b..c06c97596b 100644 --- a/modules/cockroachdb/examples_test.go +++ b/modules/cockroachdb/examples_test.go @@ -6,6 +6,7 @@ import ( "log" "net/url" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/cockroachdb" ) @@ -14,31 +15,33 @@ func ExampleRun() { ctx := context.Background() cockroachdbContainer, err := cockroachdb.Run(ctx, "cockroachdb/cockroach:latest-v23.1") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := cockroachdbContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(cockroachdbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := cockroachdbContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) addr, err := cockroachdbContainer.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) + log.Printf("failed to get connection string: %s", err) + return } u, err := url.Parse(addr) if err != nil { - log.Fatalf("failed to parse connection string: %s", err) + log.Printf("failed to parse connection string: %s", err) + return } u.Host = fmt.Sprintf("%s:%s", u.Hostname(), "xxx") fmt.Println(u.String()) diff --git a/modules/cockroachdb/go.mod b/modules/cockroachdb/go.mod index d31c6f54ec..fbc0fd6f7a 100644 --- a/modules/cockroachdb/go.mod +++ b/modules/cockroachdb/go.mod @@ -6,7 +6,7 @@ require ( github.com/docker/go-connections v0.5.0 github.com/jackc/pgx/v5 v5.5.4 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -23,7 +23,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -62,10 +62,10 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/modules/cockroachdb/go.sum b/modules/cockroachdb/go.sum index c051f94433..e8661eb69a 100644 --- a/modules/cockroachdb/go.sum +++ b/modules/cockroachdb/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -106,6 +106,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -140,8 +142,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -153,8 +155,8 @@ golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -165,14 +167,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/compose/compose.go b/modules/compose/compose.go index c63eb73bb1..be829f4575 100644 --- a/modules/compose/compose.go +++ b/modules/compose/compose.go @@ -153,22 +153,13 @@ func NewDockerComposeWith(opts ...ComposeStackOption) (*dockerCompose, error) { return nil, fmt.Errorf("initialize docker client: %w", err) } - reaperProvider, err := testcontainers.NewDockerProvider() + provider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(composeOptions.Logger)) if err != nil { - return nil, fmt.Errorf("failed to create reaper provider for compose: %w", err) + return nil, fmt.Errorf("new docker provider: %w", err) } - var composeReaper *testcontainers.Reaper - if !reaperProvider.Config().Config.RyukDisabled { - // NewReaper is deprecated: we need to find a way to create the reaper for compose - // bypassing the deprecation. - r, err := testcontainers.NewReaper(context.Background(), testcontainers.SessionID(), reaperProvider, "") - if err != nil { - return nil, fmt.Errorf("failed to create reaper for compose: %w", err) - } - - composeReaper = r - } + dockerClient := dockerCli.Client() + provider.SetClient(dockerClient) composeAPI := &dockerCompose{ name: composeOptions.Identifier, @@ -177,12 +168,12 @@ func NewDockerComposeWith(opts ...ComposeStackOption) (*dockerCompose, error) { logger: composeOptions.Logger, projectProfiles: composeOptions.Profiles, composeService: compose.NewComposeService(dockerCli), - dockerClient: dockerCli.Client(), + dockerClient: dockerClient, waitStrategies: make(map[string]wait.Strategy), containers: make(map[string]*testcontainers.DockerContainer), networks: make(map[string]*testcontainers.DockerNetwork), sessionID: testcontainers.SessionID(), - reaper: composeReaper, + provider: provider, } return composeAPI, nil diff --git a/modules/compose/compose_api.go b/modules/compose/compose_api.go index d1b18ec3b6..45dd72c6e0 100644 --- a/modules/compose/compose_api.go +++ b/modules/compose/compose_api.go @@ -2,6 +2,7 @@ package compose import ( "context" + "errors" "fmt" "io" "os" @@ -229,8 +230,8 @@ type dockerCompose struct { // sessionID is used to identify the reaper session sessionID string - // reaper is used to clean up containers after the stack is stopped - reaper *testcontainers.Reaper + // provider is used to docker operations. + provider *testcontainers.DockerProvider } func (d *dockerCompose) ServiceContainer(ctx context.Context, svcName string) (*testcontainers.DockerContainer, error) { @@ -269,12 +270,10 @@ func (d *dockerCompose) Down(ctx context.Context, opts ...StackDownOption) error return d.composeService.Down(ctx, d.name, options.DownOptions) } -func (d *dockerCompose) Up(ctx context.Context, opts ...StackUpOption) error { +func (d *dockerCompose) Up(ctx context.Context, opts ...StackUpOption) (err error) { d.lock.Lock() defer d.lock.Unlock() - var err error - d.project, err = d.compileProject(ctx) if err != nil { return err @@ -329,27 +328,57 @@ func (d *dockerCompose) Up(ctx context.Context, opts ...StackUpOption) error { return err } - if d.reaper != nil { + var termSignals []chan bool + var reaper *testcontainers.Reaper + if !d.provider.Config().Config.RyukDisabled { + // NewReaper is deprecated: we need to find a way to create the reaper for compose + // bypassing the deprecation. + reaper, err = testcontainers.NewReaper(ctx, testcontainers.SessionID(), d.provider, "") + if err != nil { + return fmt.Errorf("create reaper: %w", err) + } + + // Cleanup on error, otherwise set termSignal to nil before successful return. + defer func() { + if len(termSignals) == 0 { + // Need to call Connect at least once to ensure the initial + // connection is cleaned up. + termSignal, errc := reaper.Connect() + if errc != nil { + err = errors.Join(err, fmt.Errorf("reaper connect: %w", errc)) + } else { + termSignal <- true + } + } + + if err == nil { + // No need to cleanup. + return + } + + for _, ts := range termSignals { + ts <- true + } + }() + + // Connect to the reaper and set the termination signal for each network. for _, n := range d.networks { - termSignal, err := d.reaper.Connect() + termSignal, err := reaper.Connect() if err != nil { - return fmt.Errorf("failed to connect to reaper: %w", err) + return fmt.Errorf("reaper connect: %w", err) } - n.SetTerminationSignal(termSignal) - // Cleanup on error, otherwise set termSignal to nil before successful return. - defer func() { - if termSignal != nil { - termSignal <- true - } - }() + n.SetTerminationSignal(termSignal) + termSignals = append(termSignals, termSignal) } } errGrpContainers, errGrpCtx := errgroup.WithContext(ctx) + // Lookup the containers for each service and connect them + // to the reaper if needed. + var termSignalsMtx sync.Mutex for _, srv := range d.project.Services { - // we are going to connect each container to the reaper srv := srv errGrpContainers.Go(func() error { dc, err := d.lookupContainer(errGrpCtx, srv.Name) @@ -357,19 +386,17 @@ func (d *dockerCompose) Up(ctx context.Context, opts ...StackUpOption) error { return err } - if d.reaper != nil { - termSignal, err := d.reaper.Connect() + if reaper != nil { + termSignal, err := reaper.Connect() if err != nil { - return fmt.Errorf("failed to connect to reaper: %w", err) + return fmt.Errorf("reaper connect: %w", err) } + dc.SetTerminationSignal(termSignal) - // Cleanup on error, otherwise set termSignal to nil before successful return. - defer func() { - if termSignal != nil { - termSignal <- true - } - }() + termSignalsMtx.Lock() + defer termSignalsMtx.Unlock() + termSignals = append(termSignals, termSignal) } return nil @@ -401,7 +428,11 @@ func (d *dockerCompose) Up(ctx context.Context, opts ...StackUpOption) error { }) } - return errGrpWait.Wait() + if err := errGrpWait.Wait(); err != nil { + return fmt.Errorf("wait for services: %w", err) + } + + return nil } func (d *dockerCompose) WaitForService(s string, strategy wait.Strategy) ComposeStack { @@ -459,22 +490,11 @@ func (d *dockerCompose) lookupContainer(ctx context.Context, svcName string) (*t return nil, fmt.Errorf("no container found for service name %s", svcName) } - containerInstance := containers[0] - ctr := &testcontainers.DockerContainer{ - ID: containerInstance.ID, - Image: containerInstance.Image, - } - ctr.SetLogger(d.logger) - - dockerProvider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(d.logger)) + ctr, err := d.provider.ContainerFromType(ctx, containers[0]) if err != nil { - return nil, fmt.Errorf("new docker provider: %w", err) + return nil, fmt.Errorf("container from type: %w", err) } - dockerProvider.SetClient(d.dockerClient) - - ctr.SetProvider(dockerProvider) - d.containersLock.Lock() defer d.containersLock.Unlock() d.containers[svcName] = ctr @@ -482,6 +502,9 @@ func (d *dockerCompose) lookupContainer(ctx context.Context, svcName string) (*t return ctr, nil } +// lookupNetworks is used to retrieve the networks that are part of the compose stack. +// +// Safe for concurrent calls. func (d *dockerCompose) lookupNetworks(ctx context.Context) error { networks, err := d.dockerClient.NetworkList(ctx, dockernetwork.ListOptions{ Filters: filters.NewArgs( @@ -539,9 +562,7 @@ func (d *dockerCompose) compileProject(ctx context.Context) (*types.Project, err api.OneoffLabel: "False", // default, will be overridden by `run` command } - for k, label := range testcontainers.GenericLabels() { - s.CustomLabels[k] = label - } + testcontainers.AddGenericLabels(s.CustomLabels) for i, envFile := range compiledOptions.EnvFiles { // add a label for each env file, indexed by its position @@ -558,9 +579,7 @@ func (d *dockerCompose) compileProject(ctx context.Context) (*types.Project, err api.VersionLabel: api.ComposeVersion, } - for k, label := range testcontainers.GenericLabels() { - n.Labels[k] = label - } + testcontainers.AddGenericLabels(n.Labels) proj.Networks[key] = n } diff --git a/modules/compose/compose_api_test.go b/modules/compose/compose_api_test.go index 7879dabfa9..808433f513 100644 --- a/modules/compose/compose_api_test.go +++ b/modules/compose/compose_api_test.go @@ -2,7 +2,7 @@ package compose import ( "context" - "fmt" + "encoding/hex" "hash/fnv" "os" "path/filepath" @@ -33,6 +33,12 @@ func TestDockerComposeAPI(t *testing.T) { err = compose.Up(ctx, Wait(true)) cleanup(t, compose) require.NoError(t, err, "compose.Up()") + + for _, service := range compose.Services() { + container, err := compose.ServiceContainer(context.Background(), service) + require.NoError(t, err, "compose.ServiceContainer()") + require.True(t, container.IsRunning()) + } } func TestDockerComposeAPIStrategyForInvalidService(t *testing.T) { @@ -48,12 +54,11 @@ func TestDockerComposeAPIStrategyForInvalidService(t *testing.T) { WaitForService("non-existent-srv-1", wait.NewLogStrategy("started").WithStartupTimeout(10*time.Second).WithOccurrence(1)). Up(ctx, Wait(true)) cleanup(t, compose) - require.Error(t, err, "Expected error to be thrown because service with wait strategy is not running") - require.Equal(t, "no container found for service name non-existent-srv-1", err.Error()) + require.EqualError(t, err, "wait for services: no container found for service name non-existent-srv-1") serviceNames := compose.Services() - assert.Len(t, serviceNames, 1) + require.Len(t, serviceNames, 1) assert.Contains(t, serviceNames, "api-nginx") } @@ -73,9 +78,9 @@ func TestDockerComposeAPIWithWaitLogStrategy(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 2) - assert.Contains(t, serviceNames, "api-nginx") - assert.Contains(t, serviceNames, "api-mysql") + require.Len(t, serviceNames, 2) + require.Contains(t, serviceNames, "api-nginx") + require.Contains(t, serviceNames, "api-mysql") } func TestDockerComposeAPIWithRunServices(t *testing.T) { @@ -97,7 +102,7 @@ func TestDockerComposeAPIWithRunServices(t *testing.T) { _, err = compose.ServiceContainer(context.Background(), "api-mysql") require.Error(t, err, "Make sure there is no mysql container") - assert.Len(t, serviceNames, 1) + require.Len(t, serviceNames, 1) assert.Contains(t, serviceNames, "api-nginx") } @@ -170,9 +175,9 @@ func TestDockerComposeAPI_TestcontainersLabelsArePresent(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 2) - assert.Contains(t, serviceNames, "api-nginx") - assert.Contains(t, serviceNames, "api-mysql") + require.Len(t, serviceNames, 2) + require.Contains(t, serviceNames, "api-nginx") + require.Contains(t, serviceNames, "api-mysql") // all the services in the compose has the Testcontainers Labels for _, serviceName := range serviceNames { @@ -213,9 +218,9 @@ func TestDockerComposeAPI_WithReaper(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 2) - assert.Contains(t, serviceNames, "api-nginx") - assert.Contains(t, serviceNames, "api-mysql") + require.Len(t, serviceNames, 2) + require.Contains(t, serviceNames, "api-nginx") + require.Contains(t, serviceNames, "api-mysql") } func TestDockerComposeAPI_WithoutReaper(t *testing.T) { @@ -240,9 +245,9 @@ func TestDockerComposeAPI_WithoutReaper(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 2) - assert.Contains(t, serviceNames, "api-nginx") - assert.Contains(t, serviceNames, "api-mysql") + require.Len(t, serviceNames, 2) + require.Contains(t, serviceNames, "api-nginx") + require.Contains(t, serviceNames, "api-mysql") } func TestDockerComposeAPIWithStopServices(t *testing.T) { @@ -261,9 +266,9 @@ func TestDockerComposeAPIWithStopServices(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 2) - assert.Contains(t, serviceNames, "api-nginx") - assert.Contains(t, serviceNames, "api-mysql") + require.Len(t, serviceNames, 2) + require.Contains(t, serviceNames, "api-nginx") + require.Contains(t, serviceNames, "api-mysql") // close mysql container in purpose mysqlContainer, err := compose.ServiceContainer(context.Background(), "api-mysql") @@ -299,7 +304,7 @@ func TestDockerComposeAPIWithWaitForService(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 1) + require.Len(t, serviceNames, 1) assert.Contains(t, serviceNames, "api-nginx") } @@ -322,7 +327,7 @@ func TestDockerComposeAPIWithWaitHTTPStrategy(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 1) + require.Len(t, serviceNames, 1) assert.Contains(t, serviceNames, "api-nginx") } @@ -345,7 +350,7 @@ func TestDockerComposeAPIWithContainerName(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 1) + require.Len(t, serviceNames, 1) assert.Contains(t, serviceNames, "api-nginx") } @@ -365,7 +370,7 @@ func TestDockerComposeAPIWithWaitStrategy_NoExposedPorts(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 1) + require.Len(t, serviceNames, 1) assert.Contains(t, serviceNames, "api-nginx") } @@ -386,9 +391,9 @@ func TestDockerComposeAPIWithMultipleWaitStrategies(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 2) - assert.Contains(t, serviceNames, "api-nginx") - assert.Contains(t, serviceNames, "api-mysql") + require.Len(t, serviceNames, 2) + require.Contains(t, serviceNames, "api-nginx") + require.Contains(t, serviceNames, "api-mysql") } func TestDockerComposeAPIWithFailedStrategy(t *testing.T) { @@ -412,7 +417,7 @@ func TestDockerComposeAPIWithFailedStrategy(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 1) + require.Len(t, serviceNames, 1) assert.Contains(t, serviceNames, "api-nginx") } @@ -430,9 +435,9 @@ func TestDockerComposeAPIComplex(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 2) - assert.Contains(t, serviceNames, "api-nginx") - assert.Contains(t, serviceNames, "api-mysql") + require.Len(t, serviceNames, 2) + require.Contains(t, serviceNames, "api-nginx") + require.Contains(t, serviceNames, "api-mysql") } func TestDockerComposeAPIWithStackReader(t *testing.T) { @@ -441,7 +446,7 @@ func TestDockerComposeAPIWithStackReader(t *testing.T) { composeContent := ` services: api-nginx: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine environment: bar: ${bar} foo: ${foo} @@ -464,7 +469,7 @@ services: serviceNames := compose.Services() - assert.Len(t, serviceNames, 1) + require.Len(t, serviceNames, 1) assert.Contains(t, serviceNames, "api-nginx") require.NoError(t, compose.Down(context.Background(), RemoveOrphans(true), RemoveVolumes(true), RemoveImagesLocal), "compose.Down()") @@ -482,7 +487,7 @@ func TestDockerComposeAPIWithStackReaderAndComposeFile(t *testing.T) { composeContent := ` services: api-postgres: - image: docker.io/postgres:14 + image: postgres:14 environment: POSTGRES_PASSWORD: s3cr3t ` @@ -508,7 +513,7 @@ services: serviceNames := compose.Services() - assert.Len(t, serviceNames, 2) + require.Len(t, serviceNames, 2) assert.Contains(t, serviceNames, "api-nginx") assert.Contains(t, serviceNames, "api-postgres") @@ -541,7 +546,7 @@ func TestDockerComposeAPIWithEnvironment(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 1) + require.Len(t, serviceNames, 1) assert.Contains(t, serviceNames, "api-nginx") present := map[string]string{ @@ -578,7 +583,7 @@ func TestDockerComposeAPIWithMultipleComposeFiles(t *testing.T) { serviceNames := compose.Services() - assert.Len(t, serviceNames, 3) + require.Len(t, serviceNames, 3) assert.Contains(t, serviceNames, "api-nginx") assert.Contains(t, serviceNames, "api-mysql") assert.Contains(t, serviceNames, "api-postgres") @@ -637,11 +642,11 @@ func TestDockerComposeAPIVolumesDeletedOnDown(t *testing.T) { volumeListFilters := filters.NewArgs() // the "mydata" identifier comes from the "testdata/docker-compose-volume.yml" file - volumeListFilters.Add("name", fmt.Sprintf("%s_mydata", identifier)) + volumeListFilters.Add("name", identifier+"_mydata") volumeList, err := compose.dockerClient.VolumeList(ctx, volume.ListOptions{Filters: volumeListFilters}) require.NoError(t, err, "compose.dockerClient.VolumeList()") - assert.Empty(t, volumeList.Volumes, "Volumes are not cleaned up") + require.Empty(t, volumeList.Volumes, "Volumes are not cleaned up") } func TestDockerComposeAPIWithBuild(t *testing.T) { @@ -679,13 +684,13 @@ func TestDockerComposeApiWithWaitForShortLifespanService(t *testing.T) { services := compose.Services() - assert.Len(t, services, 2) + require.Len(t, services, 2) assert.Contains(t, services, "falafel") assert.Contains(t, services, "tzatziki") } func testNameHash(name string) StackIdentifier { - return StackIdentifier(fmt.Sprintf("%x", fnv.New32a().Sum([]byte(name)))) + return StackIdentifier(hex.EncodeToString(fnv.New32a().Sum([]byte(name)))) } // cleanup is a helper function that schedules the compose stack to be stopped when the test ends. diff --git a/modules/compose/compose_builder_test.go b/modules/compose/compose_builder_test.go index 4624c1d3c2..048bac241b 100644 --- a/modules/compose/compose_builder_test.go +++ b/modules/compose/compose_builder_test.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/stretchr/testify/require" ) const ( @@ -121,20 +123,17 @@ func getFreePort(t *testing.T) int { t.Helper() addr, err := net.ResolveTCPAddr("tcp", "localhost:0") - if err != nil { - t.Fatalf("failed to resolve TCP address: %v", err) - } + require.NoErrorf(t, err, "failed to resolve TCP address") l, err := net.ListenTCP("tcp", addr) - if err != nil { - t.Fatalf("failed to listen on TCP address: %v", err) - } + require.NoErrorf(t, err, "failed to listen on TCP address") defer l.Close() return l.Addr().(*net.TCPAddr).Port } func writeTemplate(t *testing.T, templateFile string, port ...int) string { + t.Helper() return writeTemplateWithSrvType(t, templateFile, "api", port...) } @@ -145,9 +144,7 @@ func writeTemplateWithSrvType(t *testing.T, templateFile string, srvType string, composeFile := filepath.Join(tmpDir, "docker-compose.yml") tmpl, err := template.ParseFiles(filepath.Join(testdataPackage, templateFile)) - if err != nil { - t.Fatalf("parsing template file: %s", err) - } + require.NoErrorf(t, err, "parsing template file") values := map[string]interface{}{} for i, p := range port { @@ -157,19 +154,17 @@ func writeTemplateWithSrvType(t *testing.T, templateFile string, srvType string, values["ServiceType"] = srvType output, err := os.Create(composeFile) - if err != nil { - t.Fatalf("creating output file: %s", err) - } - defer output.Close() + require.NoErrorf(t, err, "creating output file") + defer func() { + require.NoError(t, output.Close()) + }() executeTemplateFile := func(templateFile *template.Template, wr io.Writer, data any) error { return templateFile.Execute(wr, data) } err = executeTemplateFile(tmpl, output, values) - if err != nil { - t.Fatalf("executing template file: %s", err) - } + require.NoErrorf(t, err, "executing template file") return composeFile } diff --git a/modules/compose/compose_test.go b/modules/compose/compose_test.go index 2453507b06..24c0c6c635 100644 --- a/modules/compose/compose_test.go +++ b/modules/compose/compose_test.go @@ -134,7 +134,7 @@ func TestLocalDockerComposeStrategyForInvalidService(t *testing.T) { Invoke() require.Error(t, err.Error, "Expected error to be thrown because service with wait strategy is not running") - assert.Len(t, compose.Services, 1) + require.Len(t, compose.Services, 1) assert.Contains(t, compose.Services, "local-nginx") } @@ -157,7 +157,7 @@ func TestLocalDockerComposeWithWaitLogStrategy(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 2) + require.Len(t, compose.Services, 2) assert.Contains(t, compose.Services, "local-nginx") assert.Contains(t, compose.Services, "local-mysql") } @@ -183,7 +183,7 @@ func TestLocalDockerComposeWithWaitForService(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 1) + require.Len(t, compose.Services, 1) assert.Contains(t, compose.Services, "local-nginx") } @@ -207,7 +207,7 @@ func TestLocalDockerComposeWithWaitForShortLifespanService(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 2) + require.Len(t, compose.Services, 2) assert.Contains(t, compose.Services, "falafel") assert.Contains(t, compose.Services, "tzatziki") } @@ -233,7 +233,7 @@ func TestLocalDockerComposeWithWaitHTTPStrategy(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 1) + require.Len(t, compose.Services, 1) assert.Contains(t, compose.Services, "local-nginx") } @@ -258,7 +258,7 @@ func TestLocalDockerComposeWithContainerName(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 1) + require.Len(t, compose.Services, 1) assert.Contains(t, compose.Services, "local-nginx") } @@ -280,7 +280,7 @@ func TestLocalDockerComposeWithWaitStrategy_NoExposedPorts(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 1) + require.Len(t, compose.Services, 1) assert.Contains(t, compose.Services, "local-nginx") } @@ -303,7 +303,7 @@ func TestLocalDockerComposeWithMultipleWaitStrategies(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 2) + require.Len(t, compose.Services, 2) assert.Contains(t, compose.Services, "local-nginx") assert.Contains(t, compose.Services, "local-mysql") } @@ -331,7 +331,7 @@ func TestLocalDockerComposeWithFailedStrategy(t *testing.T) { // A specific error message matcher is not asserted since the docker library can change the return message, breaking this test require.Error(t, err.Error, "Expected error to be thrown because of a wrong suplied wait strategy") - assert.Len(t, compose.Services, 1) + require.Len(t, compose.Services, 1) assert.Contains(t, compose.Services, "local-nginx") } @@ -352,7 +352,7 @@ func TestLocalDockerComposeComplex(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 2) + require.Len(t, compose.Services, 2) assert.Contains(t, compose.Services, "local-nginx") assert.Contains(t, compose.Services, "local-mysql") } @@ -377,7 +377,7 @@ func TestLocalDockerComposeWithEnvironment(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 1) + require.Len(t, compose.Services, 1) assert.Contains(t, compose.Services, "local-nginx") present := map[string]string{ @@ -413,7 +413,7 @@ func TestLocalDockerComposeWithMultipleComposeFiles(t *testing.T) { Invoke() checkIfError(t, err) - assert.Len(t, compose.Services, 3) + require.Len(t, compose.Services, 3) assert.Contains(t, compose.Services, "local-nginx") assert.Contains(t, compose.Services, "local-mysql") assert.Contains(t, compose.Services, "local-postgres") @@ -446,23 +446,18 @@ func TestLocalDockerComposeWithVolume(t *testing.T) { } func assertVolumeDoesNotExist(tb testing.TB, volumeName string) { + tb.Helper() containerClient, err := testcontainers.NewDockerClientWithOpts(context.Background()) - if err != nil { - tb.Fatalf("Failed to get provider: %v", err) - } + require.NoErrorf(tb, err, "Failed to get provider") volumeList, err := containerClient.VolumeList(context.Background(), volume.ListOptions{Filters: filters.NewArgs(filters.Arg("name", volumeName))}) - if err != nil { - tb.Fatalf("Failed to list volumes: %v", err) - } + require.NoErrorf(tb, err, "Failed to list volumes") if len(volumeList.Warnings) > 0 { tb.Logf("Volume list warnings: %v", volumeList.Warnings) } - if len(volumeList.Volumes) > 0 { - tb.Fatalf("Volume list is not empty") - } + require.Emptyf(tb, volumeList.Volumes, "Volume list is not empty") } func assertContainerEnvironmentVariables( @@ -471,17 +466,13 @@ func assertContainerEnvironmentVariables( present map[string]string, absent map[string]string, ) { + tb.Helper() containerClient, err := testcontainers.NewDockerClientWithOpts(context.Background()) - if err != nil { - tb.Fatalf("Failed to get provider: %v", err) - } + require.NoErrorf(tb, err, "Failed to get provider") containers, err := containerClient.ContainerList(context.Background(), container.ListOptions{}) - if err != nil { - tb.Fatalf("Failed to list containers: %v", err) - } else if len(containers) == 0 { - tb.Fatalf("container list empty") - } + require.NoErrorf(tb, err, "Failed to list containers") + require.NotEmptyf(tb, containers, "container list empty") containerNameRegexp := regexp.MustCompile(fmt.Sprintf(`^\/?%s(_|-)%s(_|-)\d$`, composeIdentifier, serviceName)) var containerID string @@ -497,9 +488,7 @@ containerLoop: } details, err := containerClient.ContainerInspect(context.Background(), containerID) - if err != nil { - tb.Fatalf("Failed to inspect container: %v", err) - } + require.NoErrorf(tb, err, "Failed to inspect container") for k, v := range present { keyVal := k + "=" + v @@ -514,17 +503,11 @@ containerLoop: func checkIfError(t *testing.T, err ExecError) { t.Helper() - if err.Error != nil { - t.Fatalf("Failed when running %v: %v", err.Command, err.Error) - } + require.NoErrorf(t, err.Error, "Failed when running %v", err.Command) - if err.Stdout != nil { - t.Fatalf("An error in Stdout happened when running %v: %v", err.Command, err.Stdout) - } + require.NoErrorf(t, err.Stdout, "An error in Stdout happened when running %v", err.Command) - if err.Stderr != nil { - t.Fatalf("An error in Stderr happened when running %v: %v", err.Command, err.Stderr) - } + require.NoErrorf(t, err.Stderr, "An error in Stderr happened when running %v", err.Command) assert.NotNil(t, err.StdoutOutput) assert.NotNil(t, err.StderrOutput) diff --git a/modules/compose/go.mod b/modules/compose/go.mod index 487bdcb7d0..24ced86c72 100644 --- a/modules/compose/go.mod +++ b/modules/compose/go.mod @@ -11,8 +11,8 @@ require ( github.com/docker/docker v27.1.1+incompatible github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 - golang.org/x/sync v0.7.0 + github.com/testcontainers/testcontainers-go v0.34.0 + golang.org/x/sync v0.8.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -50,7 +50,7 @@ require ( github.com/containerd/platforms v0.2.1 // indirect github.com/containerd/ttrpc v1.2.5 // indirect github.com/containerd/typeurl/v2 v2.1.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/buildx v0.15.1 // indirect @@ -170,13 +170,13 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/mock v0.4.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect diff --git a/modules/compose/go.sum b/modules/compose/go.sum index c05580192e..c42e635113 100644 --- a/modules/compose/go.sum +++ b/modules/compose/go.sum @@ -113,8 +113,8 @@ github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oL github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -459,6 +459,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -543,8 +545,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -573,8 +575,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -601,20 +603,20 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/compose/testdata/docker-compose-complex.yml b/modules/compose/testdata/docker-compose-complex.yml index fe0636dff2..d84f39ec16 100644 --- a/modules/compose/testdata/docker-compose-complex.yml +++ b/modules/compose/testdata/docker-compose-complex.yml @@ -1,10 +1,10 @@ services: {{ .ServiceType }}-nginx: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine ports: - "{{ .Port_0 }}:80" {{ .ServiceType }}-mysql: - image: docker.io/mysql:8.0.36 + image: mysql:8.0.36 environment: - MYSQL_DATABASE=db - MYSQL_ROOT_PASSWORD=my-secret-pw diff --git a/modules/compose/testdata/docker-compose-container-name.yml b/modules/compose/testdata/docker-compose-container-name.yml index c46ca68888..d36bf96c87 100644 --- a/modules/compose/testdata/docker-compose-container-name.yml +++ b/modules/compose/testdata/docker-compose-container-name.yml @@ -1,7 +1,7 @@ services: {{ .ServiceType }}-nginx: container_name: {{ .ServiceType }}-nginxy - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine environment: bar: ${bar} ports: diff --git a/modules/compose/testdata/docker-compose-no-exposed-ports.yml b/modules/compose/testdata/docker-compose-no-exposed-ports.yml index 3f487156c3..e59e1a6fe9 100644 --- a/modules/compose/testdata/docker-compose-no-exposed-ports.yml +++ b/modules/compose/testdata/docker-compose-no-exposed-ports.yml @@ -1,5 +1,5 @@ services: {{ .ServiceType }}-nginx: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine ports: - "80" diff --git a/modules/compose/testdata/docker-compose-override.yml b/modules/compose/testdata/docker-compose-override.yml index c8714256e0..6112c8d595 100644 --- a/modules/compose/testdata/docker-compose-override.yml +++ b/modules/compose/testdata/docker-compose-override.yml @@ -1,8 +1,8 @@ services: {{ .ServiceType }}-nginx: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine {{ .ServiceType }}-mysql: - image: docker.io/mysql:8.0.36 + image: mysql:8.0.36 environment: MYSQL_RANDOM_ROOT_PASSWORD: Y ports: diff --git a/modules/compose/testdata/docker-compose-postgres.yml b/modules/compose/testdata/docker-compose-postgres.yml index 9671a7bdb5..012c2e0fda 100644 --- a/modules/compose/testdata/docker-compose-postgres.yml +++ b/modules/compose/testdata/docker-compose-postgres.yml @@ -1,6 +1,6 @@ services: {{ .ServiceType }}-postgres: - image: docker.io/postgres:14 + image: postgres:14 environment: POSTGRES_PASSWORD: s3cr3t ports: diff --git a/modules/compose/testdata/docker-compose-profiles.yml b/modules/compose/testdata/docker-compose-profiles.yml index fdb92853e1..a58bf014d3 100644 --- a/modules/compose/testdata/docker-compose-profiles.yml +++ b/modules/compose/testdata/docker-compose-profiles.yml @@ -1,24 +1,24 @@ services: starts-always: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine ports: - ":80" # profiles: none defined, therefore always starts. only-dev: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine ports: - ":80" profiles: - dev dev-or-test: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine ports: - ":80" profiles: - dev - test only-prod: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine ports: - ":80" profiles: diff --git a/modules/compose/testdata/docker-compose-short-lifespan.yml b/modules/compose/testdata/docker-compose-short-lifespan.yml index 47fd1314bb..19486a792b 100644 --- a/modules/compose/testdata/docker-compose-short-lifespan.yml +++ b/modules/compose/testdata/docker-compose-short-lifespan.yml @@ -1,7 +1,7 @@ services: tzatziki: - image: docker.io/alpine:latest + image: alpine:latest command: "sleep 5" falafel: - image: docker.io/alpine:latest + image: alpine:latest command: "echo 'World is your canvas'" diff --git a/modules/compose/testdata/docker-compose-simple.yml b/modules/compose/testdata/docker-compose-simple.yml index ebf8a64b6f..a3aad440bc 100644 --- a/modules/compose/testdata/docker-compose-simple.yml +++ b/modules/compose/testdata/docker-compose-simple.yml @@ -1,6 +1,6 @@ services: {{ .ServiceType }}-nginx: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine environment: bar: ${bar} foo: ${foo} diff --git a/modules/compose/testdata/docker-compose-volume.yml b/modules/compose/testdata/docker-compose-volume.yml index f284a71452..9f904d41d9 100644 --- a/modules/compose/testdata/docker-compose-volume.yml +++ b/modules/compose/testdata/docker-compose-volume.yml @@ -1,6 +1,6 @@ services: {{ .ServiceType }}-nginx: - image: docker.io/nginx:stable-alpine + image: nginx:stable-alpine volumes: - type: volume source: mydata diff --git a/modules/compose/testdata/echoserver.Dockerfile b/modules/compose/testdata/echoserver.Dockerfile index 546489ffac..aaf835f35a 100644 --- a/modules/compose/testdata/echoserver.Dockerfile +++ b/modules/compose/testdata/echoserver.Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/golang:1.13-alpine +FROM golang:1.13-alpine WORKDIR /app diff --git a/modules/consul/consul.go b/modules/consul/consul.go index fab3c5b29d..0084786afb 100644 --- a/modules/consul/consul.go +++ b/modules/consul/consul.go @@ -15,7 +15,7 @@ const ( const ( // Deprecated: it will be removed in the next major version. - DefaultBaseImage = "docker.io/hashicorp/consul:1.15" + DefaultBaseImage = "hashicorp/consul:1.15" ) // ConsulContainer represents the Consul container type used in the module. @@ -65,7 +65,7 @@ func WithConfigFile(configPath string) testcontainers.CustomizeRequestOption { // Deprecated: use Run instead // RunContainer creates an instance of the Consul container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*ConsulContainer, error) { - return Run(ctx, "docker.io/hashicorp/consul:1.15", opts...) + return Run(ctx, "hashicorp/consul:1.15", opts...) } // Run creates an instance of the Consul container type @@ -94,9 +94,14 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, containerReq) + var c *ConsulContainer + if container != nil { + c = &ConsulContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &ConsulContainer{Container: container}, nil + return c, nil } diff --git a/modules/consul/consul_test.go b/modules/consul/consul_test.go index 2b24457785..6f359b7261 100644 --- a/modules/consul/consul_test.go +++ b/modules/consul/consul_test.go @@ -7,7 +7,6 @@ import ( "testing" capi "github.com/hashicorp/consul/api" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" @@ -40,18 +39,18 @@ func TestConsul(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - container, err := consul.Run(ctx, "docker.io/hashicorp/consul:1.15", test.opts...) + ctr, err := consul.Run(ctx, "hashicorp/consul:1.15", test.opts...) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, container.Terminate(ctx), "failed to terminate container") }) // Check if API is up - host, err := container.ApiEndpoint(ctx) + host, err := ctr.ApiEndpoint(ctx) require.NoError(t, err) - assert.NotEmpty(t, len(host)) + require.NotEmpty(t, host) res, err := http.Get("http://" + host) require.NoError(t, err) - assert.Equal(t, http.StatusOK, res.StatusCode) + require.Equal(t, http.StatusOK, res.StatusCode) cfg := capi.DefaultConfig() cfg.Address = host diff --git a/modules/consul/examples_test.go b/modules/consul/examples_test.go index a65a30c066..d833575880 100644 --- a/modules/consul/examples_test.go +++ b/modules/consul/examples_test.go @@ -7,6 +7,7 @@ import ( capi "github.com/hashicorp/consul/api" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/consul" ) @@ -14,22 +15,22 @@ func ExampleRun() { // runConsulContainer { ctx := context.Background() - consulContainer, err := consul.Run(ctx, "docker.io/hashicorp/consul:1.15") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container + consulContainer, err := consul.Run(ctx, "hashicorp/consul:1.15") defer func() { - if err := consulContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(consulContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := consulContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -42,34 +43,36 @@ func ExampleRun_connect() { // connectConsul { ctx := context.Background() - consulContainer, err := consul.Run(ctx, "docker.io/hashicorp/consul:1.15") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container + consulContainer, err := consul.Run(ctx, "hashicorp/consul:1.15") defer func() { - if err := consulContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(consulContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } endpoint, err := consulContainer.ApiEndpoint(ctx) if err != nil { - log.Fatalf("failed to get endpoint: %s", err) // nolint:gocritic + log.Printf("failed to get endpoint: %s", err) + return } config := capi.DefaultConfig() config.Address = endpoint client, err := capi.NewClient(config) if err != nil { - log.Fatalf("failed to connect to Consul: %s", err) + log.Printf("failed to connect to Consul: %s", err) + return } // } node_name, err := client.Agent().NodeName() if err != nil { - log.Fatalf("failed to get node name: %s", err) // nolint:gocritic + log.Printf("failed to get node name: %s", err) + return } fmt.Println(len(node_name) > 0) diff --git a/modules/consul/go.mod b/modules/consul/go.mod index 079447aa27..21db7d081e 100644 --- a/modules/consul/go.mod +++ b/modules/consul/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/hashicorp/consul/api v1.27.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -17,7 +17,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -66,10 +66,10 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/consul/go.sum b/modules/consul/go.sum index d82d1cb435..b261343e8c 100644 --- a/modules/consul/go.sum +++ b/modules/consul/go.sum @@ -32,8 +32,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -283,8 +283,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -335,17 +335,17 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/couchbase/couchbase.go b/modules/couchbase/couchbase.go index 5bd73a4eab..e061ecf3a4 100644 --- a/modules/couchbase/couchbase.go +++ b/modules/couchbase/couchbase.go @@ -113,21 +113,23 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var couchbaseContainer *CouchbaseContainer + if container != nil { + couchbaseContainer = &CouchbaseContainer{container, config} + } if err != nil { - return nil, err + return couchbaseContainer, err } - couchbaseContainer := CouchbaseContainer{container, config} - if err = couchbaseContainer.initCluster(ctx); err != nil { - return nil, err + return couchbaseContainer, fmt.Errorf("init cluster: %w", err) } if err = couchbaseContainer.createBuckets(ctx); err != nil { - return nil, err + return couchbaseContainer, fmt.Errorf("create buckets: %w", err) } - return &couchbaseContainer, nil + return couchbaseContainer, nil } // StartContainer creates an instance of the Couchbase container type diff --git a/modules/couchbase/couchbase_test.go b/modules/couchbase/couchbase_test.go index 9fee51317e..37f7a086a3 100644 --- a/modules/couchbase/couchbase_test.go +++ b/modules/couchbase/couchbase_test.go @@ -6,7 +6,9 @@ import ( "time" "github.com/couchbase/gocb/v2" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" tccouchbase "github.com/testcontainers/testcontainers-go/modules/couchbase" ) @@ -29,23 +31,13 @@ func TestCouchbaseWithCommunityContainer(t *testing.T) { WithFlushEnabled(false). WithPrimaryIndex(true) - container, err := tccouchbase.Run(ctx, communityEdition, tccouchbase.WithBuckets(bucket)) - if err != nil { - t.Fatal(err) - } + ctr, err := tccouchbase.Run(ctx, communityEdition, tccouchbase.WithBuckets(bucket)) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // } - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - - cluster, err := connectCluster(ctx, container) - if err != nil { - t.Fatalf("could not connect couchbase: %s", err) - } + cluster, err := connectCluster(ctx, ctr) + require.NoError(t, err) testBucketUsage(t, cluster.Bucket(bucketName)) } @@ -59,25 +51,15 @@ func TestCouchbaseWithEnterpriseContainer(t *testing.T) { WithReplicas(0). WithFlushEnabled(true). WithPrimaryIndex(true) - container, err := tccouchbase.Run(ctx, + ctr, err := tccouchbase.Run(ctx, enterpriseEdition, tccouchbase.WithBuckets(bucket), ) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - - cluster, err := connectCluster(ctx, container) - if err != nil { - t.Fatalf("could not connect couchbase: %s", err) - } + cluster, err := connectCluster(ctx, ctr) + require.NoError(t, err) testBucketUsage(t, cluster.Bucket(bucketName)) } @@ -86,86 +68,70 @@ func TestWithCredentials(t *testing.T) { ctx := context.Background() bucketName := "testBucket" - _, err := tccouchbase.Run(ctx, + ctr, err := tccouchbase.Run(ctx, communityEdition, tccouchbase.WithAdminCredentials("testcontainers", "testcontainers.IS.cool!"), tccouchbase.WithBuckets(tccouchbase.NewBucket(bucketName))) - if err != nil { - t.Errorf("Expected error to be [%v] , got nil", err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) } func TestWithCredentials_Password_LessThan_6(t *testing.T) { ctx := context.Background() bucketName := "testBucket" - _, err := tccouchbase.Run(ctx, + ctr, err := tccouchbase.Run(ctx, communityEdition, tccouchbase.WithAdminCredentials("testcontainers", "12345"), tccouchbase.WithBuckets(tccouchbase.NewBucket(bucketName))) - - if err == nil { - t.Errorf("Expected error to be [%v] , got nil", err) - } + testcontainers.CleanupContainer(t, ctr) + require.Error(t, err) } func TestAnalyticsServiceWithCommunityContainer(t *testing.T) { ctx := context.Background() bucketName := "testBucket" - _, err := tccouchbase.Run(ctx, + ctr, err := tccouchbase.Run(ctx, communityEdition, tccouchbase.WithServiceAnalytics(), tccouchbase.WithBuckets(tccouchbase.NewBucket(bucketName))) - - if err == nil { - t.Errorf("Expected error to be [%v] , got nil", err) - } + testcontainers.CleanupContainer(t, ctr) + require.Error(t, err) } func TestEventingServiceWithCommunityContainer(t *testing.T) { ctx := context.Background() bucketName := "testBucket" - _, err := tccouchbase.Run(ctx, + ctr, err := tccouchbase.Run(ctx, communityEdition, tccouchbase.WithServiceEventing(), tccouchbase.WithBuckets(tccouchbase.NewBucket(bucketName))) - - if err == nil { - t.Errorf("Expected error to be [%v] , got nil", err) - } + testcontainers.CleanupContainer(t, ctr) + require.Error(t, err) } func testBucketUsage(t *testing.T, bucket *gocb.Bucket) { + t.Helper() err := bucket.WaitUntilReady(5*time.Second, nil) - if err != nil { - t.Fatalf("could not connect bucket: %s", err) - } + require.NoErrorf(t, err, "could not connect bucket") key := "foo" data := map[string]string{"key": "value"} collection := bucket.DefaultCollection() _, err = collection.Upsert(key, data, nil) - if err != nil { - t.Fatalf("could not upsert data: %s", err) - } + require.NoErrorf(t, err, "could not upsert data") result, err := collection.Get(key, nil) - if err != nil { - t.Fatalf("could not get data: %s", err) - } + require.NoErrorf(t, err, "could not get data") var resultData map[string]string err = result.Content(&resultData) - if err != nil { - t.Fatalf("could not assign content: %s", err) - } - - if resultData["key"] != "value" { - t.Errorf("Expected value to be [%s], got %s", "value", resultData["key"]) - } + require.NoErrorf(t, err, "could not assign content") + require.Contains(t, resultData, "key") + require.Equalf(t, "value", resultData["key"], "Expected value to be [%s], got %s", "value", resultData["key"]) } func connectCluster(ctx context.Context, container *tccouchbase.CouchbaseContainer) (*gocb.Cluster, error) { diff --git a/modules/couchbase/examples_test.go b/modules/couchbase/examples_test.go index 518b270613..cc1a09a9db 100644 --- a/modules/couchbase/examples_test.go +++ b/modules/couchbase/examples_test.go @@ -7,6 +7,7 @@ import ( "github.com/couchbase/gocb/v2" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/couchbase" ) @@ -27,26 +28,29 @@ func ExampleRun() { couchbase.WithAdminCredentials("testcontainers", "testcontainers.IS.cool!"), couchbase.WithBuckets(bucket), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - if err := couchbaseContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(couchbaseContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := couchbaseContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) connectionString, err := couchbaseContainer.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) + log.Printf("failed to get connection string: %s", err) + return } cluster, err := gocb.Connect(connectionString, gocb.ClusterOptions{ @@ -54,12 +58,14 @@ func ExampleRun() { Password: couchbaseContainer.Password(), }) if err != nil { - log.Fatalf("failed to connect to cluster: %s", err) + log.Printf("failed to connect to cluster: %s", err) + return } buckets, err := cluster.Buckets().GetAllBuckets(nil) if err != nil { - log.Fatalf("failed to get buckets: %s", err) + log.Printf("failed to get buckets: %s", err) + return } fmt.Println(len(buckets)) diff --git a/modules/couchbase/go.mod b/modules/couchbase/go.mod index 96365d44e7..26739fb6e3 100644 --- a/modules/couchbase/go.mod +++ b/modules/couchbase/go.mod @@ -2,13 +2,12 @@ module github.com/testcontainers/testcontainers-go/modules/couchbase go 1.22 -toolchain go1.21.7 - require ( github.com/cenkalti/backoff/v4 v4.2.1 github.com/couchbase/gocb/v2 v2.7.2 github.com/docker/go-connections v0.5.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 github.com/tidwall/gjson v1.17.1 ) @@ -23,7 +22,8 @@ require ( github.com/couchbase/gocbcoreps v0.1.2 // indirect github.com/couchbase/goprotostellar v1.0.2 // indirect github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20230515165046-68b522a21131 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect @@ -47,6 +47,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -62,13 +63,14 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/grpc v1.64.1 // indirect google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/couchbase/go.sum b/modules/couchbase/go.sum index 737e1e5236..1abe707bb5 100644 --- a/modules/couchbase/go.sum +++ b/modules/couchbase/go.sum @@ -33,8 +33,8 @@ github.com/couchbaselabs/gocaves/client v0.0.0-20230404095311-05e3ba4f0259 h1:2T github.com/couchbaselabs/gocaves/client v0.0.0-20230404095311-05e3ba4f0259/go.mod h1:AVekAZwIY2stsJOMWLAS/0uA/+qdp7pjO8EHnl61QkY= github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20230515165046-68b522a21131 h1:2EAfFswAfgYn3a05DVcegiw6DgMgn1Mv5eGz6IHt1Cw= github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20230515165046-68b522a21131/go.mod h1:o7T431UOfFVHDNvMBUmUxpHnhivwv7BziUao/nMl81E= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -91,8 +91,12 @@ github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -122,6 +126,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -134,8 +140,9 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -189,8 +196,8 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -227,14 +234,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -271,6 +278,8 @@ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGm google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/databend/Makefile b/modules/databend/Makefile new file mode 100644 index 0000000000..a8ea6a7163 --- /dev/null +++ b/modules/databend/Makefile @@ -0,0 +1,5 @@ +include ../../commons-test.mk + +.PHONY: test +test: + $(MAKE) test-databend diff --git a/modules/databend/databend.go b/modules/databend/databend.go new file mode 100644 index 0000000000..85202bbe44 --- /dev/null +++ b/modules/databend/databend.go @@ -0,0 +1,135 @@ +package databend + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + databendUser = "databend" + defaultUser = "databend" + defaultPassword = "databend" + defaultDatabaseName = "default" +) + +// DatabendContainer represents the Databend container type used in the module +type DatabendContainer struct { + testcontainers.Container + username string + password string + database string +} + +var _ testcontainers.ContainerCustomizer = (*DatabendOption)(nil) + +// DatabendOption is an option for the Databend container. +type DatabendOption func(*DatabendContainer) + +// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. +func (o DatabendOption) Customize(*testcontainers.GenericContainerRequest) error { + // NOOP to satisfy interface. + return nil +} + +// Run creates an instance of the Databend container type +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*DatabendContainer, error) { + req := testcontainers.ContainerRequest{ + Image: img, + ExposedPorts: []string{"8000/tcp"}, + Env: map[string]string{ + "QUERY_DEFAULT_USER": defaultUser, + "QUERY_DEFAULT_PASSWORD": defaultPassword, + }, + WaitingFor: wait.ForListeningPort("8000/tcp"), + } + + genericContainerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + for _, opt := range opts { + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } + } + + username := req.Env["QUERY_DEFAULT_USER"] + password := req.Env["QUERY_DEFAULT_PASSWORD"] + if password == "" && username == "" { + return nil, errors.New("empty password and user") + } + + container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *DatabendContainer + if container != nil { + c = &DatabendContainer{ + Container: container, + password: password, + username: username, + database: defaultDatabaseName, + } + } + + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + return c, nil +} + +// MustConnectionString panics if the address cannot be determined. +func (c *DatabendContainer) MustConnectionString(ctx context.Context, args ...string) string { + addr, err := c.ConnectionString(ctx, args...) + if err != nil { + panic(err) + } + return addr +} + +func (c *DatabendContainer) ConnectionString(ctx context.Context, args ...string) (string, error) { + containerPort, err := c.MappedPort(ctx, "8000/tcp") + if err != nil { + return "", fmt.Errorf("mapped port: %w", err) + } + + host, err := c.Host(ctx) + if err != nil { + return "", err + } + + extraArgs := "" + if len(args) > 0 { + extraArgs = "?" + strings.Join(args, "&") + } + if c.database == "" { + return "", errors.New("database name is empty") + } + + // databend://databend:databend@localhost:8000/default?sslmode=disable + connectionString := fmt.Sprintf("databend://%s:%s@%s:%s/%s%s", c.username, c.password, host, containerPort.Port(), c.database, extraArgs) + return connectionString, nil +} + +// WithUsername sets the username for the Databend container. +// WithUsername is [Run] option that configures the default query user by setting +// the `QUERY_DEFAULT_USER` container environment variable. +func WithUsername(username string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Env["QUERY_DEFAULT_USER"] = username + return nil + } +} + +// WithPassword sets the password for the Databend container. +func WithPassword(password string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Env["QUERY_DEFAULT_PASSWORD"] = password + return nil + } +} diff --git a/modules/databend/databend_test.go b/modules/databend/databend_test.go new file mode 100644 index 0000000000..58ac71e327 --- /dev/null +++ b/modules/databend/databend_test.go @@ -0,0 +1,74 @@ +package databend_test + +import ( + "context" + "database/sql" + "testing" + + _ "github.com/datafuselabs/databend-go" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/databend" +) + +func TestDatabend(t *testing.T) { + ctx := context.Background() + + ctr, err := databend.Run(ctx, "datafuselabs/databend:v1.2.615") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + // perform assertions + // connectionString { + connectionString, err := ctr.ConnectionString(ctx, "sslmode=disable") + // } + require.NoError(t, err) + + mustConnectionString := ctr.MustConnectionString(ctx, "sslmode=disable") + require.Equal(t, connectionString, mustConnectionString) + + db, err := sql.Open("databend", connectionString) + require.NoError(t, err) + defer db.Close() + + err = db.Ping() + require.NoError(t, err) + + _, err = db.Exec("CREATE TABLE IF NOT EXISTS a_table ( \n" + + " `col_1` VARCHAR(128) NOT NULL, \n" + + " `col_2` VARCHAR(128) NOT NULL \n" + + ")") + require.NoError(t, err) +} + +func TestDatabendWithDefaultUserAndPassword(t *testing.T) { + ctx := context.Background() + + ctr, err := databend.Run(ctx, + "datafuselabs/databend:v1.2.615", + databend.WithUsername("databend")) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + // perform assertions + connectionString, err := ctr.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + db, err := sql.Open("databend", connectionString) + require.NoError(t, err) + defer db.Close() + err = db.Ping() + require.NoError(t, err) + + var i int + row := db.QueryRow("select 1") + err = row.Scan(&i) + require.NoError(t, err) + + _, err = db.Exec("CREATE TABLE IF NOT EXISTS a_table ( \n" + + " `col_1` VARCHAR(128) NOT NULL, \n" + + " `col_2` VARCHAR(128) NOT NULL \n" + + ")") + require.NoError(t, err) +} diff --git a/modules/databend/examples_test.go b/modules/databend/examples_test.go new file mode 100644 index 0000000000..ac284ef009 --- /dev/null +++ b/modules/databend/examples_test.go @@ -0,0 +1,88 @@ +package databend_test + +import ( + "context" + "database/sql" + "fmt" + "log" + + _ "github.com/datafuselabs/databend-go" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/databend" +) + +func ExampleRun() { + ctx := context.Background() + + databendContainer, err := databend.Run(ctx, + "datafuselabs/databend:v1.2.615", + databend.WithUsername("test1"), + databend.WithPassword("pass1"), + ) + defer func() { + if err := testcontainers.TerminateContainer(databendContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + + state, err := databendContainer.State(ctx) + if err != nil { + log.Printf("failed to get container state: %s", err) + return + } + + fmt.Println(state.Running) + + // Output: + // true +} + +func ExampleRun_connect() { + ctx := context.Background() + + databendContainer, err := databend.Run(ctx, + "datafuselabs/databend:v1.2.615", + databend.WithUsername("root"), + databend.WithPassword("password"), + ) + defer func() { + if err := testcontainers.TerminateContainer(databendContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + + connectionString, err := databendContainer.ConnectionString(ctx, "sslmode=disable") + if err != nil { + log.Printf("failed to get connection string: %s", err) + return + } + + db, err := sql.Open("databend", connectionString) + if err != nil { + log.Printf("failed to connect to Databend: %s", err) + return + } + defer db.Close() + + var i int + row := db.QueryRow("select 1") + err = row.Scan(&i) + if err != nil { + log.Printf("failed to scan result: %s", err) + return + } + + fmt.Println(i) + + // Output: + // 1 +} diff --git a/modules/databend/go.mod b/modules/databend/go.mod new file mode 100644 index 0000000000..c6c0254315 --- /dev/null +++ b/modules/databend/go.mod @@ -0,0 +1,63 @@ +module github.com/testcontainers/testcontainers-go/modules/databend + +go 1.22.0 + +require ( + github.com/datafuselabs/databend-go v0.7.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/avast/retry-go v3.0.0+incompatible // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/databend/go.sum b/modules/databend/go.sum new file mode 100644 index 0000000000..a9ef44ba75 --- /dev/null +++ b/modules/databend/go.sum @@ -0,0 +1,199 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/datafuselabs/databend-go v0.7.0 h1:wPND9I8r/FfcY/nAPo8yeZbh5PMga3ICSDIaq8/eP3o= +github.com/datafuselabs/databend-go v0.7.0/go.mod h1:h/sGUBZs7EqJgqnZ3XB0KHfyUlpGvfNrw2lWcdDJVIw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/modules/dolt/dolt.go b/modules/dolt/dolt.go index d819e18f5c..9309ce4475 100644 --- a/modules/dolt/dolt.go +++ b/modules/dolt/dolt.go @@ -3,6 +3,7 @@ package dolt import ( "context" "database/sql" + "errors" "fmt" "path/filepath" "strings" @@ -84,19 +85,24 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } if len(password) == 0 && password == "" && !strings.EqualFold(rootUser, username) { - return nil, fmt.Errorf("empty password can be used only with the root user") + return nil, errors.New("empty password can be used only with the root user") } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var dc *DoltContainer + if container != nil { + dc = &DoltContainer{Container: container, username: username, password: password, database: database} + } if err != nil { - return nil, err + return dc, err } - dc := &DoltContainer{container, username, password, database} - // dolthub/dolt-sql-server does not create user or database, so we do so here - err = dc.initialize(ctx, createUser) - return dc, err + if err = dc.initialize(ctx, createUser); err != nil { + return dc, fmt.Errorf("initialize: %w", err) + } + + return dc, nil } func (c *DoltContainer) initialize(ctx context.Context, createUser bool) error { diff --git a/modules/dolt/dolt_test.go b/modules/dolt/dolt_test.go index a511549c78..a1e46cc976 100644 --- a/modules/dolt/dolt_test.go +++ b/modules/dolt/dolt_test.go @@ -9,90 +9,73 @@ import ( // Import mysql into the scope of this package (required) _ "github.com/go-sql-driver/mysql" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/dolt" ) func TestDolt(t *testing.T) { ctx := context.Background() - container, err := dolt.Run(ctx, "dolthub/dolt-sql-server:1.32.4") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := dolt.Run(ctx, "dolthub/dolt-sql-server:1.32.4") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions // connectionString { - connectionString, err := container.ConnectionString(ctx) + connectionString, err := ctr.ConnectionString(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) + _, err = db.Exec("CREATE TABLE IF NOT EXISTS a_table ( \n" + " `col_1` VARCHAR(128) NOT NULL, \n" + " `col_2` VARCHAR(128) NOT NULL, \n" + " PRIMARY KEY (`col_1`, `col_2`) \n" + ")") - if err != nil { - t.Errorf("error creating table: %+v\n", err) - } + require.NoError(t, err) } func TestDoltWithNonRootUserAndEmptyPassword(t *testing.T) { ctx := context.Background() - _, err := dolt.Run(ctx, + ctr, err := dolt.Run(ctx, "dolthub/dolt-sql-server:1.32.4", dolt.WithDatabase("foo"), dolt.WithUsername("test"), dolt.WithPassword("")) - if err.Error() != "empty password can be used only with the root user" { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.EqualError(t, err, "empty password can be used only with the root user") } func TestDoltWithPublicRemoteCloneUrl(t *testing.T) { ctx := context.Background() - _, err := dolt.Run(ctx, + ctr, err := dolt.Run(ctx, "dolthub/dolt-sql-server:1.32.4", dolt.WithDatabase("foo"), dolt.WithUsername("test"), dolt.WithPassword("test"), dolt.WithScripts(filepath.Join("testdata", "check_clone_public.sh")), dolt.WithDoltCloneRemoteUrl("fake-remote-url")) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) } func createTestCredsFile(t *testing.T) string { + t.Helper() file, err := os.CreateTemp(t.TempDir(), "prefix") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer file.Close() _, err = file.WriteString("some-fake-creds") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return file.Name() } @@ -100,7 +83,7 @@ func TestDoltWithPrivateRemoteCloneUrl(t *testing.T) { ctx := context.Background() filename := createTestCredsFile(t) - _, err := dolt.Run(ctx, + ctr, err := dolt.Run(ctx, "dolthub/dolt-sql-server:1.32.4", dolt.WithDatabase("foo"), dolt.WithUsername("test"), @@ -109,93 +92,65 @@ func TestDoltWithPrivateRemoteCloneUrl(t *testing.T) { dolt.WithDoltCloneRemoteUrl("fake-remote-url"), dolt.WithDoltCredsPublicKey("fake-public-key"), dolt.WithCredsFile(filename)) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) } func TestDoltWithRootUserAndEmptyPassword(t *testing.T) { ctx := context.Background() - container, err := dolt.Run(ctx, + ctr, err := dolt.Run(ctx, "dolthub/dolt-sql-server:1.32.4", dolt.WithDatabase("foo"), dolt.WithUsername("root"), dolt.WithPassword("")) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions - connectionString := container.MustConnectionString(ctx) + connectionString := ctr.MustConnectionString(ctx) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) + _, err = db.Exec("CREATE TABLE IF NOT EXISTS a_table ( \n" + " `col_1` VARCHAR(128) NOT NULL, \n" + " `col_2` VARCHAR(128) NOT NULL, \n" + " PRIMARY KEY (`col_1`, `col_2`) \n" + ")") - if err != nil { - t.Errorf("error creating table: %+v\n", err) - } + require.NoError(t, err) } func TestDoltWithScripts(t *testing.T) { ctx := context.Background() - container, err := dolt.Run(ctx, + ctr, err := dolt.Run(ctx, "dolthub/dolt-sql-server:1.32.4", dolt.WithScripts(filepath.Join("testdata", "schema.sql"))) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions - connectionString := container.MustConnectionString(ctx) + connectionString := ctr.MustConnectionString(ctx) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) + stmt, err := db.Prepare("SELECT name from profile") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + defer stmt.Close() row := stmt.QueryRow() var name string err = row.Scan(&name) - if err != nil { - t.Errorf("error fetching data") - } - if name != "profile 1" { - t.Fatal("The expected record was not found in the database.") - } + require.NoError(t, err) + require.Equal(t, "profile 1", name) } diff --git a/modules/dolt/examples_test.go b/modules/dolt/examples_test.go index 73e430d871..ddbf81b079 100644 --- a/modules/dolt/examples_test.go +++ b/modules/dolt/examples_test.go @@ -7,6 +7,7 @@ import ( "log" "path/filepath" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/dolt" ) @@ -22,21 +23,21 @@ func ExampleRun() { dolt.WithPassword("password"), dolt.WithScripts(filepath.Join("testdata", "schema.sql")), ) - if err != nil { - log.Fatalf("failed to run dolt container: %s", err) // nolint:gocritic - } - - // Clean up the container defer func() { - if err := doltContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate dolt container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(doltContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to run dolt container: %s", err) + return + } // } state, err := doltContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -56,37 +57,41 @@ func ExampleRun_connect() { dolt.WithPassword("password"), dolt.WithScripts(filepath.Join("testdata", "schema.sql")), ) - if err != nil { - log.Fatalf("failed to run dolt container: %s", err) // nolint:gocritic - } - defer func() { - if err := doltContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate dolt container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(doltContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to run dolt container: %s", err) + return + } connectionString := doltContainer.MustConnectionString(ctx) db, err := sql.Open("mysql", connectionString) if err != nil { - log.Fatalf("failed to open database connection: %s", err) // nolint:gocritic + log.Printf("failed to open database connection: %s", err) + return } defer db.Close() if err = db.Ping(); err != nil { - log.Fatalf("failed to ping database: %s", err) // nolint:gocritic + log.Printf("failed to ping database: %s", err) + return } stmt, err := db.Prepare("SELECT dolt_version();") if err != nil { - log.Fatalf("failed to prepate sql statement: %s", err) // nolint:gocritic + log.Printf("failed to prepate sql statement: %s", err) + return } defer stmt.Close() row := stmt.QueryRow() version := "" err = row.Scan(&version) if err != nil { - log.Fatalf("failed to scan row: %s", err) // nolint:gocritic + log.Printf("failed to scan row: %s", err) + return } fmt.Println(version) diff --git a/modules/dolt/go.mod b/modules/dolt/go.mod index 804efb8483..e79f82e78c 100644 --- a/modules/dolt/go.mod +++ b/modules/dolt/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/go-sql-driver/mysql v1.7.1 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -15,7 +16,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -27,6 +29,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -38,6 +41,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -49,11 +53,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/dolt/go.sum b/modules/dolt/go.sum index 7784d0b833..96aef09c5a 100644 --- a/modules/dolt/go.sum +++ b/modules/dolt/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -54,6 +55,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -80,6 +85,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -91,6 +98,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -124,8 +133,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -147,14 +156,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -174,6 +183,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/dynamodb/Makefile b/modules/dynamodb/Makefile new file mode 100644 index 0000000000..42d3e1226f --- /dev/null +++ b/modules/dynamodb/Makefile @@ -0,0 +1,5 @@ +include ../../commons-test.mk + +.PHONY: test +test: + $(MAKE) test-dynamodb diff --git a/modules/dynamodb/dynamodb.go b/modules/dynamodb/dynamodb.go new file mode 100644 index 0000000000..62a6938efe --- /dev/null +++ b/modules/dynamodb/dynamodb.go @@ -0,0 +1,90 @@ +package dynamodb + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + port = "8000/tcp" + containerName = "tc_dynamodb_local" +) + +// DynamoDBContainer represents the DynamoDB container type used in the module +type DynamoDBContainer struct { + testcontainers.Container +} + +// Run creates an instance of the DynamoDB container type +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*DynamoDBContainer, error) { + req := testcontainers.ContainerRequest{ + Image: img, + ExposedPorts: []string{string(port)}, + Entrypoint: []string{"java", "-Djava.library.path=./DynamoDBLocal_lib"}, + Cmd: []string{"-jar", "DynamoDBLocal.jar"}, + WaitingFor: wait.ForListeningPort(port), + } + + genericContainerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + for _, opt := range opts { + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, fmt.Errorf("customize: %w", err) + } + } + + container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *DynamoDBContainer + if container != nil { + c = &DynamoDBContainer{Container: container} + } + + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + return c, nil +} + +// ConnectionString returns DynamoDB local endpoint host and port in : format +func (c *DynamoDBContainer) ConnectionString(ctx context.Context) (string, error) { + mappedPort, err := c.MappedPort(ctx, port) + if err != nil { + return "", err + } + + hostIP, err := c.Host(ctx) + if err != nil { + return "", err + } + + return hostIP + ":" + mappedPort.Port(), nil +} + +// WithSharedDB allows container reuse between successive runs. Data will be persisted +func WithSharedDB() testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Cmd = append(req.Cmd, "-sharedDb") + + req.Reuse = true + req.Name = containerName + + return nil + } +} + +// WithDisableTelemetry - DynamoDB local will not send any telemetry +func WithDisableTelemetry() testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + // if other flags (e.g. -sharedDb) exist, append to them + req.Cmd = append(req.Cmd, "-disableTelemetry") + + return nil + } +} diff --git a/modules/dynamodb/dynamodb_test.go b/modules/dynamodb/dynamodb_test.go new file mode 100644 index 0000000000..b62766d813 --- /dev/null +++ b/modules/dynamodb/dynamodb_test.go @@ -0,0 +1,260 @@ +package dynamodb_test + +import ( + "context" + "errors" + "fmt" + "net/url" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + smithyendpoints "github.com/aws/smithy-go/endpoints" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" + tcdynamodb "github.com/testcontainers/testcontainers-go/modules/dynamodb" +) + +const ( + tableName string = "demo_table" + pkColumnName string = "demo_pk" + baseImage string = "amazon/dynamodb-local:" +) + +var image2_2_1 string = baseImage + "2.2.1" + +func TestRun(t *testing.T) { + ctx := context.Background() + + ctr, err := tcdynamodb.Run(ctx, image2_2_1) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + cli := getDynamoDBClient(t, ctr) + require.NoError(t, err, "failed to get dynamodb client handle") + + requireTableExists(t, cli, tableName) + + value := "test_value" + addDataToTable(t, cli, value) + + queryResult := queryItem(t, cli, value) + require.Equal(t, value, queryResult) +} + +func TestRun_withCustomImageVersion(t *testing.T) { + ctx := context.Background() + + ctr, err := tcdynamodb.Run(ctx, "amazon/dynamodb-local:2.2.0") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) +} + +func TestRun_withInvalidCustomImageVersion(t *testing.T) { + ctx := context.Background() + + _, err := tcdynamodb.Run(ctx, "amazon/dynamodb-local:0.0.7") + require.Error(t, err) +} + +func TestRun_withoutEndpointResolver(t *testing.T) { + ctx := context.Background() + + ctr, err := tcdynamodb.Run(ctx, image2_2_1) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err, "container should start successfully") + + cli := dynamodb.New(dynamodb.Options{}) + + err = createTable(cli) + require.Error(t, err) +} + +func TestRun_withSharedDB(t *testing.T) { + ctx := context.Background() + + ctr, err := tcdynamodb.Run(ctx, image2_2_1, tcdynamodb.WithSharedDB()) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + cli1 := getDynamoDBClient(t, ctr) + + requireTableExists(t, cli1, tableName) + + // create a second container: it should have the table created in the first container + ctr2, err := tcdynamodb.Run(ctx, image2_2_1, tcdynamodb.WithSharedDB()) + testcontainers.CleanupContainer(t, ctr2) + require.NoError(t, err) + + // fetch client handle again + cli2 := getDynamoDBClient(t, ctr2) + require.NoError(t, err, "failed to get dynamodb client handle") + + // list tables and verify + + result, err := cli2.ListTables(context.Background(), nil) + require.NoError(t, err, "dynamodb list tables operation failed") + + actualTableName := result.TableNames[0] + require.Equal(t, tableName, actualTableName) + + // add and query data from the second container + value := "test_value" + addDataToTable(t, cli2, value) + + // read data from the first container + queryResult := queryItem(t, cli1, value) + require.NoError(t, err) + require.Equal(t, value, queryResult) +} + +func TestRun_withoutSharedDB(t *testing.T) { + ctx := context.Background() + + ctr1, err := tcdynamodb.Run(ctx, image2_2_1) + testcontainers.CleanupContainer(t, ctr1) + require.NoError(t, err) + + cli := getDynamoDBClient(t, ctr1) + require.NoError(t, err, "failed to get dynamodb client handle") + + requireTableExists(t, cli, tableName) + + // create a second container: it should not have the table created in the first container + ctr2, err := tcdynamodb.Run(ctx, image2_2_1) + testcontainers.CleanupContainer(t, ctr2) + require.NoError(t, err) + + // fetch client handle again + cli = getDynamoDBClient(t, ctr2) + require.NoError(t, err, "failed to get dynamodb client handle") + + // list tables and verify + + result, err := cli.ListTables(context.Background(), nil) + require.NoError(t, err, "dynamodb list tables operation failed") + require.Empty(t, result.TableNames, "table should not exist after restarting container") +} + +func TestRun_shouldStartWithTelemetryDisabled(t *testing.T) { + ctx := context.Background() + + ctr, err := tcdynamodb.Run(ctx, image2_2_1, tcdynamodb.WithDisableTelemetry()) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) +} + +func TestRun_shouldStartWithSharedDBEnabledAndTelemetryDisabled(t *testing.T) { + ctx := context.Background() + + ctr, err := tcdynamodb.Run(ctx, image2_2_1, tcdynamodb.WithSharedDB(), tcdynamodb.WithDisableTelemetry()) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) +} + +func createTable(client *dynamodb.Client) error { + _, err := client.CreateTable(context.Background(), &dynamodb.CreateTableInput{ + TableName: aws.String(tableName), + KeySchema: []types.KeySchemaElement{ + { + AttributeName: aws.String(pkColumnName), + KeyType: types.KeyTypeHash, + }, + }, + AttributeDefinitions: []types.AttributeDefinition{ + { + AttributeName: aws.String(pkColumnName), + AttributeType: types.ScalarAttributeTypeS, + }, + }, + BillingMode: types.BillingModePayPerRequest, + }) + if err != nil { + return fmt.Errorf("create table: %w", err) + } + + return nil +} + +func addDataToTable(t *testing.T, client *dynamodb.Client, val string) { + t.Helper() + + _, err := client.PutItem(context.Background(), &dynamodb.PutItemInput{ + TableName: aws.String(tableName), + Item: map[string]types.AttributeValue{ + pkColumnName: &types.AttributeValueMemberS{Value: val}, + }, + }) + require.NoError(t, err) +} + +func queryItem(t *testing.T, client *dynamodb.Client, val string) string { + t.Helper() + + output, err := client.GetItem(context.Background(), &dynamodb.GetItemInput{ + TableName: aws.String(tableName), + Key: map[string]types.AttributeValue{ + pkColumnName: &types.AttributeValueMemberS{Value: val}, + }, + }) + require.NoError(t, err) + + result := output.Item[pkColumnName].(*types.AttributeValueMemberS) + + return result.Value +} + +type dynamoDBResolver struct { + HostPort string +} + +func (r *dynamoDBResolver) ResolveEndpoint(ctx context.Context, params dynamodb.EndpointParameters) (smithyendpoints.Endpoint, error) { + return smithyendpoints.Endpoint{ + URI: url.URL{Host: r.HostPort, Scheme: "http"}, + }, nil +} + +// getDynamoDBClient returns a new DynamoDB client with the endpoint resolver set to the DynamoDB container's host and port +func getDynamoDBClient(t *testing.T, c *tcdynamodb.DynamoDBContainer) *dynamodb.Client { + t.Helper() + + // createClient { + var errs []error + + hostPort, err := c.ConnectionString(context.Background()) + if err != nil { + errs = append(errs, fmt.Errorf("get connection string: %w", err)) + } + + cfg, err := config.LoadDefaultConfig(context.Background(), config.WithCredentialsProvider(credentials.StaticCredentialsProvider{ + Value: aws.Credentials{ + AccessKeyID: "DUMMYIDEXAMPLE", + SecretAccessKey: "DUMMYEXAMPLEKEY", + }, + })) + if err != nil { + errs = append(errs, fmt.Errorf("load default config: %w", err)) + } + + require.NoError(t, errors.Join(errs...)) + + return dynamodb.NewFromConfig(cfg, dynamodb.WithEndpointResolverV2(&dynamoDBResolver{HostPort: hostPort})) + // } +} + +func requireTableExists(t *testing.T, cli *dynamodb.Client, tableName string) { + t.Helper() + + err := createTable(cli) + require.NoError(t, err) + + result, err := cli.ListTables(context.Background(), nil) + require.NoError(t, err, "dynamodb list tables operation failed") + + actualTableName := result.TableNames[0] + require.Equal(t, tableName, actualTableName) +} diff --git a/modules/dynamodb/examples_test.go b/modules/dynamodb/examples_test.go new file mode 100644 index 0000000000..e4e478b943 --- /dev/null +++ b/modules/dynamodb/examples_test.go @@ -0,0 +1,38 @@ +package dynamodb_test + +import ( + "context" + "fmt" + "log" + + "github.com/testcontainers/testcontainers-go" + tcdynamodb "github.com/testcontainers/testcontainers-go/modules/dynamodb" +) + +func ExampleRun() { + // runDynamoDBContainer { + ctx := context.Background() + + ctr, err := tcdynamodb.Run(ctx, "amazon/dynamodb-local:2.2.1") + defer func() { + if err := testcontainers.TerminateContainer(ctr); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to run dynamodb container: %s", err) + return + } + // } + + state, err := ctr.State(ctx) + if err != nil { + log.Printf("failed to get container state: %s", err) + return + } + + fmt.Println(state.Running) + + // Output: + // true +} diff --git a/modules/dynamodb/go.mod b/modules/dynamodb/go.mod new file mode 100644 index 0000000000..4a3b063bd4 --- /dev/null +++ b/modules/dynamodb/go.mod @@ -0,0 +1,76 @@ +module github.com/testcontainers/testcontainers-go/modules/dynamodb + +go 1.22 + +require ( + github.com/aws/aws-sdk-go-v2 v1.31.0 + github.com/aws/aws-sdk-go-v2/config v1.27.37 + github.com/aws/aws-sdk-go-v2/credentials v1.17.35 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.35.1 + github.com/aws/smithy-go v1.21.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.23.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.31.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/dynamodb/go.sum b/modules/dynamodb/go.sum new file mode 100644 index 0000000000..80a9c6e219 --- /dev/null +++ b/modules/dynamodb/go.sum @@ -0,0 +1,228 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= +github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= +github.com/aws/aws-sdk-go-v2/config v1.27.37 h1:xaoIwzHVuRWRHFI0jhgEdEGc8xE1l91KaeRDsWEIncU= +github.com/aws/aws-sdk-go-v2/config v1.27.37/go.mod h1:S2e3ax9/8KnMSyRVNd3sWTKs+1clJ2f1U6nE0lpvQRg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.35 h1:7QknrZhYySEB1lEXJxGAmuD5sWwys5ZXNr4m5oEz0IE= +github.com/aws/aws-sdk-go-v2/credentials v1.17.35/go.mod h1:8Vy4kk7at4aPSmibr7K+nLTzG6qUQAUO4tW49fzUV4E= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.35.1 h1:DDN8yqYzFUDy2W5zk3tLQNKaO/1t0h3fNixPJacu264= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.35.1/go.mod h1:k5XW8MoMxsNZ20RJmsokakvENUwQyjv69R9GqrI4xdQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGfT8wMXtuP3D5CRmMjARHv9ZmzFUMJznHDOY3w= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.19 h1:dOxqOlOEa2e2heC/74+ZzcJOa27+F1aXFZpYgY/4QfA= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.19/go.mod h1:aV6U1beLFvk3qAgognjS3wnGGoDId8hlPEiBsLHXVZE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.1 h1:2jrVsMHqdLD1+PA4BA6Nh1eZp0Gsy3mFSB5MxDvcJtU= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.1/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.1 h1:0L7yGCg3Hb3YQqnSgBTZM5wepougtL1aEccdcdYhHME= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.1/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.1 h1:8K0UNOkZiK9Uh3HIF6Bx0rcNCftqGCeKmOaR7Gp5BSo= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.1/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= +github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= +github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/modules/elasticsearch/elasticsearch.go b/modules/elasticsearch/elasticsearch.go index 10a863c589..96f97ef8c1 100644 --- a/modules/elasticsearch/elasticsearch.go +++ b/modules/elasticsearch/elasticsearch.go @@ -2,6 +2,9 @@ package elasticsearch import ( "context" + "crypto/tls" + "crypto/x509" + "errors" "fmt" "io" "os" @@ -15,6 +18,7 @@ const ( defaultTCPPort = "9300" defaultPassword = "changeme" defaultUsername = "elastic" + defaultCaCertPath = "/usr/share/elasticsearch/config/certs/http_ca.crt" minimalImageVersion = "7.9.2" ) @@ -32,7 +36,7 @@ type ElasticsearchContainer struct { } // Deprecated: use Run instead -// RunContainer creates an instance of the Couchbase container type +// RunContainer creates an instance of the Elasticsearch container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*ElasticsearchContainer, error) { return Run(ctx, "docker.elastic.co/elasticsearch/elasticsearch:7.9.2", opts...) } @@ -50,78 +54,119 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom defaultHTTPPort + "/tcp", defaultTCPPort + "/tcp", }, - // regex that - // matches 8.3 JSON logging with started message and some follow up content within the message field - // matches 8.0 JSON logging with no whitespace between message field and content - // matches 7.x JSON logging with whitespace between message field and content - // matches 6.x text logging with node name in brackets and just a 'started' message till the end of the line - WaitingFor: wait.ForLog(`.*("message":\s?"started(\s|")?.*|]\sstarted\n)`).AsRegexp(), - LifecycleHooks: []testcontainers.ContainerLifecycleHooks{ - { - // the container needs a post create hook to set the default JVM options in a file - PostCreates: []testcontainers.ContainerHook{}, - PostReadies: []testcontainers.ContainerHook{}, - }, - }, }, Started: true, } // Gather all config options (defaults and then apply provided options) - settings := defaultOptions() + options := defaultOptions() for _, opt := range opts { if apply, ok := opt.(Option); ok { - apply(settings) + apply(options) } if err := opt.Customize(&req); err != nil { return nil, err } } - // Transfer the certificate settings to the container request - err := configureCertificate(settings, &req) - if err != nil { - return nil, err - } - // Transfer the password settings to the container request - err = configurePassword(settings, &req) - if err != nil { + if err := configurePassword(options, &req); err != nil { return nil, err } if isAtLeastVersion(req.Image, 7) { - req.LifecycleHooks[0].PostCreates = append(req.LifecycleHooks[0].PostCreates, configureJvmOpts) + req.LifecycleHooks = append(req.LifecycleHooks, + testcontainers.ContainerLifecycleHooks{ + PostCreates: []testcontainers.ContainerHook{configureJvmOpts}, + }, + ) } + // Set the default waiting strategy if not already set. + setWaitFor(options, &req.ContainerRequest) + container, err := testcontainers.GenericContainer(ctx, req) + var esContainer *ElasticsearchContainer + if container != nil { + esContainer = &ElasticsearchContainer{Container: container, Settings: *options} + } if err != nil { - return nil, err + return esContainer, fmt.Errorf("generic container: %w", err) } - esContainer := &ElasticsearchContainer{Container: container, Settings: *settings} + if err := esContainer.configureAddress(ctx); err != nil { + return esContainer, fmt.Errorf("configure address: %w", err) + } - address, err := configureAddress(ctx, esContainer) + return esContainer, nil +} + +// certWriter is a helper that writes the details of a CA cert to options. +type certWriter struct { + options *Options + certPool *x509.CertPool +} + +// Read reads the CA cert from the reader and appends it to the options. +func (w *certWriter) Read(r io.Reader) error { + buf, err := io.ReadAll(r) if err != nil { - return nil, err + return fmt.Errorf("read CA cert: %w", err) } - esContainer.Settings.Address = address + w.options.CACert = buf + w.certPool.AppendCertsFromPEM(w.options.CACert) - return esContainer, nil + return nil +} + +// setWaitFor sets the req.WaitingFor strategy based on settings. +func setWaitFor(options *Options, req *testcontainers.ContainerRequest) { + var strategies []wait.Strategy + if req.WaitingFor != nil { + // Custom waiting strategy, ensure we honour it. + strategies = append(strategies, req.WaitingFor) + } + + waitHTTP := wait.ForHTTP("/").WithPort(defaultHTTPPort) + if sslRequired(req) { + waitHTTP = waitHTTP.WithTLS(true).WithAllowInsecure(true) + cw := &certWriter{ + options: options, + certPool: x509.NewCertPool(), + } + + waitHTTP = waitHTTP. + WithTLS(true, &tls.Config{RootCAs: cw.certPool}) + + strategies = append(strategies, wait.ForFile(defaultCaCertPath).WithMatcher(cw.Read)) + } + + if options.Password != "" || options.Username != "" { + waitHTTP = waitHTTP.WithBasicAuth(options.Username, options.Password) + } + + strategies = append(strategies, waitHTTP) + + if len(strategies) > 1 { + req.WaitingFor = wait.ForAll(strategies...) + return + } + + req.WaitingFor = strategies[0] } // configureAddress sets the address of the Elasticsearch container. // If the certificate is set, it will use https as protocol, otherwise http. -func configureAddress(ctx context.Context, c *ElasticsearchContainer) (string, error) { +func (c *ElasticsearchContainer) configureAddress(ctx context.Context) error { containerPort, err := c.MappedPort(ctx, defaultHTTPPort+"/tcp") if err != nil { - return "", err + return fmt.Errorf("mapped port: %w", err) } host, err := c.Host(ctx) if err != nil { - return "", err + return fmt.Errorf("host: %w", err) } proto := "http" @@ -129,53 +174,33 @@ func configureAddress(ctx context.Context, c *ElasticsearchContainer) (string, e proto = "https" } - return fmt.Sprintf("%s://%s:%s", proto, host, containerPort.Port()), nil + c.Settings.Address = fmt.Sprintf("%s://%s:%s", proto, host, containerPort.Port()) + + return nil } -// configureCertificate transfers the certificate settings to the container request. -// For that, it defines a post start hook that copies the certificate from the container to the host. -// The certificate is only available since version 8, and will be located in a well-known location. -func configureCertificate(settings *Options, req *testcontainers.GenericContainerRequest) error { - if isAtLeastVersion(req.Image, 8) { - // These configuration keys explicitly disable CA generation. - // If any are set we skip the file retrieval. - configKeys := []string{ - "xpack.security.enabled", - "xpack.security.http.ssl.enabled", - "xpack.security.transport.ssl.enabled", - } - for _, configKey := range configKeys { - if value, ok := req.Env[configKey]; ok { - if value == "false" { - return nil - } +// sslRequired returns true if the SSL is required, otherwise false. +func sslRequired(req *testcontainers.ContainerRequest) bool { + if !isAtLeastVersion(req.Image, 8) { + return false + } + + // These configuration keys explicitly disable CA generation. + // If any are set we skip the file retrieval. + configKeys := []string{ + "xpack.security.enabled", + "xpack.security.http.ssl.enabled", + "xpack.security.transport.ssl.enabled", + } + for _, configKey := range configKeys { + if value, ok := req.Env[configKey]; ok { + if value == "false" { + return false } } - - // The container needs a post ready hook to copy the certificate from the container to the host. - // This certificate is only available since version 8 - req.LifecycleHooks[0].PostReadies = append(req.LifecycleHooks[0].PostReadies, - func(ctx context.Context, container testcontainers.Container) error { - const defaultCaCertPath = "/usr/share/elasticsearch/config/certs/http_ca.crt" - - readCloser, err := container.CopyFileFromContainer(ctx, defaultCaCertPath) - if err != nil { - return err - } - - // receive the bytes from the default location - certBytes, err := io.ReadAll(readCloser) - if err != nil { - return err - } - - settings.CACert = certBytes - - return nil - }) } - return nil + return true } // configurePassword transfers the password settings to the container request. @@ -188,7 +213,7 @@ func configurePassword(settings *Options, req *testcontainers.GenericContainerRe if settings.Password != "" { if isOSS(req.Image) { - return fmt.Errorf("it's not possible to activate security on Elastic OSS Image. Please switch to the default distribution.") + return errors.New("it's not possible to activate security on Elastic OSS Image. Please switch to the default distribution.") } if _, ok := req.Env["ELASTIC_PASSWORD"]; !ok { diff --git a/modules/elasticsearch/elasticsearch_test.go b/modules/elasticsearch/elasticsearch_test.go index 1bc7d79456..14c5640e72 100644 --- a/modules/elasticsearch/elasticsearch_test.go +++ b/modules/elasticsearch/elasticsearch_test.go @@ -8,6 +8,8 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/elasticsearch" ) @@ -80,28 +82,17 @@ func TestElasticsearch(t *testing.T) { } esContainer, err := elasticsearch.Run(ctx, tt.image, opts...) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - if err := esContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, esContainer) + require.NoError(t, err) httpClient := configureHTTPClient(esContainer) - req, err := http.NewRequest("GET", esContainer.Settings.Address, nil) - if err != nil { - t.Fatal(err) - } + req, err := http.NewRequest(http.MethodGet, esContainer.Settings.Address, nil) + require.NoError(t, err) // set the password for the request using the Authentication header if tt.passwordCustomiser != nil { - if esContainer.Settings.Username != "elastic" { - t.Fatal("expected username to be elastic but got", esContainer.Settings.Username) - } + require.Equalf(t, "elastic", esContainer.Settings.Username, "expected username to be elastic but got: %s", esContainer.Settings.Username) // basicAuthHeader { req.SetBasicAuth(esContainer.Settings.Username, esContainer.Settings.Password) @@ -109,56 +100,33 @@ func TestElasticsearch(t *testing.T) { } resp, err := httpClient.Do(req) - if resp != nil { - defer resp.Body.Close() - } + require.NoError(t, err) + require.NotNil(t, resp) + defer resp.Body.Close() - if tt.image != baseImage8 && err != nil { - if tt.passwordCustomiser != nil { - t.Fatal(err, "should access with authorised HTTP client.") - } else if tt.passwordCustomiser == nil { - t.Fatal(err, "should access with unauthorised HTTP client.") - } + if tt.image == baseImage8 && tt.passwordCustomiser == nil { + // Elasticsearch 8 should return 401 Unauthorized, not an error in the request + require.Equalf(t, http.StatusUnauthorized, resp.StatusCode, "expected 401 status code for unauthorised HTTP client using TLS, but got: %s", resp.StatusCode) + + // finish validating the response when the request is unauthorised + return } - if tt.image == baseImage8 { - if tt.passwordCustomiser != nil && err != nil { - t.Fatal(err, "should access with authorised HTTP client using TLS.") - } - if tt.passwordCustomiser == nil && err == nil { - // Elasticsearch 8 should return 401 Unauthorized, not an error in the request - if resp.StatusCode != http.StatusUnauthorized { - t.Fatal("expected 401 status code for unauthorised HTTP client using TLS, but got", resp.StatusCode) - } + // validate Elasticsearch response + require.Equalf(t, http.StatusOK, resp.StatusCode, "expected 200 status code but got: %s", resp.StatusCode) - // finish validating the response when the request is unauthorised - return - } + var esResp ElasticsearchResponse + err = json.NewDecoder(resp.Body).Decode(&esResp) + require.NoError(t, err) + switch tt.image { + case baseImage7: + require.Equalf(t, "7.9.2", esResp.Version.Number, "expected version to be 7.9.2 but got: %s", esResp.Version.Number) + case baseImage8: + require.Equalf(t, "8.9.0", esResp.Version.Number, "expected version to be 8.9.0 but got: %s", esResp.Version.Number) } - // validate response - if resp != nil { - // validate Elasticsearch response - if resp.StatusCode != http.StatusOK { - t.Fatal("expected 200 status code but got", resp.StatusCode) - } - - var esResp ElasticsearchResponse - if err := json.NewDecoder(resp.Body).Decode(&esResp); err != nil { - t.Fatal(err) - } - - if tt.image == baseImage7 && esResp.Version.Number != "7.9.2" { - t.Fatal("expected version to be 7.9.2 but got", esResp.Version.Number) - } else if tt.image == baseImage8 && esResp.Version.Number != "8.9.0" { - t.Fatal("expected version to be 8.9.0 but got", esResp.Version.Number) - } - - if esResp.Tagline != "You Know, for Search" { - t.Fatal("expected tagline to be 'You Know, for Search' but got", esResp.Tagline) - } - } + require.Equalf(t, "You Know, for Search", esResp.Tagline, "expected tagline to be 'You Know, for Search' but got: %s", esResp.Tagline) }) } } @@ -184,25 +152,16 @@ func TestElasticsearch8WithoutSSL(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { ctx := context.Background() - container, err := elasticsearch.Run( + ctr, err := elasticsearch.Run( ctx, baseImage8, testcontainers.WithEnv(map[string]string{ test.configKey: "false", })) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - if len(container.Settings.CACert) > 0 { - t.Fatal("expected CA cert to be empty") - } + require.Emptyf(t, ctr.Settings.CACert, "expected CA cert to be empty") }) } } @@ -210,42 +169,28 @@ func TestElasticsearch8WithoutSSL(t *testing.T) { func TestElasticsearch8WithoutCredentials(t *testing.T) { ctx := context.Background() - container, err := elasticsearch.Run(ctx, baseImage8) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := elasticsearch.Run(ctx, baseImage8) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - httpClient := configureHTTPClient(container) + httpClient := configureHTTPClient(ctr) - req, err := http.NewRequest("GET", container.Settings.Address, nil) - if err != nil { - t.Fatal(err) - } + req, err := http.NewRequest(http.MethodGet, ctr.Settings.Address, nil) + require.NoError(t, err) // elastic:changeme are the default credentials for Elasticsearch 8 - req.SetBasicAuth(container.Settings.Username, container.Settings.Password) + req.SetBasicAuth(ctr.Settings.Username, ctr.Settings.Password) resp, err := httpClient.Do(req) - if err != nil { - t.Fatal(err, "Should be able to access / URI with client using default password over HTTPS.") - } + require.NoErrorf(t, err, "Should be able to access / URI with client using default password over HTTPS.") defer resp.Body.Close() var esResp ElasticsearchResponse - if err := json.NewDecoder(resp.Body).Decode(&esResp); err != nil { - t.Fatal(err) - } + err = json.NewDecoder(resp.Body).Decode(&esResp) + require.NoError(t, err) - if esResp.Tagline != "You Know, for Search" { - t.Fatal("expected tagline to be 'You Know, for Search' but got", esResp.Tagline) - } + require.Equalf(t, "You Know, for Search", esResp.Tagline, "expected tagline to be 'You Know, for Search' but got: %s", esResp.Tagline) } func TestElasticsearchOSSCannotuseWithPassword(t *testing.T) { @@ -253,10 +198,9 @@ func TestElasticsearchOSSCannotuseWithPassword(t *testing.T) { ossImage := elasticsearch.DefaultBaseImageOSS + ":7.9.2" - _, err := elasticsearch.Run(ctx, ossImage, elasticsearch.WithPassword("foo")) - if err == nil { - t.Fatal(err, "Should not be able to use WithPassword with OSS image.") - } + ctr, err := elasticsearch.Run(ctx, ossImage, elasticsearch.WithPassword("foo")) + testcontainers.CleanupContainer(t, ctr) + require.Errorf(t, err, "Should not be able to use WithPassword with OSS image.") } // configureHTTPClient configures an HTTP client for the Elasticsearch container. diff --git a/modules/elasticsearch/examples_test.go b/modules/elasticsearch/examples_test.go index db578a46ee..f4ada5df60 100644 --- a/modules/elasticsearch/examples_test.go +++ b/modules/elasticsearch/examples_test.go @@ -9,6 +9,7 @@ import ( es "github.com/elastic/go-elasticsearch/v8" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/elasticsearch" ) @@ -16,19 +17,21 @@ func ExampleRun() { // runElasticsearchContainer { ctx := context.Background() elasticsearchContainer, err := elasticsearch.Run(ctx, "docker.elastic.co/elasticsearch/elasticsearch:8.9.0") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - if err := elasticsearchContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(elasticsearchContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := elasticsearchContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -45,15 +48,15 @@ func ExampleRun_withUsingPassword() { "docker.elastic.co/elasticsearch/elasticsearch:7.9.2", elasticsearch.WithPassword("foo"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - err := elasticsearchContainer.Terminate(ctx) - if err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(elasticsearchContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } fmt.Println(strings.HasPrefix(elasticsearchContainer.Settings.Address, "http://")) @@ -72,15 +75,15 @@ func ExampleRun_connectUsingElasticsearchClient() { "docker.elastic.co/elasticsearch/elasticsearch:8.9.0", elasticsearch.WithPassword("foo"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - err := elasticsearchContainer.Terminate(ctx) - if err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(elasticsearchContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } cfg := es.Config{ Addresses: []string{ @@ -93,19 +96,22 @@ func ExampleRun_connectUsingElasticsearchClient() { esClient, err := es.NewClient(cfg) if err != nil { - log.Fatalf("error creating the client: %s", err) // nolint:gocritic + log.Printf("error creating the client: %s", err) + return } resp, err := esClient.Info() if err != nil { - log.Fatalf("error getting response: %s", err) + log.Printf("error getting response: %s", err) + return } defer resp.Body.Close() // } var esResp ElasticsearchResponse if err := json.NewDecoder(resp.Body).Decode(&esResp); err != nil { - log.Fatalf("error decoding response: %s", err) + log.Printf("error decoding response: %s", err) + return } fmt.Println(esResp.Tagline) diff --git a/modules/elasticsearch/go.mod b/modules/elasticsearch/go.mod index a766e55dd0..daaa6e8fac 100644 --- a/modules/elasticsearch/go.mod +++ b/modules/elasticsearch/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/elastic/go-elasticsearch/v8 v8.12.1 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 golang.org/x/mod v0.16.0 ) @@ -17,7 +17,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -55,9 +55,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/elasticsearch/go.sum b/modules/elasticsearch/go.sum index 59647a3793..42bbadf11f 100644 --- a/modules/elasticsearch/go.sum +++ b/modules/elasticsearch/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -101,6 +101,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -134,8 +136,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= @@ -159,14 +161,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/elasticsearch/options.go b/modules/elasticsearch/options.go index ed801c3b09..ba4dca75c3 100644 --- a/modules/elasticsearch/options.go +++ b/modules/elasticsearch/options.go @@ -16,7 +16,6 @@ type Options struct { func defaultOptions() *Options { return &Options{ - CACert: nil, Username: defaultUsername, } } diff --git a/modules/elasticsearch/version.go b/modules/elasticsearch/version.go index 9ddc2836ad..3124e312ab 100644 --- a/modules/elasticsearch/version.go +++ b/modules/elasticsearch/version.go @@ -22,7 +22,7 @@ func isAtLeastVersion(image string, major int) bool { } if !strings.HasPrefix(version, "v") { - version = fmt.Sprintf("v%s", version) + version = "v" + version } if semver.IsValid(version) { diff --git a/modules/etcd/Makefile b/modules/etcd/Makefile new file mode 100644 index 0000000000..7531baef98 --- /dev/null +++ b/modules/etcd/Makefile @@ -0,0 +1,5 @@ +include ../../commons-test.mk + +.PHONY: test +test: + $(MAKE) test-etcd diff --git a/modules/etcd/cmd_test.go b/modules/etcd/cmd_test.go new file mode 100644 index 0000000000..918c68dc84 --- /dev/null +++ b/modules/etcd/cmd_test.go @@ -0,0 +1,108 @@ +package etcd + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_configureCMD(t *testing.T) { + t.Run("default", func(t *testing.T) { + got := configureCMD(options{}) + want := []string{"etcd", "--name=default"} + require.Equal(t, want, got) + }) + + t.Run("with-node", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1"}, + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380", + } + require.Equal(t, want, got) + }) + + t.Run("with-node-datadir", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1"}, + mountDataDir: true, + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380", + "--data-dir=/data.etcd", + } + require.Equal(t, want, got) + }) + + t.Run("with-node-datadir-additional-args", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1"}, + mountDataDir: true, + additionalArgs: []string{"--auto-compaction-retention=1"}, + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380", + "--data-dir=/data.etcd", + "--auto-compaction-retention=1", + } + require.Equal(t, want, got) + }) + + t.Run("with-cluster", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1", "node2"}, + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380,node2=http://node2:2380", + } + require.Equal(t, want, got) + }) + + t.Run("with-cluster-token", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1", "node2"}, + clusterToken: "token", + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380,node2=http://node2:2380", + "--initial-cluster-token=token", + } + require.Equal(t, want, got) + }) +} diff --git a/modules/etcd/etcd.go b/modules/etcd/etcd.go new file mode 100644 index 0000000000..7ea78b4385 --- /dev/null +++ b/modules/etcd/etcd.go @@ -0,0 +1,272 @@ +package etcd + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/testcontainers/testcontainers-go" + tcnetwork "github.com/testcontainers/testcontainers-go/network" +) + +const ( + clientPort = "2379" + peerPort = "2380" + dataDir = "/data.etcd" + defaultClusterToken = "mys3cr3ttok3n" + scheme = "http" +) + +// EtcdContainer represents the etcd container type used in the module. It can be used to create a single-node instance or a cluster. +// For the cluster, the first node creates the cluster and the other nodes join it as child nodes. +type EtcdContainer struct { + testcontainers.Container + // childNodes contains the child nodes of the current node, forming a cluster + childNodes []*EtcdContainer + opts options +} + +// Terminate terminates the etcd container, its child nodes, and the network in which the cluster is running +// to communicate between the nodes. +func (c *EtcdContainer) Terminate(ctx context.Context) error { + var errs []error + + // child nodes has no other children + for i, child := range c.childNodes { + if err := child.Terminate(ctx); err != nil { + errs = append(errs, fmt.Errorf("terminate child node(%d): %w", i, err)) + } + } + + if c.Container != nil { + if err := c.Container.Terminate(ctx); err != nil { + errs = append(errs, fmt.Errorf("terminate cluster node: %w", err)) + } + } + + // remove the cluster network if it was created, but only for the first node + // we could check if the current node is the first one (index 0), + // and/or check that there are no child nodes + if c.opts.clusterNetwork != nil && c.opts.currentNode == 0 { + if err := c.opts.clusterNetwork.Remove(ctx); err != nil { + errs = append(errs, fmt.Errorf("remove cluster network: %w", err)) + } + } + + return errors.Join(errs...) +} + +// Run creates an instance of the etcd container type +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*EtcdContainer, error) { + req := testcontainers.ContainerRequest{ + Image: img, + Cmd: []string{}, + } + + genericContainerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + settings := defaultOptions(&req) + for _, opt := range opts { + if apply, ok := opt.(Option); ok { + apply(&settings) + } + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } + } + + clusterOpts, err := configureCluster(ctx, &settings, opts) + if err != nil { + return nil, fmt.Errorf("configure cluster: %w", err) + } + + // configure CMD with the nodes + genericContainerReq.Cmd = configureCMD(settings) + + // Initialise the etcd container with the current settings. + // The cluster network, if needed, is already part of the settings, + // so the following error handling returns a partially initialised container, + // allowing the caller to clean up the resources with the Terminate method. + c := &EtcdContainer{opts: settings} + + if settings.clusterNetwork != nil { + // apply the network to the current node + err := tcnetwork.WithNetwork([]string{settings.nodeNames[settings.currentNode]}, settings.clusterNetwork)(&genericContainerReq) + if err != nil { + return c, fmt.Errorf("with network: %w", err) + } + } + + if c.Container, err = testcontainers.GenericContainer(ctx, genericContainerReq); err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + // only the first node creates the cluster + if settings.currentNode == 0 { + for i := 1; i < len(settings.nodeNames); i++ { + // move to the next node + childNode, err := Run(ctx, req.Image, append(clusterOpts, withCurrentNode(i))...) + if err != nil { + // return the parent cluster node and the error, so the caller can clean up. + return c, fmt.Errorf("run cluster node: %w", err) + } + + c.childNodes = append(c.childNodes, childNode) + } + } + + return c, nil +} + +// configureCluster configures the cluster settings, ensuring that the cluster is properly configured with the necessary network and options, +// avoiding duplicate application of options to be passed to the successive nodes. +func configureCluster(ctx context.Context, settings *options, opts []testcontainers.ContainerCustomizer) ([]testcontainers.ContainerCustomizer, error) { + var clusterOpts []testcontainers.ContainerCustomizer + if len(settings.nodeNames) == 0 { + return clusterOpts, nil + } + + // pass cluster options to each node + etcdOpts := []Option{} + for _, opt := range opts { + // if the option is of type Option, it won't be applied to the settings + // this prevents the same option from being applied multiple times (e.g. updating the current node) + if apply, ok := opt.(Option); ok { + etcdOpts = append(etcdOpts, apply) + } else { + clusterOpts = append(clusterOpts, opt) + } + } + + if settings.clusterNetwork == nil { // the first time the network is created + newNetwork, err := tcnetwork.New(ctx) + if err != nil { + return clusterOpts, fmt.Errorf("new network: %w", err) + } + + // set the network for the first node + settings.clusterNetwork = newNetwork + + clusterOpts = append(clusterOpts, withClusterNetwork(newNetwork)) // save the network for the next nodes + } + + // we finally need to re-apply all the etcd-specific options + clusterOpts = append(clusterOpts, withClusterOptions(etcdOpts)) + + return clusterOpts, nil +} + +// configureCMD configures the etcd command line arguments, based on the settings provided, +// in order to create a cluster or a single-node instance. +func configureCMD(settings options) []string { + cmds := []string{"etcd"} + + if len(settings.nodeNames) == 0 { + cmds = append(cmds, "--name=default") + } else { + clusterCmds := []string{ + "--name=" + settings.nodeNames[settings.currentNode], + "--initial-advertise-peer-urls=" + scheme + "://" + settings.nodeNames[settings.currentNode] + ":" + peerPort, + "--advertise-client-urls=" + scheme + "://" + settings.nodeNames[settings.currentNode] + ":" + clientPort, + "--listen-peer-urls=" + scheme + "://0.0.0.0:" + peerPort, + "--listen-client-urls=" + scheme + "://0.0.0.0:" + clientPort, + "--initial-cluster-state=new", + } + + clusterStateValues := make([]string, len(settings.nodeNames)) + for i, node := range settings.nodeNames { + clusterStateValues[i] = node + "=" + scheme + "://" + node + ":" + peerPort + } + clusterCmds = append(clusterCmds, "--initial-cluster="+strings.Join(clusterStateValues, ",")) + + if settings.clusterToken != "" { + clusterCmds = append(clusterCmds, "--initial-cluster-token="+settings.clusterToken) + } + + cmds = append(cmds, clusterCmds...) + } + + if settings.mountDataDir { + cmds = append(cmds, "--data-dir="+dataDir) + } + + cmds = append(cmds, settings.additionalArgs...) + + return cmds +} + +// ClientEndpoint returns the client endpoint for the etcd container, and an error if any. +// For a cluster, it returns the client endpoint of the first node. +func (c *EtcdContainer) ClientEndpoint(ctx context.Context) (string, error) { + host, err := c.Host(ctx) + if err != nil { + return "", err + } + + port, err := c.MappedPort(ctx, clientPort) + if err != nil { + return "", err + } + + return fmt.Sprintf("http://%s:%s", host, port.Port()), nil +} + +// ClientEndpoints returns the client endpoints for the etcd cluster. +func (c *EtcdContainer) ClientEndpoints(ctx context.Context) ([]string, error) { + endpoint, err := c.ClientEndpoint(ctx) + if err != nil { + return nil, err + } + + endpoints := []string{endpoint} + + for _, node := range c.childNodes { + endpoint, err := node.ClientEndpoint(ctx) + if err != nil { + return nil, err + } + endpoints = append(endpoints, endpoint) + } + + return endpoints, nil +} + +// PeerEndpoint returns the peer endpoint for the etcd container, and an error if any. +// For a cluster, it returns the peer endpoint of the first node. +func (c *EtcdContainer) PeerEndpoint(ctx context.Context) (string, error) { + host, err := c.Host(ctx) + if err != nil { + return "", err + } + + port, err := c.MappedPort(ctx, peerPort) + if err != nil { + return "", err + } + + return fmt.Sprintf("http://%s:%s", host, port.Port()), nil +} + +// PeerEndpoints returns the peer endpoints for the etcd cluster. +func (c *EtcdContainer) PeerEndpoints(ctx context.Context) ([]string, error) { + endpoint, err := c.PeerEndpoint(ctx) + if err != nil { + return nil, err + } + + endpoints := []string{endpoint} + + for _, node := range c.childNodes { + endpoint, err := node.PeerEndpoint(ctx) + if err != nil { + return nil, err + } + endpoints = append(endpoints, endpoint) + } + + return endpoints, nil +} diff --git a/modules/etcd/etcd_test.go b/modules/etcd/etcd_test.go new file mode 100644 index 0000000000..046e277cac --- /dev/null +++ b/modules/etcd/etcd_test.go @@ -0,0 +1,60 @@ +package etcd_test + +import ( + "context" + "io" + "testing" + "time" + + "github.com/stretchr/testify/require" + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/testcontainers/testcontainers-go" + tcexec "github.com/testcontainers/testcontainers-go/exec" + "github.com/testcontainers/testcontainers-go/modules/etcd" +) + +func TestRun(t *testing.T) { + ctx := context.Background() + + ctr, err := etcd.Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + c, r, err := ctr.Exec(ctx, []string{"etcdctl", "member", "list"}, tcexec.Multiplexed()) + require.NoError(t, err) + require.Zero(t, c) + + output, err := io.ReadAll(r) + require.NoError(t, err) + require.Contains(t, string(output), "default") +} + +func TestRun_PutGet(t *testing.T) { + ctx := context.Background() + + ctr, err := etcd.Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14", etcd.WithNodes("etcd-1", "etcd-2", "etcd-3")) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + clientEndpoints, err := ctr.ClientEndpoints(ctx) + require.NoError(t, err) + + cli, err := clientv3.New(clientv3.Config{ + Endpoints: clientEndpoints, + DialTimeout: 5 * time.Second, + }) + require.NoError(t, err) + defer cli.Close() + + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + _, err = cli.Put(ctx, "sample_key", "sample_value") + require.NoError(t, err) + + resp, err := cli.Get(ctx, "sample_key") + require.NoError(t, err) + + require.Len(t, resp.Kvs, 1) + require.Equal(t, "sample_value", string(resp.Kvs[0].Value)) +} diff --git a/modules/etcd/etcd_unit_test.go b/modules/etcd/etcd_unit_test.go new file mode 100644 index 0000000000..d32b9519f7 --- /dev/null +++ b/modules/etcd/etcd_unit_test.go @@ -0,0 +1,113 @@ +package etcd + +import ( + "context" + "fmt" + "io" + "testing" + + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/errdefs" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" + tcexec "github.com/testcontainers/testcontainers-go/exec" + tcnetwork "github.com/testcontainers/testcontainers-go/network" +) + +func TestRunCluster1Node(t *testing.T) { + ctx := context.Background() + + ctr, err := Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + // the topology has only one node with no children + require.Empty(t, ctr.childNodes) + require.Equal(t, defaultClusterToken, ctr.opts.clusterToken) +} + +func TestRunClusterMultipleNodes(t *testing.T) { + t.Run("2-nodes", testCluster(t, "etcd-1", "etcd-2")) + t.Run("3-nodes", testCluster(t, "etcd-1", "etcd-2", "etcd-3")) +} + +func TestTerminate(t *testing.T) { + ctx := context.Background() + + ctr, err := Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14", WithNodes("etcd-1", "etcd-2", "etcd-3")) + require.NoError(t, err) + require.NoError(t, ctr.Terminate(ctx)) + + // verify that the network and the containers does no longer exist + + cli, err := testcontainers.NewDockerClientWithOpts(context.Background()) + require.NoError(t, err) + defer cli.Close() + + _, err = cli.ContainerInspect(context.Background(), ctr.GetContainerID()) + require.True(t, errdefs.IsNotFound(err)) + + for _, child := range ctr.childNodes { + _, err := cli.ContainerInspect(context.Background(), child.GetContainerID()) + require.True(t, errdefs.IsNotFound(err)) + } + + _, err = cli.NetworkInspect(context.Background(), ctr.opts.clusterNetwork.ID, network.InspectOptions{}) + require.True(t, errdefs.IsNotFound(err)) +} + +func TestTerminate_partiallyInitialised(t *testing.T) { + newNetwork, err := tcnetwork.New(context.Background()) + require.NoError(t, err) + + ctr := &EtcdContainer{ + opts: options{ + clusterNetwork: newNetwork, + }, + } + + require.NoError(t, ctr.Terminate(context.Background())) + + cli, err := testcontainers.NewDockerClientWithOpts(context.Background()) + require.NoError(t, err) + defer cli.Close() + + _, err = cli.NetworkInspect(context.Background(), ctr.opts.clusterNetwork.ID, network.InspectOptions{}) + require.True(t, errdefs.IsNotFound(err)) +} + +// testCluster is a helper function to test the creation of an etcd cluster with the specified nodes. +func testCluster(t *testing.T, node1 string, node2 string, nodes ...string) func(t *testing.T) { + t.Helper() + + return func(tt *testing.T) { + const clusterToken string = "My-cluster-t0k3n" + + ctx := context.Background() + + ctr, err := Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14", WithNodes(node1, node2, nodes...), WithClusterToken(clusterToken)) + testcontainers.CleanupContainer(t, ctr) + require.NoError(tt, err) + + require.Equal(tt, clusterToken, ctr.opts.clusterToken) + + // the topology has one parent node, one child node and optionally more child nodes + // depending on the number of nodes specified + require.Len(tt, ctr.childNodes, 1+len(nodes)) + + for i, node := range ctr.childNodes { + require.Empty(t, node.childNodes) // child nodes has no children + + c, r, err := node.Exec(ctx, []string{"etcdctl", "member", "list"}, tcexec.Multiplexed()) + require.NoError(tt, err) + + output, err := io.ReadAll(r) + require.NoError(t, err) + require.Contains(t, string(output), fmt.Sprintf("etcd-%d", i+1)) + + require.Zero(tt, c) + require.Equal(tt, clusterToken, node.opts.clusterToken) + } + } +} diff --git a/modules/etcd/examples_test.go b/modules/etcd/examples_test.go new file mode 100644 index 0000000000..950d5ecd05 --- /dev/null +++ b/modules/etcd/examples_test.go @@ -0,0 +1,97 @@ +package etcd_test + +import ( + "context" + "fmt" + "log" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/etcd" +) + +func ExampleRun() { + // runetcdContainer { + ctx := context.Background() + + etcdContainer, err := etcd.Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14") + defer func() { + if err := testcontainers.TerminateContainer(etcdContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + // } + + state, err := etcdContainer.State(ctx) + if err != nil { + log.Printf("failed to get container state: %s", err) + return + } + + fmt.Println(state.Running) + + // Output: + // true +} + +func ExampleRun_cluster() { + ctx := context.Background() + + ctr, err := etcd.Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14", etcd.WithNodes("etcd-1", "etcd-2", "etcd-3")) + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + defer func() { + if err := testcontainers.TerminateContainer(ctr); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + + clientEndpoints, err := ctr.ClientEndpoints(ctx) + if err != nil { + log.Printf("failed to get client endpoints: %s", err) + return + } + + // we have 3 nodes, 1 cluster node and 2 child nodes + fmt.Println(len(clientEndpoints)) + + cli, err := clientv3.New(clientv3.Config{ + Endpoints: clientEndpoints, + DialTimeout: 5 * time.Second, + }) + if err != nil { + log.Printf("failed to create etcd client: %s", err) + return + } + defer cli.Close() + + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + _, err = cli.Put(ctx, "sample_key", "sample_value") + if err != nil { + log.Printf("failed to put key: %s", err) + return + } + + resp, err := cli.Get(ctx, "sample_key") + if err != nil { + log.Printf("failed to get key: %s", err) + return + } + + fmt.Println(len(resp.Kvs)) + fmt.Println(string(resp.Kvs[0].Value)) + + // Output: + // 3 + // 1 + // sample_value +} diff --git a/modules/etcd/go.mod b/modules/etcd/go.mod new file mode 100644 index 0000000000..b0946686cc --- /dev/null +++ b/modules/etcd/go.mod @@ -0,0 +1,75 @@ +module github.com/testcontainers/testcontainers-go/modules/etcd + +go 1.22 + +require ( + github.com/docker/docker v27.1.1+incompatible + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 + go.etcd.io/etcd/client/v3 v3.5.16 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.etcd.io/etcd/api/v3 v3.5.16 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.64.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/etcd/go.sum b/modules/etcd/go.sum new file mode 100644 index 0000000000..505f9ae432 --- /dev/null +++ b/modules/etcd/go.sum @@ -0,0 +1,216 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= +go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28= +go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q= +go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E= +go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE= +go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/modules/etcd/options.go b/modules/etcd/options.go new file mode 100644 index 0000000000..1359e4a3b4 --- /dev/null +++ b/modules/etcd/options.go @@ -0,0 +1,107 @@ +package etcd + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + tcexec "github.com/testcontainers/testcontainers-go/exec" +) + +type options struct { + currentNode int + clusterNetwork *testcontainers.DockerNetwork + nodeNames []string + clusterToken string + additionalArgs []string + mountDataDir bool // flag needed to avoid extra calculations with the lifecycle hooks + containerRequest *testcontainers.ContainerRequest +} + +func defaultOptions(req *testcontainers.ContainerRequest) options { + return options{ + clusterToken: defaultClusterToken, + containerRequest: req, + } +} + +// Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface. +var _ testcontainers.ContainerCustomizer = (Option)(nil) + +// Option is an option for the Etcd container. +type Option func(*options) + +// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { + // NOOP to satisfy interface. + return nil +} + +// WithAdditionalArgs is an option to pass additional arguments to the etcd container. +// They will be appended last to the command line. +func WithAdditionalArgs(args ...string) Option { + return func(o *options) { + o.additionalArgs = args + } +} + +// WithDataDir is an option to mount the data directory, which is located at /data.etcd. +// The option will add a lifecycle hook to the container to change the permissions of the data directory. +func WithDataDir() Option { + return func(o *options) { + // Avoid extra calculations with the lifecycle hooks + o.mountDataDir = true + + o.containerRequest.LifecycleHooks = append(o.containerRequest.LifecycleHooks, testcontainers.ContainerLifecycleHooks{ + PostStarts: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { + _, _, err := c.Exec(ctx, []string{"chmod", "o+rwx", "-R", dataDir}, tcexec.Multiplexed()) + if err != nil { + return fmt.Errorf("chmod etcd data dir: %w", err) + } + + return nil + }, + }, + }) + } +} + +// WithNodes is an option to set the nodes of the etcd cluster. +// It should be used to create a cluster with more than one node. +func WithNodes(node1 string, node2 string, nodes ...string) Option { + return func(o *options) { + o.nodeNames = append([]string{node1, node2}, nodes...) + } +} + +// withCurrentNode is an option to set the current node index. +// It's an internal option and should not be used by the user. +func withCurrentNode(i int) Option { + return func(o *options) { + o.currentNode = i + } +} + +// withClusterNetwork is an option to set the cluster network. +// It's an internal option and should not be used by the user. +func withClusterNetwork(n *testcontainers.DockerNetwork) Option { + return func(o *options) { + o.clusterNetwork = n + } +} + +// WithClusterToken is an option to set the cluster token. +func WithClusterToken(token string) Option { + return func(o *options) { + o.clusterToken = token + } +} + +func withClusterOptions(opts []Option) Option { + return func(o *options) { + for _, opt := range opts { + opt(o) + } + } +} diff --git a/modules/gcloud/bigquery.go b/modules/gcloud/bigquery.go index 54363dc2f2..ce8ff1fcbd 100644 --- a/modules/gcloud/bigquery.go +++ b/modules/gcloud/bigquery.go @@ -33,18 +33,5 @@ func RunBigQuery(ctx context.Context, img string, opts ...testcontainers.Contain req.Cmd = []string{"--project", settings.ProjectID} - container, err := testcontainers.GenericContainer(ctx, req) - if err != nil { - return nil, err - } - - bigQueryContainer, err := newGCloudContainer(ctx, 9050, container, settings) - if err != nil { - return nil, err - } - - // always prepend http:// to the URI - bigQueryContainer.URI = "http://" + bigQueryContainer.URI - - return bigQueryContainer, nil + return newGCloudContainer(ctx, req, 9050, settings, "http://") } diff --git a/modules/gcloud/bigquery_test.go b/modules/gcloud/bigquery_test.go index a39347a23f..957b17e3e5 100644 --- a/modules/gcloud/bigquery_test.go +++ b/modules/gcloud/bigquery_test.go @@ -13,6 +13,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/gcloud" ) @@ -25,16 +26,15 @@ func ExampleRunBigQueryContainer() { "ghcr.io/goccy/bigquery-emulator:0.4.3", gcloud.WithProjectID("bigquery-project"), ) - if err != nil { - log.Fatalf("failed to run container: %v", err) - } - - // Clean up the container defer func() { - if err := bigQueryContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %v", err) + if err := testcontainers.TerminateContainer(bigQueryContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to run container: %v", err) + return + } // } // bigQueryClient { @@ -49,7 +49,8 @@ func ExampleRunBigQueryContainer() { client, err := bigquery.NewClient(ctx, projectID, opts...) if err != nil { - log.Fatalf("failed to create bigquery client: %v", err) // nolint:gocritic + log.Printf("failed to create bigquery client: %v", err) + return } defer client.Close() // } @@ -57,13 +58,15 @@ func ExampleRunBigQueryContainer() { createFnQuery := client.Query("CREATE FUNCTION testr(arr ARRAY>) AS ((SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem))") _, err = createFnQuery.Read(ctx) if err != nil { - log.Fatalf("failed to create function: %v", err) + log.Printf("failed to create function: %v", err) + return } selectQuery := client.Query("SELECT testr([STRUCT(\"foo\", 10), STRUCT(\"bar\", 40), STRUCT(\"foo\", 20)])") it, err := selectQuery.Read(ctx) if err != nil { - log.Fatalf("failed to read query: %v", err) + log.Printf("failed to read query: %v", err) + return } var val []bigquery.Value @@ -73,7 +76,8 @@ func ExampleRunBigQueryContainer() { break } if err != nil { - log.Fatalf("failed to iterate: %v", err) + log.Printf("failed to iterate: %v", err) + return } } diff --git a/modules/gcloud/bigtable.go b/modules/gcloud/bigtable.go index 4bea521ff1..134f14d1d6 100644 --- a/modules/gcloud/bigtable.go +++ b/modules/gcloud/bigtable.go @@ -2,7 +2,6 @@ package gcloud import ( "context" - "fmt" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -33,13 +32,8 @@ func RunBigTable(ctx context.Context, img string, opts ...testcontainers.Contain req.Cmd = []string{ "/bin/sh", "-c", - "gcloud beta emulators bigtable start --host-port 0.0.0.0:9000 " + fmt.Sprintf("--project=%s", settings.ProjectID), + "gcloud beta emulators bigtable start --host-port 0.0.0.0:9000 --project=" + settings.ProjectID, } - container, err := testcontainers.GenericContainer(ctx, req) - if err != nil { - return nil, err - } - - return newGCloudContainer(ctx, 9000, container, settings) + return newGCloudContainer(ctx, req, 9000, settings, "") } diff --git a/modules/gcloud/bigtable_test.go b/modules/gcloud/bigtable_test.go index 15409e2b0e..553581bcc4 100644 --- a/modules/gcloud/bigtable_test.go +++ b/modules/gcloud/bigtable_test.go @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/gcloud" ) @@ -22,16 +23,15 @@ func ExampleRunBigTableContainer() { "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", gcloud.WithProjectID("bigtable-project"), ) - if err != nil { - log.Fatalf("failed to run container: %v", err) - } - - // Clean up the container defer func() { - if err := bigTableContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %v", err) + if err := testcontainers.TerminateContainer(bigTableContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to run container: %v", err) + return + } // } // bigTableAdminClient { @@ -49,24 +49,28 @@ func ExampleRunBigTableContainer() { } adminClient, err := bigtable.NewAdminClient(ctx, projectId, instanceId, options...) if err != nil { - log.Fatalf("failed to create admin client: %v", err) // nolint:gocritic + log.Printf("failed to create admin client: %v", err) + return } defer adminClient.Close() // } err = adminClient.CreateTable(ctx, tableName) if err != nil { - log.Fatalf("failed to create table: %v", err) + log.Printf("failed to create table: %v", err) + return } err = adminClient.CreateColumnFamily(ctx, tableName, "name") if err != nil { - log.Fatalf("failed to create column family: %v", err) + log.Printf("failed to create column family: %v", err) + return } // bigTableClient { client, err := bigtable.NewClient(ctx, projectId, instanceId, options...) if err != nil { - log.Fatalf("failed to create client: %v", err) + log.Printf("failed to create client: %v", err) + return } defer client.Close() // } @@ -77,12 +81,14 @@ func ExampleRunBigTableContainer() { mut.Set("name", "firstName", bigtable.Now(), []byte("Gopher")) err = tbl.Apply(ctx, "1", mut) if err != nil { - log.Fatalf("failed to apply mutation: %v", err) + log.Printf("failed to apply mutation: %v", err) + return } row, err := tbl.ReadRow(ctx, "1", bigtable.RowFilter(bigtable.FamilyFilter("name"))) if err != nil { - log.Fatalf("failed to read row: %v", err) + log.Printf("failed to read row: %v", err) + return } fmt.Println(string(row["name"][0].Value)) diff --git a/modules/gcloud/datastore.go b/modules/gcloud/datastore.go index 92ab671842..caf53e9879 100644 --- a/modules/gcloud/datastore.go +++ b/modules/gcloud/datastore.go @@ -2,7 +2,6 @@ package gcloud import ( "context" - "fmt" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -33,13 +32,8 @@ func RunDatastore(ctx context.Context, img string, opts ...testcontainers.Contai req.Cmd = []string{ "/bin/sh", "-c", - "gcloud beta emulators datastore start --host-port 0.0.0.0:8081 " + fmt.Sprintf("--project=%s", settings.ProjectID), + "gcloud beta emulators datastore start --host-port 0.0.0.0:8081 --project=" + settings.ProjectID, } - container, err := testcontainers.GenericContainer(ctx, req) - if err != nil { - return nil, err - } - - return newGCloudContainer(ctx, 8081, container, settings) + return newGCloudContainer(ctx, req, 8081, settings, "") } diff --git a/modules/gcloud/datastore_test.go b/modules/gcloud/datastore_test.go index 0cf04780ef..fa056bbf63 100644 --- a/modules/gcloud/datastore_test.go +++ b/modules/gcloud/datastore_test.go @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/gcloud" ) @@ -22,16 +23,15 @@ func ExampleRunDatastoreContainer() { "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", gcloud.WithProjectID("datastore-project"), ) - if err != nil { - log.Fatalf("failed to run container: %v", err) - } - - // Clean up the container defer func() { - if err := datastoreContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %v", err) + if err := testcontainers.TerminateContainer(datastoreContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to run container: %v", err) + return + } // } // datastoreClient { @@ -45,7 +45,8 @@ func ExampleRunDatastoreContainer() { dsClient, err := datastore.NewClient(ctx, projectID, options...) if err != nil { - log.Fatalf("failed to create client: %v", err) // nolint:gocritic + log.Printf("failed to create client: %v", err) + return } defer dsClient.Close() // } @@ -60,13 +61,15 @@ func ExampleRunDatastoreContainer() { } _, err = dsClient.Put(ctx, k, &data) if err != nil { - log.Fatalf("failed to put data: %v", err) + log.Printf("failed to put data: %v", err) + return } saved := Task{} err = dsClient.Get(ctx, k, &saved) if err != nil { - log.Fatalf("failed to get data: %v", err) + log.Printf("failed to get data: %v", err) + return } fmt.Println(saved.Description) diff --git a/modules/gcloud/firestore.go b/modules/gcloud/firestore.go index 7f9ced72f7..297b47f80c 100644 --- a/modules/gcloud/firestore.go +++ b/modules/gcloud/firestore.go @@ -2,7 +2,6 @@ package gcloud import ( "context" - "fmt" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -33,13 +32,8 @@ func RunFirestore(ctx context.Context, img string, opts ...testcontainers.Contai req.Cmd = []string{ "/bin/sh", "-c", - "gcloud beta emulators firestore start --host-port 0.0.0.0:8080 " + fmt.Sprintf("--project=%s", settings.ProjectID), + "gcloud beta emulators firestore start --host-port 0.0.0.0:8080 --project=" + settings.ProjectID, } - container, err := testcontainers.GenericContainer(ctx, req) - if err != nil { - return nil, err - } - - return newGCloudContainer(ctx, 8080, container, settings) + return newGCloudContainer(ctx, req, 8080, settings, "") } diff --git a/modules/gcloud/firestore_test.go b/modules/gcloud/firestore_test.go index 54e03e4522..83ccd0464c 100644 --- a/modules/gcloud/firestore_test.go +++ b/modules/gcloud/firestore_test.go @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/gcloud" ) @@ -32,16 +33,15 @@ func ExampleRunFirestoreContainer() { "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", gcloud.WithProjectID("firestore-project"), ) - if err != nil { - log.Fatalf("failed to run container: %v", err) - } - - // Clean up the container defer func() { - if err := firestoreContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %v", err) + if err := testcontainers.TerminateContainer(firestoreContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to run container: %v", err) + return + } // } // firestoreClient { @@ -49,13 +49,15 @@ func ExampleRunFirestoreContainer() { conn, err := grpc.NewClient(firestoreContainer.URI, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials(emulatorCreds{})) if err != nil { - log.Fatalf("failed to dial: %v", err) // nolint:gocritic + log.Printf("failed to dial: %v", err) + return } options := []option.ClientOption{option.WithGRPCConn(conn)} client, err := firestore.NewClient(ctx, projectID, options...) if err != nil { - log.Fatalf("failed to create client: %v", err) + log.Printf("failed to create client: %v", err) + return } defer client.Close() // } @@ -74,17 +76,20 @@ func ExampleRunFirestoreContainer() { } _, err = docRef.Create(ctx, data) if err != nil { - log.Fatalf("failed to create document: %v", err) + log.Printf("failed to create document: %v", err) + return } docsnap, err := docRef.Get(ctx) if err != nil { - log.Fatalf("failed to get document: %v", err) + log.Printf("failed to get document: %v", err) + return } var saved Person if err := docsnap.DataTo(&saved); err != nil { - log.Fatalf("failed to convert data: %v", err) + log.Printf("failed to convert data: %v", err) + return } fmt.Println(saved.Firstname, saved.Lastname) diff --git a/modules/gcloud/gcloud.go b/modules/gcloud/gcloud.go index a5886dc743..7f0e7cdffb 100644 --- a/modules/gcloud/gcloud.go +++ b/modules/gcloud/gcloud.go @@ -18,26 +18,29 @@ type GCloudContainer struct { } // newGCloudContainer creates a new GCloud container, obtaining the URL to access the container from the specified port. -func newGCloudContainer(ctx context.Context, port int, c testcontainers.Container, settings options) (*GCloudContainer, error) { +func newGCloudContainer(ctx context.Context, req testcontainers.GenericContainerRequest, port int, settings options, urlPrefix string) (*GCloudContainer, error) { + container, err := testcontainers.GenericContainer(ctx, req) + var c *GCloudContainer + if container != nil { + c = &GCloudContainer{Container: container, Settings: settings} + } + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + mappedPort, err := c.MappedPort(ctx, nat.Port(fmt.Sprintf("%d/tcp", port))) if err != nil { - return nil, err + return c, fmt.Errorf("mapped port: %w", err) } hostIP, err := c.Host(ctx) if err != nil { - return nil, err + return c, fmt.Errorf("host: %w", err) } - uri := fmt.Sprintf("%s:%s", hostIP, mappedPort.Port()) - - gCloudContainer := &GCloudContainer{ - Container: c, - Settings: settings, - URI: uri, - } + c.URI = urlPrefix + hostIP + ":" + mappedPort.Port() - return gCloudContainer, nil + return c, nil } type options struct { diff --git a/modules/gcloud/go.mod b/modules/gcloud/go.mod index 00a723f58c..5c05fce458 100644 --- a/modules/gcloud/go.mod +++ b/modules/gcloud/go.mod @@ -10,7 +10,7 @@ require ( cloud.google.com/go/pubsub v1.36.2 cloud.google.com/go/spanner v1.57.0 github.com/docker/go-connections v0.5.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 google.golang.org/api v0.169.0 google.golang.org/grpc v1.64.1 ) @@ -32,7 +32,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect @@ -68,10 +69,12 @@ require ( github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect @@ -82,13 +85,13 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect @@ -97,6 +100,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/gcloud/go.sum b/modules/gcloud/go.sum index 31d286e7d0..f8036b37f9 100644 --- a/modules/gcloud/go.sum +++ b/modules/gcloud/go.sum @@ -55,8 +55,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -146,6 +146,10 @@ github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -180,6 +184,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -190,8 +196,9 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -239,8 +246,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= @@ -274,8 +281,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -292,18 +299,18 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -361,6 +368,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/gcloud/pubsub.go b/modules/gcloud/pubsub.go index a2a4e74a1c..d57ea35c16 100644 --- a/modules/gcloud/pubsub.go +++ b/modules/gcloud/pubsub.go @@ -2,7 +2,6 @@ package gcloud import ( "context" - "fmt" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -33,13 +32,8 @@ func RunPubsub(ctx context.Context, img string, opts ...testcontainers.Container req.Cmd = []string{ "/bin/sh", "-c", - "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085 " + fmt.Sprintf("--project=%s", settings.ProjectID), + "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085 --project=" + settings.ProjectID, } - container, err := testcontainers.GenericContainer(ctx, req) - if err != nil { - return nil, err - } - - return newGCloudContainer(ctx, 8085, container, settings) + return newGCloudContainer(ctx, req, 8085, settings, "") } diff --git a/modules/gcloud/pubsub_test.go b/modules/gcloud/pubsub_test.go index e0718a4d03..151df3a546 100644 --- a/modules/gcloud/pubsub_test.go +++ b/modules/gcloud/pubsub_test.go @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/gcloud" ) @@ -22,16 +23,15 @@ func ExampleRunPubsubContainer() { "gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators", gcloud.WithProjectID("pubsub-project"), ) - if err != nil { - log.Fatalf("failed to run container: %v", err) - } - - // Clean up the container defer func() { - if err := pubsubContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %v", err) + if err := testcontainers.TerminateContainer(pubsubContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to run container: %v", err) + return + } // } // pubsubClient { @@ -39,30 +39,35 @@ func ExampleRunPubsubContainer() { conn, err := grpc.NewClient(pubsubContainer.URI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - log.Fatalf("failed to dial: %v", err) // nolint:gocritic + log.Printf("failed to dial: %v", err) + return } options := []option.ClientOption{option.WithGRPCConn(conn)} client, err := pubsub.NewClient(ctx, projectID, options...) if err != nil { - log.Fatalf("failed to create client: %v", err) + log.Printf("failed to create client: %v", err) + return } defer client.Close() // } topic, err := client.CreateTopic(ctx, "greetings") if err != nil { - log.Fatalf("failed to create topic: %v", err) + log.Printf("failed to create topic: %v", err) + return } subscription, err := client.CreateSubscription(ctx, "subscription", pubsub.SubscriptionConfig{Topic: topic}) if err != nil { - log.Fatalf("failed to create subscription: %v", err) + log.Printf("failed to create subscription: %v", err) + return } result := topic.Publish(ctx, &pubsub.Message{Data: []byte("Hello World")}) _, err = result.Get(ctx) if err != nil { - log.Fatalf("failed to publish message: %v", err) + log.Printf("failed to publish message: %v", err) + return } var data []byte @@ -73,7 +78,8 @@ func ExampleRunPubsubContainer() { defer cancel() }) if err != nil { - log.Fatalf("failed to receive message: %v", err) + log.Printf("failed to receive message: %v", err) + return } fmt.Println(string(data)) diff --git a/modules/gcloud/spanner.go b/modules/gcloud/spanner.go index d57154ab1d..8b306db4ce 100644 --- a/modules/gcloud/spanner.go +++ b/modules/gcloud/spanner.go @@ -29,10 +29,5 @@ func RunSpanner(ctx context.Context, img string, opts ...testcontainers.Containe return nil, err } - container, err := testcontainers.GenericContainer(ctx, req) - if err != nil { - return nil, err - } - - return newGCloudContainer(ctx, 9010, container, settings) + return newGCloudContainer(ctx, req, 9010, settings, "") } diff --git a/modules/gcloud/spanner_test.go b/modules/gcloud/spanner_test.go index 10fdec441f..02a1be1c78 100644 --- a/modules/gcloud/spanner_test.go +++ b/modules/gcloud/spanner_test.go @@ -15,6 +15,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/gcloud" ) @@ -27,16 +28,15 @@ func ExampleRunSpannerContainer() { "gcr.io/cloud-spanner-emulator/emulator:1.4.0", gcloud.WithProjectID("spanner-project"), ) - if err != nil { - log.Fatalf("failed to run container: %v", err) - } - - // Clean up the container defer func() { - if err := spannerContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %v", err) + if err := testcontainers.TerminateContainer(spannerContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to run container: %v", err) + return + } // } // spannerAdminClient { @@ -56,31 +56,35 @@ func ExampleRunSpannerContainer() { instanceAdmin, err := instance.NewInstanceAdminClient(ctx, options...) if err != nil { - log.Fatalf("failed to create instance admin client: %v", err) // nolint:gocritic + log.Printf("failed to create instance admin client: %v", err) + return } defer instanceAdmin.Close() // } instanceOp, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ - Parent: fmt.Sprintf("projects/%s", projectId), + Parent: "projects/" + projectId, InstanceId: instanceId, Instance: &instancepb.Instance{ DisplayName: instanceId, }, }) if err != nil { - log.Fatalf("failed to create instance: %v", err) + log.Printf("failed to create instance: %v", err) + return } _, err = instanceOp.Wait(ctx) if err != nil { - log.Fatalf("failed to wait for instance creation: %v", err) + log.Printf("failed to wait for instance creation: %v", err) + return } // spannerDBAdminClient { c, err := database.NewDatabaseAdminClient(ctx, options...) if err != nil { - log.Fatalf("failed to create admin client: %v", err) + log.Printf("failed to create admin client: %v", err) + return } defer c.Close() // } @@ -93,17 +97,20 @@ func ExampleRunSpannerContainer() { }, }) if err != nil { - log.Fatalf("failed to create database: %v", err) + log.Printf("failed to create database: %v", err) + return } _, err = databaseOp.Wait(ctx) if err != nil { - log.Fatalf("failed to wait for database creation: %v", err) + log.Printf("failed to wait for database creation: %v", err) + return } db := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseName) client, err := spanner.NewClient(ctx, db, options...) if err != nil { - log.Fatalf("failed to create client: %v", err) + log.Printf("failed to create client: %v", err) + return } defer client.Close() @@ -113,18 +120,21 @@ func ExampleRunSpannerContainer() { []interface{}{"Go", "Gopher"}), }) if err != nil { - log.Fatalf("failed to apply mutation: %v", err) + log.Printf("failed to apply mutation: %v", err) + return } row, err := client.Single().ReadRow(ctx, "Languages", spanner.Key{"Go"}, []string{"mascot"}) if err != nil { - log.Fatalf("failed to read row: %v", err) + log.Printf("failed to read row: %v", err) + return } var mascot string err = row.ColumnByName("Mascot", &mascot) if err != nil { - log.Fatalf("failed to read column: %v", err) + log.Printf("failed to read column: %v", err) + return } fmt.Println(mascot) diff --git a/modules/grafana-lgtm/examples_test.go b/modules/grafana-lgtm/examples_test.go index c13f1bbd62..a3602b7685 100644 --- a/modules/grafana-lgtm/examples_test.go +++ b/modules/grafana-lgtm/examples_test.go @@ -4,10 +4,9 @@ import ( "context" "errors" "fmt" - golog "log" + "log" "log/slog" "math/rand" - "sync" "time" "go.opentelemetry.io/contrib/bridges/otelslog" @@ -22,11 +21,13 @@ import ( "go.opentelemetry.io/otel/log/global" metricsapi "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/sdk/log" + otellog "go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/trace" + "golang.org/x/sync/errgroup" - "github.com/testcontainers/testcontainers-go/modules/grafanalgtm" + "github.com/testcontainers/testcontainers-go" + grafanalgtm "github.com/testcontainers/testcontainers-go/modules/grafana-lgtm" ) func ExampleRun() { @@ -34,21 +35,21 @@ func ExampleRun() { ctx := context.Background() grafanaLgtmContainer, err := grafanalgtm.Run(ctx, "grafana/otel-lgtm:0.6.0") - if err != nil { - golog.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := grafanaLgtmContainer.Terminate(ctx); err != nil { - golog.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(grafanaLgtmContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := grafanaLgtmContainer.State(ctx) if err != nil { - golog.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -61,66 +62,72 @@ func ExampleRun_otelCollector() { ctx := context.Background() ctr, err := grafanalgtm.Run(ctx, "grafana/otel-lgtm:0.6.0", grafanalgtm.WithAdminCredentials("admin", "123456789")) - if err != nil { - golog.Fatalf("failed to start Grafana LGTM container: %s", err) - } defer func() { - if err := ctr.Terminate(ctx); err != nil { - golog.Fatalf("failed to terminate Grafana LGTM container: %s", err) + if err := testcontainers.TerminateContainer(ctr); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start Grafana LGTM container: %s", err) + return + } // Set up OpenTelemetry. otelShutdown, err := setupOTelSDK(ctx, ctr) if err != nil { + log.Printf("failed to set up OpenTelemetry: %s", err) return } // Handle shutdown properly so nothing leaks. defer func() { - err = errors.Join(err, otelShutdown(context.Background())) + if err := otelShutdown(context.Background()); err != nil { + log.Printf("failed to shutdown OpenTelemetry: %s", err) + } }() // roll dice 10000 times, concurrently max := 10_000 - wg := sync.WaitGroup{} + var wg errgroup.Group for i := 0; i < max; i++ { - wg.Add(1) - - go func() { - defer wg.Done() - rolldice(ctx) - }() + wg.Go(func() error { + return rolldice(ctx) + }) } - wg.Wait() + if err = wg.Wait(); err != nil { + log.Printf("failed to roll dice: %s", err) + return + } // Output: - // shutdown errors: } // setupOTelSDK bootstraps the OpenTelemetry pipeline. // If it does not return an error, make sure to call shutdown for proper cleanup. -func setupOTelSDK(ctx context.Context, ctr *grafanalgtm.GrafanaLGTMContainer) (shutdown func(context.Context) error, err error) { // nolint:nonamedreturns // this is a pattern in the OpenTelemetry Go SDK +func setupOTelSDK(ctx context.Context, ctr *grafanalgtm.GrafanaLGTMContainer) (shutdown func(context.Context) error, err error) { var shutdownFuncs []func(context.Context) error // shutdown calls cleanup functions registered via shutdownFuncs. // The errors from the calls are joined. // Each registered cleanup will be invoked once. shutdown = func(ctx context.Context) error { - var err error + var errs []error for _, fn := range shutdownFuncs { - err = errors.Join(err, fn(ctx)) + if err := fn(ctx); err != nil { + errs = append(errs, err) + } } - shutdownFuncs = nil - fmt.Println("shutdown errors:", err) - return err - } - // handleErr calls shutdown for cleanup and makes sure that all errors are returned. - handleErr := func(inErr error) { - err = errors.Join(inErr, shutdown(ctx)) + return errors.Join(errs...) } + // Ensure that the OpenTelemetry SDK is properly shutdown. + defer func() { + if err != nil { + err = errors.Join(shutdown(ctx)) + } + }() + prop := propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, @@ -139,13 +146,12 @@ func setupOTelSDK(ctx context.Context, ctr *grafanalgtm.GrafanaLGTMContainer) (s ), ) if err != nil { - return nil, err + return nil, fmt.Errorf("new trace exporter: %w", err) } tracerProvider := trace.NewTracerProvider(trace.WithBatcher(traceExporter)) if err != nil { - handleErr(err) - return + return nil, fmt.Errorf("new trace provider: %w", err) } shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) otel.SetTracerProvider(tracerProvider) @@ -155,7 +161,7 @@ func setupOTelSDK(ctx context.Context, ctr *grafanalgtm.GrafanaLGTMContainer) (s otlpmetrichttp.WithEndpoint(otlpHttpEndpoint), ) if err != nil { - return nil, err + return nil, fmt.Errorf("new metric exporter: %w", err) } // The exporter embeds a default OpenTelemetry Reader and @@ -163,7 +169,7 @@ func setupOTelSDK(ctx context.Context, ctr *grafanalgtm.GrafanaLGTMContainer) (s // both a Reader and Collector. prometheusExporter, err := prometheus.New() if err != nil { - return nil, err + return nil, fmt.Errorf("new prometheus exporter: %w", err) } meterProvider := metric.NewMeterProvider( @@ -171,9 +177,9 @@ func setupOTelSDK(ctx context.Context, ctr *grafanalgtm.GrafanaLGTMContainer) (s metric.WithReader(prometheusExporter), ) if err != nil { - handleErr(err) - return + return nil, fmt.Errorf("new meter provider: %w", err) } + shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) otel.SetMeterProvider(meterProvider) @@ -182,23 +188,22 @@ func setupOTelSDK(ctx context.Context, ctr *grafanalgtm.GrafanaLGTMContainer) (s otlploghttp.WithEndpoint(otlpHttpEndpoint), ) if err != nil { - return nil, err + return nil, fmt.Errorf("new log exporter: %w", err) } - loggerProvider := log.NewLoggerProvider(log.WithProcessor(log.NewBatchProcessor(logExporter))) + loggerProvider := otellog.NewLoggerProvider(otellog.WithProcessor(otellog.NewBatchProcessor(logExporter))) if err != nil { - handleErr(err) - return + return nil, fmt.Errorf("new logger provider: %w", err) } + shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown) global.SetLoggerProvider(loggerProvider) - err = runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second)) - if err != nil { - logger.ErrorContext(ctx, "otel runtime instrumentation failed:", err) // nolint:all // this is a pattern in the OpenTelemetry Go SDK + if err = runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second)); err != nil { + return nil, fmt.Errorf("start runtime instrumentation: %w", err) } - return + return shutdown, nil } // rollDiceApp { @@ -210,7 +215,7 @@ var ( meter = otel.Meter(schemaName) ) -func rolldice(ctx context.Context) { +func rolldice(ctx context.Context) error { ctx, span := tracer.Start(ctx, "roll") defer span.End() @@ -225,9 +230,11 @@ func rolldice(ctx context.Context) { // This is the equivalent of prometheus.NewCounterVec counter, err := meter.Int64Counter("rolldice-counter", metricsapi.WithDescription("a 20-sided dice")) if err != nil { - golog.Fatal(err) + return fmt.Errorf("roll dice: %w", err) } counter.Add(ctx, int64(roll), opt) + + return nil } // } diff --git a/modules/grafana-lgtm/go.mod b/modules/grafana-lgtm/go.mod index 73a94c714b..cab19dc60a 100644 --- a/modules/grafana-lgtm/go.mod +++ b/modules/grafana-lgtm/go.mod @@ -1,10 +1,11 @@ -module github.com/testcontainers/testcontainers-go/modules/grafanalgtm +module github.com/testcontainers/testcontainers-go/modules/grafana-lgtm go 1.22 require ( github.com/docker/go-connections v0.5.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 go.opentelemetry.io/contrib/bridges/otelslog v0.3.0 go.opentelemetry.io/contrib/instrumentation/runtime v0.53.0 go.opentelemetry.io/otel v1.28.0 @@ -18,6 +19,7 @@ require ( go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/sdk/log v0.4.0 go.opentelemetry.io/otel/sdk/metric v1.28.0 + golang.org/x/sync v0.8.0 ) require ( @@ -30,7 +32,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect @@ -54,6 +57,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -68,14 +72,15 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/grpc v1.64.1 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/grafana-lgtm/go.sum b/modules/grafana-lgtm/go.sum index 9eb14f31cb..d637aca1fa 100644 --- a/modules/grafana-lgtm/go.sum +++ b/modules/grafana-lgtm/go.sum @@ -18,8 +18,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -56,6 +56,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -92,6 +96,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -103,6 +109,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -152,8 +160,8 @@ go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -165,6 +173,8 @@ golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -175,14 +185,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -202,6 +212,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/grafana-lgtm/grafana.go b/modules/grafana-lgtm/grafana.go index 1e2f33adba..3cee949938 100644 --- a/modules/grafana-lgtm/grafana.go +++ b/modules/grafana-lgtm/grafana.go @@ -45,7 +45,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom container, err := testcontainers.GenericContainer(ctx, genericContainerReq) if err != nil { - return nil, err + return nil, fmt.Errorf("generic container: %w", err) } c := &GrafanaLGTMContainer{Container: container} @@ -53,7 +53,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom url, err := c.OtlpHttpEndpoint(ctx) if err != nil { // return the container instance to allow the caller to clean up - return c, err + return c, fmt.Errorf("otlp http endpoint: %w", err) } testcontainers.Logger.Printf("Access to the Grafana dashboard: %s", url) diff --git a/modules/grafana-lgtm/grafana_test.go b/modules/grafana-lgtm/grafana_test.go index b0a4960616..826451e579 100644 --- a/modules/grafana-lgtm/grafana_test.go +++ b/modules/grafana-lgtm/grafana_test.go @@ -8,31 +8,24 @@ import ( "net/url" "testing" - "github.com/testcontainers/testcontainers-go/modules/grafanalgtm" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" + grafanalgtm "github.com/testcontainers/testcontainers-go/modules/grafana-lgtm" ) func TestGrafanaLGTM(t *testing.T) { ctx := context.Background() grafanaLgtmContainer, err := grafanalgtm.Run(ctx, "grafana/otel-lgtm:0.6.0") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := grafanaLgtmContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, grafanaLgtmContainer) + require.NoError(t, err) // perform assertions t.Run("container is running with right version", func(t *testing.T) { healthURL, err := url.Parse(fmt.Sprintf("http://%s/api/health", grafanaLgtmContainer.MustHttpEndpoint(ctx))) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) httpReq := http.Request{ Method: http.MethodGet, @@ -42,23 +35,15 @@ func TestGrafanaLGTM(t *testing.T) { httpClient := http.Client{} httpResp, err := httpClient.Do(&httpReq) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + defer httpResp.Body.Close() - if httpResp.StatusCode != http.StatusOK { - t.Fatalf("expected status code %d, got %d", http.StatusOK, httpResp.StatusCode) - } + require.Equal(t, http.StatusOK, httpResp.StatusCode) body := make(map[string]interface{}) err = json.NewDecoder(httpResp.Body).Decode(&body) - if err != nil { - t.Fatal(err) - } - - if body["version"] != "11.0.0" { - t.Fatalf("expected version %q, got %q", "11.0.0", body["version"]) - } + require.NoError(t, err) + require.Equal(t, "11.0.0", body["version"]) }) } diff --git a/modules/inbucket/examples_test.go b/modules/inbucket/examples_test.go index 7680a9563a..c10f8b5b58 100644 --- a/modules/inbucket/examples_test.go +++ b/modules/inbucket/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/inbucket" ) @@ -13,21 +14,21 @@ func ExampleRun() { ctx := context.Background() inbucketContainer, err := inbucket.Run(ctx, "inbucket/inbucket:sha-2d409bb") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := inbucketContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(inbucketContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := inbucketContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/inbucket/go.mod b/modules/inbucket/go.mod index 18939bc445..65a656dad5 100644 --- a/modules/inbucket/go.mod +++ b/modules/inbucket/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/inbucket/inbucket v2.0.0+incompatible github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -16,7 +16,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -54,9 +54,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/inbucket/go.sum b/modules/inbucket/go.sum index 2e403c06ad..84749386ff 100644 --- a/modules/inbucket/go.sum +++ b/modules/inbucket/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -100,6 +100,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -133,8 +135,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -156,14 +158,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/inbucket/inbucket.go b/modules/inbucket/inbucket.go index beae784557..56695cb581 100644 --- a/modules/inbucket/inbucket.go +++ b/modules/inbucket/inbucket.go @@ -44,7 +44,7 @@ func (c *InbucketContainer) WebInterface(ctx context.Context) (string, error) { return "", err } - return fmt.Sprintf("http://%s", net.JoinHostPort(host, containerPort.Port())), nil + return "http://" + net.JoinHostPort(host, containerPort.Port()), nil } // Deprecated: use Run instead @@ -77,9 +77,14 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *InbucketContainer + if container != nil { + c = &InbucketContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &InbucketContainer{Container: container}, nil + return c, nil } diff --git a/modules/inbucket/inbucket_test.go b/modules/inbucket/inbucket_test.go index eb6dc3c493..1cb53cedd7 100644 --- a/modules/inbucket/inbucket_test.go +++ b/modules/inbucket/inbucket_test.go @@ -7,27 +7,24 @@ import ( "github.com/inbucket/inbucket/pkg/rest/client" "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" ) func TestInbucket(t *testing.T) { ctx := context.Background() - container, err := Run(ctx, "inbucket/inbucket:sha-2d409bb") + ctr, err := Run(ctx, "inbucket/inbucket:sha-2d409bb") + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - // Clean up the container after the test is complete - t.Cleanup(func() { - err := container.Terminate(ctx) - require.NoError(t, err) - }) - // smtpConnection { - smtpUrl, err := container.SmtpConnection(ctx) + smtpUrl, err := ctr.SmtpConnection(ctx) // } require.NoError(t, err) // webInterface { - webInterfaceUrl, err := container.WebInterface(ctx) + webInterfaceUrl, err := ctr.WebInterface(ctx) // } require.NoError(t, err) restClient, err := client.New(webInterfaceUrl) diff --git a/modules/influxdb/examples_test.go b/modules/influxdb/examples_test.go index 2d37117993..30c892537f 100644 --- a/modules/influxdb/examples_test.go +++ b/modules/influxdb/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/influxdb" ) @@ -18,21 +19,21 @@ func ExampleRun() { influxdb.WithUsername("root"), influxdb.WithPassword("password"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := influxdbContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(influxdbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := influxdbContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/influxdb/go.mod b/modules/influxdb/go.mod index f11c1de449..43041bf961 100644 --- a/modules/influxdb/go.mod +++ b/modules/influxdb/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -16,7 +16,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -53,9 +53,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/influxdb/go.sum b/modules/influxdb/go.sum index ad81df94f6..4007c33ccd 100644 --- a/modules/influxdb/go.sum +++ b/modules/influxdb/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -99,6 +99,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -132,8 +134,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -155,14 +157,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/influxdb/influxdb.go b/modules/influxdb/influxdb.go index d359c8cbbc..4c6024d79c 100644 --- a/modules/influxdb/influxdb.go +++ b/modules/influxdb/influxdb.go @@ -2,9 +2,10 @@ package influxdb import ( "context" + "encoding/json" "fmt" + "io" "path" - "strings" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -34,7 +35,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom "INFLUXDB_HTTP_HTTPS_ENABLED": "false", "INFLUXDB_HTTP_AUTH_ENABLED": "false", }, - WaitingFor: wait.ForListeningPort("8086/tcp"), + WaitingFor: waitForHttpHealth(), } genericContainerReq := testcontainers.GenericContainerRequest{ ContainerRequest: req, @@ -47,44 +48,17 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } } - hasInitDb := false - - for _, f := range genericContainerReq.Files { - if f.ContainerFilePath == "/" && strings.HasSuffix(f.HostFilePath, "docker-entrypoint-initdb.d") { - // Init service in container will start influxdb, run scripts in docker-entrypoint-initdb.d and then - // terminate the influxdb server, followed by restart of influxdb. This is tricky to wait for, and - // in this case, we are assuming that data was added by init script, so we then look for an - // "Open shard" which is the last thing that happens before the server is ready to accept connections. - // This is probably different for InfluxDB 2.x, but that is left as an exercise for the reader. - strategies := []wait.Strategy{ - genericContainerReq.WaitingFor, - wait.ForLog("influxdb init process in progress..."), - wait.ForLog("Server shutdown completed"), - wait.ForLog("Opened shard"), - } - genericContainerReq.WaitingFor = wait.ForAll(strategies...) - hasInitDb = true - break - } - } - - if !hasInitDb { - if lastIndex := strings.LastIndex(genericContainerReq.Image, ":"); lastIndex != -1 { - tag := genericContainerReq.Image[lastIndex+1:] - if tag == "latest" || tag[0] == '2' { - genericContainerReq.WaitingFor = wait.ForLog(`Listening log_id=[0-9a-zA-Z_~]+ service=tcp-listener transport=http`).AsRegexp() - } - } else { - genericContainerReq.WaitingFor = wait.ForLog("Listening for signals") - } + container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *InfluxDbContainer + if container != nil { + c = &InfluxDbContainer{Container: container} } - container, err := testcontainers.GenericContainer(ctx, genericContainerReq) if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &InfluxDbContainer{container}, nil + return c, nil } func (c *InfluxDbContainer) MustConnectionUrl(ctx context.Context) string { @@ -142,9 +116,8 @@ func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption { } } -// WithInitDb will copy a 'docker-entrypoint-initdb.d' directory to the container. -// The secPath is the path to the directory on the host machine. -// The directory will be copied to the root of the container. +// WithInitDb returns a request customizer that initialises the database using the file `docker-entrypoint-initdb.d` +// located in `srcPath` directory. func WithInitDb(srcPath string) testcontainers.CustomizeRequestOption { return func(req *testcontainers.GenericContainerRequest) error { cf := testcontainers.ContainerFile{ @@ -153,6 +126,25 @@ func WithInitDb(srcPath string) testcontainers.CustomizeRequestOption { FileMode: 0o755, } req.Files = append(req.Files, cf) + + req.WaitingFor = wait.ForAll( + wait.ForLog("Server shutdown completed"), + waitForHttpHealth(), + ) return nil } } + +func waitForHttpHealth() *wait.HTTPStrategy { + return wait.ForHTTP("/health"). + WithResponseMatcher(func(body io.Reader) bool { + decoder := json.NewDecoder(body) + r := struct { + Status string `json:"status"` + }{} + if err := decoder.Decode(&r); err != nil { + return false + } + return r.Status == "pass" + }) +} diff --git a/modules/influxdb/influxdb_test.go b/modules/influxdb/influxdb_test.go index 6ec5ec7399..3c4379c66c 100644 --- a/modules/influxdb/influxdb_test.go +++ b/modules/influxdb/influxdb_test.go @@ -15,25 +15,16 @@ import ( "github.com/testcontainers/testcontainers-go/modules/influxdb" ) -func containerCleanup(t *testing.T, container testcontainers.Container) { - err := container.Terminate(context.Background()) - require.NoError(t, err, "failed to terminate container") -} - func TestV1Container(t *testing.T) { ctx := context.Background() influxDbContainer, err := influxdb.Run(ctx, "influxdb:1.8.10") + testcontainers.CleanupContainer(t, influxDbContainer) require.NoError(t, err) - t.Cleanup(func() { - containerCleanup(t, influxDbContainer) - }) state, err := influxDbContainer.State(ctx) require.NoError(t, err) - if !state.Running { - t.Fatal("InfluxDB container is not running") - } + require.Truef(t, state.Running, "InfluxDB container is not running") } func TestV2Container(t *testing.T) { @@ -44,17 +35,13 @@ func TestV2Container(t *testing.T) { influxdb.WithUsername("root"), influxdb.WithPassword("password"), ) + testcontainers.CleanupContainer(t, influxDbContainer) require.NoError(t, err) - t.Cleanup(func() { - containerCleanup(t, influxDbContainer) - }) state, err := influxDbContainer.State(ctx) require.NoError(t, err) - if !state.Running { - t.Fatal("InfluxDB container is not running") - } + require.Truef(t, state.Running, "InfluxDB container is not running") } func TestWithInitDb(t *testing.T) { @@ -63,10 +50,8 @@ func TestWithInitDb(t *testing.T) { "influxdb:1.8.10", influxdb.WithInitDb("testdata"), ) + testcontainers.CleanupContainer(t, influxDbContainer) require.NoError(t, err) - t.Cleanup(func() { - containerCleanup(t, influxDbContainer) - }) if state, err := influxDbContainer.State(ctx); err != nil || !state.Running { require.NoError(t, err) @@ -83,9 +68,7 @@ func TestWithInitDb(t *testing.T) { response, err := cli.Query(q) require.NoError(t, err) - if response.Error() != nil { - t.Fatal(response.Error()) - } + require.NoError(t, response.Error()) testJson, err := json.Marshal(response.Results) require.NoError(t, err) @@ -99,10 +82,8 @@ func TestWithConfigFile(t *testing.T) { "influxdb:"+influxVersion, influxdb.WithConfigFile(filepath.Join("testdata", "influxdb.conf")), ) + testcontainers.CleanupContainer(t, influxDbContainer) require.NoError(t, err) - t.Cleanup(func() { - containerCleanup(t, influxDbContainer) - }) if state, err := influxDbContainer.State(context.Background()); err != nil || !state.Running { require.NoError(t, err) diff --git a/modules/k3s/go.mod b/modules/k3s/go.mod index da3a23fd92..5a382b7739 100644 --- a/modules/k3s/go.mod +++ b/modules/k3s/go.mod @@ -5,7 +5,8 @@ go 1.22 require ( github.com/docker/docker v27.1.1+incompatible github.com/docker/go-connections v0.5.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.2 k8s.io/apimachinery v0.29.2 @@ -20,7 +21,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -56,6 +57,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -68,12 +70,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect diff --git a/modules/k3s/go.sum b/modules/k3s/go.sum index e08a07dd42..4584dcf7ac 100644 --- a/modules/k3s/go.sum +++ b/modules/k3s/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -143,6 +143,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -180,8 +182,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -213,18 +215,18 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/k3s/k3s.go b/modules/k3s/k3s.go index f6cfb055c4..f223c08c2b 100644 --- a/modules/k3s/k3s.go +++ b/modules/k3s/k3s.go @@ -50,7 +50,7 @@ func WithManifest(manifestPath string) testcontainers.CustomizeRequestOption { // Deprecated: use Run instead // RunContainer creates an instance of the K3s container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*K3sContainer, error) { - return Run(ctx, "docker.io/rancher/k3s:v1.27.1-k3s1", opts...) + return Run(ctx, "rancher/k3s:v1.27.1-k3s1", opts...) } // Run creates an instance of the K3s container type @@ -98,11 +98,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *K3sContainer + if container != nil { + c = &K3sContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &K3sContainer{Container: container}, nil + return c, nil } func getContainerHost(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (string, error) { @@ -215,7 +220,7 @@ func (c *K3sContainer) LoadImages(ctx context.Context, images ...string) error { return fmt.Errorf("saving images %w", err) } - containerPath := fmt.Sprintf("/tmp/%s", filepath.Base(imagesTar.Name())) + containerPath := "/tmp/" + filepath.Base(imagesTar.Name()) err = c.Container.CopyFileToContainer(ctx, imagesTar.Name(), containerPath, 0x644) if err != nil { return fmt.Errorf("copying image to container %w", err) diff --git a/modules/k3s/k3s_example_test.go b/modules/k3s/k3s_example_test.go index eef8f87280..cd1c7afc7e 100644 --- a/modules/k3s/k3s_example_test.go +++ b/modules/k3s/k3s_example_test.go @@ -9,6 +9,7 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/k3s" ) @@ -16,44 +17,48 @@ func ExampleRun() { // runK3sContainer { ctx := context.Background() - k3sContainer, err := k3s.Run(ctx, "docker.io/rancher/k3s:v1.27.1-k3s1") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container + k3sContainer, err := k3s.Run(ctx, "rancher/k3s:v1.27.1-k3s1") defer func() { - if err := k3sContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(k3sContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := k3sContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) kubeConfigYaml, err := k3sContainer.GetKubeConfig(ctx) if err != nil { - log.Fatalf("failed to get kubeconfig: %s", err) + log.Printf("failed to get kubeconfig: %s", err) + return } restcfg, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml) if err != nil { - log.Fatalf("failed to create rest config: %s", err) + log.Printf("failed to create rest config: %s", err) + return } k8s, err := kubernetes.NewForConfig(restcfg) if err != nil { - log.Fatalf("failed to create k8s client: %s", err) + log.Printf("failed to create k8s client: %s", err) + return } nodes, err := k8s.CoreV1().Nodes().List(ctx, v1.ListOptions{}) if err != nil { - log.Fatalf("failed to list nodes: %s", err) + log.Printf("failed to list nodes: %s", err) + return } fmt.Println(len(nodes.Items)) diff --git a/modules/k3s/k3s_test.go b/modules/k3s/k3s_test.go index 7a5fe0d94b..d89121dde3 100644 --- a/modules/k3s/k3s_test.go +++ b/modules/k3s/k3s_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kwait "k8s.io/apimachinery/pkg/util/wait" @@ -22,56 +23,34 @@ func Test_LoadImages(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Minute)) defer cancel() - k3sContainer, err := k3s.Run(ctx, "docker.io/rancher/k3s:v1.27.1-k3s1") - if err != nil { - t.Fatal(err) - } - - // Clean up the container - defer func() { - if err := k3sContainer.Terminate(ctx); err != nil { - t.Fatal(err) - } - }() + k3sContainer, err := k3s.Run(ctx, "rancher/k3s:v1.27.1-k3s1") + testcontainers.CleanupContainer(t, k3sContainer) + require.NoError(t, err) kubeConfigYaml, err := k3sContainer.GetKubeConfig(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) restcfg, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) k8s, err := kubernetes.NewForConfig(restcfg) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) provider, err := testcontainers.ProviderDocker.GetProvider() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // ensure nginx image is available locally err = provider.PullImage(ctx, "nginx") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) t.Run("Test load image not available", func(t *testing.T) { err := k3sContainer.LoadImages(ctx, "fake.registry/fake:non-existing") - if err == nil { - t.Fatal("should had failed") - } + require.Error(t, err) }) t.Run("Test load image in cluster", func(t *testing.T) { err := k3sContainer.LoadImages(ctx, "nginx") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) pod := &corev1.Pod{ TypeMeta: metav1.TypeMeta{ @@ -93,9 +72,7 @@ func Test_LoadImages(t *testing.T) { } _, err = k8s.CoreV1().Pods("default").Create(ctx, pod, metav1.CreateOptions{}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) err = kwait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) { state, err := getTestPodState(ctx, k8s) @@ -107,17 +84,11 @@ func Test_LoadImages(t *testing.T) { } return state.Running != nil, nil }) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) state, err := getTestPodState(ctx, k8s) - if err != nil { - t.Fatal(err) - } - if state.Running == nil { - t.Fatalf("Unexpected status %v", state) - } + require.NoError(t, err) + require.NotNil(t, state.Running) }) } @@ -134,32 +105,18 @@ func getTestPodState(ctx context.Context, k8s *kubernetes.Clientset) (corev1.Con func Test_APIServerReady(t *testing.T) { ctx := context.Background() - k3sContainer, err := k3s.Run(ctx, "docker.io/rancher/k3s:v1.27.1-k3s1") - if err != nil { - t.Fatal(err) - } - - // Clean up the container - defer func() { - if err := k3sContainer.Terminate(ctx); err != nil { - t.Fatal(err) - } - }() + k3sContainer, err := k3s.Run(ctx, "rancher/k3s:v1.27.1-k3s1") + testcontainers.CleanupContainer(t, k3sContainer) + require.NoError(t, err) kubeConfigYaml, err := k3sContainer.GetKubeConfig(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) restcfg, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) k8s, err := kubernetes.NewForConfig(restcfg) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) pod := &corev1.Pod{ TypeMeta: metav1.TypeMeta{ @@ -180,27 +137,17 @@ func Test_APIServerReady(t *testing.T) { } _, err = k8s.CoreV1().Pods("default").Create(context.Background(), pod, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("failed to create pod %v", err) - } + require.NoError(t, err) } func Test_WithManifestOption(t *testing.T) { ctx := context.Background() k3sContainer, err := k3s.Run(ctx, - "docker.io/rancher/k3s:v1.27.1-k3s1", + "rancher/k3s:v1.27.1-k3s1", k3s.WithManifest("nginx-manifest.yaml"), testcontainers.WithWaitStrategy(wait.ForExec([]string{"kubectl", "wait", "pod", "nginx", "--for=condition=Ready"})), ) - if err != nil { - t.Fatal(err) - } - - // Clean up the container - defer func() { - if err := k3sContainer.Terminate(ctx); err != nil { - t.Fatal(err) - } - }() + testcontainers.CleanupContainer(t, k3sContainer) + require.NoError(t, err) } diff --git a/modules/k6/examples_test.go b/modules/k6/examples_test.go index 468d113450..c842814d4c 100644 --- a/modules/k6/examples_test.go +++ b/modules/k6/examples_test.go @@ -29,27 +29,29 @@ func ExampleRun() { Started: true, } httpbin, err := testcontainers.GenericContainer(ctx, gcr) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := httpbin.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(httpbin); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } // getHTTPBinIP { httpbinIP, err := httpbin.ContainerIP(ctx) if err != nil { - log.Fatalf("failed to get container IP: %s", err) // nolint:gocritic + log.Printf("failed to get container IP: %s", err) + return } // } absPath, err := filepath.Abs(filepath.Join("scripts", "httpbin.js")) if err != nil { - log.Fatalf("failed to get absolute path to test script: %s", err) + log.Printf("failed to get absolute path to test script: %s", err) + return } // runK6Container { @@ -61,21 +63,32 @@ func ExampleRun() { k6.WithTestScript(absPath), k6.SetEnvVar("HTTPBIN", httpbinIP), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := k6.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + cacheMount, err := k6.CacheMount(ctx) + if err != nil { + log.Printf("failed to determine cache mount: %s", err) + } + + var options []testcontainers.TerminateOption + if cacheMount != "" { + options = append(options, testcontainers.RemoveVolumes(cacheMount)) + } + + if err = testcontainers.TerminateContainer(k6, options...); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } //} // assert the result of the test state, err := k6.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.ExitCode) diff --git a/modules/k6/go.mod b/modules/k6/go.mod index 58f96d04aa..cd1600f0f7 100644 --- a/modules/k6/go.mod +++ b/modules/k6/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/docker/docker v27.1.1+incompatible - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -15,7 +16,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -26,6 +28,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -37,6 +40,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -48,11 +52,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/k6/go.sum b/modules/k6/go.sum index 85338720c8..447eec4038 100644 --- a/modules/k6/go.sum +++ b/modules/k6/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,6 +53,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -78,6 +83,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -89,6 +96,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -122,8 +131,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -145,14 +154,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -172,6 +181,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/k6/k6.go b/modules/k6/k6.go index 591cd48f0c..e44c354d2d 100644 --- a/modules/k6/k6.go +++ b/modules/k6/k6.go @@ -17,6 +17,9 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) +// cacheTarget is the path to the cache volume in the container. +const cacheTarget = "/cache" + // K6Container represents the K6 container type used in the module type K6Container struct { testcontainers.Container @@ -140,7 +143,7 @@ func WithCache() testcontainers.CustomizeRequestOption { cacheVol := os.Getenv("TC_K6_BUILD_CACHE") // if no volume is provided, create one and ensure add labels for garbage collection if cacheVol == "" { - cacheVol = fmt.Sprintf("k6-cache-%s", testcontainers.SessionID()) + cacheVol = "k6-cache-" + testcontainers.SessionID() volOptions = &mount.VolumeOptions{ Labels: testcontainers.GenericLabels(), } @@ -152,7 +155,7 @@ func WithCache() testcontainers.CustomizeRequestOption { Name: cacheVol, VolumeOptions: volOptions, }, - Target: "/cache", + Target: cacheTarget, } req.Mounts = append(req.Mounts, mount) @@ -186,9 +189,31 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *K6Container + if container != nil { + c = &K6Container{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) + } + + return c, nil +} + +// CacheMount returns the name of volume used as a cache or an empty string +// if no cache was found. +func (k *K6Container) CacheMount(ctx context.Context) (string, error) { + inspect, err := k.Inspect(ctx) + if err != nil { + return "", fmt.Errorf("inspect: %w", err) + } + + for _, m := range inspect.Mounts { + if m.Type == mount.TypeVolume && m.Destination == cacheTarget { + return m.Name, nil + } } - return &K6Container{Container: container}, nil + return "", nil } diff --git a/modules/k6/k6_test.go b/modules/k6/k6_test.go index ead72dcac4..bc40bd13a6 100644 --- a/modules/k6/k6_test.go +++ b/modules/k6/k6_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/k6" ) @@ -39,48 +41,52 @@ func TestK6(t *testing.T) { }, } + var cacheMount string + t.Cleanup(func() { + if cacheMount == "" { + return + } + + // Ensure the cache volume is removed as mounts that specify a volume + // source as defined by the name are not removed automatically. + provider, err := testcontainers.NewDockerProvider() + require.NoError(t, err) + defer provider.Close() + + require.NoError(t, provider.Client().VolumeRemove(context.Background(), cacheMount, true)) + }) + for _, tc := range testCases { - tc := tc t.Run(tc.title, func(t *testing.T) { ctx := context.Background() var options testcontainers.CustomizeRequestOption if !strings.HasPrefix(tc.script, "http") { absPath, err := filepath.Abs(filepath.Join("scripts", tc.script)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) options = k6.WithTestScript(absPath) } else { uri, err := url.Parse(tc.script) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) desc := k6.DownloadableFile{Uri: *uri, DownloadDir: t.TempDir()} options = k6.WithRemoteTestScript(desc) } - container, err := k6.Run(ctx, "szkiba/k6x:v0.3.1", k6.WithCache(), options) - if err != nil { - t.Fatal(err) + ctr, err := k6.Run(ctx, "szkiba/k6x:v0.3.1", k6.WithCache(), options) + if ctr != nil && cacheMount == "" { + // First container, determine the cache mount. + cacheMount, err = ctr.CacheMount(ctx) + require.NoError(t, err) } - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // assert the result of the test - state, err := container.State(ctx) - if err != nil { - t.Fatal(err) - } - if state.ExitCode != tc.expect { - t.Fatalf("expected %d got %d", tc.expect, state.ExitCode) - } + state, err := ctr.State(ctx) + require.NoError(t, err) + require.Equal(t, tc.expect, state.ExitCode) }) } } diff --git a/modules/kafka/consumer_test.go b/modules/kafka/consumer_test.go index d7305540f8..9df926e72d 100644 --- a/modules/kafka/consumer_test.go +++ b/modules/kafka/consumer_test.go @@ -16,6 +16,7 @@ type TestKafkaConsumer struct { } func NewTestKafkaConsumer(t *testing.T) (*TestKafkaConsumer, <-chan bool, <-chan bool, func()) { + t.Helper() kc := &TestKafkaConsumer{ t: t, ready: make(chan bool, 1), diff --git a/modules/kafka/examples_test.go b/modules/kafka/examples_test.go index 2b547970fc..c275924ecc 100644 --- a/modules/kafka/examples_test.go +++ b/modules/kafka/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/kafka" ) @@ -16,21 +17,21 @@ func ExampleRun() { "confluentinc/confluent-local:7.5.0", kafka.WithClusterID("test-cluster"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container after defer func() { - if err := kafkaContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(kafkaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := kafkaContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(kafkaContainer.ClusterID) diff --git a/modules/kafka/go.mod b/modules/kafka/go.mod index effe129a00..0416389461 100644 --- a/modules/kafka/go.mod +++ b/modules/kafka/go.mod @@ -6,7 +6,7 @@ require ( github.com/IBM/sarama v1.43.3 github.com/docker/go-connections v0.5.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 golang.org/x/mod v0.16.0 ) @@ -18,7 +18,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -68,9 +68,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/kafka/go.sum b/modules/kafka/go.sum index 3a543e3791..5be28a065d 100644 --- a/modules/kafka/go.sum +++ b/modules/kafka/go.sum @@ -16,8 +16,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -174,8 +174,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -190,8 +190,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -213,19 +213,19 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/kafka/kafka.go b/modules/kafka/kafka.go index 431333894a..69f47908d7 100644 --- a/modules/kafka/kafka.go +++ b/modules/kafka/kafka.go @@ -2,8 +2,10 @@ package kafka import ( "context" + "errors" "fmt" "math" + "strconv" "strings" "github.com/docker/go-connections/nat" @@ -65,7 +67,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom "KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS": "1", "KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR": "1", "KAFKA_TRANSACTION_STATE_LOG_MIN_ISR": "1", - "KAFKA_LOG_FLUSH_INTERVAL_MESSAGES": fmt.Sprintf("%d", math.MaxInt), + "KAFKA_LOG_FLUSH_INTERVAL_MESSAGES": strconv.Itoa(math.MaxInt), "KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS": "0", "KAFKA_NODE_ID": "1", "KAFKA_PROCESS_ROLES": "broker,controller", @@ -126,16 +128,19 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom return nil, err } - clusterID := genericContainerReq.Env["CLUSTER_ID"] - configureControllerQuorumVoters(&genericContainerReq) container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *KafkaContainer + if container != nil { + c = &KafkaContainer{Container: container, ClusterID: genericContainerReq.Env["CLUSTER_ID"]} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &KafkaContainer{Container: container, ClusterID: clusterID}, nil + return c, nil } func trimValidateListeners(listeners []KafkaListener) error { @@ -291,7 +296,7 @@ func configureControllerQuorumVoters(req *testcontainers.GenericContainerRequest // which is available since version 7.0.0. func validateKRaftVersion(fqName string) error { if fqName == "" { - return fmt.Errorf("image cannot be empty") + return errors.New("image cannot be empty") } image := fqName[:strings.LastIndex(fqName, ":")] @@ -306,7 +311,7 @@ func validateKRaftVersion(fqName string) error { // semver requires the version to start with a "v" if !strings.HasPrefix(version, "v") { - version = fmt.Sprintf("v%s", version) + version = "v" + version } if semver.Compare(version, "v7.4.0") < 0 { // version < v7.4.0 diff --git a/modules/kafka/kafka_helpers_test.go b/modules/kafka/kafka_helpers_test.go index cb9b2b5ac1..b43cc90c68 100644 --- a/modules/kafka/kafka_helpers_test.go +++ b/modules/kafka/kafka_helpers_test.go @@ -3,6 +3,8 @@ package kafka import ( "testing" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" ) @@ -55,9 +57,7 @@ func TestConfigureQuorumVoters(t *testing.T) { t.Run(test.name, func(t *testing.T) { configureControllerQuorumVoters(test.req) - if test.req.Env["KAFKA_CONTROLLER_QUORUM_VOTERS"] != test.expectedVoters { - t.Fatalf("expected KAFKA_CONTROLLER_QUORUM_VOTERS to be %s, got %s", test.expectedVoters, test.req.Env["KAFKA_CONTROLLER_QUORUM_VOTERS"]) - } + require.Equalf(t, test.expectedVoters, test.req.Env["KAFKA_CONTROLLER_QUORUM_VOTERS"], "expected KAFKA_CONTROLLER_QUORUM_VOTERS to be %s, got %s", test.expectedVoters, test.req.Env["KAFKA_CONTROLLER_QUORUM_VOTERS"]) }) } } @@ -99,12 +99,10 @@ func TestValidateKRaftVersion(t *testing.T) { t.Run(test.name, func(t *testing.T) { err := validateKRaftVersion(test.image) - if test.wantErr && err == nil { - t.Fatalf("expected error, got nil") - } - - if !test.wantErr && err != nil { - t.Fatalf("expected no error, got %s", err) + if test.wantErr { + require.Errorf(t, err, "expected error, got nil") + } else { + require.NoErrorf(t, err, "expected no error, got %s", err) } }) } diff --git a/modules/kafka/kafka_test.go b/modules/kafka/kafka_test.go index 58892d42c1..2e6c60eddd 100644 --- a/modules/kafka/kafka_test.go +++ b/modules/kafka/kafka_test.go @@ -22,37 +22,24 @@ func TestKafka_Basic(t *testing.T) { ctx := context.Background() kafkaContainer, err := kafka.Run(ctx, "confluentinc/confluent-local:7.5.0", kafka.WithClusterID("kraftCluster")) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := kafkaContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, kafkaContainer) + require.NoError(t, err) assertAdvertisedListeners(t, kafkaContainer) - if !strings.EqualFold(kafkaContainer.ClusterID, "kraftCluster") { - t.Fatalf("expected clusterID to be %s, got %s", "kraftCluster", kafkaContainer.ClusterID) - } + require.Truef(t, strings.EqualFold(kafkaContainer.ClusterID, "kraftCluster"), "expected clusterID to be %s, got %s", "kraftCluster", kafkaContainer.ClusterID) // getBrokers { brokers, err := kafkaContainer.Brokers(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) config := sarama.NewConfig() client, err := sarama.NewConsumerGroup(brokers, "groupName", config) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) consumer, ready, done, cancel := NewTestKafkaConsumer(t) + defer cancel() go func() { if err := client.Consume(context.Background(), []string{topic}, consumer); err != nil { cancel() @@ -68,37 +55,27 @@ func TestKafka_Basic(t *testing.T) { config.Producer.Return.Successes = true producer, err := sarama.NewSyncProducer(brokers, config) - if err != nil { - cancel() - t.Fatal(err) - } + require.NoError(t, err) - if _, _, err := producer.SendMessage(&sarama.ProducerMessage{ + _, _, err = producer.SendMessage(&sarama.ProducerMessage{ Topic: topic, Key: sarama.StringEncoder("key"), Value: sarama.StringEncoder("value"), - }); err != nil { - cancel() - t.Fatal(err) - } + }) + require.NoError(t, err) <-done - if !strings.EqualFold(string(consumer.message.Key), "key") { - t.Fatalf("expected key to be %s, got %s", "key", string(consumer.message.Key)) - } - if !strings.EqualFold(string(consumer.message.Value), "value") { - t.Fatalf("expected value to be %s, got %s", "value", string(consumer.message.Value)) - } + require.Truef(t, strings.EqualFold(string(consumer.message.Key), "key"), "expected key to be %s, got %s", "key", string(consumer.message.Key)) + require.Truef(t, strings.EqualFold(string(consumer.message.Value), "value"), "expected value to be %s, got %s", "value", string(consumer.message.Value)) } func TestKafka_invalidVersion(t *testing.T) { ctx := context.Background() - _, err := kafka.Run(ctx, "confluentinc/confluent-local:6.3.3", kafka.WithClusterID("kraftCluster")) - if err == nil { - t.Fatal(err) - } + ctr, err := kafka.Run(ctx, "confluentinc/confluent-local:6.3.3", kafka.WithClusterID("kraftCluster")) + testcontainers.CleanupContainer(t, ctr) + require.Error(t, err) } func TestKafka_networkConnectivity(t *testing.T) { @@ -445,27 +422,15 @@ func createTopics(brokers []string, topics []string) error { // assertAdvertisedListeners checks that the advertised listeners are set correctly: // - The BROKER:// protocol is using the hostname of the Kafka container func assertAdvertisedListeners(t *testing.T, container testcontainers.Container) { + t.Helper() inspect, err := container.Inspect(context.Background()) - if err != nil { - t.Fatal(err) - } - hostname := inspect.Config.Hostname + require.NoError(t, err) - code, r, err := container.Exec(context.Background(), []string{"cat", "/usr/sbin/testcontainers_start.sh"}) - if err != nil { - t.Fatal(err) - } + brokerURL := "BROKER://" + inspect.Config.Hostname + ":9092" - if code != 0 { - t.Fatalf("expected exit code to be 0, got %d", code) - } + ctx := context.Background() - bs, err := io.ReadAll(r) - if err != nil { - t.Fatal(err) - } + bs := testcontainers.RequireContainerExec(ctx, t, container, []string{"cat", "/usr/sbin/testcontainers_start.sh"}) - if !strings.Contains(string(bs), "BROKER://"+hostname+":9092") { - t.Fatalf("expected advertised listeners to contain %s, got %s", "BROKER://"+hostname+":9092", string(bs)) - } + require.Containsf(t, bs, brokerURL, "expected advertised listeners to contain %s, got %s", brokerURL, bs) } diff --git a/modules/localstack/examples_test.go b/modules/localstack/examples_test.go index 65a55c317b..d503ecee6f 100644 --- a/modules/localstack/examples_test.go +++ b/modules/localstack/examples_test.go @@ -24,21 +24,21 @@ func ExampleRun() { ctx := context.Background() localstackContainer, err := localstack.Run(ctx, "localstack/localstack:1.4.0") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := localstackContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(localstackContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := localstackContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -53,9 +53,16 @@ func ExampleRun_withNetwork() { newNetwork, err := network.New(ctx) if err != nil { - log.Fatalf("failed to create network: %s", err) + log.Printf("failed to create network: %s", err) + return } + defer func() { + if err := newNetwork.Remove(context.Background()); err != nil { + log.Printf("failed to remove network: %s", err) + } + }() + nwName := newNetwork.Name localstackContainer, err := localstack.Run( @@ -64,21 +71,21 @@ func ExampleRun_withNetwork() { testcontainers.WithEnv(map[string]string{"SERVICES": "s3,sqs"}), network.WithNetwork([]string{nwName}, newNetwork), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - // } - - // Clean up the container defer func() { - if err := localstackContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(localstackContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + // } networks, err := localstackContainer.Networks(ctx) if err != nil { - log.Fatalf("failed to get container networks: %s", err) // nolint:gocritic + log.Printf("failed to get container networks: %s", err) + return } fmt.Println(len(networks)) @@ -90,14 +97,20 @@ func ExampleRun_withNetwork() { func ExampleRun_legacyMode() { ctx := context.Background() - _, err := localstack.Run( + ctr, err := localstack.Run( ctx, "localstack/localstack:0.10.0", testcontainers.WithEnv(map[string]string{"SERVICES": "s3,sqs"}), testcontainers.WithWaitStrategy(wait.ForLog("Ready.").WithStartupTimeout(5*time.Minute).WithOccurrence(1)), ) + defer func() { + if err := testcontainers.TerminateContainer(ctr); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() if err == nil { - log.Fatalf("expected an error, got nil") + log.Printf("expected an error, got nil") + return } fmt.Println(err) @@ -123,7 +136,7 @@ func ExampleRun_usingLambdas() { lambdaName := "localstack-lambda-url-example" // withCustomContainerRequest { - container, err := localstack.Run(ctx, + ctr, err := localstack.Run(ctx, "localstack/localstack:2.3.0", testcontainers.WithEnv(map[string]string{ "SERVICES": "lambda", @@ -139,17 +152,17 @@ func ExampleRun_usingLambdas() { }, }, }), - // } ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } + // } defer func() { - err := container.Terminate(ctx) - if err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(ctr); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // the three commands below are doing the following: // 1. create a lambda function @@ -169,9 +182,10 @@ func ExampleRun_usingLambdas() { {"awslocal", "lambda", "wait", "function-active-v2", "--function-name", lambdaName}, } for _, cmd := range lambdaCommands { - _, _, err := container.Exec(ctx, cmd) + _, _, err := ctr.Exec(ctx, cmd) if err != nil { - log.Fatalf("failed to execute command %v: %s", cmd, err) // nolint:gocritic + log.Printf("failed to execute command %v: %s", cmd, err) + return } } @@ -179,15 +193,17 @@ func ExampleRun_usingLambdas() { cmd := []string{ "awslocal", "lambda", "list-function-url-configs", "--function-name", lambdaName, } - _, reader, err := container.Exec(ctx, cmd, exec.Multiplexed()) + _, reader, err := ctr.Exec(ctx, cmd, exec.Multiplexed()) if err != nil { - log.Fatalf("failed to execute command %v: %s", cmd, err) + log.Printf("failed to execute command %v: %s", cmd, err) + return } buf := new(bytes.Buffer) _, err = buf.ReadFrom(reader) if err != nil { - log.Fatalf("failed to read from reader: %s", err) + log.Printf("failed to read from reader: %s", err) + return } content := buf.Bytes() @@ -205,7 +221,8 @@ func ExampleRun_usingLambdas() { v := &FunctionURLConfig{} err = json.Unmarshal(content, v) if err != nil { - log.Fatalf("failed to unmarshal content: %s", err) + log.Printf("failed to unmarshal content: %s", err) + return } httpClient := http.Client{ @@ -215,21 +232,24 @@ func ExampleRun_usingLambdas() { functionURL := v.FunctionURLConfigs[0].FunctionURL // replace the port with the one exposed by the container - mappedPort, err := container.MappedPort(ctx, "4566/tcp") + mappedPort, err := ctr.MappedPort(ctx, "4566/tcp") if err != nil { - log.Fatalf("failed to get mapped port: %s", err) + log.Printf("failed to get mapped port: %s", err) + return } functionURL = strings.ReplaceAll(functionURL, "4566", mappedPort.Port()) resp, err := httpClient.Post(functionURL, "application/json", bytes.NewBufferString(`{"num1": "10", "num2": "10"}`)) if err != nil { - log.Fatalf("failed to send request to lambda function: %s", err) + log.Printf("failed to send request to lambda function: %s", err) + return } jsonResponse, err := io.ReadAll(resp.Body) if err != nil { - log.Fatalf("failed to read response body: %s", err) + log.Printf("failed to read response body: %s", err) + return } fmt.Println(string(jsonResponse)) diff --git a/modules/localstack/go.mod b/modules/localstack/go.mod index 0bd92d1a01..a96a35a1ba 100644 --- a/modules/localstack/go.mod +++ b/modules/localstack/go.mod @@ -8,10 +8,11 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.27.5 github.com/aws/aws-sdk-go-v2/credentials v1.17.5 github.com/aws/aws-sdk-go-v2/service/s3 v1.51.2 + github.com/aws/smithy-go v1.21.0 github.com/docker/docker v27.1.1+incompatible github.com/docker/go-connections v0.5.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 golang.org/x/mod v0.16.0 ) @@ -32,12 +33,11 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.28.2 // indirect - github.com/aws/smithy-go v1.20.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -73,9 +73,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/localstack/go.sum b/modules/localstack/go.sum index 532bf9e05b..cd93facea9 100644 --- a/modules/localstack/go.sum +++ b/modules/localstack/go.sum @@ -42,8 +42,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 h1:9/GylMS45hGGFCcMrUZDVayQ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1/go.mod h1:YjAPFn4kGFqKC54VsHs5fn5B6d+PCY2tziEa3U/GB5Y= github.com/aws/aws-sdk-go-v2/service/sts v1.28.2 h1:0YjXuWdYHvsm0HnT4vO8XpwG1D+i2roxSCBoN6deJ7M= github.com/aws/aws-sdk-go-v2/service/sts v1.28.2/go.mod h1:jI+FWmYkSMn+4APWmZiZTgt0oM0TrvymD51FMqCnWgA= -github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= -github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= +github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= @@ -52,8 +52,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -141,6 +141,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -176,8 +178,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= @@ -202,14 +204,14 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/localstack/localstack.go b/modules/localstack/localstack.go index 961527cd3e..9754adba71 100644 --- a/modules/localstack/localstack.go +++ b/modules/localstack/localstack.go @@ -29,7 +29,7 @@ func isLegacyMode(image string) bool { } if !strings.HasPrefix(version, "v") { - version = fmt.Sprintf("v%s", version) + version = "v" + version } if semver.IsValid(version) { @@ -48,7 +48,7 @@ func isVersion2(image string) bool { } if !strings.HasPrefix(version, "v") { - version = fmt.Sprintf("v%s", version) + version = "v" + version } if semver.IsValid(version) { @@ -82,7 +82,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom ExposedPorts: []string{fmt.Sprintf("%d/tcp", defaultPort)}, Env: map[string]string{}, HostConfigModifier: func(hostConfig *container.HostConfig) { - hostConfig.Binds = []string{fmt.Sprintf("%s:/var/run/docker.sock", dockerHost)} + hostConfig.Binds = []string{dockerHost + ":/var/run/docker.sock"} }, } @@ -116,13 +116,15 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom localStackReq.GenericContainerRequest.Logger.Printf("Setting %s to %s (%s)\n", envVar, req.Env[envVar], hostnameExternalReason) container, err := testcontainers.GenericContainer(ctx, localStackReq.GenericContainerRequest) - if err != nil { - return nil, err + var c *LocalStackContainer + if container != nil { + c = &LocalStackContainer{Container: container} } - c := &LocalStackContainer{ - Container: container, + if err != nil { + return c, fmt.Errorf("generic container: %w", err) } + return c, nil } diff --git a/modules/localstack/localstack_test.go b/modules/localstack/localstack_test.go index 70797fe3cd..e9ad8c8330 100644 --- a/modules/localstack/localstack_test.go +++ b/modules/localstack/localstack_test.go @@ -2,13 +2,11 @@ package localstack import ( "context" - "fmt" "io" "strings" "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" @@ -43,7 +41,7 @@ func TestConfigureDockerHost(t *testing.T) { reason, err := configureDockerHost(req, tt.envVar) require.NoError(t, err) - assert.Equal(t, "explicitly as environment variable", reason) + require.Equal(t, "explicitly as environment variable", reason) }) t.Run("HOSTNAME_EXTERNAL matches the last network alias on a container with non-default network", func(t *testing.T) { @@ -58,8 +56,8 @@ func TestConfigureDockerHost(t *testing.T) { reason, err := configureDockerHost(req, tt.envVar) require.NoError(t, err) - assert.Equal(t, "to match last network alias on container with non-default network", reason) - assert.Equal(t, "foo3", req.Env[tt.envVar]) + require.Equal(t, "to match last network alias on container with non-default network", reason) + require.Equal(t, "foo3", req.Env[tt.envVar]) }) t.Run("HOSTNAME_EXTERNAL matches the daemon host because there are no aliases", func(t *testing.T) { @@ -78,8 +76,8 @@ func TestConfigureDockerHost(t *testing.T) { reason, err := configureDockerHost(req, tt.envVar) require.NoError(t, err) - assert.Equal(t, "to match host-routable address for container", reason) - assert.Equal(t, expectedDaemonHost, req.Env[tt.envVar]) + require.Equal(t, "to match host-routable address for container", reason) + require.Equal(t, expectedDaemonHost, req.Env[tt.envVar]) }) } } @@ -101,8 +99,8 @@ func TestIsLegacyMode(t *testing.T) { for _, tt := range tests { t.Run(tt.version, func(t *testing.T) { - got := isLegacyMode(fmt.Sprintf("localstack/localstack:%s", tt.version)) - assert.Equal(t, tt.want, got, "runInLegacyMode() = %v, want %v", got, tt.want) + got := isLegacyMode("localstack/localstack:" + tt.version) + require.Equal(t, tt.want, got, "runInLegacyMode() = %v, want %v", got, tt.want) }) } } @@ -118,16 +116,17 @@ func TestRunContainer(t *testing.T) { for _, tt := range tests { ctx := context.Background() - container, err := Run( + ctr, err := Run( ctx, - fmt.Sprintf("localstack/localstack:%s", tt.version), + "localstack/localstack:"+tt.version, ) + testcontainers.CleanupContainer(t, ctr) t.Run("Localstack:"+tt.version+" - multiple services exposed on same port", func(t *testing.T) { require.NoError(t, err) - assert.NotNil(t, container) + require.NotNil(t, ctr) - inspect, err := container.Inspect(ctx) + inspect, err := ctr.Inspect(ctx) require.NoError(t, err) rawPorts := inspect.NetworkSettings.Ports @@ -140,7 +139,7 @@ func TestRunContainer(t *testing.T) { } } - assert.Equal(t, 1, ports) // a single port is exposed + require.Equal(t, 1, ports) // a single port is exposed }) } } @@ -148,9 +147,10 @@ func TestRunContainer(t *testing.T) { func TestStartWithoutOverride(t *testing.T) { ctx := context.Background() - container, err := Run(ctx, "localstack/localstack:2.0.0") + ctr, err := Run(ctx, "localstack/localstack:2.0.0") + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - assert.NotNil(t, container) + require.NotNil(t, ctr) } func TestStartV2WithNetwork(t *testing.T) { @@ -158,6 +158,7 @@ func TestStartV2WithNetwork(t *testing.T) { nw, err := network.New(ctx) require.NoError(t, err) + testcontainers.CleanupNetwork(t, nw) localstack, err := Run( ctx, @@ -165,8 +166,9 @@ func TestStartV2WithNetwork(t *testing.T) { network.WithNetwork([]string{"localstack"}, nw), testcontainers.WithEnv(map[string]string{"SERVICES": "s3,sqs"}), ) + testcontainers.CleanupContainer(t, localstack) require.NoError(t, err) - assert.NotNil(t, localstack) + require.NotNil(t, localstack) networkName := nw.Name @@ -197,6 +199,7 @@ func TestStartV2WithNetwork(t *testing.T) { }, Started: true, }) + testcontainers.CleanupContainer(t, cli) require.NoError(t, err) - assert.NotNil(t, cli) + require.NotNil(t, cli) } diff --git a/modules/localstack/v1/s3_test.go b/modules/localstack/v1/s3_test.go index be643228f6..a35bbe98b2 100644 --- a/modules/localstack/v1/s3_test.go +++ b/modules/localstack/v1/s3_test.go @@ -62,10 +62,11 @@ func awsSession(ctx context.Context, l *localstack.LocalStackContainer) (*sessio func TestS3(t *testing.T) { ctx := context.Background() - container, err := localstack.Run(ctx, "localstack/localstack:1.4.0") + ctr, err := localstack.Run(ctx, "localstack/localstack:1.4.0") + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - session, err := awsSession(ctx, container) + session, err := awsSession(ctx, ctr) require.NoError(t, err) s3Uploader := s3manager.NewUploader(session) @@ -81,7 +82,7 @@ func TestS3(t *testing.T) { Bucket: aws.String(bucketName), }) require.NoError(t, err) - assert.NotNil(t, outputBucket) + require.NotNil(t, outputBucket) // put object s3Key1 := "key1" @@ -95,15 +96,15 @@ func TestS3(t *testing.T) { ContentDisposition: aws.String("attachment"), }) require.NoError(t, err) - assert.NotNil(t, outputObject) + require.NotNil(t, outputObject) t.Run("List Buckets", func(t *testing.T) { output, err := s3API.ListBuckets(nil) require.NoError(t, err) - assert.NotNil(t, output) + require.NotNil(t, output) buckets := output.Buckets - assert.Len(t, buckets, 1) + require.Len(t, buckets, 1) assert.Equal(t, bucketName, *buckets[0].Name) }) @@ -112,11 +113,11 @@ func TestS3(t *testing.T) { Bucket: aws.String(bucketName), }) require.NoError(t, err) - assert.NotNil(t, output) + require.NotNil(t, output) objects := output.Contents - assert.Len(t, objects, 1) + require.Len(t, objects, 1) assert.Equal(t, s3Key1, *objects[0].Key) assert.Equal(t, int64(len(body1)), *objects[0].Size) }) diff --git a/modules/localstack/v2/s3_test.go b/modules/localstack/v2/s3_test.go index 2b5308ddd8..2df71dcb39 100644 --- a/modules/localstack/v2/s3_test.go +++ b/modules/localstack/v2/s3_test.go @@ -3,13 +3,13 @@ package v2_test import ( "bytes" "context" - "fmt" "testing" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" + smithyendpoints "github.com/aws/smithy-go/endpoints" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,6 +25,20 @@ const ( region = "us-east-1" ) +// awsResolverV2 { +type resolverV2 struct { + // you could inject additional application context here as well +} + +func (*resolverV2) ResolveEndpoint(ctx context.Context, params s3.EndpointParameters) ( + smithyendpoints.Endpoint, error, +) { + // delegate back to the default v2 resolver otherwise + return s3.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params) +} + +// } + // awsSDKClientV2 { func s3Client(ctx context.Context, l *localstack.LocalStackContainer) (*s3.Client, error) { mappedPort, err := l.MappedPort(ctx, nat.Port("4566/tcp")) @@ -43,25 +57,18 @@ func s3Client(ctx context.Context, l *localstack.LocalStackContainer) (*s3.Clien return nil, err } - customResolver := aws.EndpointResolverWithOptionsFunc( - func(service, region string, opts ...interface{}) (aws.Endpoint, error) { - return aws.Endpoint{ - PartitionID: "aws", - URL: fmt.Sprintf("http://%s:%d", host, mappedPort.Int()), - SigningRegion: region, - }, nil - }) - awsCfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region), - config.WithEndpointResolverWithOptions(customResolver), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accesskey, secretkey, token)), ) if err != nil { return nil, err } + // reference: https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/endpoints/#with-both client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String("http://" + host + ":" + mappedPort.Port()) + o.EndpointResolverV2 = &resolverV2{} o.UsePathStyle = true }) @@ -73,10 +80,11 @@ func s3Client(ctx context.Context, l *localstack.LocalStackContainer) (*s3.Clien func TestS3(t *testing.T) { ctx := context.Background() - container, err := localstack.Run(ctx, "localstack/localstack:1.4.0") + ctr, err := localstack.Run(ctx, "localstack/localstack:1.4.0") + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - s3Client, err := s3Client(ctx, container) + s3Client, err := s3Client(ctx, ctr) require.NoError(t, err) t.Run("S3 operations", func(t *testing.T) { @@ -87,7 +95,7 @@ func TestS3(t *testing.T) { Bucket: aws.String(bucketName), }) require.NoError(t, err) - assert.NotNil(t, outputBucket) + require.NotNil(t, outputBucket) // put object s3Key1 := "key1" @@ -101,15 +109,15 @@ func TestS3(t *testing.T) { ContentDisposition: aws.String("attachment"), }) require.NoError(t, err) - assert.NotNil(t, outputObject) + require.NotNil(t, outputObject) t.Run("List Buckets", func(t *testing.T) { output, err := s3Client.ListBuckets(ctx, &s3.ListBucketsInput{}) require.NoError(t, err) - assert.NotNil(t, output) + require.NotNil(t, output) buckets := output.Buckets - assert.Len(t, buckets, 1) + require.Len(t, buckets, 1) assert.Equal(t, bucketName, *buckets[0].Name) }) @@ -118,11 +126,11 @@ func TestS3(t *testing.T) { Bucket: aws.String(bucketName), }) require.NoError(t, err) - assert.NotNil(t, output) + require.NotNil(t, output) objects := output.Contents - assert.Len(t, objects, 1) + require.Len(t, objects, 1) assert.Equal(t, s3Key1, *objects[0].Key) assert.Equal(t, aws.Int64(int64(len(body1))), objects[0].Size) }) diff --git a/modules/mariadb/examples_test.go b/modules/mariadb/examples_test.go index d33970df14..59e168d3e4 100644 --- a/modules/mariadb/examples_test.go +++ b/modules/mariadb/examples_test.go @@ -6,6 +6,7 @@ import ( "log" "path/filepath" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mariadb" ) @@ -21,21 +22,21 @@ func ExampleRun() { mariadb.WithUsername("root"), mariadb.WithPassword(""), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := mariadbContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(mariadbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := mariadbContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/mariadb/go.mod b/modules/mariadb/go.mod index e8039caf58..319e67d627 100644 --- a/modules/mariadb/go.mod +++ b/modules/mariadb/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/go-sql-driver/mysql v1.7.1 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -15,7 +16,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -27,6 +29,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -38,6 +41,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -49,11 +53,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/mariadb/go.sum b/modules/mariadb/go.sum index 7784d0b833..96aef09c5a 100644 --- a/modules/mariadb/go.sum +++ b/modules/mariadb/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -54,6 +55,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -80,6 +85,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -91,6 +98,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -124,8 +133,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -147,14 +156,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -174,6 +183,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/mariadb/mariadb.go b/modules/mariadb/mariadb.go index fae71c7871..4036cacc76 100644 --- a/modules/mariadb/mariadb.go +++ b/modules/mariadb/mariadb.go @@ -2,6 +2,7 @@ package mariadb import ( "context" + "errors" "fmt" "path/filepath" "strings" @@ -165,17 +166,25 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom password := req.Env["MARIADB_PASSWORD"] if len(password) == 0 && password == "" && !strings.EqualFold(rootUser, username) { - return nil, fmt.Errorf("empty password can be used only with the root user") + return nil, errors.New("empty password can be used only with the root user") } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) - if err != nil { - return nil, err + var c *MariaDBContainer + if container != nil { + c = &MariaDBContainer{ + Container: container, + username: username, + password: password, + database: req.Env["MARIADB_DATABASE"], + } } - database := req.Env["MARIADB_DATABASE"] + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } - return &MariaDBContainer{container, username, password, database}, nil + return c, nil } // MustConnectionString panics if the address cannot be determined. diff --git a/modules/mariadb/mariadb_test.go b/modules/mariadb/mariadb_test.go index f1863f472c..706ee2eb76 100644 --- a/modules/mariadb/mariadb_test.go +++ b/modules/mariadb/mariadb_test.go @@ -8,55 +8,41 @@ import ( // Import mysql into the scope of this package (required) _ "github.com/go-sql-driver/mysql" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mariadb" ) func TestMariaDB(t *testing.T) { ctx := context.Background() - container, err := mariadb.Run(ctx, "mariadb:11.0.3") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := mariadb.Run(ctx, "mariadb:11.0.3") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // connectionString { // By default, MariaDB transmits data between the server and clients without encrypting it. - connectionString, err := container.ConnectionString(ctx, "tls=false") + connectionString, err := ctr.ConnectionString(ctx, "tls=false") // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - mustConnectionString := container.MustConnectionString(ctx, "tls=false") - if mustConnectionString != connectionString { - t.Errorf("ConnectionString was not equal to MustConnectionString") - } + mustConnectionString := ctr.MustConnectionString(ctx, "tls=false") + require.Equal(t, connectionString, mustConnectionString) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) + _, err = db.Exec("CREATE TABLE IF NOT EXISTS a_table ( \n" + " `col_1` VARCHAR(128) NOT NULL, \n" + " `col_2` VARCHAR(128) NOT NULL, \n" + " PRIMARY KEY (`col_1`, `col_2`) \n" + ")") - if err != nil { - t.Errorf("error creating table: %+v\n", err) - } + require.NoError(t, err) } func TestMariaDBWithNonRootUserAndEmptyPassword(t *testing.T) { @@ -67,171 +53,112 @@ func TestMariaDBWithNonRootUserAndEmptyPassword(t *testing.T) { mariadb.WithDatabase("foo"), mariadb.WithUsername("test"), mariadb.WithPassword("")) - if err.Error() != "empty password can be used only with the root user" { - t.Fatal(err) - } + require.EqualError(t, err, "empty password can be used only with the root user") } func TestMariaDBWithRootUserAndEmptyPassword(t *testing.T) { ctx := context.Background() - container, err := mariadb.Run(ctx, + ctr, err := mariadb.Run(ctx, "mariadb:11.0.3", mariadb.WithDatabase("foo"), mariadb.WithUsername("root"), mariadb.WithPassword("")) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - connectionString, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + connectionString, err := ctr.ConnectionString(ctx) + require.NoError(t, err) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) + _, err = db.Exec("CREATE TABLE IF NOT EXISTS a_table ( \n" + " `col_1` VARCHAR(128) NOT NULL, \n" + " `col_2` VARCHAR(128) NOT NULL, \n" + " PRIMARY KEY (`col_1`, `col_2`) \n" + ")") - if err != nil { - t.Errorf("error creating table: %+v\n", err) - } + require.NoError(t, err) } func TestMariaDBWithMySQLEnvVars(t *testing.T) { ctx := context.Background() - container, err := mariadb.Run(ctx, "mariadb:10.3.29", + ctr, err := mariadb.Run(ctx, "mariadb:10.3.29", mariadb.WithScripts(filepath.Join("testdata", "schema.sql"))) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - assertDataCanBeFetched(t, ctx, container) + assertDataCanBeFetched(t, ctx, ctr) } func TestMariaDBWithConfigFile(t *testing.T) { ctx := context.Background() - container, err := mariadb.Run(ctx, "mariadb:11.0.3", + ctr, err := mariadb.Run(ctx, "mariadb:11.0.3", mariadb.WithConfigFile(filepath.Join("testdata", "my.cnf"))) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - - connectionString, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + connectionString, err := ctr.ConnectionString(ctx) + require.NoError(t, err) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) // In MariaDB 10.2.2 and later, the default file format is Barracuda and Antelope is deprecated. // Barracuda is a newer InnoDB file format. It supports the COMPACT, REDUNDANT, DYNAMIC and // COMPRESSED row formats. Tables with large BLOB or TEXT columns in particular could benefit // from the dynamic row format. stmt, err := db.Prepare("SELECT @@GLOBAL.innodb_default_row_format") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + defer stmt.Close() row := stmt.QueryRow() innodbFileFormat := "" err = row.Scan(&innodbFileFormat) - if err != nil { - t.Errorf("error fetching innodb_default_row_format value") - } - if innodbFileFormat != "dynamic" { - t.Fatal("The InnoDB file format has been set by the ini file content") - } + require.NoError(t, err) + require.Equal(t, "dynamic", innodbFileFormat) } func TestMariaDBWithScripts(t *testing.T) { ctx := context.Background() - container, err := mariadb.Run(ctx, + ctr, err := mariadb.Run(ctx, "mariadb:11.0.3", mariadb.WithScripts(filepath.Join("testdata", "schema.sql"))) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - - assertDataCanBeFetched(t, ctx, container) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + assertDataCanBeFetched(t, ctx, ctr) } func assertDataCanBeFetched(t *testing.T, ctx context.Context, container *mariadb.MariaDBContainer) { + t.Helper() connectionString, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } - + require.NoError(t, err) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) stmt, err := db.Prepare("SELECT name from profile") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer stmt.Close() + row := stmt.QueryRow() var name string err = row.Scan(&name) - if err != nil { - t.Errorf("error fetching data") - } - if name != "profile 1" { - t.Fatal("The expected record was not found in the database.") - } + require.NoError(t, err) + require.Equal(t, "profile 1", name) } diff --git a/modules/meilisearch/Makefile b/modules/meilisearch/Makefile new file mode 100644 index 0000000000..d1554572d4 --- /dev/null +++ b/modules/meilisearch/Makefile @@ -0,0 +1,5 @@ +include ../../commons-test.mk + +.PHONY: test +test: + $(MAKE) test-meilisearch diff --git a/modules/meilisearch/examples_test.go b/modules/meilisearch/examples_test.go new file mode 100644 index 0000000000..5d41f23f6a --- /dev/null +++ b/modules/meilisearch/examples_test.go @@ -0,0 +1,45 @@ +package meilisearch_test + +import ( + "context" + "fmt" + "log" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/meilisearch" +) + +func ExampleRun() { + // runMeilisearchContainer { + ctx := context.Background() + + meiliContainer, err := meilisearch.Run( + ctx, + "getmeili/meilisearch:v1.10.3", + meilisearch.WithMasterKey("my-master-key"), + meilisearch.WithDumpImport("testdata/movies.dump"), + ) + defer func() { + if err := testcontainers.TerminateContainer(meiliContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + // } + + state, err := meiliContainer.State(ctx) + if err != nil { + log.Printf("failed to get container state: %s", err) + return + } + + fmt.Println(state.Running) + fmt.Printf("%s\n", meiliContainer.MasterKey()) + + // Output: + // true + // my-master-key +} diff --git a/modules/meilisearch/go.mod b/modules/meilisearch/go.mod new file mode 100644 index 0000000000..2ce7f8b066 --- /dev/null +++ b/modules/meilisearch/go.mod @@ -0,0 +1,63 @@ +module github.com/testcontainers/testcontainers-go/modules/meilisearch + +go 1.22 + +require ( + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.3.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/sdk v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/time v0.7.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/meilisearch/go.sum b/modules/meilisearch/go.sum new file mode 100644 index 0000000000..58b56bee63 --- /dev/null +++ b/modules/meilisearch/go.sum @@ -0,0 +1,189 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/modules/meilisearch/meilisearch.go b/modules/meilisearch/meilisearch.go new file mode 100644 index 0000000000..687a1d61ca --- /dev/null +++ b/modules/meilisearch/meilisearch.go @@ -0,0 +1,118 @@ +package meilisearch + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + defaultMasterKey = "just-a-master-key-for-test" + defaultHTTPPort = "7700/tcp" + masterKeyEnvVar = "MEILI_MASTER_KEY" +) + +// MeilisearchContainer represents the Meilisearch container type used in the module +type MeilisearchContainer struct { + testcontainers.Container + masterKey string +} + +// MasterKey retrieves the master key of the Meilisearch container +func (c *MeilisearchContainer) MasterKey() string { + return c.masterKey +} + +// Run creates an instance of the Meilisearch container type +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*MeilisearchContainer, error) { + req := testcontainers.ContainerRequest{ + Image: img, + ExposedPorts: []string{defaultHTTPPort}, + Env: map[string]string{ + masterKeyEnvVar: defaultMasterKey, + }, + } + + genericContainerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + // Gather all config options (defaults and then apply provided options) + settings := defaultOptions() + for _, opt := range opts { + if apply, ok := opt.(Option); ok { + apply(settings) + } + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, fmt.Errorf("customize: %w", err) + } + } + + if settings.DumpDataFilePath != "" { + genericContainerReq.Files = []testcontainers.ContainerFile{ + { + HostFilePath: settings.DumpDataFilePath, + ContainerFilePath: "/dumps/" + settings.DumpDataFileName, + FileMode: 0o755, + }, + } + genericContainerReq.Cmd = []string{"meilisearch", "--import-dump", "/dumps/" + settings.DumpDataFileName} + } + + // the wait strategy does not support TLS at the moment, + // so we need to disable it in the strategy for now. + genericContainerReq.WaitingFor = wait.ForHTTP("/health"). + WithPort(defaultHTTPPort). + WithTLS(false). + WithStartupTimeout(120 * time.Second). + WithStatusCodeMatcher(func(status int) bool { + return status == http.StatusOK + }). + WithResponseMatcher(func(body io.Reader) bool { + decoder := json.NewDecoder(body) + r := struct { + Status string `json:"status"` + }{} + if err := decoder.Decode(&r); err != nil { + return false + } + + return r.Status == "available" + }) + + container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *MeilisearchContainer + if container != nil { + c = &MeilisearchContainer{Container: container, masterKey: req.Env[masterKeyEnvVar]} + } + + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + return c, nil +} + +// Address retrieves the address of the Meilisearch container. +// It will use http as protocol, as TLS is not supported at the moment. +func (c *MeilisearchContainer) Address(ctx context.Context) (string, error) { + containerPort, err := c.MappedPort(ctx, defaultHTTPPort) + if err != nil { + return "", fmt.Errorf("mapped port: %w", err) + } + + host, err := c.Host(ctx) + if err != nil { + return "", fmt.Errorf("host: %w", err) + } + + return "http://" + net.JoinHostPort(host, containerPort.Port()), nil +} diff --git a/modules/meilisearch/meilisearch_test.go b/modules/meilisearch/meilisearch_test.go new file mode 100644 index 0000000000..5c351419e9 --- /dev/null +++ b/modules/meilisearch/meilisearch_test.go @@ -0,0 +1,83 @@ +package meilisearch_test + +import ( + "context" + "io" + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/meilisearch" +) + +func TestMeilisearch(t *testing.T) { + ctx := context.Background() + + ctr, err := meilisearch.Run(ctx, "getmeili/meilisearch:v1.10.3") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + address, err := ctr.Address(ctx) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, address, nil) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() +} + +func TestMeilisearch_WithDataDump(t *testing.T) { + ctx := context.Background() + + ctr, err := meilisearch.Run(ctx, "getmeili/meilisearch:v1.10.3", + meilisearch.WithDumpImport("testdata/movies.dump"), + meilisearch.WithMasterKey("my-master-key"), + ) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + address, err := ctr.Address(ctx) + require.NoError(t, err) + + client := http.DefaultClient + + req, err := http.NewRequest(http.MethodGet, address, nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + resp.Body.Close() // not closing the body in a defer as it's not used anymore + + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + path, err := url.JoinPath(address, "/indexes/movies/documents/1212") + require.NoError(t, err) + + req, err = http.NewRequest(http.MethodGet, path, nil) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer my-master-key") + + resp, err = client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.EqualValues(t, http.StatusOK, resp.StatusCode) + + bodyBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + // Assert the response of that document. + require.JSONEq(t, `{ + "movie_id": 1212, + "overview": "When a scientists daughter is kidnapped, American Ninja, attempts to find her, but this time he teams up with a youngster he has trained in the ways of the ninja.", + "poster": "https://image.tmdb.org/t/p/w1280/iuAQVI4mvjI83wnirpD8GVNRVuY.jpg", + "release_date": 725846400, + "title": "American Ninja 5" +}`, string(bodyBytes)) +} diff --git a/modules/meilisearch/options.go b/modules/meilisearch/options.go new file mode 100644 index 0000000000..06df8b4435 --- /dev/null +++ b/modules/meilisearch/options.go @@ -0,0 +1,46 @@ +package meilisearch + +import ( + "path/filepath" + + "github.com/testcontainers/testcontainers-go" +) + +// Options is a struct for specifying options for the Meilisearch container. +type Options struct { + DumpDataFilePath string + DumpDataFileName string +} + +func defaultOptions() *Options { + return &Options{} +} + +// Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface. +var _ testcontainers.ContainerCustomizer = (*Option)(nil) + +// Option is an option for the Meilisearch container. +type Option func(*Options) + +// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { + // NOOP to satisfy interface. + return nil +} + +// WithDumpImport sets the data dump file path for the Meilisearch container. +// dumpFilePath either relative to where you call meilisearch run or absolute path +func WithDumpImport(dumpFilePath string) Option { + return func(o *Options) { + o.DumpDataFilePath, o.DumpDataFileName = dumpFilePath, filepath.Base(dumpFilePath) + } +} + +// WithMasterKey sets the master key for the Meilisearch container +// it satisfies the testcontainers.ContainerCustomizer interface +func WithMasterKey(masterKey string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Env["MEILI_MASTER_KEY"] = masterKey + return nil + } +} diff --git a/modules/meilisearch/testdata/movies.dump b/modules/meilisearch/testdata/movies.dump new file mode 100644 index 0000000000..f6b83419a8 Binary files /dev/null and b/modules/meilisearch/testdata/movies.dump differ diff --git a/modules/milvus/examples_test.go b/modules/milvus/examples_test.go index 79ca1b9812..a8242f15b2 100644 --- a/modules/milvus/examples_test.go +++ b/modules/milvus/examples_test.go @@ -8,6 +8,7 @@ import ( "github.com/milvus-io/milvus-sdk-go/v2/client" "github.com/milvus-io/milvus-sdk-go/v2/entity" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/milvus" ) @@ -16,21 +17,21 @@ func ExampleRun() { ctx := context.Background() milvusContainer, err := milvus.Run(ctx, "milvusdb/milvus:v2.3.9") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := milvusContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(milvusContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := milvusContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -44,26 +45,27 @@ func ExampleMilvusContainer_collections() { ctx := context.Background() milvusContainer, err := milvus.Run(ctx, "milvusdb/milvus:v2.3.9") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := milvusContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(milvusContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } connectionStr, err := milvusContainer.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) // nolint:gocritic + log.Printf("failed to get connection string: %s", err) + return } // Create a client to interact with the Milvus container milvusClient, err := client.NewGrpcClient(context.Background(), connectionStr) if err != nil { - log.Fatal("failed to connect to Milvus:", err.Error()) + log.Print("failed to connect to Milvus:", err.Error()) + return } defer milvusClient.Close() @@ -101,12 +103,14 @@ func ExampleMilvusContainer_collections() { 2, // shardNum ) if err != nil { - log.Fatalf("failed to create collection: %s", err) // nolint:gocritic + log.Printf("failed to create collection: %s", err) + return } list, err := milvusClient.ListCollections(context.Background()) if err != nil { - log.Fatalf("failed to list collections: %s", err) // nolint:gocritic + log.Printf("failed to list collections: %s", err) + return } // } diff --git a/modules/milvus/go.mod b/modules/milvus/go.mod index 1b60fe8cf7..53ac64489c 100644 --- a/modules/milvus/go.mod +++ b/modules/milvus/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/milvus-io/milvus-sdk-go/v2 v2.4.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -19,7 +19,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -65,10 +65,10 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/grpc v1.64.1 // indirect google.golang.org/protobuf v1.33.0 // indirect diff --git a/modules/milvus/go.sum b/modules/milvus/go.sum index f8cd7dcf33..1fe9c4270a 100644 --- a/modules/milvus/go.sum +++ b/modules/milvus/go.sum @@ -39,8 +39,8 @@ github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7np github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -343,8 +343,8 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -410,19 +410,19 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/modules/milvus/milvus.go b/modules/milvus/milvus.go index 9b944f6160..b35cc99335 100644 --- a/modules/milvus/milvus.go +++ b/modules/milvus/milvus.go @@ -85,11 +85,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *MilvusContainer + if container != nil { + c = &MilvusContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &MilvusContainer{Container: container}, nil + return c, nil } type embedEtcdConfigTplParams struct { diff --git a/modules/milvus/milvus_test.go b/modules/milvus/milvus_test.go index c49f37c92f..c1ad0a070e 100644 --- a/modules/milvus/milvus_test.go +++ b/modules/milvus/milvus_test.go @@ -7,24 +7,20 @@ import ( "github.com/milvus-io/milvus-sdk-go/v2/client" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/milvus" ) func TestMilvus(t *testing.T) { ctx := context.Background() - container, err := milvus.Run(ctx, "milvusdb/milvus:v2.3.9") + ctr, err := milvus.Run(ctx, "milvusdb/milvus:v2.3.9") + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - // Clean up the container after the test is complete - t.Cleanup(func() { - err = container.Terminate(ctx) - require.NoError(t, err) - }) - t.Run("Connect to Milvus with gRPC", func(tt *testing.T) { // connectionString { - connectionStr, err := container.ConnectionString(ctx) + connectionStr, err := ctr.ConnectionString(ctx) // } require.NoError(t, err) diff --git a/modules/minio/examples_test.go b/modules/minio/examples_test.go index c13e679388..a1e50b6c84 100644 --- a/modules/minio/examples_test.go +++ b/modules/minio/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/minio" ) @@ -13,21 +14,21 @@ func ExampleRun() { ctx := context.Background() minioContainer, err := minio.Run(ctx, "minio/minio:RELEASE.2024-01-16T16-07-38Z") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := minioContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(minioContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := minioContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/minio/go.mod b/modules/minio/go.mod index c50c958c21..53e4caa164 100644 --- a/modules/minio/go.mod +++ b/modules/minio/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/minio/minio-go/v7 v7.0.68 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -15,7 +16,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -30,6 +32,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.6 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/minio/md5-simd v1.1.2 // indirect @@ -45,6 +48,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rs/xid v1.5.0 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect @@ -57,13 +61,14 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/minio/go.sum b/modules/minio/go.sum index 55013e4221..f1ac4ddea7 100644 --- a/modules/minio/go.sum +++ b/modules/minio/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -60,6 +61,10 @@ github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6K github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -97,6 +102,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= @@ -110,6 +117,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -144,8 +153,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -168,14 +177,14 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -195,6 +204,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/minio/minio.go b/modules/minio/minio.go index c7e2898d9f..6547a9003b 100644 --- a/modules/minio/minio.go +++ b/modules/minio/minio.go @@ -2,6 +2,7 @@ package minio import ( "context" + "errors" "fmt" "github.com/testcontainers/testcontainers-go" @@ -59,7 +60,7 @@ func (c *MinioContainer) ConnectionString(ctx context.Context) (string, error) { // Deprecated: use Run instead // RunContainer creates an instance of the Minio container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*MinioContainer, error) { - return Run(ctx, "docker.io/minio/minio:RELEASE.2024-01-16T16-07-38Z", opts...) + return Run(ctx, "minio/minio:RELEASE.2024-01-16T16-07-38Z", opts...) } // Run creates an instance of the Minio container type @@ -89,13 +90,18 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom username := req.Env["MINIO_ROOT_USER"] password := req.Env["MINIO_ROOT_PASSWORD"] if username == "" || password == "" { - return nil, fmt.Errorf("username or password has not been set") + return nil, errors.New("username or password has not been set") } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *MinioContainer + if container != nil { + c = &MinioContainer{Container: container, Username: username, Password: password} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &MinioContainer{Container: container, Username: username, Password: password}, nil + return c, nil } diff --git a/modules/minio/minio_test.go b/modules/minio/minio_test.go index 60bf8034b3..d8ca857cb3 100644 --- a/modules/minio/minio_test.go +++ b/modules/minio/minio_test.go @@ -8,51 +8,39 @@ import ( "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" tcminio "github.com/testcontainers/testcontainers-go/modules/minio" ) func TestMinio(t *testing.T) { ctx := context.Background() - container, err := tcminio.Run(ctx, + ctr, err := tcminio.Run(ctx, "minio/minio:RELEASE.2024-01-16T16-07-38Z", tcminio.WithUsername("thisismyuser"), tcminio.WithPassword("thisismypassword")) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions // connectionString { - url, err := container.ConnectionString(ctx) + url, err := ctr.ConnectionString(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) minioClient, err := minio.New(url, &minio.Options{ - Creds: credentials.NewStaticV4(container.Username, container.Password, ""), + Creds: credentials.NewStaticV4(ctr.Username, ctr.Password, ""), Secure: false, }) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) bucketName := "testcontainers" location := "eu-west-2" // create bucket err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: location}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) objectName := "testdata" contentType := "applcation/octet-stream" @@ -60,23 +48,15 @@ func TestMinio(t *testing.T) { contentLength := int64(len(content)) uploadInfo, err := minioClient.PutObject(ctx, bucketName, objectName, strings.NewReader(content), contentLength, minio.PutObjectOptions{ContentType: contentType}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // object is a readSeekCloser object, err := minioClient.GetObject(ctx, uploadInfo.Bucket, uploadInfo.Key, minio.GetObjectOptions{}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + defer object.Close() n, err := io.Copy(io.Discard, object) - if err != nil { - t.Fatal(err) - } - - if n != contentLength { - t.Fatalf("expected %d; got %d", contentLength, n) - } + require.NoError(t, err) + require.Equal(t, contentLength, n) } diff --git a/modules/mockserver/examples_test.go b/modules/mockserver/examples_test.go index 17f4cfffea..a93c8bcbf0 100644 --- a/modules/mockserver/examples_test.go +++ b/modules/mockserver/examples_test.go @@ -10,6 +10,7 @@ import ( client "github.com/BraspagDevelopers/mock-server-client" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mockserver" ) @@ -18,21 +19,21 @@ func ExampleRun() { ctx := context.Background() mockserverContainer, err := mockserver.Run(ctx, "mockserver/mockserver:5.15.0") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := mockserverContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(mockserverContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := mockserverContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -46,20 +47,20 @@ func ExampleRun_connect() { ctx := context.Background() mockserverContainer, err := mockserver.Run(ctx, "mockserver/mockserver:5.15.0") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := mockserverContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(mockserverContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } url, err := mockserverContainer.URL(ctx) if err != nil { - log.Fatalf("failed to get container URL: %s", err) // nolint:gocritic + log.Printf("failed to get container URL: %s", err) + return } ms := client.NewClientURL(url) // } @@ -71,18 +72,21 @@ func ExampleRun_connect() { requestMatcher = requestMatcher.WithJSONFields(map[string]interface{}{"name": "Tools"}) err = ms.RegisterExpectation(client.NewExpectation(requestMatcher).WithResponse(client.NewResponseOK().WithJSONBody(map[string]any{"test": "value"}))) if err != nil { - log.Fatalf("failed to register expectation: %s", err) + log.Printf("failed to register expectation: %s", err) + return } httpClient := &http.Client{} resp, err := httpClient.Post(url+"/api/categories", "application/json", strings.NewReader(`{"name": "Tools"}`)) if err != nil { - log.Fatalf("failed to send request: %s", err) + log.Printf("failed to send request: %s", err) + return } buf, err := io.ReadAll(resp.Body) if err != nil { - log.Fatalf("failed to read response: %s", err) + log.Printf("failed to read response: %s", err) + return } resp.Body.Close() diff --git a/modules/mockserver/go.mod b/modules/mockserver/go.mod index e2ba76d72d..2c67de0eb5 100644 --- a/modules/mockserver/go.mod +++ b/modules/mockserver/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/BraspagDevelopers/mock-server-client v0.2.2 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -15,7 +15,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -28,6 +29,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -39,10 +41,12 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect @@ -50,11 +54,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/mockserver/go.sum b/modules/mockserver/go.sum index e752fd0812..5588ff118a 100644 --- a/modules/mockserver/go.sum +++ b/modules/mockserver/go.sum @@ -16,8 +16,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -56,6 +57,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -82,6 +87,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -93,6 +100,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -126,8 +135,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -151,14 +160,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -178,6 +187,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/mongodb/cli.go b/modules/mongodb/cli.go new file mode 100644 index 0000000000..f990bf17c8 --- /dev/null +++ b/modules/mongodb/cli.go @@ -0,0 +1,32 @@ +package mongodb + +import "fmt" + +// mongoCli is cli to interact with MongoDB. If username and password are provided +// it will use credentials to authenticate. +type mongoCli struct { + mongoshBaseCmd string + mongoBaseCmd string +} + +func newMongoCli(username string, password string) mongoCli { + authArgs := "" + if username != "" && password != "" { + authArgs = fmt.Sprintf("--username %s --password %s", username, password) + } + + return mongoCli{ + mongoshBaseCmd: fmt.Sprintf("mongosh %s --quiet", authArgs), + mongoBaseCmd: fmt.Sprintf("mongo %s --quiet", authArgs), + } +} + +func (m mongoCli) eval(command string, args ...any) []string { + command = "\"" + fmt.Sprintf(command, args...) + "\"" + + return []string{ + "sh", + "-c", + m.mongoshBaseCmd + " --eval " + command + " || " + m.mongoBaseCmd + " --eval " + command, + } +} diff --git a/modules/mongodb/examples_test.go b/modules/mongodb/examples_test.go index 5e8cbe8009..98a31d61fa 100644 --- a/modules/mongodb/examples_test.go +++ b/modules/mongodb/examples_test.go @@ -19,21 +19,21 @@ func ExampleRun() { ctx := context.Background() mongodbContainer, err := mongodb.Run(ctx, "mongo:6") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := mongodbContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(mongodbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := mongodbContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -47,31 +47,33 @@ func ExampleRun_connect() { ctx := context.Background() mongodbContainer, err := mongodb.Run(ctx, "mongo:6") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := mongodbContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(mongodbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } endpoint, err := mongodbContainer.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) // nolint:gocritic + log.Printf("failed to get connection string: %s", err) + return } mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(endpoint)) if err != nil { - log.Fatalf("failed to connect to MongoDB: %s", err) + log.Printf("failed to connect to MongoDB: %s", err) + return } // } err = mongoClient.Ping(ctx, nil) if err != nil { - log.Fatalf("failed to ping MongoDB: %s", err) + log.Printf("failed to ping MongoDB: %s", err) + return } fmt.Println(mongoClient.Database("test").Name()) @@ -83,36 +85,38 @@ func ExampleRun_connect() { func ExampleRun_withCredentials() { ctx := context.Background() - container, err := mongodb.Run(ctx, + ctr, err := mongodb.Run(ctx, "mongo:6", mongodb.WithUsername("root"), mongodb.WithPassword("password"), testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := container.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(ctr); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } - connStr, err := container.ConnectionString(ctx) + connStr, err := ctr.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) // nolint:gocritic + log.Printf("failed to get connection string: %s", err) + return } mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(connStr)) if err != nil { - log.Fatalf("failed to connect to MongoDB: %s", err) + log.Printf("failed to connect to MongoDB: %s", err) + return } err = mongoClient.Ping(ctx, nil) if err != nil { - log.Fatalf("failed to ping MongoDB: %s", err) + log.Printf("failed to ping MongoDB: %s", err) + return } fmt.Println(strings.Split(connStr, "@")[0]) diff --git a/modules/mongodb/go.mod b/modules/mongodb/go.mod index e7e8c724bc..18d13a4cae 100644 --- a/modules/mongodb/go.mod +++ b/modules/mongodb/go.mod @@ -3,7 +3,8 @@ module github.com/testcontainers/testcontainers-go/modules/mongodb go 1.22 require ( - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 go.mongodb.org/mongo-driver v1.13.1 ) @@ -15,7 +16,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -28,6 +30,7 @@ require ( github.com/golang/snappy v0.0.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -40,6 +43,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -55,13 +59,14 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/mongodb/go.sum b/modules/mongodb/go.sum index 0f2d5d5336..2b2714addd 100644 --- a/modules/mongodb/go.sum +++ b/modules/mongodb/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -56,6 +57,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -84,6 +89,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -95,6 +102,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -141,8 +150,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -159,8 +168,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -176,20 +185,20 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -210,6 +219,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 188c55e85b..b2fa8bb023 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -1,18 +1,32 @@ package mongodb import ( + "bytes" "context" + _ "embed" + "errors" "fmt" + "time" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) +//go:embed mount/entrypoint-tc.sh +var entrypointContent []byte + +const ( + entrypointPath = "/tmp/entrypoint-tc.sh" + keyFilePath = "/tmp/mongo_keyfile" + replicaSetOptEnvKey = "testcontainers.mongodb.replicaset_name" +) + // MongoDBContainer represents the MongoDB container type used in the module type MongoDBContainer struct { testcontainers.Container - username string - password string + username string + password string + replicaSet string } // Deprecated: use Run instead @@ -46,18 +60,27 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom username := req.Env["MONGO_INITDB_ROOT_USERNAME"] password := req.Env["MONGO_INITDB_ROOT_PASSWORD"] if username != "" && password == "" || username == "" && password != "" { - return nil, fmt.Errorf("if you specify username or password, you must provide both of them") + return nil, errors.New("if you specify username or password, you must provide both of them") + } + + replicaSet := req.Env[replicaSetOptEnvKey] + if replicaSet != "" { + if err := configureRequestForReplicaset(username, password, replicaSet, &genericContainerReq); err != nil { + return nil, err + } } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) - if err != nil { - return nil, err + var c *MongoDBContainer + if container != nil { + c = &MongoDBContainer{Container: container, username: username, password: password, replicaSet: replicaSet} } - if username != "" && password != "" { - return &MongoDBContainer{Container: container, username: username, password: password}, nil + if err != nil { + return c, fmt.Errorf("generic container: %w", err) } - return &MongoDBContainer{Container: container}, nil + + return c, nil } // WithUsername sets the initial username to be created when the container starts @@ -82,24 +105,10 @@ func WithPassword(password string) testcontainers.CustomizeRequestOption { } } -// WithReplicaSet configures the container to run a single-node MongoDB replica set named "rs". -// It will wait until the replica set is ready. +// WithReplicaSet sets the replica set name for Single node MongoDB replica set. func WithReplicaSet(replSetName string) testcontainers.CustomizeRequestOption { return func(req *testcontainers.GenericContainerRequest) error { - req.Cmd = append(req.Cmd, "--replSet", replSetName) - req.LifecycleHooks = append(req.LifecycleHooks, testcontainers.ContainerLifecycleHooks{ - PostStarts: []testcontainers.ContainerHook{ - func(ctx context.Context, c testcontainers.Container) error { - ip, err := c.ContainerIP(ctx) - if err != nil { - return fmt.Errorf("container ip: %w", err) - } - - cmd := eval("rs.initiate({ _id: '%s', members: [ { _id: 0, host: '%s:27017' } ] })", replSetName, ip) - return wait.ForExec(cmd).WaitUntilReady(ctx, c) - }, - }, - }) + req.Env[replicaSetOptEnvKey] = replSetName return nil } @@ -122,14 +131,80 @@ func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) return c.Endpoint(ctx, "mongodb") } -// eval builds an mongosh|mongo eval command. -func eval(command string, args ...any) []string { - command = "\"" + fmt.Sprintf(command, args...) + "\"" +func setupEntrypointForAuth(req *testcontainers.GenericContainerRequest) { + req.Files = append( + req.Files, testcontainers.ContainerFile{ + Reader: bytes.NewReader(entrypointContent), + ContainerFilePath: entrypointPath, + FileMode: 0o755, + }, + ) + req.Entrypoint = []string{entrypointPath} + req.Env["MONGO_KEYFILE"] = keyFilePath +} + +func configureRequestForReplicaset( + username string, + password string, + replicaSet string, + genericContainerReq *testcontainers.GenericContainerRequest, +) error { + if !(username != "" && password != "") { + return noAuthReplicaSet(replicaSet)(genericContainerReq) + } + + return withAuthReplicaset(replicaSet, username, password)(genericContainerReq) +} + +func noAuthReplicaSet(replSetName string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + cli := newMongoCli("", "") + req.Cmd = append(req.Cmd, "--replSet", replSetName) + initiateReplicaSet(req, cli, replSetName) - return []string{ - "sh", - "-c", - // In previous versions, the binary "mongosh" was named "mongo". - "mongosh --quiet --eval " + command + " || mongo --quiet --eval " + command, + return nil + } +} + +func initiateReplicaSet(req *testcontainers.GenericContainerRequest, cli mongoCli, replSetName string) { + req.WaitingFor = wait.ForAll( + req.WaitingFor, + wait.ForExec(cli.eval("rs.status().ok")), + ).WithDeadline(60 * time.Second) + + req.LifecycleHooks = append( + req.LifecycleHooks, testcontainers.ContainerLifecycleHooks{ + PostStarts: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { + ip, err := c.ContainerIP(ctx) + if err != nil { + return fmt.Errorf("container ip: %w", err) + } + + cmd := cli.eval( + "rs.initiate({ _id: '%s', members: [ { _id: 0, host: '%s:27017' } ] })", + replSetName, + ip, + ) + + return wait.ForExec(cmd).WaitUntilReady(ctx, c) + }, + }, + }, + ) +} + +func withAuthReplicaset( + replSetName string, + username string, + password string, +) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + setupEntrypointForAuth(req) + cli := newMongoCli(username, password) + req.Cmd = append(req.Cmd, "--replSet", replSetName, "--keyFile", keyFilePath) + initiateReplicaSet(req, cli, replSetName) + + return nil } } diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index ead2b1818b..8cc49e629c 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -34,19 +36,79 @@ func TestMongoDB(t *testing.T) { opts: []testcontainers.ContainerCustomizer{}, }, { - name: "With Replica set and mongo:4", + name: "with-replica/mongo:4", img: "mongo:4", opts: []testcontainers.ContainerCustomizer{ mongodb.WithReplicaSet("rs"), }, }, { - name: "With Replica set and mongo:6", + name: "with-replica/mongo:6", img: "mongo:6", opts: []testcontainers.ContainerCustomizer{ mongodb.WithReplicaSet("rs"), }, }, + { + name: "with-replica/mongo:7", + img: "mongo:7", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + }, + }, + { + name: "with-auth/replica/mongo:7", + img: "mongo:7", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/replica/mongo:6", + img: "mongo:6", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/mongo:6", + img: "mongo:6", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/replica/mongodb-enterprise-server:7.0.0-ubi8", + img: "mongodb/mongodb-enterprise-server:7.0.0-ubi8", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/replica/mongodb-community-server:7.0.2-ubi8", + img: "mongodb/mongodb-community-server:7.0.2-ubi8", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/replica/mongo:4", + img: "mongo:4", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, } for _, tc := range testCases { @@ -57,37 +119,24 @@ func TestMongoDB(t *testing.T) { ctx := context.Background() mongodbContainer, err := mongodb.Run(ctx, tc.img, tc.opts...) - if err != nil { - tt.Fatalf("failed to start container: %s", err) - } - - defer func() { - if err := mongodbContainer.Terminate(ctx); err != nil { - tt.Fatalf("failed to terminate container: %s", err) - } - }() + testcontainers.CleanupContainer(t, mongodbContainer) + require.NoError(tt, err) endpoint, err := mongodbContainer.ConnectionString(ctx) - if err != nil { - tt.Fatalf("failed to get connection string: %s", err) - } + require.NoError(tt, err) // Force direct connection to the container to avoid the replica set // connection string that is returned by the container itself when // using the replica set option. - mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(endpoint+"/?connect=direct")) - if err != nil { - tt.Fatalf("failed to connect to MongoDB: %s", err) - } + mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(endpoint).SetDirect(true)) + require.NoError(tt, err) err = mongoClient.Ping(ctx, nil) - if err != nil { - tt.Fatalf("failed to ping MongoDB: %s", err) - } + require.NoError(tt, err) + require.Equal(t, "test", mongoClient.Database("test").Name()) - if mongoClient.Database("test").Name() != "test" { - tt.Fatalf("failed to connect to the correct database") - } + _, err = mongoClient.Database("testcontainer").Collection("test").InsertOne(context.Background(), bson.M{}) + require.NoError(tt, err) }) } } diff --git a/modules/mongodb/mount/entrypoint-tc.sh b/modules/mongodb/mount/entrypoint-tc.sh new file mode 100644 index 0000000000..1561415aad --- /dev/null +++ b/modules/mongodb/mount/entrypoint-tc.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -Eeuo pipefail + +# detect mongo user and group +function get_user_group() { + user_group=$(cut -d: -f1,5 /etc/passwd | grep mongo) + echo "${user_group}" +} + +# detect the entrypoint +function get_entrypoint() { + entrypoint=$(find /usr/local/bin -name 'docker-entrypoint.*') + if [[ "${entrypoint}" == *.py ]]; then + entrypoint="python3 ${entrypoint}" + else + entrypoint="exec ${entrypoint}" + fi + echo "${entrypoint}" +} + +ENTRYPOINT=$(get_entrypoint) +MONGO_USER_GROUP=$(get_user_group) + +# Create the keyfile +openssl rand -base64 756 > "${MONGO_KEYFILE}" + +# Set the permissions and ownership of the keyfile +chown "${MONGO_USER_GROUP}" "${MONGO_KEYFILE}" +chmod 400 "${MONGO_KEYFILE}" + +${ENTRYPOINT} "$@" diff --git a/modules/mssql/examples_test.go b/modules/mssql/examples_test.go index 10363d9b48..8c1f2c0cac 100644 --- a/modules/mssql/examples_test.go +++ b/modules/mssql/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mssql" ) @@ -15,25 +16,25 @@ func ExampleRun() { password := "SuperStrong@Passw0rd" mssqlContainer, err := mssql.Run(ctx, - "mcr.microsoft.com/mssql/server:2022-RTM-GDR1-ubuntu-20.04", + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04", mssql.WithAcceptEULA(), mssql.WithPassword(password), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := mssqlContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(mssqlContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := mssqlContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/mssql/go.mod b/modules/mssql/go.mod index 83411fb7aa..93217a8b80 100644 --- a/modules/mssql/go.mod +++ b/modules/mssql/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/microsoft/go-mssqldb v1.7.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -15,7 +16,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -29,6 +31,7 @@ require ( github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -40,6 +43,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -51,12 +55,13 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/mssql/go.sum b/modules/mssql/go.sum index 4160a61ee2..a0079e350c 100644 --- a/modules/mssql/go.sum +++ b/modules/mssql/go.sum @@ -26,8 +26,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -70,6 +71,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -102,6 +107,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -113,6 +120,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -146,8 +155,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -169,14 +178,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -196,6 +205,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/mssql/mssql.go b/modules/mssql/mssql.go index ca30d02385..17337bf85b 100644 --- a/modules/mssql/mssql.go +++ b/modules/mssql/mssql.go @@ -44,7 +44,7 @@ func WithPassword(password string) testcontainers.CustomizeRequestOption { // Deprecated: use Run instead // RunContainer creates an instance of the MSSQLServer container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*MSSQLServerContainer, error) { - return Run(ctx, "mcr.microsoft.com/mssql/server:2022-CU10-ubuntu-22.04", opts...) + return Run(ctx, "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04", opts...) } // Run creates an instance of the MSSQLServer container type @@ -70,14 +70,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) - if err != nil { - return nil, err + var c *MSSQLServerContainer + if container != nil { + c = &MSSQLServerContainer{Container: container, password: req.Env["MSSQL_SA_PASSWORD"], username: defaultUsername} } - username := defaultUsername - password := req.Env["MSSQL_SA_PASSWORD"] + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } - return &MSSQLServerContainer{Container: container, password: password, username: username}, nil + return c, nil } func (c *MSSQLServerContainer) ConnectionString(ctx context.Context, args ...string) (string, error) { diff --git a/modules/mssql/mssql_test.go b/modules/mssql/mssql_test.go index 4e2050385a..737c97414e 100644 --- a/modules/mssql/mssql_test.go +++ b/modules/mssql/mssql_test.go @@ -6,6 +6,7 @@ import ( "testing" _ "github.com/microsoft/go-mssqldb" + "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mssql" @@ -15,63 +16,45 @@ import ( func TestMSSQLServer(t *testing.T) { ctx := context.Background() - container, err := mssql.Run(ctx, - "mcr.microsoft.com/mssql/server:2022-CU10-ubuntu-22.04", + ctr, err := mssql.Run(ctx, + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04", mssql.WithAcceptEULA(), ) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions - connectionString, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + connectionString, err := ctr.ConnectionString(ctx) + require.NoError(t, err) db, err := sql.Open("sqlserver", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) _, err = db.Exec("CREATE TABLE a_table ( " + " [col_1] NVARCHAR(128) NOT NULL, " + " [col_2] NVARCHAR(128) NOT NULL, " + " PRIMARY KEY ([col_1], [col_2]) " + ")") - if err != nil { - t.Errorf("error creating table: %+v\n", err) - } + require.NoError(t, err) } func TestMSSQLServerWithMissingEulaOption(t *testing.T) { ctx := context.Background() - container, err := mssql.Run(ctx, - "mcr.microsoft.com/mssql/server:2022-CU10-ubuntu-22.04", + ctr, err := mssql.Run(ctx, + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04", testcontainers.WithWaitStrategy( wait.ForLog("The SQL Server End-User License Agreement (EULA) must be accepted")), ) - if err != nil { - t.Fatalf("Expected a log to confirm missing EULA but got error: %s", err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - state, err := container.State(ctx) - if err != nil { - t.Fatalf("failed to get container state: %s", err) - } + state, err := ctr.State(ctx) + require.NoError(t, err) if !state.Running { t.Log("Success: Confirmed proper handling of missing EULA, so container is not running.") @@ -81,140 +64,67 @@ func TestMSSQLServerWithMissingEulaOption(t *testing.T) { func TestMSSQLServerWithConnectionStringParameters(t *testing.T) { ctx := context.Background() - container, err := mssql.Run(ctx, - "mcr.microsoft.com/mssql/server:2022-CU10-ubuntu-22.04", + ctr, err := mssql.Run(ctx, + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04", mssql.WithAcceptEULA(), ) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions - connectionString, err := container.ConnectionString(ctx, "encrypt=false", "TrustServerCertificate=true") - if err != nil { - t.Fatal(err) - } + connectionString, err := ctr.ConnectionString(ctx, "encrypt=false", "TrustServerCertificate=true") + require.NoError(t, err) db, err := sql.Open("sqlserver", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) _, err = db.Exec("CREATE TABLE a_table ( " + " [col_1] NVARCHAR(128) NOT NULL, " + " [col_2] NVARCHAR(128) NOT NULL, " + " PRIMARY KEY ([col_1], [col_2]) " + ")") - if err != nil { - t.Errorf("error creating table: %+v\n", err) - } + require.NoError(t, err) } func TestMSSQLServerWithCustomStrongPassword(t *testing.T) { ctx := context.Background() - container, err := mssql.Run(ctx, - "mcr.microsoft.com/mssql/server:2022-CU10-ubuntu-22.04", + ctr, err := mssql.Run(ctx, + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04", mssql.WithAcceptEULA(), mssql.WithPassword("Strong@Passw0rd"), ) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions - connectionString, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + connectionString, err := ctr.ConnectionString(ctx) + require.NoError(t, err) db, err := sql.Open("sqlserver", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) } // tests that a weak password is not accepted by the container due to Microsoft's password strength policy func TestMSSQLServerWithInvalidPassword(t *testing.T) { ctx := context.Background() - container, err := mssql.Run(ctx, - "mcr.microsoft.com/mssql/server:2022-CU10-ubuntu-22.04", + ctr, err := mssql.Run(ctx, + "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04", testcontainers.WithWaitStrategy( wait.ForLog("Password validation failed")), mssql.WithAcceptEULA(), mssql.WithPassword("weakPassword"), ) - - if err == nil { - t.Log("Success: Received invalid password validation docker log.") - } else { - t.Fatalf("Expected a password validation log but got error: %s", err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) -} - -func TestMSSQLServerWithAlternativeImage(t *testing.T) { - ctx := context.Background() - - container, err := mssql.Run(ctx, - "mcr.microsoft.com/mssql/server:2022-RTM-GDR1-ubuntu-20.04", - mssql.WithAcceptEULA(), - ) - if err != nil { - t.Fatalf("Failed to create the container with alternative image: %s", err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - - // perform assertions - connectionString, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } - - db, err := sql.Open("sqlserver", connectionString) - if err != nil { - t.Fatal(err) - } - defer db.Close() - - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) } diff --git a/modules/mysql/examples_test.go b/modules/mysql/examples_test.go index bf203c9018..61ee33113d 100644 --- a/modules/mysql/examples_test.go +++ b/modules/mysql/examples_test.go @@ -7,6 +7,7 @@ import ( "log" "path/filepath" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mysql" ) @@ -22,21 +23,21 @@ func ExampleRun() { mysql.WithPassword("password"), mysql.WithScripts(filepath.Join("testdata", "schema.sql")), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := mysqlContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(mysqlContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := mysqlContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -56,40 +57,45 @@ func ExampleRun_connect() { mysql.WithPassword("password"), mysql.WithScripts(filepath.Join("testdata", "schema.sql")), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := mysqlContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(mysqlContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } connectionString, err := mysqlContainer.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) // nolint:gocritic + log.Printf("failed to get connection string: %s", err) + return } db, err := sql.Open("mysql", connectionString) if err != nil { - log.Fatalf("failed to connect to MySQL: %s", err) // nolint:gocritic + log.Printf("failed to connect to MySQL: %s", err) + return } defer db.Close() if err = db.Ping(); err != nil { - log.Fatalf("failed to ping MySQL: %s", err) + log.Printf("failed to ping MySQL: %s", err) + return } stmt, err := db.Prepare("SELECT @@GLOBAL.tmpdir") if err != nil { - log.Fatalf("failed to prepare statement: %s", err) + log.Printf("failed to prepare statement: %s", err) + return } defer stmt.Close() row := stmt.QueryRow() tmpDir := "" err = row.Scan(&tmpDir) if err != nil { - log.Fatalf("failed to scan row: %s", err) + log.Printf("failed to scan row: %s", err) + return } fmt.Println(tmpDir) diff --git a/modules/mysql/go.mod b/modules/mysql/go.mod index f3f00dcb8b..a6984fead4 100644 --- a/modules/mysql/go.mod +++ b/modules/mysql/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/go-sql-driver/mysql v1.7.1 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) @@ -16,7 +17,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -28,6 +30,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -39,6 +42,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -50,11 +54,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/mysql/go.sum b/modules/mysql/go.sum index 7784d0b833..96aef09c5a 100644 --- a/modules/mysql/go.sum +++ b/modules/mysql/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -54,6 +55,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -80,6 +85,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -91,6 +98,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -124,8 +133,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -147,14 +156,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -174,6 +183,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/mysql/mysql.go b/modules/mysql/mysql.go index 7bc6bf7e25..44c3688ab2 100644 --- a/modules/mysql/mysql.go +++ b/modules/mysql/mysql.go @@ -2,6 +2,7 @@ package mysql import ( "context" + "errors" "fmt" "path/filepath" "strings" @@ -82,17 +83,25 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom password := req.Env["MYSQL_PASSWORD"] if len(password) == 0 && password == "" && !strings.EqualFold(rootUser, username) { - return nil, fmt.Errorf("empty password can be used only with the root user") + return nil, errors.New("empty password can be used only with the root user") } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) - if err != nil { - return nil, err + var c *MySQLContainer + if container != nil { + c = &MySQLContainer{ + Container: container, + password: password, + username: username, + database: req.Env["MYSQL_DATABASE"], + } } - database := req.Env["MYSQL_DATABASE"] + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } - return &MySQLContainer{container, username, password, database}, nil + return c, nil } // MustConnectionString panics if the address cannot be determined. diff --git a/modules/mysql/mysql_test.go b/modules/mysql/mysql_test.go index e40ce9bf58..364f2a97a8 100644 --- a/modules/mysql/mysql_test.go +++ b/modules/mysql/mysql_test.go @@ -8,151 +8,110 @@ import ( // Import mysql into the scope of this package (required) _ "github.com/go-sql-driver/mysql" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/mysql" ) func TestMySQL(t *testing.T) { ctx := context.Background() - container, err := mysql.Run(ctx, "mysql:8.0.36") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := mysql.Run(ctx, "mysql:8.0.36") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions // connectionString { - connectionString, err := container.ConnectionString(ctx, "tls=skip-verify") + connectionString, err := ctr.ConnectionString(ctx, "tls=skip-verify") // } - if err != nil { - t.Fatal(err) - } - mustConnectionString := container.MustConnectionString(ctx, "tls=skip-verify") - if mustConnectionString != connectionString { - t.Errorf("ConnectionString was not equal to MustConnectionString") - } + require.NoError(t, err) + + mustConnectionString := ctr.MustConnectionString(ctx, "tls=skip-verify") + require.Equal(t, connectionString, mustConnectionString) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) + _, err = db.Exec("CREATE TABLE IF NOT EXISTS a_table ( \n" + " `col_1` VARCHAR(128) NOT NULL, \n" + " `col_2` VARCHAR(128) NOT NULL, \n" + " PRIMARY KEY (`col_1`, `col_2`) \n" + ")") - if err != nil { - t.Errorf("error creating table: %+v\n", err) - } + require.NoError(t, err) } func TestMySQLWithNonRootUserAndEmptyPassword(t *testing.T) { ctx := context.Background() - _, err := mysql.Run(ctx, + ctr, err := mysql.Run(ctx, "mysql:8.0.36", mysql.WithDatabase("foo"), mysql.WithUsername("test"), mysql.WithPassword("")) - if err.Error() != "empty password can be used only with the root user" { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.EqualError(t, err, "empty password can be used only with the root user") } func TestMySQLWithRootUserAndEmptyPassword(t *testing.T) { ctx := context.Background() - container, err := mysql.Run(ctx, + ctr, err := mysql.Run(ctx, "mysql:8.0.36", mysql.WithDatabase("foo"), mysql.WithUsername("root"), mysql.WithPassword("")) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions - connectionString, _ := container.ConnectionString(ctx) + connectionString, _ := ctr.ConnectionString(ctx) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) + _, err = db.Exec("CREATE TABLE IF NOT EXISTS a_table ( \n" + " `col_1` VARCHAR(128) NOT NULL, \n" + " `col_2` VARCHAR(128) NOT NULL, \n" + " PRIMARY KEY (`col_1`, `col_2`) \n" + ")") - if err != nil { - t.Errorf("error creating table: %+v\n", err) - } + require.NoError(t, err) } func TestMySQLWithScripts(t *testing.T) { ctx := context.Background() - container, err := mysql.Run(ctx, + ctr, err := mysql.Run(ctx, "mysql:8.0.36", mysql.WithScripts(filepath.Join("testdata", "schema.sql"))) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions - connectionString, _ := container.ConnectionString(ctx) + connectionString, _ := ctr.ConnectionString(ctx) db, err := sql.Open("mysql", connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if err = db.Ping(); err != nil { - t.Errorf("error pinging db: %+v\n", err) - } + err = db.Ping() + require.NoError(t, err) + stmt, err := db.Prepare("SELECT name from profile") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer stmt.Close() + row := stmt.QueryRow() var name string err = row.Scan(&name) - if err != nil { - t.Errorf("error fetching data") - } - if name != "profile 1" { - t.Fatal("The expected record was not found in the database.") - } + require.NoError(t, err) + require.Equal(t, "profile 1", name) } diff --git a/modules/nats/examples_test.go b/modules/nats/examples_test.go index 56ade42187..b88fba4c4a 100644 --- a/modules/nats/examples_test.go +++ b/modules/nats/examples_test.go @@ -8,6 +8,7 @@ import ( natsgo "github.com/nats-io/nats.go" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/nats" "github.com/testcontainers/testcontainers-go/network" ) @@ -17,21 +18,21 @@ func ExampleRun() { ctx := context.Background() natsContainer, err := nats.Run(ctx, "nats:2.9") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := natsContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(natsContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := natsContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -44,26 +45,27 @@ func ExampleRun_connectWithCredentials() { // natsConnect { ctx := context.Background() - container, err := nats.Run(ctx, "nats:2.9", nats.WithUsername("foo"), nats.WithPassword("bar")) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container + ctr, err := nats.Run(ctx, "nats:2.9", nats.WithUsername("foo"), nats.WithPassword("bar")) defer func() { - if err := container.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(ctr); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } - uri, err := container.ConnectionString(ctx) + uri, err := ctr.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) // nolint:gocritic + log.Printf("failed to get connection string: %s", err) + return } - nc, err := natsgo.Connect(uri, natsgo.UserInfo(container.User, container.Password)) + nc, err := natsgo.Connect(uri, natsgo.UserInfo(ctr.User, ctr.Password)) if err != nil { - log.Fatalf("failed to connect to NATS: %s", err) + log.Printf("failed to connect to NATS: %s", err) + return } defer nc.Close() // } @@ -79,9 +81,16 @@ func ExampleRun_cluster() { nwr, err := network.New(ctx) if err != nil { - log.Fatalf("failed to create network: %s", err) + log.Printf("failed to create network: %s", err) + return } + defer func() { + if err := nwr.Remove(context.Background()); err != nil { + log.Printf("failed to remove network: %s", err) + } + }() + // withArguments { natsContainer1, err := nats.Run(ctx, "nats:2.9", @@ -93,15 +102,15 @@ func ExampleRun_cluster() { nats.WithArgument("http_port", "8222"), ) // } - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - // Clean up the container defer func() { - if err := natsContainer1.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(natsContainer1); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } natsContainer2, err := nats.Run(ctx, "nats:2.9", @@ -112,15 +121,15 @@ func ExampleRun_cluster() { nats.WithArgument("routes", "nats://nats1:6222,nats://nats2:6222,nats://nats3:6222"), nats.WithArgument("http_port", "8222"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) // nolint:gocritic - } - // Clean up the container defer func() { - if err := natsContainer2.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(natsContainer2); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } natsContainer3, err := nats.Run(ctx, "nats:2.9", @@ -131,28 +140,34 @@ func ExampleRun_cluster() { nats.WithArgument("routes", "nats://nats1:6222,nats://nats2:6222,nats://nats3:6222"), nats.WithArgument("http_port", "8222"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) // nolint:gocritic - } defer func() { - if err := natsContainer3.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(natsContainer3); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // cluster URL servers := natsContainer1.MustConnectionString(ctx) + "," + natsContainer2.MustConnectionString(ctx) + "," + natsContainer3.MustConnectionString(ctx) nc, err := natsgo.Connect(servers, natsgo.MaxReconnects(5), natsgo.ReconnectWait(2*time.Second)) if err != nil { - log.Fatalf("connecting to nats container failed:\n\t%v\n", err) // nolint:gocritic + log.Printf("connecting to nats container failed:\n\t%v\n", err) + return } + // Close connection + defer nc.Close() + { // Simple Publisher err = nc.Publish("foo", []byte("Hello World")) if err != nil { - log.Fatalf("failed to publish message: %s", err) // nolint:gocritic + log.Printf("failed to publish message: %s", err) + return } } @@ -161,13 +176,15 @@ func ExampleRun_cluster() { ch := make(chan *natsgo.Msg, 64) sub, err := nc.ChanSubscribe("channel", ch) if err != nil { - log.Fatalf("failed to subscribe to message: %s", err) // nolint:gocritic + log.Printf("failed to subscribe to message: %s", err) + return } // Request err = nc.Publish("channel", []byte("Hello NATS Cluster!")) if err != nil { - log.Fatalf("failed to publish message: %s", err) // nolint:gocritic + log.Printf("failed to publish message: %s", err) + return } msg := <-ch @@ -175,12 +192,14 @@ func ExampleRun_cluster() { err = sub.Unsubscribe() if err != nil { - log.Fatalf("failed to unsubscribe: %s", err) // nolint:gocritic + log.Printf("failed to unsubscribe: %s", err) + return } err = sub.Drain() if err != nil { - log.Fatalf("failed to drain: %s", err) // nolint:gocritic + log.Printf("failed to drain: %s", err) + return } } @@ -189,29 +208,34 @@ func ExampleRun_cluster() { sub, err := nc.Subscribe("request", func(m *natsgo.Msg) { err1 := m.Respond([]byte("answer is 42")) if err1 != nil { - log.Fatalf("failed to respond to message: %s", err1) // nolint:gocritic + log.Printf("failed to respond to message: %s", err1) + return } }) if err != nil { - log.Fatalf("failed to subscribe to message: %s", err) // nolint:gocritic + log.Printf("failed to subscribe to message: %s", err) + return } // Request msg, err := nc.Request("request", []byte("what is the answer?"), 1*time.Second) if err != nil { - log.Fatalf("failed to send request: %s", err) // nolint:gocritic + log.Printf("failed to send request: %s", err) + return } fmt.Println(string(msg.Data)) err = sub.Unsubscribe() if err != nil { - log.Fatalf("failed to unsubscribe: %s", err) // nolint:gocritic + log.Printf("failed to unsubscribe: %s", err) + return } err = sub.Drain() if err != nil { - log.Fatalf("failed to drain: %s", err) // nolint:gocritic + log.Printf("failed to drain: %s", err) + return } } @@ -219,12 +243,10 @@ func ExampleRun_cluster() { // Close() not needed if this is called. err = nc.Drain() if err != nil { - log.Fatalf("failed to drain connection: %s", err) // nolint:gocritic + log.Printf("failed to drain connection: %s", err) + return } - // Close connection - nc.Close() - // Output: // Hello NATS Cluster! // answer is 42 diff --git a/modules/nats/go.mod b/modules/nats/go.mod index 0a4863c41e..d055e61dd1 100644 --- a/modules/nats/go.mod +++ b/modules/nats/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/nats-io/nats.go v1.33.1 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -15,7 +16,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -27,6 +29,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -40,6 +43,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -51,11 +55,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/nats/go.sum b/modules/nats/go.sum index 11a786d5d4..bb69595f80 100644 --- a/modules/nats/go.sum +++ b/modules/nats/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,6 +53,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -84,6 +89,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -95,6 +102,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -128,8 +137,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -151,14 +160,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -178,6 +187,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/nats/nats.go b/modules/nats/nats.go index 0ded01dd09..cd040c09e2 100644 --- a/modules/nats/nats.go +++ b/modules/nats/nats.go @@ -59,17 +59,20 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) - if err != nil { - return nil, err + var c *NATSContainer + if container != nil { + c = &NATSContainer{ + Container: container, + User: settings.CmdArgs["user"], + Password: settings.CmdArgs["pass"], + } } - natsContainer := NATSContainer{ - Container: container, - User: settings.CmdArgs["user"], - Password: settings.CmdArgs["pass"], + if err != nil { + return c, fmt.Errorf("generic container: %w", err) } - return &natsContainer, nil + return c, nil } func (c *NATSContainer) MustConnectionString(ctx context.Context, args ...string) string { diff --git a/modules/nats/nats_test.go b/modules/nats/nats_test.go index e223f0aa24..660473c2e5 100644 --- a/modules/nats/nats_test.go +++ b/modules/nats/nats_test.go @@ -5,7 +5,9 @@ import ( "testing" "github.com/nats-io/nats.go" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" tcnats "github.com/testcontainers/testcontainers-go/modules/nats" ) @@ -13,67 +15,45 @@ func TestNATS(t *testing.T) { ctx := context.Background() // createNATSContainer { - container, err := tcnats.Run(ctx, "nats:2.9") + ctr, err := tcnats.Run(ctx, "nats:2.9") // } - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // connectionString { - uri, err := container.ConnectionString(ctx) + uri, err := ctr.ConnectionString(ctx) // } - if err != nil { - t.Fatalf("failed to get connection string: %s", err) - } - mustUri := container.MustConnectionString(ctx) - if mustUri != uri { - t.Errorf("URI was not equal to MustUri") - } + require.NoError(t, err) + + mustUri := ctr.MustConnectionString(ctx) + require.Equal(t, mustUri, uri) + // perform assertions nc, err := nats.Connect(uri) - if err != nil { - t.Fatalf("failed to connect to nats: %s", err) - } + require.NoError(t, err) defer nc.Close() js, err := nc.JetStream() - if err != nil { - t.Fatalf("failed to create jetstream context: %s", err) - } + require.NoError(t, err) // add stream to nats - if _, err = js.AddStream(&nats.StreamConfig{ + _, err = js.AddStream(&nats.StreamConfig{ Name: "hello", Subjects: []string{"hello"}, - }); err != nil { - t.Fatalf("failed to add stream: %s", err) - } + }) + require.NoError(t, err) // add subscriber to nats sub, err := js.SubscribeSync("hello", nats.Durable("worker")) - if err != nil { - t.Fatalf("failed to subscribe to hello: %s", err) - } + require.NoError(t, err) // publish a message to nats - if _, err = js.Publish("hello", []byte("hello")); err != nil { - t.Fatalf("failed to publish hello: %s", err) - } + _, err = js.Publish("hello", []byte("hello")) + require.NoError(t, err) // wait for the message to be received msg, err := sub.NextMsgWithContext(ctx) - if err != nil { - t.Fatalf("failed to get message: %s", err) - } + require.NoError(t, err) - if string(msg.Data) != "hello" { - t.Fatalf("expected message to be 'hello', got '%s'", msg.Data) - } + require.Equal(t, "hello", string(msg.Data)) } diff --git a/modules/neo4j/config.go b/modules/neo4j/config.go index 91eb3e415b..aff88d4f20 100644 --- a/modules/neo4j/config.go +++ b/modules/neo4j/config.go @@ -35,7 +35,7 @@ func WithAdminPassword(adminPassword string) testcontainers.CustomizeRequestOpti return func(req *testcontainers.GenericContainerRequest) error { pwd := "none" if adminPassword != "" { - pwd = fmt.Sprintf("neo4j/%s", adminPassword) + pwd = "neo4j/" + adminPassword } req.Env["NEO4J_AUTH"] = pwd @@ -127,7 +127,7 @@ func validate(req *testcontainers.GenericContainerRequest) error { func formatNeo4jConfig(name string) string { result := strings.ReplaceAll(name, "_", "__") result = strings.ReplaceAll(result, ".", "_") - return fmt.Sprintf("NEO4J_%s", result) + return "NEO4J_" + result } // WithAcceptCommercialLicenseAgreement sets the environment variable diff --git a/modules/neo4j/examples_test.go b/modules/neo4j/examples_test.go index 375184a1d6..51b11bf49a 100644 --- a/modules/neo4j/examples_test.go +++ b/modules/neo4j/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/neo4j" ) @@ -15,26 +16,26 @@ func ExampleRun() { testPassword := "letmein!" neo4jContainer, err := neo4j.Run(ctx, - "docker.io/neo4j:4.4", + "neo4j:4.4", neo4j.WithAdminPassword(testPassword), neo4j.WithLabsPlugin(neo4j.Apoc), neo4j.WithNeo4jSetting("dbms.tx_log.rotation.size", "42M"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := neo4jContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(neo4jContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := neo4jContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/neo4j/go.mod b/modules/neo4j/go.mod index 13f310eeb5..a9ca222a30 100644 --- a/modules/neo4j/go.mod +++ b/modules/neo4j/go.mod @@ -5,7 +5,8 @@ go 1.22 require ( github.com/docker/go-connections v0.5.0 github.com/neo4j/neo4j-go-driver/v5 v5.18.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -16,7 +17,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect @@ -27,6 +29,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -38,6 +41,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -49,11 +53,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/neo4j/go.sum b/modules/neo4j/go.sum index 2576f66232..b7d1b4f201 100644 --- a/modules/neo4j/go.sum +++ b/modules/neo4j/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,6 +53,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -80,6 +85,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -91,6 +98,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -124,8 +133,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -147,14 +156,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -174,6 +183,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/neo4j/neo4j.go b/modules/neo4j/neo4j.go index e4f7fc3314..ed9579d46d 100644 --- a/modules/neo4j/neo4j.go +++ b/modules/neo4j/neo4j.go @@ -56,9 +56,9 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom "NEO4J_AUTH": "none", }, ExposedPorts: []string{ - fmt.Sprintf("%s/tcp", defaultBoltPort), - fmt.Sprintf("%s/tcp", defaultHttpPort), - fmt.Sprintf("%s/tcp", defaultHttpsPort), + defaultBoltPort + "/tcp", + defaultHttpPort + "/tcp", + defaultHttpsPort + "/tcp", }, WaitingFor: &wait.MultiStrategy{ Strategies: []wait.Strategy{ @@ -93,11 +93,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *Neo4jContainer + if container != nil { + c = &Neo4jContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &Neo4jContainer{Container: container}, nil + return c, nil } func isHttpOk() func(status int) bool { diff --git a/modules/neo4j/neo4j_test.go b/modules/neo4j/neo4j_test.go index 20bd17188b..51a01817ce 100644 --- a/modules/neo4j/neo4j_test.go +++ b/modules/neo4j/neo4j_test.go @@ -3,12 +3,13 @@ package neo4j_test import ( "context" "fmt" - "io" "strings" "testing" neo "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/neo4j" ) @@ -19,43 +20,33 @@ func TestNeo4j(outer *testing.T) { ctx := context.Background() - container := setupNeo4j(ctx, outer) - - outer.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - outer.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := setupNeo4j(ctx) + testcontainers.CleanupContainer(outer, ctr) + require.NoError(outer, err) outer.Run("connects via Bolt", func(t *testing.T) { - driver := createDriver(t, ctx, container) + driver := createDriver(t, ctx, ctr) err := driver.VerifyConnectivity(ctx) - if err != nil { - t.Fatalf("should have successfully connected to server but did not: %s", err) - } + require.NoErrorf(t, err, "should have successfully connected to server but did not") }) outer.Run("exercises APOC plugin", func(t *testing.T) { - driver := createDriver(t, ctx, container) + driver := createDriver(t, ctx, ctr) result, err := neo.ExecuteQuery(ctx, driver, "RETURN apoc.number.arabicToRoman(1986) AS output", nil, neo.EagerResultTransformer) - if err != nil { - t.Fatalf("expected APOC query to successfully run but did not: %s", err) - } - if value, _ := result.Records[0].Get("output"); value != "MCMLXXXVI" { - t.Fatalf("did not get expected roman number: %s", value) - } + require.NoErrorf(t, err, "expected APOC query to successfully run but did not") + require.NotEmpty(t, result.Records) + value, _ := result.Records[0].Get("output") + require.Equalf(t, "MCMLXXXVI", value, "did not get expected roman number: %s", value) }) outer.Run("is configured with custom Neo4j settings", func(t *testing.T) { - env := getContainerEnv(t, ctx, container) + env := getContainerEnv(t, ctx, ctr) - if !strings.Contains(env, "NEO4J_dbms_tx__log_rotation_size=42M") { - t.Fatal("expected to custom setting to be exported but was not") - } + require.Containsf(t, env, "NEO4J_dbms_tx__log_rotation_size=42M", "expected to custom setting to be exported but was not") }) } @@ -65,34 +56,25 @@ func TestNeo4jWithEnterpriseLicense(t *testing.T) { ctx := context.Background() images := map[string]string{ - "StandardEdition": "docker.io/neo4j:4.4", - "EnterpriseEdition": "docker.io/neo4j:4.4-enterprise", + "StandardEdition": "neo4j:4.4", + "EnterpriseEdition": "neo4j:4.4-enterprise", } for edition, img := range images { edition, img := edition, img t.Run(edition, func(t *testing.T) { t.Parallel() - container, err := neo4j.Run(ctx, + ctr, err := neo4j.Run(ctx, img, neo4j.WithAdminPassword(testPassword), neo4j.WithAcceptCommercialLicenseAgreement(), ) - if err != nil { - t.Fatalf("expected container to successfully initialize but did not: %s", err) - } - - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - env := getContainerEnv(t, ctx, container) + env := getContainerEnv(t, ctx, ctr) - if !strings.Contains(env, "NEO4J_ACCEPT_LICENSE_AGREEMENT=yes") { - t.Fatal("expected to accept license agreement but did not") - } + require.Containsf(t, env, "NEO4J_ACCEPT_LICENSE_AGREEMENT=yes", "expected to accept license agreement but did not") }) } } @@ -103,35 +85,26 @@ func TestNeo4jWithWrongSettings(outer *testing.T) { ctx := context.Background() outer.Run("without authentication", func(t *testing.T) { - container, err := neo4j.Run(ctx, "neo4j:4.4") - if err != nil { - t.Fatalf("expected env to successfully run but did not: %s", err) - } - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - outer.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := neo4j.Run(ctx, "neo4j:4.4") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) }) outer.Run("auth setting outside WithAdminPassword raises error", func(t *testing.T) { - container, err := neo4j.Run(ctx, + ctr, err := neo4j.Run(ctx, "neo4j:4.4", neo4j.WithAdminPassword(testPassword), neo4j.WithNeo4jSetting("AUTH", "neo4j/thisisgonnafail"), ) - if err == nil { - t.Fatalf("expected env to fail due to conflicting auth settings but did not") - } - if container != nil { - t.Fatalf("container must not be created with conflicting auth settings") - } + testcontainers.CleanupContainer(t, ctr) + require.Errorf(t, err, "expected env to fail due to conflicting auth settings but did not") + require.Nilf(t, ctr, "container must not be created with conflicting auth settings") }) outer.Run("warns about overwrites of setting keys", func(t *testing.T) { // withSettings { logger := &inMemoryLogger{} - container, err := neo4j.Run(ctx, + ctr, err := neo4j.Run(ctx, "neo4j:4.4", neo4j.WithLogger(logger), // needs to go before WithNeo4jSetting and WithNeo4jSettings neo4j.WithAdminPassword(testPassword), @@ -140,39 +113,25 @@ func TestNeo4jWithWrongSettings(outer *testing.T) { neo4j.WithNeo4jSetting("some.key", "value3"), ) // } - if err != nil { - t.Fatalf("expected env to successfully run but did not: %s", err) - } - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - outer.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) errorLogs := logger.Logs() - if !Contains(errorLogs, `setting "some.key" with value "value1" is now overwritten with value "value2"`+"\n") || - !Contains(errorLogs, `setting "some.key" with value "value2" is now overwritten with value "value3"`+"\n") { - t.Fatalf("expected setting overwrites to be logged") - } - if !strings.Contains(getContainerEnv(t, ctx, container), "NEO4J_some_key=value3") { - t.Fatalf("expected custom setting to be set with last value") - } + require.Containsf(t, errorLogs, `setting "some.key" with value "value1" is now overwritten with value "value2"`+"\n", "expected setting overwrites to be logged") + require.Containsf(t, errorLogs, `setting "some.key" with value "value2" is now overwritten with value "value3"`+"\n", "expected setting overwrites to be logged") + require.Containsf(t, getContainerEnv(t, ctx, ctr), "NEO4J_some_key=value3", "expected custom setting to be set with last value") }) outer.Run("rejects nil logger", func(t *testing.T) { - container, err := neo4j.Run(ctx, "neo4j:4.4", neo4j.WithLogger(nil)) - - if container != nil { - t.Fatalf("container must not be created with nil logger") - } - if err == nil || err.Error() != "nil logger is not permitted" { - t.Fatalf("expected config validation error but got no error") - } + ctr, err := neo4j.Run(ctx, "neo4j:4.4", neo4j.WithLogger(nil)) + testcontainers.CleanupContainer(t, ctr) + require.Nilf(t, ctr, "container must not be created with nil logger") + require.EqualErrorf(t, err, "nil logger is not permitted", "expected config validation error but got no error") }) } -func setupNeo4j(ctx context.Context, t *testing.T) *neo4j.Neo4jContainer { - container, err := neo4j.Run(ctx, +func setupNeo4j(ctx context.Context) (*neo4j.Neo4jContainer, error) { + return neo4j.Run(ctx, "neo4j:4.4", neo4j.WithAdminPassword(testPassword), // withLabsPlugin { @@ -180,44 +139,26 @@ func setupNeo4j(ctx context.Context, t *testing.T) *neo4j.Neo4jContainer { // } neo4j.WithNeo4jSetting("dbms.tx_log.rotation.size", "42M"), ) - if err != nil { - t.Fatalf("expected container to successfully initialize but did not: %s", err) - } - return container } func createDriver(t *testing.T, ctx context.Context, container *neo4j.Neo4jContainer) neo.DriverWithContext { + t.Helper() // boltURL { boltUrl, err := container.BoltUrl(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) driver, err := neo.NewDriverWithContext(boltUrl, neo.BasicAuth("neo4j", testPassword, "")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) t.Cleanup(func() { - if err := driver.Close(ctx); err != nil { - t.Fatalf("failed to close neo: %s", err) - } + err := driver.Close(ctx) + require.NoErrorf(t, err, "failed to close neo: %s", err) }) return driver } func getContainerEnv(t *testing.T, ctx context.Context, container *neo4j.Neo4jContainer) string { - exec, reader, err := container.Exec(ctx, []string{"env"}) - if err != nil { - t.Fatalf("expected env to successfully run but did not: %s", err) - } - if exec != 0 { - t.Fatalf("expected env to exit with status 0 but exited with: %d", exec) - } - envVars, err := io.ReadAll(reader) - if err != nil { - t.Fatalf("expected to read all bytes from env output but did not: %s", err) - } - return string(envVars) + t.Helper() + return testcontainers.RequireContainerExec(ctx, t, container, []string{"env"}) } const logSeparator = "---$$$---" diff --git a/modules/ollama/examples_test.go b/modules/ollama/examples_test.go index 3e2a273854..741db846be 100644 --- a/modules/ollama/examples_test.go +++ b/modules/ollama/examples_test.go @@ -10,6 +10,7 @@ import ( "github.com/tmc/langchaingo/llms" langchainollama "github.com/tmc/langchaingo/llms/ollama" + "github.com/testcontainers/testcontainers-go" tcollama "github.com/testcontainers/testcontainers-go/modules/ollama" ) @@ -18,21 +19,21 @@ func ExampleRun() { ctx := context.Background() ollamaContainer, err := tcollama.Run(ctx, "ollama/ollama:0.1.25") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := ollamaContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(ollamaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := ollamaContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -46,30 +47,34 @@ func ExampleRun_withModel_llama2_http() { ctx := context.Background() ollamaContainer, err := tcollama.Run(ctx, "ollama/ollama:0.1.25") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - if err := ollamaContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(ollamaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } model := "llama2" _, _, err = ollamaContainer.Exec(ctx, []string{"ollama", "pull", model}) if err != nil { - log.Fatalf("failed to pull model %s: %s", model, err) // nolint:gocritic + log.Printf("failed to pull model %s: %s", model, err) + return } _, _, err = ollamaContainer.Exec(ctx, []string{"ollama", "run", model}) if err != nil { - log.Fatalf("failed to run model %s: %s", model, err) // nolint:gocritic + log.Printf("failed to run model %s: %s", model, err) + return } connectionStr, err := ollamaContainer.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) // nolint:gocritic + log.Printf("failed to get connection string: %s", err) + return } httpClient := &http.Client{} @@ -80,14 +85,16 @@ func ExampleRun_withModel_llama2_http() { "prompt":"Why is the sky blue?" }` - req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/generate", connectionStr), strings.NewReader(payload)) + req, err := http.NewRequest(http.MethodPost, connectionStr+"/api/generate", strings.NewReader(payload)) if err != nil { - log.Fatalf("failed to create request: %s", err) // nolint:gocritic + log.Printf("failed to create request: %s", err) + return } resp, err := httpClient.Do(req) if err != nil { - log.Fatalf("failed to get response: %s", err) // nolint:gocritic + log.Printf("failed to get response: %s", err) + return } // } @@ -101,30 +108,34 @@ func ExampleRun_withModel_llama2_langchain() { ctx := context.Background() ollamaContainer, err := tcollama.Run(ctx, "ollama/ollama:0.1.25") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - if err := ollamaContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(ollamaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } model := "llama2" _, _, err = ollamaContainer.Exec(ctx, []string{"ollama", "pull", model}) if err != nil { - log.Fatalf("failed to pull model %s: %s", model, err) // nolint:gocritic + log.Printf("failed to pull model %s: %s", model, err) + return } _, _, err = ollamaContainer.Exec(ctx, []string{"ollama", "run", model}) if err != nil { - log.Fatalf("failed to run model %s: %s", model, err) // nolint:gocritic + log.Printf("failed to run model %s: %s", model, err) + return } connectionStr, err := ollamaContainer.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) // nolint:gocritic + log.Printf("failed to get connection string: %s", err) + return } var llm *langchainollama.LLM @@ -132,7 +143,8 @@ func ExampleRun_withModel_llama2_langchain() { langchainollama.WithModel(model), langchainollama.WithServerURL(connectionStr), ); err != nil { - log.Fatalf("failed to create langchain ollama: %s", err) // nolint:gocritic + log.Printf("failed to create langchain ollama: %s", err) + return } completion, err := llm.Call( @@ -142,7 +154,8 @@ func ExampleRun_withModel_llama2_langchain() { llms.WithTemperature(0.0), // the lower the temperature, the more creative the completion ) if err != nil { - log.Fatalf("failed to create langchain ollama: %s", err) // nolint:gocritic + log.Printf("failed to create langchain ollama: %s", err) + return } words := []string{ diff --git a/modules/ollama/go.mod b/modules/ollama/go.mod index 5e586e858e..b3a1b1e5c2 100644 --- a/modules/ollama/go.mod +++ b/modules/ollama/go.mod @@ -5,7 +5,8 @@ go 1.22 require ( github.com/docker/docker v27.1.1+incompatible github.com/google/uuid v1.6.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 github.com/tmc/langchaingo v0.1.5 ) @@ -17,7 +18,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.8.1 // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -40,6 +42,7 @@ require ( github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -51,11 +54,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/ollama/go.sum b/modules/ollama/go.sum index 07f9ef689f..87536fd72c 100644 --- a/modules/ollama/go.sum +++ b/modules/ollama/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -54,6 +54,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -82,6 +86,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -93,6 +99,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -128,8 +136,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -151,14 +159,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -178,6 +186,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/ollama/ollama.go b/modules/ollama/ollama.go index b8a2fc1de6..203d80103f 100644 --- a/modules/ollama/ollama.go +++ b/modules/ollama/ollama.go @@ -101,9 +101,14 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *OllamaContainer + if container != nil { + c = &OllamaContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &OllamaContainer{Container: container}, nil + return c, nil } diff --git a/modules/ollama/ollama_test.go b/modules/ollama/ollama_test.go index b60538835b..94212dc171 100644 --- a/modules/ollama/ollama_test.go +++ b/modules/ollama/ollama_test.go @@ -4,13 +4,14 @@ import ( "context" "fmt" "io" - "log" "net/http" "strings" "testing" "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/exec" "github.com/testcontainers/testcontainers-go/modules/ollama" ) @@ -18,52 +19,34 @@ import ( func TestOllama(t *testing.T) { ctx := context.Background() - container, err := ollama.Run(ctx, "ollama/ollama:0.1.25") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := ollama.Run(ctx, "ollama/ollama:0.1.25") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) t.Run("ConnectionString", func(t *testing.T) { // connectionString { - connectionStr, err := container.ConnectionString(ctx) + connectionStr, err := ctr.ConnectionString(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) httpClient := &http.Client{} resp, err := httpClient.Get(connectionStr) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("expected status code 200, got %d", resp.StatusCode) - } + require.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("Pull and Run Model", func(t *testing.T) { model := "all-minilm" - _, _, err = container.Exec(context.Background(), []string{"ollama", "pull", model}) - if err != nil { - log.Fatalf("failed to pull model %s: %s", model, err) - } + _, _, err = ctr.Exec(context.Background(), []string{"ollama", "pull", model}) + require.NoError(t, err) - _, _, err = container.Exec(context.Background(), []string{"ollama", "run", model}) - if err != nil { - log.Fatalf("failed to run model %s: %s", model, err) - } + _, _, err = ctr.Exec(context.Background(), []string{"ollama", "run", model}) + require.NoError(t, err) - assertLoadedModel(t, container) + assertLoadedModel(t, ctr) }) t.Run("Commit to image including model", func(t *testing.T) { @@ -73,24 +56,16 @@ func TestOllama(t *testing.T) { // Users can change the way this is generated, but it should be unique. targetImage := fmt.Sprintf("%s-%s", ollama.DefaultOllamaImage, strings.ToLower(uuid.New().String()[:4])) - err := container.Commit(context.Background(), targetImage) + err := ctr.Commit(context.Background(), targetImage) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) newOllamaContainer, err := ollama.Run( context.Background(), targetImage, ) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := newOllamaContainer.Terminate(context.Background()); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, newOllamaContainer) + require.NoError(t, err) assertLoadedModel(t, newOllamaContainer) }) @@ -100,31 +75,22 @@ func TestOllama(t *testing.T) { // For that, it checks if the response of the /api/tags endpoint // contains the model name. func assertLoadedModel(t *testing.T, c *ollama.OllamaContainer) { + t.Helper() url, err := c.ConnectionString(context.Background()) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) httpCli := &http.Client{} resp, err := httpCli.Get(url + "/api/tags") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("expected status code 200, got %d", resp.StatusCode) - } + require.Equal(t, http.StatusOK, resp.StatusCode) bs, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if !strings.Contains(string(bs), "all-minilm") { - t.Fatalf("expected response to contain all-minilm, got %s", string(bs)) - } + require.Contains(t, string(bs), "all-minilm") } func TestRunContainer_withModel_error(t *testing.T) { @@ -134,30 +100,21 @@ func TestRunContainer_withModel_error(t *testing.T) { ctx, "ollama/ollama:0.1.25", ) - if err != nil { - t.Fatalf("expected error to be nil, got %s", err) - } + testcontainers.CleanupContainer(t, ollamaContainer) + require.NoError(t, err) model := "non-existent" _, _, err = ollamaContainer.Exec(ctx, []string{"ollama", "pull", model}) - if err != nil { - log.Fatalf("expected nil error, got %s", err) - } + require.NoError(t, err) // we need to parse the response here to check if the error message is correct _, r, err := ollamaContainer.Exec(ctx, []string{"ollama", "run", model}, exec.Multiplexed()) - if err != nil { - log.Fatalf("expected nil error, got %s", err) - } + require.NoError(t, err) bs, err := io.ReadAll(r) - if err != nil { - t.Fatalf("failed to run %s model: %s", model, err) - } + require.NoError(t, err) stdOutput := string(bs) - if !strings.Contains(stdOutput, "Error: pull model manifest: file does not exist") { - t.Fatalf("expected output to contain %q, got %s", "Error: pull model manifest: file does not exist", stdOutput) - } + require.Contains(t, stdOutput, "Error: pull model manifest: file does not exist") } diff --git a/modules/openfga/examples_test.go b/modules/openfga/examples_test.go index 38609451ef..cb0443b863 100644 --- a/modules/openfga/examples_test.go +++ b/modules/openfga/examples_test.go @@ -22,21 +22,21 @@ func ExampleRun() { ctx := context.Background() openfgaContainer, err := openfga.Run(ctx, "openfga/openfga:v1.5.0") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := openfgaContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(openfgaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := openfgaContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -47,21 +47,21 @@ func ExampleRun() { func ExampleRun_connectToPlayground() { openfgaContainer, err := openfga.Run(context.Background(), "openfga/openfga:v1.5.0") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := openfgaContainer.Terminate(context.Background()); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(openfgaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // playgroundEndpoint { playgroundEndpoint, err := openfgaContainer.PlaygroundEndpoint(context.Background()) if err != nil { - log.Fatalf("failed to get playground endpoint: %s", err) // nolint:gocritic + log.Printf("failed to get playground endpoint: %s", err) + return } // } @@ -69,7 +69,8 @@ func ExampleRun_connectToPlayground() { resp, err := httpClient.Get(playgroundEndpoint) if err != nil { - log.Fatalf("failed to get playground endpoint: %s", err) // nolint:gocritic + log.Printf("failed to get playground endpoint: %s", err) + return } fmt.Println(resp.StatusCode) @@ -80,21 +81,21 @@ func ExampleRun_connectToPlayground() { func ExampleRun_connectWithSDKClient() { openfgaContainer, err := openfga.Run(context.Background(), "openfga/openfga:v1.5.0") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := openfgaContainer.Terminate(context.Background()); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(openfgaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // httpEndpoint { httpEndpoint, err := openfgaContainer.HttpEndpoint(context.Background()) if err != nil { - log.Fatalf("failed to get HTTP endpoint: %s", err) // nolint:gocritic + log.Printf("failed to get HTTP endpoint: %s", err) + return } // } @@ -103,26 +104,30 @@ func ExampleRun_connectWithSDKClient() { ApiUrl: httpEndpoint, // required }) if err != nil { - log.Fatalf("failed to create SDK client: %s", err) // nolint:gocritic + log.Printf("failed to create SDK client: %s", err) + return } list, err := fgaClient.ListStores(context.Background()).Execute() if err != nil { - log.Fatalf("failed to list stores: %s", err) // nolint:gocritic + log.Printf("failed to list stores: %s", err) + return } fmt.Println(len(list.Stores)) store, err := fgaClient.CreateStore(context.Background()).Body(client.ClientCreateStoreRequest{Name: "test"}).Execute() if err != nil { - log.Fatalf("failed to create store: %s", err) // nolint:gocritic + log.Printf("failed to create store: %s", err) + return } fmt.Println(store.Name) list, err = fgaClient.ListStores(context.Background()).Execute() if err != nil { - log.Fatalf("failed to list stores: %s", err) // nolint:gocritic + log.Printf("failed to list stores: %s", err) + return } fmt.Println(len(list.Stores)) @@ -145,20 +150,20 @@ func ExampleRun_writeModel() { "OPENFGA_AUTHN_PRESHARED_KEYS": secret, }), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := openfgaContainer.Terminate(context.Background()); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(openfgaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } httpEndpoint, err := openfgaContainer.HttpEndpoint(context.Background()) if err != nil { - log.Fatalf("failed to get HTTP endpoint: %s", err) // nolint:gocritic + log.Printf("failed to get HTTP endpoint: %s", err) + return } fgaClient, err := client.NewSdkClient(&client.ClientConfiguration{ @@ -177,28 +182,33 @@ func ExampleRun_writeModel() { StoreId: "11111111111111111111111111", }) if err != nil { - log.Fatalf("failed to create openfga client: %v", err) + log.Printf("failed to create openfga client: %v", err) + return } f, err := os.Open(filepath.Join("testdata", "authorization_model.json")) if err != nil { - log.Fatalf("failed to open file: %v", err) + log.Printf("failed to open file: %v", err) + return } defer f.Close() bs, err := io.ReadAll(f) if err != nil { - log.Fatalf("failed to read file: %v", err) + log.Printf("failed to read file: %v", err) + return } var body client.ClientWriteAuthorizationModelRequest if err := json.Unmarshal(bs, &body); err != nil { - log.Fatalf("failed to unmarshal json: %v", err) + log.Printf("failed to unmarshal json: %v", err) + return } resp, err := fgaClient.WriteAuthorizationModel(context.Background()).Body(body).Execute() if err != nil { - log.Fatalf("failed to write authorization model: %v", err) + log.Printf("failed to write authorization model: %v", err) + return } // } diff --git a/modules/openfga/go.mod b/modules/openfga/go.mod index 388b9c040c..2a0c0d9df8 100644 --- a/modules/openfga/go.mod +++ b/modules/openfga/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/openfga/go-sdk v0.3.5 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -15,7 +16,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -27,6 +29,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -38,6 +41,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -49,12 +53,13 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/openfga/go.sum b/modules/openfga/go.sum index a2fe09d6b4..21cf8ea02c 100644 --- a/modules/openfga/go.sum +++ b/modules/openfga/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -54,6 +55,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -82,6 +87,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -93,6 +100,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -126,8 +135,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -151,14 +160,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -178,6 +187,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/openfga/openfga.go b/modules/openfga/openfga.go index ccdaab71cb..67f79b30e1 100644 --- a/modules/openfga/openfga.go +++ b/modules/openfga/openfga.go @@ -36,7 +36,7 @@ func (c *OpenFGAContainer) PlaygroundEndpoint(ctx context.Context) (string, erro return "", fmt.Errorf("failed to get playground endpoint: %w", err) } - return fmt.Sprintf("%s/playground", endpoint), nil + return endpoint + "/playground", nil } // Deprecated: use Run instead @@ -78,9 +78,14 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *OpenFGAContainer + if container != nil { + c = &OpenFGAContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &OpenFGAContainer{Container: container}, nil + return c, nil } diff --git a/modules/openfga/openfga_test.go b/modules/openfga/openfga_test.go index ec0a16bf1b..85e1966198 100644 --- a/modules/openfga/openfga_test.go +++ b/modules/openfga/openfga_test.go @@ -4,23 +4,18 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/openfga" ) func TestOpenFGA(t *testing.T) { ctx := context.Background() - container, err := openfga.Run(ctx, "openfga/openfga:v1.5.0") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := openfga.Run(ctx, "openfga/openfga:v1.5.0") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // perform assertions } diff --git a/modules/openldap/examples_test.go b/modules/openldap/examples_test.go index f3bf2f40f5..757385cc92 100644 --- a/modules/openldap/examples_test.go +++ b/modules/openldap/examples_test.go @@ -7,6 +7,7 @@ import ( "github.com/go-ldap/ldap/v3" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/openldap" ) @@ -15,21 +16,21 @@ func ExampleRun() { ctx := context.Background() openldapContainer, err := openldap.Run(ctx, "bitnami/openldap:2.6.6") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := openldapContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(openldapContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := openldapContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -43,32 +44,34 @@ func ExampleRun_connect() { ctx := context.Background() openldapContainer, err := openldap.Run(ctx, "bitnami/openldap:2.6.6") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := openldapContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(openldapContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } connectionString, err := openldapContainer.ConnectionString(ctx) if err != nil { - log.Fatalf("failed to get connection string: %s", err) // nolint:gocritic + log.Printf("failed to get connection string: %s", err) + return } client, err := ldap.DialURL(connectionString) if err != nil { - log.Fatalf("failed to connect to LDAP server: %s", err) + log.Printf("failed to connect to LDAP server: %s", err) + return } defer client.Close() // First bind with a read only user err = client.Bind("cn=admin,dc=example,dc=org", "adminpassword") if err != nil { - log.Fatalf("failed to bind to LDAP server: %s", err) + log.Printf("failed to bind to LDAP server: %s", err) + return } // Search for the given username @@ -82,11 +85,13 @@ func ExampleRun_connect() { sr, err := client.Search(searchRequest) if err != nil { - log.Fatalf("failed to search LDAP server: %s", err) + log.Printf("failed to search LDAP server: %s", err) + return } if len(sr.Entries) != 1 { - log.Fatal("User does not exist or too many entries returned") + log.Print("User does not exist or too many entries returned") + return } fmt.Println(sr.Entries[0].DN) diff --git a/modules/openldap/go.mod b/modules/openldap/go.mod index 2f13ed78cc..fcf3aa4677 100644 --- a/modules/openldap/go.mod +++ b/modules/openldap/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/go-ldap/ldap/v3 v3.4.6 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -16,7 +17,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -29,6 +31,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -40,6 +43,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -51,11 +55,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/openldap/go.sum b/modules/openldap/go.sum index 57de498077..35bec868a2 100644 --- a/modules/openldap/go.sum +++ b/modules/openldap/go.sum @@ -18,8 +18,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -61,6 +62,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -87,6 +92,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -98,6 +105,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -134,8 +143,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -171,23 +180,23 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -209,6 +218,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/openldap/openldap.go b/modules/openldap/openldap.go index dc215226c1..5a1bdf24f8 100644 --- a/modules/openldap/openldap.go +++ b/modules/openldap/openldap.go @@ -38,7 +38,7 @@ func (c *OpenLDAPContainer) ConnectionString(ctx context.Context, args ...string return "", err } - connStr := fmt.Sprintf("ldap://%s", net.JoinHostPort(host, containerPort.Port())) + connStr := "ldap://" + net.JoinHostPort(host, containerPort.Port()) return connStr, nil } @@ -161,14 +161,19 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *OpenLDAPContainer + if container != nil { + c = &OpenLDAPContainer{ + Container: container, + adminUsername: req.Env["LDAP_ADMIN_USERNAME"], + adminPassword: req.Env["LDAP_ADMIN_PASSWORD"], + rootDn: req.Env["LDAP_ROOT"], + } + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &OpenLDAPContainer{ - Container: container, - adminUsername: req.Env["LDAP_ADMIN_USERNAME"], - adminPassword: req.Env["LDAP_ADMIN_PASSWORD"], - rootDn: req.Env["LDAP_ROOT"], - }, nil + return c, nil } diff --git a/modules/openldap/openldap_test.go b/modules/openldap/openldap_test.go index 40639b9932..b73a8dce36 100644 --- a/modules/openldap/openldap_test.go +++ b/modules/openldap/openldap_test.go @@ -6,112 +6,70 @@ import ( "testing" "github.com/go-ldap/ldap/v3" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/openldap" ) func TestOpenLDAP(t *testing.T) { ctx := context.Background() - container, err := openldap.Run(ctx, "bitnami/openldap:2.6.6") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := openldap.Run(ctx, "bitnami/openldap:2.6.6") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) } func TestOpenLDAPWithAdminUsernameAndPassword(t *testing.T) { ctx := context.Background() - container, err := openldap.Run(ctx, + ctr, err := openldap.Run(ctx, "bitnami/openldap:2.6.6", openldap.WithAdminUsername("openldap"), openldap.WithAdminPassword("openldap"), ) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - connectionString, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + connectionString, err := ctr.ConnectionString(ctx) + require.NoError(t, err) client, err := ldap.DialURL(connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer client.Close() // First bind with a read only user err = client.Bind("cn=openldap,dc=example,dc=org", "openldap") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } func TestOpenLDAPWithDifferentRoot(t *testing.T) { ctx := context.Background() - container, err := openldap.Run(ctx, "bitnami/openldap:2.6.6", openldap.WithRoot("dc=mydomain,dc=com")) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := openldap.Run(ctx, "bitnami/openldap:2.6.6", openldap.WithRoot("dc=mydomain,dc=com")) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // connectionString { - connectionString, err := container.ConnectionString(ctx) + connectionString, err := ctr.ConnectionString(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) client, err := ldap.DialURL(connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer client.Close() // First bind with a read only user err = client.Bind("cn=admin,dc=mydomain,dc=com", "adminpassword") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } func TestOpenLDAPLoadLdif(t *testing.T) { ctx := context.Background() - container, err := openldap.Run(ctx, "bitnami/openldap:2.6.6") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := openldap.Run(ctx, "bitnami/openldap:2.6.6") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // loadLdif { ldif := ` @@ -124,28 +82,20 @@ mail: test.user@example.org userPassword: Password1 ` - err = container.LoadLdif(ctx, []byte(ldif)) + err = ctr.LoadLdif(ctx, []byte(ldif)) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - connectionString, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + connectionString, err := ctr.ConnectionString(ctx) + require.NoError(t, err) client, err := ldap.DialURL(connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer client.Close() // First bind with a read only user err = client.Bind("cn=admin,dc=example,dc=org", "adminpassword") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) result, err := client.Search(&ldap.SearchRequest{ BaseDN: "uid=test.user,ou=users,dc=example,dc=org", @@ -153,16 +103,9 @@ userPassword: Password1 Filter: "(objectClass=*)", Attributes: []string{"dn"}, }) - if err != nil { - t.Fatal(err) - } - - if len(result.Entries) != 1 { - t.Fatal("Invalid number of entries returned", result.Entries) - } - if result.Entries[0].DN != "uid=test.user,ou=users,dc=example,dc=org" { - t.Fatal("Invalid entry returned", result.Entries[0].DN) - } + require.NoError(t, err) + require.Len(t, result.Entries, 1) + require.Equal(t, "uid=test.user,ou=users,dc=example,dc=org", result.Entries[0].DN) } func TestOpenLDAPWithInitialLdif(t *testing.T) { @@ -178,47 +121,28 @@ userPassword: Password1 ` f, err := os.CreateTemp(t.TempDir(), "test.ldif") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = f.WriteString(ldif) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + err = f.Close() - if err != nil { - t.Fatal(err) - } - - container, err := openldap.Run(ctx, "bitnami/openldap:2.6.6", openldap.WithInitialLdif(f.Name())) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + require.NoError(t, err) - connectionString, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + ctr, err := openldap.Run(ctx, "bitnami/openldap:2.6.6", openldap.WithInitialLdif(f.Name())) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + connectionString, err := ctr.ConnectionString(ctx) + require.NoError(t, err) client, err := ldap.DialURL(connectionString) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer client.Close() // First bind with a read only user err = client.Bind("cn=admin,dc=example,dc=org", "adminpassword") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) result, err := client.Search(&ldap.SearchRequest{ BaseDN: "uid=test.user,ou=users,dc=example,dc=org", @@ -226,14 +150,8 @@ userPassword: Password1 Filter: "(objectClass=*)", Attributes: []string{"dn"}, }) - if err != nil { - t.Fatal(err) - } - - if len(result.Entries) != 1 { - t.Fatal("Invalid number of entries returned", result.Entries) - } - if result.Entries[0].DN != "uid=test.user,ou=users,dc=example,dc=org" { - t.Fatal("Invalid entry returned", result.Entries[0].DN) - } + require.NoError(t, err) + + require.Len(t, result.Entries, 1) + require.Equal(t, "uid=test.user,ou=users,dc=example,dc=org", result.Entries[0].DN) } diff --git a/modules/opensearch/examples_test.go b/modules/opensearch/examples_test.go index 89bc9cb7c6..d2e4c5807d 100644 --- a/modules/opensearch/examples_test.go +++ b/modules/opensearch/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/opensearch" ) @@ -18,21 +19,21 @@ func ExampleRun() { opensearch.WithUsername("new-username"), opensearch.WithPassword("new-password"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := opensearchContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(opensearchContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := opensearchContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/opensearch/go.mod b/modules/opensearch/go.mod index 50146f5964..0712f43830 100644 --- a/modules/opensearch/go.mod +++ b/modules/opensearch/go.mod @@ -5,7 +5,8 @@ go 1.22 require ( github.com/docker/docker v27.1.1+incompatible github.com/docker/go-units v0.5.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -16,7 +17,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -26,6 +28,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -37,6 +40,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -48,11 +52,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/opensearch/go.sum b/modules/opensearch/go.sum index 85338720c8..447eec4038 100644 --- a/modules/opensearch/go.sum +++ b/modules/opensearch/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,6 +53,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -78,6 +83,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -89,6 +96,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -122,8 +131,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -145,14 +154,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -172,6 +181,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/opensearch/opensearch.go b/modules/opensearch/opensearch.go index 83177c8471..fcc5a1f714 100644 --- a/modules/opensearch/opensearch.go +++ b/modules/opensearch/opensearch.go @@ -118,11 +118,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom }) container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *OpenSearchContainer + if container != nil { + c = &OpenSearchContainer{Container: container, User: username, Password: password} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &OpenSearchContainer{Container: container, User: username, Password: password}, nil + return c, nil } // Address retrieves the address of the OpenSearch container. diff --git a/modules/opensearch/opensearch_test.go b/modules/opensearch/opensearch_test.go index 64d0db37a5..15304dbfd6 100644 --- a/modules/opensearch/opensearch_test.go +++ b/modules/opensearch/opensearch_test.go @@ -5,41 +5,30 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/opensearch" ) func TestOpenSearch(t *testing.T) { ctx := context.Background() - container, err := opensearch.Run(ctx, "opensearchproject/opensearch:2.11.1") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := opensearch.Run(ctx, "opensearchproject/opensearch:2.11.1") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) t.Run("Connect to Address", func(t *testing.T) { - address, err := container.Address(ctx) - if err != nil { - t.Fatal(err) - } + address, err := ctr.Address(ctx) + require.NoError(t, err) client := &http.Client{} - req, err := http.NewRequest("GET", address, nil) - if err != nil { - t.Fatal(err) - } + req, err := http.NewRequest(http.MethodGet, address, nil) + require.NoError(t, err) resp, err := client.Do(req) - if err != nil { - t.Fatalf("failed to perform GET request: %s", err) - } + require.NoError(t, err) defer resp.Body.Close() }) } diff --git a/modules/postgres/examples_test.go b/modules/postgres/examples_test.go index d579068686..1f5b0e1c86 100644 --- a/modules/postgres/examples_test.go +++ b/modules/postgres/examples_test.go @@ -21,7 +21,7 @@ func ExampleRun() { dbPassword := "password" postgresContainer, err := postgres.Run(ctx, - "docker.io/postgres:16-alpine", + "postgres:16-alpine", postgres.WithInitScripts(filepath.Join("testdata", "init-user-db.sh")), postgres.WithConfigFile(filepath.Join("testdata", "my-postgres.conf")), postgres.WithDatabase(dbName), @@ -32,21 +32,21 @@ func ExampleRun() { WithOccurrence(2). WithStartupTimeout(5*time.Second)), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := postgresContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(postgresContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := postgresContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/postgres/go.mod b/modules/postgres/go.mod index 4b57997260..ac70814b3d 100644 --- a/modules/postgres/go.mod +++ b/modules/postgres/go.mod @@ -7,7 +7,7 @@ require ( github.com/jackc/pgx/v5 v5.5.4 github.com/lib/pq v1.10.9 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) @@ -19,7 +19,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -58,11 +58,11 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/postgres/go.sum b/modules/postgres/go.sum index 4293379353..e12245f686 100644 --- a/modules/postgres/go.sum +++ b/modules/postgres/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -106,6 +106,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -140,8 +142,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -153,8 +155,8 @@ golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -165,14 +167,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/postgres/options.go b/modules/postgres/options.go index ad24c79fc3..5779f85c04 100644 --- a/modules/postgres/options.go +++ b/modules/postgres/options.go @@ -7,11 +7,13 @@ import ( type options struct { // SQLDriverName is the name of the SQL driver to use. SQLDriverName string + Snapshot string } func defaultOptions() options { return options{ SQLDriverName: "postgres", + Snapshot: defaultSnapshotName, } } diff --git a/modules/postgres/postgres.go b/modules/postgres/postgres.go index 0ab4d889d1..ce3719575d 100644 --- a/modules/postgres/postgres.go +++ b/modules/postgres/postgres.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "errors" "fmt" "io" "net" @@ -136,7 +137,7 @@ func WithUsername(user string) testcontainers.CustomizeRequestOption { // Deprecated: use Run instead // RunContainer creates an instance of the Postgres container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*PostgresContainer, error) { - return Run(ctx, "docker.io/postgres:16-alpine", opts...) + return Run(ctx, "postgres:16-alpine", opts...) } // Run creates an instance of the Postgres container type @@ -169,15 +170,23 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) - if err != nil { - return nil, err + var c *PostgresContainer + if container != nil { + c = &PostgresContainer{ + Container: container, + dbName: req.Env["POSTGRES_DB"], + password: req.Env["POSTGRES_PASSWORD"], + user: req.Env["POSTGRES_USER"], + sqlDriverName: settings.SQLDriverName, + snapshotName: settings.Snapshot, + } } - user := req.Env["POSTGRES_USER"] - password := req.Env["POSTGRES_PASSWORD"] - dbName := req.Env["POSTGRES_DB"] + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } - return &PostgresContainer{Container: container, dbName: dbName, password: password, user: user, sqlDriverName: settings.SQLDriverName}, nil + return c, nil } type snapshotConfig struct { @@ -208,7 +217,10 @@ func (c *PostgresContainer) Snapshot(ctx context.Context, opts ...SnapshotOption // execute the commands to create the snapshot, in order if err := c.execCommandsSQL(ctx, - // Drop the snapshot database if it already exists + // Update pg_database to remove the template flag, then drop the database if it exists. + // This is needed because dropping a template database will fail. + // https://www.postgresql.org/docs/current/manage-ag-templatedbs.html + fmt.Sprintf(`UPDATE pg_database SET datistemplate = FALSE WHERE datname = '%s'`, snapshotName), fmt.Sprintf(`DROP DATABASE IF EXISTS "%s"`, snapshotName), // Create a copy of the database to another database to use as a template now that it was fully migrated fmt.Sprintf(`CREATE DATABASE "%s" WITH TEMPLATE "%s" OWNER "%s"`, snapshotName, c.dbName, c.user), @@ -251,7 +263,7 @@ func (c *PostgresContainer) checkSnapshotConfig(opts []SnapshotOption) (string, } if c.dbName == "postgres" { - return "", fmt.Errorf("cannot restore the postgres system database as it cannot be dropped to be restored") + return "", errors.New("cannot restore the postgres system database as it cannot be dropped to be restored") } return snapshotName, nil } diff --git a/modules/postgres/postgres_test.go b/modules/postgres/postgres_test.go index adef0defe3..8c02e68476 100644 --- a/modules/postgres/postgres_test.go +++ b/modules/postgres/postgres_test.go @@ -3,7 +3,6 @@ package postgres_test import ( "context" "database/sql" - "errors" "fmt" "path/filepath" "testing" @@ -13,7 +12,6 @@ import ( "github.com/jackc/pgx/v5" _ "github.com/jackc/pgx/v5/stdlib" _ "github.com/lib/pq" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" @@ -36,77 +34,67 @@ func TestPostgres(t *testing.T) { }{ { name: "Postgres", - image: "docker.io/postgres:15.2-alpine", + image: "postgres:15.2-alpine", }, { name: "Timescale", // timescale { - image: "docker.io/timescale/timescaledb:2.1.0-pg11", + image: "timescale/timescaledb:2.1.0-pg11", // } }, { name: "Postgis", // postgis { - image: "docker.io/postgis/postgis:12-3.0", + image: "postgis/postgis:12-3.0", // } }, { name: "Pgvector", // pgvector { - image: "docker.io/pgvector/pgvector:pg16", + image: "pgvector/pgvector:pg16", // } }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - container, err := postgres.Run(ctx, + ctr, err := postgres.Run(ctx, tt.image, postgres.WithDatabase(dbname), postgres.WithUsername(user), postgres.WithPassword(password), postgres.BasicWaitStrategies(), ) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // connectionString { // explicitly set sslmode=disable because the container is not configured to use TLS - connStr, err := container.ConnectionString(ctx, "sslmode=disable", "application_name=test") + connStr, err := ctr.ConnectionString(ctx, "sslmode=disable", "application_name=test") // } require.NoError(t, err) - mustConnStr := container.MustConnectionString(ctx, "sslmode=disable", "application_name=test") - if mustConnStr != connStr { - t.Errorf("ConnectionString was not equal to MustConnectionString") - } + mustConnStr := ctr.MustConnectionString(ctx, "sslmode=disable", "application_name=test") + require.Equalf(t, mustConnStr, connStr, "ConnectionString was not equal to MustConnectionString") // Ensure connection string is using generic format - id, err := container.MappedPort(ctx, "5432/tcp") + id, err := ctr.MappedPort(ctx, "5432/tcp") require.NoError(t, err) - assert.Equal(t, fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable&application_name=test", user, password, "localhost", id.Port(), dbname), connStr) + require.Equal(t, fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable&application_name=test", user, password, "localhost", id.Port(), dbname), connStr) // perform assertions db, err := sql.Open("postgres", connStr) require.NoError(t, err) - assert.NotNil(t, db) + require.NotNil(t, db) defer db.Close() result, err := db.Exec("CREATE TABLE IF NOT EXISTS test (id int, name varchar(255));") require.NoError(t, err) - assert.NotNil(t, result) + require.NotNil(t, result) result, err = db.Exec("INSERT INTO test (id, name) VALUES (1, 'test');") require.NoError(t, err) - assert.NotNil(t, result) + require.NotNil(t, result) }) } } @@ -120,210 +108,188 @@ func TestContainerWithWaitForSQL(t *testing.T) { } t.Run("default query", func(t *testing.T) { - container, err := postgres.Run( + ctr, err := postgres.Run( ctx, - "docker.io/postgres:16-alpine", + "postgres:16-alpine", postgres.WithDatabase(dbname), postgres.WithUsername(user), postgres.WithPassword(password), testcontainers.WithWaitStrategy(wait.ForSQL(nat.Port(port), "postgres", dbURL)), ) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - require.NotNil(t, container) + require.NotNil(t, ctr) }) t.Run("custom query", func(t *testing.T) { - container, err := postgres.Run( + ctr, err := postgres.Run( ctx, - "docker.io/postgres:16-alpine", + "postgres:16-alpine", postgres.WithDatabase(dbname), postgres.WithUsername(user), postgres.WithPassword(password), testcontainers.WithWaitStrategy(wait.ForSQL(nat.Port(port), "postgres", dbURL).WithStartupTimeout(time.Second*5).WithQuery("SELECT 10")), ) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - require.NotNil(t, container) + require.NotNil(t, ctr) }) t.Run("custom bad query", func(t *testing.T) { - container, err := postgres.Run( + ctr, err := postgres.Run( ctx, - "docker.io/postgres:16-alpine", + "postgres:16-alpine", postgres.WithDatabase(dbname), postgres.WithUsername(user), postgres.WithPassword(password), testcontainers.WithWaitStrategy(wait.ForSQL(nat.Port(port), "postgres", dbURL).WithStartupTimeout(time.Second*5).WithQuery("SELECT 'a' from b")), ) + testcontainers.CleanupContainer(t, ctr) require.Error(t, err) - require.Nil(t, container) }) } func TestWithConfigFile(t *testing.T) { ctx := context.Background() - container, err := postgres.Run(ctx, - "docker.io/postgres:16-alpine", + ctr, err := postgres.Run(ctx, + "postgres:16-alpine", postgres.WithConfigFile(filepath.Join("testdata", "my-postgres.conf")), postgres.WithDatabase(dbname), postgres.WithUsername(user), postgres.WithPassword(password), postgres.BasicWaitStrategies(), ) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // explicitly set sslmode=disable because the container is not configured to use TLS - connStr, err := container.ConnectionString(ctx, "sslmode=disable") + connStr, err := ctr.ConnectionString(ctx, "sslmode=disable") require.NoError(t, err) db, err := sql.Open("postgres", connStr) require.NoError(t, err) - assert.NotNil(t, db) + require.NotNil(t, db) defer db.Close() } func TestWithInitScript(t *testing.T) { ctx := context.Background() - container, err := postgres.Run(ctx, - "docker.io/postgres:15.2-alpine", + ctr, err := postgres.Run(ctx, + "postgres:15.2-alpine", postgres.WithInitScripts(filepath.Join("testdata", "init-user-db.sh")), postgres.WithDatabase(dbname), postgres.WithUsername(user), postgres.WithPassword(password), postgres.BasicWaitStrategies(), ) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // explicitly set sslmode=disable because the container is not configured to use TLS - connStr, err := container.ConnectionString(ctx, "sslmode=disable") + connStr, err := ctr.ConnectionString(ctx, "sslmode=disable") require.NoError(t, err) db, err := sql.Open("postgres", connStr) require.NoError(t, err) - assert.NotNil(t, db) + require.NotNil(t, db) defer db.Close() // database created in init script. See testdata/init-user-db.sh result, err := db.Exec("SELECT * FROM testdb;") require.NoError(t, err) - assert.NotNil(t, result) + require.NotNil(t, result) } func TestSnapshot(t *testing.T) { - // snapshotAndReset { - ctx := context.Background() + tests := []struct { + name string + options []postgres.SnapshotOption + }{ + { + name: "snapshot/default", + options: nil, + }, - // 1. Start the postgres container and run any migrations on it - container, err := postgres.Run( - ctx, - "docker.io/postgres:16-alpine", - postgres.WithDatabase(dbname), - postgres.WithUsername(user), - postgres.WithPassword(password), - postgres.BasicWaitStrategies(), - postgres.WithSQLDriver("pgx"), - ) - if err != nil { - t.Fatal(err) + { + name: "snapshot/custom", + options: []postgres.SnapshotOption{ + postgres.WithSnapshotName("custom-snapshot"), + }, + }, } - // Run any migrations on the database - _, _, err = container.Exec(ctx, []string{"psql", "-U", user, "-d", dbname, "-c", "CREATE TABLE users (id SERIAL, name TEXT NOT NULL, age INT NOT NULL)"}) - if err != nil { - t.Fatal(err) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // snapshotAndReset { + ctx := context.Background() - // 2. Create a snapshot of the database to restore later - err = container.Snapshot(ctx, postgres.WithSnapshotName("test-snapshot")) - if err != nil { - t.Fatal(err) - } + // 1. Start the postgres ctr and run any migrations on it + ctr, err := postgres.Run( + ctx, + "postgres:16-alpine", + postgres.WithDatabase(dbname), + postgres.WithUsername(user), + postgres.WithPassword(password), + postgres.BasicWaitStrategies(), + postgres.WithSQLDriver("pgx"), + ) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + // Run any migrations on the database + _, _, err = ctr.Exec(ctx, []string{"psql", "-U", user, "-d", dbname, "-c", "CREATE TABLE users (id SERIAL, name TEXT NOT NULL, age INT NOT NULL)"}) + require.NoError(t, err) - dbURL, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + // 2. Create a snapshot of the database to restore later + // tt.options comes the test case, it can be specified as e.g. `postgres.WithSnapshotName("custom-snapshot")` or omitted, to use default name + err = ctr.Snapshot(ctx, tt.options...) + require.NoError(t, err) - t.Run("Test inserting a user", func(t *testing.T) { - t.Cleanup(func() { - // 3. In each test, reset the DB to its snapshot state. - err = container.Restore(ctx) - if err != nil { - t.Fatal(err) - } - }) + dbURL, err := ctr.ConnectionString(ctx) + require.NoError(t, err) - conn, err := pgx.Connect(context.Background(), dbURL) - if err != nil { - t.Fatal(err) - } - defer conn.Close(context.Background()) + t.Run("Test inserting a user", func(t *testing.T) { + t.Cleanup(func() { + // 3. In each test, reset the DB to its snapshot state. + err = ctr.Restore(ctx) + require.NoError(t, err) + }) - _, err = conn.Exec(ctx, "INSERT INTO users(name, age) VALUES ($1, $2)", "test", 42) - if err != nil { - t.Fatal(err) - } + conn, err := pgx.Connect(context.Background(), dbURL) + require.NoError(t, err) + defer conn.Close(context.Background()) - var name string - var age int64 - err = conn.QueryRow(context.Background(), "SELECT name, age FROM users LIMIT 1").Scan(&name, &age) - if err != nil { - t.Fatal(err) - } - - if name != "test" { - t.Fatalf("Expected %s to equal `test`", name) - } - if age != 42 { - t.Fatalf("Expected %d to equal `42`", age) - } - }) + _, err = conn.Exec(ctx, "INSERT INTO users(name, age) VALUES ($1, $2)", "test", 42) + require.NoError(t, err) - // 4. Run as many tests as you need, they will each get a clean database - t.Run("Test querying empty DB", func(t *testing.T) { - t.Cleanup(func() { - err = container.Restore(ctx) - if err != nil { - t.Fatal(err) - } - }) + var name string + var age int64 + err = conn.QueryRow(context.Background(), "SELECT name, age FROM users LIMIT 1").Scan(&name, &age) + require.NoError(t, err) - conn, err := pgx.Connect(context.Background(), dbURL) - if err != nil { - t.Fatal(err) - } - defer conn.Close(context.Background()) + require.Equal(t, "test", name) + require.EqualValues(t, 42, age) + }) - var name string - var age int64 - err = conn.QueryRow(context.Background(), "SELECT name, age FROM users LIMIT 1").Scan(&name, &age) - if !errors.Is(err, pgx.ErrNoRows) { - t.Fatalf("Expected error to be a NoRows error, since the DB should be empty on every test. Got %s instead", err) - } - }) - // } + // 4. Run as many tests as you need, they will each get a clean database + t.Run("Test querying empty DB", func(t *testing.T) { + t.Cleanup(func() { + err = ctr.Restore(ctx) + require.NoError(t, err) + }) + + conn, err := pgx.Connect(context.Background(), dbURL) + require.NoError(t, err) + defer conn.Close(context.Background()) + + var name string + var age int64 + err = conn.QueryRow(context.Background(), "SELECT name, age FROM users LIMIT 1").Scan(&name, &age) + require.ErrorIs(t, err, pgx.ErrNoRows) + }) + // } + }) + } } func TestSnapshotWithOverrides(t *testing.T) { @@ -333,69 +299,73 @@ func TestSnapshotWithOverrides(t *testing.T) { user := "other-user" password := "other-password" - container, err := postgres.Run( + ctr, err := postgres.Run( ctx, - "docker.io/postgres:16-alpine", + "postgres:16-alpine", postgres.WithDatabase(dbname), postgres.WithUsername(user), postgres.WithPassword(password), postgres.BasicWaitStrategies(), ) - if err != nil { - t.Fatal(err) - } - - _, _, err = container.Exec(ctx, []string{"psql", "-U", user, "-d", dbname, "-c", "CREATE TABLE users (id SERIAL, name TEXT NOT NULL, age INT NOT NULL)"}) - if err != nil { - t.Fatal(err) - } - - err = container.Snapshot(ctx, postgres.WithSnapshotName("other-snapshot")) - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + _, _, err = ctr.Exec(ctx, []string{"psql", "-U", user, "-d", dbname, "-c", "CREATE TABLE users (id SERIAL, name TEXT NOT NULL, age INT NOT NULL)"}) + require.NoError(t, err) + err = ctr.Snapshot(ctx, postgres.WithSnapshotName("other-snapshot")) + require.NoError(t, err) - dbURL, err := container.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + dbURL, err := ctr.ConnectionString(ctx) + require.NoError(t, err) t.Run("Test that the restore works when not using defaults", func(t *testing.T) { - _, _, err = container.Exec(ctx, []string{"psql", "-U", user, "-d", dbname, "-c", "INSERT INTO users(name, age) VALUES ('test', 42)"}) - if err != nil { - t.Fatal(err) - } + _, _, err = ctr.Exec(ctx, []string{"psql", "-U", user, "-d", dbname, "-c", "INSERT INTO users(name, age) VALUES ('test', 42)"}) + require.NoError(t, err) // Doing the restore before we connect since this resets the pgx connection - err = container.Restore(ctx) - if err != nil { - t.Fatal(err) - } + err = ctr.Restore(ctx) + require.NoError(t, err) conn, err := pgx.Connect(context.Background(), dbURL) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer conn.Close(context.Background()) var count int64 err = conn.QueryRow(context.Background(), "SELECT COUNT(1) FROM users").Scan(&count) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if count != 0 { - t.Fatalf("Expected %d to equal `0`", count) - } + require.Zero(t, count) }) } +func TestSnapshotDuplicate(t *testing.T) { + ctx := context.Background() + + dbname := "other-db" + user := "other-user" + password := "other-password" + + ctr, err := postgres.Run( + ctx, + "postgres:16-alpine", + postgres.WithDatabase(dbname), + postgres.WithUsername(user), + postgres.WithPassword(password), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + _, _, err = ctr.Exec(ctx, []string{"psql", "-U", user, "-d", dbname, "-c", "CREATE TABLE users (id SERIAL, name TEXT NOT NULL, age INT NOT NULL)"}) + require.NoError(t, err) + + err = ctr.Snapshot(ctx, postgres.WithSnapshotName("other-snapshot")) + require.NoError(t, err) + + err = ctr.Snapshot(ctx, postgres.WithSnapshotName("other-snapshot")) + require.NoError(t, err) +} + func TestSnapshotWithDockerExecFallback(t *testing.T) { ctx := context.Background() @@ -403,7 +373,7 @@ func TestSnapshotWithDockerExecFallback(t *testing.T) { // 1. Start the postgres container and run any migrations on it ctr, err := postgres.Run( ctx, - "docker.io/postgres:16-alpine", + "postgres:16-alpine", postgres.WithDatabase(dbname), postgres.WithUsername(user), postgres.WithPassword(password), @@ -413,90 +383,58 @@ func TestSnapshotWithDockerExecFallback(t *testing.T) { postgres.WithSQLDriver("DoesNotExist"), ) // } - if err != nil { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // Run any migrations on the database _, _, err = ctr.Exec(ctx, []string{"psql", "-U", user, "-d", dbname, "-c", "CREATE TABLE users (id SERIAL, name TEXT NOT NULL, age INT NOT NULL)"}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // 2. Create a snapshot of the database to restore later err = ctr.Snapshot(ctx, postgres.WithSnapshotName("test-snapshot")) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := ctr.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + require.NoError(t, err) dbURL, err := ctr.ConnectionString(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) t.Run("Test inserting a user", func(t *testing.T) { t.Cleanup(func() { // 3. In each test, reset the DB to its snapshot state. err := ctr.Restore(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) }) conn, err2 := pgx.Connect(context.Background(), dbURL) - if err2 != nil { - t.Fatal(err2) - } + require.NoError(t, err2) defer conn.Close(context.Background()) _, err2 = conn.Exec(ctx, "INSERT INTO users(name, age) VALUES ($1, $2)", "test", 42) - if err2 != nil { - t.Fatal(err2) - } + require.NoError(t, err2) var name string var age int64 err2 = conn.QueryRow(context.Background(), "SELECT name, age FROM users LIMIT 1").Scan(&name, &age) - if err2 != nil { - t.Fatal(err2) - } - - if name != "test" { - t.Fatalf("Expected %s to equal `test`", name) - } - if age != 42 { - t.Fatalf("Expected %d to equal `42`", age) - } + require.NoError(t, err2) + + require.Equal(t, "test", name) + require.EqualValues(t, 42, age) }) t.Run("Test querying empty DB", func(t *testing.T) { // 4. Run as many tests as you need, they will each get a clean database t.Cleanup(func() { err := ctr.Restore(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) }) conn, err2 := pgx.Connect(context.Background(), dbURL) - if err2 != nil { - t.Fatal(err2) - } + require.NoError(t, err2) defer conn.Close(context.Background()) var name string var age int64 err2 = conn.QueryRow(context.Background(), "SELECT name, age FROM users LIMIT 1").Scan(&name, &age) - if !errors.Is(err2, pgx.ErrNoRows) { - t.Fatalf("Expected error to be a NoRows error, since the DB should be empty on every test. Got %s instead", err2) - } + require.ErrorIs(t, err2, pgx.ErrNoRows) }) // } } diff --git a/modules/pulsar/examples_test.go b/modules/pulsar/examples_test.go index 2ee1f3c38e..9561914207 100644 --- a/modules/pulsar/examples_test.go +++ b/modules/pulsar/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/pulsar" ) @@ -12,22 +13,22 @@ func ExampleRun() { // runPulsarContainer { ctx := context.Background() - pulsarContainer, err := pulsar.Run(ctx, "docker.io/apachepulsar/pulsar:2.10.2") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container + pulsarContainer, err := pulsar.Run(ctx, "apachepulsar/pulsar:2.10.2") defer func() { - if err := pulsarContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(pulsarContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := pulsarContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/pulsar/go.mod b/modules/pulsar/go.mod index b9e77f4d58..e65e5f8da4 100644 --- a/modules/pulsar/go.mod +++ b/modules/pulsar/go.mod @@ -7,7 +7,7 @@ require ( github.com/docker/docker v27.1.1+incompatible github.com/docker/go-connections v0.5.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) replace github.com/testcontainers/testcontainers-go => ../.. @@ -28,7 +28,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect @@ -80,12 +80,12 @@ require ( go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/atomic v1.7.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect diff --git a/modules/pulsar/go.sum b/modules/pulsar/go.sum index e8aa08578a..0d29f7e91b 100644 --- a/modules/pulsar/go.sum +++ b/modules/pulsar/go.sum @@ -84,8 +84,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -389,8 +389,8 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -530,12 +530,12 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -544,8 +544,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/modules/pulsar/pulsar.go b/modules/pulsar/pulsar.go index c1c5c94517..be9ea10925 100644 --- a/modules/pulsar/pulsar.go +++ b/modules/pulsar/pulsar.go @@ -132,12 +132,12 @@ func WithTransactions() testcontainers.CustomizeRequestOption { // Deprecated: use Run instead // RunContainer creates an instance of the Pulsar container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*Container, error) { - return Run(ctx, "docker.io/apachepulsar/pulsar:2.10.2", opts...) + return Run(ctx, "apachepulsar/pulsar:2.10.2", opts...) } // Run creates an instance of the Pulsar container type, being possible to pass a custom request and options // The created container will use the following defaults: -// - image: docker.io/apachepulsar/pulsar:2.10.2 +// - image: apachepulsar/pulsar:2.10.2 // - exposed ports: 6650/tcp, 8080/tcp // - waiting strategy: wait for all the following strategies: // - the Pulsar admin API ("/admin/v2/clusters") to be ready on port 8080/tcp and return the response `["standalone"]` @@ -164,14 +164,15 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } } - c, err := testcontainers.GenericContainer(ctx, genericContainerReq) - if err != nil { - return nil, err + container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *Container + if container != nil { + c = &Container{Container: container} } - pc := &Container{ - Container: c, + if err != nil { + return c, fmt.Errorf("generic container: %w", err) } - return pc, nil + return c, nil } diff --git a/modules/pulsar/pulsar_test.go b/modules/pulsar/pulsar_test.go index 6d911bf9e5..de2c4bd437 100644 --- a/modules/pulsar/pulsar_test.go +++ b/modules/pulsar/pulsar_test.go @@ -3,7 +3,6 @@ package pulsar_test import ( "context" "encoding/json" - "fmt" "io" "net/http" "strings" @@ -11,6 +10,7 @@ import ( "time" "github.com/apache/pulsar-client-go/pulsar" + "github.com/apache/pulsar-client-go/pulsar/log" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/stretchr/testify/assert" @@ -21,12 +21,20 @@ import ( tcnetwork "github.com/testcontainers/testcontainers-go/network" ) +// noopLogConsumer implements testcontainers.LogConsumer +// and does nothing with the logs. +type noopLogConsumer struct{} + +// Accept implements testcontainers.LogConsumer. +func (*noopLogConsumer) Accept(testcontainers.Log) {} + func TestPulsar(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() nw, err := tcnetwork.New(ctx) require.NoError(t, err) + testcontainers.CleanupNetwork(t, nw) nwName := nw.Name @@ -80,7 +88,7 @@ func TestPulsar(t *testing.T) { name: "with log consumers", opts: []testcontainers.ContainerCustomizer{ // withLogconsumers { - testcontainers.WithLogConsumers(&testcontainers.StdoutLogConsumer{}), + testcontainers.WithLogConsumers(&noopLogConsumer{}), // } }, }, @@ -90,14 +98,11 @@ func TestPulsar(t *testing.T) { t.Run(tt.name, func(t *testing.T) { c, err := testcontainerspulsar.Run( ctx, - "docker.io/apachepulsar/pulsar:2.10.2", + "apachepulsar/pulsar:2.10.2", tt.opts..., ) + testcontainers.CleanupContainer(t, c) require.NoError(t, err) - defer func() { - err := c.Terminate(ctx) - require.NoError(t, err) - }() // getBrokerURL { brokerURL, err := c.BrokerURL(ctx) @@ -116,6 +121,7 @@ func TestPulsar(t *testing.T) { URL: brokerURL, OperationTimeout: 30 * time.Second, ConnectionTimeout: 30 * time.Second, + Logger: log.DefaultNopLogger(), }) require.NoError(t, err) t.Cleanup(func() { pc.Close() }) @@ -134,13 +140,13 @@ func TestPulsar(t *testing.T) { go func() { msg, err := consumer.Receive(ctx) if err != nil { - fmt.Println("failed to receive message", err) + t.Log("failed to receive message", err) return } msgChan <- msg.Payload() err = consumer.Ack(msg) if err != nil { - fmt.Println("failed to send ack", err) + t.Log("failed to send ack", err) return } }() @@ -160,9 +166,7 @@ func TestPulsar(t *testing.T) { case <-ticker.C: t.Fatal("did not receive message in time") case msg := <-msgChan: - if string(msg) != "hello world" { - t.Fatal("received unexpected message bytes") - } + require.Equalf(t, "hello world", string(msg), "received unexpected message bytes") } // get topic statistics using the Admin endpoint @@ -188,14 +192,7 @@ func TestPulsar(t *testing.T) { // check that the subscription exists _, ok := subscriptionsMap[subscriptionName] - assert.True(t, ok) + require.True(t, ok) }) } - - // remove the network after the last, so that all containers are already removed - // and there are no active endpoints on the network - t.Cleanup(func() { - err := nw.Remove(context.Background()) - require.NoError(t, err) - }) } diff --git a/modules/qdrant/examples_test.go b/modules/qdrant/examples_test.go index eca3adb4ad..85bd2763de 100644 --- a/modules/qdrant/examples_test.go +++ b/modules/qdrant/examples_test.go @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/qdrant" ) @@ -18,21 +19,21 @@ func ExampleRun() { ctx := context.Background() qdrantContainer, err := qdrant.Run(ctx, "qdrant/qdrant:v1.7.4") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := qdrantContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(qdrantContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := qdrantContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -44,25 +45,27 @@ func ExampleRun() { func ExampleRun_createPoints() { // fullExample { qdrantContainer, err := qdrant.Run(context.Background(), "qdrant/qdrant:v1.7.4") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := qdrantContainer.Terminate(context.Background()); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(qdrantContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } grpcEndpoint, err := qdrantContainer.GRPCEndpoint(context.Background()) if err != nil { - log.Fatalf("failed to get gRPC endpoint: %s", err) // nolint:gocritic + log.Printf("failed to get gRPC endpoint: %s", err) + return } // Set up a connection to the server. conn, err := grpc.NewClient(grpcEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - log.Fatalf("did not connect: %v", err) + log.Printf("did not connect: %v", err) + return } defer conn.Close() @@ -89,7 +92,8 @@ func ExampleRun_createPoints() { }, }) if err != nil { - log.Fatalf("Could not create collection: %v", err) + log.Printf("Could not create collection: %v", err) + return } // 2. Contact the server and print out its response. @@ -97,7 +101,8 @@ func ExampleRun_createPoints() { defer cancel() r, err := collections_client.List(ctx, &pb.ListCollectionsRequest{}) if err != nil { - log.Fatalf("could not get collections: %v", err) + log.Printf("could not get collections: %v", err) + return } fmt.Printf("List of collections: %s\n", r.GetCollections()) @@ -113,7 +118,8 @@ func ExampleRun_createPoints() { FieldType: &fieldIndex1Type, }) if err != nil { - log.Fatalf("Could not create field index: %v", err) + log.Printf("Could not create field index: %v", err) + return } // 5. Create integer field index @@ -125,7 +131,8 @@ func ExampleRun_createPoints() { FieldType: &fieldIndex2Type, }) if err != nil { - log.Fatalf("Could not create field index: %v", err) + log.Printf("Could not create field index: %v", err) + return } // 6. Upsert points @@ -258,7 +265,8 @@ func ExampleRun_createPoints() { Points: upsertPoints, }) if err != nil { - log.Fatalf("Could not upsert points: %v", err) + log.Printf("Could not upsert points: %v", err) + return } // 7. Retrieve points by ids @@ -270,7 +278,8 @@ func ExampleRun_createPoints() { }, }) if err != nil { - log.Fatalf("Could not retrieve points: %v", err) + log.Printf("Could not retrieve points: %v", err) + return } fmt.Printf("Retrieved points: %d\n", len(pointsById.GetResult())) @@ -285,7 +294,8 @@ func ExampleRun_createPoints() { WithPayload: &pb.WithPayloadSelector{SelectorOptions: &pb.WithPayloadSelector_Enable{Enable: true}}, }) if err != nil { - log.Fatalf("Could not search points: %v", err) + log.Printf("Could not search points: %v", err) + return } fmt.Printf("Found points: %d\n", len(unfilteredSearchResult.GetResult())) @@ -313,7 +323,8 @@ func ExampleRun_createPoints() { }, }) if err != nil { - log.Fatalf("Could not search points: %v", err) + log.Printf("Could not search points: %v", err) + return } // } diff --git a/modules/qdrant/go.mod b/modules/qdrant/go.mod index 85db7229b3..d5b28b795a 100644 --- a/modules/qdrant/go.mod +++ b/modules/qdrant/go.mod @@ -4,7 +4,8 @@ go 1.22 require ( github.com/qdrant/go-client v1.7.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 google.golang.org/grpc v1.64.1 ) @@ -16,7 +17,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -28,6 +30,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -39,6 +42,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -50,12 +54,13 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/qdrant/go.sum b/modules/qdrant/go.sum index c4425e8182..98e982016f 100644 --- a/modules/qdrant/go.sum +++ b/modules/qdrant/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,6 +53,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -80,6 +85,8 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/qdrant/go-client v1.7.0 h1:2TeeWyZAWIup7vvD7Ne6aAvo0H+F5OUb1pB9Z8Y4pFk= github.com/qdrant/go-client v1.7.0/go.mod h1:680gkxNAsVtre0Z8hAQmtPzJtz1xFAyCu2TUxULtnoE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -91,6 +98,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -124,8 +133,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -147,14 +156,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -175,6 +184,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/qdrant/qdrant.go b/modules/qdrant/qdrant.go index e934403f96..d4eb11b95a 100644 --- a/modules/qdrant/qdrant.go +++ b/modules/qdrant/qdrant.go @@ -2,6 +2,7 @@ package qdrant import ( "context" + "errors" "fmt" "time" @@ -43,11 +44,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *QdrantContainer + if container != nil { + c = &QdrantContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &QdrantContainer{Container: container}, nil + return c, nil } // RESTEndpoint returns the REST endpoint of the Qdrant container @@ -59,7 +65,7 @@ func (c *QdrantContainer) RESTEndpoint(ctx context.Context) (string, error) { host, err := c.Host(ctx) if err != nil { - return "", fmt.Errorf("failed to get container host") + return "", errors.New("failed to get container host") } return fmt.Sprintf("http://%s:%s", host, containerPort.Port()), nil @@ -74,7 +80,7 @@ func (c *QdrantContainer) GRPCEndpoint(ctx context.Context) (string, error) { host, err := c.Host(ctx) if err != nil { - return "", fmt.Errorf("failed to get container host") + return "", errors.New("failed to get container host") } return fmt.Sprintf("%s:%s", host, containerPort.Port()), nil diff --git a/modules/qdrant/qdrant_test.go b/modules/qdrant/qdrant_test.go index 2b9bcdbb75..63b95d30ea 100644 --- a/modules/qdrant/qdrant_test.go +++ b/modules/qdrant/qdrant_test.go @@ -5,79 +5,57 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/qdrant" ) func TestQdrant(t *testing.T) { ctx := context.Background() - container, err := qdrant.Run(ctx, "qdrant/qdrant:v1.7.4") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := qdrant.Run(ctx, "qdrant/qdrant:v1.7.4") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) t.Run("REST Endpoint", func(tt *testing.T) { // restEndpoint { - restEndpoint, err := container.RESTEndpoint(ctx) + restEndpoint, err := ctr.RESTEndpoint(ctx) // } - if err != nil { - tt.Fatalf("failed to get REST endpoint: %s", err) - } + require.NoError(t, err) cli := &http.Client{} resp, err := cli.Get(restEndpoint) - if err != nil { - tt.Fatalf("failed to perform GET request: %s", err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - tt.Fatalf("unexpected status code: %d", resp.StatusCode) - } + require.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("gRPC Endpoint", func(tt *testing.T) { // gRPCEndpoint { - grpcEndpoint, err := container.GRPCEndpoint(ctx) + grpcEndpoint, err := ctr.GRPCEndpoint(ctx) // } - if err != nil { - tt.Fatalf("failed to get REST endpoint: %s", err) - } + require.NoError(t, err) conn, err := grpc.NewClient(grpcEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("did not connect: %v", err) - } + require.NoError(t, err) defer conn.Close() }) t.Run("Web UI", func(tt *testing.T) { // webUIEndpoint { - webUI, err := container.WebUI(ctx) + webUI, err := ctr.WebUI(ctx) // } - if err != nil { - tt.Fatalf("failed to get REST endpoint: %s", err) - } + require.NoError(t, err) cli := &http.Client{} resp, err := cli.Get(webUI) - if err != nil { - tt.Fatalf("failed to perform GET request: %s", err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - tt.Fatalf("unexpected status code: %d", resp.StatusCode) - } + require.Equal(t, http.StatusOK, resp.StatusCode) }) } diff --git a/modules/rabbitmq/examples_test.go b/modules/rabbitmq/examples_test.go index 4471d512d4..bc6a849456 100644 --- a/modules/rabbitmq/examples_test.go +++ b/modules/rabbitmq/examples_test.go @@ -24,21 +24,21 @@ func ExampleRun() { rabbitmq.WithAdminUsername("admin"), rabbitmq.WithAdminPassword("password"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := rabbitmqContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(rabbitmqContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := rabbitmqContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -55,28 +55,31 @@ func ExampleRun_connectUsingAmqp() { rabbitmq.WithAdminUsername("admin"), rabbitmq.WithAdminPassword("password"), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - if err := rabbitmqContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(rabbitmqContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } amqpURL, err := rabbitmqContainer.AmqpURL(ctx) if err != nil { - log.Fatalf("failed to get AMQP URL: %s", err) // nolint:gocritic + log.Printf("failed to get AMQP URL: %s", err) + return } amqpConnection, err := amqp.Dial(amqpURL) if err != nil { - log.Fatalf("failed to connect to RabbitMQ: %s", err) // nolint:gocritic + log.Printf("failed to connect to RabbitMQ: %s", err) + return } defer func() { err := amqpConnection.Close() if err != nil { - log.Fatalf("failed to close connection: %s", err) // nolint:gocritic + log.Printf("failed to close connection: %s", err) } }() @@ -93,7 +96,8 @@ func ExampleRun_withSSL() { tmpDir := os.TempDir() certDirs := tmpDir + "/rabbitmq" if err := os.MkdirAll(certDirs, 0o755); err != nil { - log.Fatalf("failed to create temporary directory: %s", err) + log.Printf("failed to create temporary directory: %s", err) + return } defer os.RemoveAll(certDirs) @@ -105,7 +109,8 @@ func ExampleRun_withSSL() { ParentDir: certDirs, }) if caCert == nil { - log.Fatal("failed to generate CA certificate") // nolint:gocritic + log.Print("failed to generate CA certificate") + return } cert := tlscert.SelfSignedFromRequest(tlscert.Request{ @@ -116,7 +121,8 @@ func ExampleRun_withSSL() { ParentDir: certDirs, }) if cert == nil { - log.Fatal("failed to generate certificate") // nolint:gocritic + log.Print("failed to generate certificate") + return } sslSettings := rabbitmq.SSLSettings{ @@ -132,20 +138,21 @@ func ExampleRun_withSSL() { "rabbitmq:3.7.25-management-alpine", rabbitmq.WithSSL(sslSettings), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) // nolint:gocritic - } - // } - defer func() { - if err := rabbitmqContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(rabbitmqContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + // } state, err := rabbitmqContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -166,17 +173,22 @@ func ExampleRun_withPlugins() { testcontainers.NewRawCommand([]string{"rabbitmq_random_exchange"}), ), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := rabbitmqContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(rabbitmqContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + + if err = assertPlugins(rabbitmqContainer, "rabbitmq_shovel", "rabbitmq_random_exchange"); err != nil { + log.Printf("failed to find plugin: %s", err) + return + } - fmt.Println(assertPlugins(rabbitmqContainer, "rabbitmq_shovel", "rabbitmq_random_exchange")) + fmt.Println(true) // Output: // true @@ -188,24 +200,26 @@ func ExampleRun_withCustomConfigFile() { rabbitmqContainer, err := rabbitmq.Run(ctx, "rabbitmq:3.7.25-management-alpine", ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := rabbitmqContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(rabbitmqContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } logs, err := rabbitmqContainer.Logs(ctx) if err != nil { - log.Fatalf("failed to get logs: %s", err) // nolint:gocritic + log.Printf("failed to get logs: %s", err) + return } bytes, err := io.ReadAll(logs) if err != nil { - log.Fatalf("failed to read logs: %s", err) // nolint:gocritic + log.Printf("failed to read logs: %s", err) + return } fmt.Println(strings.Contains(string(bytes), "config file(s) : /etc/rabbitmq/rabbitmq-testcontainers.conf")) @@ -214,25 +228,24 @@ func ExampleRun_withCustomConfigFile() { // true } -func assertPlugins(container testcontainers.Container, plugins ...string) bool { +func assertPlugins(container testcontainers.Container, plugins ...string) error { ctx := context.Background() for _, plugin := range plugins { - _, out, err := container.Exec(ctx, []string{"rabbitmq-plugins", "is_enabled", plugin}) if err != nil { - log.Fatalf("failed to execute command: %s", err) + return fmt.Errorf("failed to execute command: %w", err) } check, err := io.ReadAll(out) if err != nil { - log.Fatalf("failed to read output: %s", err) + return fmt.Errorf("failed to read output: %w", err) } if !strings.Contains(string(check), plugin+" is enabled") { - return false + return fmt.Errorf("plugin %q is not enabled", plugin) } } - return true + return nil } diff --git a/modules/rabbitmq/go.mod b/modules/rabbitmq/go.mod index e9d4a267fa..de71df8704 100644 --- a/modules/rabbitmq/go.mod +++ b/modules/rabbitmq/go.mod @@ -5,15 +5,8 @@ go 1.22 require ( github.com/docker/go-connections v0.5.0 github.com/rabbitmq/amqp091-go v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 -) - -require ( - github.com/containerd/platforms v0.2.1 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/net v0.26.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -23,7 +16,9 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect @@ -37,6 +32,7 @@ require ( github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mdelapenya/tlscert v0.1.0 + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/sys/user v0.1.0 // indirect @@ -45,6 +41,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -56,8 +53,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.26.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/rabbitmq/go.sum b/modules/rabbitmq/go.sum index f57610f497..e7d5675b93 100644 --- a/modules/rabbitmq/go.sum +++ b/modules/rabbitmq/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -53,7 +53,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= @@ -85,6 +88,8 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -96,6 +101,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -131,8 +138,8 @@ go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -154,14 +161,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -182,6 +189,8 @@ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGm google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/rabbitmq/rabbitmq.go b/modules/rabbitmq/rabbitmq.go index bcc1a849d7..32d18c09a9 100644 --- a/modules/rabbitmq/rabbitmq.go +++ b/modules/rabbitmq/rabbitmq.go @@ -133,14 +133,17 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) - if err != nil { - return nil, err + var c *RabbitMQContainer + if container != nil { + c = &RabbitMQContainer{ + Container: container, + AdminUsername: settings.AdminUsername, + AdminPassword: settings.AdminPassword, + } } - c := &RabbitMQContainer{ - Container: container, - AdminUsername: settings.AdminUsername, - AdminPassword: settings.AdminPassword, + if err != nil { + return c, fmt.Errorf("generic container: %w", err) } return c, nil diff --git a/modules/rabbitmq/rabbitmq_test.go b/modules/rabbitmq/rabbitmq_test.go index 9b024d7b5a..1cdd7e9137 100644 --- a/modules/rabbitmq/rabbitmq_test.go +++ b/modules/rabbitmq/rabbitmq_test.go @@ -5,13 +5,13 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "io" "os" "strings" "testing" "github.com/mdelapenya/tlscert" amqp "github.com/rabbitmq/amqp091-go" + "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/rabbitmq" @@ -21,29 +21,17 @@ func TestRunContainer_connectUsingAmqp(t *testing.T) { ctx := context.Background() rabbitmqContainer, err := rabbitmq.Run(ctx, "rabbitmq:3.12.11-management-alpine") - if err != nil { - t.Fatal(err) - } - - defer func() { - if err := rabbitmqContainer.Terminate(ctx); err != nil { - t.Fatal(err) - } - }() + testcontainers.CleanupContainer(t, rabbitmqContainer) + require.NoError(t, err) amqpURL, err := rabbitmqContainer.AmqpURL(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) amqpConnection, err := amqp.Dial(amqpURL) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if err = amqpConnection.Close(); err != nil { - t.Fatal(err) - } + err = amqpConnection.Close() + require.NoError(t, err) } func TestRunContainer_connectUsingAmqps(t *testing.T) { @@ -57,9 +45,7 @@ func TestRunContainer_connectUsingAmqps(t *testing.T) { IsCA: true, ParentDir: tmpDir, }) - if caCert == nil { - t.Fatal("failed to generate CA certificate") - } + require.NotNilf(t, caCert, "failed to generate CA certificate") cert := tlscert.SelfSignedFromRequest(tlscert.Request{ Name: "client", @@ -68,9 +54,7 @@ func TestRunContainer_connectUsingAmqps(t *testing.T) { Parent: caCert, ParentDir: tmpDir, }) - if cert == nil { - t.Fatal("failed to generate certificate") - } + require.NotNilf(t, cert, "failed to generate certificate") sslSettings := rabbitmq.SSLSettings{ CACertFile: caCert.CertPath, @@ -82,44 +66,26 @@ func TestRunContainer_connectUsingAmqps(t *testing.T) { } rabbitmqContainer, err := rabbitmq.Run(ctx, "rabbitmq:3.12.11-management-alpine", rabbitmq.WithSSL(sslSettings)) - if err != nil { - t.Fatal(err) - } - - defer func() { - if err := rabbitmqContainer.Terminate(ctx); err != nil { - t.Fatal(err) - } - }() + testcontainers.CleanupContainer(t, rabbitmqContainer) + require.NoError(t, err) amqpsURL, err := rabbitmqContainer.AmqpsURL(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if !strings.HasPrefix(amqpsURL, "amqps") { - t.Fatal(fmt.Errorf("AMQPS Url should begin with `amqps`")) - } + require.Truef(t, strings.HasPrefix(amqpsURL, "amqps"), "AMQPS Url should begin with `amqps`") certs := x509.NewCertPool() pemData, err := os.ReadFile(sslSettings.CACertFile) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) certs.AppendCertsFromPEM(pemData) amqpsConnection, err := amqp.DialTLS(amqpsURL, &tls.Config{InsecureSkipVerify: false, RootCAs: certs}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if amqpsConnection.IsClosed() { - t.Fatal(fmt.Errorf("AMQPS Connection unexpectdely closed")) - } - if err = amqpsConnection.Close(); err != nil { - t.Fatal(err) - } + require.Falsef(t, amqpsConnection.IsClosed(), "AMQPS Connection unexpectdely closed") + err = amqpsConnection.Close() + require.NoError(t, err) } func TestRunContainer_withAllSettings(t *testing.T) { @@ -238,66 +204,32 @@ func TestRunContainer_withAllSettings(t *testing.T) { testcontainers.WithAfterReadyCommand(Plugin{Name: "rabbitmq_shovel"}, Plugin{Name: "rabbitmq_random_exchange"}), // } ) - if err != nil { - t.Fatal(err) - } - - defer func() { - if err := rabbitmqContainer.Terminate(ctx); err != nil { - t.Fatal(err) - } - }() - - if !assertEntity(t, rabbitmqContainer, "queues", "queue1", "queue2", "queue3", "queue4") { - t.Fatal(err) - } - if !assertEntity(t, rabbitmqContainer, "exchanges", "direct-exchange", "topic-exchange", "topic-exchange-2", "topic-exchange-3", "topic-exchange-4") { - t.Fatal(err) - } - if !assertEntity(t, rabbitmqContainer, "users", "user1", "user2") { - t.Fatal(err) - } - if !assertEntity(t, rabbitmqContainer, "policies", "max length policy", "alternate exchange policy") { - t.Fatal(err) - } - if !assertEntityWithVHost(t, rabbitmqContainer, "policies", 2, "max length policy", "alternate exchange policy") { - t.Fatal(err) - } - if !assertEntity(t, rabbitmqContainer, "operator_policies", "operator policy 1") { - t.Fatal(err) - } - if !assertPluginIsEnabled(t, rabbitmqContainer, "rabbitmq_shovel", "rabbitmq_random_exchange") { - t.Fatal(err) - } + testcontainers.CleanupContainer(t, rabbitmqContainer) + require.NoError(t, err) + + requireEntity(t, rabbitmqContainer, "queues", "queue1", "queue2", "queue3", "queue4") + requireEntity(t, rabbitmqContainer, "exchanges", "direct-exchange", "topic-exchange", "topic-exchange-2", "topic-exchange-3", "topic-exchange-4") + requireEntity(t, rabbitmqContainer, "users", "user1", "user2") + requireEntity(t, rabbitmqContainer, "policies", "max length policy", "alternate exchange policy") + requireEntityWithVHost(t, rabbitmqContainer, "policies", 2, "max length policy", "alternate exchange policy") + requireEntity(t, rabbitmqContainer, "operator_policies", "operator policy 1") + requirePluginIsEnabled(t, rabbitmqContainer, "rabbitmq_shovel", "rabbitmq_random_exchange") } -func assertEntity(t *testing.T, container testcontainers.Container, listCommand string, entities ...string) bool { +func requireEntity(t *testing.T, container testcontainers.Container, listCommand string, entities ...string) { t.Helper() ctx := context.Background() cmd := []string{"rabbitmqadmin", "list", listCommand} - _, out, err := container.Exec(ctx, cmd) - if err != nil { - t.Fatal(err) - } - - check, err := io.ReadAll(out) - if err != nil { - t.Fatal(err) - } - + check := testcontainers.RequireContainerExec(ctx, t, container, cmd) for _, e := range entities { - if !strings.Contains(string(check), e) { - return false - } + require.Contains(t, check, e) } - - return true } -func assertEntityWithVHost(t *testing.T, container testcontainers.Container, listCommand string, vhostID int, entities ...string) bool { +func requireEntityWithVHost(t *testing.T, container testcontainers.Container, listCommand string, vhostID int, entities ...string) { t.Helper() ctx := context.Background() @@ -307,46 +239,22 @@ func assertEntityWithVHost(t *testing.T, container testcontainers.Container, lis cmd = append(cmd, fmt.Sprintf("--vhost=vhost%d", vhostID)) } - _, out, err := container.Exec(ctx, cmd) - if err != nil { - t.Fatal(err) - } - - check, err := io.ReadAll(out) - if err != nil { - t.Fatal(err) - } - + check := testcontainers.RequireContainerExec(ctx, t, container, cmd) for _, e := range entities { - if !strings.Contains(string(check), e) { - return false - } + require.Contains(t, check, e) } - - return true } -func assertPluginIsEnabled(t *testing.T, container testcontainers.Container, plugins ...string) bool { +func requirePluginIsEnabled(t *testing.T, container testcontainers.Container, plugins ...string) { t.Helper() ctx := context.Background() for _, plugin := range plugins { - _, out, err := container.Exec(ctx, []string{"rabbitmq-plugins", "is_enabled", plugin}) - if err != nil { - t.Fatal(err) - } + cmd := []string{"rabbitmq-plugins", "is_enabled", plugin} - check, err := io.ReadAll(out) - if err != nil { - t.Fatal(err) - } - - if !strings.Contains(string(check), plugin+" is enabled") { - return false - } + check := testcontainers.RequireContainerExec(ctx, t, container, cmd) + require.Contains(t, check, plugin+" is enabled") } - - return true } diff --git a/modules/rabbitmq/types_test.go b/modules/rabbitmq/types_test.go index 8b607f6632..e29bf2d88a 100644 --- a/modules/rabbitmq/types_test.go +++ b/modules/rabbitmq/types_test.go @@ -46,16 +46,16 @@ func (b Binding) AsCommand() []string { cmd := []string{"rabbitmqadmin"} if b.VHost != "" { - cmd = append(cmd, fmt.Sprintf("--vhost=%s", b.VHost)) + cmd = append(cmd, "--vhost="+b.VHost) } - cmd = append(cmd, "declare", "binding", fmt.Sprintf("source=%s", b.Source), fmt.Sprintf("destination=%s", b.Destination)) + cmd = append(cmd, "declare", "binding", "source="+b.Source, "destination="+b.Destination) if b.DestinationType != "" { - cmd = append(cmd, fmt.Sprintf("destination_type=%s", b.DestinationType)) + cmd = append(cmd, "destination_type="+b.DestinationType) } if b.RoutingKey != "" { - cmd = append(cmd, fmt.Sprintf("routing_key=%s", b.RoutingKey)) + cmd = append(cmd, "routing_key="+b.RoutingKey) } if len(b.Args) > 0 { @@ -92,7 +92,7 @@ func (e Exchange) AsCommand() []string { cmd = append(cmd, "--vhost="+e.VHost) } - cmd = append(cmd, "declare", "exchange", fmt.Sprintf("name=%s", e.Name), fmt.Sprintf("type=%s", e.Type)) + cmd = append(cmd, "declare", "exchange", "name="+e.Name, "type="+e.Type) if e.AutoDelete { cmd = append(cmd, "auto_delete=true") @@ -130,13 +130,13 @@ type OperatorPolicy struct { } func (op OperatorPolicy) AsCommand() []string { - cmd := []string{"rabbitmqadmin", "declare", "operator_policy", fmt.Sprintf("name=%s", op.Name), fmt.Sprintf("pattern=%s", op.Pattern)} + cmd := []string{"rabbitmqadmin", "declare", "operator_policy", "name=" + op.Name, "pattern=" + op.Pattern} if op.Priority > 0 { cmd = append(cmd, fmt.Sprintf("priority=%d", op.Priority)) } if op.ApplyTo != "" { - cmd = append(cmd, fmt.Sprintf("apply-to=%s", op.ApplyTo)) + cmd = append(cmd, "apply-to="+op.ApplyTo) } if len(op.Definition) > 0 { @@ -173,7 +173,7 @@ func NewParameter(component string, name string, value string) Parameter { func (p Parameter) AsCommand() []string { return []string{ "rabbitmqadmin", "declare", "parameter", - fmt.Sprintf("component=%s", p.Component), fmt.Sprintf("name=%s", p.Name), fmt.Sprintf("value=%s", p.Value), + "component=" + p.Component, "name=" + p.Name, "value=" + p.Value, } } @@ -203,8 +203,8 @@ func NewPermission(vhost string, user string, configure string, write string, re func (p Permission) AsCommand() []string { return []string{ "rabbitmqadmin", "declare", "permission", - fmt.Sprintf("vhost=%s", p.VHost), fmt.Sprintf("user=%s", p.User), - fmt.Sprintf("configure=%s", p.Configure), fmt.Sprintf("write=%s", p.Write), fmt.Sprintf("read=%s", p.Read), + "vhost=" + p.VHost, "user=" + p.User, + "configure=" + p.Configure, "write=" + p.Write, "read=" + p.Read, } } @@ -242,13 +242,13 @@ func (p Policy) AsCommand() []string { cmd = append(cmd, "--vhost="+p.VHost) } - cmd = append(cmd, "declare", "policy", fmt.Sprintf("name=%s", p.Name), fmt.Sprintf("pattern=%s", p.Pattern)) + cmd = append(cmd, "declare", "policy", "name="+p.Name, "pattern="+p.Pattern) if p.Priority > 0 { cmd = append(cmd, fmt.Sprintf("priority=%d", p.Priority)) } if p.ApplyTo != "" { - cmd = append(cmd, fmt.Sprintf("apply-to=%s", p.ApplyTo)) + cmd = append(cmd, "apply-to="+p.ApplyTo) } if len(p.Definition) > 0 { @@ -283,7 +283,7 @@ func (q Queue) AsCommand() []string { cmd = append(cmd, "--vhost="+q.VHost) } - cmd = append(cmd, "declare", "queue", fmt.Sprintf("name=%s", q.Name)) + cmd = append(cmd, "declare", "queue", "name="+q.Name) if q.AutoDelete { cmd = append(cmd, "auto_delete=true") @@ -328,8 +328,8 @@ func (u User) AsCommand() []string { return []string{ "rabbitmqadmin", "declare", "user", - fmt.Sprintf("name=%s", u.Name), fmt.Sprintf("password=%s", u.Password), - fmt.Sprintf("tags=%s", strings.Join(uniqueTags, ",")), + "name=" + u.Name, "password=" + u.Password, + "tags=" + strings.Join(uniqueTags, ","), } } @@ -344,7 +344,7 @@ type VirtualHost struct { } func (v VirtualHost) AsCommand() []string { - cmd := []string{"rabbitmqadmin", "declare", "vhost", fmt.Sprintf("name=%s", v.Name)} + cmd := []string{"rabbitmqadmin", "declare", "vhost", "name=" + v.Name} if v.Tracing { cmd = append(cmd, "tracing=true") @@ -361,7 +361,7 @@ type VirtualHostLimit struct { } func (v VirtualHostLimit) AsCommand() []string { - return []string{"rabbitmqadmin", "declare", "vhost_limit", fmt.Sprintf("vhost=%s", v.VHost), fmt.Sprintf("name=%s", v.Name), fmt.Sprintf("value=%d", v.Value)} + return []string{"rabbitmqadmin", "declare", "vhost_limit", "vhost=" + v.VHost, "name=" + v.Name, fmt.Sprintf("value=%d", v.Value)} } // --------- Virtual Hosts --------- diff --git a/modules/redis/examples_test.go b/modules/redis/examples_test.go index 9fb7c2cf11..86be00be85 100644 --- a/modules/redis/examples_test.go +++ b/modules/redis/examples_test.go @@ -6,6 +6,7 @@ import ( "log" "path/filepath" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/redis" ) @@ -14,26 +15,26 @@ func ExampleRun() { ctx := context.Background() redisContainer, err := redis.Run(ctx, - "docker.io/redis:7", + "redis:7", redis.WithSnapshotting(10, 1), redis.WithLogLevel(redis.LogLevelVerbose), redis.WithConfigFile(filepath.Join("testdata", "redis7.conf")), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := redisContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(redisContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := redisContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/redis/go.mod b/modules/redis/go.mod index e76c925c7e..774b7e677e 100644 --- a/modules/redis/go.mod +++ b/modules/redis/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-redis/redis/v8 v8.11.5 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) @@ -21,7 +21,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect @@ -58,9 +58,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/redis/go.sum b/modules/redis/go.sum index 1698de2ecf..8ca0c6da81 100644 --- a/modules/redis/go.sum +++ b/modules/redis/go.sum @@ -16,8 +16,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -111,6 +111,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -144,8 +146,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -167,14 +169,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/redis/redis.go b/modules/redis/redis.go index 33ce823994..d824036642 100644 --- a/modules/redis/redis.go +++ b/modules/redis/redis.go @@ -3,6 +3,7 @@ package redis import ( "context" "fmt" + "strconv" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -46,7 +47,7 @@ func (c *RedisContainer) ConnectionString(ctx context.Context) (string, error) { // Deprecated: use Run instead // RunContainer creates an instance of the Redis container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*RedisContainer, error) { - return Run(ctx, "docker.io/redis:7", opts...) + return Run(ctx, "redis:7", opts...) } // Run creates an instance of the Redis container type @@ -69,11 +70,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *RedisContainer + if container != nil { + c = &RedisContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &RedisContainer{Container: container}, nil + return c, nil } // WithConfigFile sets the config file to be used for the redis container, and sets the command to run the redis server @@ -130,7 +136,7 @@ func WithSnapshotting(seconds int, changedKeys int) testcontainers.CustomizeRequ } return func(req *testcontainers.GenericContainerRequest) error { - processRedisServerArgs(req, []string{"--save", fmt.Sprintf("%d", seconds), fmt.Sprintf("%d", changedKeys)}) + processRedisServerArgs(req, []string{"--save", strconv.Itoa(seconds), strconv.Itoa(changedKeys)}) return nil } } diff --git a/modules/redis/redis_test.go b/modules/redis/redis_test.go index f98079685f..e2352a1bf6 100644 --- a/modules/redis/redis_test.go +++ b/modules/redis/redis_test.go @@ -11,19 +11,16 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" tcredis "github.com/testcontainers/testcontainers-go/modules/redis" ) func TestIntegrationSetGet(t *testing.T) { ctx := context.Background() - redisContainer, err := tcredis.Run(ctx, "docker.io/redis:7") + redisContainer, err := tcredis.Run(ctx, "redis:7") + testcontainers.CleanupContainer(t, redisContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := redisContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, redisContainer, 1) } @@ -31,13 +28,9 @@ func TestIntegrationSetGet(t *testing.T) { func TestRedisWithConfigFile(t *testing.T) { ctx := context.Background() - redisContainer, err := tcredis.Run(ctx, "docker.io/redis:7", tcredis.WithConfigFile(filepath.Join("testdata", "redis7.conf"))) + redisContainer, err := tcredis.Run(ctx, "redis:7", tcredis.WithConfigFile(filepath.Join("testdata", "redis7.conf"))) + testcontainers.CleanupContainer(t, redisContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := redisContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, redisContainer, 1) } @@ -51,22 +44,22 @@ func TestRedisWithImage(t *testing.T) { }{ { name: "Redis6", - image: "docker.io/redis:6", + image: "redis:6", }, { name: "Redis7", - image: "docker.io/redis:7", + image: "redis:7", }, { name: "Redis Stack", // redisStackImage { - image: "docker.io/redis/redis-stack:latest", + image: "redis/redis-stack:latest", // } }, { name: "Redis Stack Server", // redisStackServerImage { - image: "docker.io/redis/redis-stack-server:latest", + image: "redis/redis-stack-server:latest", // } }, } @@ -74,12 +67,8 @@ func TestRedisWithImage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { redisContainer, err := tcredis.Run(ctx, tt.image, tcredis.WithConfigFile(filepath.Join("testdata", "redis6.conf"))) + testcontainers.CleanupContainer(t, redisContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := redisContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, redisContainer, 1) }) @@ -89,13 +78,9 @@ func TestRedisWithImage(t *testing.T) { func TestRedisWithLogLevel(t *testing.T) { ctx := context.Background() - redisContainer, err := tcredis.Run(ctx, "docker.io/redis:7", tcredis.WithLogLevel(tcredis.LogLevelVerbose)) + redisContainer, err := tcredis.Run(ctx, "redis:7", tcredis.WithLogLevel(tcredis.LogLevelVerbose)) + testcontainers.CleanupContainer(t, redisContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := redisContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, redisContainer, 10) } @@ -103,18 +88,15 @@ func TestRedisWithLogLevel(t *testing.T) { func TestRedisWithSnapshotting(t *testing.T) { ctx := context.Background() - redisContainer, err := tcredis.Run(ctx, "docker.io/redis:7", tcredis.WithSnapshotting(10, 1)) + redisContainer, err := tcredis.Run(ctx, "redis:7", tcredis.WithSnapshotting(10, 1)) + testcontainers.CleanupContainer(t, redisContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := redisContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, redisContainer, 10) } func assertSetsGets(t *testing.T, ctx context.Context, redisContainer *tcredis.RedisContainer, keyCount int) { + t.Helper() // connectionString { uri, err := redisContainer.ConnectionString(ctx) // } @@ -128,6 +110,7 @@ func assertSetsGets(t *testing.T, ctx context.Context, redisContainer *tcredis.R client := redis.NewClient(options) defer func(t *testing.T, ctx context.Context, client *redis.Client) { + t.Helper() require.NoError(t, flushRedis(ctx, *client)) }(t, ctx, client) @@ -137,9 +120,7 @@ func assertSetsGets(t *testing.T, ctx context.Context, redisContainer *tcredis.R t.Log("received response from redis") - if pong != "PONG" { - t.Fatalf("received unexpected response from redis: %s", pong) - } + require.Equalf(t, "PONG", pong, "received unexpected response from redis: %s", pong) for i := 0; i < keyCount; i++ { // Set data @@ -154,9 +135,7 @@ func assertSetsGets(t *testing.T, ctx context.Context, redisContainer *tcredis.R savedValue, err := client.Get(ctx, key).Result() require.NoError(t, err) - if savedValue != value { - t.Fatalf("Expected value %s. Got %s.", savedValue, value) - } + require.Equal(t, savedValue, value) } } diff --git a/modules/redis/testdata/Dockerfile b/modules/redis/testdata/Dockerfile index 7157611a13..14cfaf1e23 100644 --- a/modules/redis/testdata/Dockerfile +++ b/modules/redis/testdata/Dockerfile @@ -1 +1 @@ -FROM docker.io/redis:5.0-alpine@sha256:1a3c609295332f1ce603948142a132656c92a08149d7096e203058533c415b8c +FROM redis:5.0-alpine@sha256:1a3c609295332f1ce603948142a132656c92a08149d7096e203058533c415b8c diff --git a/modules/redpanda/examples_test.go b/modules/redpanda/examples_test.go index 68cb314418..69fb0c9d6a 100644 --- a/modules/redpanda/examples_test.go +++ b/modules/redpanda/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/redpanda" ) @@ -25,21 +26,21 @@ func ExampleRun() { redpanda.WithSuperusers("superuser-1", "superuser-2"), redpanda.WithEnableSchemaRegistryHTTPBasicAuth(), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := redpandaContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(redpandaContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := redpandaContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/redpanda/go.mod b/modules/redpanda/go.mod index 4ce12872ee..9c5065458d 100644 --- a/modules/redpanda/go.mod +++ b/modules/redpanda/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/docker/go-connections v0.5.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 github.com/twmb/franz-go v1.16.1 github.com/twmb/franz-go/pkg/kadm v1.11.0 golang.org/x/mod v0.16.0 @@ -25,7 +25,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -63,8 +63,8 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/modules/redpanda/go.sum b/modules/redpanda/go.sum index cf26162896..740496f896 100644 --- a/modules/redpanda/go.sum +++ b/modules/redpanda/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -101,6 +101,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -140,8 +142,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= @@ -165,14 +167,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/redpanda/options.go b/modules/redpanda/options.go index ae15e33001..180edcc48f 100644 --- a/modules/redpanda/options.go +++ b/modules/redpanda/options.go @@ -139,7 +139,7 @@ func WithTLS(cert, key []byte) Option { // WithListener adds a custom listener to the Redpanda containers. Listener // will be aliases to all networks, so they can be accessed from within docker -// networks. At leas one network must be attached to the container, if not an +// networks. At least one network must be attached to the container, if not an // error will be thrown when starting the container. func WithListener(lis string) Option { host, port, err := net.SplitHostPort(lis) diff --git a/modules/redpanda/redpanda.go b/modules/redpanda/redpanda.go index 3ed3932066..21c3ca4c44 100644 --- a/modules/redpanda/redpanda.go +++ b/modules/redpanda/redpanda.go @@ -6,10 +6,10 @@ import ( "crypto/tls" "crypto/x509" _ "embed" + "errors" "fmt" "math" "net/http" - "os" "path/filepath" "strings" "text/template" @@ -60,12 +60,6 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize // Run creates an instance of the Redpanda container type func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { - tmpDir, err := os.MkdirTemp("", "redpanda") - if err != nil { - return nil, fmt.Errorf("failed to create directory: %w", err) - } - defer os.RemoveAll(tmpDir) - // 1. Create container request. // Some (e.g. Image) may be overridden by providing an option argument to this function. req := testcontainers.GenericContainerRequest{ @@ -113,153 +107,149 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom settings.EnableWasmTransform = false } - // 3. Create temporary entrypoint file. We need a custom entrypoint that waits - // until the actual Redpanda node config is mounted. Once the redpanda config is - // mounted we will call the original entrypoint with the same parameters. - // We have to do this kind of two-step process, because we need to know the mapped - // port, so that we can use this in Redpanda's advertised listeners configuration for - // the Kafka API. - entrypointPath := filepath.Join(tmpDir, entrypointFile) - if err := os.WriteFile(entrypointPath, entrypoint, 0o700); err != nil { - return nil, fmt.Errorf("failed to create entrypoint file: %w", err) - } - - // 4. Register extra kafka listeners if provided, network aliases will be + // 3. Register extra kafka listeners if provided, network aliases will be // set if err := registerListeners(settings, req); err != nil { - return nil, fmt.Errorf("failed to register listeners: %w", err) + return nil, fmt.Errorf("register listeners: %w", err) } // Bootstrap config file contains cluster configurations which will only be considered // the very first time you start a cluster. - bootstrapConfigPath := filepath.Join(tmpDir, bootstrapConfigFile) bootstrapConfig, err := renderBootstrapConfig(settings) if err != nil { - return nil, fmt.Errorf("failed to create bootstrap config file: %w", err) - } - if err := os.WriteFile(bootstrapConfigPath, bootstrapConfig, 0o600); err != nil { - return nil, fmt.Errorf("failed to create bootstrap config file: %w", err) + return nil, err } + // We need a custom entrypoint that waits until the actual Redpanda node config is mounted. + // Once the redpanda config is mounted we will call the original entrypoint with the same parameters. + // We have to do this kind of two-step process, because we need to know the mapped + // port, so that we can use this in Redpanda's advertised listeners configuration for + // the Kafka API. req.Files = append(req.Files, testcontainers.ContainerFile{ - HostFilePath: entrypointPath, + Reader: bytes.NewReader(entrypoint), ContainerFilePath: entrypointFile, FileMode: 700, }, testcontainers.ContainerFile{ - HostFilePath: bootstrapConfigPath, + Reader: bytes.NewReader(bootstrapConfig), ContainerFilePath: filepath.Join(redpandaDir, bootstrapConfigFile), FileMode: 600, }, ) - // 5. Create certificate and key for TLS connections. + // 4. Create certificate and key for TLS connections. if settings.EnableTLS { - certPath := filepath.Join(tmpDir, certFile) - if err := os.WriteFile(certPath, settings.cert, 0o600); err != nil { - return nil, fmt.Errorf("failed to create certificate file: %w", err) - } - keyPath := filepath.Join(tmpDir, keyFile) - if err := os.WriteFile(keyPath, settings.key, 0o600); err != nil { - return nil, fmt.Errorf("failed to create key file: %w", err) - } - req.Files = append(req.Files, testcontainers.ContainerFile{ - HostFilePath: certPath, + Reader: bytes.NewReader(settings.cert), ContainerFilePath: filepath.Join(redpandaDir, certFile), FileMode: 600, }, testcontainers.ContainerFile{ - HostFilePath: keyPath, + Reader: bytes.NewReader(settings.key), ContainerFilePath: filepath.Join(redpandaDir, keyFile), FileMode: 600, }, ) } - container, err := testcontainers.GenericContainer(ctx, req) + ctr, err := testcontainers.GenericContainer(ctx, req) + var c *Container + if ctr != nil { + c = &Container{Container: ctr} + } if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - // 6. Get mapped port for the Kafka API, so that we can render and then mount + // 5. Get mapped port for the Kafka API, so that we can render and then mount // the Redpanda config with the advertised Kafka address. - hostIP, err := container.Host(ctx) + hostIP, err := ctr.Host(ctx) if err != nil { - return nil, fmt.Errorf("failed to get container host: %w", err) + return c, fmt.Errorf("host: %w", err) } - kafkaPort, err := container.MappedPort(ctx, nat.Port(defaultKafkaAPIPort)) + kafkaPort, err := ctr.MappedPort(ctx, nat.Port(defaultKafkaAPIPort)) if err != nil { - return nil, fmt.Errorf("failed to get mapped Kafka port: %w", err) + return c, fmt.Errorf("mapped kafka port: %w", err) } - // 7. Render redpanda.yaml config and mount it. + // 6. Render redpanda.yaml config and mount it. nodeConfig, err := renderNodeConfig(settings, hostIP, kafkaPort.Int()) if err != nil { - return nil, fmt.Errorf("failed to render node config: %w", err) + return c, err } - err = container.CopyToContainer(ctx, nodeConfig, filepath.Join(redpandaDir, "redpanda.yaml"), 600) + err = ctr.CopyToContainer(ctx, nodeConfig, filepath.Join(redpandaDir, "redpanda.yaml"), 0o600) if err != nil { - return nil, fmt.Errorf("failed to copy redpanda.yaml into container: %w", err) + return c, fmt.Errorf("copy to container: %w", err) } - // 8. Wait until Redpanda is ready to serve requests. + // 7. Wait until Redpanda is ready to serve requests. + waitHTTP := wait.ForHTTP(defaultAdminAPIPort). + WithStatusCodeMatcher(func(status int) bool { + // Redpanda's admin API returns 404 for requests to "/". + return status == http.StatusNotFound + }) + + var tlsConfig *tls.Config + if settings.EnableTLS { + cert, err := tls.X509KeyPair(settings.cert, settings.key) + if err != nil { + return c, fmt.Errorf("create admin cert: %w", err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(settings.cert) + tlsConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + } + waitHTTP = waitHTTP.WithTLS(true, tlsConfig) + } err = wait.ForAll( wait.ForListeningPort(defaultKafkaAPIPort), - wait.ForListeningPort(defaultAdminAPIPort), + waitHTTP, wait.ForListeningPort(defaultSchemaRegistryPort), wait.ForLog("Successfully started Redpanda!"), - ).WaitUntilReady(ctx, container) + ).WaitUntilReady(ctx, ctr) if err != nil { - return nil, fmt.Errorf("failed to wait for Redpanda readiness: %w", err) + return c, fmt.Errorf("wait for readiness: %w", err) } - scheme := "http" + c.urlScheme = "http" if settings.EnableTLS { - scheme += "s" + c.urlScheme += "s" } - // 9. Create Redpanda Service Accounts if configured to do so. + // 8. Create Redpanda Service Accounts if configured to do so. if len(settings.ServiceAccounts) > 0 { - adminAPIPort, err := container.MappedPort(ctx, nat.Port(defaultAdminAPIPort)) + adminAPIPort, err := ctr.MappedPort(ctx, nat.Port(defaultAdminAPIPort)) if err != nil { - return nil, fmt.Errorf("failed to get mapped Admin API port: %w", err) + return c, fmt.Errorf("mapped admin port: %w", err) } - adminAPIUrl := fmt.Sprintf("%s://%v:%d", scheme, hostIP, adminAPIPort.Int()) + adminAPIUrl := fmt.Sprintf("%s://%v:%d", c.urlScheme, hostIP, adminAPIPort.Int()) adminCl := NewAdminAPIClient(adminAPIUrl) if settings.EnableTLS { - cert, err := tls.X509KeyPair(settings.cert, settings.key) - if err != nil { - return nil, fmt.Errorf("failed to create admin client with cert: %w", err) - } - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(settings.cert) adminCl = adminCl.WithHTTPClient(&http.Client{ Timeout: 5 * time.Second, Transport: &http.Transport{ ForceAttemptHTTP2: true, TLSHandshakeTimeout: 10 * time.Second, - TLSClientConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: caCertPool, - }, + TLSClientConfig: tlsConfig, }, }) } for username, password := range settings.ServiceAccounts { if err := adminCl.CreateUser(ctx, username, password); err != nil { - return nil, fmt.Errorf("failed to create service account with username %q: %w", username, err) + return c, fmt.Errorf("create user %q: %w", username, err) } } } - return &Container{Container: container, urlScheme: scheme}, nil + return c, nil } // KafkaSeedBroker returns the seed broker that should be used for connecting @@ -295,12 +285,12 @@ func renderBootstrapConfig(settings options) ([]byte, error) { tpl, err := template.New("bootstrap.yaml").Parse(bootstrapConfigTpl) if err != nil { - return nil, fmt.Errorf("failed to parse redpanda config file template: %w", err) + return nil, fmt.Errorf("parse bootstrap template: %w", err) } var bootstrapConfig bytes.Buffer if err := tpl.Execute(&bootstrapConfig, bootstrapTplParams); err != nil { - return nil, fmt.Errorf("failed to render redpanda bootstrap config template: %w", err) + return nil, fmt.Errorf("render bootstrap template: %w", err) } return bootstrapConfig.Bytes(), nil @@ -314,7 +304,7 @@ func registerListeners(settings options, req testcontainers.GenericContainerRequ } if len(req.Networks) == 0 { - return fmt.Errorf("container must be attached to at least one network") + return errors.New("container must be attached to at least one network") } for _, listener := range settings.Listeners { @@ -349,12 +339,12 @@ func renderNodeConfig(settings options, hostIP string, advertisedKafkaPort int) ncTpl, err := template.New("redpanda.yaml").Parse(nodeConfigTpl) if err != nil { - return nil, fmt.Errorf("failed to parse redpanda config file template: %w", err) + return nil, fmt.Errorf("parse node config template: %w", err) } var redpandaYaml bytes.Buffer if err := ncTpl.Execute(&redpandaYaml, tplParams); err != nil { - return nil, fmt.Errorf("failed to render redpanda node config template: %w", err) + return nil, fmt.Errorf("render node config template: %w", err) } return redpandaYaml.Bytes(), nil @@ -403,11 +393,11 @@ func isAtLeastVersion(image, major string) bool { } if !strings.HasPrefix(version, "v") { - version = fmt.Sprintf("v%s", version) + version = "v" + version } if semver.IsValid(version) { - return semver.Compare(version, fmt.Sprintf("v%s", major)) >= 0 // version >= v8.x + return semver.Compare(version, "v"+major) >= 0 // version >= v8.x } return false diff --git a/modules/redpanda/redpanda_test.go b/modules/redpanda/redpanda_test.go index 80668964f6..c295357dae 100644 --- a/modules/redpanda/redpanda_test.go +++ b/modules/redpanda/redpanda_test.go @@ -27,18 +27,12 @@ import ( func TestRedpanda(t *testing.T) { ctx := context.Background() - container, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3") + ctr, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3") + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - // Test Kafka API - seedBroker, err := container.KafkaSeedBroker(ctx) + seedBroker, err := ctr.KafkaSeedBroker(ctx) require.NoError(t, err) kafkaCl, err := kgo.NewClient( @@ -50,30 +44,30 @@ func TestRedpanda(t *testing.T) { kafkaAdmCl := kadm.NewClient(kafkaCl) metadata, err := kafkaAdmCl.Metadata(ctx) require.NoError(t, err) - assert.Len(t, metadata.Brokers, 1) + require.Len(t, metadata.Brokers, 1) // Test Schema Registry API httpCl := &http.Client{Timeout: 5 * time.Second} - schemaRegistryURL, err := container.SchemaRegistryAddress(ctx) + schemaRegistryURL, err := ctr.SchemaRegistryAddress(ctx) require.NoError(t, err) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/subjects", schemaRegistryURL), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, schemaRegistryURL+"/subjects", nil) require.NoError(t, err) resp, err := httpCl.Do(req) require.NoError(t, err) defer resp.Body.Close() - assert.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, http.StatusOK, resp.StatusCode) // Test Admin API // adminAPIAddress { - adminAPIURL, err := container.AdminAPIAddress(ctx) + adminAPIURL, err := ctr.AdminAPIAddress(ctx) // } require.NoError(t, err) - req, err = http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/cluster/health_overview", adminAPIURL), nil) + req, err = http.NewRequestWithContext(ctx, http.MethodGet, adminAPIURL+"/v1/cluster/health_overview", nil) require.NoError(t, err) resp, err = httpCl.Do(req) require.NoError(t, err) defer resp.Body.Close() - assert.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, http.StatusOK, resp.StatusCode) // Test produce to unknown topic results := kafkaCl.ProduceSync(ctx, &kgo.Record{Topic: "test", Value: []byte("test message")}) @@ -83,7 +77,7 @@ func TestRedpanda(t *testing.T) { func TestRedpandaWithAuthentication(t *testing.T) { ctx := context.Background() // redpandaCreateContainer { - container, err := redpanda.Run(ctx, + ctr, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3", redpanda.WithEnableSASL(), redpanda.WithEnableKafkaAuthorization(), @@ -94,18 +88,12 @@ func TestRedpandaWithAuthentication(t *testing.T) { redpanda.WithSuperusers("superuser-1", "superuser-2"), redpanda.WithEnableSchemaRegistryHTTPBasicAuth(), ) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) // } - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - // kafkaSeedBroker { - seedBroker, err := container.KafkaSeedBroker(ctx) + seedBroker, err := ctr.KafkaSeedBroker(ctx) // } require.NoError(t, err) @@ -169,16 +157,16 @@ func TestRedpandaWithAuthentication(t *testing.T) { // Test Schema Registry API httpCl := &http.Client{Timeout: 5 * time.Second} // schemaRegistryAddress { - schemaRegistryURL, err := container.SchemaRegistryAddress(ctx) + schemaRegistryURL, err := ctr.SchemaRegistryAddress(ctx) // } require.NoError(t, err) // Failed authentication - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/subjects", schemaRegistryURL), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, schemaRegistryURL+"/subjects", nil) require.NoError(t, err) resp, err := httpCl.Do(req) require.NoError(t, err) - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) resp.Body.Close() // Successful authentication @@ -186,7 +174,7 @@ func TestRedpandaWithAuthentication(t *testing.T) { req.SetBasicAuth(user, password) resp, err = httpCl.Do(req) require.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, http.StatusOK, resp.StatusCode) resp.Body.Close() } } @@ -195,7 +183,7 @@ func TestRedpandaWithOldVersionAndWasm(t *testing.T) { ctx := context.Background() // redpandaCreateContainer { // this would fail to start if we weren't ignoring wasm transforms for older versions - container, err := redpanda.Run(ctx, + ctr, err := redpanda.Run(ctx, "redpandadata/redpanda:v23.2.18", redpanda.WithEnableSASL(), redpanda.WithEnableKafkaAuthorization(), @@ -206,18 +194,12 @@ func TestRedpandaWithOldVersionAndWasm(t *testing.T) { redpanda.WithSuperusers("superuser-1", "superuser-2"), redpanda.WithEnableSchemaRegistryHTTPBasicAuth(), ) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) // } - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - // kafkaSeedBroker { - seedBroker, err := container.KafkaSeedBroker(ctx) + seedBroker, err := ctr.KafkaSeedBroker(ctx) // } require.NoError(t, err) @@ -298,16 +280,16 @@ func TestRedpandaWithOldVersionAndWasm(t *testing.T) { // Test Schema Registry API httpCl := &http.Client{Timeout: 5 * time.Second} // schemaRegistryAddress { - schemaRegistryURL, err := container.SchemaRegistryAddress(ctx) + schemaRegistryURL, err := ctr.SchemaRegistryAddress(ctx) // } require.NoError(t, err) // Failed authentication - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/subjects", schemaRegistryURL), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, schemaRegistryURL+"/subjects", nil) require.NoError(t, err) resp, err := httpCl.Do(req) require.NoError(t, err) - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) resp.Body.Close() // Successful authentication @@ -323,16 +305,11 @@ func TestRedpandaWithOldVersionAndWasm(t *testing.T) { func TestRedpandaProduceWithAutoCreateTopics(t *testing.T) { ctx := context.Background() - container, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3", redpanda.WithAutoCreateTopics()) + ctr, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3", redpanda.WithAutoCreateTopics()) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - - brokers, err := container.KafkaSeedBroker(ctx) + brokers, err := ctr.KafkaSeedBroker(ctx) require.NoError(t, err) kafkaCl, err := kgo.NewClient( @@ -357,15 +334,10 @@ func TestRedpandaWithTLS(t *testing.T) { ctx := context.Background() - container, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3", redpanda.WithTLS(cert.Bytes, cert.KeyBytes)) + ctr, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3", redpanda.WithTLS(cert.Bytes, cert.KeyBytes)) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - tlsConfig := cert.TLSConfig() httpCl := &http.Client{ @@ -378,28 +350,28 @@ func TestRedpandaWithTLS(t *testing.T) { } // Test Admin API - adminAPIURL, err := container.AdminAPIAddress(ctx) + adminAPIURL, err := ctr.AdminAPIAddress(ctx) require.NoError(t, err) require.True(t, strings.HasPrefix(adminAPIURL, "https://"), "AdminAPIAddress should return https url") - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/cluster/health_overview", adminAPIURL), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, adminAPIURL+"/v1/cluster/health_overview", nil) require.NoError(t, err) resp, err := httpCl.Do(req) require.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, http.StatusOK, resp.StatusCode) resp.Body.Close() // Test Schema Registry API - schemaRegistryURL, err := container.SchemaRegistryAddress(ctx) + schemaRegistryURL, err := ctr.SchemaRegistryAddress(ctx) require.NoError(t, err) require.True(t, strings.HasPrefix(adminAPIURL, "https://"), "SchemaRegistryAddress should return https url") - req, err = http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/subjects", schemaRegistryURL), nil) + req, err = http.NewRequestWithContext(ctx, http.MethodGet, schemaRegistryURL+"/subjects", nil) require.NoError(t, err) resp, err = httpCl.Do(req) require.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, http.StatusOK, resp.StatusCode) resp.Body.Close() - brokers, err := container.KafkaSeedBroker(ctx) + brokers, err := ctr.KafkaSeedBroker(ctx) require.NoError(t, err) kafkaCl, err := kgo.NewClient( @@ -426,7 +398,7 @@ func TestRedpandaWithTLSAndSASL(t *testing.T) { ctx := context.Background() - container, err := redpanda.Run(ctx, + ctr, err := redpanda.Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3", redpanda.WithTLS(cert.Bytes, cert.KeyBytes), redpanda.WithEnableSASL(), @@ -434,17 +406,12 @@ func TestRedpandaWithTLSAndSASL(t *testing.T) { redpanda.WithNewServiceAccount("superuser-1", "test"), redpanda.WithSuperusers("superuser-1"), ) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - tlsConfig := cert.TLSConfig() - broker, err := container.KafkaSeedBroker(ctx) + broker, err := ctr.KafkaSeedBroker(ctx) require.NoError(t, err) kafkaCl, err := kgo.NewClient( @@ -469,14 +436,17 @@ func TestRedpandaListener_Simple(t *testing.T) { rpNetwork, err := network.New(ctx) require.NoError(t, err) - // 2. Start Redpanda container + testcontainers.CleanupNetwork(t, rpNetwork) + + // 2. Start Redpanda ctr // withListenerRP { - container, err := redpanda.Run(ctx, + ctr, err := redpanda.Run(ctx, "redpandadata/redpanda:v23.2.18", network.WithNetwork([]string{"redpanda-host"}, rpNetwork), redpanda.WithListener("redpanda:29092"), redpanda.WithAutoCreateTopics(), ) // } + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) // 3. Start KCat container @@ -498,7 +468,7 @@ func TestRedpandaListener_Simple(t *testing.T) { Started: true, }) // } - + testcontainers.CleanupContainer(t, kcat) require.NoError(t, err) // 4. Copy message to kcat @@ -519,21 +489,7 @@ func TestRedpandaListener_Simple(t *testing.T) { // 7. Read Message from stdout out, err := io.ReadAll(stdout) require.NoError(t, err) - require.Contains(t, string(out), "Message produced by kcat") - - t.Cleanup(func() { - if err := kcat.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate kcat container: %s", err) - } - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate redpanda container: %s", err) - } - - if err := rpNetwork.Remove(ctx); err != nil { - t.Fatalf("failed to remove network: %s", err) - } - }) } func TestRedpandaListener_InvalidPort(t *testing.T) { @@ -542,37 +498,28 @@ func TestRedpandaListener_InvalidPort(t *testing.T) { // 1. Create network RPNetwork, err := network.New(ctx) require.NoError(t, err) + testcontainers.CleanupNetwork(t, RPNetwork) - // 2. Attempt Start Redpanda container - _, err = redpanda.Run(ctx, + // 2. Attempt Start Redpanda ctr + ctr, err := redpanda.Run(ctx, "redpandadata/redpanda:v23.2.18", redpanda.WithListener("redpanda:99092"), network.WithNetwork([]string{"redpanda-host"}, RPNetwork), ) - - require.Error(t, err) - - require.Contains(t, err.Error(), "invalid port on listener redpanda:99092") - - t.Cleanup(func() { - if err := RPNetwork.Remove(ctx); err != nil { - t.Fatalf("failed to remove network: %s", err) - } - }) + testcontainers.CleanupContainer(t, ctr) + require.ErrorContains(t, err, "invalid port on listener redpanda:99092") } func TestRedpandaListener_NoNetwork(t *testing.T) { ctx := context.Background() - // 1. Attempt Start Redpanda container - _, err := redpanda.Run(ctx, + // 1. Attempt Start Redpanda ctr + ctr, err := redpanda.Run(ctx, "redpandadata/redpanda:v23.2.18", redpanda.WithListener("redpanda:99092"), ) - - require.Error(t, err) - - require.Contains(t, err.Error(), "container must be attached to at least one network") + testcontainers.CleanupContainer(t, ctr) + require.ErrorContains(t, err, "container must be attached to at least one network") } func TestRedpandaBootstrapConfig(t *testing.T) { @@ -599,7 +546,7 @@ func TestRedpandaBootstrapConfig(t *testing.T) { { // Check that the configs reflect specified values - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/cluster_config", adminAPIUrl), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, adminAPIUrl+"/v1/cluster_config", nil) require.NoError(t, err) resp, err := httpCl.Do(req) require.NoError(t, err) @@ -616,7 +563,7 @@ func TestRedpandaBootstrapConfig(t *testing.T) { { // Check that no restart is required. i.e. that the configs were applied via bootstrap config - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/cluster_config/status", adminAPIUrl), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, adminAPIUrl+"/v1/cluster_config/status", nil) require.NoError(t, err) resp, err := httpCl.Do(req) require.NoError(t, err) diff --git a/modules/registry/examples_test.go b/modules/registry/examples_test.go index ada7e33b85..8742456eef 100644 --- a/modules/registry/examples_test.go +++ b/modules/registry/examples_test.go @@ -14,21 +14,21 @@ import ( func ExampleRun() { // runRegistryContainer { registryContainer, err := registry.Run(context.Background(), "registry:2.8.3") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := registryContainer.Terminate(context.Background()); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(registryContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := registryContainer.State(context.Background()) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -47,23 +47,26 @@ func ExampleRun_withAuthentication() { registry.WithData(filepath.Join("testdata", "data")), ) // } - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - if err := registryContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(registryContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } registryHost, err := registryContainer.HostAddress(ctx) if err != nil { - log.Fatalf("failed to get host: %s", err) // nolint:gocritic + log.Printf("failed to get host: %s", err) + return } cleanup, err := registry.SetDockerAuthConfig(registryHost, "testuser", "testpassword") if err != nil { - log.Fatalf("failed to set docker auth config: %s", err) // nolint:gocritic + log.Printf("failed to set docker auth config: %s", err) + return } defer cleanup() @@ -77,7 +80,6 @@ func ExampleRun_withAuthentication() { BuildArgs: map[string]*string{ "REGISTRY_HOST": ®istryHost, }, - PrintBuildLog: true, }, AlwaysPullImage: true, // make sure the authentication takes place ExposedPorts: []string{"6379/tcp"}, @@ -85,18 +87,20 @@ func ExampleRun_withAuthentication() { }, Started: true, }) - if err != nil { - log.Fatalf("failed to start container: %s", err) // nolint:gocritic - } defer func() { - if err := redisC.Terminate(context.Background()); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(redisC); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } state, err := redisC.State(context.Background()) if err != nil { - log.Fatalf("failed to get redis container state: %s", err) // nolint:gocritic + log.Printf("failed to get redis container state: %s", err) + return } fmt.Println(state.Running) @@ -113,18 +117,20 @@ func ExampleRun_pushImage() { registry.WithHtpasswdFile(filepath.Join("testdata", "auth", "htpasswd")), registry.WithData(filepath.Join("testdata", "data")), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } defer func() { - if err := registryContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(registryContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } registryHost, err := registryContainer.HostAddress(ctx) if err != nil { - log.Fatalf("failed to get host: %s", err) // nolint:gocritic + log.Printf("failed to get host: %s", err) + return } // Besides, we are also setting the authentication @@ -135,13 +141,14 @@ func ExampleRun_pushImage() { registryContainer.RegistryName, "testuser", "testpassword", ) if err != nil { - log.Fatalf("failed to set docker auth config: %s", err) // nolint:gocritic + log.Printf("failed to set docker auth config: %s", err) + return } defer cleanup() // build a custom redis image from the private registry, // using RegistryName of the container as the registry. - // We are agoing to build the image with a fixed tag + // We are going to build the image with a fixed tag // that matches the private registry, and we are going to // push it again to the registry after the build. @@ -155,9 +162,8 @@ func ExampleRun_pushImage() { BuildArgs: map[string]*string{ "REGISTRY_HOST": ®istryHost, }, - Repo: repo, - Tag: tag, - PrintBuildLog: true, + Repo: repo, + Tag: tag, }, AlwaysPullImage: true, // make sure the authentication takes place ExposedPorts: []string{"6379/tcp"}, @@ -165,21 +171,23 @@ func ExampleRun_pushImage() { }, Started: true, }) - if err != nil { - log.Fatalf("failed to start container: %s", err) // nolint:gocritic - } defer func() { - if err := redisC.Terminate(context.Background()); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(redisC); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // pushingImage { // repo is localhost:32878/customredis // tag is v1.2.3 err = registryContainer.PushImage(context.Background(), fmt.Sprintf("%s:%s", repo, tag)) if err != nil { - log.Fatalf("failed to push image: %s", err) // nolint:gocritic + log.Printf("failed to push image: %s", err) + return } // } @@ -192,7 +200,8 @@ func ExampleRun_pushImage() { // newImage is customredis:v1.2.3 err = registryContainer.DeleteImage(context.Background(), newImage) if err != nil { - log.Fatalf("failed to delete image: %s", err) // nolint:gocritic + log.Printf("failed to delete image: %s", err) + return } // } @@ -204,18 +213,20 @@ func ExampleRun_pushImage() { }, Started: true, }) - if err != nil { - log.Fatalf("failed to start container from %s: %s", newImage, err) // nolint:gocritic - } defer func() { - if err := newRedisC.Terminate(context.Background()); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(newRedisC); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container from %s: %s", newImage, err) + return + } state, err := newRedisC.State(context.Background()) if err != nil { - log.Fatalf("failed to get redis container state from %s: %s", newImage, err) // nolint:gocritic + log.Printf("failed to get redis container state from %s: %s", newImage, err) + return } fmt.Println(state.Running) diff --git a/modules/registry/go.mod b/modules/registry/go.mod index 36e95dfdf0..c796908087 100644 --- a/modules/registry/go.mod +++ b/modules/registry/go.mod @@ -3,10 +3,10 @@ module github.com/testcontainers/testcontainers-go/modules/registry go 1.22 require ( - github.com/cpuguy83/dockercfg v0.3.1 + github.com/cpuguy83/dockercfg v0.3.2 github.com/docker/docker v27.1.1+incompatible github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -52,9 +52,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/registry/go.sum b/modules/registry/go.sum index 5583e9b0fc..447eec4038 100644 --- a/modules/registry/go.sum +++ b/modules/registry/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -96,6 +96,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -129,8 +131,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -152,14 +154,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/registry/registry.go b/modules/registry/registry.go index b554f8dcc7..1b1c42017c 100644 --- a/modules/registry/registry.go +++ b/modules/registry/registry.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "net" "net/http" @@ -220,10 +221,9 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom // convenient for testing "REGISTRY_STORAGE_DELETE_ENABLED": "true", }, - WaitingFor: wait.ForAll( - wait.ForExposedPort(), - wait.ForLog("listening on [::]:5000").WithStartupTimeout(10*time.Second), - ), + WaitingFor: wait.ForHTTP("/"). + WithPort(registryPort). + WithStartupTimeout(10 * time.Second), } genericContainerReq := testcontainers.GenericContainerRequest{ @@ -238,15 +238,17 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *RegistryContainer + if container != nil { + c = &RegistryContainer{Container: container} + } if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - c := &RegistryContainer{Container: container} - address, err := c.Address(ctx) if err != nil { - return c, err + return c, fmt.Errorf("address: %w", err) } c.RegistryName = strings.TrimPrefix(address, "http://") @@ -285,7 +287,7 @@ func SetDockerAuthConfig(host, username, password string, additional ...string) // triples to add more auth configurations. func DockerAuthConfig(host, username, password string, additional ...string) (map[string]dockercfg.AuthConfig, error) { if len(additional)%3 != 0 { - return nil, fmt.Errorf("additional must be a multiple of 3") + return nil, errors.New("additional must be a multiple of 3") } additional = append(additional, host, username, password) diff --git a/modules/registry/registry_test.go b/modules/registry/registry_test.go index d40f75125e..2b647c2c86 100644 --- a/modules/registry/registry_test.go +++ b/modules/registry/registry_test.go @@ -17,11 +17,11 @@ import ( func TestRegistry_unauthenticated(t *testing.T) { ctx := context.Background() - container, err := registry.Run(ctx, registry.DefaultImage) - terminateContainerOnEnd(t, ctx, container) + ctr, err := registry.Run(ctx, registry.DefaultImage) + testcontainers.CleanupContainer(t, ctr) require.NoError(t, err) - httpAddress, err := container.Address(ctx) + httpAddress, err := ctr.Address(ctx) require.NoError(t, err) resp, err := http.Get(httpAddress + "/v2/_catalog") @@ -39,7 +39,7 @@ func TestRunContainer_authenticated(t *testing.T) { registry.WithHtpasswdFile(filepath.Join("testdata", "auth", "htpasswd")), registry.WithData(filepath.Join("testdata", "data")), ) - terminateContainerOnEnd(t, ctx, registryContainer) + testcontainers.CleanupContainer(t, registryContainer) require.NoError(t, err) // httpAddress { @@ -107,7 +107,7 @@ func TestRunContainer_authenticated(t *testing.T) { }, Started: true, }) - terminateContainerOnEnd(tt, ctx, redisC) + testcontainers.CleanupContainer(tt, redisC) require.Error(tt, err) require.Contains(tt, err.Error(), "unauthorized: authentication required") }) @@ -134,7 +134,7 @@ func TestRunContainer_authenticated(t *testing.T) { }, Started: true, }) - terminateContainerOnEnd(tt, ctx, redisC) + testcontainers.CleanupContainer(tt, redisC) require.NoError(tt, err) state, err := redisC.State(context.Background()) @@ -152,7 +152,7 @@ func TestRunContainer_authenticated_withCredentials(t *testing.T) { registry.WithHtpasswd("testuser:$2y$05$tTymaYlWwJOqie.bcSUUN.I.kxmo1m5TLzYQ4/ejJ46UMXGtq78EO"), ) // } - terminateContainerOnEnd(t, ctx, registryContainer) + testcontainers.CleanupContainer(t, registryContainer) require.NoError(t, err) httpAddress, err := registryContainer.Address(ctx) @@ -179,7 +179,7 @@ func TestRunContainer_wrongData(t *testing.T) { registry.WithHtpasswdFile(filepath.Join("testdata", "auth", "htpasswd")), registry.WithData(filepath.Join("testdata", "wrongdata")), ) - terminateContainerOnEnd(t, ctx, registryContainer) + testcontainers.CleanupContainer(t, registryContainer) require.NoError(t, err) registryHost, err := registryContainer.HostAddress(ctx) @@ -206,9 +206,8 @@ func TestRunContainer_wrongData(t *testing.T) { }, Started: true, }) - terminateContainerOnEnd(t, ctx, redisC) - require.Error(t, err) - require.Contains(t, err.Error(), "manifest unknown") + testcontainers.CleanupContainer(t, redisC) + require.ErrorContains(t, err, "manifest unknown") } // setAuthConfig sets the DOCKER_AUTH_CONFIG environment variable with @@ -223,16 +222,3 @@ func setAuthConfig(t *testing.T, host, username, password string) { t.Setenv("DOCKER_AUTH_CONFIG", string(auth)) } - -// terminateContainerOnEnd terminates the container when the test ends if it is not nil. -func terminateContainerOnEnd(tb testing.TB, ctx context.Context, container testcontainers.Container) { - tb.Helper() - - if container == nil { - return - } - - tb.Cleanup(func() { - require.NoError(tb, container.Terminate(ctx)) - }) -} diff --git a/modules/surrealdb/examples_test.go b/modules/surrealdb/examples_test.go index 2063903d42..7d5c13a598 100644 --- a/modules/surrealdb/examples_test.go +++ b/modules/surrealdb/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/surrealdb" ) @@ -13,21 +14,21 @@ func ExampleRun() { ctx := context.Background() surrealdbContainer, err := surrealdb.Run(ctx, "surrealdb/surrealdb:v1.1.1") - if err != nil { - log.Fatal(err) - } - - // Clean up the container defer func() { - if err := surrealdbContainer.Terminate(ctx); err != nil { - log.Fatal(err) + if err := testcontainers.TerminateContainer(surrealdbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Print(err) + return + } // } state, err := surrealdbContainer.State(ctx) if err != nil { - log.Fatal(err) // nolint:gocritic + log.Print(err) + return } fmt.Println(state.Running) diff --git a/modules/surrealdb/go.mod b/modules/surrealdb/go.mod index d3d2d049d6..8af514ae28 100644 --- a/modules/surrealdb/go.mod +++ b/modules/surrealdb/go.mod @@ -3,8 +3,9 @@ module github.com/testcontainers/testcontainers-go/modules/surrealdb go 1.22 require ( + github.com/stretchr/testify v1.9.0 github.com/surrealdb/surrealdb.go v0.2.1 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 ) require ( @@ -15,7 +16,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -28,6 +30,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -39,6 +42,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -50,11 +54,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/surrealdb/go.sum b/modules/surrealdb/go.sum index 4a82bd3217..059e8ac355 100644 --- a/modules/surrealdb/go.sum +++ b/modules/surrealdb/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -54,6 +55,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -80,6 +85,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -91,6 +98,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -126,8 +135,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -149,14 +158,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -176,6 +185,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/surrealdb/surrealdb.go b/modules/surrealdb/surrealdb.go index 1968ca5d98..cc9ae744dc 100644 --- a/modules/surrealdb/surrealdb.go +++ b/modules/surrealdb/surrealdb.go @@ -116,9 +116,14 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *SurrealDBContainer + if container != nil { + c = &SurrealDBContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &SurrealDBContainer{Container: container}, nil + return c, nil } diff --git a/modules/surrealdb/surrealdb_test.go b/modules/surrealdb/surrealdb_test.go index 7a3de29bfd..5823ca9e2f 100644 --- a/modules/surrealdb/surrealdb_test.go +++ b/modules/surrealdb/surrealdb_test.go @@ -4,133 +4,87 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" "github.com/surrealdb/surrealdb.go" + + "github.com/testcontainers/testcontainers-go" ) func TestSurrealDBSelect(t *testing.T) { ctx := context.Background() - container, err := Run(ctx, "surrealdb/surrealdb:v1.1.1") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := Run(ctx, "surrealdb/surrealdb:v1.1.1") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) - url, err := container.URL(ctx) - if err != nil { - t.Fatal(err) - } + url, err := ctr.URL(ctx) + require.NoError(t, err) db, err := surrealdb.New(url) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if _, err := db.Use("test", "test"); err != nil { - t.Fatal(err) - } + _, err = db.Use("test", "test") + require.NoError(t, err) - if _, err := db.Create("person.tobie", map[string]any{ + _, err = db.Create("person.tobie", map[string]any{ "title": "Founder & CEO", "name": map[string]string{ "first": "Tobie", "last": "Morgan Hitchcock", }, "marketing": true, - }); err != nil { - t.Fatal(err) - } + }) + require.NoError(t, err) result, err := db.Select("person.tobie") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) resultData := result.([]any)[0].(map[string]interface{}) - if resultData["title"] != "Founder & CEO" { - t.Fatal("title is not Founder & CEO") - } - if resultData["name"].(map[string]interface{})["first"] != "Tobie" { - t.Fatal("name.first is not Tobie") - } - if resultData["name"].(map[string]interface{})["last"] != "Morgan Hitchcock" { - t.Fatal("name.last is not Morgan Hitchcock") - } - if resultData["marketing"] != true { - t.Fatal("marketing is not true") - } + require.Equal(t, "Founder & CEO", resultData["title"]) + require.Equal(t, "Tobie", resultData["name"].(map[string]interface{})["first"]) + require.Equal(t, "Morgan Hitchcock", resultData["name"].(map[string]interface{})["last"]) + require.Equal(t, true, resultData["marketing"]) } func TestSurrealDBWithAuth(t *testing.T) { ctx := context.Background() - container, err := Run(ctx, "surrealdb/surrealdb:v1.1.1", WithAuthentication()) - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := Run(ctx, "surrealdb/surrealdb:v1.1.1", WithAuthentication()) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) // websocketURL { - url, err := container.URL(ctx) + url, err := ctr.URL(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) db, err := surrealdb.New(url) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer db.Close() - if _, err := db.Signin(map[string]string{"user": "root", "pass": "root"}); err != nil { - t.Fatal(err) - } + _, err = db.Signin(map[string]string{"user": "root", "pass": "root"}) + require.NoError(t, err) - if _, err := db.Use("test", "test"); err != nil { - t.Fatal(err) - } + _, err = db.Use("test", "test") + require.NoError(t, err) - if _, err := db.Create("person.tobie", map[string]any{ + _, err = db.Create("person.tobie", map[string]any{ "title": "Founder & CEO", "name": map[string]string{ "first": "Tobie", "last": "Morgan Hitchcock", }, "marketing": true, - }); err != nil { - t.Fatal(err) - } + }) + require.NoError(t, err) result, err := db.Select("person.tobie") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) resultData := result.([]any)[0].(map[string]interface{}) - if resultData["title"] != "Founder & CEO" { - t.Fatal("title is not Founder & CEO") - } - if resultData["name"].(map[string]interface{})["first"] != "Tobie" { - t.Fatal("name.first is not Tobie") - } - if resultData["name"].(map[string]interface{})["last"] != "Morgan Hitchcock" { - t.Fatal("name.last is not Morgan Hitchcock") - } - if resultData["marketing"] != true { - t.Fatal("marketing is not true") - } + require.Equal(t, "Founder & CEO", resultData["title"]) + require.Equal(t, "Tobie", resultData["name"].(map[string]interface{})["first"]) + require.Equal(t, "Morgan Hitchcock", resultData["name"].(map[string]interface{})["last"]) + require.Equal(t, true, resultData["marketing"]) } diff --git a/modules/valkey/examples_test.go b/modules/valkey/examples_test.go index b302fc6326..c700e8d3f3 100644 --- a/modules/valkey/examples_test.go +++ b/modules/valkey/examples_test.go @@ -6,6 +6,7 @@ import ( "log" "path/filepath" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/valkey" ) @@ -14,26 +15,26 @@ func ExampleRun() { ctx := context.Background() valkeyContainer, err := valkey.Run(ctx, - "docker.io/valkey/valkey:7.2.5", + "valkey/valkey:7.2.5", valkey.WithSnapshotting(10, 1), valkey.WithLogLevel(valkey.LogLevelVerbose), valkey.WithConfigFile(filepath.Join("testdata", "valkey7.conf")), ) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := valkeyContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(valkeyContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := valkeyContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/valkey/go.mod b/modules/valkey/go.mod index 0c1173f987..1b4105c155 100644 --- a/modules/valkey/go.mod +++ b/modules/valkey/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 github.com/valkey-io/valkey-go v1.0.41 ) @@ -19,7 +19,7 @@ require ( github.com/containerd/containerd v1.7.19 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -55,9 +55,9 @@ require ( go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect - golang.org/x/crypto v0.25.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/modules/valkey/go.sum b/modules/valkey/go.sum index f92f0dbcc7..19e158d82c 100644 --- a/modules/valkey/go.sum +++ b/modules/valkey/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -96,6 +96,8 @@ github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnj github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -128,8 +130,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -149,14 +151,14 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/valkey/valkey.go b/modules/valkey/valkey.go index e3b3728ddd..ac50d54797 100644 --- a/modules/valkey/valkey.go +++ b/modules/valkey/valkey.go @@ -3,6 +3,7 @@ package valkey import ( "context" "fmt" + "strconv" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -48,7 +49,7 @@ func (c *ValkeyContainer) ConnectionString(ctx context.Context) (string, error) // Deprecated: use Run instead // RunContainer creates an instance of the Valkey container type func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*ValkeyContainer, error) { - return Run(ctx, "docker.io/valkey/valkey:7.2.5", opts...) + return Run(ctx, "valkey/valkey:7.2.5", opts...) } // Run creates an instance of the Valkey container type @@ -71,11 +72,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *ValkeyContainer + if container != nil { + c = &ValkeyContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &ValkeyContainer{Container: container}, nil + return c, nil } // WithConfigFile sets the config file to be used for the valkey container, and sets the command to run the valkey server @@ -132,7 +138,7 @@ func WithSnapshotting(seconds int, changedKeys int) testcontainers.CustomizeRequ } return func(req *testcontainers.GenericContainerRequest) error { - processValkeyServerArgs(req, []string{"--save", fmt.Sprintf("%d", seconds), fmt.Sprintf("%d", changedKeys)}) + processValkeyServerArgs(req, []string{"--save", strconv.Itoa(seconds), strconv.Itoa(changedKeys)}) return nil } } diff --git a/modules/valkey/valkey_test.go b/modules/valkey/valkey_test.go index c440f79179..44412afa4a 100644 --- a/modules/valkey/valkey_test.go +++ b/modules/valkey/valkey_test.go @@ -11,19 +11,16 @@ import ( "github.com/stretchr/testify/require" "github.com/valkey-io/valkey-go" + "github.com/testcontainers/testcontainers-go" tcvalkey "github.com/testcontainers/testcontainers-go/modules/valkey" ) func TestIntegrationSetGet(t *testing.T) { ctx := context.Background() - valkeyContainer, err := tcvalkey.Run(ctx, "docker.io/valkey/valkey:7.2.5") + valkeyContainer, err := tcvalkey.Run(ctx, "valkey/valkey:7.2.5") + testcontainers.CleanupContainer(t, valkeyContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := valkeyContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, valkeyContainer, 1) } @@ -31,13 +28,9 @@ func TestIntegrationSetGet(t *testing.T) { func TestValkeyWithConfigFile(t *testing.T) { ctx := context.Background() - valkeyContainer, err := tcvalkey.Run(ctx, "docker.io/valkey/valkey:7.2.5", tcvalkey.WithConfigFile(filepath.Join("testdata", "valkey7.conf"))) + valkeyContainer, err := tcvalkey.Run(ctx, "valkey/valkey:7.2.5", tcvalkey.WithConfigFile(filepath.Join("testdata", "valkey7.conf"))) + testcontainers.CleanupContainer(t, valkeyContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := valkeyContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, valkeyContainer, 1) } @@ -52,19 +45,15 @@ func TestValkeyWithImage(t *testing.T) { // There is only one release of Valkey at the time of writing { name: "Valkey7.2.5", - image: "docker.io/valkey/valkey:7.2.5", + image: "valkey/valkey:7.2.5", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { valkeyContainer, err := tcvalkey.Run(ctx, tt.image, tcvalkey.WithConfigFile(filepath.Join("testdata", "valkey7.conf"))) + testcontainers.CleanupContainer(t, valkeyContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := valkeyContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, valkeyContainer, 1) }) @@ -74,13 +63,9 @@ func TestValkeyWithImage(t *testing.T) { func TestValkeyWithLogLevel(t *testing.T) { ctx := context.Background() - valkeyContainer, err := tcvalkey.Run(ctx, "docker.io/valkey/valkey:7.2.5", tcvalkey.WithLogLevel(tcvalkey.LogLevelVerbose)) + valkeyContainer, err := tcvalkey.Run(ctx, "valkey/valkey:7.2.5", tcvalkey.WithLogLevel(tcvalkey.LogLevelVerbose)) + testcontainers.CleanupContainer(t, valkeyContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := valkeyContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, valkeyContainer, 10) } @@ -88,18 +73,15 @@ func TestValkeyWithLogLevel(t *testing.T) { func TestValkeyWithSnapshotting(t *testing.T) { ctx := context.Background() - valkeyContainer, err := tcvalkey.Run(ctx, "docker.io/valkey/valkey:7.2.5", tcvalkey.WithSnapshotting(10, 1)) + valkeyContainer, err := tcvalkey.Run(ctx, "valkey/valkey:7.2.5", tcvalkey.WithSnapshotting(10, 1)) + testcontainers.CleanupContainer(t, valkeyContainer) require.NoError(t, err) - t.Cleanup(func() { - if err := valkeyContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) assertSetsGets(t, ctx, valkeyContainer, 10) } func assertSetsGets(t *testing.T, ctx context.Context, valkeyContainer *tcvalkey.ValkeyContainer, keyCount int) { + t.Helper() // connectionString { uri, err := valkeyContainer.ConnectionString(ctx) // } @@ -114,6 +96,7 @@ func assertSetsGets(t *testing.T, ctx context.Context, valkeyContainer *tcvalkey client, err := valkey.NewClient(options) require.NoError(t, err) defer func(t *testing.T, ctx context.Context, client *valkey.Client) { + t.Helper() require.NoError(t, flushValkey(ctx, *client)) }(t, ctx, &client) @@ -126,9 +109,7 @@ func assertSetsGets(t *testing.T, ctx context.Context, valkeyContainer *tcvalkey msg, err := res.ToString() require.NoError(t, err) - if msg != "PONG" { - t.Fatalf("received unexpected response from valkey: %s", res.String()) - } + require.Equalf(t, "PONG", msg, "received unexpected response from valkey: %s", res.String()) for i := 0; i < keyCount; i++ { // Set data @@ -149,9 +130,7 @@ func assertSetsGets(t *testing.T, ctx context.Context, valkeyContainer *tcvalkey retVal, err := resp.ToString() require.NoError(t, err) - if retVal != value { - t.Fatalf("Expected value %s. Got %s.", value, retVal) - } + require.Equal(t, retVal, value) } } diff --git a/modules/vault/examples_test.go b/modules/vault/examples_test.go index 75dc908f6b..0b3d257c06 100644 --- a/modules/vault/examples_test.go +++ b/modules/vault/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/exec" "github.com/testcontainers/testcontainers-go/modules/vault" ) @@ -14,21 +15,21 @@ func ExampleRun() { ctx := context.Background() vaultContainer, err := vault.Run(ctx, "hashicorp/vault:1.13.0") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := vaultContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(vaultContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := vaultContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -42,21 +43,21 @@ func ExampleRun_withToken() { ctx := context.Background() vaultContainer, err := vault.Run(ctx, "hashicorp/vault:1.13.0", vault.WithToken("MyToKeN")) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := vaultContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(vaultContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := vaultContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -66,7 +67,8 @@ func ExampleRun_withToken() { } exitCode, _, err := vaultContainer.Exec(ctx, cmds, exec.Multiplexed()) if err != nil { - log.Fatalf("failed to execute command: %s", err) + log.Printf("failed to execute command: %s", err) + return } fmt.Println(exitCode) @@ -87,21 +89,21 @@ func ExampleRun_withInitCommand() { "write --force auth/approle/role/myrole", // Create a role "write secret/testing top_secret=password123", // Create a secret )) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := vaultContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(vaultContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := vaultContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/vault/go.mod b/modules/vault/go.mod index 064a23bcce..3109854195 100644 --- a/modules/vault/go.mod +++ b/modules/vault/go.mod @@ -6,7 +6,7 @@ require ( github.com/docker/docker v27.1.1+incompatible github.com/hashicorp/vault-client-go v0.4.3 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 github.com/tidwall/gjson v1.17.1 ) @@ -18,7 +18,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -62,9 +62,9 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect diff --git a/modules/vault/go.sum b/modules/vault/go.sum index 5bed30736e..09d227fcfe 100644 --- a/modules/vault/go.sum +++ b/modules/vault/go.sum @@ -14,8 +14,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -119,6 +119,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -158,8 +160,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -181,14 +183,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/vault/vault.go b/modules/vault/vault.go index 37237748ed..b679d45c21 100644 --- a/modules/vault/vault.go +++ b/modules/vault/vault.go @@ -52,11 +52,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *VaultContainer + if container != nil { + c = &VaultContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &VaultContainer{container}, nil + return c, nil } // WithToken is a container option function that sets the root token for the Vault diff --git a/modules/vault/vault_test.go b/modules/vault/vault_test.go index 23a52cee57..22b87930bb 100644 --- a/modules/vault/vault_test.go +++ b/modules/vault/vault_test.go @@ -3,7 +3,6 @@ package vault_test import ( "context" "io" - "log" "net/http" "testing" "time" @@ -35,6 +34,7 @@ func TestVault(t *testing.T) { } vaultContainer, err := testcontainervault.Run(ctx, "hashicorp/vault:1.13.0", opts...) + testcontainers.CleanupContainer(t, vaultContainer) require.NoError(t, err) // httpHostAddress { @@ -50,7 +50,7 @@ func TestVault(t *testing.T) { exec, reader, err := vaultContainer.Exec(ctx, []string{"vault", "kv", "get", "-format=json", "secret/test1"}) // } require.NoError(t, err) - assert.Equal(t, 0, exec) + require.Zero(t, exec) bytes, err := io.ReadAll(reader) require.NoError(t, err) @@ -118,11 +118,4 @@ func TestVault(t *testing.T) { assert.Equal(t, "bar", s.Data.Data["foo"]) }) }) - - t.Cleanup(func() { - // Clean up the vault after the test is complete - if err := vaultContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate vault: %s", err) - } - }) } diff --git a/modules/vearch/examples_test.go b/modules/vearch/examples_test.go index d75bd8e66a..97ef8d8fe4 100644 --- a/modules/vearch/examples_test.go +++ b/modules/vearch/examples_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/vearch" ) @@ -13,21 +14,21 @@ func ExampleRun() { ctx := context.Background() vearchContainer, err := vearch.Run(ctx, "vearch/vearch:3.5.1") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := vearchContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(vearchContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := vearchContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) diff --git a/modules/vearch/go.mod b/modules/vearch/go.mod index eaad237db7..596be1a7f8 100644 --- a/modules/vearch/go.mod +++ b/modules/vearch/go.mod @@ -2,7 +2,10 @@ module github.com/testcontainers/testcontainers-go/modules/vearch go 1.22.0 -require github.com/testcontainers/testcontainers-go v0.33.0 +require ( + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 +) require ( dario.cat/mergo v1.0.0 // indirect @@ -12,7 +15,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -24,6 +28,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -35,6 +40,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -46,11 +52,12 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/vearch/go.sum b/modules/vearch/go.sum index 85338720c8..447eec4038 100644 --- a/modules/vearch/go.sum +++ b/modules/vearch/go.sum @@ -14,8 +14,9 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,6 +53,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -78,6 +83,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -89,6 +96,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -122,8 +131,8 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -145,14 +154,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -172,6 +181,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/vearch/vearch.go b/modules/vearch/vearch.go index dccedacad1..a7bcb69083 100644 --- a/modules/vearch/vearch.go +++ b/modules/vearch/vearch.go @@ -2,6 +2,7 @@ package vearch import ( "context" + "errors" "fmt" "time" @@ -52,11 +53,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *VearchContainer + if container != nil { + c = &VearchContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &VearchContainer{Container: container}, nil + return c, nil } // RESTEndpoint returns the REST endpoint of the Vearch container @@ -68,7 +74,7 @@ func (c *VearchContainer) RESTEndpoint(ctx context.Context) (string, error) { host, err := c.Host(ctx) if err != nil { - return "", fmt.Errorf("failed to get container host") + return "", errors.New("failed to get container host") } return fmt.Sprintf("http://%s:%s", host, containerPort.Port()), nil diff --git a/modules/vearch/vearch_test.go b/modules/vearch/vearch_test.go index f73abda7de..43c0f7e1f5 100644 --- a/modules/vearch/vearch_test.go +++ b/modules/vearch/vearch_test.go @@ -5,40 +5,30 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/vearch" ) func TestVearch(t *testing.T) { ctx := context.Background() - container, err := vearch.Run(ctx, "vearch/vearch:3.5.1") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := vearch.Run(ctx, "vearch/vearch:3.5.1") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) t.Run("REST Endpoint", func(tt *testing.T) { // restEndpoint { - restEndpoint, err := container.RESTEndpoint(ctx) + restEndpoint, err := ctr.RESTEndpoint(ctx) // } - if err != nil { - tt.Fatalf("failed to get REST endpoint: %s", err) - } + require.NoError(t, err) cli := &http.Client{} resp, err := cli.Get(restEndpoint) - if err != nil { - tt.Fatalf("failed to perform GET request: %s", err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - tt.Fatalf("unexpected status code: %d", resp.StatusCode) - } + + require.Equal(t, http.StatusOK, resp.StatusCode) }) } diff --git a/modules/weaviate/examples_test.go b/modules/weaviate/examples_test.go index d6c8f50988..443f782413 100644 --- a/modules/weaviate/examples_test.go +++ b/modules/weaviate/examples_test.go @@ -20,21 +20,21 @@ func ExampleRun() { ctx := context.Background() weaviateContainer, err := tcweaviate.Run(ctx, "semitechnologies/weaviate:1.24.5") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - - // Clean up the container defer func() { - if err := weaviateContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(weaviateContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } // } state, err := weaviateContainer.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -48,30 +48,32 @@ func ExampleRun_connectWithClient() { ctx := context.Background() weaviateContainer, err := tcweaviate.Run(ctx, "semitechnologies/weaviate:1.23.9") - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := weaviateContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(weaviateContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } scheme, host, err := weaviateContainer.HttpHostAddress(ctx) if err != nil { - log.Fatalf("failed to get http schema and host: %s", err) // nolint:gocritic + log.Printf("failed to get http schema and host: %s", err) + return } grpcHost, err := weaviateContainer.GrpcHostAddress(ctx) if err != nil { - log.Fatalf("failed to get gRPC host: %s", err) // nolint:gocritic + log.Printf("failed to get gRPC host: %s", err) + return } connectionClient := &http.Client{} headers := map[string]string{ // put here the custom API key, e.g. for OpenAPI - "Authorization": fmt.Sprintf("Bearer %s", "custom-api-key"), + "Authorization": "Bearer custom-api-key", } cli := weaviate.New(weaviate.Config{ @@ -115,30 +117,32 @@ func ExampleRun_connectWithClientWithModules() { } weaviateContainer, err := tcweaviate.Run(ctx, "semitechnologies/weaviate:1.25.5", opts...) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := weaviateContainer.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + if err := testcontainers.TerminateContainer(weaviateContainer); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } scheme, host, err := weaviateContainer.HttpHostAddress(ctx) if err != nil { - log.Fatalf("failed to get http schema and host: %s", err) // nolint:gocritic + log.Printf("failed to get http schema and host: %s", err) + return } grpcHost, err := weaviateContainer.GrpcHostAddress(ctx) if err != nil { - log.Fatalf("failed to get gRPC host: %s", err) // nolint:gocritic + log.Printf("failed to get gRPC host: %s", err) + return } connectionClient := &http.Client{} headers := map[string]string{ // put here the custom API key, e.g. for OpenAPI - "Authorization": fmt.Sprintf("Bearer %s", "custom-api-key"), + "Authorization": "Bearer custom-api-key", } cli := weaviate.New(weaviate.Config{ diff --git a/modules/weaviate/go.mod b/modules/weaviate/go.mod index 6572fb3607..e99308f1f5 100644 --- a/modules/weaviate/go.mod +++ b/modules/weaviate/go.mod @@ -3,7 +3,8 @@ module github.com/testcontainers/testcontainers-go/modules/weaviate go 1.22 require ( - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 github.com/weaviate/weaviate-go-client/v4 v4.13.1 google.golang.org/grpc v1.64.1 ) @@ -19,7 +20,8 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -56,6 +58,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -69,11 +72,11 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/protobuf v1.33.0 // indirect diff --git a/modules/weaviate/go.sum b/modules/weaviate/go.sum index 4d55fd59af..da152d0a52 100644 --- a/modules/weaviate/go.sum +++ b/modules/weaviate/go.sum @@ -22,8 +22,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -203,6 +203,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -262,8 +264,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -308,20 +310,20 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/modules/weaviate/weaviate.go b/modules/weaviate/weaviate.go index 93665efc66..e773174d57 100644 --- a/modules/weaviate/weaviate.go +++ b/modules/weaviate/weaviate.go @@ -2,6 +2,7 @@ package weaviate import ( "context" + "errors" "fmt" "time" @@ -54,11 +55,16 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom } container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *WeaviateContainer + if container != nil { + c = &WeaviateContainer{Container: container} + } + if err != nil { - return nil, err + return c, fmt.Errorf("generic container: %w", err) } - return &WeaviateContainer{Container: container}, nil + return c, nil } // HttpHostAddress returns the schema and host of the Weaviate container. @@ -71,7 +77,7 @@ func (c *WeaviateContainer) HttpHostAddress(ctx context.Context) (string, string host, err := c.Host(ctx) if err != nil { - return "", "", fmt.Errorf("failed to get container host") + return "", "", errors.New("failed to get container host") } return "http", fmt.Sprintf("%s:%s", host, port.Port()), nil @@ -87,7 +93,7 @@ func (c *WeaviateContainer) GrpcHostAddress(ctx context.Context) (string, error) host, err := c.Host(ctx) if err != nil { - return "", fmt.Errorf("failed to get container host") + return "", errors.New("failed to get container host") } return fmt.Sprintf("%s:%s", host, port.Port()), nil diff --git a/modules/weaviate/weaviate_test.go b/modules/weaviate/weaviate_test.go index bb44e7a90a..c85a25726c 100644 --- a/modules/weaviate/weaviate_test.go +++ b/modules/weaviate/weaviate_test.go @@ -6,96 +6,67 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" wvt "github.com/weaviate/weaviate-go-client/v4/weaviate" wvtgrpc "github.com/weaviate/weaviate-go-client/v4/weaviate/grpc" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/health/grpc_health_v1" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/weaviate" ) func TestWeaviate(t *testing.T) { ctx := context.Background() - container, err := weaviate.Run(ctx, "semitechnologies/weaviate:1.25.5") - if err != nil { - t.Fatal(err) - } - - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := weaviate.Run(ctx, "semitechnologies/weaviate:1.25.5") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) t.Run("HttpHostAddress", func(tt *testing.T) { // httpHostAddress { - schema, host, err := container.HttpHostAddress(ctx) + schema, host, err := ctr.HttpHostAddress(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) cli := &http.Client{} resp, err := cli.Get(fmt.Sprintf("%s://%s", schema, host)) - if err != nil { - tt.Fatalf("failed to perform GET request: %s", err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - tt.Fatalf("unexpected status code: %d", resp.StatusCode) - } + require.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("GrpcHostAddress", func(tt *testing.T) { // gRPCHostAddress { - host, err := container.GrpcHostAddress(ctx) + host, err := ctr.GrpcHostAddress(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var opts []grpc.DialOption opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) conn, err := grpc.NewClient(host, opts...) - if err != nil { - tt.Fatalf("failed to dial connection: %v", err) - } + require.NoErrorf(t, err, "failed to dial connection") client := grpc_health_v1.NewHealthClient(conn) check, err := client.Check(context.TODO(), &grpc_health_v1.HealthCheckRequest{}) - if err != nil { - tt.Fatalf("failed to get a health check: %v", err) - } - if grpc_health_v1.HealthCheckResponse_SERVING.Enum().Number() != check.Status.Number() { - tt.Fatalf("unexpected status code: %d", check.Status.Number()) - } + require.NoErrorf(t, err, "failed to get a health check") + require.Equalf(t, grpc_health_v1.HealthCheckResponse_SERVING.Enum().Number(), check.Status.Number(), "unexpected status code: %d", check.Status.Number()) }) t.Run("Weaviate client", func(tt *testing.T) { - httpScheme, httpHost, err := container.HttpHostAddress(ctx) - if err != nil { - tt.Fatal(err) - } - grpcHost, err := container.GrpcHostAddress(ctx) - if err != nil { - tt.Fatal(err) - } + httpScheme, httpHost, err := ctr.HttpHostAddress(ctx) + require.NoError(tt, err) + grpcHost, err := ctr.GrpcHostAddress(ctx) + require.NoError(tt, err) config := wvt.Config{Scheme: httpScheme, Host: httpHost, GrpcConfig: &wvtgrpc.Config{Host: grpcHost}} client, err := wvt.NewClient(config) - if err != nil { - tt.Fatal(err) - } + require.NoError(tt, err) meta, err := client.Misc().MetaGetter().Do(ctx) - if err != nil { - tt.Fatal(err) - } + require.NoError(tt, err) - if meta == nil || meta.Version == "" { - tt.Fatal("failed to get /v1/meta response") - } + require.NotNilf(tt, meta, "failed to get /v1/meta response") + require.NotEmptyf(tt, meta.Version, "failed to get /v1/meta response") }) } diff --git a/modules/yugabytedb/Makefile b/modules/yugabytedb/Makefile new file mode 100644 index 0000000000..a56dee99f2 --- /dev/null +++ b/modules/yugabytedb/Makefile @@ -0,0 +1,5 @@ +include ../../commons-test.mk + +.PHONY: test +test: + $(MAKE) test-yugabytedb diff --git a/modules/yugabytedb/examples_test.go b/modules/yugabytedb/examples_test.go new file mode 100644 index 0000000000..641fc5a53f --- /dev/null +++ b/modules/yugabytedb/examples_test.go @@ -0,0 +1,155 @@ +package yugabytedb_test + +import ( + "context" + "database/sql" + "fmt" + "log" + "net" + + _ "github.com/lib/pq" + "github.com/yugabyte/gocql" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/yugabytedb" +) + +func ExampleRun() { + // runyugabyteDBContainer { + ctx := context.Background() + + yugabytedbContainer, err := yugabytedb.Run( + ctx, + "yugabytedb/yugabyte:2024.1.3.0-b105", + yugabytedb.WithKeyspace("custom-keyspace"), + yugabytedb.WithUser("custom-user"), + yugabytedb.WithDatabaseName("custom-db"), + yugabytedb.WithDatabaseUser("custom-user"), + yugabytedb.WithDatabasePassword("custom-password"), + ) + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + + defer func() { + if err := testcontainers.TerminateContainer(yugabytedbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + // } + + state, err := yugabytedbContainer.State(ctx) + if err != nil { + log.Printf("failed to get container state: %s", err) + return + } + + fmt.Println(state.Running) + + // Output: true +} + +func ExampleContainer_YSQLConnectionString() { + ctx := context.Background() + + yugabytedbContainer, err := yugabytedb.Run( + ctx, + "yugabytedb/yugabyte:2024.1.3.0-b105", + ) + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + + defer func() { + if err := testcontainers.TerminateContainer(yugabytedbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + + connStr, err := yugabytedbContainer.YSQLConnectionString(ctx, "sslmode=disable") + if err != nil { + log.Printf("failed to get connection string: %s", err) + return + } + + db, err := sql.Open("postgres", connStr) + if err != nil { + log.Printf("failed to open connection: %s", err) + return + } + + defer db.Close() + + var i int + row := db.QueryRowContext(ctx, "SELECT 1") + if err := row.Scan(&i); err != nil { + log.Printf("failed to scan row: %s", err) + return + } + + fmt.Println(i) + + // Output: 1 +} + +func ExampleContainer_newCluster() { + ctx := context.Background() + + yugabytedbContainer, err := yugabytedb.Run( + ctx, + "yugabytedb/yugabyte:2024.1.3.0-b105", + ) + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + + defer func() { + if err := testcontainers.TerminateContainer(yugabytedbContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + + yugabytedbContainerHost, err := yugabytedbContainer.Host(ctx) + if err != nil { + log.Printf("failed to get container host: %s", err) + return + } + + yugabyteContainerPort, err := yugabytedbContainer.MappedPort(ctx, "9042/tcp") + if err != nil { + log.Printf("failed to get container port: %s", err) + return + } + + cluster := gocql.NewCluster(net.JoinHostPort(yugabytedbContainerHost, yugabyteContainerPort.Port())) + cluster.Keyspace = "yugabyte" + cluster.Authenticator = gocql.PasswordAuthenticator{ + Username: "yugabyte", + Password: "yugabyte", + } + + session, err := cluster.CreateSession() + if err != nil { + log.Printf("failed to create session: %s", err) + return + } + + defer session.Close() + + var i int + if err := session.Query(` + SELECT COUNT(*) + FROM system_schema.keyspaces + WHERE keyspace_name = 'yugabyte' + `).Scan(&i); err != nil { + log.Printf("failed to scan row: %s", err) + return + } + + fmt.Println(i) + + // Output: 1 +} diff --git a/modules/yugabytedb/go.mod b/modules/yugabytedb/go.mod new file mode 100644 index 0000000000..57d7f8b64b --- /dev/null +++ b/modules/yugabytedb/go.mod @@ -0,0 +1,65 @@ +module github.com/testcontainers/testcontainers-go/modules/yugabytedb + +go 1.22 + +require ( + github.com/lib/pq v1.10.9 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.34.0 + github.com/yugabyte/gocql v1.6.0-yb-1 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/yugabytedb/go.sum b/modules/yugabytedb/go.sum new file mode 100644 index 0000000000..4fa4e1cea6 --- /dev/null +++ b/modules/yugabytedb/go.sum @@ -0,0 +1,209 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yugabyte/gocql v1.6.0-yb-1 h1:3anNiHsJwKQ8Dn7RdmkTEuIzV1l7e9QJZ8wkOZ87ELg= +github.com/yugabyte/gocql v1.6.0-yb-1/go.mod h1:LAokR6+vevDCrTxk52U7p6ki+4qELu4XU7JUGYa2O2M= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/modules/yugabytedb/options.go b/modules/yugabytedb/options.go new file mode 100644 index 0000000000..485b979468 --- /dev/null +++ b/modules/yugabytedb/options.go @@ -0,0 +1,53 @@ +package yugabytedb + +import ( + "github.com/testcontainers/testcontainers-go" +) + +// WithDatabaseName sets the initial database name for the yugabyteDB container. +func WithDatabaseName(dbName string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Env[ysqlDatabaseNameEnv] = dbName + return nil + } +} + +// WithDatabaseUser sets the initial database user for the yugabyteDB container. +func WithDatabaseUser(dbUser string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Env[ysqlDatabaseUserEnv] = dbUser + return nil + } +} + +// WithDatabasePassword sets the initial database password for the yugabyteDB container. +func WithDatabasePassword(dbPassword string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Env[ysqlDatabasePasswordEnv] = dbPassword + return nil + } +} + +// WithKeyspace sets the initial keyspace for the yugabyteDB container. +func WithKeyspace(keyspace string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Env[ycqlKeyspaceEnv] = keyspace + return nil + } +} + +// WithUser sets the initial user for the yugabyteDB container. +func WithUser(user string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Env[ycqlUserNameEnv] = user + return nil + } +} + +// WithPassword sets the initial password for the yugabyteDB container. +func WithPassword(password string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Env[ycqlPasswordEnv] = password + return nil + } +} diff --git a/modules/yugabytedb/yugabytedb.go b/modules/yugabytedb/yugabytedb.go new file mode 100644 index 0000000000..13d6e9ccb0 --- /dev/null +++ b/modules/yugabytedb/yugabytedb.go @@ -0,0 +1,126 @@ +package yugabytedb + +import ( + "context" + "fmt" + "net" + "strings" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + ycqlPort = "9042/tcp" + + ycqlKeyspaceEnv = "YCQL_KEYSPACE" + ycqlUserNameEnv = "YCQL_USER" + ycqlPasswordEnv = "YCQL_PASSWORD" + + ycqlKeyspace = "yugabyte" + ycqlUserName = "yugabyte" + ycqlPassword = "yugabyte" +) + +const ( + ysqlPort = "5433/tcp" + + ysqlDatabaseNameEnv = "YSQL_DB" + ysqlDatabaseUserEnv = "YSQL_USER" + ysqlDatabasePasswordEnv = "YSQL_PASSWORD" + + ysqlDatabaseName = "yugabyte" + ysqlDatabaseUser = "yugabyte" + ysqlDatabasePassword = "yugabyte" +) + +// Container represents the yugabyteDB container type used in the module +type Container struct { + testcontainers.Container + + ysqlDatabaseName string + ysqlDatabaseUser string + ysqlDatabasePassword string +} + +// Run creates an instance of the yugabyteDB container type and automatically starts it. +// A default configuration is used for the container, but it can be customized using the +// provided options. +// When using default configuration values it is recommended to use the provided +// [*Container.YSQLConnectionString] and [*Container.YCQLConfigureClusterConfig] +// methods to use the container in their respective clients. +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) { + req := testcontainers.ContainerRequest{ + Image: img, + Cmd: []string{"bin/yugabyted", "start", "--background=false"}, + WaitingFor: wait.ForAll( + wait.ForLog("YugabyteDB Started").WithOccurrence(1), + wait.ForLog("Data placement constraint successfully verified").WithOccurrence(1), + wait.ForListeningPort(ysqlPort), + wait.ForListeningPort(ycqlPort), + ), + ExposedPorts: []string{ycqlPort, ysqlPort}, + Env: map[string]string{ + ycqlKeyspaceEnv: ycqlKeyspace, + ycqlUserNameEnv: ycqlUserName, + ycqlPasswordEnv: ycqlPassword, + ysqlDatabaseNameEnv: ysqlDatabaseName, + ysqlDatabaseUserEnv: ysqlDatabaseUser, + ysqlDatabasePasswordEnv: ysqlDatabasePassword, + }, + } + + genericContainerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + for _, opt := range opts { + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, fmt.Errorf("customize: %w", err) + } + } + + container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + var c *Container + if container != nil { + c = &Container{ + Container: container, + ysqlDatabaseName: req.Env[ysqlDatabaseNameEnv], + ysqlDatabaseUser: req.Env[ysqlDatabaseUserEnv], + ysqlDatabasePassword: req.Env[ysqlDatabasePasswordEnv], + } + } + + if err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + return c, nil +} + +// YSQLConnectionString returns a connection string for the yugabyteDB container +// using the configured database name, user, password, port, host and additional +// arguments. +// Additional arguments are appended to the connection string as query parameters +// in the form of key=value pairs separated by "&". +func (y *Container) YSQLConnectionString(ctx context.Context, args ...string) (string, error) { + host, err := y.Host(ctx) + if err != nil { + return "", fmt.Errorf("host: %w", err) + } + + mappedPort, err := y.MappedPort(ctx, ysqlPort) + if err != nil { + return "", fmt.Errorf("mapped port: %w", err) + } + + return fmt.Sprintf( + "postgres://%s:%s@%s/%s?%s", + y.ysqlDatabaseUser, + y.ysqlDatabasePassword, + net.JoinHostPort(host, mappedPort.Port()), + y.ysqlDatabaseName, + strings.Join(args, "&"), + ), nil +} diff --git a/modules/yugabytedb/yugabytedb_test.go b/modules/yugabytedb/yugabytedb_test.go new file mode 100644 index 0000000000..38a93f0c89 --- /dev/null +++ b/modules/yugabytedb/yugabytedb_test.go @@ -0,0 +1,129 @@ +package yugabytedb_test + +import ( + "context" + "database/sql" + "fmt" + "net" + "testing" + + _ "github.com/lib/pq" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yugabyte/gocql" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/yugabytedb" +) + +func TestYugabyteDB_YSQL(t *testing.T) { + t.Run("Run", func(t *testing.T) { + ctx := context.Background() + + ctr, err := yugabytedb.Run(ctx, "yugabytedb/yugabyte:2024.1.3.0-b105") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + ctrHost, err := ctr.Host(ctx) + require.NoError(t, err) + + ctrPort, err := ctr.MappedPort(ctx, "5433/tcp") + require.NoError(t, err) + + ysqlConnStr, err := ctr.YSQLConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("postgres://yugabyte:yugabyte@%s:%s/yugabyte?sslmode=disable", ctrHost, ctrPort.Port()), ysqlConnStr) + + db, err := sql.Open("postgres", ysqlConnStr) + require.NoError(t, err) + require.NotNil(t, db) + + err = db.Ping() + require.NoError(t, err) + }) + + t.Run("custom-options", func(t *testing.T) { + ctx := context.Background() + ctr, err := yugabytedb.Run(ctx, "yugabytedb/yugabyte:2024.1.3.0-b105", + yugabytedb.WithDatabaseName("custom-db"), + yugabytedb.WithDatabaseUser("custom-user"), + yugabytedb.WithDatabasePassword("custom-password"), + ) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + ctrHost, err := ctr.Host(ctx) + require.NoError(t, err) + + ctrPort, err := ctr.MappedPort(ctx, "5433/tcp") + require.NoError(t, err) + + ysqlConnStr, err := ctr.YSQLConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("postgres://custom-user:custom-password@%s:%s/custom-db?sslmode=disable", ctrHost, ctrPort.Port()), ysqlConnStr) + + db, err := sql.Open("postgres", ysqlConnStr) + require.NoError(t, err) + require.NotNil(t, db) + + err = db.Ping() + require.NoError(t, err) + }) +} + +func TestYugabyteDB_YCQL(t *testing.T) { + t.Run("Run", func(t *testing.T) { + ctx := context.Background() + + ctr, err := yugabytedb.Run(ctx, "yugabytedb/yugabyte:2024.1.3.0-b105") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + ctrHost, err := ctr.Host(ctx) + require.NoError(t, err) + + ctrPort, err := ctr.MappedPort(ctx, "9042/tcp") + require.NoError(t, err) + + cluster := gocql.NewCluster(net.JoinHostPort(ctrHost, ctrPort.Port())) + cluster.Keyspace = "yugabyte" + cluster.Authenticator = gocql.PasswordAuthenticator{ + Username: "yugabyte", + Password: "yugabyte", + } + + session, err := cluster.CreateSession() + require.NoError(t, err) + session.Close() + }) + + t.Run("custom-options", func(t *testing.T) { + ctx := context.Background() + + ctr, err := yugabytedb.Run(ctx, "yugabytedb/yugabyte:2024.1.3.0-b105", + yugabytedb.WithKeyspace("custom-keyspace"), + yugabytedb.WithUser("custom-user"), + yugabytedb.WithPassword("custom-password"), + ) + + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + ctrHost, err := ctr.Host(ctx) + require.NoError(t, err) + + ctrPort, err := ctr.MappedPort(ctx, "9042/tcp") + require.NoError(t, err) + + cluster := gocql.NewCluster(net.JoinHostPort(ctrHost, ctrPort.Port())) + cluster.Keyspace = "custom-keyspace" + cluster.Authenticator = gocql.PasswordAuthenticator{ + Username: "custom-user", + Password: "custom-password", + } + + session, err := cluster.CreateSession() + require.NoError(t, err) + session.Close() + }) +} diff --git a/mounts_test.go b/mounts_test.go index 533b584feb..b1ac51d305 100644 --- a/mounts_test.go +++ b/mounts_test.go @@ -171,13 +171,14 @@ func TestContainerMounts_PrepareMounts(t *testing.T) { } func TestCreateContainerWithVolume(t *testing.T) { + volumeName := "test-volume" // volumeMounts { req := testcontainers.ContainerRequest{ Image: "alpine", Mounts: testcontainers.ContainerMounts{ { Source: testcontainers.GenericVolumeMountSource{ - Name: "test-volume", + Name: volumeName, }, Target: "/data", }, @@ -190,8 +191,8 @@ func TestCreateContainerWithVolume(t *testing.T) { ContainerRequest: req, Started: true, }) + testcontainers.CleanupContainer(t, c, testcontainers.RemoveVolumes(volumeName)) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) // Check if volume is created client, err := testcontainers.NewDockerClientWithOpts(ctx) @@ -204,12 +205,13 @@ func TestCreateContainerWithVolume(t *testing.T) { } func TestMountsReceiveRyukLabels(t *testing.T) { + volumeName := "app-data" req := testcontainers.ContainerRequest{ Image: "alpine", Mounts: testcontainers.ContainerMounts{ { Source: testcontainers.GenericVolumeMountSource{ - Name: "app-data", + Name: volumeName, }, Target: "/data", }, @@ -223,18 +225,18 @@ func TestMountsReceiveRyukLabels(t *testing.T) { // Ensure the volume is removed before creating the container // otherwise the volume will be reused and the labels won't be set. - err = client.VolumeRemove(ctx, "app-data", true) + err = client.VolumeRemove(ctx, volumeName, true) require.NoError(t, err) c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) + testcontainers.CleanupContainer(t, c, testcontainers.RemoveVolumes(volumeName)) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) // Check if volume is created with the expected labels. - volume, err := client.VolumeInspect(ctx, "app-data") + volume, err := client.VolumeInspect(ctx, volumeName) require.NoError(t, err) require.Equal(t, testcontainers.GenericLabels(), volume.Labels) } diff --git a/network.go b/network.go index 9544bee129..e0cc83f510 100644 --- a/network.go +++ b/network.go @@ -4,6 +4,8 @@ import ( "context" "github.com/docker/docker/api/types/network" + + "github.com/testcontainers/testcontainers-go/internal/core" ) // NetworkProvider allows the creation of networks on an arbitrary system @@ -23,12 +25,12 @@ type DefaultNetwork string // Deprecated: will be removed in the future. func (n DefaultNetwork) ApplyGenericTo(opts *GenericProviderOptions) { - opts.DefaultNetwork = string(n) + opts.defaultNetwork = string(n) } // Deprecated: will be removed in the future. func (n DefaultNetwork) ApplyDockerTo(opts *DockerProviderOptions) { - opts.DefaultNetwork = string(n) + opts.defaultNetwork = string(n) } // Deprecated: will be removed in the future @@ -47,3 +49,12 @@ type NetworkRequest struct { ReaperImage string // Deprecated: use WithImageName ContainerOption instead. Alternative reaper registry ReaperOptions []ContainerOption // Deprecated: the reaper is configured at the properties level, for an entire test session } + +// sessionID returns the session ID for the network request. +func (r NetworkRequest) sessionID() string { + if sessionID := r.Labels[core.LabelSessionID]; sessionID != "" { + return sessionID + } + + return core.SessionID() +} diff --git a/network/examples_test.go b/network/examples_test.go index 7335b85564..a6b6bec495 100644 --- a/network/examples_test.go +++ b/network/examples_test.go @@ -21,7 +21,7 @@ func ExampleNew() { } defer func() { if err := net.Remove(ctx); err != nil { - log.Fatalf("failed to remove network: %s", err) + log.Printf("failed to remove network: %s", err) } }() // } @@ -62,7 +62,7 @@ func ExampleNew_withOptions() { } defer func() { if err := net.Remove(ctx); err != nil { - log.Fatalf("failed to remove network: %s", err) + log.Printf("failed to remove network: %s", err) } }() // } diff --git a/network/network_test.go b/network/network_test.go index 4ff80e2bed..bbe5d45c7c 100644 --- a/network/network_test.go +++ b/network/network_test.go @@ -7,7 +7,6 @@ import ( "github.com/docker/docker/api/types/filters" dockernetwork "github.com/docker/docker/api/types/network" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" @@ -17,7 +16,7 @@ import ( ) const ( - nginxAlpineImage = "docker.io/nginx:alpine" + nginxAlpineImage = "nginx:alpine" nginxDefaultPort = "80/tcp" ) @@ -26,23 +25,19 @@ func TestNew(t *testing.T) { net, err := network.New(ctx, network.WithAttachable(), - // Makes the network internal only, meaning the host machine cannot access it. - // Remove or use `network.WithDriver("bridge")` to change the network's mode. - network.WithInternal(), + network.WithDriver("bridge"), network.WithLabels(map[string]string{"this-is-a-test": "value"}), ) require.NoError(t, err) defer func() { - if err := net.Remove(ctx); err != nil { - t.Fatalf("failed to remove network: %s", err) - } + require.NoError(t, net.Remove(ctx)) }() networkName := net.Name - nginxC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ - Image: "nginx:alpine", + Image: nginxAlpineImage, ExposedPorts: []string{ "80/tcp", }, @@ -52,11 +47,8 @@ func TestNew(t *testing.T) { }, Started: true, }) - defer func() { - if err := nginxC.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }() + testcontainers.CleanupContainer(t, nginxC) + require.NoError(t, err) client, err := testcontainers.NewDockerClientWithOpts(context.Background()) require.NoError(t, err) @@ -65,19 +57,16 @@ func TestNew(t *testing.T) { Filters: filters.NewArgs(filters.Arg("name", networkName)), }) require.NoError(t, err) - - assert.Len(t, resources, 1) + require.Len(t, resources, 1) newNetwork := resources[0] - expectedLabels := testcontainers.GenericLabels() expectedLabels["this-is-a-test"] = "true" - assert.True(t, newNetwork.Attachable) - assert.True(t, newNetwork.Internal) - assert.Equal(t, "value", newNetwork.Labels["this-is-a-test"]) - - require.NoError(t, err) + require.True(t, newNetwork.Attachable) + require.False(t, newNetwork.Internal) + require.Equal(t, "value", newNetwork.Labels["this-is-a-test"]) + require.NoError(t, testcontainers.TerminateContainer(nginxC)) } // testNetworkAliases { @@ -85,12 +74,8 @@ func TestContainerAttachedToNewNetwork(t *testing.T) { ctx := context.Background() newNetwork, err := network.New(ctx) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - require.NoError(t, newNetwork.Remove(ctx)) - }) + require.NoError(t, err) + testcontainers.CleanupNetwork(t, newNetwork) networkName := newNetwork.Name @@ -113,33 +98,21 @@ func TestContainerAttachedToNewNetwork(t *testing.T) { } nginx, err := testcontainers.GenericContainer(ctx, gcr) + testcontainers.CleanupContainer(t, nginx) require.NoError(t, err) - defer func() { - require.NoError(t, nginx.Terminate(ctx)) - }() networks, err := nginx.Networks(ctx) - if err != nil { - t.Fatal(err) - } - if len(networks) != 1 { - t.Errorf("Expected networks 1. Got '%d'.", len(networks)) - } + require.NoError(t, err) + require.Len(t, networks, 1) + nw := networks[0] - if nw != networkName { - t.Errorf("Expected network name '%s'. Got '%s'.", networkName, nw) - } + require.Equal(t, networkName, nw) networkAliases, err := nginx.NetworkAliases(ctx) - if err != nil { - t.Fatal(err) - } - if len(networkAliases) != 1 { - t.Errorf("Expected network aliases for 1 network. Got '%d'.", len(networkAliases)) - } + require.NoError(t, err) + require.Len(t, networkAliases, 1) networkAlias := networkAliases[networkName] - require.NotEmpty(t, networkAlias) for _, alias := range aliases { @@ -147,12 +120,8 @@ func TestContainerAttachedToNewNetwork(t *testing.T) { } networkIP, err := nginx.ContainerIP(ctx) - if err != nil { - t.Fatal(err) - } - if len(networkIP) == 0 { - t.Errorf("Expected an IP address, got %v", networkIP) - } + require.NoError(t, err) + require.NotEmpty(t, networkIP) } // } @@ -161,12 +130,8 @@ func TestContainerIPs(t *testing.T) { ctx := context.Background() newNetwork, err := network.New(ctx) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - require.NoError(t, newNetwork.Remove(ctx)) - }) + require.NoError(t, err) + testcontainers.CleanupNetwork(t, newNetwork) networkName := newNetwork.Name @@ -184,19 +149,12 @@ func TestContainerIPs(t *testing.T) { }, Started: true, }) + testcontainers.CleanupContainer(t, nginx) require.NoError(t, err) - defer func() { - require.NoError(t, nginx.Terminate(ctx)) - }() ips, err := nginx.ContainerIPs(ctx) - if err != nil { - t.Fatal(err) - } - - if len(ips) != 2 { - t.Errorf("Expected two IP addresses, got %v", len(ips)) - } + require.NoError(t, err) + require.Len(t, ips, 2) } func TestContainerWithReaperNetwork(t *testing.T) { @@ -212,10 +170,7 @@ func TestContainerWithReaperNetwork(t *testing.T) { for i := 0; i < maxNetworksCount; i++ { n, err := network.New(ctx) require.NoError(t, err) - // use t.Cleanup to run after terminateContainerOnEnd - t.Cleanup(func() { - require.NoError(t, n.Remove(ctx)) - }) + testcontainers.CleanupNetwork(t, n) networks = append(networks, n.Name) } @@ -232,11 +187,8 @@ func TestContainerWithReaperNetwork(t *testing.T) { }, Started: true, }) - + testcontainers.CleanupContainer(t, nginx) require.NoError(t, err) - defer func() { - require.NoError(t, nginx.Terminate(ctx)) - }() containerId := nginx.GetContainerID() @@ -246,21 +198,17 @@ func TestContainerWithReaperNetwork(t *testing.T) { cnt, err := cli.ContainerInspect(ctx, containerId) require.NoError(t, err) - assert.Len(t, cnt.NetworkSettings.Networks, maxNetworksCount) - assert.NotNil(t, cnt.NetworkSettings.Networks[networks[0]]) - assert.NotNil(t, cnt.NetworkSettings.Networks[networks[1]]) + require.Len(t, cnt.NetworkSettings.Networks, maxNetworksCount) + require.NotNil(t, cnt.NetworkSettings.Networks[networks[0]]) + require.NotNil(t, cnt.NetworkSettings.Networks[networks[1]]) } func TestMultipleContainersInTheNewNetwork(t *testing.T) { ctx := context.Background() net, err := network.New(ctx, network.WithDriver("bridge")) - if err != nil { - t.Fatal("cannot create network") - } - defer func() { - require.NoError(t, net.Remove(ctx)) - }() + require.NoError(t, err) + testcontainers.CleanupNetwork(t, net) networkName := net.Name @@ -271,12 +219,8 @@ func TestMultipleContainersInTheNewNetwork(t *testing.T) { }, Started: true, }) - if err != nil { - t.Fatal(err) - } - defer func() { - require.NoError(t, c1.Terminate(ctx)) - }() + testcontainers.CleanupContainer(t, c1) + require.NoError(t, err) c2, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ @@ -285,13 +229,8 @@ func TestMultipleContainersInTheNewNetwork(t *testing.T) { }, Started: true, }) - if err != nil { - t.Fatal(err) - return - } - defer func() { - require.NoError(t, c2.Terminate(ctx)) - }() + testcontainers.CleanupContainer(t, c2) + require.NoError(t, err) pNets, err := c1.Networks(ctx) require.NoError(t, err) @@ -299,11 +238,11 @@ func TestMultipleContainersInTheNewNetwork(t *testing.T) { rNets, err := c2.Networks(ctx) require.NoError(t, err) - assert.Len(t, pNets, 1) - assert.Len(t, rNets, 1) + require.Len(t, pNets, 1) + require.Len(t, rNets, 1) - assert.Equal(t, networkName, pNets[0]) - assert.Equal(t, networkName, rNets[0]) + require.Equal(t, networkName, pNets[0]) + require.Equal(t, networkName, rNets[0]) } func TestNew_withOptions(t *testing.T) { @@ -329,18 +268,14 @@ func TestNew_withOptions(t *testing.T) { network.WithDriver("bridge"), ) // } - if err != nil { - t.Fatal("cannot create network: ", err) - } - defer func() { - require.NoError(t, net.Remove(ctx)) - }() + require.NoError(t, err) + testcontainers.CleanupNetwork(t, net) networkName := net.Name nginx, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ - Image: "nginx:alpine", + Image: nginxAlpineImage, ExposedPorts: []string{ "80/tcp", }, @@ -349,32 +284,24 @@ func TestNew_withOptions(t *testing.T) { }, }, }) + testcontainers.CleanupContainer(t, nginx) require.NoError(t, err) - defer func() { - require.NoError(t, nginx.Terminate(ctx)) - }() provider, err := testcontainers.ProviderDocker.GetProvider() - if err != nil { - t.Fatal("Cannot get Provider") - } + require.NoError(t, err) defer provider.Close() //nolint:staticcheck foundNetwork, err := provider.GetNetwork(ctx, testcontainers.NetworkRequest{Name: networkName}) - if err != nil { - t.Fatal("Cannot get created network by name") - } - assert.Equal(t, ipamConfig, foundNetwork.IPAM) + require.NoError(t, err) + require.Equal(t, ipamConfig, foundNetwork.IPAM) } func TestWithNetwork(t *testing.T) { // first create the network to be reused nw, err := network.New(context.Background(), network.WithLabels(map[string]string{"network-type": "unique"})) require.NoError(t, err) - defer func() { - require.NoError(t, nw.Remove(context.Background())) - }() + testcontainers.CleanupNetwork(t, nw) networkName := nw.Name @@ -387,11 +314,11 @@ func TestWithNetwork(t *testing.T) { err := network.WithNetwork([]string{"alias"}, nw)(&req) require.NoError(t, err) - assert.Len(t, req.Networks, 1) - assert.Equal(t, networkName, req.Networks[0]) + require.Len(t, req.Networks, 1) + require.Equal(t, networkName, req.Networks[0]) - assert.Len(t, req.NetworkAliases, 1) - assert.Equal(t, map[string][]string{networkName: {"alias"}}, req.NetworkAliases) + require.Len(t, req.NetworkAliases, 1) + require.Equal(t, map[string][]string{networkName: {"alias"}}, req.NetworkAliases) } // verify that the network is created only once @@ -402,17 +329,17 @@ func TestWithNetwork(t *testing.T) { Filters: filters.NewArgs(filters.Arg("name", networkName)), }) require.NoError(t, err) - assert.Len(t, resources, 1) + require.Len(t, resources, 1) newNetwork := resources[0] expectedLabels := testcontainers.GenericLabels() expectedLabels["network-type"] = "unique" - assert.Equal(t, networkName, newNetwork.Name) - assert.False(t, newNetwork.Attachable) - assert.False(t, newNetwork.Internal) - assert.Equal(t, expectedLabels, newNetwork.Labels) + require.Equal(t, networkName, newNetwork.Name) + require.False(t, newNetwork.Attachable) + require.False(t, newNetwork.Internal) + require.Equal(t, expectedLabels, newNetwork.Labels) } func TestWithSyntheticNetwork(t *testing.T) { @@ -431,11 +358,11 @@ func TestWithSyntheticNetwork(t *testing.T) { err := network.WithNetwork([]string{"alias"}, nw)(&req) require.NoError(t, err) - assert.Len(t, req.Networks, 1) - assert.Equal(t, networkName, req.Networks[0]) + require.Len(t, req.Networks, 1) + require.Equal(t, networkName, req.Networks[0]) - assert.Len(t, req.NetworkAliases, 1) - assert.Equal(t, map[string][]string{networkName: {"alias"}}, req.NetworkAliases) + require.Len(t, req.NetworkAliases, 1) + require.Equal(t, map[string][]string{networkName: {"alias"}}, req.NetworkAliases) // verify that the network is created only once client, err := testcontainers.NewDockerClientWithOpts(context.Background()) @@ -445,14 +372,12 @@ func TestWithSyntheticNetwork(t *testing.T) { Filters: filters.NewArgs(filters.Arg("name", networkName)), }) require.NoError(t, err) - assert.Empty(t, resources) // no Docker network was created + require.Empty(t, resources) // no Docker network was created c, err := testcontainers.GenericContainer(context.Background(), req) + testcontainers.CleanupContainer(t, c) require.NoError(t, err) - assert.NotNil(t, c) - defer func() { - require.NoError(t, c.Terminate(context.Background())) - }() + require.NotNil(t, c) } func TestWithNewNetwork(t *testing.T) { @@ -466,13 +391,12 @@ func TestWithNewNetwork(t *testing.T) { network.WithLabels(map[string]string{"this-is-a-test": "value"}), )(&req) require.NoError(t, err) - - assert.Len(t, req.Networks, 1) + require.Len(t, req.Networks, 1) networkName := req.Networks[0] - assert.Len(t, req.NetworkAliases, 1) - assert.Equal(t, map[string][]string{networkName: {"alias"}}, req.NetworkAliases) + require.Len(t, req.NetworkAliases, 1) + require.Equal(t, map[string][]string{networkName: {"alias"}}, req.NetworkAliases) client, err := testcontainers.NewDockerClientWithOpts(context.Background()) require.NoError(t, err) @@ -481,7 +405,7 @@ func TestWithNewNetwork(t *testing.T) { Filters: filters.NewArgs(filters.Arg("name", networkName)), }) require.NoError(t, err) - assert.Len(t, resources, 1) + require.Len(t, resources, 1) newNetwork := resources[0] defer func() { @@ -491,10 +415,10 @@ func TestWithNewNetwork(t *testing.T) { expectedLabels := testcontainers.GenericLabels() expectedLabels["this-is-a-test"] = "value" - assert.Equal(t, networkName, newNetwork.Name) - assert.True(t, newNetwork.Attachable) - assert.True(t, newNetwork.Internal) - assert.Equal(t, expectedLabels, newNetwork.Labels) + require.Equal(t, networkName, newNetwork.Name) + require.True(t, newNetwork.Attachable) + require.True(t, newNetwork.Internal) + require.Equal(t, expectedLabels, newNetwork.Labels) } func TestWithNewNetworkContextTimeout(t *testing.T) { @@ -513,6 +437,6 @@ func TestWithNewNetworkContextTimeout(t *testing.T) { require.Error(t, err) // we do not want to fail, just skip the network creation - assert.Empty(t, req.Networks) - assert.Empty(t, req.NetworkAliases) + require.Empty(t, req.Networks) + require.Empty(t, req.NetworkAliases) } diff --git a/options.go b/options.go index 2849b15667..4eb39058c8 100644 --- a/options.go +++ b/options.go @@ -186,7 +186,7 @@ func (p prependHubRegistry) Description() string { // - if the prefix is empty, the image is returned as is. // - if the image is a non-hub image (e.g. where another registry is set), the image is returned as is. // - if the image is a Docker Hub image where the hub registry is explicitly part of the name -// (i.e. anything with a docker.io or registry.hub.docker.com host part), the image is returned as is. +// (i.e. anything with a registry.hub.docker.com host part), the image is returned as is. func (p prependHubRegistry) Substitute(image string) (string, error) { registry := core.ExtractRegistry(image, "") diff --git a/options_test.go b/options_test.go index e19ecde96e..a1d5af0fa5 100644 --- a/options_test.go +++ b/options_test.go @@ -54,7 +54,7 @@ func TestOverrideContainerRequest(t *testing.T) { // toBeMergedRequest should not be changed assert.Equal(t, "", toBeMergedRequest.Env["BAR"]) - assert.Len(t, toBeMergedRequest.ExposedPorts, 1) + require.Len(t, toBeMergedRequest.ExposedPorts, 1) assert.Equal(t, "67890/tcp", toBeMergedRequest.ExposedPorts[0]) // req should be merged with toBeMergedRequest @@ -93,11 +93,10 @@ func TestWithLogConsumers(t *testing.T) { ctx := context.Background() c, err := testcontainers.GenericContainer(ctx, req) - terminateContainerOnEnd(t, ctx, c) + testcontainers.CleanupContainer(t, c) // we expect an error because the MySQL environment variables are not set // but this is expected because we just want to test the log consumer - require.Error(t, err) - require.Contains(t, err.Error(), "container exited with code 1") + require.ErrorContains(t, err, "container exited with code 1") require.NotEmpty(t, lc.msgs) } @@ -115,15 +114,12 @@ func TestWithStartupCommand(t *testing.T) { err := testcontainers.WithStartupCommand(testExec)(&req) require.NoError(t, err) - assert.Len(t, req.LifecycleHooks, 1) - assert.Len(t, req.LifecycleHooks[0].PostStarts, 1) + require.Len(t, req.LifecycleHooks, 1) + require.Len(t, req.LifecycleHooks[0].PostStarts, 1) c, err := testcontainers.GenericContainer(context.Background(), req) + testcontainers.CleanupContainer(t, c) require.NoError(t, err) - defer func() { - err = c.Terminate(context.Background()) - require.NoError(t, err) - }() _, reader, err := c.Exec(context.Background(), []string{"ls", "/tmp/.testcontainers"}, exec.Multiplexed()) require.NoError(t, err) @@ -147,15 +143,12 @@ func TestWithAfterReadyCommand(t *testing.T) { err := testcontainers.WithAfterReadyCommand(testExec)(&req) require.NoError(t, err) - assert.Len(t, req.LifecycleHooks, 1) - assert.Len(t, req.LifecycleHooks[0].PostReadies, 1) + require.Len(t, req.LifecycleHooks, 1) + require.Len(t, req.LifecycleHooks[0].PostReadies, 1) c, err := testcontainers.GenericContainer(context.Background(), req) + testcontainers.CleanupContainer(t, c) require.NoError(t, err) - defer func() { - err = c.Terminate(context.Background()) - require.NoError(t, err) - }() _, reader, err := c.Exec(context.Background(), []string{"ls", "/tmp/.testcontainers"}, exec.Multiplexed()) require.NoError(t, err) diff --git a/parallel.go b/parallel.go index 34740eeaf4..0349023ba2 100644 --- a/parallel.go +++ b/parallel.go @@ -31,25 +31,28 @@ func (gpe ParallelContainersError) Error() string { return fmt.Sprintf("%v", gpe.Errors) } +// parallelContainersResult represents result. +type parallelContainersResult struct { + ParallelContainersRequestError + Container Container +} + func parallelContainersRunner( ctx context.Context, requests <-chan GenericContainerRequest, - errors chan<- ParallelContainersRequestError, - containers chan<- Container, + results chan<- parallelContainersResult, wg *sync.WaitGroup, ) { + defer wg.Done() for req := range requests { c, err := GenericContainer(ctx, req) + res := parallelContainersResult{Container: c} if err != nil { - errors <- ParallelContainersRequestError{ - Request: req, - Error: err, - } - continue + res.Request = req + res.Error = err } - containers <- c + results <- res } - wg.Done() } // ParallelContainers creates a generic containers with parameters and run it in parallel mode @@ -64,41 +67,26 @@ func ParallelContainers(ctx context.Context, reqs ParallelContainerRequest, opt } tasksChan := make(chan GenericContainerRequest, tasksChanSize) - errsChan := make(chan ParallelContainersRequestError) - resChan := make(chan Container) - waitRes := make(chan struct{}) - - containers := make([]Container, 0) - errors := make([]ParallelContainersRequestError, 0) + resultsChan := make(chan parallelContainersResult, tasksChanSize) + done := make(chan struct{}) - wg := sync.WaitGroup{} + var wg sync.WaitGroup wg.Add(tasksChanSize) // run workers for i := 0; i < tasksChanSize; i++ { - go parallelContainersRunner(ctx, tasksChan, errsChan, resChan, &wg) + go parallelContainersRunner(ctx, tasksChan, resultsChan, &wg) } + var errs []ParallelContainersRequestError + containers := make([]Container, 0, len(reqs)) go func() { - for { - select { - case c, ok := <-resChan: - if !ok { - resChan = nil - } else { - containers = append(containers, c) - } - case e, ok := <-errsChan: - if !ok { - errsChan = nil - } else { - errors = append(errors, e) - } - } - - if resChan == nil && errsChan == nil { - waitRes <- struct{}{} - break + defer close(done) + for res := range resultsChan { + if res.Error != nil { + errs = append(errs, res.ParallelContainersRequestError) + } else { + containers = append(containers, res.Container) } } }() @@ -107,14 +95,15 @@ func ParallelContainers(ctx context.Context, reqs ParallelContainerRequest, opt tasksChan <- req } close(tasksChan) + wg.Wait() - close(resChan) - close(errsChan) - <-waitRes + close(resultsChan) + + <-done - if len(errors) != 0 { - return containers, ParallelContainersError{Errors: errors} + if len(errs) != 0 { + return containers, ParallelContainersError{Errors: errs} } return containers, nil diff --git a/parallel_test.go b/parallel_test.go index 122f59a4f7..25f919e99d 100644 --- a/parallel_test.go +++ b/parallel_test.go @@ -2,7 +2,6 @@ package testcontainers import ( "context" - "errors" "fmt" "testing" "time" @@ -99,23 +98,18 @@ func TestParallelContainers(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { res, err := ParallelContainers(context.Background(), tc.reqs, ParallelContainersOptions{}) - if err != nil { - require.NotZero(t, tc.expErrors) - var e ParallelContainersError - errors.As(err, &e) - if len(e.Errors) != tc.expErrors { - t.Fatalf("expected errors: %d, got: %d\n", tc.expErrors, len(e.Errors)) - } - } - for _, c := range res { - c := c - terminateContainerOnEnd(t, context.Background(), c) + CleanupContainer(t, c) } - if len(res) != tc.resLen { - t.Fatalf("expected containers: %d, got: %d\n", tc.resLen, len(res)) + if tc.expErrors != 0 { + require.Error(t, err) + var errs ParallelContainersError + require.ErrorAs(t, err, &errs) + require.Len(t, errs.Errors, tc.expErrors) } + + require.Len(t, res, tc.resLen) }) } } @@ -157,11 +151,8 @@ func TestParallelContainersWithReuse(t *testing.T) { ctx := context.Background() res, err := ParallelContainers(ctx, parallelRequest, ParallelContainersOptions{}) - if err != nil { - var e ParallelContainersError - errors.As(err, &e) - t.Fatalf("expected errors: %d, got: %d\n", 0, len(e.Errors)) + for _, c := range res { + CleanupContainer(t, c) } - // Container is reused, only terminate first container - terminateContainerOnEnd(t, ctx, res[0]) + require.NoError(t, err) } diff --git a/port_forwarding.go b/port_forwarding.go index 1d86a8cdd5..bb6bae2393 100644 --- a/port_forwarding.go +++ b/port_forwarding.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net" + "sync" "time" "github.com/docker/docker/api/types/container" @@ -38,11 +39,9 @@ var sshPassword = uuid.NewString() // 1. Create a new SSHD container. // 2. Expose the host ports to the container after the container is ready. // 3. Close the SSH sessions before killing the container. -func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) (ContainerLifecycleHooks, error) { - var sshdConnectHook ContainerLifecycleHooks - +func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) (sshdConnectHook ContainerLifecycleHooks, err error) { if len(ports) == 0 { - return sshdConnectHook, fmt.Errorf("no ports to expose") + return sshdConnectHook, errors.New("no ports to expose") } // Use the first network of the container to connect to the SSHD container. @@ -91,14 +90,36 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) ( // start the SSHD container with the provided options sshdContainer, err := newSshdContainer(ctx, opts...) + // Ensure the SSHD container is stopped and removed in case of error. + defer func() { + if err != nil { + err = errors.Join(err, TerminateContainer(sshdContainer)) + } + }() if err != nil { return sshdConnectHook, fmt.Errorf("new sshd container: %w", err) } - // IP in the first network of the container - sshdIP, err := sshdContainer.ContainerIP(context.Background()) + // IP in the first network of the container. + inspect, err := sshdContainer.Inspect(ctx) if err != nil { - return sshdConnectHook, fmt.Errorf("get sshd container IP: %w", err) + return sshdConnectHook, fmt.Errorf("inspect sshd container: %w", err) + } + + // TODO: remove once we have docker context support via #2810 + sshdIP := inspect.NetworkSettings.IPAddress + if sshdIP == "" { + single := len(inspect.NetworkSettings.Networks) == 1 + for name, network := range inspect.NetworkSettings.Networks { + if name == sshdFirstNetwork || single { + sshdIP = network.IPAddress + break + } + } + } + + if sshdIP == "" { + return sshdConnectHook, errors.New("sshd container IP not found") } if req.HostConfigModifier == nil { @@ -129,6 +150,20 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) ( originalHCM(hostConfig) } + stopHooks := []ContainerHook{ + func(ctx context.Context, _ Container) error { + if ctx.Err() != nil { + // Context already canceled, need to create a new one to ensure + // the SSH session is closed. + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + } + + return TerminateContainer(sshdContainer, StopContext(ctx)) + }, + } + // after the container is ready, create the SSH tunnel // for each exposed port from the host. sshdConnectHook = ContainerLifecycleHooks{ @@ -137,12 +172,8 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) ( return sshdContainer.exposeHostPort(ctx, req.HostAccessPorts...) }, }, - PreTerminates: []ContainerHook{ - func(ctx context.Context, _ Container) error { - // before killing the container, close the SSH sessions - return sshdContainer.Terminate(ctx) - }, - }, + PreStops: stopHooks, + PreTerminates: stopHooks, } return sshdConnectHook, nil @@ -152,11 +183,10 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) ( func newSshdContainer(ctx context.Context, opts ...ContainerCustomizer) (*sshdContainer, error) { req := GenericContainerRequest{ ContainerRequest: ContainerRequest{ - Image: sshdImage, - HostAccessPorts: []int{}, // empty list because it does not need any port - ExposedPorts: []string{sshPort}, - Env: map[string]string{"PASSWORD": sshPassword}, - WaitingFor: wait.ForListeningPort(sshPort), + Image: sshdImage, + ExposedPorts: []string{sshPort}, + Env: map[string]string{"PASSWORD": sshPassword}, + WaitingFor: wait.ForListeningPort(sshPort), }, Started: true, } @@ -168,183 +198,230 @@ func newSshdContainer(ctx context.Context, opts ...ContainerCustomizer) (*sshdCo } c, err := GenericContainer(ctx, req) - if err != nil { - return nil, err + var sshd *sshdContainer + if c != nil { + sshd = &sshdContainer{Container: c} } - // force a type assertion to return a concrete type, - // because GenericContainer returns a Container interface. - dc := c.(*DockerContainer) - - sshd := &sshdContainer{ - DockerContainer: dc, - portForwarders: []PortForwarder{}, + if err != nil { + return sshd, fmt.Errorf("generic container: %w", err) } - sshClientConfig, err := configureSSHConfig(ctx, sshd) - if err != nil { - // return the container and the error to the caller to handle it + if err = sshd.clientConfig(ctx); err != nil { + // Return the container and the error to the caller to handle it. return sshd, err } - sshd.sshConfig = sshClientConfig - return sshd, nil } // sshdContainer represents the SSHD container type used for the port forwarding container. -// It's an internal type that extends the DockerContainer type, to add the SSH tunneling capabilities. +// It's an internal type that extends the DockerContainer type, to add the SSH tunnelling capabilities. type sshdContainer struct { - *DockerContainer + Container port string sshConfig *ssh.ClientConfig - portForwarders []PortForwarder + portForwarders []*portForwarder } // Terminate stops the container and closes the SSH session func (sshdC *sshdContainer) Terminate(ctx context.Context) error { + return errors.Join( + sshdC.closePorts(), + sshdC.Container.Terminate(ctx), + ) +} + +// Stop stops the container and closes the SSH session +func (sshdC *sshdContainer) Stop(ctx context.Context, timeout *time.Duration) error { + return errors.Join( + sshdC.closePorts(), + sshdC.Container.Stop(ctx, timeout), + ) +} + +// closePorts closes all port forwarders. +func (sshdC *sshdContainer) closePorts() error { + var errs []error for _, pfw := range sshdC.portForwarders { - pfw.Close(ctx) + if err := pfw.Close(); err != nil { + errs = append(errs, err) + } } - - return sshdC.DockerContainer.Terminate(ctx) + sshdC.portForwarders = nil // Ensure the port forwarders are not used after closing. + return errors.Join(errs...) } -func configureSSHConfig(ctx context.Context, sshdC *sshdContainer) (*ssh.ClientConfig, error) { +// clientConfig sets up the the SSHD client configuration. +func (sshdC *sshdContainer) clientConfig(ctx context.Context) error { mappedPort, err := sshdC.MappedPort(ctx, sshPort) if err != nil { - return nil, err + return fmt.Errorf("mapped port: %w", err) } - sshdC.port = mappedPort.Port() - sshConfig := ssh.ClientConfig{ + sshdC.port = mappedPort.Port() + sshdC.sshConfig = &ssh.ClientConfig{ User: user, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Auth: []ssh.AuthMethod{ssh.Password(sshPassword)}, - Timeout: 30 * time.Second, } - return &sshConfig, nil + return nil } -func (sshdC *sshdContainer) exposeHostPort(ctx context.Context, ports ...int) error { +// exposeHostPort exposes the host ports to the container. +func (sshdC *sshdContainer) exposeHostPort(ctx context.Context, ports ...int) (err error) { + defer func() { + if err != nil { + err = errors.Join(err, sshdC.closePorts()) + } + }() for _, port := range ports { - pw := NewPortForwarder(fmt.Sprintf("localhost:%s", sshdC.port), sshdC.sshConfig, port, port) - sshdC.portForwarders = append(sshdC.portForwarders, *pw) - - go pw.Forward(ctx) //nolint:errcheck // Nothing we can usefully do with the error - } - - var err error + pf, err := newPortForwarder(ctx, "localhost:"+sshdC.port, sshdC.sshConfig, port) + if err != nil { + return fmt.Errorf("new port forwarder: %w", err) + } - // continue when all port forwarders have created the connection - for _, pfw := range sshdC.portForwarders { - err = errors.Join(err, <-pfw.connectionCreated) + sshdC.portForwarders = append(sshdC.portForwarders, pf) } - return err + return nil } -type PortForwarder struct { - sshDAddr string - sshConfig *ssh.ClientConfig - remotePort int - localPort int - connectionCreated chan error // used to signal that the connection has been created, so the caller can proceed - terminateChan chan struct{} // used to signal that the connection has been terminated +// portForwarder forwards a port from the container to the host. +type portForwarder struct { + client *ssh.Client + listener net.Listener + dialTimeout time.Duration + localAddr string + ctx context.Context + cancel context.CancelFunc + + // closeMtx protects the close operation + closeMtx sync.Mutex + closeErr error } -func NewPortForwarder(sshDAddr string, sshConfig *ssh.ClientConfig, remotePort, localPort int) *PortForwarder { - return &PortForwarder{ - sshDAddr: sshDAddr, - sshConfig: sshConfig, - remotePort: remotePort, - localPort: localPort, - connectionCreated: make(chan error), - terminateChan: make(chan struct{}), +// newPortForwarder creates a new running portForwarder for the given port. +// The context is only used for the initial SSH connection. +func newPortForwarder(ctx context.Context, sshDAddr string, sshConfig *ssh.ClientConfig, port int) (pf *portForwarder, err error) { + var d net.Dialer + conn, err := d.DialContext(ctx, "tcp", sshDAddr) + if err != nil { + return nil, fmt.Errorf("ssh dial: %w", err) } -} -func (pf *PortForwarder) Close(ctx context.Context) { - close(pf.terminateChan) - close(pf.connectionCreated) -} + // Ensure the connection is closed in case of error. + defer func() { + if err != nil { + err = errors.Join(err, conn.Close()) + } + }() -func (pf *PortForwarder) Forward(ctx context.Context) error { - client, err := ssh.Dial("tcp", pf.sshDAddr, pf.sshConfig) + c, chans, reqs, err := ssh.NewClientConn(conn, sshDAddr, sshConfig) if err != nil { - err = fmt.Errorf("error dialing ssh server: %w", err) - pf.connectionCreated <- err - return err + return nil, fmt.Errorf("ssh new client conn: %w", err) } - defer client.Close() - listener, err := client.Listen("tcp", fmt.Sprintf("localhost:%d", pf.remotePort)) + client := ssh.NewClient(c, chans, reqs) + + listener, err := client.Listen("tcp", fmt.Sprintf("localhost:%d", port)) if err != nil { - err = fmt.Errorf("error listening on remote port: %w", err) - pf.connectionCreated <- err - return err + return nil, fmt.Errorf("listening on remote port %d: %w", port, err) + } + + ctx, cancel := context.WithCancel(context.Background()) + + pf = &portForwarder{ + client: client, + listener: listener, + localAddr: fmt.Sprintf("localhost:%d", port), + ctx: ctx, + cancel: cancel, + dialTimeout: time.Second * 2, } - defer listener.Close() - // signal that the connection has been created - pf.connectionCreated <- nil + go pf.run() + + return pf, nil +} + +// Close closes the port forwarder. +func (pf *portForwarder) Close() error { + pf.closeMtx.Lock() + defer pf.closeMtx.Unlock() - // check if the context or the terminateChan has been closed select { - case <-ctx.Done(): - if err := listener.Close(); err != nil { - return fmt.Errorf("error closing listener: %w", err) - } - if err := client.Close(); err != nil { - return fmt.Errorf("error closing client: %w", err) - } - return nil - case <-pf.terminateChan: - if err := listener.Close(); err != nil { - return fmt.Errorf("error closing listener: %w", err) - } - if err := client.Close(); err != nil { - return fmt.Errorf("error closing client: %w", err) - } - return nil + case <-pf.ctx.Done(): + // Already closed. + return pf.closeErr default: } + var errs []error + if err := pf.listener.Close(); err != nil { + errs = append(errs, fmt.Errorf("close listener: %w", err)) + } + if err := pf.client.Close(); err != nil { + errs = append(errs, fmt.Errorf("close client: %w", err)) + } + + pf.closeErr = errors.Join(errs...) + pf.cancel() + + return pf.closeErr +} + +// run forwards the port from the remote connection to the local connection. +func (pf *portForwarder) run() { for { - remote, err := listener.Accept() + remote, err := pf.listener.Accept() if err != nil { - return fmt.Errorf("error accepting connection: %w", err) + if errors.Is(err, io.EOF) { + // The listener has been closed. + return + } + + // Ignore errors as they are transient and we want requests to + // continue to be accepted. + continue } - go pf.runTunnel(ctx, remote) + go pf.tunnel(remote) } } -// runTunnel runs a tunnel between two connections; as soon as one connection -// reaches EOF or reports an error, both connections are closed and this -// function returns. -func (pf *PortForwarder) runTunnel(ctx context.Context, remote net.Conn) { +// tunnel runs a tunnel between two connections; as soon as the forwarder +// context is cancelled or one connection copies returns, irrespective of +// the error, both connections are closed. +func (pf *portForwarder) tunnel(remote net.Conn) { + defer remote.Close() + + ctx, cancel := context.WithTimeout(pf.ctx, pf.dialTimeout) + defer cancel() + var dialer net.Dialer - local, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("localhost:%d", pf.localPort)) + local, err := dialer.DialContext(ctx, "tcp", pf.localAddr) if err != nil { - remote.Close() + // Nothing we can do with the error. return } defer local.Close() - defer remote.Close() - done := make(chan struct{}, 2) + ctx, cancel = context.WithCancel(pf.ctx) go func() { - io.Copy(local, remote) //nolint:errcheck // Nothing we can usefully do with the error - done <- struct{}{} + defer cancel() + io.Copy(local, remote) //nolint:errcheck // Nothing useful we can do with the error. }() go func() { - io.Copy(remote, local) //nolint:errcheck // Nothing we can usefully do with the error - done <- struct{}{} + defer cancel() + io.Copy(remote, local) //nolint:errcheck // Nothing useful we can do with the error. }() - <-done + // Wait for the context to be done before returning which triggers + // both connections to close. This is done to to prevent the copies + // blocking forever on unused connections. + <-ctx.Done() } diff --git a/port_forwarding_test.go b/port_forwarding_test.go index 471736150b..d6395b20a7 100644 --- a/port_forwarding_test.go +++ b/port_forwarding_test.go @@ -22,140 +22,135 @@ const ( ) func TestExposeHostPorts(t *testing.T) { - tests := []struct { - name string - numberOfPorts int - hasNetwork bool - hasHostAccess bool - }{ - { - name: "single port", - numberOfPorts: 1, - hasHostAccess: true, - }, - { - name: "single port using a network", - numberOfPorts: 1, - hasNetwork: true, - hasHostAccess: true, - }, - { - name: "multiple ports", - numberOfPorts: 3, - hasHostAccess: true, - }, - { - name: "single port with cancellation", - numberOfPorts: 1, - hasHostAccess: false, + hostPorts := make([]int, 3) + for i := range hostPorts { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, expectedResponse) + })) + hostPorts[i] = server.Listener.Addr().(*net.TCPAddr).Port + t.Cleanup(server.Close) + } + + singlePort := hostPorts[0:1] + + t.Run("single-port", func(t *testing.T) { + testExposeHostPorts(t, singlePort, false, false) + }) + + t.Run("single-port-network", func(t *testing.T) { + testExposeHostPorts(t, singlePort, true, false) + }) + + t.Run("single-port-host-access", func(t *testing.T) { + testExposeHostPorts(t, singlePort, false, true) + }) + + t.Run("single-port-network-host-access", func(t *testing.T) { + testExposeHostPorts(t, singlePort, true, true) + }) + + t.Run("multi-port", func(t *testing.T) { + testExposeHostPorts(t, hostPorts, false, false) + }) + + t.Run("multi-port-network", func(t *testing.T) { + testExposeHostPorts(t, hostPorts, true, false) + }) + + t.Run("multi-port-host-access", func(t *testing.T) { + testExposeHostPorts(t, hostPorts, false, true) + }) + + t.Run("multi-port-network-host-access", func(t *testing.T) { + testExposeHostPorts(t, hostPorts, true, true) + }) +} + +func testExposeHostPorts(t *testing.T, hostPorts []int, hasNetwork, hasHostAccess bool) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + var hostAccessPorts []int + if hasHostAccess { + hostAccessPorts = hostPorts + } + req := testcontainers.GenericContainerRequest{ + // hostAccessPorts { + ContainerRequest: testcontainers.ContainerRequest{ + Image: "alpine:3.17", + HostAccessPorts: hostAccessPorts, + Cmd: []string{"top"}, }, + // } + Started: true, } - for _, tc := range tests { - t.Run(tc.name, func(tt *testing.T) { - freePorts := make([]int, tc.numberOfPorts) - for i := range freePorts { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, expectedResponse) - })) - freePorts[i] = server.Listener.Addr().(*net.TCPAddr).Port - tt.Cleanup(func() { - server.Close() - }) - } - - req := testcontainers.GenericContainerRequest{ - // hostAccessPorts { - ContainerRequest: testcontainers.ContainerRequest{ - Image: "alpine:3.17", - HostAccessPorts: freePorts, - Cmd: []string{"top"}, - }, - // } - Started: true, - } - - var nw *testcontainers.DockerNetwork - if tc.hasNetwork { - var err error - nw, err = network.New(context.Background()) - require.NoError(tt, err) - - tt.Cleanup(func() { - require.NoError(tt, nw.Remove(context.Background())) - }) - - req.Networks = []string{nw.Name} - req.NetworkAliases = map[string][]string{nw.Name: {"myalpine"}} - } - - ctx := context.Background() - if !tc.hasHostAccess { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, 10*time.Second) - defer cancel() - } - - c, err := testcontainers.GenericContainer(ctx, req) - require.NoError(tt, err) - tt.Cleanup(func() { - require.NoError(tt, c.Terminate(context.Background())) - }) - - if tc.hasHostAccess { - // create a container that has host access, which will - // automatically forward the port to the container - assertContainerHasHostAccess(tt, c, freePorts...) - } else { - // force cancellation because of timeout - time.Sleep(11 * time.Second) - - assertContainerHasNoHostAccess(tt, c, freePorts...) - } - }) + if hasNetwork { + nw, err := network.New(ctx) + require.NoError(t, err) + testcontainers.CleanupNetwork(t, nw) + + req.Networks = []string{nw.Name} + req.NetworkAliases = map[string][]string{nw.Name: {"myalpine"}} + } + + c, err := testcontainers.GenericContainer(ctx, req) + testcontainers.CleanupContainer(t, c) + require.NoError(t, err) + + if hasHostAccess { + // Verify that the container can access the host ports. + containerHasHostAccess(t, c, hostPorts...) + return } + + // Verify that the container cannot access the host ports. + containerHasNoHostAccess(t, c, hostPorts...) } +// httpRequest sends an HTTP request from the container to the host port via +// [testcontainers.HostInternal] address. func httpRequest(t *testing.T, c testcontainers.Container, port int) (int, string) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + // wgetHostInternal { code, reader, err := c.Exec( - context.Background(), - []string{"wget", "-q", "-O", "-", fmt.Sprintf("http://%s:%d", testcontainers.HostInternal, port)}, + ctx, + []string{"wget", "-q", "-O", "-", "-T", "2", fmt.Sprintf("http://%s:%d", testcontainers.HostInternal, port)}, tcexec.Multiplexed(), ) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // read the response bs, err := io.ReadAll(reader) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return code, string(bs) } -func assertContainerHasHostAccess(t *testing.T, c testcontainers.Container, ports ...int) { +// containerHasHostAccess verifies that the container can access the host ports +// via [testcontainers.HostInternal] address. +func containerHasHostAccess(t *testing.T, c testcontainers.Container, ports ...int) { + t.Helper() for _, port := range ports { code, response := httpRequest(t, c, port) - if code != 0 { - t.Fatalf("expected status code [%d] but got [%d]", 0, code) - } - - if response != expectedResponse { - t.Fatalf("expected [%s] but got [%s]", expectedResponse, response) - } + require.Zero(t, code) + require.Equal(t, expectedResponse, response) } } -func assertContainerHasNoHostAccess(t *testing.T, c testcontainers.Container, ports ...int) { +// containerHasNoHostAccess verifies that the container cannot access the host ports +// via [testcontainers.HostInternal] address. +func containerHasNoHostAccess(t *testing.T, c testcontainers.Container, ports ...int) { + t.Helper() for _, port := range ports { - _, response := httpRequest(t, c, port) - - if response == expectedResponse { - t.Fatalf("expected not to get [%s] but got [%s]", expectedResponse, response) - } + code, response := httpRequest(t, c, port) + require.NotZero(t, code) + require.Contains(t, response, "bad address") } } diff --git a/provider.go b/provider.go index b5e5ffa997..31714c0c14 100644 --- a/provider.go +++ b/provider.go @@ -25,7 +25,7 @@ type ( // GenericProviderOptions defines options applicable to all providers GenericProviderOptions struct { Logger Logging - DefaultNetwork string + defaultNetwork string } // GenericProviderOption defines a common interface to modify GenericProviderOptions diff --git a/provider_test.go b/provider_test.go index 097c83a02c..94206e46bf 100644 --- a/provider_test.go +++ b/provider_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/internal/core" ) @@ -16,7 +18,6 @@ func TestProviderTypeGetProviderAutodetect(t *testing.T) { tr ProviderType DockerHost string want string - wantErr bool }{ { name: "default provider without podman.socket", @@ -65,17 +66,10 @@ func TestProviderTypeGetProviderAutodetect(t *testing.T) { t.Setenv("DOCKER_HOST", tt.DockerHost) got, err := tt.tr.GetProvider() - if (err != nil) != tt.wantErr { - t.Errorf("ProviderType.GetProvider() error = %v, wantErr %v", err, tt.wantErr) - return - } + require.NoErrorf(t, err, "ProviderType.GetProvider()") provider, ok := got.(*DockerProvider) - if !ok { - t.Fatalf("ProviderType.GetProvider() = %T, want %T", got, &DockerProvider{}) - } - if provider.defaultBridgeNetworkName != tt.want { - t.Errorf("ProviderType.GetProvider() = %v, want %v", provider.defaultBridgeNetworkName, tt.want) - } + require.Truef(t, ok, "ProviderType.GetProvider() = %T, want %T", got, &DockerProvider{}) + require.Equalf(t, tt.want, provider.defaultBridgeNetworkName, "ProviderType.GetProvider() = %v, want %v", provider.defaultBridgeNetworkName, tt.want) }) } } diff --git a/reaper.go b/reaper.go index c41520b5b7..1d97a36ffa 100644 --- a/reaper.go +++ b/reaper.go @@ -1,13 +1,16 @@ package testcontainers import ( - "bufio" + "bytes" "context" + "errors" "fmt" - "math/rand" + "io" "net" + "os" "strings" "sync" + "syscall" "time" "github.com/cenkalti/backoff/v4" @@ -34,9 +37,23 @@ const ( var ( // Deprecated: it has been replaced by an internal value ReaperDefaultImage = config.ReaperDefaultImage - reaperInstance *Reaper // We would like to create reaper only once - reaperMutex sync.Mutex - reaperOnce sync.Once + + // defaultReaperPort is the default port that the reaper listens on if not + // overridden by the RYUK_PORT environment variable. + defaultReaperPort = nat.Port("8080/tcp") + + // errReaperNotFound is returned when no reaper container is found. + errReaperNotFound = errors.New("reaper not found") + + // errReaperDisabled is returned if a reaper is requested but the + // config has it disabled. + errReaperDisabled = errors.New("reaper disabled") + + // spawner is the singleton instance of reaperSpawner. + spawner = &reaperSpawner{} + + // reaperAck is the expected response from the reaper container. + reaperAck = []byte("ACK\n") ) // ReaperProvider represents a provider for the reaper to run itself with @@ -47,10 +64,18 @@ type ReaperProvider interface { } // NewReaper creates a Reaper with a sessionID to identify containers and a provider to use -// Deprecated: it's not possible to create a reaper anymore. Compose module uses this method +// Deprecated: it's not possible to create a reaper any more. Compose module uses this method // to create a reaper for the compose stack. +// +// The caller must call Connect at least once on the returned Reaper and use the returned +// result otherwise the reaper will be kept open until the process exits. func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, reaperImageName string) (*Reaper, error) { - return reuseOrCreateReaper(ctx, sessionID, provider) + reaper, err := spawner.reaper(ctx, sessionID, provider) + if err != nil { + return nil, fmt.Errorf("reaper: %w", err) + } + + return reaper, nil } // reaperContainerNameFromSessionID returns the container name that uniquely @@ -58,34 +83,90 @@ func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, r func reaperContainerNameFromSessionID(sessionID string) string { // The session id is 64 characters, so we will not hit the limit of 128 // characters for container names. - return fmt.Sprintf("reaper_%s", sessionID) + return "reaper_" + sessionID +} + +// reaperSpawner is a singleton that manages the reaper container. +type reaperSpawner struct { + instance *Reaper + mtx sync.Mutex +} + +// port returns the port that a new reaper should listens on. +func (r *reaperSpawner) port() nat.Port { + if port := os.Getenv("RYUK_PORT"); port != "" { + natPort, err := nat.NewPort("tcp", port) + if err != nil { + panic(fmt.Sprintf("invalid RYUK_PORT value %q: %s", port, err)) + } + return natPort + } + + return defaultReaperPort } -// lookUpReaperContainer returns a DockerContainer type with the reaper container in the case +// backoff returns a backoff policy for the reaper spawner. +// It will take at most 20 seconds, doing each attempt every 100ms - 250ms. +func (r *reaperSpawner) backoff() *backoff.ExponentialBackOff { + // We want random intervals between 100ms and 250ms for concurrent executions + // to not be synchronized: it could be the case that multiple executions of this + // function happen at the same time (specifically when called from a different test + // process execution), and we want to avoid that they all try to find the reaper + // container at the same time. + b := &backoff.ExponentialBackOff{ + InitialInterval: time.Millisecond * 100, + RandomizationFactor: backoff.DefaultRandomizationFactor, + Multiplier: backoff.DefaultMultiplier, + // Adjust MaxInterval to compensate for randomization factor which can be added to + // returned interval so we have a maximum of 250ms. + MaxInterval: time.Duration(float64(time.Millisecond*250) * backoff.DefaultRandomizationFactor), + MaxElapsedTime: time.Second * 20, + Stop: backoff.Stop, + Clock: backoff.SystemClock, + } + b.Reset() + + return b +} + +// cleanup terminates the reaper container if set. +func (r *reaperSpawner) cleanup() error { + r.mtx.Lock() + defer r.mtx.Unlock() + + return r.cleanupLocked() +} + +// cleanupLocked terminates the reaper container if set. +// It must be called with the lock held. +func (r *reaperSpawner) cleanupLocked() error { + if r.instance == nil { + return nil + } + + err := TerminateContainer(r.instance.container) + r.instance = nil + + return err +} + +// lookupContainer returns a DockerContainer type with the reaper container in the case // it's found in the running state, and including the labels for sessionID, reaper, and ryuk. // It will perform a retry with exponential backoff to allow for the container to be started and // avoid potential false negatives. -func lookUpReaperContainer(ctx context.Context, sessionID string) (*DockerContainer, error) { +func (r *reaperSpawner) lookupContainer(ctx context.Context, sessionID string) (*DockerContainer, error) { dockerClient, err := NewDockerClientWithOpts(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("new client: %w", err) } defer dockerClient.Close() - // the backoff will take at most 5 seconds to find the reaper container - // doing each attempt every 100ms - exp := backoff.NewExponentialBackOff() + provider, err := NewDockerProvider() + if err != nil { + return nil, fmt.Errorf("new provider: %w", err) + } - // we want random intervals between 100ms and 500ms for concurrent executions - // to not be synchronized: it could be the case that multiple executions of this - // function happen at the same time (specifically when called from a different test - // process execution), and we want to avoid that they all try to find the reaper - // container at the same time. - exp.InitialInterval = time.Duration(rand.Intn(5)*100) * time.Millisecond - exp.RandomizationFactor = rand.Float64() * 0.5 - exp.Multiplier = rand.Float64() * 2.0 - exp.MaxInterval = 5.0 * time.Second // max interval between attempts - exp.MaxElapsedTime = 1 * time.Minute // max time to keep trying + provider.SetClient(dockerClient) opts := container.ListOptions{ All: true, @@ -97,159 +178,211 @@ func lookUpReaperContainer(ctx context.Context, sessionID string) (*DockerContai ), } - return backoff.RetryNotifyWithData( + return backoff.RetryWithData( func() (*DockerContainer, error) { resp, err := dockerClient.ContainerList(ctx, opts) if err != nil { - return nil, err + return nil, fmt.Errorf("container list: %w", err) } if len(resp) == 0 { - // reaper container not found in the running state: do not look for it again - return nil, nil + // No reaper container not found. + return nil, backoff.Permanent(errReaperNotFound) } if len(resp) > 1 { - return nil, fmt.Errorf("not possible to have multiple reaper containers found for session ID %s", sessionID) + return nil, fmt.Errorf("found %d reaper containers for session ID %q", len(resp), sessionID) } - r, err := containerFromDockerResponse(ctx, resp[0]) + r, err := provider.ContainerFromType(ctx, resp[0]) if err != nil { - return nil, err + return nil, fmt.Errorf("from docker: %w", err) } - if r.healthStatus == types.Healthy || r.healthStatus == types.NoHealthcheck { + switch { + case r.healthStatus == types.Healthy, + r.healthStatus == types.NoHealthcheck: return r, nil - } - - // if a health status is present on the container, and the container is healthy, error - if r.healthStatus != "" { - return nil, fmt.Errorf("container %s is not healthy, wanted status=%s, got status=%s", resp[0].ID[:8], types.Healthy, r.healthStatus) + case r.healthStatus != "": + return nil, fmt.Errorf("container not healthy: %s", r.healthStatus) } return r, nil }, - backoff.WithContext(exp, ctx), - func(err error, duration time.Duration) { - Logger.Printf("Error looking up reaper container, will retry: %v", err) - }, + backoff.WithContext(r.backoff(), ctx), ) } -// reuseOrCreateReaper returns an existing Reaper instance if it exists and is running. Otherwise, a new Reaper instance -// will be created with a sessionID to identify containers in the same test session/program. -func reuseOrCreateReaper(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) { - reaperMutex.Lock() - defer reaperMutex.Unlock() - - // 1. if the reaper instance has been already created, return it - if reaperInstance != nil { - // Verify this instance is still running by checking state. - // Can't use Container.IsRunning because the bool is not updated when Reaper is terminated - state, err := reaperInstance.container.State(ctx) - if err != nil { - if !errdefs.IsNotFound(err) { - return nil, err +// isRunning returns an error if the container is not running. +func (r *reaperSpawner) isRunning(ctx context.Context, ctr Container) error { + state, err := ctr.State(ctx) + if err != nil { + return fmt.Errorf("container state: %w", err) + } + + if !state.Running { + // Use NotFound error to indicate the container is not running + // and should be recreated. + return errdefs.NotFound(fmt.Errorf("container state: %s", state.Status)) + } + + return nil +} + +// retryError returns a permanent error if the error is not considered retryable. +func (r *reaperSpawner) retryError(err error) error { + var timeout interface { + Timeout() bool + } + switch { + case isCleanupSafe(err), + createContainerFailDueToNameConflictRegex.MatchString(err.Error()), + errors.Is(err, syscall.ECONNREFUSED), + errors.Is(err, syscall.ECONNRESET), + errors.Is(err, syscall.ECONNABORTED), + errors.Is(err, syscall.ETIMEDOUT), + errors.Is(err, os.ErrDeadlineExceeded), + errors.As(err, &timeout) && timeout.Timeout(), + errors.Is(err, context.DeadlineExceeded), + errors.Is(err, context.Canceled): + // Retryable error. + return err + default: + return backoff.Permanent(err) + } +} + +// reaper returns an existing Reaper instance if it exists and is running, otherwise +// a new Reaper instance will be created with a sessionID to identify containers in +// the same test session/program. If connect is true, the reaper will be connected +// to the reaper container. +// Returns an error if config.RyukDisabled is true. +// +// Safe for concurrent calls. +func (r *reaperSpawner) reaper(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) { + if config.Read().RyukDisabled { + return nil, errReaperDisabled + } + + r.mtx.Lock() + defer r.mtx.Unlock() + + return backoff.RetryWithData( + r.retryLocked(ctx, sessionID, provider), + backoff.WithContext(r.backoff(), ctx), + ) +} + +// retryLocked returns a function that can be used to create or reuse a reaper container. +// If connect is true, the reaper will be connected to the reaper container. +// It must be called with the lock held. +func (r *reaperSpawner) retryLocked(ctx context.Context, sessionID string, provider ReaperProvider) func() (*Reaper, error) { + return func() (reaper *Reaper, err error) { + reaper, err = r.reuseOrCreate(ctx, sessionID, provider) + // Ensure that the reaper is terminated if an error occurred. + defer func() { + if err != nil { + if reaper != nil { + err = errors.Join(err, TerminateContainer(reaper.container)) + } + err = r.retryError(errors.Join(err, r.cleanupLocked())) } - } else if state.Running { - return reaperInstance, nil - } - // else: the reaper instance has been terminated, so we need to create a new one - reaperOnce = sync.Once{} - } - - // 2. because the reaper instance has not been created yet, look for it in the Docker daemon, which - // will happen if the reaper container has been created in the same test session but in a different - // test process execution (e.g. when running tests in parallel), not having initialized the reaper - // instance yet. - reaperContainer, err := lookUpReaperContainer(context.Background(), sessionID) - if err == nil && reaperContainer != nil { - // The reaper container exists as a Docker container: re-use it - Logger.Printf("🔥 Reaper obtained from Docker for this test session %s", reaperContainer.ID) - reaperInstance, err = reuseReaperContainer(ctx, sessionID, provider, reaperContainer) + }() if err != nil { return nil, err } - return reaperInstance, nil - } + if err = r.isRunning(ctx, reaper.container); err != nil { + return nil, err + } - // 3. the reaper container does not exist in the Docker daemon: create it, and do it using the - // synchronization primitive to avoid multiple executions of this function to create the reaper - var reaperErr error - reaperOnce.Do(func() { - r, err := newReaper(ctx, sessionID, provider) + // Check we can still connect. + termSignal, err := reaper.connect(ctx) if err != nil { - reaperErr = err - return + return nil, fmt.Errorf("connect: %w", err) } - reaperInstance, reaperErr = r, nil - }) - if reaperErr != nil { - reaperOnce = sync.Once{} - return nil, reaperErr - } + reaper.setOrSignal(termSignal) + + r.instance = reaper - return reaperInstance, nil + return reaper, nil + } } -// reuseReaperContainer constructs a Reaper from an already running reaper -// DockerContainer. -func reuseReaperContainer(ctx context.Context, sessionID string, provider ReaperProvider, reaperContainer *DockerContainer) (*Reaper, error) { - endpoint, err := reaperContainer.PortEndpoint(ctx, "8080", "") +// reuseOrCreate returns an existing Reaper instance if it exists, otherwise a new Reaper instance. +func (r *reaperSpawner) reuseOrCreate(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) { + if r.instance != nil { + // We already have an associated reaper. + return r.instance, nil + } + + // Look for an existing reaper created in the same test session but in a + // different test process execution e.g. when running tests in parallel. + container, err := r.lookupContainer(context.Background(), sessionID) + if err != nil { + if !errors.Is(err, errReaperNotFound) { + return nil, fmt.Errorf("look up container: %w", err) + } + + // The reaper container was not found, continue to create a new one. + reaper, err := r.newReaper(ctx, sessionID, provider) + if err != nil { + return nil, fmt.Errorf("new reaper: %w", err) + } + + return reaper, nil + } + + // A reaper container exists re-use it. + reaper, err := r.fromContainer(ctx, sessionID, provider, container) if err != nil { - return nil, err + return nil, fmt.Errorf("from container %q: %w", container.ID[:8], err) } - Logger.Printf("⏳ Waiting for Reaper port to be ready") + return reaper, nil +} - var containerJson *types.ContainerJSON +// fromContainer constructs a Reaper from an already running reaper DockerContainer. +func (r *reaperSpawner) fromContainer(ctx context.Context, sessionID string, provider ReaperProvider, dockerContainer *DockerContainer) (*Reaper, error) { + Logger.Printf("⏳ Waiting for Reaper %q to be ready", dockerContainer.ID[:8]) - if containerJson, err = reaperContainer.Inspect(ctx); err != nil { - return nil, fmt.Errorf("failed to inspect reaper container %s: %w", reaperContainer.ID[:8], err) + // Reusing an existing container so we determine the port from the container's exposed ports. + if err := wait.ForExposedPort(). + WithPollInterval(100*time.Millisecond). + SkipInternalCheck(). + WaitUntilReady(ctx, dockerContainer); err != nil { + return nil, fmt.Errorf("wait for reaper %s: %w", dockerContainer.ID[:8], err) } - if containerJson != nil && containerJson.NetworkSettings != nil { - for port := range containerJson.NetworkSettings.Ports { - err := wait.ForListeningPort(port). - WithPollInterval(100*time.Millisecond). - WaitUntilReady(ctx, reaperContainer) - if err != nil { - return nil, fmt.Errorf("failed waiting for reaper container %s port %s/%s to be ready: %w", - reaperContainer.ID[:8], port.Proto(), port.Port(), err) - } - } + endpoint, err := dockerContainer.Endpoint(ctx, "") + if err != nil { + return nil, fmt.Errorf("port endpoint: %w", err) } + Logger.Printf("🔥 Reaper obtained from Docker for this test session %s", dockerContainer.ID[:8]) + return &Reaper{ Provider: provider, SessionID: sessionID, Endpoint: endpoint, - container: reaperContainer, + container: dockerContainer, }, nil } -// newReaper creates a Reaper with a sessionID to identify containers and a -// provider to use. Do not call this directly, use reuseOrCreateReaper instead. -func newReaper(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) { +// newReaper creates a connected Reaper with a sessionID to identify containers +// and a provider to use. +func (r *reaperSpawner) newReaper(ctx context.Context, sessionID string, provider ReaperProvider) (reaper *Reaper, err error) { dockerHostMount := core.MustExtractDockerSocket(ctx) - reaper := &Reaper{ - Provider: provider, - SessionID: sessionID, - } - - listeningPort := nat.Port("8080/tcp") - + port := r.port() tcConfig := provider.Config().Config - req := ContainerRequest{ Image: config.ReaperDefaultImage, - ExposedPorts: []string{string(listeningPort)}, + ExposedPorts: []string{string(port)}, Labels: core.DefaultLabels(sessionID), Privileged: tcConfig.RyukPrivileged, - WaitingFor: wait.ForListeningPort(listeningPort), + WaitingFor: wait.ForListeningPort(port), Name: reaperContainerNameFromSessionID(sessionID), HostConfigModifier: func(hc *container.HostConfig) { hc.AutoRemove = true @@ -268,133 +401,179 @@ func newReaper(ctx context.Context, sessionID string, provider ReaperProvider) ( req.Env["RYUK_VERBOSE"] = "true" } - // include reaper-specific labels to the reaper container + // Setup reaper-specific labels for the reaper container. req.Labels[core.LabelReaper] = "true" req.Labels[core.LabelRyuk] = "true" + delete(req.Labels, core.LabelReap) // Attach reaper container to a requested network if it is specified if p, ok := provider.(*DockerProvider); ok { - req.Networks = append(req.Networks, p.DefaultNetwork) + defaultNetwork, err := p.ensureDefaultNetwork(ctx) + if err != nil { + return nil, fmt.Errorf("ensure default network: %w", err) + } + + req.Networks = append(req.Networks, defaultNetwork) } c, err := provider.RunContainer(ctx, req) - if err != nil { - // We need to check whether the error is caused by a container with the same name - // already existing due to race conditions. We manually match the error message - // as we do not have any error types to check against. - if createContainerFailDueToNameConflictRegex.MatchString(err.Error()) { - // Manually retrieve the already running reaper container. However, we need to - // use retries here as there are two possible race conditions that might lead to - // errors: In most cases, there is a small delay between container creation and - // actually being visible in list-requests. This means that creation might fail - // due to name conflicts, but when we list containers with this name, we do not - // get any results. In another case, the container might have simply died in the - // meantime and therefore cannot be found. - const timeout = 5 * time.Second - const cooldown = 100 * time.Millisecond - start := time.Now() - var reaperContainer *DockerContainer - for time.Since(start) < timeout { - reaperContainer, err = lookUpReaperContainer(ctx, sessionID) - if err == nil && reaperContainer != nil { - break - } - select { - case <-ctx.Done(): - case <-time.After(cooldown): - } - } - if err != nil { - return nil, fmt.Errorf("look up reaper container due to name conflict failed: %w", err) - } - // If the reaper container was not found, it is most likely to have died in - // between as we can exclude any client errors because of the previous error - // check. Because the reaper should only die if it performed clean-ups, we can - // fail here as the reaper timeout needs to be increased, anyway. - if reaperContainer == nil { - return nil, fmt.Errorf("look up reaper container returned nil although creation failed due to name conflict") - } - Logger.Printf("🔥 Reaper obtained from Docker for this test session %s", reaperContainer.ID) - reaper, err := reuseReaperContainer(ctx, sessionID, provider, reaperContainer) - if err != nil { - return nil, err - } - return reaper, nil + defer func() { + if err != nil { + err = errors.Join(err, TerminateContainer(c)) } - return nil, err + }() + if err != nil { + return nil, fmt.Errorf("run container: %w", err) } - reaper.container = c - endpoint, err := c.PortEndpoint(ctx, "8080", "") + endpoint, err := c.PortEndpoint(ctx, port, "") if err != nil { - return nil, err + return nil, fmt.Errorf("port endpoint: %w", err) } - reaper.Endpoint = endpoint - return reaper, nil + return &Reaper{ + Provider: provider, + SessionID: sessionID, + Endpoint: endpoint, + container: c, + }, nil } // Reaper is used to start a sidecar container that cleans up resources type Reaper struct { - Provider ReaperProvider - SessionID string - Endpoint string - container Container + Provider ReaperProvider + SessionID string + Endpoint string + container Container + mtx sync.Mutex // Protects termSignal. + termSignal chan bool } -// Connect runs a goroutine which can be terminated by sending true into the returned channel +// Connect connects to the reaper container and sends the labels to it +// so that it can clean up the containers with the same labels. +// +// It returns a channel that can be closed to terminate the connection. +// Returns an error if config.RyukDisabled is true. func (r *Reaper) Connect() (chan bool, error) { - conn, err := net.DialTimeout("tcp", r.Endpoint, 10*time.Second) - if err != nil { - return nil, fmt.Errorf("%w: Connecting to Ryuk on %s failed", err, r.Endpoint) + if config.Read().RyukDisabled { + return nil, errReaperDisabled } - terminationSignal := make(chan bool) - go func(conn net.Conn) { - sock := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) - defer conn.Close() + if termSignal := r.useTermSignal(); termSignal != nil { + return termSignal, nil + } - labelFilters := []string{} - for l, v := range core.DefaultLabels(r.SessionID) { - labelFilters = append(labelFilters, fmt.Sprintf("label=%s=%s", l, v)) - } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() - retryLimit := 3 - for retryLimit > 0 { - retryLimit-- + return r.connect(ctx) +} - if _, err := sock.WriteString(strings.Join(labelFilters, "&")); err != nil { - continue - } +// close signals the connection to close if needed. +// Safe for concurrent calls. +func (r *Reaper) close() { + r.mtx.Lock() + defer r.mtx.Unlock() - if _, err := sock.WriteString("\n"); err != nil { - continue - } + if r.termSignal != nil { + r.termSignal <- true + r.termSignal = nil + } +} - if err := sock.Flush(); err != nil { - continue - } +// setOrSignal sets the reapers termSignal field if nil +// otherwise consumes by sending true to it. +// Safe for concurrent calls. +func (r *Reaper) setOrSignal(termSignal chan bool) { + r.mtx.Lock() + defer r.mtx.Unlock() + + if r.termSignal != nil { + // Already have an existing connection, close the new one. + termSignal <- true + return + } - resp, err := sock.ReadString('\n') - if err != nil { - continue - } + // First or new unused termSignal, assign for caller to reuse. + r.termSignal = termSignal +} - if resp == "ACK\n" { - break - } - } +// useTermSignal if termSignal is not nil returns it +// and sets it to nil, otherwise returns nil. +// +// Safe for concurrent calls. +func (r *Reaper) useTermSignal() chan bool { + r.mtx.Lock() + defer r.mtx.Unlock() + + if r.termSignal == nil { + return nil + } + + // Use existing connection. + term := r.termSignal + r.termSignal = nil + + return term +} + +// connect connects to the reaper container and sends the labels to it +// so that it can clean up the containers with the same labels. +// +// It returns a channel that can be sent true to terminate the connection. +// Returns an error if config.RyukDisabled is true. +func (r *Reaper) connect(ctx context.Context) (chan bool, error) { + var d net.Dialer + conn, err := d.DialContext(ctx, "tcp", r.Endpoint) + if err != nil { + return nil, fmt.Errorf("dial reaper %s: %w", r.Endpoint, err) + } + terminationSignal := make(chan bool) + go func() { + defer conn.Close() + if err := r.handshake(conn); err != nil { + Logger.Printf("Reaper handshake failed: %s", err) + } <-terminationSignal - }(conn) + }() return terminationSignal, nil } +// handshake sends the labels to the reaper container and reads the ACK. +func (r *Reaper) handshake(conn net.Conn) error { + labels := core.DefaultLabels(r.SessionID) + labelFilters := make([]string, 0, len(labels)) + for l, v := range labels { + labelFilters = append(labelFilters, fmt.Sprintf("label=%s=%s", l, v)) + } + + filters := []byte(strings.Join(labelFilters, "&") + "\n") + buf := make([]byte, 4) + if _, err := conn.Write(filters); err != nil { + return fmt.Errorf("writing filters: %w", err) + } + + n, err := io.ReadFull(conn, buf) + if err != nil { + return fmt.Errorf("read ack: %w", err) + } + + if !bytes.Equal(reaperAck, buf[:n]) { + // We have received the ACK so all done. + return fmt.Errorf("unexpected reaper response: %s", buf[:n]) + } + + return nil +} + // Labels returns the container labels to use so that this Reaper cleans them up // Deprecated: internally replaced by core.DefaultLabels(sessionID) func (r *Reaper) Labels() map[string]string { - return map[string]string{ - core.LabelLang: "go", - core.LabelSessionID: r.SessionID, - } + return GenericLabels() +} + +// isReaperImage returns true if the image name is the reaper image. +func isReaperImage(name string) bool { + return strings.HasSuffix(name, config.ReaperDefaultImage) } diff --git a/reaper_test.go b/reaper_test.go index e526e8ec9a..e9bc5ccb9f 100644 --- a/reaper_test.go +++ b/reaper_test.go @@ -4,14 +4,15 @@ import ( "context" "errors" "os" + "strconv" "sync" "testing" "time" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" + "github.com/docker/docker/errdefs" "github.com/docker/go-connections/nat" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go/internal/config" @@ -23,48 +24,29 @@ import ( const testSessionID = "this-is-a-different-session-id" type mockReaperProvider struct { - req ContainerRequest - hostConfig *container.HostConfig - enpointSettings map[string]*network.EndpointSettings - config TestcontainersConfig - initialReaper *Reaper - initialReaperOnce sync.Once - t *testing.T + req ContainerRequest + hostConfig *container.HostConfig + endpointSettings map[string]*network.EndpointSettings + config TestcontainersConfig } -func newMockReaperProvider(t *testing.T) *mockReaperProvider { +func newMockReaperProvider(cfg config.Config) *mockReaperProvider { m := &mockReaperProvider{ config: TestcontainersConfig{ - Config: config.Config{}, + Config: cfg, }, - t: t, - initialReaper: reaperInstance, - //nolint:govet - initialReaperOnce: reaperOnce, } - // explicitly reset the reaperInstance to nil to start from a fresh state - reaperInstance = nil - reaperOnce = sync.Once{} - return m } var errExpected = errors.New("expected") -func (m *mockReaperProvider) RestoreReaperState() { - m.t.Cleanup(func() { - reaperInstance = m.initialReaper - //nolint:govet - reaperOnce = m.initialReaperOnce - }) -} - func (m *mockReaperProvider) RunContainer(ctx context.Context, req ContainerRequest) (Container, error) { m.req = req m.hostConfig = &container.HostConfig{} - m.enpointSettings = map[string]*network.EndpointSettings{} + m.endpointSettings = map[string]*network.EndpointSettings{} if req.HostConfigModifier == nil { req.HostConfigModifier = defaultHostConfigModifier(req) @@ -72,7 +54,7 @@ func (m *mockReaperProvider) RunContainer(ctx context.Context, req ContainerRequ req.HostConfigModifier(m.hostConfig) if req.EnpointSettingsModifier != nil { - req.EnpointSettingsModifier(m.enpointSettings) + req.EnpointSettingsModifier(m.endpointSettings) } // we're only interested in the request, so instead of mocking the Docker client @@ -84,8 +66,8 @@ func (m *mockReaperProvider) Config() TestcontainersConfig { return m.config } -// createContainerRequest creates the expected request and allows for customization -func createContainerRequest(customize func(ContainerRequest) ContainerRequest) ContainerRequest { +// expectedReaperRequest creates the expected reaper container request with the given customizations. +func expectedReaperRequest(customize ...func(*ContainerRequest)) ContainerRequest { req := ContainerRequest{ Image: config.ReaperDefaultImage, ExposedPorts: []string{"8080/tcp"}, @@ -102,24 +84,29 @@ func createContainerRequest(customize func(ContainerRequest) ContainerRequest) C req.Labels[core.LabelReaper] = "true" req.Labels[core.LabelRyuk] = "true" + delete(req.Labels, core.LabelReap) - if customize == nil { - return req + for _, customize := range customize { + customize(&req) } - return customize(req) + return req } -func TestContainerStartsWithoutTheReaper(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if !tcConfig.RyukDisabled { - t.Skip("Ryuk is enabled, skipping test") - } +// reaperDisable disables / enables the reaper for the duration of the test. +func reaperDisable(t *testing.T, disabled bool) { + t.Helper() + + config.Reset() + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", strconv.FormatBool(disabled)) + t.Cleanup(config.Reset) +} +func testContainerStart(t *testing.T) { + t.Helper() ctx := context.Background() - container, err := GenericContainer(ctx, GenericContainerRequest{ + ctr, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ Image: nginxAlpineImage, @@ -129,62 +116,57 @@ func TestContainerStartsWithoutTheReaper(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, ctr) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, container) +} - sessionID := core.SessionID() +// testReaperRunning validates that a reaper is running. +func testReaperRunning(t *testing.T) { + t.Helper() - reaperContainer, err := lookUpReaperContainer(ctx, sessionID) - if err != nil { - t.Fatal(err, "expected reaper container not found.") - } - if reaperContainer != nil { - t.Fatal("expected zero reaper running.") - } + ctx := context.Background() + sessionID := core.SessionID() + reaperContainer, err := spawner.lookupContainer(ctx, sessionID) + require.NoError(t, err) + require.NotNil(t, reaperContainer) } -func TestContainerStartsWithTheReaper(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if tcConfig.RyukDisabled { - t.Skip("Ryuk is disabled, skipping test") - } +func TestContainer(t *testing.T) { + reaperDisable(t, false) - ctx := context.Background() + t.Run("start/reaper-enabled", func(t *testing.T) { + testContainerStart(t) + testReaperRunning(t) + }) - c, err := GenericContainer(ctx, GenericContainerRequest{ - ProviderType: providerType, - ContainerRequest: ContainerRequest{ - Image: nginxAlpineImage, - ExposedPorts: []string{ - nginxDefaultPort, - }, - }, - Started: true, + t.Run("stop/reaper-enabled", func(t *testing.T) { + testContainerStop(t) + testReaperRunning(t) }) - if err != nil { - t.Fatal(err) - } - terminateContainerOnEnd(t, ctx, c) - sessionID := core.SessionID() + t.Run("terminate/reaper-enabled", func(t *testing.T) { + testContainerTerminate(t) + testReaperRunning(t) + }) - reaperContainer, err := lookUpReaperContainer(ctx, sessionID) - if err != nil { - t.Fatal(err, "expected reaper container running.") - } - if reaperContainer == nil { - t.Fatal("expected one reaper to be running.") - } + reaperDisable(t, true) + + t.Run("start/reaper-disabled", func(t *testing.T) { + testContainerStart(t) + }) + + t.Run("stop/reaper-disabled", func(t *testing.T) { + testContainerStop(t) + }) + + t.Run("terminate/reaper-disabled", func(t *testing.T) { + testContainerTerminate(t) + }) } -func TestContainerStopWithReaper(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if tcConfig.RyukDisabled { - t.Skip("Ryuk is disabled, skipping test") - } +// testContainerStop tests stopping a container. +func testContainerStop(t *testing.T) { + t.Helper() ctx := context.Background() @@ -198,42 +180,26 @@ func TestContainerStopWithReaper(t *testing.T) { }, Started: true, }) - + CleanupContainer(t, nginxA) require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxA) state, err := nginxA.State(ctx) - if err != nil { - t.Fatal(err) - } - if state.Running != true { - t.Fatal("The container shoud be in running state") - } + require.NoError(t, err) + require.True(t, state.Running) + stopTimeout := 10 * time.Second err = nginxA.Stop(ctx, &stopTimeout) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) state, err = nginxA.State(ctx) - if err != nil { - t.Fatal(err) - } - if state.Running != false { - t.Fatal("The container shoud not be running") - } - if state.Status != "exited" { - t.Fatal("The container shoud be in exited state") - } + require.NoError(t, err) + require.False(t, state.Running) + require.Equal(t, "exited", state.Status) } -func TestContainerTerminationWithReaper(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if tcConfig.RyukDisabled { - t.Skip("Ryuk is disabled, skipping test") - } - +// testContainerTerminate tests terminating a container. +func testContainerTerminate(t *testing.T) { + t.Helper() ctx := context.Background() nginxA, err := GenericContainer(ctx, GenericContainerRequest{ @@ -246,344 +212,288 @@ func TestContainerTerminationWithReaper(t *testing.T) { }, Started: true, }) - if err != nil { - t.Fatal(err) - } + CleanupContainer(t, nginxA) + require.NoError(t, err) state, err := nginxA.State(ctx) - if err != nil { - t.Fatal(err) - } - if state.Running != true { - t.Fatal("The container shoud be in running state") - } - err = nginxA.Terminate(ctx) - if err != nil { - t.Fatal(err) - } - _, err = nginxA.State(ctx) - if err == nil { - t.Fatal("expected error from container inspect.") - } -} - -func TestContainerTerminationWithoutReaper(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if !tcConfig.RyukDisabled { - t.Skip("Ryuk is enabled, skipping test") - } - - ctx := context.Background() - - nginxA, err := GenericContainer(ctx, GenericContainerRequest{ - ProviderType: providerType, - ContainerRequest: ContainerRequest{ - Image: nginxAlpineImage, - ExposedPorts: []string{ - nginxDefaultPort, - }, - }, - Started: true, - }) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + require.True(t, state.Running) - state, err := nginxA.State(ctx) - if err != nil { - t.Fatal(err) - } - if state.Running != true { - t.Fatal("The container shoud be in running state") - } err = nginxA.Terminate(ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = nginxA.State(ctx) - if err == nil { - t.Fatal("expected error from container inspect.") - } + require.Error(t, err) } func Test_NewReaper(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if tcConfig.RyukDisabled { - t.Skip("Ryuk is disabled, skipping test") - } + reaperDisable(t, false) - type cases struct { - name string - req ContainerRequest - config TestcontainersConfig - ctx context.Context - env map[string]string - } + ctx := context.Background() - tests := []cases{ - { - name: "non-privileged", - req: createContainerRequest(nil), - config: TestcontainersConfig{Config: config.Config{ + t.Run("non-privileged", func(t *testing.T) { + testNewReaper(ctx, t, + config.Config{ RyukConnectionTimeout: time.Minute, RyukReconnectionTimeout: 10 * time.Second, - }}, - }, - { - name: "privileged", - req: createContainerRequest(func(req ContainerRequest) ContainerRequest { - req.Privileged = true - return req - }), - config: TestcontainersConfig{Config: config.Config{ + }, + expectedReaperRequest(), + ) + }) + + t.Run("privileged", func(t *testing.T) { + testNewReaper(ctx, t, + config.Config{ RyukPrivileged: true, RyukConnectionTimeout: time.Minute, RyukReconnectionTimeout: 10 * time.Second, - }}, - }, - { - name: "configured non-default timeouts", - req: createContainerRequest(func(req ContainerRequest) ContainerRequest { - req.Env = map[string]string{ - "RYUK_CONNECTION_TIMEOUT": "1m0s", - "RYUK_RECONNECTION_TIMEOUT": "10m0s", - } - return req - }), - config: TestcontainersConfig{Config: config.Config{ + }, + expectedReaperRequest(), + ) + }) + + t.Run("custom-timeouts", func(t *testing.T) { + testNewReaper(ctx, t, + config.Config{ RyukPrivileged: true, - RyukConnectionTimeout: time.Minute, - RyukReconnectionTimeout: 10 * time.Minute, - }}, - }, - { - name: "configured verbose mode", - req: createContainerRequest(func(req ContainerRequest) ContainerRequest { + RyukConnectionTimeout: 2 * time.Minute, + RyukReconnectionTimeout: 20 * time.Second, + }, + expectedReaperRequest(func(req *ContainerRequest) { req.Env = map[string]string{ - "RYUK_VERBOSE": "true", + "RYUK_CONNECTION_TIMEOUT": "2m0s", + "RYUK_RECONNECTION_TIMEOUT": "20s", } - return req }), - config: TestcontainersConfig{Config: config.Config{ + ) + }) + + t.Run("verbose", func(t *testing.T) { + testNewReaper(ctx, t, + config.Config{ RyukPrivileged: true, RyukVerbose: true, - }}, - }, - { - name: "docker-host in context", - req: createContainerRequest(func(req ContainerRequest) ContainerRequest { - req.HostConfigModifier = func(hostConfig *container.HostConfig) { - hostConfig.Binds = []string{core.MustExtractDockerSocket(context.Background()) + ":/var/run/docker.sock"} + }, + expectedReaperRequest(func(req *ContainerRequest) { + req.Env = map[string]string{ + "RYUK_VERBOSE": "true", } - return req }), - config: TestcontainersConfig{Config: config.Config{ + ) + }) + + t.Run("docker-host", func(t *testing.T) { + testNewReaper(context.WithValue(ctx, core.DockerHostContextKey, core.DockerSocketPathWithSchema), t, + config.Config{ RyukConnectionTimeout: time.Minute, RyukReconnectionTimeout: 10 * time.Second, - }}, - ctx: context.WithValue(context.TODO(), core.DockerHostContextKey, core.DockerSocketPathWithSchema), - }, - { - name: "Reaper including custom Hub prefix", - req: createContainerRequest(func(req ContainerRequest) ContainerRequest { - req.Image = config.ReaperDefaultImage - req.Privileged = true - return req + }, + expectedReaperRequest(func(req *ContainerRequest) { + req.HostConfigModifier = func(hostConfig *container.HostConfig) { + hostConfig.Binds = []string{core.MustExtractDockerSocket(ctx) + ":/var/run/docker.sock"} + } }), - config: TestcontainersConfig{Config: config.Config{ + ) + }) + + t.Run("hub-prefix", func(t *testing.T) { + testNewReaper(context.WithValue(ctx, core.DockerHostContextKey, core.DockerSocketPathWithSchema), t, + config.Config{ HubImageNamePrefix: "registry.mycompany.com/mirror", RyukPrivileged: true, RyukConnectionTimeout: time.Minute, RyukReconnectionTimeout: 10 * time.Second, - }}, - }, - { - name: "Reaper including custom Hub prefix as env var", - req: createContainerRequest(func(req ContainerRequest) ContainerRequest { + }, + expectedReaperRequest(func(req *ContainerRequest) { req.Image = config.ReaperDefaultImage req.Privileged = true - return req }), - config: TestcontainersConfig{Config: config.Config{ + ) + }) + + t.Run("hub-prefix-env", func(t *testing.T) { + config.Reset() + t.Cleanup(config.Reset) + + t.Setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "registry.mycompany.com/mirror") + testNewReaper(context.WithValue(ctx, core.DockerHostContextKey, core.DockerSocketPathWithSchema), t, + config.Config{ RyukPrivileged: true, RyukConnectionTimeout: time.Minute, RyukReconnectionTimeout: 10 * time.Second, - }}, - env: map[string]string{ - "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX": "registry.mycompany.com/mirror", }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.env != nil { - config.Reset() // reset the config using the internal method to avoid the sync.Once - for k, v := range test.env { - t.Setenv(k, v) - } - } + expectedReaperRequest(func(req *ContainerRequest) { + req.Image = config.ReaperDefaultImage + req.Privileged = true + }), + ) + }) +} - if prefix := os.Getenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX"); prefix != "" { - test.config.Config.HubImageNamePrefix = prefix - } +func testNewReaper(ctx context.Context, t *testing.T, cfg config.Config, expected ContainerRequest) { + t.Helper() - provider := newMockReaperProvider(t) - provider.config = test.config - t.Cleanup(provider.RestoreReaperState) + if prefix := os.Getenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX"); prefix != "" { + cfg.HubImageNamePrefix = prefix + } - if test.ctx == nil { - test.ctx = context.TODO() - } + provider := newMockReaperProvider(cfg) - _, err := reuseOrCreateReaper(test.ctx, testSessionID, provider) - // we should have errored out see mockReaperProvider.RunContainer - require.EqualError(t, err, "expected") + // We need a new reaperSpawner for each test case to avoid reusing + // an existing reaper instance. + spawner := &reaperSpawner{} + reaper, err := spawner.reaper(ctx, testSessionID, provider) + cleanupReaper(t, reaper, spawner) + // We should have errored out see mockReaperProvider.RunContainer. + require.ErrorIs(t, err, errExpected) - assert.Equal(t, test.req.Image, provider.req.Image, "expected image doesn't match the submitted request") - assert.Equal(t, test.req.ExposedPorts, provider.req.ExposedPorts, "expected exposed ports don't match the submitted request") - assert.Equal(t, test.req.Labels, provider.req.Labels, "expected labels don't match the submitted request") - assert.Equal(t, test.req.Mounts, provider.req.Mounts, "expected mounts don't match the submitted request") - assert.Equal(t, test.req.WaitingFor, provider.req.WaitingFor, "expected waitingFor don't match the submitted request") - assert.Equal(t, test.req.Env, provider.req.Env, "expected env doesn't match the submitted request") + require.Equal(t, expected.Image, provider.req.Image, "expected image doesn't match the submitted request") + require.Equal(t, expected.ExposedPorts, provider.req.ExposedPorts, "expected exposed ports don't match the submitted request") + require.Equal(t, expected.Labels, provider.req.Labels, "expected labels don't match the submitted request") + require.Equal(t, expected.Mounts, provider.req.Mounts, "expected mounts don't match the submitted request") + require.Equal(t, expected.WaitingFor, provider.req.WaitingFor, "expected waitingFor don't match the submitted request") + require.Equal(t, expected.Env, provider.req.Env, "expected env doesn't match the submitted request") - // checks for reaper's preCreationCallback fields - assert.Equal(t, container.NetworkMode(Bridge), provider.hostConfig.NetworkMode, "expected networkMode doesn't match the submitted request") - assert.True(t, provider.hostConfig.AutoRemove, "expected networkMode doesn't match the submitted request") - }) - } + // checks for reaper's preCreationCallback fields + require.Equal(t, container.NetworkMode(Bridge), provider.hostConfig.NetworkMode, "expected networkMode doesn't match the submitted request") + require.True(t, provider.hostConfig.AutoRemove, "expected networkMode doesn't match the submitted request") } func Test_ReaperReusedIfHealthy(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if tcConfig.RyukDisabled { - t.Skip("Ryuk is disabled, skipping test") - } - - testProvider := newMockReaperProvider(t) - t.Cleanup(testProvider.RestoreReaperState) + reaperDisable(t, false) SkipIfProviderIsNotHealthy(t) ctx := context.Background() // As other integration tests run with the (shared) Reaper as well, re-use the instance to not interrupt other tests - wasReaperRunning := reaperInstance != nil + if spawner.instance != nil { + t.Cleanup(func() { + require.NoError(t, spawner.cleanup()) + }) + } - provider, _ := ProviderDocker.GetProvider() - reaper, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + provider, err := ProviderDocker.GetProvider() + require.NoError(t, err) + + reaper, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + cleanupReaper(t, reaper, spawner) require.NoError(t, err, "creating the Reaper should not error") - reaperReused, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + reaperReused, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + cleanupReaper(t, reaper, spawner) require.NoError(t, err, "reusing the Reaper should not error") - // assert that the internal state of both reaper instances is the same - assert.Equal(t, reaper.SessionID, reaperReused.SessionID, "expecting the same SessionID") - assert.Equal(t, reaper.Endpoint, reaperReused.Endpoint, "expecting the same reaper endpoint") - assert.Equal(t, reaper.Provider, reaperReused.Provider, "expecting the same container provider") - assert.Equal(t, reaper.container.GetContainerID(), reaperReused.container.GetContainerID(), "expecting the same container ID") - assert.Equal(t, reaper.container.SessionID(), reaperReused.container.SessionID(), "expecting the same session ID") - - terminate, err := reaper.Connect() - defer func(term chan bool) { - term <- true - }(terminate) - require.NoError(t, err, "connecting to Reaper should be successful") - if !wasReaperRunning { - terminateContainerOnEnd(t, ctx, reaper.container) - } + // Ensure the internal state of both reaper instances is the same + require.Equal(t, reaper.SessionID, reaperReused.SessionID, "expecting the same SessionID") + require.Equal(t, reaper.Endpoint, reaperReused.Endpoint, "expecting the same reaper endpoint") + require.Equal(t, reaper.Provider, reaperReused.Provider, "expecting the same container provider") + require.Equal(t, reaper.container.GetContainerID(), reaperReused.container.GetContainerID(), "expecting the same container ID") + require.Equal(t, reaper.container.SessionID(), reaperReused.container.SessionID(), "expecting the same session ID") + + termSignal, err := reaper.Connect() + cleanupTermSignal(t, termSignal) + require.NoError(t, err, "connecting to Reaper should be successful") } func Test_RecreateReaperIfTerminated(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if tcConfig.RyukDisabled { - t.Skip("Ryuk is disabled, skipping test") - } - - mockProvider := newMockReaperProvider(t) - t.Cleanup(mockProvider.RestoreReaperState) + reaperDisable(t, false) SkipIfProviderIsNotHealthy(t) - provider, _ := ProviderDocker.GetProvider() + provider, err := ProviderDocker.GetProvider() + require.NoError(t, err) + ctx := context.Background() - reaper, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + reaper, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + cleanupReaper(t, reaper, spawner) require.NoError(t, err, "creating the Reaper should not error") - terminate, err := reaper.Connect() - require.NoError(t, err, "connecting to Reaper should be successful") - terminate <- true + termSignal, err := reaper.Connect() + if termSignal != nil { + termSignal <- true + } + require.NoError(t, err) + + // Wait for up to ryuk's default reconnect timeout + 1s to allow for a graceful shutdown/cleanup of the container. + timeout := time.NewTimer(time.Second * 11) + t.Cleanup(func() { + timeout.Stop() + }) + for { + state, err := reaper.container.State(ctx) + if err != nil { + if errdefs.IsNotFound(err) { + break + } + require.NoError(t, err) + } + + if !state.Running { + break + } + + select { + case <-timeout.C: + t.Fatal("reaper container should have been terminated") + default: + } - // Wait for ryuk's default timeout (10s) + 1s to allow for a graceful shutdown/cleanup of the container. - time.Sleep(11 * time.Second) + time.Sleep(time.Millisecond * 100) + } - recreatedReaper, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + recreatedReaper, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + cleanupReaper(t, recreatedReaper, spawner) require.NoError(t, err, "creating the Reaper should not error") - assert.NotEqual(t, reaper.container.GetContainerID(), recreatedReaper.container.GetContainerID(), "expected different container ID") + require.NotEqual(t, reaper.container.GetContainerID(), recreatedReaper.container.GetContainerID(), "expected different container ID") - terminate, err = recreatedReaper.Connect() - defer func(term chan bool) { - term <- true - }(terminate) + recreatedTermSignal, err := recreatedReaper.Connect() + cleanupTermSignal(t, recreatedTermSignal) require.NoError(t, err, "connecting to Reaper should be successful") - terminateContainerOnEnd(t, ctx, recreatedReaper.container) } func TestReaper_reuseItFromOtherTestProgramUsingDocker(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if tcConfig.RyukDisabled { - t.Skip("Ryuk is disabled, skipping test") - } - - mockProvider := &mockReaperProvider{ - initialReaper: reaperInstance, - //nolint:govet - initialReaperOnce: reaperOnce, - t: t, - } - t.Cleanup(mockProvider.RestoreReaperState) + reaperDisable(t, false) - // explicitly set the reaperInstance to nil to simulate another test program in the same session accessing the same reaper - reaperInstance = nil - reaperOnce = sync.Once{} + // Explicitly set the reaper instance to nil to simulate another test + // program in the same session accessing the same reaper. + spawner.instance = nil SkipIfProviderIsNotHealthy(t) ctx := context.Background() - // As other integration tests run with the (shared) Reaper as well, re-use the instance to not interrupt other tests - wasReaperRunning := reaperInstance != nil + // As other integration tests run with the (shared) Reaper as well, + // re-use the instance to not interrupt other tests. + if spawner.instance != nil { + t.Cleanup(func() { + require.NoError(t, spawner.cleanup()) + }) + } + + provider, err := ProviderDocker.GetProvider() + require.NoError(t, err) - provider, _ := ProviderDocker.GetProvider() - reaper, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + reaper, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + cleanupReaper(t, reaper, spawner) require.NoError(t, err, "creating the Reaper should not error") - // explicitly reset the reaperInstance to nil to simulate another test program in the same session accessing the same reaper - reaperInstance = nil - reaperOnce = sync.Once{} + // Explicitly reset the reaper instance to nil to simulate another test + // program in the same session accessing the same reaper. + spawner.instance = nil - reaperReused, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + reaperReused, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, provider.(*DockerProvider).host), testSessionID, provider) + cleanupReaper(t, reaper, spawner) require.NoError(t, err, "reusing the Reaper should not error") - // assert that the internal state of both reaper instances is the same - assert.Equal(t, reaper.SessionID, reaperReused.SessionID, "expecting the same SessionID") - assert.Equal(t, reaper.Endpoint, reaperReused.Endpoint, "expecting the same reaper endpoint") - assert.Equal(t, reaper.Provider, reaperReused.Provider, "expecting the same container provider") - assert.Equal(t, reaper.container.GetContainerID(), reaperReused.container.GetContainerID(), "expecting the same container ID") - assert.Equal(t, reaper.container.SessionID(), reaperReused.container.SessionID(), "expecting the same session ID") - - terminate, err := reaper.Connect() - defer func(term chan bool) { - term <- true - }(terminate) - require.NoError(t, err, "connecting to Reaper should be successful") - if !wasReaperRunning { - terminateContainerOnEnd(t, ctx, reaper.container) - } + // Ensure that the internal state of both reaper instances is the same. + require.Equal(t, reaper.SessionID, reaperReused.SessionID, "expecting the same SessionID") + require.Equal(t, reaper.Endpoint, reaperReused.Endpoint, "expecting the same reaper endpoint") + require.Equal(t, reaper.Provider, reaperReused.Provider, "expecting the same container provider") + require.Equal(t, reaper.container.GetContainerID(), reaperReused.container.GetContainerID(), "expecting the same container ID") + require.Equal(t, reaper.container.SessionID(), reaperReused.container.SessionID(), "expecting the same session ID") + + termSignal, err := reaper.Connect() + cleanupTermSignal(t, termSignal) + require.NoError(t, err, "connecting to Reaper should be successful") } // TestReaper_ReuseRunning tests whether reusing the reaper if using @@ -594,15 +504,11 @@ func TestReaper_reuseItFromOtherTestProgramUsingDocker(t *testing.T) { // already running for the same session id by returning its container instance // instead. func TestReaper_ReuseRunning(t *testing.T) { - config.Reset() // reset the config using the internal method to avoid the sync.Once - tcConfig := config.Read() - if tcConfig.RyukDisabled { - t.Skip("Ryuk is disabled, skipping test") - } + reaperDisable(t, false) const concurrency = 64 - timeout, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + timeout, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() sessionID := SessionID() @@ -613,27 +519,54 @@ func TestReaper_ReuseRunning(t *testing.T) { obtainedReaperContainerIDs := make([]string, concurrency) var wg sync.WaitGroup for i := 0; i < concurrency; i++ { - i := i wg.Add(1) - go func() { + go func(i int) { defer wg.Done() - reaperContainer, err := lookUpReaperContainer(timeout, sessionID) - if err == nil && reaperContainer != nil { - // Found. - obtainedReaperContainerIDs[i] = reaperContainer.GetContainerID() - return - } - // Not found -> create. - createdReaper, err := newReaper(timeout, sessionID, dockerProvider) - require.NoError(t, err, "new reaper should not fail") - obtainedReaperContainerIDs[i] = createdReaper.container.GetContainerID() - }() + spawner := &reaperSpawner{} + reaper, err := spawner.reaper(timeout, sessionID, dockerProvider) + cleanupReaper(t, reaper, spawner) + require.NoError(t, err) + + obtainedReaperContainerIDs[i] = reaper.container.GetContainerID() + }(i) } wg.Wait() // Assure that all calls returned the same container. firstContainerID := obtainedReaperContainerIDs[0] for i, containerID := range obtainedReaperContainerIDs { - assert.Equal(t, firstContainerID, containerID, "call %d should have returned same container id", i) + require.Equal(t, firstContainerID, containerID, "call %d should have returned same container id", i) } } + +func TestSpawnerBackoff(t *testing.T) { + b := spawner.backoff() + for i := 0; i < 100; i++ { + require.LessOrEqual(t, b.NextBackOff(), time.Millisecond*250, "backoff should not exceed max interval") + } +} + +// cleanupReaper schedules reaper for cleanup if it's not nil. +func cleanupReaper(t *testing.T, reaper *Reaper, spawner *reaperSpawner) { + t.Helper() + + if reaper == nil { + return + } + + t.Cleanup(func() { + reaper.close() + require.NoError(t, spawner.cleanup()) + }) +} + +// cleanupTermSignal ensures that termSignal +func cleanupTermSignal(t *testing.T, termSignal chan bool) { + t.Helper() + + t.Cleanup(func() { + if termSignal != nil { + termSignal <- true + } + }) +} diff --git a/requirements.txt b/requirements.txt index 83689b0f87..e4db8827e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ mkdocs==1.5.3 mkdocs-codeinclude-plugin==0.2.1 -mkdocs-include-markdown-plugin==6.0.4 +mkdocs-include-markdown-plugin==6.2.2 mkdocs-material==9.5.18 -mkdocs-markdownextradata-plugin==0.2.5 +mkdocs-markdownextradata-plugin==0.2.6 diff --git a/scripts/bump-reaper.sh b/scripts/bump-reaper.sh index 34d17ce463..86af6b31c0 100755 --- a/scripts/bump-reaper.sh +++ b/scripts/bump-reaper.sh @@ -5,11 +5,11 @@ # dry-run mode, which will print the commands that would be executed, without actually # executing them. # -# Usage: ./scripts/bump-reaper.sh "docker.io/testcontainers/ryuk:1.2.3" +# Usage: ./scripts/bump-reaper.sh "testcontainers/ryuk:1.2.3" # # It's possible to run the script without dry-run mode actually executing the commands. # -# Usage: DRY_RUN="false" ./scripts/bump-reaper.sh "docker.io/testcontainers/ryuk:1.2.3" +# Usage: DRY_RUN="false" ./scripts/bump-reaper.sh "testcontainers/ryuk:1.2.3" readonly CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" readonly DRY_RUN="${DRY_RUN:-true}" diff --git a/scripts/release.sh b/scripts/release.sh index 6709c5c358..b675cb51e2 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -13,7 +13,7 @@ # Usage: DRY_RUN="false" ./scripts/release.sh readonly BUMP_TYPE="${BUMP_TYPE:-minor}" -readonly DOCKER_IMAGE_SEMVER="docker.io/mdelapenya/semver-tool:3.4.0" +readonly DOCKER_IMAGE_SEMVER="mdelapenya/semver-tool:3.4.0" readonly DRY_RUN="${DRY_RUN:-true}" readonly CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" readonly ROOT_DIR="$(dirname "$CURRENT_DIR")" diff --git a/sonar-project.properties b/sonar-project.properties index aaa203e905..67ef15fcd5 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -7,7 +7,7 @@ sonar.projectKey=testcontainers_testcontainers-go sonar.projectName=testcontainers-go -sonar.projectVersion=v0.33.0 +sonar.projectVersion=v0.34.0 sonar.sources=. @@ -18,4 +18,4 @@ sonar.test.inclusions=**/*_test.go sonar.test.exclusions=**/vendor/** sonar.go.coverage.reportPaths=**/coverage.out -sonar.go.tests.reportPaths=TEST-unit.xml,examples/nginx/TEST-unit.xml,examples/toxiproxy/TEST-unit.xml,modulegen/TEST-unit.xml,modules/artemis/TEST-unit.xml,modules/azurite/TEST-unit.xml,modules/cassandra/TEST-unit.xml,modules/chroma/TEST-unit.xml,modules/clickhouse/TEST-unit.xml,modules/cockroachdb/TEST-unit.xml,modules/compose/TEST-unit.xml,modules/consul/TEST-unit.xml,modules/couchbase/TEST-unit.xml,modules/dolt/TEST-unit.xml,modules/elasticsearch/TEST-unit.xml,modules/gcloud/TEST-unit.xml,modules/grafana-lgtm/TEST-unit.xml,modules/inbucket/TEST-unit.xml,modules/influxdb/TEST-unit.xml,modules/k3s/TEST-unit.xml,modules/k6/TEST-unit.xml,modules/kafka/TEST-unit.xml,modules/localstack/TEST-unit.xml,modules/mariadb/TEST-unit.xml,modules/milvus/TEST-unit.xml,modules/minio/TEST-unit.xml,modules/mockserver/TEST-unit.xml,modules/mongodb/TEST-unit.xml,modules/mssql/TEST-unit.xml,modules/mysql/TEST-unit.xml,modules/nats/TEST-unit.xml,modules/neo4j/TEST-unit.xml,modules/ollama/TEST-unit.xml,modules/openfga/TEST-unit.xml,modules/openldap/TEST-unit.xml,modules/opensearch/TEST-unit.xml,modules/postgres/TEST-unit.xml,modules/pulsar/TEST-unit.xml,modules/qdrant/TEST-unit.xml,modules/rabbitmq/TEST-unit.xml,modules/redis/TEST-unit.xml,modules/redpanda/TEST-unit.xml,modules/registry/TEST-unit.xml,modules/surrealdb/TEST-unit.xml,modules/valkey/TEST-unit.xml,modules/vault/TEST-unit.xml,modules/vearch/TEST-unit.xml,modules/weaviate/TEST-unit.xml +sonar.go.tests.reportPaths=TEST-unit.xml,examples/nginx/TEST-unit.xml,examples/toxiproxy/TEST-unit.xml,modulegen/TEST-unit.xml,modules/artemis/TEST-unit.xml,modules/azurite/TEST-unit.xml,modules/cassandra/TEST-unit.xml,modules/chroma/TEST-unit.xml,modules/clickhouse/TEST-unit.xml,modules/cockroachdb/TEST-unit.xml,modules/compose/TEST-unit.xml,modules/consul/TEST-unit.xml,modules/couchbase/TEST-unit.xml,modules/databend/TEST-unit.xml,modules/dolt/TEST-unit.xml,modules/dynamodb/TEST-unit.xml,modules/elasticsearch/TEST-unit.xml,modules/etcd/TEST-unit.xml,modules/gcloud/TEST-unit.xml,modules/grafana-lgtm/TEST-unit.xml,modules/inbucket/TEST-unit.xml,modules/influxdb/TEST-unit.xml,modules/k3s/TEST-unit.xml,modules/k6/TEST-unit.xml,modules/kafka/TEST-unit.xml,modules/localstack/TEST-unit.xml,modules/mariadb/TEST-unit.xml,modules/meilisearch/TEST-unit.xml,modules/milvus/TEST-unit.xml,modules/minio/TEST-unit.xml,modules/mockserver/TEST-unit.xml,modules/mongodb/TEST-unit.xml,modules/mssql/TEST-unit.xml,modules/mysql/TEST-unit.xml,modules/nats/TEST-unit.xml,modules/neo4j/TEST-unit.xml,modules/ollama/TEST-unit.xml,modules/openfga/TEST-unit.xml,modules/openldap/TEST-unit.xml,modules/opensearch/TEST-unit.xml,modules/postgres/TEST-unit.xml,modules/pulsar/TEST-unit.xml,modules/qdrant/TEST-unit.xml,modules/rabbitmq/TEST-unit.xml,modules/redis/TEST-unit.xml,modules/redpanda/TEST-unit.xml,modules/registry/TEST-unit.xml,modules/surrealdb/TEST-unit.xml,modules/valkey/TEST-unit.xml,modules/vault/TEST-unit.xml,modules/vearch/TEST-unit.xml,modules/weaviate/TEST-unit.xml,modules/yugabytedb/TEST-unit.xml diff --git a/testcontainers_test.go b/testcontainers_test.go index fe5af71e89..6c06d87483 100644 --- a/testcontainers_test.go +++ b/testcontainers_test.go @@ -1,27 +1,24 @@ package testcontainers import ( - "fmt" "os" "os/exec" "regexp" "testing" + + "github.com/stretchr/testify/require" ) func TestSessionID(t *testing.T) { t.Run("SessionID() returns a non-empty string", func(t *testing.T) { sessionID := SessionID() - if sessionID == "" { - t.Error("SessionID() returned an empty string") - } + require.NotEmptyf(t, sessionID, "SessionID() returned an empty string") }) t.Run("Multiple calls to SessionID() return the same value", func(t *testing.T) { sessionID1 := SessionID() sessionID2 := SessionID() - if sessionID1 != sessionID2 { - t.Errorf("SessionID() returned different values: %s != %s", sessionID1, sessionID2) - } + require.Equalf(t, sessionID1, sessionID2, "SessionID() returned different values: %s != %s", sessionID1, sessionID2) }) t.Run("Multiple calls to SessionID() in multiple goroutines return the same value", func(t *testing.T) { @@ -42,9 +39,7 @@ func TestSessionID(t *testing.T) { <-done <-done - if sessionID1 != sessionID2 { - t.Errorf("SessionID() returned different values: %s != %s", sessionID1, sessionID2) - } + require.Equalf(t, sessionID1, sessionID2, "SessionID() returned different values: %s != %s", sessionID1, sessionID2) }) t.Run("SessionID() from different child processes returns the same value", func(t *testing.T) { @@ -56,22 +51,16 @@ func TestSessionID(t *testing.T) { cmd1 := exec.Command("go", args...) cmd1.Env = env stdoutStderr1, err := cmd1.CombinedOutput() - if err != nil { - t.Errorf("cmd1.Run() failed with %s", err) - } + require.NoErrorf(t, err, "cmd1.Run() failed with %s", err) sessionID1 := re.FindString(string(stdoutStderr1)) cmd2 := exec.Command("go", args...) cmd2.Env = env stdoutStderr2, err := cmd2.CombinedOutput() - if err != nil { - t.Errorf("cmd2.Run() failed with %s", err) - } + require.NoErrorf(t, err, "cmd2.Run() failed with %s", err) sessionID2 := re.FindString(string(stdoutStderr2)) - if sessionID1 != sessionID2 { - t.Errorf("SessionID() returned different values: %s != %s", sessionID1, sessionID2) - } + require.Equalf(t, sessionID1, sessionID2, "SessionID() returned different values: %s != %s", sessionID1, sessionID2) }) } @@ -81,5 +70,5 @@ func TestSessionIDHelper(t *testing.T) { t.Skip("Not a real test, used as a test helper") } - fmt.Printf(">>>%s<<<\n", SessionID()) + t.Logf(">>>%s<<<\n", SessionID()) } diff --git a/testdata/Dockerfile b/testdata/Dockerfile index 7157611a13..14cfaf1e23 100644 --- a/testdata/Dockerfile +++ b/testdata/Dockerfile @@ -1 +1 @@ -FROM docker.io/redis:5.0-alpine@sha256:1a3c609295332f1ce603948142a132656c92a08149d7096e203058533c415b8c +FROM redis:5.0-alpine@sha256:1a3c609295332f1ce603948142a132656c92a08149d7096e203058533c415b8c diff --git a/testdata/args.Dockerfile b/testdata/args.Dockerfile index 984ef51eee..0260639719 100644 --- a/testdata/args.Dockerfile +++ b/testdata/args.Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/golang:1.13-alpine +FROM golang:1.13-alpine ARG FOO diff --git a/testdata/buildlog.Dockerfile b/testdata/buildlog.Dockerfile index 0a9bc82c98..67fd379018 100644 --- a/testdata/buildlog.Dockerfile +++ b/testdata/buildlog.Dockerfile @@ -1 +1 @@ -FROM docker.io/alpine +FROM alpine diff --git a/testdata/echo.Dockerfile b/testdata/echo.Dockerfile index 10ab9febf4..36951e1aa6 100644 --- a/testdata/echo.Dockerfile +++ b/testdata/echo.Dockerfile @@ -1,3 +1,3 @@ -FROM docker.io/alpine +FROM alpine CMD ["echo", "this is from the echo test Dockerfile"] \ No newline at end of file diff --git a/testdata/echoserver.Dockerfile b/testdata/echoserver.Dockerfile index 546489ffac..aaf835f35a 100644 --- a/testdata/echoserver.Dockerfile +++ b/testdata/echoserver.Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/golang:1.13-alpine +FROM golang:1.13-alpine WORKDIR /app diff --git a/testdata/echoserver.go b/testdata/echoserver.go index a62c783f5d..1222b7045f 100644 --- a/testdata/echoserver.go +++ b/testdata/echoserver.go @@ -36,7 +36,8 @@ func main() { ln, err := net.Listen("tcp", ":8080") if err != nil { - log.Fatal(err) + log.Println(err) + return } fmt.Println("ready") diff --git a/testdata/error.Dockerfile b/testdata/error.Dockerfile index 1284e7285f..5d31293182 100644 --- a/testdata/error.Dockerfile +++ b/testdata/error.Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/alpine +FROM alpine RUN exit 1 diff --git a/testdata/invalid-config/.docker/config.json b/testdata/invalid-config/.docker/config.json new file mode 100644 index 0000000000..f0f444f355 --- /dev/null +++ b/testdata/invalid-config/.docker/config.json @@ -0,0 +1,3 @@ +{ + "auths": [] +} diff --git a/testdata/target.Dockerfile b/testdata/target.Dockerfile index 996a83552f..f6a20273c7 100644 --- a/testdata/target.Dockerfile +++ b/testdata/target.Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/alpine AS target0 +FROM alpine AS target0 CMD ["echo", "target0"] FROM target0 AS target1 diff --git a/testhelpers_test.go b/testhelpers_test.go index 3be3b7c50d..8d7587c17c 100644 --- a/testhelpers_test.go +++ b/testhelpers_test.go @@ -1,25 +1,6 @@ package testcontainers_test -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/testcontainers/testcontainers-go" -) - const ( - nginxAlpineImage = "docker.io/nginx:alpine" + nginxAlpineImage = "nginx:alpine" nginxDefaultPort = "80/tcp" ) - -func terminateContainerOnEnd(tb testing.TB, ctx context.Context, ctr testcontainers.Container) { - tb.Helper() - if ctr == nil { - return - } - tb.Cleanup(func() { - require.NoError(tb, ctr.Terminate(ctx)) - }) -} diff --git a/testing.go b/testing.go index eab23cb805..35ce4f0a39 100644 --- a/testing.go +++ b/testing.go @@ -3,14 +3,24 @@ package testcontainers import ( "context" "fmt" + "io" + "regexp" "testing" + + "github.com/docker/docker/errdefs" + "github.com/stretchr/testify/require" ) +// 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`) + // 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. // In this way tests that depend on Testcontainers won't run if the provider is provisioned correctly. func SkipIfProviderIsNotHealthy(t *testing.T) { + t.Helper() ctx := context.Background() provider, err := ProviderDocker.GetProvider() if err != nil { @@ -25,15 +35,12 @@ func SkipIfProviderIsNotHealthy(t *testing.T) { // SkipIfDockerDesktop is a utility function capable of skipping tests // if tests are run using Docker Desktop. func SkipIfDockerDesktop(t *testing.T, ctx context.Context) { + t.Helper() cli, err := NewDockerClientWithOpts(ctx) - if err != nil { - t.Fatalf("failed to create docker client: %s", err) - } + require.NoErrorf(t, err, "failed to create docker client: %s", err) info, err := cli.Info(ctx) - if err != nil { - t.Fatalf("failed to get docker info: %s", err) - } + require.NoErrorf(t, err, "failed to get docker info: %s", err) if info.OperatingSystem == "Docker Desktop" { t.Skip("Skipping test that requires host network access when running in Docker Desktop") @@ -51,3 +58,108 @@ func (lc *StdoutLogConsumer) Accept(l Log) { } // } + +// CleanupContainer is a helper function that schedules the container +// to be stopped / terminated when the test ends. +// +// This should be called as a defer directly after (before any error check) +// of [GenericContainer](...) or a modules Run(...) in a test to ensure the +// container is stopped when the function ends. +// +// before any error check. If container is nil, its a no-op. +func CleanupContainer(tb testing.TB, ctr Container, options ...TerminateOption) { + tb.Helper() + + tb.Cleanup(func() { + noErrorOrIgnored(tb, TerminateContainer(ctr, options...)) + }) +} + +// CleanupNetwork is a helper function that schedules the network to be +// removed when the test ends. +// This should be the first call after NewNetwork(...) in a test before +// any error check. If network is nil, its a no-op. +func CleanupNetwork(tb testing.TB, network Network) { + tb.Helper() + + tb.Cleanup(func() { + noErrorOrIgnored(tb, network.Remove(context.Background())) + }) +} + +// noErrorOrIgnored is a helper function that checks if the error is nil or an error +// we can ignore. +func noErrorOrIgnored(tb testing.TB, err error) { + tb.Helper() + + if isCleanupSafe(err) { + return + } + + require.NoError(tb, err) +} + +// 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 + } +} + +// RequireContainerExec is a helper function that executes a command in a container +// It insures that there is no error during the execution +// Finally returns the output of its execution +func RequireContainerExec(ctx context.Context, t *testing.T, container Container, cmd []string) string { + t.Helper() + + code, out, err := container.Exec(ctx, cmd) + require.NoError(t, err) + require.Zero(t, code) + + checkBytes, err := io.ReadAll(out) + require.NoError(t, err) + return string(checkBytes) +} diff --git a/testing_test.go b/testing_test.go index 56817d655a..6c50738220 100644 --- a/testing_test.go +++ b/testing_test.go @@ -1,7 +1,86 @@ package testcontainers -import "testing" +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) func ExampleSkipIfProviderIsNotHealthy() { SkipIfProviderIsNotHealthy(&testing.T{}) } + +type notFoundError struct{} + +func (notFoundError) NotFound() {} + +func (notFoundError) Error() string { + return "not found" +} + +func Test_isNotFound(t *testing.T) { + tests := map[string]struct { + err error + want bool + }{ + "nil": { + err: nil, + want: true, + }, + "join-nils": { + err: errors.Join(nil, nil), + want: true, + }, + "join-nil-not-found": { + err: errors.Join(nil, notFoundError{}), + want: true, + }, + "not-found": { + err: notFoundError{}, + want: true, + }, + "other": { + err: errors.New("other"), + want: false, + }, + "join-other": { + err: errors.Join(nil, notFoundError{}, errors.New("other")), + want: false, + }, + "warp": { + err: fmt.Errorf("wrap: %w", notFoundError{}), + want: true, + }, + "multi-warp": { + err: fmt.Errorf("wrap: %w", fmt.Errorf("wrap: %w", notFoundError{})), + want: true, + }, + "multi-warp-other": { + err: fmt.Errorf("wrap: %w", fmt.Errorf("wrap: %w", errors.New("other"))), + want: false, + }, + "multi-warp-other-not-found": { + err: fmt.Errorf("wrap: %w", fmt.Errorf("wrap: %w %w", errors.New("other"), notFoundError{})), + want: false, + }, + "multi-warp-not-found-nil": { + err: fmt.Errorf("wrap: %w", fmt.Errorf("wrap: %w %w", nil, notFoundError{})), + want: true, + }, + "multi-join-not-found-other": { + err: errors.Join(nil, fmt.Errorf("wrap: %w", errors.Join(notFoundError{}, errors.New("other")))), + want: false, + }, + "multi-join-not-found-nil": { + err: errors.Join(nil, fmt.Errorf("wrap: %w", errors.Join(notFoundError{}, nil))), + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + require.Equal(t, tc.want, isCleanupSafe(tc.err)) + }) + } +} diff --git a/wait/all.go b/wait/all.go index fb097fb5ea..fb7eb4e5f3 100644 --- a/wait/all.go +++ b/wait/all.go @@ -2,7 +2,7 @@ package wait import ( "context" - "fmt" + "errors" "time" ) @@ -58,7 +58,7 @@ func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarg } if len(ms.Strategies) == 0 { - return fmt.Errorf("no wait strategy supplied") + return errors.New("no wait strategy supplied") } for _, strategy := range ms.Strategies { diff --git a/wait/all_test.go b/wait/all_test.go index 770a54f32c..87b00bb5ee 100644 --- a/wait/all_test.go +++ b/wait/all_test.go @@ -7,6 +7,8 @@ import ( "io" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestMultiStrategy_WaitUntilReady(t *testing.T) { @@ -113,8 +115,11 @@ func TestMultiStrategy_WaitUntilReady(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - if err := tt.strategy.WaitUntilReady(tt.args.ctx, tt.args.target); (err != nil) != tt.wantErr { - t.Errorf("ForAll.WaitUntilReady() error = %v, wantErr = %v", err, tt.wantErr) + err := tt.strategy.WaitUntilReady(tt.args.ctx, tt.args.target) + if tt.wantErr { + require.Error(t, err, "ForAll.WaitUntilReady()") + } else { + require.NoErrorf(t, err, "ForAll.WaitUntilReady()") } }) } diff --git a/wait/exec_test.go b/wait/exec_test.go index 13e3e47511..a431b146da 100644 --- a/wait/exec_test.go +++ b/wait/exec_test.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" tcexec "github.com/testcontainers/testcontainers-go/exec" @@ -21,27 +22,29 @@ import ( func ExampleExecStrategy() { ctx := context.Background() req := testcontainers.ContainerRequest{ - Image: "localstack/localstack:latest", - WaitingFor: wait.ForExec([]string{"awslocal", "dynamodb", "list-tables"}), + Image: "alpine:latest", + Entrypoint: []string{"tail", "-f", "/dev/null"}, // needed for the container to stay alive + WaitingFor: wait.ForExec([]string{"ls", "/"}).WithStartupTimeout(1 * time.Second), } - localstack, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := localstack.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(ctr); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } - state, err := localstack.State(ctx) + state, err := ctr.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -111,18 +114,14 @@ func TestExecStrategyWaitUntilReady(t *testing.T) { wg := wait.NewExecStrategy([]string{"true"}). WithStartupTimeout(30 * time.Second) err := wg.WaitUntilReady(context.Background(), target) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } func TestExecStrategyWaitUntilReadyForExec(t *testing.T) { target := mockExecTarget{} wg := wait.ForExec([]string{"true"}) err := wg.WaitUntilReady(context.Background(), target) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } func TestExecStrategyWaitUntilReady_MultipleChecks(t *testing.T) { @@ -133,9 +132,7 @@ func TestExecStrategyWaitUntilReady_MultipleChecks(t *testing.T) { wg := wait.NewExecStrategy([]string{"true"}). WithPollInterval(500 * time.Millisecond) err := wg.WaitUntilReady(context.Background(), target) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } func TestExecStrategyWaitUntilReady_DeadlineExceeded(t *testing.T) { @@ -147,9 +144,7 @@ func TestExecStrategyWaitUntilReady_DeadlineExceeded(t *testing.T) { } wg := wait.NewExecStrategy([]string{"true"}) err := wg.WaitUntilReady(ctx, target) - if !errors.Is(err, context.DeadlineExceeded) { - t.Fatal(err) - } + require.ErrorIs(t, err, context.DeadlineExceeded) } func TestExecStrategyWaitUntilReady_CustomExitCode(t *testing.T) { @@ -160,9 +155,7 @@ func TestExecStrategyWaitUntilReady_CustomExitCode(t *testing.T) { return exitCode == 10 }) err := wg.WaitUntilReady(context.Background(), target) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } func TestExecStrategyWaitUntilReady_withExitCode(t *testing.T) { @@ -173,23 +166,19 @@ func TestExecStrategyWaitUntilReady_withExitCode(t *testing.T) { // Default is 60. Let's shorten that wg.WithStartupTimeout(time.Second * 2) err := wg.WaitUntilReady(context.Background(), target) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // Ensure we aren't spuriously returning on any code wg = wait.NewExecStrategy([]string{"true"}).WithExitCode(0) wg.WithStartupTimeout(time.Second * 2) err = wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatalf("Expected strategy to timeout out") - } + require.Errorf(t, err, "Expected strategy to timeout out") } func TestExecStrategyWaitUntilReady_CustomResponseMatcher(t *testing.T) { // waitForExecExitCodeResponse { dockerReq := testcontainers.ContainerRequest{ - Image: "docker.io/nginx:latest", + Image: "nginx:latest", WaitingFor: wait.ForExec([]string{"echo", "hello world!"}). WithStartupTimeout(time.Second * 10). WithExitCodeMatcher(func(exitCode int) bool { @@ -203,14 +192,8 @@ func TestExecStrategyWaitUntilReady_CustomResponseMatcher(t *testing.T) { // } ctx := context.Background() - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ContainerRequest: dockerReq, Started: true}) - if err != nil { - t.Error(err) - return - } - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ContainerRequest: dockerReq, Started: true}) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + // } } diff --git a/wait/exit_test.go b/wait/exit_test.go index 4795fd4ad6..5c2ec004db 100644 --- a/wait/exit_test.go +++ b/wait/exit_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/require" tcexec "github.com/testcontainers/testcontainers-go/exec" ) @@ -56,7 +57,5 @@ func TestWaitForExit(t *testing.T) { } wg := NewExitStrategy().WithExitTimeout(100 * time.Millisecond) err := wg.WaitUntilReady(context.Background(), target) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } diff --git a/wait/file.go b/wait/file.go index 148907f3db..d9cab7a6e4 100644 --- a/wait/file.go +++ b/wait/file.go @@ -97,7 +97,7 @@ func (ws *FileStrategy) matchFile(ctx context.Context, target StrategyTarget) er if err != nil { return fmt.Errorf("copy from container: %w", err) } - defer rc.Close() //nolint: errcheck // Read close error can't tell us anything useful. + defer rc.Close() if ws.matcher == nil { // No matcher, just check if the file exists. diff --git a/wait/file_test.go b/wait/file_test.go index a25d8aa65f..22133ba349 100644 --- a/wait/file_test.go +++ b/wait/file_test.go @@ -79,7 +79,7 @@ func TestFileStrategyWaitUntilReady_WithMatcher(t *testing.T) { // waitForFileWithMatcher { var out bytes.Buffer dockerReq := testcontainers.ContainerRequest{ - Image: "docker.io/nginx:latest", + Image: "nginx:latest", WaitingFor: wait.ForFile("/etc/nginx/nginx.conf"). WithStartupTimeout(time.Second * 10). WithPollInterval(time.Second). diff --git a/wait/host_port.go b/wait/host_port.go index b349cc0371..7d8b9e76ff 100644 --- a/wait/host_port.go +++ b/wait/host_port.go @@ -13,13 +13,21 @@ import ( "github.com/docker/go-connections/nat" ) +const ( + exitEaccess = 126 // container cmd can't be invoked (permission denied) + exitCmdNotFound = 127 // container cmd not found/does not exist or invalid bind-mount +) + // Implement interface var ( _ Strategy = (*HostPortStrategy)(nil) _ StrategyTimeout = (*HostPortStrategy)(nil) ) -var errShellNotExecutable = errors.New("/bin/sh command not executable") +var ( + errShellNotExecutable = errors.New("/bin/sh command not executable") + errShellNotFound = errors.New("/bin/sh command not found") +) type HostPortStrategy struct { // Port is a string containing port number and protocol in the format "80/tcp" @@ -118,7 +126,7 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT } if internalPort == "" { - return fmt.Errorf("no port to wait for") + return errors.New("no port to wait for") } var port nat.Port @@ -130,31 +138,37 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT select { case <-ctx.Done(): - return fmt.Errorf("%w: %w", ctx.Err(), err) + return fmt.Errorf("mapped port: retries: %d, port: %q, last err: %w, ctx err: %w", i, port, err, ctx.Err()) case <-time.After(waitInterval): if err := checkTarget(ctx, target); err != nil { - return err + return fmt.Errorf("check target: retries: %d, port: %q, last err: %w", i, port, err) } port, err = target.MappedPort(ctx, internalPort) if err != nil { - log.Printf("(%d) [%s] %s\n", i, port, err) + log.Printf("mapped port: retries: %d, port: %q, err: %s\n", i, port, err) } } } if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil { - return err + return fmt.Errorf("external check: %w", err) } if hp.skipInternalCheck { return nil } - err = internalCheck(ctx, internalPort, target) - if err != nil && errors.Is(errShellNotExecutable, err) { - log.Println("Shell not executable in container, only external port check will be performed") - } else { - return err + if err = internalCheck(ctx, internalPort, target); err != nil { + switch { + case errors.Is(err, errShellNotExecutable): + log.Println("Shell not executable in container, only external port validated") + return nil + case errors.Is(err, errShellNotFound): + log.Println("Shell not found in container") + return nil + default: + return fmt.Errorf("internal check: %w", err) + } } return nil @@ -167,9 +181,9 @@ func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target dialer := net.Dialer{} address := net.JoinHostPort(ipAddress, portString) - for { + for i := 0; ; i++ { if err := checkTarget(ctx, target); err != nil { - return err + return fmt.Errorf("check target: retries: %d address: %s: %w", i, address, err) } conn, err := dialer.DialContext(ctx, proto, address) if err != nil { @@ -183,7 +197,7 @@ func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target } } } - return err + return fmt.Errorf("dial: %w", err) } conn.Close() @@ -205,13 +219,18 @@ func internalCheck(ctx context.Context, internalPort nat.Port, target StrategyTa return fmt.Errorf("%w, host port waiting failed", err) } - if exitCode == 0 { - break - } else if exitCode == 126 { + // Docker has a issue which override exit code 127 to 126 due to: + // https://github.com/moby/moby/issues/45795 + // Handle both to ensure compatibility with Docker and Podman for now. + switch exitCode { + case 0: + return nil + case exitEaccess: return errShellNotExecutable + case exitCmdNotFound: + return errShellNotFound } } - return nil } func buildInternalCheckCommand(internalPort int) string { diff --git a/wait/host_port_test.go b/wait/host_port_test.go index c31c3dabc9..4dbaad741f 100644 --- a/wait/host_port_test.go +++ b/wait/host_port_test.go @@ -1,8 +1,10 @@ package wait import ( + "bytes" "context" "io" + "log" "net" "strconv" "testing" @@ -10,22 +12,19 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go/exec" ) func TestWaitForListeningPortSucceeds(t *testing.T) { listener, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer listener.Close() rawPort := listener.Addr().(*net.TCPAddr).Port port, err := nat.NewPort("tcp", strconv.Itoa(rawPort)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var mappedPortCount, execCount int target := &MockStrategyTarget{ @@ -57,23 +56,18 @@ func TestWaitForListeningPortSucceeds(t *testing.T) { WithStartupTimeout(5 * time.Second). WithPollInterval(100 * time.Millisecond) - if err := wg.WaitUntilReady(context.Background(), target); err != nil { - t.Fatal(err) - } + err = wg.WaitUntilReady(context.Background(), target) + require.NoError(t, err) } func TestWaitForExposedPortSucceeds(t *testing.T) { listener, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer listener.Close() rawPort := listener.Addr().(*net.TCPAddr).Port port, err := nat.NewPort("tcp", strconv.Itoa(rawPort)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var mappedPortCount, execCount int target := &MockStrategyTarget{ @@ -121,9 +115,8 @@ func TestWaitForExposedPortSucceeds(t *testing.T) { WithStartupTimeout(5 * time.Second). WithPollInterval(100 * time.Millisecond) - if err := wg.WaitUntilReady(context.Background(), target); err != nil { - t.Fatal(err) - } + err = wg.WaitUntilReady(context.Background(), target) + require.NoError(t, err) } func TestHostPortStrategyFailsWhileGettingPortDueToOOMKilledContainer(t *testing.T) { @@ -152,14 +145,7 @@ func TestHostPortStrategyFailsWhileGettingPortDueToOOMKilledContainer(t *testing { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container crashed with out-of-memory (OOMKilled)" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.ErrorContains(t, err, "container crashed with out-of-memory (OOMKilled)") } } @@ -190,14 +176,7 @@ func TestHostPortStrategyFailsWhileGettingPortDueToExitedContainer(t *testing.T) { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container exited with code 1" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.ErrorContains(t, err, "container exited with code 1") } } @@ -227,14 +206,7 @@ func TestHostPortStrategyFailsWhileGettingPortDueToUnexpectedContainerStatus(t * { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "unexpected container status \"dead\"" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.ErrorContains(t, err, "unexpected container status \"dead\"") } } @@ -259,14 +231,7 @@ func TestHostPortStrategyFailsWhileExternalCheckingDueToOOMKilledContainer(t *te { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container crashed with out-of-memory (OOMKilled)" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.ErrorContains(t, err, "container crashed with out-of-memory (OOMKilled)") } } @@ -292,14 +257,7 @@ func TestHostPortStrategyFailsWhileExternalCheckingDueToExitedContainer(t *testi { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container exited with code 1" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.ErrorContains(t, err, "container exited with code 1") } } @@ -324,29 +282,18 @@ func TestHostPortStrategyFailsWhileExternalCheckingDueToUnexpectedContainerStatu { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "unexpected container status \"dead\"" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.ErrorContains(t, err, "unexpected container status \"dead\"") } } func TestHostPortStrategyFailsWhileInternalCheckingDueToOOMKilledContainer(t *testing.T) { listener, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer listener.Close() rawPort := listener.Addr().(*net.TCPAddr).Port port, err := nat.NewPort("tcp", strconv.Itoa(rawPort)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var stateCount int target := &MockStrategyTarget{ @@ -375,29 +322,18 @@ func TestHostPortStrategyFailsWhileInternalCheckingDueToOOMKilledContainer(t *te { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container crashed with out-of-memory (OOMKilled)" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.ErrorContains(t, err, "container crashed with out-of-memory (OOMKilled)") } } func TestHostPortStrategyFailsWhileInternalCheckingDueToExitedContainer(t *testing.T) { listener, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer listener.Close() rawPort := listener.Addr().(*net.TCPAddr).Port port, err := nat.NewPort("tcp", strconv.Itoa(rawPort)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var stateCount int target := &MockStrategyTarget{ @@ -427,29 +363,18 @@ func TestHostPortStrategyFailsWhileInternalCheckingDueToExitedContainer(t *testi { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container exited with code 1" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.ErrorContains(t, err, "container exited with code 1") } } func TestHostPortStrategyFailsWhileInternalCheckingDueToUnexpectedContainerStatus(t *testing.T) { listener, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer listener.Close() rawPort := listener.Addr().(*net.TCPAddr).Port port, err := nat.NewPort("tcp", strconv.Itoa(rawPort)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var stateCount int target := &MockStrategyTarget{ @@ -478,29 +403,18 @@ func TestHostPortStrategyFailsWhileInternalCheckingDueToUnexpectedContainerStatu { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "unexpected container status \"dead\"" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.ErrorContains(t, err, "unexpected container status \"dead\"") } } func TestHostPortStrategySucceedsGivenShellIsNotInstalled(t *testing.T) { listener, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer listener.Close() rawPort := listener.Addr().(*net.TCPAddr).Port port, err := nat.NewPort("tcp", strconv.Itoa(rawPort)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) target := &MockStrategyTarget{ HostImpl: func(_ context.Context) (string, error) { @@ -532,7 +446,7 @@ func TestHostPortStrategySucceedsGivenShellIsNotInstalled(t *testing.T) { }, ExecImpl: func(_ context.Context, _ []string, _ ...exec.ProcessOption) (int, io.Reader, error) { // This is the error that would be returned if the shell is not installed. - return 126, nil, nil + return exitEaccess, nil, nil }, } @@ -540,7 +454,75 @@ func TestHostPortStrategySucceedsGivenShellIsNotInstalled(t *testing.T) { WithStartupTimeout(5 * time.Second). WithPollInterval(100 * time.Millisecond) - if err := wg.WaitUntilReady(context.Background(), target); err != nil { - t.Fatal(err) + oldWriter := log.Default().Writer() + var buf bytes.Buffer + log.Default().SetOutput(&buf) + t.Cleanup(func() { + log.Default().SetOutput(oldWriter) + }) + + err = wg.WaitUntilReady(context.Background(), target) + require.NoError(t, err) + + require.Contains(t, buf.String(), "Shell not executable in container, only external port validated") +} + +func TestHostPortStrategySucceedsGivenShellIsNotFound(t *testing.T) { + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + defer listener.Close() + + rawPort := listener.Addr().(*net.TCPAddr).Port + port, err := nat.NewPort("tcp", strconv.Itoa(rawPort)) + require.NoError(t, err) + + target := &MockStrategyTarget{ + HostImpl: func(_ context.Context) (string, error) { + return "localhost", nil + }, + InspectImpl: func(_ context.Context) (*types.ContainerJSON, error) { + return &types.ContainerJSON{ + NetworkSettings: &types.NetworkSettings{ + NetworkSettingsBase: types.NetworkSettingsBase{ + Ports: nat.PortMap{ + "80": []nat.PortBinding{ + { + HostIP: "0.0.0.0", + HostPort: port.Port(), + }, + }, + }, + }, + }, + }, nil + }, + MappedPortImpl: func(_ context.Context, _ nat.Port) (nat.Port, error) { + return port, nil + }, + StateImpl: func(_ context.Context) (*types.ContainerState, error) { + return &types.ContainerState{ + Running: true, + }, nil + }, + ExecImpl: func(_ context.Context, _ []string, _ ...exec.ProcessOption) (int, io.Reader, error) { + // This is the error that would be returned if the shell is not found. + return exitCmdNotFound, nil, nil + }, } + + wg := NewHostPortStrategy("80"). + WithStartupTimeout(5 * time.Second). + WithPollInterval(100 * time.Millisecond) + + oldWriter := log.Default().Writer() + var buf bytes.Buffer + log.Default().SetOutput(&buf) + t.Cleanup(func() { + log.Default().SetOutput(oldWriter) + }) + + err = wg.WaitUntilReady(context.Background(), target) + require.NoError(t, err) + + require.Contains(t, buf.String(), "Shell not found in container") } diff --git a/wait/http_test.go b/wait/http_test.go index 54610f4686..32479bddd4 100644 --- a/wait/http_test.go +++ b/wait/http_test.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -36,20 +37,21 @@ func ExampleHTTPStrategy() { ContainerRequest: req, Started: true, }) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - // } - defer func() { - if err := c.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(c); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + // } state, err := c.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -62,12 +64,14 @@ func ExampleHTTPStrategy_WithHeaders() { capath := filepath.Join("testdata", "root.pem") cafile, err := os.ReadFile(capath) if err != nil { - log.Fatalf("can't load ca file: %v", err) + log.Printf("can't load ca file: %v", err) + return } certpool := x509.NewCertPool() if !certpool.AppendCertsFromPEM(cafile) { - log.Fatalf("the ca file isn't valid") + log.Printf("the ca file isn't valid") + return } ctx := context.Background() @@ -94,19 +98,20 @@ func ExampleHTTPStrategy_WithHeaders() { ContainerRequest: req, Started: true, }) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := c.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(c); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } state, err := c.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -128,20 +133,21 @@ func ExampleHTTPStrategy_WithPort() { ContainerRequest: req, Started: true, }) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - // } - defer func() { - if err := c.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(c); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + // } state, err := c.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -162,19 +168,20 @@ func ExampleHTTPStrategy_WithForcedIPv4LocalHost() { ContainerRequest: req, Started: true, }) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - defer func() { - if err := c.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(c); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } state, err := c.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -196,20 +203,21 @@ func ExampleHTTPStrategy_WithBasicAuth() { ContainerRequest: req, Started: true, }) - if err != nil { - log.Fatalf("failed to start container: %s", err) - } - // } - defer func() { - if err := gogs.Terminate(ctx); err != nil { - log.Fatalf("failed to terminate container: %s", err) + if err := testcontainers.TerminateContainer(gogs); err != nil { + log.Printf("failed to terminate container: %s", err) } }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + // } state, err := gogs.State(ctx) if err != nil { - log.Fatalf("failed to get container state: %s", err) // nolint:gocritic + log.Printf("failed to get container state: %s", err) + return } fmt.Println(state.Running) @@ -220,23 +228,14 @@ func ExampleHTTPStrategy_WithBasicAuth() { func TestHTTPStrategyWaitUntilReady(t *testing.T) { workdir, err := os.Getwd() - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) capath := filepath.Join(workdir, "testdata", "root.pem") cafile, err := os.ReadFile(capath) - if err != nil { - t.Errorf("can't load ca file: %v", err) - return - } + require.NoError(t, err) certpool := x509.NewCertPool() - if !certpool.AppendCertsFromPEM(cafile) { - t.Errorf("the ca file isn't valid") - return - } + require.Truef(t, certpool.AppendCertsFromPEM(cafile), "the ca file isn't valid") tlsconfig := &tls.Config{RootCAs: certpool, ServerName: "testcontainer.go.test"} dockerReq := testcontainers.ContainerRequest{ @@ -254,24 +253,17 @@ func TestHTTPStrategyWaitUntilReady(t *testing.T) { WithMethod(http.MethodPost).WithBody(bytes.NewReader([]byte("ping"))), } - container, err := testcontainers.GenericContainer(context.Background(), + ctr, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{ContainerRequest: dockerReq, Started: true}) - if err != nil { - t.Error(err) - return - } - defer container.Terminate(context.Background()) // nolint: errcheck + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + host, err := ctr.Host(context.Background()) + require.NoError(t, err) + + port, err := ctr.MappedPort(context.Background(), "6443/tcp") + require.NoError(t, err) - host, err := container.Host(context.Background()) - if err != nil { - t.Error(err) - return - } - port, err := container.MappedPort(context.Background(), "6443/tcp") - if err != nil { - t.Error(err) - return - } client := http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsconfig, @@ -289,36 +281,22 @@ func TestHTTPStrategyWaitUntilReady(t *testing.T) { }, } resp, err := client.Get(fmt.Sprintf("https://%s:%s", host, port.Port())) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("status code isn't ok: %s", resp.Status) - return - } + require.Equal(t, http.StatusOK, resp.StatusCode) } func TestHTTPStrategyWaitUntilReadyWithQueryString(t *testing.T) { workdir, err := os.Getwd() - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) capath := filepath.Join(workdir, "testdata", "root.pem") cafile, err := os.ReadFile(capath) - if err != nil { - t.Errorf("can't load ca file: %v", err) - return - } + require.NoError(t, err) certpool := x509.NewCertPool() - if !certpool.AppendCertsFromPEM(cafile) { - t.Errorf("the ca file isn't valid") - return - } + require.Truef(t, certpool.AppendCertsFromPEM(cafile), "the ca file isn't valid") tlsconfig := &tls.Config{RootCAs: certpool, ServerName: "testcontainer.go.test"} dockerReq := testcontainers.ContainerRequest{ @@ -335,24 +313,17 @@ func TestHTTPStrategyWaitUntilReadyWithQueryString(t *testing.T) { }), } - container, err := testcontainers.GenericContainer(context.Background(), + ctr, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{ContainerRequest: dockerReq, Started: true}) - if err != nil { - t.Error(err) - return - } - defer container.Terminate(context.Background()) // nolint: errcheck + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + host, err := ctr.Host(context.Background()) + require.NoError(t, err) + + port, err := ctr.MappedPort(context.Background(), "6443/tcp") + require.NoError(t, err) - host, err := container.Host(context.Background()) - if err != nil { - t.Error(err) - return - } - port, err := container.MappedPort(context.Background(), "6443/tcp") - if err != nil { - t.Error(err) - return - } client := http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsconfig, @@ -370,36 +341,22 @@ func TestHTTPStrategyWaitUntilReadyWithQueryString(t *testing.T) { }, } resp, err := client.Get(fmt.Sprintf("https://%s:%s", host, port.Port())) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("status code isn't ok: %s", resp.Status) - return - } + require.Equal(t, http.StatusOK, resp.StatusCode) } func TestHTTPStrategyWaitUntilReadyNoBasicAuth(t *testing.T) { workdir, err := os.Getwd() - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) capath := filepath.Join(workdir, "testdata", "root.pem") cafile, err := os.ReadFile(capath) - if err != nil { - t.Errorf("can't load ca file: %v", err) - return - } + require.NoError(t, err) certpool := x509.NewCertPool() - if !certpool.AppendCertsFromPEM(cafile) { - t.Errorf("the ca file isn't valid") - return - } + require.Truef(t, certpool.AppendCertsFromPEM(cafile), "the ca file isn't valid") // waitForHTTPStatusCode { tlsconfig := &tls.Config{RootCAs: certpool, ServerName: "testcontainer.go.test"} @@ -424,27 +381,16 @@ func TestHTTPStrategyWaitUntilReadyNoBasicAuth(t *testing.T) { // } ctx := context.Background() - container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ContainerRequest: dockerReq, Started: true}) - if err != nil { - t.Error(err) - return - } - t.Cleanup(func() { - if err := container.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ContainerRequest: dockerReq, Started: true}) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + host, err := ctr.Host(ctx) + require.NoError(t, err) + + port, err := ctr.MappedPort(ctx, "6443/tcp") + require.NoError(t, err) - host, err := container.Host(ctx) - if err != nil { - t.Error(err) - return - } - port, err := container.MappedPort(ctx, "6443/tcp") - if err != nil { - t.Error(err) - return - } client := http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsconfig, @@ -462,15 +408,10 @@ func TestHTTPStrategyWaitUntilReadyNoBasicAuth(t *testing.T) { }, } resp, err := client.Get(fmt.Sprintf("https://%s:%s", host, port.Port())) - if err != nil { - t.Error(err) - return - } + require.NoError(t, err) + defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Errorf("status code isn't ok: %s", resp.Status) - return - } + require.Equal(t, http.StatusOK, resp.StatusCode) } func TestHttpStrategyFailsWhileGettingPortDueToOOMKilledContainer(t *testing.T) { @@ -513,17 +454,9 @@ func TestHttpStrategyFailsWhileGettingPortDueToOOMKilledContainer(t *testing.T) WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - { - err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container crashed with out-of-memory (OOMKilled)" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } - } + err := wg.WaitUntilReady(context.Background(), target) + expected := "container crashed with out-of-memory (OOMKilled)" + require.EqualError(t, err, expected) } func TestHttpStrategyFailsWhileGettingPortDueToExitedContainer(t *testing.T) { @@ -567,17 +500,9 @@ func TestHttpStrategyFailsWhileGettingPortDueToExitedContainer(t *testing.T) { WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - { - err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container exited with code 1" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } - } + err := wg.WaitUntilReady(context.Background(), target) + expected := "container exited with code 1" + require.EqualError(t, err, expected) } func TestHttpStrategyFailsWhileGettingPortDueToUnexpectedContainerStatus(t *testing.T) { @@ -620,17 +545,9 @@ func TestHttpStrategyFailsWhileGettingPortDueToUnexpectedContainerStatus(t *test WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - { - err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "unexpected container status \"dead\"" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } - } + err := wg.WaitUntilReady(context.Background(), target) + expected := "unexpected container status \"dead\"" + require.EqualError(t, err, expected) } func TestHTTPStrategyFailsWhileRequestSendingDueToOOMKilledContainer(t *testing.T) { @@ -668,17 +585,9 @@ func TestHTTPStrategyFailsWhileRequestSendingDueToOOMKilledContainer(t *testing. WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - { - err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container crashed with out-of-memory (OOMKilled)" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } - } + err := wg.WaitUntilReady(context.Background(), target) + expected := "container crashed with out-of-memory (OOMKilled)" + require.EqualError(t, err, expected) } func TestHttpStrategyFailsWhileRequestSendingDueToExitedContainer(t *testing.T) { @@ -717,17 +626,9 @@ func TestHttpStrategyFailsWhileRequestSendingDueToExitedContainer(t *testing.T) WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - { - err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container exited with code 1" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } - } + err := wg.WaitUntilReady(context.Background(), target) + expected := "container exited with code 1" + require.EqualError(t, err, expected) } func TestHttpStrategyFailsWhileRequestSendingDueToUnexpectedContainerStatus(t *testing.T) { @@ -765,17 +666,9 @@ func TestHttpStrategyFailsWhileRequestSendingDueToUnexpectedContainerStatus(t *t WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - { - err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "unexpected container status \"dead\"" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } - } + err := wg.WaitUntilReady(context.Background(), target) + expected := "unexpected container status \"dead\"" + require.EqualError(t, err, expected) } func TestHttpStrategyFailsWhileGettingPortDueToNoExposedPorts(t *testing.T) { @@ -812,17 +705,9 @@ func TestHttpStrategyFailsWhileGettingPortDueToNoExposedPorts(t *testing.T) { WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - { - err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "No exposed tcp ports or mapped ports - cannot wait for status" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } - } + err := wg.WaitUntilReady(context.Background(), target) + expected := "No exposed tcp ports or mapped ports - cannot wait for status" + require.EqualError(t, err, expected) } func TestHttpStrategyFailsWhileGettingPortDueToOnlyUDPPorts(t *testing.T) { @@ -866,17 +751,9 @@ func TestHttpStrategyFailsWhileGettingPortDueToOnlyUDPPorts(t *testing.T) { WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - { - err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "No exposed tcp ports or mapped ports - cannot wait for status" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } - } + err := wg.WaitUntilReady(context.Background(), target) + expected := "No exposed tcp ports or mapped ports - cannot wait for status" + require.EqualError(t, err, expected) } func TestHttpStrategyFailsWhileGettingPortDueToExposedPortNoBindings(t *testing.T) { @@ -915,15 +792,7 @@ func TestHttpStrategyFailsWhileGettingPortDueToExposedPortNoBindings(t *testing. WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - { - err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "No exposed tcp ports or mapped ports - cannot wait for status" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } - } + err := wg.WaitUntilReady(context.Background(), target) + expected := "No exposed tcp ports or mapped ports - cannot wait for status" + require.EqualError(t, err, expected) } diff --git a/wait/sql_test.go b/wait/sql_test.go index 053ed965e8..63179ee8ab 100644 --- a/wait/sql_test.go +++ b/wait/sql_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/require" ) func Test_waitForSql_WithQuery(t *testing.T) { @@ -17,9 +18,7 @@ func Test_waitForSql_WithQuery(t *testing.T) { return "fake-url" }) - if got := w.query; got != defaultForSqlQuery { - t.Fatalf("expected %s, got %s", defaultForSqlQuery, got) - } + require.Equal(t, defaultForSqlQuery, w.query) }) t.Run("custom query", func(t *testing.T) { const q = "SELECT 100;" @@ -28,9 +27,7 @@ func Test_waitForSql_WithQuery(t *testing.T) { return "fake-url" }).WithQuery(q) - if got := w.query; got != q { - t.Fatalf("expected %s, got %s", q, got) - } + require.Equal(t, q, w.query) }) } @@ -102,9 +99,8 @@ func TestWaitForSQLSucceeds(t *testing.T) { WithStartupTimeout(500 * time.Millisecond). WithPollInterval(100 * time.Millisecond) - if err := wg.WaitUntilReady(context.Background(), target); err != nil { - t.Fatal(err) - } + err := wg.WaitUntilReady(context.Background(), target) + require.NoError(t, err) } func TestWaitForSQLFailsWhileGettingPortDueToOOMKilledContainer(t *testing.T) { @@ -133,14 +129,7 @@ func TestWaitForSQLFailsWhileGettingPortDueToOOMKilledContainer(t *testing.T) { { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container crashed with out-of-memory (OOMKilled)" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.EqualError(t, err, "container crashed with out-of-memory (OOMKilled)") } } @@ -171,14 +160,7 @@ func TestWaitForSQLFailsWhileGettingPortDueToExitedContainer(t *testing.T) { { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container exited with code 1" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.EqualError(t, err, "container exited with code 1") } } @@ -208,14 +190,7 @@ func TestWaitForSQLFailsWhileGettingPortDueToUnexpectedContainerStatus(t *testin { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "unexpected container status \"dead\"" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.EqualError(t, err, "unexpected container status \"dead\"") } } @@ -240,14 +215,7 @@ func TestWaitForSQLFailsWhileQueryExecutingDueToOOMKilledContainer(t *testing.T) { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container crashed with out-of-memory (OOMKilled)" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.EqualError(t, err, "container crashed with out-of-memory (OOMKilled)") } } @@ -273,14 +241,7 @@ func TestWaitForSQLFailsWhileQueryExecutingDueToExitedContainer(t *testing.T) { { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "container exited with code 1" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.EqualError(t, err, "container exited with code 1") } } @@ -305,13 +266,6 @@ func TestWaitForSQLFailsWhileQueryExecutingDueToUnexpectedContainerStatus(t *tes { err := wg.WaitUntilReady(context.Background(), target) - if err == nil { - t.Fatal("no error") - } - - expected := "unexpected container status \"dead\"" - if err.Error() != expected { - t.Fatalf("expected %q, got %q", expected, err.Error()) - } + require.EqualError(t, err, "unexpected container status \"dead\"") } } diff --git a/wait/testdata/main.go b/wait/testdata/main.go index f6f965fe6b..523278ba0b 100644 --- a/wait/testdata/main.go +++ b/wait/testdata/main.go @@ -83,7 +83,7 @@ func run() error { go func() { log.Println("serving...") if err := server.ListenAndServeTLS("tls.pem", "tls-key.pem"); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + log.Println(err) } }()