Skip to content

Commit

Permalink
Add support for re-usable containers
Browse files Browse the repository at this point in the history
* Add support for re-usable containers

* Improve logging

* Add inline comments

* Fix typo

* Add reusable flow tests

* Fix lint issues

* Unify sidecar chan lifecycle
  • Loading branch information
devuo committed Oct 20, 2022
1 parent 5e86a8b commit e6e2499
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 19 deletions.
86 changes: 67 additions & 19 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
Expand Down Expand Up @@ -110,19 +111,53 @@ func (d *docker) pullImage(ctx context.Context, image string, cfg *Options) erro
}

func (d *docker) startContainer(ctx context.Context, image string, ports NamedPorts, cfg *Options) (*Container, error) {
if cfg.Reuse {
container, ok, err := d.findReusableContainer(ctx, image, ports, cfg)
if err != nil {
return nil, err
}

if ok {
d.log.Info("re-using container")
return container, nil
}
}

d.log.Info("starting container")

resp, err := d.prepareContainer(ctx, image, ports, cfg)
if err != nil {
return nil, fmt.Errorf("can't prepare container: %w", err)
}

sidecarChan := d.setupContainerCleanup(resp.ID, cfg)

err = d.client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
return nil, fmt.Errorf("can't start container %s: %w", resp.ID, err)
}

container, err := d.waitForContainerNetwork(ctx, resp.ID, ports)
if err != nil {
return nil, fmt.Errorf("container network isn't ready: %w", err)
}

if sidecar, ok := <-sidecarChan; ok {
container.ID = generateID(container.ID, sidecar)
}

d.log.Infow("container started", "container", container)

return container, nil
}

func (d *docker) setupContainerCleanup(id string, cfg *Options) chan string {
sidecarChan := make(chan string)

go func() {
defer close(sidecarChan)

if cfg.DisableAutoCleanup || cfg.Debug {
if cfg.DisableAutoCleanup || cfg.Reuse || cfg.Debug {
return
}

Expand All @@ -133,7 +168,7 @@ func (d *docker) startContainer(ctx context.Context, image string, ports NamedPo
return health.HTTPGet(ctx, c.DefaultAddress())
}),
WithInit(func(ctx context.Context, c *Container) error {
return cleaner.Notify(context.Background(), c.DefaultAddress(), resp.ID)
return cleaner.Notify(context.Background(), c.DefaultAddress(), id)
}),
}
if cfg.UseLocalImagesFirst {
Expand All @@ -148,23 +183,7 @@ func (d *docker) startContainer(ctx context.Context, image string, ports NamedPo
}
}()

err = d.client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
return nil, fmt.Errorf("can't start container %s: %w", resp.ID, err)
}

container, err := d.waitForContainerNetwork(ctx, resp.ID, ports)
if err != nil {
return nil, fmt.Errorf("container network isn't ready: %w", err)
}

if sidecar, ok := <-sidecarChan; ok {
container.ID = generateID(container.ID, sidecar)
}

d.log.Infow("container started", "container", container)

return container, nil
return sidecarChan
}

func (d *docker) prepareContainer(
Expand Down Expand Up @@ -326,6 +345,35 @@ func (d *docker) createContainer(ctx context.Context, image string, ports NamedP
return &resp, err
}

func (d *docker) findReusableContainer(
ctx context.Context,
image string,
ports NamedPorts,
cfg *Options,
) (*Container, bool, error) {
if cfg.ContainerName == "" {
return nil, false, fmt.Errorf("container name is required when container reuse is enabled")
}

list, err := d.client.ContainerList(ctx, types.ContainerListOptions{
Filters: filters.NewArgs(
filters.Arg("name", cfg.ContainerName),
filters.Arg("ancestor", image),
filters.Arg("status", "running"),
),
})
if err != nil || len(list) < 1 {
return nil, false, err
}

container, err := d.waitForContainerNetwork(ctx, list[0].ID, ports)
if err != nil {
return nil, false, err
}

return container, true, nil
}

func (d *docker) boundNamedPorts(json types.ContainerJSON, namedPorts NamedPorts) (NamedPorts, error) {
boundNamedPorts := make(NamedPorts)

Expand Down
14 changes: 14 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ func WithRegistryAuth(auth string) Option {
}
}

// WithContainerReuse disables Gnomock default behaviour of automatic container
// cleanup and also disables the automatic replacement at startup of an existing
// container with the same name and image. Effectively this makes Gnomock reuse
// a container from a previous Gnomock execution.
func WithContainerReuse() Option {
return func(o *Options) {
o.Reuse = true
}
}

// HealthcheckFunc defines a function to be used to determine container health.
// It receives a host and a port, and returns an error if the container is not
// ready, or nil when the container can be used. One example of HealthcheckFunc
Expand Down Expand Up @@ -269,6 +279,10 @@ type Options struct {
// {"username":"foo","password":"bar"}
Auth string `json:"auth"`

// Reuse prevents the container from being automatically stopped and enables
// its re-use in posterior executions.
Reuse bool `json:"reuse"`

ctx context.Context
init InitFunc
healthcheck HealthcheckFunc
Expand Down
38 changes: 38 additions & 0 deletions preset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,44 @@ func TestPreset_duplicateContainerName(t *testing.T) {
require.NoError(t, gnomock.Stop(newContainer))
}

func TestPreset_reusableContainerSucceeds(t *testing.T) {
t.Parallel()

p := &testutil.TestPreset{Img: testutil.TestImage}
originalContainer, err := gnomock.Start(
p,
gnomock.WithTimeout(time.Minute),
gnomock.WithContainerName("gnomock-reuse"),
gnomock.WithDebugMode(),
)
require.NoError(t, err)

newContainer, err := gnomock.Start(
p,
gnomock.WithTimeout(time.Minute),
gnomock.WithContainerName("gnomock-reuse"),
gnomock.WithDebugMode(),
gnomock.WithContainerReuse(),
)
require.NoError(t, err)

require.Equal(t, originalContainer.ID, newContainer.ID)
require.NoError(t, gnomock.Stop(newContainer))
}

func TestPreset_reusableContainerFailsWithoutName(t *testing.T) {
t.Parallel()

p := &testutil.TestPreset{Img: testutil.TestImage}
_, err := gnomock.Start(
p,
gnomock.WithTimeout(time.Minute),
gnomock.WithContainerReuse(),
gnomock.WithDebugMode(),
)
require.EqualError(t, err, "can't start container: container name is required when container reuse is enabled")
}

func TestPreset_customNamedPorts(t *testing.T) {
t.Parallel()

Expand Down

0 comments on commit e6e2499

Please sign in to comment.