Skip to content

Commit

Permalink
Merge pull request #464 from VladimirStepanov/issue-439-use-existing-…
Browse files Browse the repository at this point in the history
…container

Issue 439: use an existing container
  • Loading branch information
mdelapenya authored Jul 5, 2022
2 parents 686e4ea + cb81fb6 commit 2feda90
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 11 deletions.
14 changes: 8 additions & 6 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ type DeprecatedContainer interface {

// ContainerProvider allows the creation of containers on an arbitrary system
type ContainerProvider interface {
CreateContainer(context.Context, ContainerRequest) (Container, error) // create a container without starting it
RunContainer(context.Context, ContainerRequest) (Container, error) // create a container and start it
CreateContainer(context.Context, ContainerRequest) (Container, error) // create a container without starting it
ReuseOrCreateContainer(context.Context, ContainerRequest) (Container, error) // reuses a container if it exists or creates a container without starting
RunContainer(context.Context, ContainerRequest) (Container, error) // create a container and start it
Health(context.Context) error
Config() TestContainersConfig
}
Expand All @@ -41,10 +42,11 @@ type Container interface {
MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port
Ports(context.Context) (nat.PortMap, error) // get all exposed ports
SessionID() string // get session id
Start(context.Context) error // start the container
Stop(context.Context, *time.Duration) error // stop the container
Terminate(context.Context) error // terminate the container
Logs(context.Context) (io.ReadCloser, error) // Get logs of the container
IsRunning() bool
Start(context.Context) error // start the container
Stop(context.Context, *time.Duration) error // stop the container
Terminate(context.Context) error // terminate the container
Logs(context.Context) (io.ReadCloser, error) // Get logs of the container
FollowOutput(LogConsumer)
StartLogProducer(context.Context) error
StopLogProducer() error
Expand Down
67 changes: 65 additions & 2 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"strings"
"time"

"github.com/docker/docker/api/types/filters"

"github.com/cenkalti/backoff/v4"
"github.com/containerd/containerd/platforms"
"github.com/docker/docker/api/types"
Expand Down Expand Up @@ -54,6 +56,7 @@ type DockerContainer struct {
WaitingFor wait.Strategy
Image string

isRunning bool
imageWasBuilt bool
provider *DockerProvider
sessionID uuid.UUID
Expand All @@ -69,6 +72,10 @@ func (c *DockerContainer) GetContainerID() string {
return c.ID
}

func (c *DockerContainer) IsRunning() bool {
return c.isRunning
}

// Endpoint gets proto://host:port string for the first exposed port
// Will returns just host:port if proto is ""
func (c *DockerContainer) Endpoint(ctx context.Context, proto string) (string, error) {
Expand Down Expand Up @@ -180,7 +187,7 @@ func (c *DockerContainer) Start(ctx context.Context) error {
}
}
c.logger.Printf("Container is ready id: %s image: %s", shortID, c.Image)

c.isRunning = true
return nil
}

Expand All @@ -202,7 +209,7 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro
}

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

c.isRunning = false
return nil
}

Expand Down Expand Up @@ -236,6 +243,7 @@ func (c *DockerContainer) Terminate(ctx context.Context) error {
}

c.sessionID = uuid.UUID{}
c.isRunning = false
return nil
}

Expand Down Expand Up @@ -1030,6 +1038,61 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
return c, nil
}

func (p *DockerProvider) findContainerByName(ctx context.Context, name string) (*types.Container, error) {
if name == "" {
return nil, nil
}
filter := filters.NewArgs(filters.KeyValuePair{
Key: "name",
Value: name,
})
containers, err := p.client.ContainerList(ctx, types.ContainerListOptions{Filters: filter})
if err != nil {
return nil, err
}
if len(containers) > 0 {
return &containers[0], nil
}
return nil, nil
}

func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req ContainerRequest) (Container, error) {
c, err := p.findContainerByName(ctx, req.Name)
if err != nil {
return nil, err
}
if c == nil {
return p.CreateContainer(ctx, req)
}

sessionID := uuid.New()
var termSignal chan bool
if !req.SkipReaper {
r, err := NewReaper(ctx, sessionID.String(), p, req.ReaperImage)
if err != nil {
return nil, fmt.Errorf("%w: creating reaper failed", err)
}
termSignal, err = r.Connect()
if err != nil {
return nil, fmt.Errorf("%w: connecting to reaper failed", err)
}
}
dc := &DockerContainer{
ID: c.ID,
WaitingFor: req.WaitingFor,
Image: c.Image,
sessionID: sessionID,
provider: p,
terminationSignal: termSignal,
skipReaper: req.SkipReaper,
stopProducer: make(chan bool),
logger: p.Logger,
isRunning: c.State == "running",
}
return dc, nil

}

