Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions .github/workflows/ci-lint-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,35 @@ jobs:
cache-dependency-path: "${{ inputs.project-directory == '' && '.' || inputs.project-directory }}/go.sum"
id: go

- name: Determine golangci-lint version
id: linter-version
working-directory: ./${{ inputs.project-directory == '' && '.' || inputs.project-directory }}
shell: bash
run: |
# golangci-lint must be built with a Go version >= the module's go directive.
MODULE_GO_VERSION=$(grep '^go ' go.mod | awk '{print $2}')
MODULE_MAJOR_MINOR=$(echo "${MODULE_GO_VERSION}" | grep -oE '^[0-9]+\.[0-9]+')

case "${MODULE_MAJOR_MINOR}" in
1.24)
LINTER_VERSION=v2.0.2
;;
1.25)
LINTER_VERSION=v2.9.0
;;
*)
echo "::error::No golangci-lint version mapped for Go ${MODULE_GO_VERSION}. Please update ci-lint-go.yml."
exit 1
;;
esac

echo "version=${LINTER_VERSION}" >> "$GITHUB_OUTPUT"
echo "Module requires Go ${MODULE_GO_VERSION}, using golangci-lint ${LINTER_VERSION}"

- name: golangci-lint
uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v2.0.2
version: ${{ steps.linter-version.outputs.version }}
# Optional: working directory, useful for monorepos
working-directory: ${{ inputs.project-directory }}

Expand Down
37 changes: 31 additions & 6 deletions .github/workflows/ci-test-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,34 @@ jobs:
cache-dependency-path: '${{ inputs.project-directory }}/go.sum'
id: go

- name: Check Go version compatibility
id: go-compat
working-directory: ./${{ inputs.project-directory == '' && '.' || inputs.project-directory }}
shell: bash
run: |
# Read the minimum Go version required by the module
MODULE_GO_VERSION=$(grep '^go ' go.mod | awk '{print $2}')
RUNNER_GO_VERSION=$(go env GOVERSION | sed 's/^go//')
echo "Module requires Go ${MODULE_GO_VERSION}, runner has Go ${RUNNER_GO_VERSION}"

# Extract major.minor for comparison
MODULE_MAJOR_MINOR=$(echo "${MODULE_GO_VERSION}" | grep -oE '^[0-9]+\.[0-9]+')
RUNNER_MAJOR_MINOR=$(echo "${RUNNER_GO_VERSION}" | grep -oE '^[0-9]+\.[0-9]+')

if [ "$(printf '%s\n' "${MODULE_MAJOR_MINOR}" "${RUNNER_MAJOR_MINOR}" | sort -V | head -n1)" = "${MODULE_MAJOR_MINOR}" ]; then
echo "compatible=true" >> $GITHUB_OUTPUT
else
echo "compatible=false" >> $GITHUB_OUTPUT
echo "::warning::Skipping tests: module requires Go ${MODULE_GO_VERSION} but runner has Go ${RUNNER_GO_VERSION}"
fi

- name: ensure compilation
if: steps.go-compat.outputs.compatible == 'true'
working-directory: ./${{ inputs.project-directory }}
run: go build ./...

- name: Install dependencies
if: steps.go-compat.outputs.compatible == 'true'
shell: bash
run: |
SCRIPT_PATH="./.github/scripts/${{ inputs.project-directory }}/install-dependencies.sh"
Expand All @@ -88,23 +111,25 @@ jobs:

# Setup Testcontainers Cloud Client right before your Testcontainers tests
- name: Setup Testcontainers Cloud Client
if: ${{ inputs.testcontainers-cloud }}
if: ${{ steps.go-compat.outputs.compatible == 'true' && inputs.testcontainers-cloud }}
uses: atomicjar/testcontainers-cloud-setup-action@c335bdbb570ec7c48f72c7d450c077f0a002293e # v1.3
with:
token: ${{ secrets.TCC_TOKEN }}

- name: go test
if: steps.go-compat.outputs.compatible == 'true'
working-directory: ./${{ inputs.project-directory }}
timeout-minutes: 30
run: make test-unit

- name: Run checker
if: steps.go-compat.outputs.compatible == 'true'
run: |
./scripts/check_environment.sh

# (Optionally) When you don't need Testcontainers Cloud anymore, you could terminate sessions eagerly
- name: Terminate Testcontainers Cloud Client active sessions
if: ${{ inputs.testcontainers-cloud }}
if: ${{ steps.go-compat.outputs.compatible == 'true' && inputs.testcontainers-cloud }}
uses: atomicjar/testcontainers-cloud-setup-action@c335bdbb570ec7c48f72c7d450c077f0a002293e # v1.3
with:
action: terminate
Expand All @@ -113,10 +138,10 @@ jobs:
uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4
with:
paths: "**/${{ inputs.project-directory }}/TEST-unit*.xml"
if: always()
if: ${{ always() && steps.go-compat.outputs.compatible == 'true' }}

