diff --git a/docker_test.go b/docker_test.go index 9d3a16eb61..4387cbe1a7 100644 --- a/docker_test.go +++ b/docker_test.go @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/internal/core" "github.com/testcontainers/testcontainers-go/log" "github.com/testcontainers/testcontainers-go/wait" @@ -94,6 +95,9 @@ func TestContainerWithHostNetworkOptions(t *testing.T) { } func TestContainerWithHostNetworkOptions_UseExposePortsFromImageConfigs(t *testing.T) { + t.Setenv("TESTCONTAINERS_AUTO_EXPOSE_PORTS", "true") + config.Reset() + ctx := context.Background() gcr := GenericContainerRequest{ ContainerRequest: ContainerRequest{ @@ -1911,6 +1915,9 @@ func assertExtractedFiles(t *testing.T, ctx context.Context, container Container } func TestDockerProviderFindContainerByName(t *testing.T) { + t.Setenv("TESTCONTAINERS_AUTO_EXPOSE_PORTS", "true") + config.Reset() + ctx := context.Background() provider, err := NewDockerProvider(WithLogger(log.TestLogger(t))) require.NoError(t, err) diff --git a/docs/features/common_functional_options.md b/docs/features/common_functional_options.md index 9a7e459dd8..f910d00d6c 100644 --- a/docs/features/common_functional_options.md +++ b/docs/features/common_functional_options.md @@ -25,7 +25,7 @@ ctr, err = mymodule.Run(ctx, "docker.io/myservice:1.2.3", testcontainers.WithEnv - Since :material-tag: v0.20.0 -If you need to set a different wait strategy for the container, you can use `testcontainers.WithWaitStrategy` with a valid wait strategy. +If you need to set a different wait strategy for the container, replacing the existing one, you can use `testcontainers.WithWaitStrategy` with a valid wait strategy. !!!info The default deadline for the wait strategy is 60 seconds. diff --git a/internal/config/config.go b/internal/config/config.go index deb8f0a9f8..3d43a0a5f2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,6 +9,8 @@ import ( "time" "github.com/magiconair/properties" + + "github.com/testcontainers/testcontainers-go/log" ) const ReaperDefaultImage = "testcontainers/ryuk:0.13.0" @@ -85,6 +87,11 @@ type Config struct { // // Environment variable: TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE TestcontainersHost string `properties:"tc.host,default="` + + // AutoExposePorts is a flag to enable or disable the automatic exposure of ports when no ports are explicitly exposed. + // + // Environment variable: TESTCONTAINERS_AUTO_EXPOSE_PORTS + AutoExposePorts bool `properties:"tc.auto.expose.ports,default=true"` } // } @@ -141,6 +148,15 @@ func read() Config { config.RyukConnectionTimeout = timeout } + autoExposePortsEnv := readTestcontainersEnv("TESTCONTAINERS_AUTO_EXPOSE_PORTS") + if parseBool(autoExposePortsEnv) { + config.AutoExposePorts = autoExposePortsEnv == "true" + } + + if config.AutoExposePorts { + log.Printf("⚠️ Testcontainers is configured to automatically expose ports from the Image definition, but this is deprecated and will be removed in a future version. Please set `tc.auto.expose.ports=false` in the testcontainers.properties file.") + } + return config } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 591fcff11c..9d29a0bf5c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -26,6 +26,7 @@ func resetTestEnv(t *testing.T) { t.Setenv("RYUK_VERBOSE", "") t.Setenv("RYUK_RECONNECTION_TIMEOUT", "") t.Setenv("RYUK_CONNECTION_TIMEOUT", "") + t.Setenv("TESTCONTAINERS_AUTO_EXPOSE_PORTS", "true") } func TestReadConfig(t *testing.T) { @@ -42,8 +43,9 @@ func TestReadConfig(t *testing.T) { config := Read() expected := Config{ - RyukDisabled: true, - Host: "", // docker socket is empty at the properties file + RyukDisabled: true, + Host: "", // docker socket is empty at the properties file + AutoExposePorts: true, } require.Equal(t, expected, config) @@ -66,7 +68,9 @@ func TestReadTCConfig(t *testing.T) { config := read() - expected := Config{} + expected := Config{ + AutoExposePorts: true, + } assert.Equal(t, expected, config) }) @@ -89,6 +93,7 @@ func TestReadTCConfig(t *testing.T) { Host: "", // docker socket is empty at the properties file RyukReconnectionTimeout: 13 * time.Second, RyukConnectionTimeout: 12 * time.Second, + AutoExposePorts: true, } assert.Equal(t, expected, config) @@ -101,7 +106,9 @@ func TestReadTCConfig(t *testing.T) { config := read() - expected := Config{} + expected := Config{ + AutoExposePorts: true, + } assert.Equal(t, expected, config) }) @@ -113,7 +120,10 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("DOCKER_HOST", tcpDockerHost33293) config := read() - expected := Config{} // the config does not read DOCKER_HOST, that's why it's empty + // the config does not read DOCKER_HOST, that's why it's empty + expected := Config{ + AutoExposePorts: true, + } assert.Equal(t, expected, config) }) @@ -128,7 +138,7 @@ func TestReadTCConfig(t *testing.T) { t.Setenv("RYUK_VERBOSE", "true") t.Setenv("RYUK_RECONNECTION_TIMEOUT", "13s") t.Setenv("RYUK_CONNECTION_TIMEOUT", "12s") - + t.Setenv("TESTCONTAINERS_AUTO_EXPOSE_PORTS", "true") config := read() expected := Config{ HubImageNamePrefix: defaultHubPrefix, @@ -137,6 +147,7 @@ func TestReadTCConfig(t *testing.T) { RyukVerbose: true, RyukReconnectionTimeout: 13 * time.Second, RyukConnectionTimeout: 12 * time.Second, + AutoExposePorts: true, } assert.Equal(t, expected, config) @@ -145,9 +156,11 @@ func TestReadTCConfig(t *testing.T) { t.Run("HOME contains TC properties file", func(t *testing.T) { defaultRyukConnectionTimeout := 60 * time.Second defaultRyukReconnectionTimeout := 10 * time.Second + defaultAutoExposePorts := true defaultConfig := Config{ RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, } tests := []struct { @@ -164,6 +177,7 @@ func TestReadTCConfig(t *testing.T) { Host: tcpDockerHost33293, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -176,6 +190,7 @@ func TestReadTCConfig(t *testing.T) { Host: tcpDockerHost4711, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -191,6 +206,7 @@ func TestReadTCConfig(t *testing.T) { TLSVerify: 1, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -200,6 +216,7 @@ func TestReadTCConfig(t *testing.T) { Config{ RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -212,6 +229,7 @@ func TestReadTCConfig(t *testing.T) { Host: tcpDockerHost1234, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -222,6 +240,7 @@ func TestReadTCConfig(t *testing.T) { Host: tcpDockerHost33293, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -242,6 +261,7 @@ func TestReadTCConfig(t *testing.T) { CertPath: "/tmp/certs", RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -252,6 +272,7 @@ func TestReadTCConfig(t *testing.T) { RyukDisabled: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -262,6 +283,7 @@ func TestReadTCConfig(t *testing.T) { RyukPrivileged: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -272,6 +294,7 @@ func TestReadTCConfig(t *testing.T) { Config{ RyukReconnectionTimeout: 13 * time.Second, RyukConnectionTimeout: 12 * time.Second, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -284,6 +307,7 @@ func TestReadTCConfig(t *testing.T) { Config{ RyukReconnectionTimeout: 13 * time.Second, RyukConnectionTimeout: 12 * time.Second, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -297,6 +321,7 @@ func TestReadTCConfig(t *testing.T) { Config{ RyukReconnectionTimeout: 13 * time.Second, RyukConnectionTimeout: 12 * time.Second, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -307,6 +332,7 @@ func TestReadTCConfig(t *testing.T) { RyukVerbose: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -319,6 +345,7 @@ func TestReadTCConfig(t *testing.T) { RyukDisabled: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -331,6 +358,7 @@ func TestReadTCConfig(t *testing.T) { RyukPrivileged: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -343,6 +371,7 @@ func TestReadTCConfig(t *testing.T) { RyukDisabled: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -355,6 +384,7 @@ func TestReadTCConfig(t *testing.T) { RyukDisabled: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -383,6 +413,7 @@ func TestReadTCConfig(t *testing.T) { RyukVerbose: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -395,6 +426,7 @@ func TestReadTCConfig(t *testing.T) { RyukVerbose: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -423,6 +455,7 @@ func TestReadTCConfig(t *testing.T) { RyukPrivileged: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -435,6 +468,7 @@ func TestReadTCConfig(t *testing.T) { RyukPrivileged: true, RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -462,8 +496,9 @@ func TestReadTCConfig(t *testing.T) { "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED": "true", }, Config{ - RyukDisabled: true, - RyukPrivileged: true, + RyukDisabled: true, + RyukPrivileged: true, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -490,6 +525,7 @@ func TestReadTCConfig(t *testing.T) { HubImageNamePrefix: defaultHubPrefix + "/props/", RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -502,6 +538,7 @@ func TestReadTCConfig(t *testing.T) { HubImageNamePrefix: defaultHubPrefix + "/env/", RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, }, }, { @@ -514,6 +551,55 @@ func TestReadTCConfig(t *testing.T) { HubImageNamePrefix: defaultHubPrefix + "/env/", RyukConnectionTimeout: defaultRyukConnectionTimeout, RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: defaultAutoExposePorts, + }, + }, + { + "auto-expose-ports/env-var/properties/0", + `tc.auto.expose.ports=true`, + map[string]string{ + "TESTCONTAINERS_AUTO_EXPOSE_PORTS": "true", + }, + Config{ + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: true, + }, + }, + { + "auto-expose-ports/env-var/properties/1", + `tc.auto.expose.ports=false`, + map[string]string{ + "TESTCONTAINERS_AUTO_EXPOSE_PORTS": "true", + }, + Config{ + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: true, + }, + }, + { + "auto-expose-ports/env-var/properties/2", + `tc.auto.expose.ports=true`, + map[string]string{ + "TESTCONTAINERS_AUTO_EXPOSE_PORTS": "false", + }, + Config{ + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: false, + }, + }, + { + "auto-expose-ports/env-var/properties/3", + `tc.auto.expose.ports=false`, + map[string]string{ + "TESTCONTAINERS_AUTO_EXPOSE_PORTS": "false", + }, + Config{ + RyukConnectionTimeout: defaultRyukConnectionTimeout, + RyukReconnectionTimeout: defaultRyukReconnectionTimeout, + AutoExposePorts: false, }, }, } diff --git a/lifecycle.go b/lifecycle.go index 7887ebedc4..4464e84184 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/log" ) @@ -520,12 +521,9 @@ func (p *DockerProvider) preCreateContainerHook(ctx context.Context, req Contain exposedPorts := req.ExposedPorts // this check must be done after the pre-creation Modifiers are called, so the network mode is already set if len(exposedPorts) == 0 && !hostConfig.NetworkMode.IsContainer() { - image, err := p.client.ImageInspect(ctx, dockerInput.Image) - if err != nil { - return err - } - for p := range image.Config.ExposedPorts { - exposedPorts = append(exposedPorts, string(p)) + // Only expose the ports defined in the image if configured in the Testcontainers properties file. + if config.Read().AutoExposePorts { + hostConfig.PublishAllPorts = true } } diff --git a/lifecycle_test.go b/lifecycle_test.go index c568eb09e8..38ff8d2592 100644 --- a/lifecycle_test.go +++ b/lifecycle_test.go @@ -19,9 +19,58 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/internal/config" "github.com/testcontainers/testcontainers-go/wait" ) +func TestPreCreateModifierHook_NoExposedPorts(t *testing.T) { + t.Run("auto-expose-ports-enabled", func(t *testing.T) { + t.Setenv("TESTCONTAINERS_AUTO_EXPOSE_PORTS", "true") + config.Reset() + + req := GenericContainerRequest{ + ContainerRequest: ContainerRequest{ + Image: nginxAlpineImage, + }, + Started: true, + } + + ctr, err := GenericContainer(context.Background(), req) + require.NoError(t, err) + CleanupContainer(t, ctr) + + json, err := ctr.Inspect(context.Background()) + require.NoError(t, err) + + require.Equal(t, nat.PortSet(nat.PortSet{"80/tcp": struct{}{}}), json.Config.ExposedPorts) + require.Equal(t, nat.PortMap{}, json.HostConfig.PortBindings) + require.NotNil(t, json.NetworkSettings.Ports["80/tcp"]) + }) + + t.Run("auto-expose-ports-disabled", func(t *testing.T) { + t.Setenv("TESTCONTAINERS_AUTO_EXPOSE_PORTS", "false") + config.Reset() + + req := GenericContainerRequest{ + ContainerRequest: ContainerRequest{ + Image: nginxAlpineImage, + }, + Started: true, + } + + ctr, err := GenericContainer(context.Background(), req) + require.NoError(t, err) + CleanupContainer(t, ctr) + + json, err := ctr.Inspect(context.Background()) + require.NoError(t, err) + + require.Equal(t, nat.PortSet(nat.PortSet{"80/tcp": struct{}{}}), json.Config.ExposedPorts) + require.Equal(t, nat.PortMap{}, json.HostConfig.PortBindings) + require.Nil(t, json.NetworkSettings.Ports["80/tcp"]) + }) +} + func TestPreCreateModifierHook(t *testing.T) { ctx := context.Background() @@ -55,7 +104,7 @@ func TestPreCreateModifierHook(t *testing.T) { require.Len(t, errs, 2) // one valid and two invalid mounts }) - t.Run("No exposed ports", func(t *testing.T) { + t.Run("no-exposed-ports", func(t *testing.T) { // reqWithModifiers { req := ContainerRequest{ Image: nginxAlpineImage, // alpine image does expose port 80 @@ -104,18 +153,8 @@ func TestPreCreateModifierHook(t *testing.T) { // assertions - assert.Equal( - t, - []string{"a=b"}, - inputConfig.Env, - "Docker config's env should be overwritten by the modifier", - ) - assert.Equal(t, - nat.PortSet(nat.PortSet{"80/tcp": struct{}{}}), - inputConfig.ExposedPorts, - "Docker config's exposed ports should be overwritten by the modifier", - ) - assert.Equal( + require.Equal(t, []string{"a=b"}, inputConfig.Env) + require.Equal( t, []mount.Mount{ { @@ -128,32 +167,9 @@ func TestPreCreateModifierHook(t *testing.T) { }, }, inputHostConfig.Mounts, - "Host config's mounts should be mapped to Docker types", - ) - - assert.Equal(t, nat.PortMap{ - "80/tcp": []nat.PortBinding{ - { - HostIP: "", - HostPort: "", - }, - }, - }, inputHostConfig.PortBindings, - "Host config's port bindings should be overwritten by the modifier", - ) - - assert.Equal( - t, - []string{"b"}, - inputNetworkingConfig.EndpointsConfig["a"].Aliases, - "Networking config's aliases should be overwritten by the modifier", - ) - assert.Equal( - t, - []string{"link1", "link2"}, - inputNetworkingConfig.EndpointsConfig["a"].Links, - "Networking config's links should be overwritten by the modifier", ) + require.Equal(t, []string{"b"}, inputNetworkingConfig.EndpointsConfig["a"].Aliases) + require.Equal(t, []string{"link1", "link2"}, inputNetworkingConfig.EndpointsConfig["a"].Links) }) t.Run("No exposed ports and network mode IsContainer", func(t *testing.T) {