// attemptToPullImage tries to pull the image while respecting the ctx cancellations.
// Besides, if the image cannot be pulled due to ErrorNotFound then no need to retry but terminate immediately.
func (p *DockerProvider) attemptToPullImage(ctx context.Context, tag string, pullOpt types.ImagePullOptions) error {
Expand Down
58 changes: 58 additions & 0 deletions docs/features/creating_container.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type nginxContainer struct {
URI string
}


func setupNginx(ctx context.Context) (*nginxContainer, error) {
req := testcontainers.ContainerRequest{
Image: "nginx",
Expand Down Expand Up @@ -82,6 +83,63 @@ func TestIntegrationNginxLatestReturn(t *testing.T) {
}
```

## Reusable container

With `Reuse` option you can reuse an existing container. Reusing will work only if you pass an
existing container name via 'req.Name' field. If the name is not in a list of existing containers,
the function will create a new generic container. If `Reuse` is true and `Name` is empty, you will get error.

The following test creates an NGINX container, adds a file into it and then reuses the container again for checking the file:
```go

const (
reusableContainerName = "my_test_reusable_container"
)

ctx := context.Background()

n1, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: ContainerRequest{
Image: "nginx:1.17.6",
ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForListeningPort("80/tcp"),
Name: reusableContainerName,
},
Started: true,
})
if err != nil {
log.Fatal(err)
}
defer n1.Terminate(ctx)

copiedFileName := "hello_copy.sh"
err = n1.CopyFileToContainer(ctx, "./testresources/hello.sh", "/"+copiedFileName, 700)

if err != nil {
log.Fatal(err)
}

n2, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: ContainerRequest{
Image: "nginx:1.17.6",
ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForListeningPort("80/tcp"),
Name: reusableContainerName,
},
Started: true,
Reuse: true,
})
if err != nil {
log.Fatal(err)
}

c, _, err := n2.Exec(ctx, []string{"bash", copiedFileName})
if err != nil {
log.Fatal(err)
}
fmt.Println(c)
````

# Parallel running

`testcontainers.ParallelContainers` - defines the containers that should be run in parallel mode.
Expand Down
20 changes: 17 additions & 3 deletions generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ package testcontainers

import (
"context"
"errors"
"fmt"
)

var (
ErrReuseEmptyName = errors.New("with reuse option a container name mustn't be empty")
)

// GenericContainerRequest represents parameters to a generic container
type GenericContainerRequest struct {
ContainerRequest // embedded request for provider
Started bool // whether to auto-start the container
ProviderType ProviderType // which provider to use, Docker if empty
Logger Logging // provide a container specific Logging - use default global logger if empty
Reuse bool // reuse an existing container if it exists or create a new one. a container name mustn't be empty
}

// GenericNetworkRequest represents parameters to a generic network
Expand All @@ -35,6 +41,10 @@ func GenericNetwork(ctx context.Context, req GenericNetworkRequest) (Network, er

// GenericContainer creates a generic container with parameters
func GenericContainer(ctx context.Context, req GenericContainerRequest) (Container, error) {
if req.Reuse && req.Name == "" {
return nil, ErrReuseEmptyName
}

logging := req.Logger
if logging == nil {
logging = Logger
Expand All @@ -44,17 +54,21 @@ func GenericContainer(ctx context.Context, req GenericContainerRequest) (Contain
return nil, err
}

c, err := provider.CreateContainer(ctx, req.ContainerRequest)
var c Container
if req.Reuse {
c, err = provider.ReuseOrCreateContainer(ctx, req.ContainerRequest)
} else {
c, err = provider.CreateContainer(ctx, req.ContainerRequest)
}
if err != nil {
return nil, fmt.Errorf("%w: failed to create container", err)
}

if req.Started {
if req.Started && !c.IsRunning() {
if err := c.Start(ctx); err != nil {
return c, fmt.Errorf("%w: failed to start container", err)
}
}

return c, nil
}

Expand Down
84 changes: 84 additions & 0 deletions generic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package testcontainers

import (
"context"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/testcontainers/testcontainers-go/wait"
)

const (
reusableContainerName = "my_test_reusable_container"
)

func TestGenericReusableContainer(t *testing.T) {
ctx := context.Background()

n1, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: ContainerRequest{
Image: "nginx:1.17.6",
ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForListeningPort("80/tcp"),
Name: reusableContainerName,
},
Started: true,
})
require.NoError(t, err)
require.True(t, n1.IsRunning())
defer n1.Terminate(ctx)

copiedFileName := "hello_copy.sh"
err = n1.CopyFileToContainer(ctx, "./testresources/hello.sh", "/"+copiedFileName, 700)
require.NoError(t, err)

tests := []struct {
name string
containerName string
errMsg string
reuseOption bool
}{
{
name: "reuse option with empty name",
errMsg: ErrReuseEmptyName.Error(),
reuseOption: true,
},
{
name: "container already exists (reuse=false)",
containerName: reusableContainerName,
errMsg: "is already in use by container",
reuseOption: false,
},
{
name: "success reusing",
containerName: reusableContainerName,
reuseOption: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
n2, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: ContainerRequest{
Image: "nginx:1.17.6",
ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForListeningPort("80/tcp"),
Name: tc.containerName,
},
Started: true,
Reuse: tc.reuseOption,
})
if tc.errMsg == "" {
c, _, err := n2.Exec(ctx, []string{"bash", copiedFileName})
require.NoError(t, err)
require.Zero(t, c)
} else {
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), tc.errMsg))
}
})
}

}

0 comments on commit 2feda90

Please sign in to comment.