diff --git a/.github/workflows/ci-reaper-off.yml b/.github/workflows/ci-reaper-off.yml new file mode 100644 index 0000000000..5e1b2428ef --- /dev/null +++ b/.github/workflows/ci-reaper-off.yml @@ -0,0 +1,43 @@ +name: Reaper-Off pipeline + +on: [push, pull_request] + +concurrency: + group: "${{ github.workflow }}-${{ github.head_ref || github.sha }}" + cancel-in-progress: true + +jobs: + test-reaper-off: + strategy: + matrix: + go-version: [1.19.x, 1.x] + runs-on: ubuntu-latest + env: + TESTCONTAINERS_RYUK_DISABLED: "true" + + steps: + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: gotestsum + # only run tests on linux, there are a number of things that won't allow the tests to run on anything else + # many (maybe, all?) images used can only be build on Linux, they don't have Windows in their manifest, and + # we can't put Windows Server in "Linux Mode" in Github actions + # another, host mode is only available on Linux, and we have tests around that, do we skip them? + run: make test-unit + + - name: Run checker + run: | + ./scripts/check_environment.sh + + - name: Test Summary + uses: test-summary/action@4ee9ece4bca777a38f05c8fc578ac2007fe266f7 + with: + paths: "**/TEST-*.xml" + if: always() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bb1a304c8..0f3810766f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ jobs: go-version: [1.19.x, 1.x] platform: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.platform }} + env: + TESTCONTAINERS_RYUK_DISABLED: "false" steps: - name: Set up Go diff --git a/config.go b/config.go new file mode 100644 index 0000000000..664f8c5ad5 --- /dev/null +++ b/config.go @@ -0,0 +1,101 @@ +package testcontainers + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "sync" + + "github.com/magiconair/properties" +) + +var tcConfig TestcontainersConfig +var tcConfigOnce *sync.Once = new(sync.Once) + +// TestcontainersConfig represents the configuration for Testcontainers +// testcontainersConfig { +type TestcontainersConfig struct { + Host string `properties:"docker.host,default="` + TLSVerify int `properties:"docker.tls.verify,default=0"` + CertPath string `properties:"docker.cert.path,default="` + RyukDisabled bool `properties:"ryuk.disabled,default=false"` + RyukPrivileged bool `properties:"ryuk.container.privileged,default=false"` +} + +// } + +// ReadConfig reads from testcontainers properties file, storing the result in a singleton instance +// of the TestcontainersConfig struct +func ReadConfig() TestcontainersConfig { + tcConfigOnce.Do(func() { + tcConfig = readConfig() + + if tcConfig.RyukDisabled { + ryukDisabledMessage := ` +********************************************************************************************** +Ryuk has been disabled for the current execution. This can cause unexpected behavior in your environment. +More on this: https://golang.testcontainers.org/features/garbage_collector/ +**********************************************************************************************` + Logger.Printf(ryukDisabledMessage) + Logger.Printf("\n%+v", tcConfig) + } + }) + + return tcConfig +} + +// readConfig reads from testcontainers properties file, if it exists +// it is possible that certain values get overridden when set as environment variables +func readConfig() TestcontainersConfig { + config := TestcontainersConfig{} + + applyEnvironmentConfiguration := func(config TestcontainersConfig) TestcontainersConfig { + if dockerHostEnv := os.Getenv("DOCKER_HOST"); dockerHostEnv != "" { + config.Host = dockerHostEnv + } + if config.Host == "" { + config.Host = "unix:///var/run/docker.sock" + } + + ryukDisabledEnv := os.Getenv("TESTCONTAINERS_RYUK_DISABLED") + if parseBool(ryukDisabledEnv) { + config.RyukDisabled = ryukDisabledEnv == "true" + } + + ryukPrivilegedEnv := os.Getenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") + if parseBool(ryukPrivilegedEnv) { + config.RyukPrivileged = ryukPrivilegedEnv == "true" + } + + return config + } + + home, err := os.UserHomeDir() + if err != nil { + return applyEnvironmentConfiguration(config) + } + + tcProp := filepath.Join(home, ".testcontainers.properties") + // init from a file + properties, err := properties.LoadFile(tcProp, properties.UTF8) + if err != nil { + return applyEnvironmentConfiguration(config) + } + + if err := properties.Decode(&config); err != nil { + fmt.Printf("invalid testcontainers properties file, returning an empty Testcontainers configuration: %v\n", err) + return applyEnvironmentConfiguration(config) + } + + fmt.Printf("Testcontainers properties file has been found: %s\n", tcProp) + + return applyEnvironmentConfiguration(config) +} + +func parseBool(input string) bool { + if _, err := strconv.ParseBool(input); err == nil { + return true + } + return false +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000000..300dd66bca --- /dev/null +++ b/config_test.go @@ -0,0 +1,427 @@ +package testcontainers + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + dockerSock = "unix:///var/run/docker.sock" + tcpDockerHost1234 = "tcp://127.0.0.1:1234" + tcpDockerHost33293 = "tcp://127.0.0.1:33293" + tcpDockerHost4711 = "tcp://127.0.0.1:4711" +) + +// unset environment variables to avoid side effects +// execute this function before each test +func resetTestEnv(t *testing.T) { + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "") + t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "") +} + +func TestReadConfig(t *testing.T) { + resetTestEnv(t) + + t.Run("Config is read just once", func(t *testing.T) { + t.Setenv("HOME", "") + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + + config := ReadConfig() + + expected := TestcontainersConfig{ + RyukDisabled: true, + Host: dockerSock, + } + + assert.Equal(t, expected, config) + + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "false") + config = ReadConfig() + assert.Equal(t, expected, config) + }) +} + +func TestReadTCConfig(t *testing.T) { + resetTestEnv(t) + + t.Run("HOME is not set", func(t *testing.T) { + t.Setenv("HOME", "") + + config := readConfig() + + expected := TestcontainersConfig{} + expected.Host = dockerSock + + assert.Equal(t, expected, config) + }) + + t.Run("HOME is not set - TESTCONTAINERS_ env is set", func(t *testing.T) { + t.Setenv("HOME", "") + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") + + config := readConfig() + + expected := TestcontainersConfig{} + expected.RyukDisabled = true + expected.RyukPrivileged = true + expected.Host = dockerSock + + assert.Equal(t, expected, config) + }) + + t.Run("HOME does not contain TC props file", func(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("HOME", tmpDir) + + config := readConfig() + + expected := TestcontainersConfig{} + expected.Host = dockerSock + + assert.Equal(t, expected, config) + }) + + t.Run("HOME does not contain TC props file - DOCKER_HOST env is set", func(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("HOME", tmpDir) + t.Setenv("DOCKER_HOST", tcpDockerHost33293) + + config := readConfig() + expected := TestcontainersConfig{} + expected.Host = tcpDockerHost33293 + + assert.Equal(t, expected, config) + }) + + t.Run("HOME does not contain TC props file - TESTCONTAINERS_ env is set", func(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("HOME", tmpDir) + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") + + config := readConfig() + expected := TestcontainersConfig{} + expected.RyukDisabled = true + expected.RyukPrivileged = true + expected.Host = dockerSock + + assert.Equal(t, expected, config) + }) + + t.Run("HOME contains TC properties file", func(t *testing.T) { + tests := []struct { + name string + content string + env map[string]string + expected TestcontainersConfig + }{ + { + "Single Docker host with spaces", + "docker.host = " + tcpDockerHost33293, + map[string]string{}, + TestcontainersConfig{ + Host: tcpDockerHost33293, + TLSVerify: 0, + CertPath: "", + }, + }, + { + "Multiple docker host entries, last one wins", + `docker.host = ` + tcpDockerHost33293 + ` + docker.host = ` + tcpDockerHost4711 + ` + `, + map[string]string{}, + TestcontainersConfig{ + Host: tcpDockerHost4711, + TLSVerify: 0, + CertPath: "", + }, + }, + { + "Multiple docker host entries, last one wins, with TLS", + `docker.host = ` + tcpDockerHost33293 + ` + docker.host = ` + tcpDockerHost4711 + ` + docker.host = ` + tcpDockerHost1234 + ` + docker.tls.verify = 1 + `, + map[string]string{}, + TestcontainersConfig{ + Host: tcpDockerHost1234, + TLSVerify: 1, + CertPath: "", + }, + }, + { + "Empty file", + "", + map[string]string{}, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + }, + }, + { + "Non-valid properties are ignored", + `foo = bar + docker.host = ` + tcpDockerHost1234 + ` + `, + map[string]string{}, + TestcontainersConfig{ + Host: tcpDockerHost1234, + TLSVerify: 0, + CertPath: "", + }, + }, + { + "Single Docker host without spaces", + "docker.host=" + tcpDockerHost33293, + map[string]string{}, + TestcontainersConfig{ + Host: tcpDockerHost33293, + TLSVerify: 0, + CertPath: "", + }, + }, + { + "Comments are ignored", + `#docker.host=` + tcpDockerHost33293, + map[string]string{}, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + }, + }, + { + "Multiple docker host entries, last one wins, with TLS and cert path", + `#docker.host = ` + tcpDockerHost33293 + ` + docker.host = ` + tcpDockerHost4711 + ` + docker.host = ` + tcpDockerHost1234 + ` + docker.cert.path=/tmp/certs`, + map[string]string{}, + TestcontainersConfig{ + Host: tcpDockerHost1234, + TLSVerify: 0, + CertPath: "/tmp/certs", + }, + }, + { + "With Ryuk disabled using properties", + `ryuk.disabled=true`, + map[string]string{}, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukDisabled: true, + }, + }, + { + "With Ryuk container privileged using properties", + `ryuk.container.privileged=true`, + map[string]string{}, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukPrivileged: true, + }, + }, + { + "With Ryuk disabled using an env var", + ``, + map[string]string{ + "TESTCONTAINERS_RYUK_DISABLED": "true", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukDisabled: true, + }, + }, + { + "With Ryuk container privileged using an env var", + ``, + map[string]string{ + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukPrivileged: true, + }, + }, + { + "With Ryuk disabled using an env var and properties. Env var wins (0)", + `ryuk.disabled=true`, + map[string]string{ + "TESTCONTAINERS_RYUK_DISABLED": "true", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukDisabled: true, + }, + }, + { + "With Ryuk disabled using an env var and properties. Env var wins (1)", + `ryuk.disabled=false`, + map[string]string{ + "TESTCONTAINERS_RYUK_DISABLED": "true", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukDisabled: true, + }, + }, + { + "With Ryuk disabled using an env var and properties. Env var wins (2)", + `ryuk.disabled=true`, + map[string]string{ + "TESTCONTAINERS_RYUK_DISABLED": "false", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukDisabled: false, + }, + }, + { + "With Ryuk disabled using an env var and properties. Env var wins (3)", + `ryuk.disabled=false`, + map[string]string{ + "TESTCONTAINERS_RYUK_DISABLED": "false", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukDisabled: false, + }, + }, + { + "With Ryuk container privileged using an env var and properties. Env var wins (0)", + `ryuk.container.privileged=true`, + map[string]string{ + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukPrivileged: true, + }, + }, + { + "With Ryuk container privileged using an env var and properties. Env var wins (1)", + `ryuk.container.privileged=false`, + map[string]string{ + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukPrivileged: true, + }, + }, + { + "With Ryuk container privileged using an env var and properties. Env var wins (2)", + `ryuk.container.privileged=true`, + map[string]string{ + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "false", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukPrivileged: false, + }, + }, + { + "With Ryuk container privileged using an env var and properties. Env var wins (3)", + `ryuk.container.privileged=false`, + map[string]string{ + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "false", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukPrivileged: false, + }, + }, + { + "With TLS verify using properties when value is wrong", + `ryuk.container.privileged=false + docker.tls.verify = ERROR`, + map[string]string{ + "TESTCONTAINERS_RYUK_DISABLED": "true", + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukDisabled: true, + RyukPrivileged: true, + }, + }, + { + "With Ryuk disabled using an env var and properties. Env var does not win because it's not a boolean value", + `ryuk.disabled=false`, + map[string]string{ + "TESTCONTAINERS_RYUK_DISABLED": "foo", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukDisabled: false, + }, + }, + { + "With Ryuk container privileged using an env var and properties. Env var does not win because it's not a boolean value", + `ryuk.container.privileged=false`, + map[string]string{ + "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "foo", + }, + TestcontainersConfig{ + Host: dockerSock, + TLSVerify: 0, + CertPath: "", + RyukPrivileged: false, + }, + }, + } + for _, tt := range tests { + t.Run(fmt.Sprintf(tt.name), func(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("HOME", tmpDir) + 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 + } + + config := readConfig() + + assert.Equal(t, tt.expected, config, "Configuration doesn't not match") + }) + } + }) +} diff --git a/container.go b/container.go index 35afaa00e4..92ea80999e 100644 --- a/container.go +++ b/container.go @@ -108,7 +108,7 @@ type ContainerRequest struct { Resources container.Resources // Deprecated: Use HostConfigModifier instead Files []ContainerFile // files which will be copied when container starts User string // for specifying uid:gid - SkipReaper bool // indicates whether we skip setting up a reaper for this + SkipReaper bool // Deprecated: The reaper is globally controlled by the .testcontainers.properties file or the TESTCONTAINERS_RYUK_DISABLED environment variable ReaperImage string // Deprecated: use WithImageName ContainerOption instead. Alternative reaper image ReaperOptions []ContainerOption // options for the reaper AutoRemove bool // Deprecated: Use HostConfigModifier instead. If set to true, the container will be removed from the host when stopped diff --git a/docker.go b/docker.go index 73ebf83d5f..48edb8782f 100644 --- a/docker.go +++ b/docker.go @@ -30,7 +30,6 @@ import ( "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/go-connections/nat" "github.com/google/uuid" - "github.com/magiconair/properties" "github.com/moby/term" specs "github.com/opencontainers/image-spec/specs-go/v1" @@ -68,7 +67,6 @@ type DockerContainer struct { provider *DockerProvider sessionID uuid.UUID terminationSignal chan bool - skipReaper bool consumers []LogConsumer raw *types.ContainerJSON stopProducer chan bool @@ -722,7 +720,7 @@ type DockerProvider struct { client client.APIClient host string hostCache string - config TestContainersConfig + config TestcontainersConfig } // Client gets the docker client used by the provider @@ -746,18 +744,10 @@ func (p *DockerProvider) SetClient(c client.APIClient) { var _ ContainerProvider = (*DockerProvider)(nil) -// or through Decode -type TestContainersConfig struct { - Host string `properties:"docker.host,default="` - TLSVerify int `properties:"docker.tls.verify,default=0"` - CertPath string `properties:"docker.cert.path,default="` - RyukPrivileged bool `properties:"ryuk.container.privileged,default=false"` -} - -func NewDockerClient() (cli *client.Client, host string, tcConfig TestContainersConfig, err error) { - tcConfig = configureTC() +func NewDockerClient() (cli *client.Client, err error) { + tcConfig = ReadConfig() - host = tcConfig.Host + host := tcConfig.Host opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()} if host != "" { @@ -771,10 +761,6 @@ func NewDockerClient() (cli *client.Client, host string, tcConfig TestContainers opts = append(opts, client.WithTLSClientConfig(cacertPath, certPath, keyPath)) } - } else if dockerHostEnv := os.Getenv("DOCKER_HOST"); dockerHostEnv != "" { - host = dockerHostEnv - } else { - host = "unix:///var/run/docker.sock" } opts = append(opts, client.WithHTTPHeaders( @@ -785,7 +771,7 @@ func NewDockerClient() (cli *client.Client, host string, tcConfig TestContainers cli, err = client.NewClientWithOpts(opts...) if err != nil { - return nil, "", TestContainersConfig{}, err + return nil, err } _, err = cli.Ping(context.TODO()) @@ -793,46 +779,12 @@ func NewDockerClient() (cli *client.Client, host string, tcConfig TestContainers // fallback to environment cli, err = testcontainersdocker.NewClient(context.Background()) if err != nil { - return nil, "", TestContainersConfig{}, err + return nil, err } } defer cli.Close() - return cli, host, tcConfig, nil -} - -// configureTC reads from testcontainers properties file, if it exists -// it is possible that certain values get overridden when set as environment variables -func configureTC() TestContainersConfig { - config := TestContainersConfig{} - - applyEnvironmentConfiguration := func(config TestContainersConfig) TestContainersConfig { - ryukPrivilegedEnv := os.Getenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") - if ryukPrivilegedEnv != "" { - config.RyukPrivileged = ryukPrivilegedEnv == "true" - } - - return config - } - - home, err := os.UserHomeDir() - if err != nil { - return applyEnvironmentConfiguration(config) - } - - tcProp := filepath.Join(home, ".testcontainers.properties") - // init from a file - properties, err := properties.LoadFile(tcProp, properties.UTF8) - if err != nil { - return applyEnvironmentConfiguration(config) - } - - if err := properties.Decode(&config); err != nil { - fmt.Printf("invalid testcontainers properties file, returning an empty Testcontainers configuration: %v\n", err) - return applyEnvironmentConfiguration(config) - } - - return applyEnvironmentConfiguration(config) + return cli, nil } // BuildImage will build and image from context and Dockerfile, then return the tag @@ -945,10 +897,12 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque opt(&reaperOpts) } + tcConfig := p.Config() + var termSignal chan bool // the reaper does not need to start a reaper for itself isReaperContainer := strings.EqualFold(req.Image, reaperImage(reaperOpts.ImageName)) - if !req.SkipReaper && !isReaperContainer { + if !tcConfig.RyukDisabled && !isReaperContainer { r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.String(), p, req.ReaperOptions...) if err != nil { return nil, fmt.Errorf("%w: creating reaper failed", err) @@ -962,8 +916,6 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque req.Labels[k] = v } } - } else if !isReaperContainer { - p.printReaperBanner("container") } if err = req.Validate(); err != nil { @@ -1085,7 +1037,6 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque sessionID: testcontainerssession.ID(), provider: p, terminationSignal: termSignal, - skipReaper: req.SkipReaper, stopProducer: make(chan bool), logger: p.Logger, } @@ -1128,8 +1079,10 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain return p.CreateContainer(ctx, req) } + tcConfig := p.Config() + var termSignal chan bool - if !req.SkipReaper { + if !tcConfig.RyukDisabled { r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.String(), p, req.ReaperOptions...) if err != nil { return nil, fmt.Errorf("%w: creating reaper failed", err) @@ -1138,9 +1091,8 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain if err != nil { return nil, fmt.Errorf("%w: connecting to reaper failed", err) } - } else { - p.printReaperBanner("container") } + dc := &DockerContainer{ ID: c.ID, WaitingFor: req.WaitingFor, @@ -1148,7 +1100,6 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain sessionID: testcontainerssession.ID(), provider: p, terminationSignal: termSignal, - skipReaper: req.SkipReaper, stopProducer: make(chan bool), logger: p.Logger, isRunning: c.State == "running", @@ -1210,9 +1161,9 @@ func (p *DockerProvider) RunContainer(ctx context.Context, req ContainerRequest) return c, nil } -// Config provides the TestContainersConfig read from $HOME/.testcontainers.properties or +// Config provides the TestcontainersConfig read from $HOME/.testcontainers.properties or // the environment variables -func (p *DockerProvider) Config() TestContainersConfig { +func (p *DockerProvider) Config() TestcontainersConfig { return p.config } @@ -1283,6 +1234,8 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) req.Labels = make(map[string]string) } + tcConfig := p.Config() + nc := types.NetworkCreate{ Driver: req.Driver, CheckDuplicate: req.CheckDuplicate, @@ -1294,7 +1247,7 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) } var termSignal chan bool - if !req.SkipReaper { + if !tcConfig.RyukDisabled { r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.String(), p, req.ReaperOptions...) if err != nil { return nil, fmt.Errorf("%w: creating network reaper failed", err) @@ -1308,8 +1261,6 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) req.Labels[k] = v } } - } else { - p.printReaperBanner("network") } response, err := p.client.NetworkCreate(ctx, req.Name, nc) @@ -1368,15 +1319,6 @@ func (p *DockerProvider) GetGatewayIP(ctx context.Context) (string, error) { return ip, nil } -func (p *DockerProvider) printReaperBanner(resource string) { - ryukDisabledMessage := ` - ********************************************************************************************** - Ryuk has been disabled for the ` + resource + `. This can cause unexpected behavior in your environment. - More on this: https://golang.testcontainers.org/features/garbage_collector/ - **********************************************************************************************` - p.Logger.Printf(ryukDisabledMessage) -} - func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APIClient) (string, error) { // Get list of available networks networkResources, err := cli.NetworkList(ctx, types.NetworkListOptions{}) diff --git a/docker_test.go b/docker_test.go index 3e23441e35..7f5bdf7f7e 100644 --- a/docker_test.go +++ b/docker_test.go @@ -34,12 +34,13 @@ import ( ) const ( - mysqlImage = "docker.io/mysql:8.0.30" - nginxImage = "docker.io/nginx" - nginxAlpineImage = "docker.io/nginx:alpine" - nginxDefaultPort = "80/tcp" - nginxHighPort = "8080/tcp" - daemonMaxVersion = "1.41" + mysqlImage = "docker.io/mysql:8.0.30" + nginxDelayedImage = "docker.io/menedev/delayed-nginx:1.15.2" + nginxImage = "docker.io/nginx" + nginxAlpineImage = "docker.io/nginx:alpine" + nginxDefaultPort = "80/tcp" + nginxHighPort = "8080/tcp" + daemonMaxVersion = "1.41" ) var providerType = ProviderDocker @@ -140,9 +141,8 @@ func TestContainerWithHostNetworkOptions(t *testing.T) { gcr := GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ - Image: nginxAlpineImage, - SkipReaper: true, - Mounts: Mounts(BindMount(absPath, "/etc/nginx/conf.d/default.conf")), + Image: nginxAlpineImage, + Mounts: Mounts(BindMount(absPath, "/etc/nginx/conf.d/default.conf")), ExposedPorts: []string{ nginxHighPort, }, @@ -181,7 +181,6 @@ func TestContainerWithHostNetworkOptions_UseExposePortsFromImageConfigs(t *testi ContainerRequest: ContainerRequest{ Image: "nginx", Privileged: true, - SkipReaper: true, WaitingFor: wait.ForExposedPort(), }, Started: true, @@ -210,9 +209,8 @@ func TestContainerWithNetworkModeAndNetworkTogether(t *testing.T) { gcr := GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ - Image: nginxImage, - SkipReaper: true, - Networks: []string{"new-network"}, + Image: nginxImage, + Networks: []string{"new-network"}, HostConfigModifier: func(hc *container.HostConfig) { hc.NetworkMode = "host" }, @@ -240,7 +238,6 @@ func TestContainerWithHostNetworkOptionsAndWaitStrategy(t *testing.T) { ProviderType: providerType, ContainerRequest: ContainerRequest{ Image: nginxAlpineImage, - SkipReaper: true, WaitingFor: wait.ForListeningPort(nginxHighPort), Mounts: Mounts(BindMount(absPath, "/etc/nginx/conf.d/default.conf")), HostConfigModifier: func(hc *container.HostConfig) { @@ -278,7 +275,6 @@ func TestContainerWithHostNetworkAndEndpoint(t *testing.T) { ProviderType: providerType, ContainerRequest: ContainerRequest{ Image: nginxAlpineImage, - SkipReaper: true, WaitingFor: wait.ForListeningPort(nginxHighPort), Mounts: Mounts(BindMount(absPath, "/etc/nginx/conf.d/default.conf")), HostConfigModifier: func(hc *container.HostConfig) { @@ -305,45 +301,6 @@ func TestContainerWithHostNetworkAndEndpoint(t *testing.T) { } } -func TestContainerWithHostNetworkAndPortEndpoint(t *testing.T) { - ctx := context.Background() - - absPath, err := filepath.Abs("./testresources/nginx-highport.conf") - if err != nil { - t.Fatal(err) - } - - gcr := GenericContainerRequest{ - ProviderType: providerType, - ContainerRequest: ContainerRequest{ - Image: nginxAlpineImage, - SkipReaper: true, - WaitingFor: wait.ForListeningPort(nginxHighPort), - Mounts: Mounts(BindMount(absPath, "/etc/nginx/conf.d/default.conf")), - HostConfigModifier: func(hc *container.HostConfig) { - hc.NetworkMode = "host" - }, - }, - Started: true, - } - - nginxC, err := GenericContainer(ctx, gcr) - - require.NoError(t, err) - terminateContainerOnEnd(t, ctx, nginxC) - - origin, err := nginxC.PortEndpoint(ctx, nginxHighPort, "http") - if err != nil { - t.Errorf("Expected host %s. Got '%d'.", origin, err) - } - t.Log(origin) - - _, err = http.Get(origin) - if err != nil { - t.Errorf("Expected OK response. Got '%d'.", err) - } -} - func TestContainerReturnItsContainerID(t *testing.T) { ctx := context.Background() nginxA, err := GenericContainer(ctx, GenericContainerRequest{ @@ -365,6 +322,11 @@ func TestContainerReturnItsContainerID(t *testing.T) { } func TestContainerStartsWithoutTheReaper(t *testing.T) { + tcConfig := readConfig() // read the config using the private method to avoid the sync.Once + if !tcConfig.RyukDisabled { + t.Skip("Ryuk is enabled, skipping test") + } + ctx := context.Background() client, err := testcontainersdocker.NewClient(ctx) if err != nil { @@ -380,7 +342,6 @@ func TestContainerStartsWithoutTheReaper(t *testing.T) { ExposedPorts: []string{ nginxDefaultPort, }, - SkipReaper: true, }, Started: true, }) @@ -400,6 +361,11 @@ func TestContainerStartsWithoutTheReaper(t *testing.T) { } func TestContainerStartsWithTheReaper(t *testing.T) { + tcConfig := readConfig() // read the config using the private method to avoid the sync.Once + if tcConfig.RyukDisabled { + t.Skip("Ryuk is disabled, skipping test") + } + ctx := context.Background() client, err := testcontainersdocker.NewClient(ctx) if err != nil { @@ -407,7 +373,7 @@ func TestContainerStartsWithTheReaper(t *testing.T) { } defer client.Close() - _, err = GenericContainer(ctx, GenericContainerRequest{ + c, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ Image: nginxAlpineImage, @@ -420,6 +386,8 @@ func TestContainerStartsWithTheReaper(t *testing.T) { if err != nil { t.Fatal(err) } + terminateContainerOnEnd(t, ctx, c) + filtersJSON := fmt.Sprintf(`{"label":{"%s":true}}`, testcontainersdocker.LabelReaper) f, err := filters.FromJSON(filtersJSON) if err != nil { @@ -446,7 +414,6 @@ func TestContainerTerminationResetsState(t *testing.T) { ExposedPorts: []string{ nginxDefaultPort, }, - SkipReaper: true, }, Started: true, }) @@ -526,6 +493,11 @@ func TestContainerStateAfterTermination(t *testing.T) { } func TestContainerStopWithReaper(t *testing.T) { + tcConfig := readConfig() // read the config using the private method to avoid the sync.Once + if tcConfig.RyukDisabled { + t.Skip("Ryuk is disabled, skipping test") + } + ctx := context.Background() nginxA, err := GenericContainer(ctx, GenericContainerRequest{ @@ -568,6 +540,11 @@ func TestContainerStopWithReaper(t *testing.T) { } func TestContainerTerminationWithReaper(t *testing.T) { + tcConfig := readConfig() // read the config using the private method to avoid the sync.Once + if tcConfig.RyukDisabled { + t.Skip("Ryuk is disabled, skipping test") + } + ctx := context.Background() nginxA, err := GenericContainer(ctx, GenericContainerRequest{ @@ -602,6 +579,11 @@ func TestContainerTerminationWithReaper(t *testing.T) { } func TestContainerTerminationWithoutReaper(t *testing.T) { + tcConfig := readConfig() // read the config using the private method to avoid the sync.Once + if !tcConfig.RyukDisabled { + t.Skip("Ryuk is enabled, skipping test") + } + ctx := context.Background() nginxA, err := GenericContainer(ctx, GenericContainerRequest{ @@ -611,7 +593,6 @@ func TestContainerTerminationWithoutReaper(t *testing.T) { ExposedPorts: []string{ nginxDefaultPort, }, - SkipReaper: true, }, Started: true, }) @@ -653,7 +634,6 @@ func TestContainerTerminationRemovesDockerImage(t *testing.T) { ExposedPorts: []string{ nginxDefaultPort, }, - SkipReaper: true, }, Started: true, }) @@ -932,7 +912,7 @@ func TestContainerCreationAndWaitForListeningPortLongEnough(t *testing.T) { nginxC, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ - Image: "docker.io/menedev/delayed-nginx:1.15.2", + Image: nginxDelayedImage, ExposedPorts: []string{ nginxDefaultPort, }, @@ -963,7 +943,7 @@ func TestContainerCreationTimesOut(t *testing.T) { nginxC, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ - Image: "docker.io/menedev/delayed-nginx:1.15.2", + Image: nginxDelayedImage, ExposedPorts: []string{ nginxDefaultPort, }, @@ -971,12 +951,11 @@ func TestContainerCreationTimesOut(t *testing.T) { }, Started: true, }) + + terminateContainerOnEnd(t, ctx, nginxC) + if err == nil { t.Error("Expected timeout") - err := nginxC.Terminate(ctx) - if err != nil { - t.Fatal(err) - } } } @@ -1018,7 +997,7 @@ func TestContainerCreationTimesOutWithHttp(t *testing.T) { nginxC, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ - Image: "docker.io/menedev/delayed-nginx:1.15.2", + Image: nginxDelayedImage, ExposedPorts: []string{ nginxDefaultPort, }, @@ -1044,15 +1023,16 @@ func TestContainerCreationWaitsForLogContextTimeout(t *testing.T) { }, WaitingFor: wait.ForLog("test context timeout").WithStartupTimeout(1 * time.Second), } - _, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, }) - if err == nil { t.Error("Expected timeout") } + + terminateContainerOnEnd(t, ctx, c) } func TestContainerCreationWaitsForLog(t *testing.T) { @@ -1179,15 +1159,16 @@ func TestContainerCreationWaitsForLogAndPortContextTimeout(t *testing.T) { wait.ForListeningPort("3306/tcp"), ), } - _, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, }) - if err == nil { t.Fatal("Expected timeout") } + + terminateContainerOnEnd(t, ctx, c) } func TestContainerCreationWaitingForHostPort(t *testing.T) { @@ -1280,263 +1261,6 @@ func TestEntrypoint(t *testing.T) { terminateContainerOnEnd(t, ctx, c) } -func TestReadTCPropsFile(t *testing.T) { - t.Run("HOME is not set", func(t *testing.T) { - t.Setenv("HOME", "") - - config := configureTC() - - assert.Empty(t, config, "TC props file should not exist") - }) - - t.Run("HOME is not set - TESTCONTAINERS_ env is set", func(t *testing.T) { - t.Setenv("HOME", "") - t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") - - config := configureTC() - - expected := TestContainersConfig{} - expected.RyukPrivileged = true - - assert.Equal(t, expected, config) - }) - - t.Run("HOME does not contain TC props file", func(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("HOME", tmpDir) - - config := configureTC() - - assert.Empty(t, config, "TC props file should not exist") - }) - - t.Run("HOME does not contain TC props file - TESTCONTAINERS_ env is set", func(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("HOME", tmpDir) - t.Setenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") - - config := configureTC() - expected := TestContainersConfig{} - expected.RyukPrivileged = true - - assert.Equal(t, expected, config) - }) - - t.Run("HOME contains TC properties file", func(t *testing.T) { - tests := []struct { - content string - env map[string]string - expected TestContainersConfig - }{ - { - "docker.host = tcp://127.0.0.1:33293", - map[string]string{}, - TestContainersConfig{ - Host: "tcp://127.0.0.1:33293", - TLSVerify: 0, - CertPath: "", - }, - }, - { - "docker.host = tcp://127.0.0.1:33293", - map[string]string{}, - TestContainersConfig{ - Host: "tcp://127.0.0.1:33293", - TLSVerify: 0, - CertPath: "", - }, - }, - { - `docker.host = tcp://127.0.0.1:33293 - docker.host = tcp://127.0.0.1:4711 - `, - map[string]string{}, - TestContainersConfig{ - Host: "tcp://127.0.0.1:4711", - TLSVerify: 0, - CertPath: "", - }, - }, - { - `docker.host = tcp://127.0.0.1:33293 - docker.host = tcp://127.0.0.1:4711 - docker.host = tcp://127.0.0.1:1234 - docker.tls.verify = 1 - `, - map[string]string{}, - TestContainersConfig{ - Host: "tcp://127.0.0.1:1234", - TLSVerify: 1, - CertPath: "", - }, - }, - { - "", - map[string]string{}, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - }, - }, - { - `foo = bar - docker.host = tcp://127.0.0.1:1234 - `, - map[string]string{}, - TestContainersConfig{ - Host: "tcp://127.0.0.1:1234", - TLSVerify: 0, - CertPath: "", - }, - }, - { - "docker.host=tcp://127.0.0.1:33293", - map[string]string{}, - TestContainersConfig{ - Host: "tcp://127.0.0.1:33293", - TLSVerify: 0, - CertPath: "", - }, - }, - { - `#docker.host=tcp://127.0.0.1:33293`, - map[string]string{}, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - }, - }, - { - `#docker.host = tcp://127.0.0.1:33293 - docker.host = tcp://127.0.0.1:4711 - docker.host = tcp://127.0.0.1:1234 - docker.cert.path=/tmp/certs`, - map[string]string{}, - TestContainersConfig{ - Host: "tcp://127.0.0.1:1234", - TLSVerify: 0, - CertPath: "/tmp/certs", - }, - }, - { - `ryuk.container.privileged=true`, - map[string]string{}, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - RyukPrivileged: true, - }, - }, - { - ``, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", - }, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - RyukPrivileged: true, - }, - }, - { - `ryuk.container.privileged=true`, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", - }, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - RyukPrivileged: true, - }, - }, - { - `ryuk.container.privileged=false`, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", - }, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - RyukPrivileged: true, - }, - }, - { - `ryuk.container.privileged=true`, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "false", - }, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - RyukPrivileged: false, - }, - }, - { - `ryuk.container.privileged=false`, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "false", - }, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - RyukPrivileged: false, - }, - }, - { - `ryuk.container.privileged=false - docker.tls.verify = ERROR`, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", - }, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - RyukPrivileged: true, - }, - }, - { - `ryuk.container.privileged=false`, - map[string]string{ - "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "foo", - }, - TestContainersConfig{ - Host: "", - TLSVerify: 0, - CertPath: "", - RyukPrivileged: false, - }, - }, - } - for i, tt := range tests { - t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) { - tmpDir := t.TempDir() - t.Setenv("HOME", tmpDir) - 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 - } - - config := configureTC() - - assert.Equal(t, tt.expected, config, "Configuration doesn't not match") - - }) - } - }) -} - func ExampleDockerProvider_CreateContainer() { ctx := context.Background() req := ContainerRequest{ @@ -1645,7 +1369,7 @@ func TestContainerCreationWithBindAndVolume(t *testing.T) { ctx, cnl := context.WithTimeout(context.Background(), 30*time.Second) defer cnl() // Create a Docker client. - dockerCli, _, _, err := NewDockerClient() + dockerCli, err := NewDockerClient() if err != nil { t.Fatal(err) } @@ -1730,8 +1454,7 @@ 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{ ContainerRequest: ContainerRequest{ - Image: "postgres:nonexistent-version", - SkipReaper: true, + Image: "postgres:nonexistent-version", }, Started: true, }) @@ -1745,18 +1468,19 @@ func TestContainerNonExistentImage(t *testing.T) { t.Run("the context cancellation is propagated to container creation", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - _, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := GenericContainer(ctx, GenericContainerRequest{ ProviderType: providerType, ContainerRequest: ContainerRequest{ Image: "docker.io/postgres:12", WaitingFor: wait.ForLog("log"), - SkipReaper: true, }, Started: true, }) if !errors.Is(err, ctx.Err()) { t.Fatalf("err should be a ctx cancelled error %v", err) } + + terminateContainerOnEnd(t, ctx, c) }) } @@ -1773,7 +1497,6 @@ func TestContainerCustomPlatformImage(t *testing.T) { ProviderType: providerType, ContainerRequest: ContainerRequest{ Image: "docker.io/redis:latest", - SkipReaper: true, ImagePlatform: nonExistentPlatform, }, Started: false, @@ -1792,7 +1515,6 @@ func TestContainerCustomPlatformImage(t *testing.T) { ProviderType: providerType, ContainerRequest: ContainerRequest{ Image: "docker.io/mysql:5.7", - SkipReaper: true, ImagePlatform: "linux/amd64", }, Started: false, @@ -1801,7 +1523,7 @@ func TestContainerCustomPlatformImage(t *testing.T) { require.NoError(t, err) terminateContainerOnEnd(t, ctx, c) - dockerCli, _, _, err := NewDockerClient() + dockerCli, err := NewDockerClient() require.NoError(t, err) ctr, err := dockerCli.ContainerInspect(ctx, c.GetContainerID()) @@ -1838,7 +1560,7 @@ func TestContainerWithCustomHostname(t *testing.T) { } func readHostname(tb testing.TB, containerId string) string { - containerClient, _, _, err := NewDockerClient() + containerClient, err := NewDockerClient() if err != nil { tb.Fatalf("Failed to create Docker client: %v", err) } diff --git a/docs/features/configuration.md b/docs/features/configuration.md new file mode 100644 index 0000000000..c5c650c331 --- /dev/null +++ b/docs/features/configuration.md @@ -0,0 +1,61 @@ +# Custom configuration + +You can override some default properties if your environment requires that. + +## Configuration locations +The configuration will be loaded from multiple locations. Properties are considered in the following order: + +1. Environment variables +2. `.testcontainers.properties` in user's home folder. Example locations: +**Linux:** `/home/myuser/.testcontainers.properties` +**Windows:** `C:/Users/myuser/.testcontainers.properties` +**macOS:** `/Users/myuser/.testcontainers.properties` + +Note that when using environment variables, configuration property names should be set in upper +case with underscore separators, preceded by `TESTCONTAINERS_` - e.g. `ryuk.disabled` becomes +`TESTCONTAINERS_RYUK_DISABLED`. + +### Supported properties + +_Testcontainers for Go_ provides a struct type to represent the configuration: + + +[Supported properties](../../config.go) inside_block:testcontainersConfig + + +You can read it with the `ReadConfig()` function: + +```go +cfg := testcontainers.ReadConfig() +``` + +### Disabling Ryuk +Ryuk must be started as a privileged container. +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** to `true`. + +!!!info + For more information about Ryuk, see [Garbage Collector](garbage_collector.md). + +## Customizing Docker host detection + +Testcontainers will attempt to detect the Docker environment and configure everything to work automatically. + +However, sometimes customization is required. Testcontainers will respect the following **environment variables**: + +> **DOCKER_HOST** = unix:///var/run/docker.sock +> See [Docker environment variables](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables) +> +> **TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE** +> Path to Docker's socket. Used by Ryuk, Docker Compose, and a few other containers that need to perform Docker actions. +> Example: `/var/run/docker-alt.sock` + +For advanced users, the Docker host connection can be configured **via configuration** in `~/.testcontainers.properties`. +The example below illustrates usage: + +```properties +docker.host=tcp://my.docker.host:1234 # Equivalent to the DOCKER_HOST environment variable. +docker.tls.verify=1 # Equivalent to the DOCKER_TLS_VERIFY environment variable +docker.cert.path=/some/path # Equivalent to the DOCKER_CERT_PATH environment variable +``` diff --git a/docs/features/garbage_collector.md b/docs/features/garbage_collector.md index 643d6ca46d..e725f5a9bd 100644 --- a/docs/features/garbage_collector.md +++ b/docs/features/garbage_collector.md @@ -39,8 +39,10 @@ for more than 10 seconds, it will be killed. !!!warning - This feature can be disabled when creating a container or a network, - but it can cause **unexpected behavior** in your environment. + This feature can be disabled in two different manners, but it can cause **unexpected behavior** in your environment: + + 1. adding `ryuk.disabled=true` to the `.testcontainers.properties` file. + 2. setting the `TESTCONTAINERS_RYUK_DISABLED=true` environment variable. This manner takes precedence over the properties file. We recommend using it only for Continuous Integration services that have their own mechanism to clean up resources. diff --git a/logconsumer_test.go b/logconsumer_test.go index fe8634f3d4..367c6d5b89 100644 --- a/logconsumer_test.go +++ b/logconsumer_test.go @@ -240,7 +240,15 @@ func TestContainerLogWithErrClosed(t *testing.T) { } defer client.Close() - provider := &DockerProvider{client: client, DockerProviderOptions: &DockerProviderOptions{GenericProviderOptions: &GenericProviderOptions{Logger: TestLogger(t)}}} + provider := &DockerProvider{ + client: client, + config: ReadConfig(), + DockerProviderOptions: &DockerProviderOptions{ + GenericProviderOptions: &GenericProviderOptions{ + Logger: TestLogger(t), + }, + }, + } nginx, err := provider.CreateContainer(ctx, ContainerRequest{Image: "nginx", ExposedPorts: []string{"80/tcp"}}) if err != nil { diff --git a/mkdocs.yml b/mkdocs.yml index 93b20983cc..94789ac53e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ nav: - Quickstart: quickstart.md - Features: - features/creating_container.md + - features/configuration.md - features/networking.md - features/garbage_collector.md - features/build_from_dockerfile.md diff --git a/modules/compose/compose_api.go b/modules/compose/compose_api.go index c6e23b50a4..c7d35c2692 100644 --- a/modules/compose/compose_api.go +++ b/modules/compose/compose_api.go @@ -309,7 +309,11 @@ func (d *dockerCompose) lookupContainer(ctx context.Context, svcName string) (*t } container.SetLogger(d.logger) - dockerProvider := &testcontainers.DockerProvider{} + dockerProvider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(d.logger)) + if err != nil { + return nil, err + } + dockerProvider.SetClient(d.dockerClient) container.SetProvider(dockerProvider) @@ -371,7 +375,7 @@ func withEnv(env map[string]string) func(*cli.ProjectOptions) error { } func makeClient(*command.DockerCli) (client.APIClient, error) { - dockerClient, _, _, err := testcontainers.NewDockerClient() + dockerClient, err := testcontainers.NewDockerClient() if err != nil { return nil, err } diff --git a/modules/compose/compose_test.go b/modules/compose/compose_test.go index bf243885e6..6daa8387d6 100644 --- a/modules/compose/compose_test.go +++ b/modules/compose/compose_test.go @@ -445,7 +445,7 @@ func TestLocalDockerComposeWithVolume(t *testing.T) { } func assertVolumeDoesNotExist(tb testing.TB, volumeName string) { - containerClient, _, _, err := testcontainers.NewDockerClient() + containerClient, err := testcontainers.NewDockerClient() if err != nil { tb.Fatalf("Failed to get provider: %v", err) } @@ -470,7 +470,7 @@ func assertContainerEnvironmentVariables( present map[string]string, absent map[string]string, ) { - containerClient, _, _, err := testcontainers.NewDockerClient() + containerClient, err := testcontainers.NewDockerClient() if err != nil { tb.Fatalf("Failed to get provider: %v", err) } diff --git a/modules/localstack/types_test.go b/modules/localstack/types_test.go index d5eb3cf283..faae146e51 100644 --- a/modules/localstack/types_test.go +++ b/modules/localstack/types_test.go @@ -14,7 +14,6 @@ func TestOverrideContainerRequest(t *testing.T) { Env: map[string]string{}, Image: "foo", ExposedPorts: []string{}, - SkipReaper: false, WaitingFor: wait.ForNop( func(ctx context.Context, target wait.StrategyTarget) error { return nil @@ -37,11 +36,9 @@ func TestOverrideContainerRequest(t *testing.T) { "foo1": {"bar"}, }, WaitingFor: wait.ForLog("foo"), - SkipReaper: true, })(req) assert.Equal(t, "BAR", merged.Env["FOO"]) - assert.True(t, merged.SkipReaper) assert.Equal(t, "bar", merged.Image) assert.Equal(t, []string{"12345/tcp"}, merged.ExposedPorts) assert.Equal(t, []string{"foo1", "bar1"}, merged.Networks) diff --git a/network.go b/network.go index e8436240fc..841347f11b 100644 --- a/network.go +++ b/network.go @@ -40,7 +40,7 @@ type NetworkRequest struct { Attachable bool IPAM *network.IPAM - SkipReaper bool // indicates whether we skip setting up a reaper for this + SkipReaper bool // Deprecated: The reaper is globally controlled by the .testcontainers.properties file or the TESTCONTAINERS_RYUK_DISABLED environment variable ReaperImage string // Deprecated: use WithImageName ContainerOption instead. Alternative reaper registry ReaperOptions []ContainerOption // Reaper options to use for this network } diff --git a/provider.go b/provider.go index 479a287117..c16d449e50 100644 --- a/provider.go +++ b/provider.go @@ -84,7 +84,7 @@ type ContainerProvider interface { ReuseOrCreateContainer(context.Context, ContainerRequest) (Container, error) // reuses a container if it exists or creates a container without starting RunContainer(context.Context, ContainerRequest) (Container, error) // create a container and start it Health(context.Context) error - Config() TestContainersConfig + Config() TestcontainersConfig } // GetProvider provides the provider implementation for a certain type @@ -128,14 +128,16 @@ func NewDockerProvider(provOpts ...DockerProviderOption) (*DockerProvider, error provOpts[idx].ApplyDockerTo(o) } - c, host, tcConfig, err := NewDockerClient() + c, err := NewDockerClient() if err != nil { return nil, err } + tcConfig := ReadConfig() + p := &DockerProvider{ DockerProviderOptions: o, - host: host, + host: tcConfig.Host, client: c, config: tcConfig, } diff --git a/reaper.go b/reaper.go index e36a46680d..bfb91cbdb1 100644 --- a/reaper.go +++ b/reaper.go @@ -37,7 +37,7 @@ var ( // The ContainerProvider interface should usually satisfy this as well, so it is pluggable type ReaperProvider interface { RunContainer(ctx context.Context, req ContainerRequest) (Container, error) - Config() TestContainersConfig + Config() TestcontainersConfig } // NewReaper creates a Reaper with a sessionID to identify containers and a provider to use @@ -97,7 +97,6 @@ func newReaper(ctx context.Context, sessionID string, provider ReaperProvider, o TestcontainerLabelIsReaper: "true", testcontainersdocker.LabelReaper: "true", }, - SkipReaper: true, Mounts: Mounts(BindMount(dockerHost, "/var/run/docker.sock")), Privileged: tcConfig.RyukPrivileged, WaitingFor: wait.ForListeningPort(listeningPort), diff --git a/reaper_test.go b/reaper_test.go index 1032c88ee4..36e9d4ebef 100644 --- a/reaper_test.go +++ b/reaper_test.go @@ -18,7 +18,7 @@ type mockReaperProvider struct { req ContainerRequest hostConfig *container.HostConfig enpointSettings map[string]*network.EndpointSettings - config TestContainersConfig + config TestcontainersConfig } var errExpected = errors.New("expected") @@ -43,7 +43,7 @@ func (m *mockReaperProvider) RunContainer(ctx context.Context, req ContainerRequ return nil, errExpected } -func (m *mockReaperProvider) Config() TestContainersConfig { +func (m *mockReaperProvider) Config() TestcontainersConfig { return m.config } @@ -60,7 +60,6 @@ func createContainerRequest(customize func(ContainerRequest) ContainerRequest) C testcontainersdocker.LabelLang: "go", testcontainersdocker.LabelVersion: internal.Version, }, - SkipReaper: true, Mounts: Mounts(BindMount("/var/run/docker.sock", "/var/run/docker.sock")), WaitingFor: wait.ForListeningPort(nat.Port("8080/tcp")), ReaperOptions: []ContainerOption{ @@ -78,7 +77,7 @@ func Test_NewReaper(t *testing.T) { type cases struct { name string req ContainerRequest - config TestContainersConfig + config TestcontainersConfig ctx context.Context } @@ -86,7 +85,7 @@ func Test_NewReaper(t *testing.T) { { name: "non-privileged", req: createContainerRequest(nil), - config: TestContainersConfig{}, + config: TestcontainersConfig{}, }, { name: "privileged", @@ -94,7 +93,7 @@ func Test_NewReaper(t *testing.T) { req.Privileged = true return req }), - config: TestContainersConfig{ + config: TestcontainersConfig{ RyukPrivileged: true, }, }, @@ -104,7 +103,7 @@ func Test_NewReaper(t *testing.T) { req.Mounts = Mounts(BindMount("/value/in/context.sock", "/var/run/docker.sock")) return req }), - config: TestContainersConfig{}, + config: TestcontainersConfig{}, ctx: context.WithValue(context.TODO(), testcontainersdocker.DockerHostContextKey, "unix:///value/in/context.sock"), }, } @@ -126,7 +125,6 @@ func Test_NewReaper(t *testing.T) { 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.SkipReaper, provider.req.SkipReaper, "expected skipReaper doesn'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") @@ -153,7 +151,7 @@ func Test_ReaperForNetwork(t *testing.T) { } provider := &mockReaperProvider{ - config: TestContainersConfig{}, + config: TestcontainersConfig{}, } _, err := newReaper(ctx, "sessionId", provider, req.ReaperOptions...)