Skip to content

Commit

Permalink
Merge pull request #113 from ClaytonNorthey92/feature/archive-context
Browse files Browse the repository at this point in the history
#110 feature: Context as io.Reader
  • Loading branch information
gianarb committed Nov 20, 2019
2 parents cb2a3aa + 4c02348 commit 5f5b63b
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 10 deletions.
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ nginxC.Terminate(ctx, t)`. `t` is `*testing.T` and it is used to notify is the

Testcontainers-go gives you the ability to build and image and run a container from a Dockerfile.

You can do so by specifiying a `Context` and optionally a `Dockerfile` (defaults to "Dockerfile") like so:
You can do so by specifiying a `Context` (the filepath to the build context on your local filesystem)
and optionally a `Dockerfile` (defaults to "Dockerfile") like so:

```
req := ContainerRequest{
Expand All @@ -78,6 +79,30 @@ req := ContainerRequest{
}
```

### Dynamic Build Context

If you would like to send a build context that you created in code (maybe you have a dynamic Dockerfile), you can
send the build context as an `io.Reader` since the Docker Daemon accepts is as a tar file, you can use the [tar](https://golang.org/pkg/archive/tar/) package to create your context.


To do this you would use the `ContextArchive` attribute in the `FromDockerfile` struct.

```go
var buf bytes.Buffer
tarWriter := tar.NewWriter(&buf)
// ... add some files
if err := tarWriter.Close(); err != nil {
// do something with err
}
reader := bytes.NewReader(buf.Bytes())
fromDockerfile := testcontainers.FromDockerfile{
ContextArchive: reader,
}
```

**Please Note** if you specify a `ContextArchive` this will cause testcontainers to ignore the path passed
in to `Context`

## Sending a CMD to a Container

If you would like to send a CMD (command) to a container, you can pass it in to the container request via the `Cmd` field...
Expand Down
37 changes: 31 additions & 6 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"io"

"github.com/docker/docker/pkg/archive"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/testcontainers/testcontainers-go/wait"
Expand Down Expand Up @@ -44,15 +45,17 @@ type Container interface {

// ImageBuildInfo defines what is needed to build an image
type ImageBuildInfo interface {
GetContext() string // the path to the build context
GetDockerfile() string // the relative path to the Dockerfile, including the fileitself
GetContext() (io.Reader, error) // the path to the build context
GetDockerfile() string // the relative path to the Dockerfile, including the fileitself
ShouldBuildImage() bool // return true if the image needs to be built
}

// FromDockerfile represents the parameters needed to build an image from a Dockerfile
// rather than using a pre-built one
type FromDockerfile struct {
Context string // the path to the context of of the docker build
Dockerfile string // the path from the context to the Dockerfile for the image, defaults to "Dockerfile"
Context string // the path to the context of of the docker build
ContextArchive io.Reader // the tar archive file to send to docker that contains the build context
Dockerfile string // the path from the context to the Dockerfile for the image, defaults to "Dockerfile"
}

// ContainerRequest represents the parameters used to get a running container
Expand Down Expand Up @@ -100,6 +103,7 @@ func (c *ContainerRequest) Validate() error {

validationMethods := []func() error{
c.validateContextAndImage,
c.validateContexOrImageIsSpecified,
}

var err error
Expand All @@ -114,8 +118,17 @@ func (c *ContainerRequest) Validate() error {
}

// GetContext retrieve the build context for the request
func (c *ContainerRequest) GetContext() string {
return c.FromDockerfile.Context
func (c *ContainerRequest) GetContext() (io.Reader, error) {
if c.ContextArchive != nil {
return c.ContextArchive, nil
}

buildContext, err := archive.TarWithOptions(c.Context, &archive.TarOptions{})
if err != nil {
return nil, err
}

return buildContext, nil
}

// GetDockerfile returns the Dockerfile from the ContainerRequest, defaults to "Dockerfile"
Expand All @@ -128,10 +141,22 @@ func (c *ContainerRequest) GetDockerfile() string {
return f
}

func (c *ContainerRequest) ShouldBuildImage() bool {
return c.FromDockerfile.Context != "" || c.FromDockerfile.ContextArchive != nil
}

func (c *ContainerRequest) validateContextAndImage() error {
if c.FromDockerfile.Context != "" && c.Image != "" {
return errors.New("you cannot specify both an Image and Context in a ContainerRequest")
}

return nil
}

func (c *ContainerRequest) validateContexOrImageIsSpecified() error {
if c.FromDockerfile.Context == "" && c.FromDockerfile.ContextArchive == nil && c.Image == "" {
return errors.New("you must specify either a build context or an image")
}

return nil
}
165 changes: 165 additions & 0 deletions container_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package testcontainers

import (
"archive/tar"
"bytes"
"context"
"io"
"testing"
"time"

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

func Test_ContainerValidation(t *testing.T) {
Expand Down Expand Up @@ -100,3 +106,162 @@ func Test_GetDockerfile(t *testing.T) {
})
}
}

func Test_BuildImageWithContexts(t *testing.T) {
type TestCase struct {
Name string
ContextPath string
ContextArchive func() (io.Reader, error)
ExpectedEchoOutput string
Dockerfile string
ExpectedError error
}

testCases := []TestCase{
TestCase{
Name: "test build from context archive",
ContextArchive: func() (io.Reader, error) {
var buf bytes.Buffer
tarWriter := tar.NewWriter(&buf)
files := []struct {
Name string
Contents string
}{
{
Name: "Dockerfile",
Contents: `FROM alpine
CMD ["echo", "this is from the archive"]`,
},
}

for _, f := range files {
header := tar.Header{
Name: f.Name,
Mode: 0777,
Size: int64(len(f.Contents)),
Typeflag: tar.TypeReg,
Format: tar.FormatGNU,
}

if err := tarWriter.WriteHeader(&header); err != nil {
return nil, err
}

if _, err := tarWriter.Write([]byte(f.Contents)); err != nil {
return nil, err
}

if err := tarWriter.Close(); err != nil {
return nil, err
}
}

reader := bytes.NewReader(buf.Bytes())

return reader, nil
},
ExpectedEchoOutput: "this is from the archive",
},
TestCase{
Name: "test build from context archive and be able to use files in it",
ContextArchive: func() (io.Reader, error) {
var buf bytes.Buffer
tarWriter := tar.NewWriter(&buf)
files := []struct {
Name string
Contents string
}{
{
Name: "say_hi.sh",
Contents: `echo hi this is from the say_hi.sh file!`,
},
{
Name: "Dockerfile",
Contents: `FROM alpine
WORKDIR /app
COPY . .
CMD ["sh", "./say_hi.sh"]`,
},
}

for _, f := range files {
header := tar.Header{
Name: f.Name,
Mode: 0777,
Size: int64(len(f.Contents)),
Typeflag: tar.TypeReg,
Format: tar.FormatGNU,
}

if err := tarWriter.WriteHeader(&header); err != nil {
return nil, err
}

if _, err := tarWriter.Write([]byte(f.Contents)); err != nil {
return nil, err
}
}

if err := tarWriter.Close(); err != nil {
return nil, err
}

reader := bytes.NewReader(buf.Bytes())

return reader, nil
},
ExpectedEchoOutput: "hi this is from the say_hi.sh file!",
},
TestCase{
Name: "test buildling from a context on the filesystem",
ContextPath: "./testresources",
Dockerfile: "echo.Dockerfile",
ExpectedEchoOutput: "this is from the echo test Dockerfile",
ContextArchive: func() (io.Reader, error) {
return nil, nil
},
},
TestCase{
Name: "it should error if neither a context nor a context archive are specified",
ContextPath: "",
ContextArchive: func() (io.Reader, error) {
return nil, nil
},
ExpectedError: errors.New("failed to create container: you must specify either a build context or an image"),
},
}

for _, testCase := range testCases {
t.Run(testCase.Name, func(t *testing.T) {
ctx := context.Background()
a, err := testCase.ContextArchive()
if err != nil {
t.Fatal(err)
}
req := ContainerRequest{
FromDockerfile: FromDockerfile{
ContextArchive: a,
Context: testCase.ContextPath,
Dockerfile: testCase.Dockerfile,
},
WaitingFor: wait.ForLog(testCase.ExpectedEchoOutput).WithStartupTimeout(1 * time.Minute),
}

c, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if testCase.ExpectedError != nil && err != nil {
if testCase.ExpectedError.Error() != err.Error() {
t.Fatalf("unexpected error: %s, was expecting %s", err.Error(), testCase.ExpectedError.Error())
}
} else if err != nil {
t.Fatal(err)
} else {
c.Terminate(ctx)
}

})

}
}
5 changes: 2 additions & 3 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/docker/go-connections/nat"

"github.com/pkg/errors"
Expand Down Expand Up @@ -311,7 +310,7 @@ func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (st

repoTag := fmt.Sprintf("%s:%s", repo, tag)

buildContext, err := archive.TarWithOptions(img.GetContext(), &archive.TarOptions{})
buildContext, err := img.GetContext()
if err != nil {
return "", err
}
Expand Down Expand Up @@ -380,7 +379,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
}

var tag string
if req.FromDockerfile.Context != "" {
if req.ShouldBuildImage() {
tag, err = p.BuildImage(ctx, &req)
if err != nil {
return nil, err
Expand Down
3 changes: 3 additions & 0 deletions testresources/echo.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM alpine

CMD ["echo", "this is from the echo test Dockerfile"]

0 comments on commit 5f5b63b

Please sign in to comment.