Skip to content

Commit

Permalink
chore: define proper execution order for container lifecycle hooks (#…
Browse files Browse the repository at this point in the history
…1922)

* chore: simplify test method

* feat: define execution order for default hooks and user-defined hooks

* chore: extract default hooks to variables

* docs: include execution order in docs

* docs: update docs for default logging hook

* chore: use ✅ consistently in post hooks

* fix: lint

* feat: define Readiness hooks

* fix: move cassandra's startup commands to the post-ready hook

* feat: add a WithReadyCommand that happens after the container is ready

* chore: move rabbitmq post-starts to post-ready

* chore: move elasticsearch post-starts to post-ready

* chore: simplify using new functional option

* chore: remove PreRedies

* chore: apply exec options to ready commands

* chore: add unit test for ready command

* fix: lint

* chore: use WithAfterReadyCommand

* docs: reword

* docs: remove extra spaces

* fix: lint

* chore: update postreadies in openLDAP module

* docs: refine
  • Loading branch information
mdelapenya authored Feb 15, 2024
1 parent 50fc8e7 commit c906e65
Show file tree
Hide file tree
Showing 12 changed files with 488 additions and 134 deletions.
90 changes: 12 additions & 78 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions docs/features/common_functional_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

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 <a href="https://github.com/testcontainers/testcontainers-go/releases/tag/v0.27.0"><span class="tc-version">:material-tag: v0.27.0</span></a>
Expand Down
24 changes: 19 additions & 5 deletions docs/features/creating_container.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<!--codeinclude-->
Expand All @@ -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:

<!--codeinclude-->
[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
<!--/codeinclude-->

### Advanced Settings
Expand Down
Loading

0 comments on commit c906e65

Please sign in to comment.