Skip to content

Commit

Permalink
feat: add image-keep option for built images (#1785)
Browse files Browse the repository at this point in the history
* feat: add image-keep option for built images

Adds an option to keep images if they have been built.
This avoids the need to build these images everytime from scratch.

* fix: lint file

* refactor: move image-keep option to FromDockerfile

* docs: add documentation for keeping built images

* style: change test names for keeping built images

---------

Co-authored-by: Manuel de la Peña <mdelapenya@gmail.com>
  • Loading branch information
lefinal and mdelapenya committed Oct 26, 2023
1 parent 25a7e3f commit 636c3cb
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 3 deletions.
8 changes: 8 additions & 0 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ type FromDockerfile struct {
BuildArgs map[string]*string // enable user to pass build args to docker daemon
PrintBuildLog bool // enable user to print build log
AuthConfigs map[string]registry.AuthConfig // Deprecated. Testcontainers will detect registry credentials automatically. Enable auth configs to be able to pull from an authenticated docker registry
// KeepImage describes whether DockerContainer.Terminate should not delete the
// container image. Useful for images that are built from a Dockerfile and take a
// long time to build. Keeping the image also Docker to reuse it.
KeepImage bool
}

type ContainerFile struct {
Expand Down Expand Up @@ -276,6 +280,10 @@ func (c *ContainerRequest) ShouldBuildImage() bool {
return c.FromDockerfile.Context != "" || c.FromDockerfile.ContextArchive != nil
}

func (c *ContainerRequest) ShouldKeepBuiltImage() bool {
return c.FromDockerfile.KeepImage
}

func (c *ContainerRequest) ShouldPrintBuildLog() bool {
return c.FromDockerfile.PrintBuildLog
}
Expand Down
9 changes: 6 additions & 3 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ type DockerContainer struct {
WaitingFor wait.Strategy
Image string

isRunning bool
imageWasBuilt bool
isRunning bool
imageWasBuilt bool
// keepBuiltImage makes Terminate not remove the image if imageWasBuilt.
keepBuiltImage bool
provider *DockerProvider
sessionID string
terminationSignal chan bool
Expand Down Expand Up @@ -274,7 +276,7 @@ func (c *DockerContainer) Terminate(ctx context.Context) error {
return err
}

if c.imageWasBuilt {
if c.imageWasBuilt && !c.keepBuiltImage {
_, err := c.provider.client.ImageRemove(ctx, c.Image, types.ImageRemoveOptions{
Force: true,
PruneChildren: true,
Expand Down Expand Up @@ -1089,6 +1091,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
WaitingFor: req.WaitingFor,
Image: imageName,
imageWasBuilt: req.ShouldBuildImage(),
keepBuiltImage: req.ShouldKeepBuiltImage(),
sessionID: testcontainerssession.SessionID(),
provider: p,
terminationSignal: termSignal,
Expand Down
55 changes: 55 additions & 0 deletions docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"testing"
"time"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/api/types/volume"
Expand Down Expand Up @@ -2081,3 +2082,57 @@ func TestDockerProviderFindContainerByName(t *testing.T) {
require.NotNil(t, c)
assert.Contains(t, c.Names, c1Name)
}

func TestImageBuiltFromDockerfile_KeepBuiltImage(t *testing.T) {
tests := []struct {
keepBuiltImage bool
}{
{keepBuiltImage: true},
{keepBuiltImage: false},
}

for _, tt := range tests {
t.Run(fmt.Sprintf("Keep built image: %t", tt.keepBuiltImage), func(t *testing.T) {
ctx := context.Background()
// Set up CLI.
provider, err := NewDockerProvider()
require.NoError(t, err, "get docker provider should not fail")
defer func() { _ = provider.Close() }()
cli := provider.Client()
// Create container.
c, err := GenericContainer(ctx, GenericContainerRequest{
ProviderType: providerType,
ContainerRequest: ContainerRequest{
FromDockerfile: FromDockerfile{
Context: "testdata",
Dockerfile: "echo.Dockerfile",
KeepImage: tt.keepBuiltImage,
},
},
})
require.NoError(t, err, "create container should not fail")
defer func() { _ = c.Terminate(context.Background()) }()
// Get the image ID.
containerName, err := c.Name(ctx)
require.NoError(t, err, "get container name should not fail")
containerDetails, err := cli.ContainerInspect(ctx, containerName)
require.NoError(t, err, "inspect container should not fail")
containerImage := containerDetails.Image
t.Cleanup(func() {
_, _ = cli.ImageRemove(ctx, containerImage, types.ImageRemoveOptions{
Force: true,
PruneChildren: true,
})
})
// Now, we terminate the container and check whether the image still exists.
err = c.Terminate(ctx)
require.NoError(t, err, "terminate container should not fail")
_, _, err = cli.ImageInspectWithRaw(ctx, containerImage)
if tt.keepBuiltImage {
assert.Nil(t, err, "image should still exist")
} else {
assert.NotNil(t, err, "image should not exist anymore")
}
})
}
}
17 changes: 17 additions & 0 deletions docs/features/build_from_dockerfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,20 @@ req := ContainerRequest{
},
}
```

## Keeping built images

Per default, built images are deleted after being used.
However, some images you build might have no or only minor changes during development.
Building them for each test run might take a lot of time.
You can avoid this by setting `KeepImage` in `FromDockerfile`.
If the image is being kept, cached layers might be reused during building or even the whole image.

```go
req := ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
// ...
KeepImage: true,
},
}
```

0 comments on commit 636c3cb

Please sign in to comment.