Skip to content

Commit

Permalink
chore: support for multiple lifecycles defined by the user (#1037)
Browse files Browse the repository at this point in the history
* chore: support for multiple container life cycles

* chore: define a default logging hook

It uses the container logger to print out the container lifecycle events

* chore: extract copying files to the postCreate hook

* fix: honour error message
  • Loading branch information
mdelapenya authored Apr 5, 2023
1 parent 13da621 commit e1539bc
Show file tree
Hide file tree
Showing 5 changed files with 446 additions and 204 deletions.
2 changes: 1 addition & 1 deletion container.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ type ContainerRequest struct {
ConfigModifier func(*container.Config) // Modifier for the config before container creation
HostConfigModifier func(*container.HostConfig) // Modifier for the host config before container creation
EnpointSettingsModifier func(map[string]*network.EndpointSettings) // Modifier for the network settings before container creation
LifecycleHooks ContainerLifecycleHooks // define hooks to be executed during container lifecycle
LifecycleHooks []ContainerLifecycleHooks // define hooks to be executed during container lifecycle
}

// containerOptions functional options for a container
Expand Down
111 changes: 53 additions & 58 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ type DockerContainer struct {
raw *types.ContainerJSON
stopProducer chan bool
logger Logging
lifecycleHooks ContainerLifecycleHooks
lifecycleHooks []ContainerLifecycleHooks
}

// SetLogger sets the logger for the container
Expand Down Expand Up @@ -188,15 +188,12 @@ func (c *DockerContainer) SessionID() string {

// Start will start an already created container
func (c *DockerContainer) Start(ctx context.Context) error {
if len(c.lifecycleHooks.PreStarts) > 0 {
err := c.lifecycleHooks.Starting(ctx)(c)
if err != nil {
return err
}
err := c.startingHook(ctx)
if err != nil {
return err
}

shortID := c.ID[:12]
c.logger.Printf("🐳 Starting container id: %s image: %s", shortID, c.Image)

if err := c.provider.client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil {
return err
Expand All @@ -210,14 +207,12 @@ func (c *DockerContainer) Start(ctx context.Context) error {
return err
}
}
c.logger.Printf("✅ Container is ready id: %s image: %s", shortID, c.Image)

c.isRunning = true

if len(c.lifecycleHooks.PostStarts) > 0 {
err := c.lifecycleHooks.Started(ctx)(c)
if err != nil {
return err
}
err = c.startedHook(ctx)
if err != nil {
return err
}

return nil
Expand All @@ -233,14 +228,9 @@ func (c *DockerContainer) Start(ctx context.Context) error {
// otherwise the engine default. A negative timeout value can be specified,
// meaning no timeout, i.e. no forceful termination is performed.
func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) error {
shortID := c.ID[:12]
c.logger.Printf("Stopping container id: %s image: %s", shortID, c.Image)

if len(c.lifecycleHooks.PreStops) > 0 {
err := c.lifecycleHooks.Stopping(ctx)(c)
if err != nil {
return err
}
err := c.stoppingHook(ctx)
if err != nil {
return err
}

var options container.StopOptions
Expand All @@ -255,29 +245,24 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro
}
defer c.provider.Close()

c.logger.Printf("Container is stopped id: %s image: %s", shortID, c.Image)
c.isRunning = false

if len(c.lifecycleHooks.PostStops) > 0 {
err := c.lifecycleHooks.Stopped(ctx)(c)
if err != nil {
return err
}
err = c.stoppedHook(ctx)
if err != nil {
return err
}

return nil
}

// Terminate is used to kill the container. It is usually triggered by as defer function.
func (c *DockerContainer) Terminate(ctx context.Context) error {
if len(c.lifecycleHooks.PreTerminates) > 0 {
err := c.lifecycleHooks.Terminating(ctx)(c)
if err != nil {
return err
}
err := c.terminatingHook(ctx)
if err != nil {
return err
}

err := c.StopLogProducer()
err = c.StopLogProducer()
if err != nil {
return err
}
Expand All @@ -295,11 +280,9 @@ func (c *DockerContainer) Terminate(ctx context.Context) error {
return err
}

if len(c.lifecycleHooks.PostTerminates) > 0 {
err := c.lifecycleHooks.Terminated(ctx)(c)
if err != nil {
return err
}
err = c.terminatedHook(ctx)
if err != nil {
return err
}

if c.imageWasBuilt {
Expand Down Expand Up @@ -1054,16 +1037,37 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque

networkingConfig := &network.NetworkingConfig{}

err = p.preCreateContainerHook(ctx, req, dockerInput, hostConfig, networkingConfig)
if err != nil {
return nil, err
// 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
},
},
},
}

if len(req.LifecycleHooks.PreCreates) > 0 {
err := req.LifecycleHooks.Creating(ctx)(req)
if err != nil {
return nil, err
}
// always prepend default lifecycle hooks to user-defined hooks
req.LifecycleHooks = append(defaultHooks, req.LifecycleHooks...)

err = req.creatingHook(ctx)
if err != nil {
return nil, err
}

resp, err := p.client.ContainerCreate(ctx, dockerInput, hostConfig, networkingConfig, platform, req.Name)
Expand Down Expand Up @@ -1102,18 +1106,9 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
lifecycleHooks: req.LifecycleHooks,
}

for _, f := range req.Files {
err := c.CopyFileToContainer(ctx, f.HostFilePath, f.ContainerFilePath, f.FileMode)
if err != nil {
return nil, fmt.Errorf("can't copy %s to container: %w", f.HostFilePath, err)
}
}

if len(req.LifecycleHooks.PostCreates) > 0 {
err := req.LifecycleHooks.Created(ctx)(c)
if err != nil {
return nil, err
}
err = c.createdHook(ctx)
if err != nil {
return nil, err
}

return c, nil
Expand Down
32 changes: 22 additions & 10 deletions docs/features/creating_container.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,33 @@ func TestIntegrationNginxLatestReturn(t *testing.T) {

### Lifecycle hooks

_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. The `testcontainers.ContainerLifecycleHooks` struct has the following methods:
_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.

* `Creating` - called before the container is created
* `Created` - called after the container is created
* `Starting` - called before the container is started
* `Started` - called after the container is started
* `Stopping` - called before the container is stopped
* `Stopped` - called after the container is stopped
* `Terminating` - called before the container is terminated
* `Terminated` - called after the container is terminated
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:

* `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
* `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

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-->
[Extending container with life cycle hooks](../../lifecycle_test.go) inside_block:reqWithLifecycleHooks
[Extending container with lifecycle hooks](../../lifecycle_test.go) inside_block:reqWithLifecycleHooks
<!--/codeinclude-->

#### 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:

<!--codeinclude-->
[Extending container with life cycle hooks](../../lifecycle_test.go) inside_block:reqWithDefaultLogginHook
<!--/codeinclude-->

### Advanced Settings
Expand Down
153 changes: 153 additions & 0 deletions lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,159 @@ type ContainerLifecycleHooks struct {
PostTerminates []ContainerHook
}

var DefaultLoggingHook = func(logger Logging) ContainerLifecycleHooks {
shortContainerID := func(c Container) string {
return c.GetContainerID()[:12]
}

return ContainerLifecycleHooks{
PreCreates: []ContainerRequestHook{
func(ctx context.Context, req ContainerRequest) error {
logger.Printf("🐳 Creating container for image %s", req.Image)
return nil
},
},
PostCreates: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("✅ Container created: %s", shortContainerID(c))
return nil
},
},
PreStarts: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("🐳 Starting container: %s", shortContainerID(c))
return nil
},
},
PostStarts: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("✅ Container started: %s", shortContainerID(c))
return nil
},
},
PreStops: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("🐳 Stopping container: %s", shortContainerID(c))
return nil
},
},
PostStops: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("✋ Container stopped: %s", shortContainerID(c))
return nil
},
},
PreTerminates: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("🐳 Terminating container: %s", shortContainerID(c))
return nil
},
},
PostTerminates: []ContainerHook{
func(ctx context.Context, c Container) error {
logger.Printf("🚫 Container terminated: %s", shortContainerID(c))
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 {
err := lifecycleHooks.Creating(ctx)(req)
if err != nil {
return err
}
}

return nil
}

// createdHook is a hook that will be called after a container is created
func (c *DockerContainer) createdHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PostCreates)(c)
if err != nil {
return err
}
}

