robocar-base/vendor/github.com/testcontainers/testcontainers-go
2020-03-01 17:06:34 +01:00
..
wait Fix dependencies 2020-03-01 17:06:34 +01:00
.gitignore Fix dependencies 2020-03-01 17:06:34 +01:00
.travis.yml Fix dependencies 2020-03-01 17:06:34 +01:00
container.go Fix dependencies 2020-03-01 17:06:34 +01:00
docker.go Fix dependencies 2020-03-01 17:06:34 +01:00
generic.go Fix dependencies 2020-03-01 17:06:34 +01:00
go.mod Fix dependencies 2020-03-01 17:06:34 +01:00
go.sum Fix dependencies 2020-03-01 17:06:34 +01:00
LICENSE Fix dependencies 2020-03-01 17:06:34 +01:00
logconsumer.go Fix dependencies 2020-03-01 17:06:34 +01:00
network.go Fix dependencies 2020-03-01 17:06:34 +01:00
README.md Fix dependencies 2020-03-01 17:06:34 +01:00
reaper.go Fix dependencies 2020-03-01 17:06:34 +01:00

Build Status

When I was working on a Zipkin PR I discovered a nice Java library called testcontainers.

It provides an easy and clean API over the go docker sdk to run, terminate and connect to containers in your tests.

I found myself comfortable programmatically writing the containers I need to run an integration/smoke tests. So I started porting this library in Go.

This is the API I have defined:

package main

import (
	"context"
	"fmt"
	"net/http"
	"testing"

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

func TestNginxLatestReturn(t *testing.T) {
	ctx := context.Background()
	req := testcontainers.ContainerRequest{
		Image:        "nginx",
		ExposedPorts: []string{"80/tcp"},
		WaitingFor:   wait.ForHTTP("/"),
	}
	nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	if err != nil {
		t.Error(err)
	}
	defer nginxC.Terminate(ctx)
	ip, err := nginxC.Host(ctx)
	if err != nil {
		t.Error(err)
	}
	port, err := nginxC.MappedPort(ctx, "80")
	if err != nil {
		t.Error(err)
	}
	resp, err := http.Get(fmt.Sprintf("http://%s:%s", ip, port.Port()))
	if resp.StatusCode != http.StatusOK {
		t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode)
	}
}

This is a simple example, you can create one container in my case using the nginx image. You can get its IP ip, err := nginxC.GetContainerIpAddress(ctx) and you can use it to make a GET: resp, err := http.Get(fmt.Sprintf("http://%s", ip))

To clean your environment you can defer the container termination defer nginxC.Terminate(ctx, t). t is *testing.T and it is used to notify is the defer failed marking the test as failed.

Build from Dockerfile

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

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

req := ContainerRequest{
		FromDockerfile: testcontainers.FromDockerfile{
			Context: "/path/to/build/context",
			Dockerfile: "CustomDockerfile",
		},
	}

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 package to create your context.

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

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...

req := ContainerRequest{
	Image: "alpine",
	WaitingFor: wait.ForAll(
		wait.ForLog("command override!"),
	),
	Cmd: []string{"echo", "command override!"},
}

Following Container Logs

If you wish to follow container logs, you can set up LogConsumers. The log following functionality follows a producer-consumer model. You will need to explicitly start and stop the producer. As logs are written to either stdout, or stderr (stdin is not supported) they will be forwarded (produced) to any associated LogConsumers. You can associate LogConsumers with the .FollowOutput function.

Please note if you start the producer you should always stop it explicitly.

for example, this consumer will just add logs to a slice

type TestLogConsumer struct {
	Msgs []string
}

func (g *TestLogConsumer) Accept(l Log) {
	g.Msgs = append(g.Msgs, string(l.Content))
}

this can be used like so:

g := TestLogConsumer{
	Msgs: []string{},
}

err := c.StartLogProducer(ctx)
if err != nil {
	// do something with err
}

c.FollowOutput(&g)

// some stuff happens...

err = c.StopLogProducer()
if err != nil {
	// do something with err
}