diff --git a/docs/features/common_functional_options.md b/docs/features/common_functional_options.md
index 91a7fb5fdd..7acebf12e0 100644
--- a/docs/features/common_functional_options.md
+++ b/docs/features/common_functional_options.md
@@ -188,6 +188,12 @@ func (g *TestLogConsumer) Accept(l Log) {
}
```
+#### WithLogConsumerConfig
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+If you need to set the log consumer config for the container, you can use `testcontainers.WithLogConsumerConfig`. This option completely replaces the existing log consumer config, including the log consumers and the log production options.
+
#### WithLogger
- Since testcontainers-go :material-tag: v0.29.0
@@ -214,6 +220,26 @@ func TestHandler(t *testing.T) {
Please read the [Following Container Logs](/features/follow_logs) documentation for more information about creating log consumers.
+#### WithAlwaysPull
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+If you need to pull the image before starting the container, you can use `testcontainers.WithAlwaysPull()`.
+
+#### WithImagePlatform
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+If you need to set the platform for a container, you can use `testcontainers.WithImagePlatform(platform string)`.
+
+#### LifecycleHooks
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+If you need to set the lifecycle hooks for the container, you can use `testcontainers.WithLifecycleHooks`, which replaces the existing lifecycle hooks with the new ones.
+
+You can also use `testcontainers.WithAdditionalLifecycleHooks`, which appends the new lifecycle hooks to the existing ones.
+
#### Wait Strategies
If you need to set a different wait strategy for the container, you can use `testcontainers.WithWaitStrategy` with a valid wait strategy.
@@ -282,6 +308,24 @@ In the case you need to retrieve the network name, you can simply read it from t
!!!warning
This option is not checking whether the network exists or not. If you use a network that doesn't exist, the container will start in the default Docker network, as in the default behavior.
+#### WithNetworkByName
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+If you want to attach your containers to an already existing Docker network by its name, you can use the `network.WithNetworkName(aliases []string, networkName string)` option, which receives an alias as parameter and the network name, attaching the container to it, and setting the network alias for that network.
+
+!!!warning
+ In case the network name is `bridge`, no aliases are set. This is because network-scoped alias is supported only for containers in user defined networks.
+
+#### WithBridgeNetwork
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+If you want to attach your containers to the `bridge` network, you can use the `network.WithBridgeNetwork()` option.
+
+!!!warning
+ The `bridge` network is the default network for Docker. It's not a user defined network, so it doesn't support network-scoped aliases.
+
#### WithNewNetwork
- Since testcontainers-go :material-tag: v0.27.0
@@ -335,3 +379,27 @@ ctr, err := mymodule.Run(ctx, "docker.io/myservice:1.2.3",
!!!warning
Reusing a container is experimental and the API is subject to change for a more robust implementation that is not based on container names.
+
+#### WithName
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+If you need to set the name of the container, you can use the `testcontainers.WithName` option.
+
+```golang
+ctr, err := mymodule.Run(ctx, "docker.io/myservice:1.2.3",
+ testcontainers.WithName("my-container-name"),
+)
+```
+
+!!!warning
+ This option is not checking whether the container name is already in use. If you use a name that is already in use, an error is returned.
+ At the same time, we discourage using this option as it might lead to unexpected behavior, but we understand that in some cases it might be useful.
+
+#### WithNoStart
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+If you need to prevent the container from being started after creation, you can use the `testcontainers.WithNoStart` option.
+
+
diff --git a/docs/modules/index.md b/docs/modules/index.md
index abf6a519a7..046f05416b 100644
--- a/docs/modules/index.md
+++ b/docs/modules/index.md
@@ -204,7 +204,12 @@ In order to simplify the creation of the container for a given module, `Testcont
- `testcontainers.WithTmpfs`: a function that adds tmpfs mounts to the container.
- `testcontainers.WithHostPortAccess`: a function that enables the container to access a port that is already running in the host.
- `testcontainers.WithLogConsumers`: a function that sets the log consumers for the container request.
+- `testcontainers.WithLogConsumerConfig`: a function that sets the log consumer config for the container request.
- `testcontainers.WithLogger`: a function that sets the logger for the container request.
+- `testcontainers.WithLifecycleHooks`: a function that sets the lifecycle hooks for the container request.
+- `testcontainers.WithAdditionalLifecycleHooks`: a function that appends lifecycle hooks to the existing ones for the container request.
+- `testcontainers.WithAlwaysPull`: a function that pulls the image before starting the container.
+- `testcontainers.WithImagePlatform`: a function that sets the image platform for the container request.
- `testcontainers.WithWaitStrategy`: a function that sets the wait strategy for the container request.
- `testcontainers.WithWaitStrategyAndDeadline`: a function that sets the wait strategy for the container request with a deadline.
- `testcontainers.WithStartupCommand`: a function that sets the execution of a command when the container starts.
@@ -217,6 +222,12 @@ In order to simplify the creation of the container for a given module, `Testcont
- `testcontainers.WithEndpointSettingsModifier`: a function that sets the endpoint settings Docker type for the container request. Please see [Advanced Settings](../features/creating_container.md#advanced-settings) for more information.
- `testcontainers.CustomizeRequest`: a function that merges the default options with the ones provided by the user. Recommended for completely customizing the container request.
- `testcontainers.WithReuseByName`: a function that marks a container to be reused if it exists or create a new one if it doesn't.
+- `testcontainers.WithName`: a function that sets the name of the container.
+- `testcontainers.WithNoStart`: a function that prevents the container from being started after creation, so it must be started manually.
+- `network.WithNetwork`: a function that sets the network and the network aliases for the container request, reusing an already existing network.
+- `network.WithNetworkName`: a function that sets the network aliases for an already existing network, by its name.
+- `network.WithBridgeNetwork`: a function that sets the container to be attached to the `bridge` network.
+- `network.WithNewNetwork`: a function that sets the network aliases for a throw-away network for the container request.
### Update Go dependencies in the modules
diff --git a/network/network.go b/network/network.go
index 394c60514e..b5b4314a47 100644
--- a/network/network.go
+++ b/network/network.go
@@ -2,6 +2,7 @@ package network
import (
"context"
+ "errors"
"fmt"
"github.com/docker/docker/api/types/network"
@@ -137,8 +138,18 @@ func WithIPAM(ipam *network.IPAM) CustomizeNetworkOption {
// WithNetwork reuses an already existing network, attaching the container to it.
// Finally it sets the network alias on that network to the given alias.
func WithNetwork(aliases []string, nw *testcontainers.DockerNetwork) testcontainers.CustomizeRequestOption {
+ return WithNetworkName(aliases, nw.Name)
+}
+
+// WithNetworkName attachs a container to an already existing network, by its name.
+// If the network is not "bridge", it sets the network alias on that network
+// to the given alias, else, it returns an error. This is because network-scoped alias
+// is supported only for containers in user defined networks.
+func WithNetworkName(aliases []string, networkName string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) error {
- networkName := nw.Name
+ if networkName == "bridge" {
+ return errors.New("network-scoped aliases are supported only for containers in user defined networks")
+ }
// attaching to the network because it was created with success or it already existed.
req.Networks = append(req.Networks, networkName)
@@ -152,6 +163,15 @@ func WithNetwork(aliases []string, nw *testcontainers.DockerNetwork) testcontain
}
}
+// WithBridgeNetwork attachs a container to the "bridge" network.
+// There is no need to set the network alias, as it is not supported for the "bridge" network.
+func WithBridgeNetwork() testcontainers.CustomizeRequestOption {
+ return func(req *testcontainers.GenericContainerRequest) error {
+ req.Networks = append(req.Networks, "bridge")
+ return nil
+ }
+}
+
// WithNewNetwork creates a new network with random name and customizers, and attaches the container to it.
// Finally it sets the network alias on that network to the given alias.
func WithNewNetwork(ctx context.Context, aliases []string, opts ...NetworkCustomizer) testcontainers.CustomizeRequestOption {
diff --git a/network/network_test.go b/network/network_test.go
index 08aa4fb74f..8b9ea632a2 100644
--- a/network/network_test.go
+++ b/network/network_test.go
@@ -342,6 +342,44 @@ func TestWithNetwork(t *testing.T) {
require.Equal(t, expectedLabels, newNetwork.Labels)
}
+func TestWithNetworkName(t *testing.T) {
+ t.Run("bridge/success", func(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{},
+ }
+
+ err := network.WithBridgeNetwork()(&req)
+ require.NoError(t, err)
+
+ require.Len(t, req.Networks, 1)
+ require.Equal(t, "bridge", req.Networks[0])
+ })
+
+ t.Run("bridge/error/network-scoped-alias", func(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{},
+ }
+
+ err := network.WithNetworkName([]string{"alias"}, "bridge")(&req)
+ require.Error(t, err)
+ })
+
+ t.Run("user-defined/success", func(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{},
+ }
+
+ err := network.WithNetworkName([]string{"alias"}, "user-defined")(&req)
+ require.NoError(t, err)
+
+ require.Len(t, req.Networks, 1)
+ require.Equal(t, "user-defined", req.Networks[0])
+
+ require.Len(t, req.NetworkAliases, 1)
+ require.Equal(t, map[string][]string{"user-defined": {"alias"}}, req.NetworkAliases)
+ })
+}
+
func TestWithSyntheticNetwork(t *testing.T) {
nw := &testcontainers.DockerNetwork{
Name: "synthetic-network",
diff --git a/options.go b/options.go
index 9afbcd7e39..81ce4c2b68 100644
--- a/options.go
+++ b/options.go
@@ -106,14 +106,33 @@ func WithHostPortAccess(ports ...int) CustomizeRequestOption {
}
}
+// WithName will set the name of the container.
+func WithName(containerName string) CustomizeRequestOption {
+ return func(req *GenericContainerRequest) error {
+ if containerName == "" {
+ return errors.New("container name must be provided")
+ }
+ req.Name = containerName
+ return nil
+ }
+}
+
+// WithNoStart will prevent the container from being started after creation.
+func WithNoStart() CustomizeRequestOption {
+ return func(req *GenericContainerRequest) error {
+ req.Started = false
+ return nil
+ }
+}
+
// WithReuseByName will mark a container to be reused if it exists or create a new one if it doesn't.
// A container name must be provided to identify the container to be reused.
func WithReuseByName(containerName string) CustomizeRequestOption {
return func(req *GenericContainerRequest) error {
- if containerName == "" {
- return errors.New("container name must be provided for reuse")
+ if err := WithName(containerName)(req); err != nil {
+ return err
}
- req.Name = containerName
+
req.Reuse = true
return nil
}
@@ -255,6 +274,17 @@ func WithLogConsumers(consumer ...LogConsumer) CustomizeRequestOption {
}
}
+// WithLogConsumerConfig sets the log consumer config for a container.
+// Beware that this option completely replaces the existing log consumer config,
+// including the log consumers and the log production options,
+// so it should be used with care.
+func WithLogConsumerConfig(config *LogConsumerConfig) CustomizeRequestOption {
+ return func(req *GenericContainerRequest) error {
+ req.LogConsumerCfg = config
+ return nil
+ }
+}
+
// Executable represents an executable command to be sent to a container, including options,
// as part of the different lifecycle hooks.
type Executable interface {
@@ -376,6 +406,22 @@ func WithImageMount(source string, subpath string, target ContainerMountTarget)
}
}
+// WithAlwaysPull will pull the image before starting the container
+func WithAlwaysPull() CustomizeRequestOption {
+ return func(req *GenericContainerRequest) error {
+ req.AlwaysPullImage = true
+ return nil
+ }
+}
+
+// WithImagePlatform sets the platform for a container
+func WithImagePlatform(platform string) CustomizeRequestOption {
+ return func(req *GenericContainerRequest) error {
+ req.ImagePlatform = platform
+ return nil
+ }
+}
+
// WithEntrypoint completely replaces the entrypoint of a container
func WithEntrypoint(entrypoint ...string) CustomizeRequestOption {
return func(req *GenericContainerRequest) error {
@@ -429,6 +475,22 @@ func WithLabels(labels map[string]string) CustomizeRequestOption {
}
}
+// WithLifecycleHooks completely replaces the lifecycle hooks for a container
+func WithLifecycleHooks(hooks ...ContainerLifecycleHooks) CustomizeRequestOption {
+ return func(req *GenericContainerRequest) error {
+ req.LifecycleHooks = hooks
+ return nil
+ }
+}
+
+// WithAdditionalLifecycleHooks appends lifecycle hooks to the existing ones for a container
+func WithAdditionalLifecycleHooks(hooks ...ContainerLifecycleHooks) CustomizeRequestOption {
+ return func(req *GenericContainerRequest) error {
+ req.LifecycleHooks = append(req.LifecycleHooks, hooks...)
+ return nil
+ }
+}
+
// WithMounts appends the mounts to the mounts for a container
func WithMounts(mounts ...ContainerMount) CustomizeRequestOption {
return func(req *GenericContainerRequest) error {
diff --git a/options_test.go b/options_test.go
index 34ac401b3d..69ae12d583 100644
--- a/options_test.go
+++ b/options_test.go
@@ -99,6 +99,43 @@ func TestWithLogConsumers(t *testing.T) {
require.NotEmpty(t, lc.msgs)
}
+func TestWithLogConsumerConfig(t *testing.T) {
+ lc := &msgsLogConsumer{}
+
+ t.Run("add-to-nil", func(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ },
+ }
+
+ err := testcontainers.WithLogConsumerConfig(&testcontainers.LogConsumerConfig{
+ Consumers: []testcontainers.LogConsumer{lc},
+ })(&req)
+ require.NoError(t, err)
+
+ require.Equal(t, []testcontainers.LogConsumer{lc}, req.LogConsumerCfg.Consumers)
+ })
+
+ t.Run("replace-existing", func(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ LogConsumerCfg: &testcontainers.LogConsumerConfig{
+ Consumers: []testcontainers.LogConsumer{testcontainers.NewFooLogConsumer(t)},
+ },
+ },
+ }
+
+ err := testcontainers.WithLogConsumerConfig(&testcontainers.LogConsumerConfig{
+ Consumers: []testcontainers.LogConsumer{lc},
+ })(&req)
+ require.NoError(t, err)
+
+ require.Equal(t, []testcontainers.LogConsumer{lc}, req.LogConsumerCfg.Consumers)
+ })
+}
+
func TestWithStartupCommand(t *testing.T) {
req := testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
@@ -360,6 +397,30 @@ func TestWithCmd(t *testing.T) {
})
}
+func TestWithAlwaysPull(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ },
+ }
+
+ opt := testcontainers.WithAlwaysPull()
+ require.NoError(t, opt.Customize(&req))
+ require.True(t, req.AlwaysPullImage)
+}
+
+func TestWithImagePlatform(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ },
+ }
+
+ opt := testcontainers.WithImagePlatform("linux/amd64")
+ require.NoError(t, opt.Customize(&req))
+ require.Equal(t, "linux/amd64", req.ImagePlatform)
+}
+
func TestWithCmdArgs(t *testing.T) {
testCmd := func(t *testing.T, initial []string, add []string, expected []string) {
t.Helper()
@@ -422,6 +483,68 @@ func TestWithLabels(t *testing.T) {
})
}
+func TestWithLifecycleHooks(t *testing.T) {
+ testHook := testcontainers.DefaultLoggingHook(nil)
+
+ testLifecycleHooks := func(t *testing.T, replace bool, initial []testcontainers.ContainerLifecycleHooks, add []testcontainers.ContainerLifecycleHooks, expected []testcontainers.ContainerLifecycleHooks) {
+ t.Helper()
+
+ req := &testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ LifecycleHooks: initial,
+ },
+ }
+
+ var opt testcontainers.CustomizeRequestOption
+ if replace {
+ opt = testcontainers.WithLifecycleHooks(add...)
+ } else {
+ opt = testcontainers.WithAdditionalLifecycleHooks(add...)
+ }
+ require.NoError(t, opt.Customize(req))
+ require.Len(t, req.LifecycleHooks, len(expected))
+ for i, hook := range expected {
+ require.Equal(t, hook, req.LifecycleHooks[i])
+ }
+ }
+
+ t.Run("replace-nil", func(t *testing.T) {
+ testLifecycleHooks(t,
+ true,
+ nil,
+ []testcontainers.ContainerLifecycleHooks{testHook},
+ []testcontainers.ContainerLifecycleHooks{testHook},
+ )
+ })
+
+ t.Run("replace-existing", func(t *testing.T) {
+ testLifecycleHooks(t,
+ true,
+ []testcontainers.ContainerLifecycleHooks{testHook},
+ []testcontainers.ContainerLifecycleHooks{testHook},
+ []testcontainers.ContainerLifecycleHooks{testHook},
+ )
+ })
+
+ t.Run("add-to-nil", func(t *testing.T) {
+ testLifecycleHooks(t,
+ false,
+ nil,
+ []testcontainers.ContainerLifecycleHooks{testHook},
+ []testcontainers.ContainerLifecycleHooks{testHook},
+ )
+ })
+
+ t.Run("add-to-existing", func(t *testing.T) {
+ testLifecycleHooks(t,
+ false,
+ []testcontainers.ContainerLifecycleHooks{testHook},
+ []testcontainers.ContainerLifecycleHooks{testHook},
+ []testcontainers.ContainerLifecycleHooks{testHook, testHook},
+ )
+ })
+}
+
func TestWithMounts(t *testing.T) {
testMounts := func(t *testing.T, initial []testcontainers.ContainerMount, add []testcontainers.ContainerMount, expected testcontainers.ContainerMounts) {
t.Helper()
@@ -629,7 +752,35 @@ func TestWithReuseByName_ErrorsWithoutContainerNameProvided(t *testing.T) {
opt := testcontainers.WithReuseByName("")
err := opt.Customize(req)
- require.ErrorContains(t, err, "container name must be provided for reuse")
+ require.ErrorContains(t, err, "container name must be provided")
require.False(t, req.Reuse)
require.Empty(t, req.Name)
}
+
+func TestWithName(t *testing.T) {
+ t.Parallel()
+ req := &testcontainers.GenericContainerRequest{}
+
+ opt := testcontainers.WithName("pg-test")
+ err := opt.Customize(req)
+ require.NoError(t, err)
+ require.Equal(t, "pg-test", req.Name)
+
+ t.Run("empty", func(t *testing.T) {
+ req := &testcontainers.GenericContainerRequest{}
+
+ opt := testcontainers.WithName("")
+ err := opt.Customize(req)
+ require.ErrorContains(t, err, "container name must be provided")
+ })
+}
+
+func TestWithNoStart(t *testing.T) {
+ t.Parallel()
+ req := &testcontainers.GenericContainerRequest{}
+
+ opt := testcontainers.WithNoStart()
+ err := opt.Customize(req)
+ require.NoError(t, err)
+ require.False(t, req.Started)
+}