diff --git a/docker.go b/docker.go
index 9af552c3dd..a559b8d7ea 100644
--- a/docker.go
+++ b/docker.go
@@ -203,6 +203,13 @@ func (c *DockerContainer) Start(ctx context.Context) error {
return err
}
+ c.isRunning = true
+
+ err = c.readiedHook(ctx)
+ if err != nil {
+ return err
+ }
+
return nil
}
@@ -1066,86 +1073,13 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
// default hooks include logger hook and pre-create hook
defaultHooks := []ContainerLifecycleHooks{
DefaultLoggingHook(p.Logger),
- {
- PreCreates: []ContainerRequestHook{
- func(ctx context.Context, req ContainerRequest) error {
- return p.preCreateContainerHook(ctx, req, dockerInput, hostConfig, networkingConfig)
- },
- },
- PostCreates: []ContainerHook{
- // copy files to container after it's created
- func(ctx context.Context, c Container) error {
- for _, f := range req.Files {
- err := c.CopyFileToContainer(ctx, f.HostFilePath, f.ContainerFilePath, f.FileMode)
- if err != nil {
- return fmt.Errorf("can't copy %s to container: %w", f.HostFilePath, err)
- }
- }
-
- return nil
- },
- },
- PostStarts: []ContainerHook{
- // first post-start hook is to produce logs and start log consumers
- func(ctx context.Context, c Container) error {
- dockerContainer := c.(*DockerContainer)
-
- logConsumerConfig := req.LogConsumerCfg
- if logConsumerConfig == nil {
- return nil
- }
-
- for _, consumer := range logConsumerConfig.Consumers {
- dockerContainer.followOutput(consumer)
- }
-
- if len(logConsumerConfig.Consumers) > 0 {
- return dockerContainer.startLogProduction(ctx, logConsumerConfig.Opts...)
- }
- return nil
- },
- // second post-start hook is to wait for the container to be ready
- func(ctx context.Context, c Container) error {
- dockerContainer := c.(*DockerContainer)
-
- // if a Wait Strategy has been specified, wait before returning
- if dockerContainer.WaitingFor != nil {
- dockerContainer.logger.Printf(
- "🚧 Waiting for container id %s image: %s. Waiting for: %+v",
- dockerContainer.ID[:12], dockerContainer.Image, dockerContainer.WaitingFor,
- )
- if err := dockerContainer.WaitingFor.WaitUntilReady(ctx, c); err != nil {
- return err
- }
- }
-
- dockerContainer.isRunning = true
-
- return nil
- },
- },
- PreTerminates: []ContainerHook{
- // first pre-terminate hook is to stop the log production
- func(ctx context.Context, c Container) error {
- logConsumerConfig := req.LogConsumerCfg
-
- if logConsumerConfig == nil {
- return nil
- }
- if len(logConsumerConfig.Consumers) == 0 {
- return nil
- }
-
- dockerContainer := c.(*DockerContainer)
-
- return dockerContainer.stopLogProduction()
- },
- },
- },
+ defaultPreCreateHook(ctx, p, req, dockerInput, hostConfig, networkingConfig),
+ defaultCopyFileToContainerHook(req.Files),
+ defaultLogConsumersHook(req.LogConsumerCfg),
+ defaultReadinessHook(),
}
- // always prepend default lifecycle hooks to user-defined hooks
- req.LifecycleHooks = append(defaultHooks, req.LifecycleHooks...)
+ req.LifecycleHooks = []ContainerLifecycleHooks{combineContainerHooks(defaultHooks, req.LifecycleHooks)}
err = req.creatingHook(ctx)
if err != nil {
diff --git a/docs/features/common_functional_options.md b/docs/features/common_functional_options.md
index b60dbd288d..ad617ed814 100644
--- a/docs/features/common_functional_options.md
+++ b/docs/features/common_functional_options.md
@@ -58,6 +58,19 @@ It also exports an `Executable` interface, defining the following methods:
You could use this feature to run a custom script, or to run a command that is not supported by the module right after the container is started.
+#### Ready Commands
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+Testcontainers exposes the `WithAfterReadyCommand(e ...Executable)` option to run arbitrary commands in the container right after it's ready, which happens when the defined wait strategies have finished with success.
+
+!!!info
+ To better understand how this feature works, please read the [Create containers: Lifecycle Hooks](/features/creating_container/#lifecycle-hooks) documentation.
+
+It leverages the `Executable` interface to represent the command and positional arguments to be executed in the container.
+
+You could use this feature to run a custom script, or to run a command that is not supported by the module right after the container is ready.
+
#### WithNetwork
- Since testcontainers-go :material-tag: v0.27.0
diff --git a/docs/features/creating_container.md b/docs/features/creating_container.md
index 0bbfa27fbc..decabc2a35 100644
--- a/docs/features/creating_container.md
+++ b/docs/features/creating_container.md
@@ -91,19 +91,32 @@ func TestIntegrationNginxLatestReturn(t *testing.T) {
_Testcontainers for Go_ allows you to define your own lifecycle hooks for better control over your containers. You just need to define functions that return an error and receive the Go context as first argument, and a `ContainerRequest` for the `Creating` hook, and a `Container` for the rest of them as second argument.
-You'll be able to pass multiple lifecycle hooks at the `ContainerRequest` as an array of `testcontainers.ContainerLifecycleHooks`, which will be processed one by one in the order they are passed.
-
-The `testcontainers.ContainerLifecycleHooks` struct defines the following lifecycle hooks, each of them backed by an array of functions representing the hooks:
+You'll be able to pass multiple lifecycle hooks at the `ContainerRequest` as an array of `testcontainers.ContainerLifecycleHooks`. The `testcontainers.ContainerLifecycleHooks` struct defines the following lifecycle hooks, each of them backed by an array of functions representing the hooks:
* `PreCreates` - hooks that are executed before the container is created
* `PostCreates` - hooks that are executed after the container is created
* `PreStarts` - hooks that are executed before the container is started
* `PostStarts` - hooks that are executed after the container is started
+* `PostReadies` - hooks that are executed after the container is ready
* `PreStops` - hooks that are executed before the container is stopped
* `PostStops` - hooks that are executed after the container is stopped
* `PreTerminates` - hooks that are executed before the container is terminated
* `PostTerminates` - hooks that are executed after the container is terminated
+_Testcontainers for Go_ defines some default lifecycle hooks that are always executed in a specific order with respect to the user-defined hooks. The order of execution is the following:
+
+1. default `pre` hooks.
+2. user-defined `pre` hooks.
+3. user-defined `post` hooks.
+4. default `post` hooks.
+
+Inside each group, the hooks will be executed in the order they were defined.
+
+!!!info
+ The default hooks are for logging (applied to all hooks), customising the Docker config (applied to the pre-create hook), copying files in to the container (applied to the post-create hook), adding log consumers (applied to the post-start and pre-terminate hooks), and running the wait strategies as a readiness check (applied to the post-start hook).
+
+It's important to notice that the `Readiness` of a container is defined by the wait strategies defined for the container. **This hook will be executed right after the `PostStarts` hook**. If you want to add your own readiness checks, you can do it by adding a `PostReadies` hook to the container request, which will execute your own readiness check after the default ones. That said, the `PostStarts` hooks don't warrant that the container is ready, so you should not rely on that.
+
In the following example, we are going to create a container using all the lifecycle hooks, all of them printing a message when any of the lifecycle hooks is called:
@@ -112,10 +125,11 @@ In the following example, we are going to create a container using all the lifec
#### Default Logging Hook
-_Testcontainers for Go_ comes with a default logging hook that will print a log message for each container lifecycle event. You can enable it by passing the `testcontainers.DefaultLoggingHook` option to the `ContainerRequest`, passing a reference to the container logger like this:
+_Testcontainers for Go_ comes with a default logging hook that will print a log message for each container lifecycle event, using the default logger. You can add your own logger by passing the `testcontainers.DefaultLoggingHook` option to the `ContainerRequest`, passing a reference to your preferred logger:
-[Extending container with life cycle hooks](../../lifecycle_test.go) inside_block:reqWithDefaultLogginHook
+[Use a custom logger for container hooks](../../lifecycle_test.go) inside_block:reqWithDefaultLogginHook
+[Custom Logger implementation](../../lifecycle_test.go) inside_block:customLoggerImplementation
### Advanced Settings
diff --git a/lifecycle.go b/lifecycle.go
index 7d4e4af78d..fc1b28e17e 100644
--- a/lifecycle.go
+++ b/lifecycle.go
@@ -2,6 +2,7 @@ package testcontainers
import (
"context"
+ "fmt"
"io"
"strings"
@@ -24,6 +25,7 @@ type ContainerRequestHook func(ctx context.Context, req ContainerRequest) error
// - Created
// - Starting
// - Started
+// - Readied
// - Stopping
// - Stopped
// - Terminating
@@ -39,12 +41,14 @@ type ContainerLifecycleHooks struct {
PostCreates []ContainerHook
PreStarts []ContainerHook
PostStarts []ContainerHook
+ PostReadies []ContainerHook
PreStops []ContainerHook
PostStops []ContainerHook
PreTerminates []ContainerHook
PostTerminates []ContainerHook
}
+// DefaultLoggingHook is a hook that will log the container lifecycle events
var DefaultLoggingHook = func(logger Logging) ContainerLifecycleHooks {
shortContainerID := func(c Container) string {
return c.GetContainerID()[:12]
@@ -75,6 +79,12 @@ var DefaultLoggingHook = func(logger Logging) ContainerLifecycleHooks {
return nil
},
},
+ PostReadies: []ContainerHook{
+ func(ctx context.Context, c Container) error {
+ logger.Printf("🔔 Container is ready: %s", shortContainerID(c))
+ return nil
+ },
+ },
PreStops: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("🐳 Stopping container: %s", shortContainerID(c))
@@ -83,7 +93,7 @@ var DefaultLoggingHook = func(logger Logging) ContainerLifecycleHooks {
},
PostStops: []ContainerHook{
func(ctx context.Context, c Container) error {
- logger.Printf("✋ Container stopped: %s", shortContainerID(c))
+ logger.Printf("✅ Container stopped: %s", shortContainerID(c))
return nil
},
},
@@ -102,6 +112,101 @@ var DefaultLoggingHook = func(logger Logging) ContainerLifecycleHooks {
}
}
+// defaultPreCreateHook is a hook that will apply the default configuration to the container
+var defaultPreCreateHook = func(ctx context.Context, p *DockerProvider, req ContainerRequest, dockerInput *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) ContainerLifecycleHooks {
+ return ContainerLifecycleHooks{
+ PreCreates: []ContainerRequestHook{
+ func(ctx context.Context, req ContainerRequest) error {
+ return p.preCreateContainerHook(ctx, req, dockerInput, hostConfig, networkingConfig)
+ },
+ },
+ }
+}
+
+// defaultCopyFileToContainerHook is a hook that will copy files to the container after it's created
+// but before it's started
+var defaultCopyFileToContainerHook = func(files []ContainerFile) ContainerLifecycleHooks {
+ return ContainerLifecycleHooks{
+ PostCreates: []ContainerHook{
+ // copy files to container after it's created
+ func(ctx context.Context, c Container) error {
+ for _, f := range files {
+ err := c.CopyFileToContainer(ctx, f.HostFilePath, f.ContainerFilePath, f.FileMode)
+ if err != nil {
+ return fmt.Errorf("can't copy %s to container: %w", f.HostFilePath, err)
+ }
+ }
+
+ return nil
+ },
+ },
+ }
+}
+
+// defaultLogConsumersHook is a hook that will start log consumers after the container is started
+var defaultLogConsumersHook = func(cfg *LogConsumerConfig) ContainerLifecycleHooks {
+ return ContainerLifecycleHooks{
+ PostStarts: []ContainerHook{
+ // first post-start hook is to produce logs and start log consumers
+ func(ctx context.Context, c Container) error {
+ dockerContainer := c.(*DockerContainer)
+
+ if cfg == nil {
+ return nil
+ }
+
+ for _, consumer := range cfg.Consumers {
+ dockerContainer.followOutput(consumer)
+ }
+
+ if len(cfg.Consumers) > 0 {
+ return dockerContainer.startLogProduction(ctx, cfg.Opts...)
+ }
+ return nil
+ },
+ },
+ PreTerminates: []ContainerHook{
+ // first pre-terminate hook is to stop the log production
+ func(ctx context.Context, c Container) error {
+ if cfg == nil || len(cfg.Consumers) == 0 {
+ return nil
+ }
+
+ dockerContainer := c.(*DockerContainer)
+
+ return dockerContainer.stopLogProduction()
+ },
+ },
+ }
+}
+
+// defaultReadinessHook is a hook that will wait for the container to be ready
+var defaultReadinessHook = func() ContainerLifecycleHooks {
+ return ContainerLifecycleHooks{
+ PostStarts: []ContainerHook{
+ // wait for the container to be ready
+ func(ctx context.Context, c Container) error {
+ dockerContainer := c.(*DockerContainer)
+
+ // if a Wait Strategy has been specified, wait before returning
+ if dockerContainer.WaitingFor != nil {
+ dockerContainer.logger.Printf(
+ "🚧 Waiting for container id %s image: %s. Waiting for: %+v",
+ dockerContainer.ID[:12], dockerContainer.Image, dockerContainer.WaitingFor,
+ )
+ if err := dockerContainer.WaitingFor.WaitUntilReady(ctx, c); err != nil {
+ return err
+ }
+ }
+
+ dockerContainer.isRunning = true
+
+ return nil
+ },
+ },
+ }
+}
+
// creatingHook is a hook that will be called before a container is created.
func (req ContainerRequest) creatingHook(ctx context.Context) error {
for _, lifecycleHooks := range req.LifecycleHooks {
@@ -152,6 +257,19 @@ func (c *DockerContainer) startedHook(ctx context.Context) error {
return nil
}
+// readiedHook is a hook that will be called after a container is ready
+func (c *DockerContainer) readiedHook(ctx context.Context) error {
+ for _, lifecycleHooks := range c.lifecycleHooks {
+ err := containerHookFn(ctx, lifecycleHooks.PostReadies)(c)
+ if err != nil {
+ c.printLogs(ctx, err)
+ return err
+ }
+ }
+
+ return nil
+}
+
// printLogs is a helper function that will print the logs of a Docker container
// We are going to use this helper function to inform the user of the logs when an error occurs
func (c *DockerContainer) printLogs(ctx context.Context, cause error) {
@@ -260,6 +378,11 @@ func (c ContainerLifecycleHooks) Started(ctx context.Context) func(container Con
return containerHookFn(ctx, c.PostStarts)
}
+// Readied is a hook that will be called after a container is ready
+func (c ContainerLifecycleHooks) Readied(ctx context.Context) func(container Container) error {
+ return containerHookFn(ctx, c.PostReadies)
+}
+
// Stopping is a hook that will be called before a container is stopped
func (c ContainerLifecycleHooks) Stopping(ctx context.Context) func(container Container) error {
return containerHookFn(ctx, c.PreStops)
@@ -352,6 +475,67 @@ func (p *DockerProvider) preCreateContainerHook(ctx context.Context, req Contain
return nil
}
+// combineContainerHooks it returns just one ContainerLifecycle hook, as the result of combining
+// the default hooks with the user-defined hooks. The function will loop over all the default hooks,
+// storing each of the hooks in a slice, and then it will loop over all the user-defined hooks,
+// appending or prepending them to the slice of hooks. The order of hooks is the following:
+// - for Pre-hooks, always run the default hooks first, then append the user-defined hooks
+// - for Post-hooks, always run the user-defined hooks first, then the default hooks
+func combineContainerHooks(defaultHooks, userDefinedHooks []ContainerLifecycleHooks) ContainerLifecycleHooks {
+ preCreates := []ContainerRequestHook{}
+ postCreates := []ContainerHook{}
+ preStarts := []ContainerHook{}
+ postStarts := []ContainerHook{}
+ postReadies := []ContainerHook{}
+ preStops := []ContainerHook{}
+ postStops := []ContainerHook{}
+ preTerminates := []ContainerHook{}
+ postTerminates := []ContainerHook{}
+
+ for _, defaultHook := range defaultHooks {
+ preCreates = append(preCreates, defaultHook.PreCreates...)
+ preStarts = append(preStarts, defaultHook.PreStarts...)
+ preStops = append(preStops, defaultHook.PreStops...)
+ preTerminates = append(preTerminates, defaultHook.PreTerminates...)
+ }
+
+ // append the user-defined hooks after the default pre-hooks
+ // and because the post hooks are still empty, the user-defined post-hooks
+ // will be the first ones to be executed
+ for _, userDefinedHook := range userDefinedHooks {
+ preCreates = append(preCreates, userDefinedHook.PreCreates...)
+ postCreates = append(postCreates, userDefinedHook.PostCreates...)
+ preStarts = append(preStarts, userDefinedHook.PreStarts...)
+ postStarts = append(postStarts, userDefinedHook.PostStarts...)
+ postReadies = append(postReadies, userDefinedHook.PostReadies...)
+ preStops = append(preStops, userDefinedHook.PreStops...)
+ postStops = append(postStops, userDefinedHook.PostStops...)
+ preTerminates = append(preTerminates, userDefinedHook.PreTerminates...)
+ postTerminates = append(postTerminates, userDefinedHook.PostTerminates...)
+ }
+
+ // finally, append the default post-hooks
+ for _, defaultHook := range defaultHooks {
+ postCreates = append(postCreates, defaultHook.PostCreates...)
+ postStarts = append(postStarts, defaultHook.PostStarts...)
+ postReadies = append(postReadies, defaultHook.PostReadies...)
+ postStops = append(postStops, defaultHook.PostStops...)
+ postTerminates = append(postTerminates, defaultHook.PostTerminates...)
+ }
+
+ return ContainerLifecycleHooks{
+ PreCreates: preCreates,
+ PostCreates: postCreates,
+ PreStarts: preStarts,
+ PostStarts: postStarts,
+ PostReadies: postReadies,
+ PreStops: preStops,
+ PostStops: postStops,
+ PreTerminates: preTerminates,
+ PostTerminates: postTerminates,
+ }
+}
+
func mergePortBindings(configPortMap, exposedPortMap nat.PortMap, exposedPorts []string) nat.PortMap {
if exposedPortMap == nil {
exposedPortMap = make(map[nat.Port][]nat.PortBinding)
diff --git a/lifecycle_test.go b/lifecycle_test.go
index ac8d8e1ee3..6316df739e 100644
--- a/lifecycle_test.go
+++ b/lifecycle_test.go
@@ -489,6 +489,16 @@ func TestLifecycleHooks(t *testing.T) {
return nil
},
},
+ PostReadies: []ContainerHook{
+ func(ctx context.Context, c Container) error {
+ prints = append(prints, fmt.Sprintf("post-ready hook 1: %#v", c))
+ return nil
+ },
+ func(ctx context.Context, c Container) error {
+ prints = append(prints, fmt.Sprintf("post-ready hook 2: %#v", c))
+ return nil
+ },
+ },
PreStops: []ContainerHook{
func(ctx context.Context, c Container) error {
prints = append(prints, fmt.Sprintf("pre-stop hook 1: %#v", c))
@@ -556,11 +566,12 @@ func TestLifecycleHooks(t *testing.T) {
err = c.Terminate(ctx)
require.NoError(t, err)
- lifecycleHooksIsHonouredFn(t, ctx, c, prints)
+ lifecycleHooksIsHonouredFn(t, ctx, prints)
})
}
}
+// customLoggerImplementation {
type inMemoryLogger struct {
data []string
}
@@ -569,6 +580,8 @@ func (l *inMemoryLogger) Printf(format string, args ...interface{}) {
l.data = append(l.data, fmt.Sprintf(format, args...))
}
+// }
+
func TestLifecycleHooks_WithDefaultLogger(t *testing.T) {
ctx := context.Background()
@@ -600,7 +613,140 @@ func TestLifecycleHooks_WithDefaultLogger(t *testing.T) {
err = c.Terminate(ctx)
require.NoError(t, err)
- require.Len(t, dl.data, 10)
+ require.Len(t, dl.data, 12)
+}
+
+func TestCombineLifecycleHooks(t *testing.T) {
+ prints := []string{}
+
+ preCreateFunc := func(prefix string, hook string, lifecycleID int, hookID int) func(ctx context.Context, req ContainerRequest) error {
+ return func(ctx context.Context, _ ContainerRequest) error {
+ prints = append(prints, fmt.Sprintf("[%s] pre-%s hook %d.%d", prefix, hook, lifecycleID, hookID))
+ return nil
+ }
+ }
+ hookFunc := func(prefix string, hookType string, hook string, lifecycleID int, hookID int) func(ctx context.Context, c Container) error {
+ return func(ctx context.Context, _ Container) error {
+ prints = append(prints, fmt.Sprintf("[%s] %s-%s hook %d.%d", prefix, hookType, hook, lifecycleID, hookID))
+ return nil
+ }
+ }
+ preFunc := func(prefix string, hook string, lifecycleID int, hookID int) func(ctx context.Context, c Container) error {
+ return hookFunc(prefix, "pre", hook, lifecycleID, hookID)
+ }
+ postFunc := func(prefix string, hook string, lifecycleID int, hookID int) func(ctx context.Context, c Container) error {
+ return hookFunc(prefix, "post", hook, lifecycleID, hookID)
+ }
+
+ lifecycleHookFunc := func(prefix string, lifecycleID int) ContainerLifecycleHooks {
+ return ContainerLifecycleHooks{
+ PreCreates: []ContainerRequestHook{preCreateFunc(prefix, "create", lifecycleID, 1), preCreateFunc(prefix, "create", lifecycleID, 2)},
+ PostCreates: []ContainerHook{postFunc(prefix, "create", lifecycleID, 1), postFunc(prefix, "create", lifecycleID, 2)},
+ PreStarts: []ContainerHook{preFunc(prefix, "start", lifecycleID, 1), preFunc(prefix, "start", lifecycleID, 2)},
+ PostStarts: []ContainerHook{postFunc(prefix, "start", lifecycleID, 1), postFunc(prefix, "start", lifecycleID, 2)},
+ PostReadies: []ContainerHook{postFunc(prefix, "ready", lifecycleID, 1), postFunc(prefix, "ready", lifecycleID, 2)},
+ PreStops: []ContainerHook{preFunc(prefix, "stop", lifecycleID, 1), preFunc(prefix, "stop", lifecycleID, 2)},
+ PostStops: []ContainerHook{postFunc(prefix, "stop", lifecycleID, 1), postFunc(prefix, "stop", lifecycleID, 2)},
+ PreTerminates: []ContainerHook{preFunc(prefix, "terminate", lifecycleID, 1), preFunc(prefix, "terminate", lifecycleID, 2)},
+ PostTerminates: []ContainerHook{postFunc(prefix, "terminate", lifecycleID, 1), postFunc(prefix, "terminate", lifecycleID, 2)},
+ }
+ }
+
+ defaultHooks := []ContainerLifecycleHooks{lifecycleHookFunc("default", 1), lifecycleHookFunc("default", 2)}
+ userDefinedHooks := []ContainerLifecycleHooks{lifecycleHookFunc("user-defined", 1), lifecycleHookFunc("user-defined", 2), lifecycleHookFunc("user-defined", 3)}
+
+ hooks := combineContainerHooks(defaultHooks, userDefinedHooks)
+
+ // call all the hooks in the right order, honouring the lifecycle
+
+ req := ContainerRequest{}
+ err := hooks.Creating(context.Background())(req)
+ require.NoError(t, err)
+
+ c := &DockerContainer{}
+
+ err = hooks.Created(context.Background())(c)
+ require.NoError(t, err)
+ err = hooks.Starting(context.Background())(c)
+ require.NoError(t, err)
+ err = hooks.Started(context.Background())(c)
+ require.NoError(t, err)
+ err = hooks.Readied(context.Background())(c)
+ require.NoError(t, err)
+ err = hooks.Stopping(context.Background())(c)
+ require.NoError(t, err)
+ err = hooks.Stopped(context.Background())(c)
+ require.NoError(t, err)
+ err = hooks.Terminating(context.Background())(c)
+ require.NoError(t, err)
+ err = hooks.Terminated(context.Background())(c)
+ require.NoError(t, err)
+
+ // assertions
+
+ // There are 2 default container lifecycle hooks and 3 user-defined container lifecycle hooks.
+ // Each lifecycle hook has 2 pre-create hooks and 2 post-create hooks.
+ // That results in 16 hooks per lifecycle (8 defaults + 12 user-defined = 20)
+
+ // There are 5 lifecycles (create, start, ready, stop, terminate),
+ // but ready has only half of the hooks (it only has post), so we have 90 hooks in total.
+ assert.Len(t, prints, 90)
+
+ // The order of the hooks is:
+ // - pre-X hooks: first default (2*2), then user-defined (3*2)
+ // - post-X hooks: first user-defined (3*2), then default (2*2)
+
+ for i := 0; i < 5; i++ {
+ var hookType string
+ // this is the particular order of execution for the hooks
+ switch i {
+ case 0:
+ hookType = "create"
+ case 1:
+ hookType = "start"
+ case 2:
+ hookType = "ready"
+ case 3:
+ hookType = "stop"
+ case 4:
+ hookType = "terminate"
+ }
+
+ initialIndex := i * 20
+ if i >= 2 {
+ initialIndex -= 10
+ }
+
+ if hookType != "ready" {
+ // default pre-hooks: 4 hooks
+ assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 1.1", hookType), prints[initialIndex])
+ assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 1.2", hookType), prints[initialIndex+1])
+ assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 2.1", hookType), prints[initialIndex+2])
+ assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 2.2", hookType), prints[initialIndex+3])
+
+ // user-defined pre-hooks: 6 hooks
+ assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 1.1", hookType), prints[initialIndex+4])
+ assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 1.2", hookType), prints[initialIndex+5])
+ assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 2.1", hookType), prints[initialIndex+6])
+ assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 2.2", hookType), prints[initialIndex+7])
+ assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 3.1", hookType), prints[initialIndex+8])
+ assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 3.2", hookType), prints[initialIndex+9])
+ }
+
+ // user-defined post-hooks: 6 hooks
+ assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 1.1", hookType), prints[initialIndex+10])
+ assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 1.2", hookType), prints[initialIndex+11])
+ assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 2.1", hookType), prints[initialIndex+12])
+ assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 2.2", hookType), prints[initialIndex+13])
+ assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 3.1", hookType), prints[initialIndex+14])
+ assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 3.2", hookType), prints[initialIndex+15])
+
+ // default post-hooks: 4 hooks
+ assert.Equal(t, fmt.Sprintf("[default] post-%s hook 1.1", hookType), prints[initialIndex+16])
+ assert.Equal(t, fmt.Sprintf("[default] post-%s hook 1.2", hookType), prints[initialIndex+17])
+ assert.Equal(t, fmt.Sprintf("[default] post-%s hook 2.1", hookType), prints[initialIndex+18])
+ assert.Equal(t, fmt.Sprintf("[default] post-%s hook 2.2", hookType), prints[initialIndex+19])
+ }
}
func TestLifecycleHooks_WithMultipleHooks(t *testing.T) {
@@ -633,7 +779,7 @@ func TestLifecycleHooks_WithMultipleHooks(t *testing.T) {
err = c.Terminate(ctx)
require.NoError(t, err)
- require.Len(t, dl.data, 20)
+ require.Len(t, dl.data, 24)
}
type linesTestLogger struct {
@@ -646,11 +792,6 @@ func (l *linesTestLogger) Printf(format string, args ...interface{}) {
func TestPrintContainerLogsOnError(t *testing.T) {
ctx := context.Background()
- client, err := NewDockerClientWithOpts(ctx)
- if err != nil {
- t.Fatal(err)
- }
- defer client.Close()
req := ContainerRequest{
Image: "docker.io/alpine",
@@ -705,8 +846,8 @@ func TestPrintContainerLogsOnError(t *testing.T) {
}
}
-func lifecycleHooksIsHonouredFn(t *testing.T, ctx context.Context, container Container, prints []string) {
- require.Len(t, prints, 20)
+func lifecycleHooksIsHonouredFn(t *testing.T, ctx context.Context, prints []string) {
+ require.Len(t, prints, 24)
assert.True(t, strings.HasPrefix(prints[0], "pre-create hook 1: "))
assert.True(t, strings.HasPrefix(prints[1], "pre-create hook 2: "))
@@ -720,21 +861,27 @@ func lifecycleHooksIsHonouredFn(t *testing.T, ctx context.Context, container Con
assert.True(t, strings.HasPrefix(prints[6], "post-start hook 1: "))
assert.True(t, strings.HasPrefix(prints[7], "post-start hook 2: "))
- assert.True(t, strings.HasPrefix(prints[8], "pre-stop hook 1: "))
- assert.True(t, strings.HasPrefix(prints[9], "pre-stop hook 2: "))
+ assert.True(t, strings.HasPrefix(prints[8], "post-ready hook 1: "))
+ assert.True(t, strings.HasPrefix(prints[9], "post-ready hook 2: "))
+
+ assert.True(t, strings.HasPrefix(prints[10], "pre-stop hook 1: "))
+ assert.True(t, strings.HasPrefix(prints[11], "pre-stop hook 2: "))
+
+ assert.True(t, strings.HasPrefix(prints[12], "post-stop hook 1: "))
+ assert.True(t, strings.HasPrefix(prints[13], "post-stop hook 2: "))
- assert.True(t, strings.HasPrefix(prints[10], "post-stop hook 1: "))
- assert.True(t, strings.HasPrefix(prints[11], "post-stop hook 2: "))
+ assert.True(t, strings.HasPrefix(prints[14], "pre-start hook 1: "))
+ assert.True(t, strings.HasPrefix(prints[15], "pre-start hook 2: "))
- assert.True(t, strings.HasPrefix(prints[12], "pre-start hook 1: "))
- assert.True(t, strings.HasPrefix(prints[13], "pre-start hook 2: "))
+ assert.True(t, strings.HasPrefix(prints[16], "post-start hook 1: "))
+ assert.True(t, strings.HasPrefix(prints[17], "post-start hook 2: "))
- assert.True(t, strings.HasPrefix(prints[14], "post-start hook 1: "))
- assert.True(t, strings.HasPrefix(prints[15], "post-start hook 2: "))
+ assert.True(t, strings.HasPrefix(prints[18], "post-ready hook 1: "))
+ assert.True(t, strings.HasPrefix(prints[19], "post-ready hook 2: "))
- assert.True(t, strings.HasPrefix(prints[16], "pre-terminate hook 1: "))
- assert.True(t, strings.HasPrefix(prints[17], "pre-terminate hook 2: "))
+ assert.True(t, strings.HasPrefix(prints[20], "pre-terminate hook 1: "))
+ assert.True(t, strings.HasPrefix(prints[21], "pre-terminate hook 2: "))
- assert.True(t, strings.HasPrefix(prints[18], "post-terminate hook 1: "))
- assert.True(t, strings.HasPrefix(prints[19], "post-terminate hook 2: "))
+ assert.True(t, strings.HasPrefix(prints[22], "post-terminate hook 1: "))
+ assert.True(t, strings.HasPrefix(prints[23], "post-terminate hook 2: "))
}
diff --git a/modules/cassandra/cassandra.go b/modules/cassandra/cassandra.go
index 8956927ae5..c37c10d90d 100644
--- a/modules/cassandra/cassandra.go
+++ b/modules/cassandra/cassandra.go
@@ -55,6 +55,7 @@ func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption {
func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) {
var initScripts []testcontainers.ContainerFile
+ var execs []testcontainers.Executable
for _, script := range scripts {
cf := testcontainers.ContainerFile{
HostFilePath: script,
@@ -63,9 +64,11 @@ func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption {
}
initScripts = append(initScripts, cf)
- testcontainers.WithStartupCommand(initScript{File: cf.ContainerFilePath})(req)
+ execs = append(execs, initScript{File: cf.ContainerFilePath})
}
+
req.Files = append(req.Files, initScripts...)
+ testcontainers.WithAfterReadyCommand(execs...)(req)
}
}
diff --git a/modules/elasticsearch/elasticsearch.go b/modules/elasticsearch/elasticsearch.go
index 9dfd95904b..79a364fd01 100644
--- a/modules/elasticsearch/elasticsearch.go
+++ b/modules/elasticsearch/elasticsearch.go
@@ -52,6 +52,7 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize
{
// the container needs a post create hook to set the default JVM options in a file
PostCreates: []testcontainers.ContainerHook{},
+ PostReadies: []testcontainers.ContainerHook{},
},
},
},
@@ -126,9 +127,9 @@ func configureAddress(ctx context.Context, c *ElasticsearchContainer) (string, e
// The certificate is only available since version 8, and will be located in a well-known location.
func configureCertificate(settings *Options, req *testcontainers.GenericContainerRequest) error {
if isAtLeastVersion(req.Image, 8) {
- // The container needs a post start hook to copy the certificate from the container to the host.
+ // The container needs a post ready hook to copy the certificate from the container to the host.
// This certificate is only available since version 8
- req.LifecycleHooks[0].PostStarts = append(req.LifecycleHooks[0].PostStarts,
+ req.LifecycleHooks[0].PostReadies = append(req.LifecycleHooks[0].PostReadies,
func(ctx context.Context, container testcontainers.Container) error {
const defaultCaCertPath = "/usr/share/elasticsearch/config/certs/http_ca.crt"
diff --git a/modules/openldap/openldap.go b/modules/openldap/openldap.go
index 03d8b4e379..e29658c639 100644
--- a/modules/openldap/openldap.go
+++ b/modules/openldap/openldap.go
@@ -94,7 +94,7 @@ func WithInitialLdif(ldif string) testcontainers.CustomizeRequestOption {
})
req.LifecycleHooks = append(req.LifecycleHooks, testcontainers.ContainerLifecycleHooks{
- PostStarts: []testcontainers.ContainerHook{
+ PostReadies: []testcontainers.ContainerHook{
func(ctx context.Context, container testcontainers.Container) error {
username := req.Env["LDAP_ADMIN_USERNAME"]
rootDn := req.Env["LDAP_ROOT"]
@@ -128,6 +128,11 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize
wait.ForLog("** Starting slapd **"),
wait.ForListeningPort("1389/tcp"),
),
+ LifecycleHooks: []testcontainers.ContainerLifecycleHooks{
+ {
+ PostReadies: []testcontainers.ContainerHook{},
+ },
+ },
}
genericContainerReq := testcontainers.GenericContainerRequest{
diff --git a/modules/rabbitmq/examples_test.go b/modules/rabbitmq/examples_test.go
index f24bae0169..a6fc829424 100644
--- a/modules/rabbitmq/examples_test.go
+++ b/modules/rabbitmq/examples_test.go
@@ -131,7 +131,7 @@ func ExampleRunContainer_withPlugins() {
testcontainers.WithImage("rabbitmq:3.7.25-management-alpine"),
// Multiple test implementations of the Executable interface, specific to RabbitMQ, exist in the types_test.go file.
// Please refer to them for more examples.
- testcontainers.WithStartupCommand(
+ testcontainers.WithAfterReadyCommand(
testcontainers.NewRawCommand([]string{"rabbitmq_shovel"}),
testcontainers.NewRawCommand([]string{"rabbitmq_random_exchange"}),
),
diff --git a/modules/rabbitmq/rabbitmq_test.go b/modules/rabbitmq/rabbitmq_test.go
index 6636da3061..0c85c66607 100644
--- a/modules/rabbitmq/rabbitmq_test.go
+++ b/modules/rabbitmq/rabbitmq_test.go
@@ -48,17 +48,17 @@ func TestRunContainer_withAllSettings(t *testing.T) {
rabbitmqContainer, err := rabbitmq.RunContainer(ctx,
testcontainers.WithImage("rabbitmq:3.12.11-management-alpine"),
// addVirtualHosts {
- testcontainers.WithStartupCommand(VirtualHost{Name: "vhost1"}),
- testcontainers.WithStartupCommand(VirtualHostLimit{VHost: "vhost1", Name: "max-connections", Value: 1}),
- testcontainers.WithStartupCommand(VirtualHost{Name: "vhost2", Tracing: true}),
+ testcontainers.WithAfterReadyCommand(VirtualHost{Name: "vhost1"}),
+ testcontainers.WithAfterReadyCommand(VirtualHostLimit{VHost: "vhost1", Name: "max-connections", Value: 1}),
+ testcontainers.WithAfterReadyCommand(VirtualHost{Name: "vhost2", Tracing: true}),
// }
// addExchanges {
- testcontainers.WithStartupCommand(Exchange{Name: "direct-exchange", Type: "direct"}),
- testcontainers.WithStartupCommand(Exchange{
+ testcontainers.WithAfterReadyCommand(Exchange{Name: "direct-exchange", Type: "direct"}),
+ testcontainers.WithAfterReadyCommand(Exchange{
Name: "topic-exchange",
Type: "topic",
}),
- testcontainers.WithStartupCommand(Exchange{
+ testcontainers.WithAfterReadyCommand(Exchange{
VHost: "vhost1",
Name: "topic-exchange-2",
Type: "topic",
@@ -67,12 +67,12 @@ func TestRunContainer_withAllSettings(t *testing.T) {
Durable: true,
Args: map[string]interface{}{},
}),
- testcontainers.WithStartupCommand(Exchange{
+ testcontainers.WithAfterReadyCommand(Exchange{
VHost: "vhost2",
Name: "topic-exchange-3",
Type: "topic",
}),
- testcontainers.WithStartupCommand(Exchange{
+ testcontainers.WithAfterReadyCommand(Exchange{
Name: "topic-exchange-4",
Type: "topic",
AutoDelete: false,
@@ -82,26 +82,26 @@ func TestRunContainer_withAllSettings(t *testing.T) {
}),
// }
// addQueues {
- testcontainers.WithStartupCommand(Queue{Name: "queue1"}),
- testcontainers.WithStartupCommand(Queue{
+ testcontainers.WithAfterReadyCommand(Queue{Name: "queue1"}),
+ testcontainers.WithAfterReadyCommand(Queue{
Name: "queue2",
AutoDelete: true,
Durable: false,
Args: map[string]interface{}{"x-message-ttl": 1000},
}),
- testcontainers.WithStartupCommand(Queue{
+ testcontainers.WithAfterReadyCommand(Queue{
VHost: "vhost1",
Name: "queue3",
AutoDelete: true,
Durable: false,
Args: map[string]interface{}{"x-message-ttl": 1000},
}),
- testcontainers.WithStartupCommand(Queue{VHost: "vhost2", Name: "queue4"}),
+ testcontainers.WithAfterReadyCommand(Queue{VHost: "vhost2", Name: "queue4"}),
// }
// addBindings {
- testcontainers.WithStartupCommand(NewBinding("direct-exchange", "queue1")),
- testcontainers.WithStartupCommand(NewBindingWithVHost("vhost1", "topic-exchange-2", "queue3")),
- testcontainers.WithStartupCommand(Binding{
+ testcontainers.WithAfterReadyCommand(NewBinding("direct-exchange", "queue1")),
+ testcontainers.WithAfterReadyCommand(NewBindingWithVHost("vhost1", "topic-exchange-2", "queue3")),
+ testcontainers.WithAfterReadyCommand(Binding{
VHost: "vhost2",
Source: "topic-exchange-3",
Destination: "queue4",
@@ -111,33 +111,33 @@ func TestRunContainer_withAllSettings(t *testing.T) {
}),
// }
// addUsers {
- testcontainers.WithStartupCommand(User{
+ testcontainers.WithAfterReadyCommand(User{
Name: "user1",
Password: "password1",
}),
- testcontainers.WithStartupCommand(User{
+ testcontainers.WithAfterReadyCommand(User{
Name: "user2",
Password: "password2",
Tags: []string{"administrator"},
}),
// }
// addPermissions {
- testcontainers.WithStartupCommand(NewPermission("vhost1", "user1", ".*", ".*", ".*")),
+ testcontainers.WithAfterReadyCommand(NewPermission("vhost1", "user1", ".*", ".*", ".*")),
// }
// addPolicies {
- testcontainers.WithStartupCommand(Policy{
+ testcontainers.WithAfterReadyCommand(Policy{
Name: "max length policy",
Pattern: "^dog",
Definition: map[string]interface{}{"max-length": 1},
Priority: 1,
ApplyTo: "queues",
}),
- testcontainers.WithStartupCommand(Policy{
+ testcontainers.WithAfterReadyCommand(Policy{
Name: "alternate exchange policy",
Pattern: "^direct-exchange",
Definition: map[string]interface{}{"alternate-exchange": "amq.direct"},
}),
- testcontainers.WithStartupCommand(Policy{
+ testcontainers.WithAfterReadyCommand(Policy{
VHost: "vhost2",
Name: "ha-all",
Pattern: ".*",
@@ -146,7 +146,7 @@ func TestRunContainer_withAllSettings(t *testing.T) {
"ha-sync-mode": "automatic",
},
}),
- testcontainers.WithStartupCommand(OperatorPolicy{
+ testcontainers.WithAfterReadyCommand(OperatorPolicy{
Name: "operator policy 1",
Pattern: "^queue1",
Definition: map[string]interface{}{"message-ttl": 1000},
@@ -155,7 +155,7 @@ func TestRunContainer_withAllSettings(t *testing.T) {
}),
// }
// enablePlugins {
- testcontainers.WithStartupCommand(Plugin{Name: "rabbitmq_shovel"}, Plugin{Name: "rabbitmq_random_exchange"}),
+ testcontainers.WithAfterReadyCommand(Plugin{Name: "rabbitmq_shovel"}, Plugin{Name: "rabbitmq_random_exchange"}),
// }
)
if err != nil {
diff --git a/options.go b/options.go
index 40bf671bcc..13711e8b56 100644
--- a/options.go
+++ b/options.go
@@ -201,6 +201,28 @@ func WithStartupCommand(execs ...Executable) CustomizeRequestOption {
}
}
+// WithAfterReadyCommand will execute the command representation of each Executable into the container.
+// It will leverage the container lifecycle hooks to call the command right after the container
+// is ready.
+func WithAfterReadyCommand(execs ...Executable) CustomizeRequestOption {
+ return func(req *GenericContainerRequest) {
+ postReadiesHook := []ContainerHook{}
+
+ for _, exec := range execs {
+ execFn := func(ctx context.Context, c Container) error {
+ _, _, err := c.Exec(ctx, exec.AsCommand(), exec.Options()...)
+ return err
+ }
+
+ postReadiesHook = append(postReadiesHook, execFn)
+ }
+
+ req.LifecycleHooks = append(req.LifecycleHooks, ContainerLifecycleHooks{
+ PostReadies: postReadiesHook,
+ })
+ }
+}
+
// WithWaitStrategy sets the wait strategy for a container, using 60 seconds as deadline
func WithWaitStrategy(strategies ...wait.Strategy) CustomizeRequestOption {
return WithWaitStrategyAndDeadline(60*time.Second, strategies...)
diff --git a/options_test.go b/options_test.go
index f402d42b7c..e13642ac09 100644
--- a/options_test.go
+++ b/options_test.go
@@ -131,3 +131,34 @@ func TestWithStartupCommand(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "/tmp/.testcontainers\n", string(content))
}
+
+func TestWithAfterReadyCommand(t *testing.T) {
+ req := testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ Image: "alpine",
+ Entrypoint: []string{"tail", "-f", "/dev/null"},
+ },
+ Started: true,
+ }
+
+ testExec := testcontainers.NewRawCommand([]string{"touch", "/tmp/.testcontainers"})
+
+ testcontainers.WithAfterReadyCommand(testExec)(&req)
+
+ assert.Len(t, req.LifecycleHooks, 1)
+ assert.Len(t, req.LifecycleHooks[0].PostReadies, 1)
+
+ c, err := testcontainers.GenericContainer(context.Background(), req)
+ require.NoError(t, err)
+ defer func() {
+ err = c.Terminate(context.Background())
+ require.NoError(t, err)
+ }()
+
+ _, reader, err := c.Exec(context.Background(), []string{"ls", "/tmp/.testcontainers"}, exec.Multiplexed())
+ require.NoError(t, err)
+
+ content, err := io.ReadAll(reader)
+ require.NoError(t, err)
+ assert.Equal(t, "/tmp/.testcontainers\n", string(content))
+}