Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: include PostReady hook, defining proper execution order for container lifecycle hooks #1922

Merged
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5afe94d
chore: simplify test method
mdelapenya Nov 15, 2023
84446b6
feat: define execution order for default hooks and user-defined hooks
mdelapenya Nov 16, 2023
bfb00cf
chore: extract default hooks to variables
mdelapenya Nov 16, 2023
ef7dccc
docs: include execution order in docs
mdelapenya Nov 16, 2023
bdbe5ec
docs: update docs for default logging hook
mdelapenya Nov 16, 2023
0aab31f
chore: use ✅ consistently in post hooks
mdelapenya Nov 16, 2023
8c5b4ce
fix: lint
mdelapenya Nov 16, 2023
8229b65
feat: define Readiness hooks
mdelapenya Nov 17, 2023
5fb36d4
fix: move cassandra's startup commands to the post-ready hook
mdelapenya Nov 17, 2023
d6887e9
feat: add a WithReadyCommand that happens after the container is ready
mdelapenya Nov 17, 2023
6643460
chore: move rabbitmq post-starts to post-ready
mdelapenya Nov 17, 2023
855df62
chore: move elasticsearch post-starts to post-ready
mdelapenya Nov 17, 2023
38cca20
chore: simplify using new functional option
mdelapenya Nov 17, 2023
11e7c22
chore: remove PreRedies
mdelapenya Nov 17, 2023
f91da5a
Merge branch 'main' into user-defined-lifecycle-hooks
mdelapenya Nov 17, 2023
6df03ab
chore: apply exec options to ready commands
mdelapenya Nov 17, 2023
fcceaea
chore: add unit test for ready command
mdelapenya Nov 17, 2023
681dea1
fix: lint
mdelapenya Nov 20, 2023
38770a4
chore: use WithAfterReadyCommand
mdelapenya Nov 20, 2023
dc310c8
docs: reword
mdelapenya Nov 20, 2023
d2a2a88
Merge branch 'main' into user-defined-lifecycle-hooks
mdelapenya Nov 24, 2023
c0bdd75
Merge branch 'main' into user-defined-lifecycle-hooks
mdelapenya Dec 1, 2023
497c226
Merge branch 'main' into user-defined-lifecycle-hooks
mdelapenya Dec 12, 2023
1d3e1d1
Merge branch 'main' into user-defined-lifecycle-hooks
mdelapenya Dec 18, 2023
d8316b7
docs: remove extra spaces
mdelapenya Dec 19, 2023
474ddea
Merge branch 'main' into user-defined-lifecycle-hooks
mdelapenya Jan 11, 2024
88e1961
Merge branch 'main' into user-defined-lifecycle-hooks
mdelapenya Jan 24, 2024
b051c90
fix: lint
mdelapenya Jan 24, 2024
3fa1e37
Merge branch 'main' into user-defined-lifecycle-hooks
mdelapenya Feb 14, 2024
dcb7246
chore: update postreadies in openLDAP module
mdelapenya Feb 14, 2024
a0f21bf
docs: refine
mdelapenya Feb 14, 2024
8dd85e8
Merge branch 'main' into user-defined-lifecycle-hooks
mdelapenya Feb 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 23 additions & 43 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,26 @@ func (c *DockerContainer) Start(ctx context.Context) error {
return err
}

// if a Wait Strategy has been specified, wait before returning
if c.WaitingFor != nil {
c.logger.Printf(
"🚧 Waiting for container id %s image: %s. Waiting for: %+v",
c.ID[:12], c.Image, c.WaitingFor,
)
if err := c.WaitingFor.WaitUntilReady(ctx, c); err != nil {
c.printLogs(ctx, err)
return err
}
c.logger.Printf("✅ Finished waiting for container id %s.", c.ID[:12])
}

c.isRunning = true

err = c.readiedHook(ctx)
if err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -1056,51 +1076,11 @@ 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 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
},
},
},
defaultPreCreateHook(ctx, p, req, dockerInput, hostConfig, networkingConfig),
defaultCopyFileToContainerHook(req.Files),
}

// 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 @@ -40,6 +40,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
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved
* `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 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) and copying files in to the container (applied to the post-create 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
122 changes: 121 additions & 1 deletion lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package testcontainers

import (
"context"
"fmt"
"io"
"strings"

Expand All @@ -24,6 +25,7 @@ type ContainerRequestHook func(ctx context.Context, req ContainerRequest) error
// - Created
// - Starting
// - Started
// - Readied
// - Stopping
// - Stopped
// - Terminating
Expand All @@ -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]
Expand Down Expand Up @@ -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))
Expand All @@ -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
},
},
Expand All @@ -102,6 +112,37 @@ 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
},
},
}
}

// 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 {
Expand Down Expand Up @@ -152,6 +193,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) {
Expand Down Expand Up @@ -260,6 +314,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)
Expand Down Expand Up @@ -352,6 +411,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)
Expand Down
Loading
Loading