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) +}