return nil
}

// startingHook is a hook that will be called before a container is started
func (c *DockerContainer) startingHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PreStarts)(c)
if err != nil {
return err
}
}

return nil
}

// startedHook is a hook that will be called after a container is started
func (c *DockerContainer) startedHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PostStarts)(c)
if err != nil {
return err
}
}

return nil
}

// stoppingHook is a hook that will be called before a container is stopped
func (c *DockerContainer) stoppingHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PreStops)(c)
if err != nil {
return err
}
}

return nil
}

// stoppedHook is a hook that will be called after a container is stopped
func (c *DockerContainer) stoppedHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PostStops)(c)
if err != nil {
return err
}
}

return nil
}

// terminatingHook is a hook that will be called before a container is terminated
func (c *DockerContainer) terminatingHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PreTerminates)(c)
if err != nil {
return err
}
}

return nil
}

// terminatedHook is a hook that will be called after a container is terminated
func (c *DockerContainer) terminatedHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PostTerminates)(c)
if err != nil {
return err
}
}

return nil
}

// Creating is a hook that will be called before a container is created.
func (c ContainerLifecycleHooks) Creating(ctx context.Context) func(req ContainerRequest) error {
return func(req ContainerRequest) error {
Expand Down
Loading

0 comments on commit e1539bc

Please sign in to comment.