- name: Decide if Sonar must be run
if: ${{ matrix.platform == 'ubuntu-latest' }}
if: ${{ steps.go-compat.outputs.compatible == 'true' && matrix.platform == 'ubuntu-latest' }}
run: |
if [[ "1.24.x" == "${{ inputs.go-version }}" ]] && \
[[ "true" != "${{ inputs.rootless-docker }}" ]] && \
Expand All @@ -128,7 +153,7 @@ jobs:
fi

- name: Set Sonar Cloud environment variables
if: ${{ env.SHOULD_RUN_SONAR == 'true' }}
if: ${{ steps.go-compat.outputs.compatible == 'true' && env.SHOULD_RUN_SONAR == 'true' }}
run: |
echo "PROJECT_VERSION=$(grep 'latest_version' mkdocs.yml | cut -d':' -f2 | tr -d ' ')" >> $GITHUB_ENV
if [ "${{ inputs.project-directory }}" == "" ]; then
Expand All @@ -144,7 +169,7 @@ jobs:
fi

- name: SonarQube Scan
if: ${{ env.SHOULD_RUN_SONAR == 'true' }}
if: ${{ steps.go-compat.outputs.compatible == 'true' && env.SHOULD_RUN_SONAR == 'true' }}
uses: SonarSource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Expand Down
5 changes: 4 additions & 1 deletion docker_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package testcontainers
import (
"context"
"fmt"
"strings"
"sync"

"github.com/docker/docker/api/types"
Expand Down Expand Up @@ -69,9 +70,11 @@ func (c *DockerClient) Info(ctx context.Context) (system.Info, error) {
if len(dockerInfo.Labels) > 0 {
infoLabels = `
Labels:`
var infoLabelsSb72 strings.Builder
for _, lb := range dockerInfo.Labels {
infoLabels += "\n " + lb
infoLabelsSb72.WriteString("\n " + lb)
}
infoLabels += infoLabelsSb72.String()
}

log.Printf(infoMessage, packagePath,
Expand Down
4 changes: 2 additions & 2 deletions modules/azurite/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ require (
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/sys v0.40.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
24 changes: 12 additions & 12 deletions modules/azurite/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
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/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.3.0 h1:NnE8y/opvxowwNcSNHubQUiSSEhfk3dmooLGAOmPuKs=
github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.3.0/go.mod h1:GhHzPHiiHxZloo6WvKu9X7krmSAKTyGoIwoKMbrKTTA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
Expand Down Expand Up @@ -134,20 +134,20 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
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/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
27 changes: 20 additions & 7 deletions modules/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/google/uuid"

"github.com/testcontainers/testcontainers-go"
Expand Down Expand Up @@ -145,21 +145,34 @@ func NewDockerComposeWith(opts ...ComposeStackOption) (*DockerCompose, error) {
return nil, ErrNoStackConfigured
}

// Create Docker CLI for compose service (uses moby/moby client internally)
dockerCli, err := command.NewDockerCli()
if err != nil {
return nil, fmt.Errorf("new docker client: %w", err)
return nil, fmt.Errorf("new docker cli: %w", err)
}

if err = dockerCli.Initialize(flags.NewClientOptions()); err != nil {
return nil, fmt.Errorf("initialize docker cli: %w", err)
}

if err = dockerCli.Initialize(flags.NewClientOptions(), command.WithInitializeClient(makeClient)); err != nil {
return nil, fmt.Errorf("initialize docker client: %w", err)
composeService, err := compose.NewComposeService(dockerCli)
if err != nil {
return nil, fmt.Errorf("new compose service: %w", err)
}

// Create a separate testcontainers Docker client for provider and direct API calls.
// Compose v5 uses moby/moby/client internally, which is not type-compatible with
// docker/docker/client used by testcontainers, so we cannot share the CLI client.
provider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(composeOptions.Logger))
if err != nil {
return nil, fmt.Errorf("new docker provider: %w", err)
}

dockerClient := dockerCli.Client()
dockerClient, err := testcontainers.NewDockerClientWithOpts(context.Background())
if err != nil {
return nil, fmt.Errorf("new docker client: %w", err)
}

provider.SetClient(dockerClient)
Comment thread
mdelapenya marked this conversation as resolved.

composeAPI := &DockerCompose{
Expand All @@ -168,7 +181,7 @@ func NewDockerComposeWith(opts ...ComposeStackOption) (*DockerCompose, error) {
temporaryConfigs: composeOptions.temporaryPaths,
logger: composeOptions.Logger,
projectProfiles: composeOptions.Profiles,
composeService: compose.NewComposeService(dockerCli),
composeService: composeService,
dockerClient: dockerClient,
waitStrategies: make(map[string]wait.Strategy),
containers: make(map[string]*testcontainers.DockerContainer),
Expand Down
40 changes: 9 additions & 31 deletions modules/compose/compose_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import (

"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dockernetwork "github.com/docker/docker/api/types/network"
Expand Down Expand Up @@ -530,29 +529,16 @@ func (d *DockerCompose) lookupNetworks(ctx context.Context) error {
}

func (d *DockerCompose) compileProject(ctx context.Context) (*types.Project, error) {
const nameAndDefaultConfigPath = 2
projectOptions := make([]cli.ProjectOptionsFn, len(d.projectOptions), len(d.projectOptions)+nameAndDefaultConfigPath)

copy(projectOptions, d.projectOptions)
projectOptions = append(projectOptions, cli.WithName(d.name), cli.WithDefaultConfigPath)

compiledOptions, err := cli.NewProjectOptions(d.configs, projectOptions...)
if err != nil {
return nil, fmt.Errorf("new project options: %w", err)
}

proj, err := compiledOptions.LoadProject(ctx)
proj, err := d.composeService.LoadProject(ctx, api.ProjectLoadOptions{
ProjectName: d.name,
ConfigPaths: d.configs,
Profiles: d.projectProfiles,
ProjectOptionsFns: d.projectOptions,
})
if err != nil {
return nil, fmt.Errorf("load project: %w", err)
}

if len(d.projectProfiles) > 0 {
proj, err = proj.WithProfiles(d.projectProfiles)
if err != nil {
return nil, fmt.Errorf("with profiles: %w", err)
}
}

for i, s := range proj.Services {
s.CustomLabels = map[string]string{
api.ProjectLabel: proj.Name,
Expand All @@ -565,9 +551,9 @@ func (d *DockerCompose) compileProject(ctx context.Context) (*types.Project, err

testcontainers.AddGenericLabels(s.CustomLabels)

for i, envFile := range compiledOptions.EnvFiles {
for j, envFile := range s.EnvFiles {
// add a label for each env file, indexed by its position
s.CustomLabels[fmt.Sprintf("%s.%d", api.EnvironmentFileLabel, i)] = envFile
s.CustomLabels[fmt.Sprintf("%s.%d", api.EnvironmentFileLabel, j)] = envFile.Path
}

proj.Services[i] = s
Expand Down Expand Up @@ -600,11 +586,3 @@ func withEnv(env map[string]string) func(*cli.ProjectOptions) error {
return nil
}
}

func makeClient(*command.DockerCli) (client.APIClient, error) {
dockerClient, err := testcontainers.NewDockerClientWithOpts(context.Background())
if err != nil {
return nil, fmt.Errorf("new docker client: %w", err)
}
return dockerClient, nil
}
2 changes: 1 addition & 1 deletion modules/compose/compose_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"testing"
"time"

"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"github.com/google/uuid"
Expand Down
16 changes: 10 additions & 6 deletions modules/compose/compose_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,11 @@ func (dc *LocalDockerCompose) getDockerComposeEnvironment() map[string]string {
environment := map[string]string{}

composeFileEnvVariableValue := ""
var composeFileEnvVariableValueSb121 strings.Builder
for _, abs := range dc.absComposeFilePaths {
composeFileEnvVariableValue += abs + string(os.PathListSeparator)
composeFileEnvVariableValueSb121.WriteString(abs + string(os.PathListSeparator))
}
composeFileEnvVariableValue += composeFileEnvVariableValueSb121.String()

environment[envProjectName] = dc.Identifier
environment[envComposeFile] = composeFileEnvVariableValue
Expand Down Expand Up @@ -238,10 +240,10 @@ func (dc *LocalDockerCompose) determineVersion() error {
return fmt.Errorf("parsing major version: %w", err)
}

switch majorVersion {
case 1:
switch {
case majorVersion == 1:
dc.ComposeVersion = composeVersion1{}
case 2:
case majorVersion >= 2:
dc.ComposeVersion = composeVersion2{}
default:
return fmt.Errorf("unexpected compose version %d", majorVersion)
Expand Down Expand Up @@ -327,7 +329,8 @@ func execute(
stderr := newCapturingPassThroughWriter(os.Stderr)

if err = cmd.Start(); err != nil {
execCmd := []string{"Starting command", dirContext, binary}
execCmd := make([]string, 0, 3+len(args))
execCmd = append(execCmd, "Starting command", dirContext, binary)
execCmd = append(execCmd, args...)

return ExecError{
Expand All @@ -354,7 +357,8 @@ func execute(

err = cmd.Wait()

execCmd := []string{"Reading std", dirContext, binary}
execCmd := make([]string, 0, 3+len(args))
execCmd = append(execCmd, "Reading std", dirContext, binary)
execCmd = append(execCmd, args...)

return ExecError{
Expand Down
Loading
Loading