Skip to content

Commit be92e11

Browse files
author
Gianluca Arbezzano
committed
Bootstrap API
Signed-off-by: Gianluca Arbezzano <gianarb92@gmail.com>
0 parents  commit be92e11

File tree

6 files changed

+339
-0
lines changed

6 files changed

+339
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
debug.test
2+
vendor

Gopkg.lock

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Gopkg.toml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Gopkg.toml example
2+
#
3+
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
4+
# for detailed Gopkg.toml documentation.
5+
#
6+
# required = ["github.com/user/thing/cmd/thing"]
7+
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8+
#
9+
# [[constraint]]
10+
# name = "github.com/user/project"
11+
# version = "1.0.0"
12+
#
13+
# [[constraint]]
14+
# name = "github.com/user/project2"
15+
# branch = "dev"
16+
# source = "github.com/myfork/project2"
17+
#
18+
# [[override]]
19+
# name = "github.com/x/y"
20+
# version = "2.4.0"
21+
#
22+
# [prune]
23+
# non-go = false
24+
# go-tests = true
25+
# unused-packages = true
26+
27+
28+
[[constraint]]
29+
name = "github.com/docker/docker"
30+
version = "1.13.0"
31+
32+
[[constraint]]
33+
name = "github.com/docker/go-connections"
34+
version = "0.3.0"
35+
36+
[prune]
37+
go-tests = true
38+
unused-packages = true

README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
When I was working on a Zipkin PR I discovered a nice Java library called
2+
[testcontainers](https://www.testcontainers.org/).
3+
4+
It provides an easy and clean API over the go docker sdk to run, terminate and
5+
connect to containers in your tests.
6+
7+
I found myself comfortable programmatically writing the containers I need to run
8+
an integration/smoke tests. So I started porting this library in Go.
9+
10+
11+
This is the API I have defined:
12+
13+
```go
14+
package main
15+
16+
import (
17+
"testing",
18+
"github.com/gianarb/testcontainer"
19+
)
20+
21+
func TestNginxLatestReturn(t *testing.T) {
22+
ctx := context.Background()
23+
nginxC, err := testcontainer.RunContainer(ctx, "nginx", testcontainer.RequestContainer{
24+
ExportedPort: []string{
25+
"80/tpc",
26+
},
27+
})
28+
if err != nil {
29+
t.Error(err)
30+
}
31+
defer nginxC.Terminate(ctx)
32+
ip, err := nginxC.GetIPAddress(ctx)
33+
if err != nil {
34+
t.Error(err)
35+
}
36+
resp, err := http.Get(fmt.Sprintf("http://%s", ip))
37+
if resp.StatusCode != http.StatusOK {
38+
t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode)
39+
}
40+
}
41+
```
42+
This is a simple example, you can create one container in my case using the
43+
`nginx` image. You can get its IP `ip, err := nginxC.GetIPAddress(ctx)` and you
44+
can use it to make a GET: `resp, err := http.Get(fmt.Sprintf("http://%s", ip))`
45+
46+
To clean your environment you can defer the container termination `defer nginxC.Terminate(ctx)`.
47+
48+
You can build more complex flow using envvar to configure the containers. Let's
49+
suppose you are testing an application that requites redis:
50+
51+
```go
52+
ctx := context.Background()
53+
redisC, err := testcontainer.RunContainer(ctx, "redis", testcontainer.RequestContainer{
54+
ExportedPort: []string{
55+
"6379/tpc",
56+
},
57+
})
58+
if err != nil {
59+
t.Error(err)
60+
}
61+
defer redisC.Terminate(ctx)
62+
redisIP, err := redisC.GetIPAddress(ctx)
63+
if err != nil {
64+
t.Error(err)
65+
}
66+
67+
appC, err := testcontainer.RunContainer(ctx, "your/app", testcontainer.RequestContainer{
68+
ExportedPort: []string{
69+
"8081/tpc",
70+
},
71+
Env: map[string]string{
72+
"REDIS_HOST": fmt.Sprintf("http://%s:6379", redisIP),
73+
},
74+
})
75+
if err != nil {
76+
t.Error(err)
77+
}
78+
defer appC.Terminate(ctx)
79+
80+
// your assertions
81+
```

docker.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package testcontainer
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"github.com/docker/docker/api/types"
8+
"github.com/docker/docker/api/types/container"
9+
"github.com/docker/docker/client"
10+
"github.com/docker/go-connections/nat"
11+
)
12+
13+
// RequestContainer is the input object used to get a running container.
14+
type RequestContainer struct {
15+
Env map[string]string
16+
ExportedPort []string
17+
Cmd string
18+
}
19+
20+
// Container is the struct used to represent a single container.
21+
type Container struct {
22+
// Container ID from Docker
23+
ID string
24+
// Cache to retrieve container infromation without re-fetching them from dockerd
25+
raw *types.ContainerJSON
26+
}
27+
28+
// Terminate is used to kill the container. It is usally triggered by as defer function.
29+
func (c *Container) Terminate(ctx context.Context) error {
30+
var err error
31+
cli, err := client.NewEnvClient()
32+
if err != nil {
33+
return err
34+
}
35+
return cli.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{
36+
Force: true,
37+
})
38+
}
39+
40+
func inspectContainer(ctx context.Context, c *Container) (*types.ContainerJSON, error) {
41+
if c.raw != nil {
42+
return c.raw, nil
43+
}
44+
cli, err := client.NewEnvClient()
45+
if err != nil {
46+
return nil, err
47+
}
48+
inspect, err := cli.ContainerInspect(ctx, c.ID)
49+
if err != nil {
50+
return nil, err
51+
}
52+
c.raw = &inspect
53+
return c.raw, nil
54+
}
55+
56+
// GetIPAddress returns the ip address for the running container.
57+
func (c *Container) GetIPAddress(ctx context.Context) (string, error) {
58+
inspect, err := inspectContainer(ctx, c)
59+
if err != nil {
60+
return "", err
61+
}
62+
return inspect.NetworkSettings.IPAddress, nil
63+
}
64+
65+
// RunContainer takes a RequestContainer as input and it runs a container via the docker sdk
66+
func RunContainer(ctx context.Context, containerImage string, input RequestContainer) (*Container, error) {
67+
cli, err := client.NewEnvClient()
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
exposedPorts := nat.PortSet{}
73+
for _, p := range input.ExportedPort {
74+
exposedPorts[nat.Port(p)] = struct{}{}
75+
}
76+
77+
env := []string{}
78+
for envKey, envVar := range input.Env {
79+
env = append(env, envKey+"="+envVar)
80+
}
81+
82+
dockerInput := &container.Config{
83+
Image: containerImage,
84+
Env: env,
85+
ExposedPorts: exposedPorts,
86+
}
87+
88+
if input.Cmd != "" {
89+
dockerInput.Cmd = strings.Split(input.Cmd, " ")
90+
}
91+
92+
resp, err := cli.ContainerCreate(ctx, dockerInput, nil, nil, "")
93+
if err != nil {
94+
return nil, err
95+
}
96+
if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
97+
return nil, err
98+
}
99+
return &Container{
100+
ID: resp.ID,
101+
}, nil
102+
}

docker_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package testcontainer
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"testing"
8+
)
9+
10+
func TestContainerCreation(t *testing.T) {
11+
ctx := context.Background()
12+
nginxC, err := RunContainer(ctx, "nginx", RequestContainer{
13+
ExportedPort: []string{
14+
"80/tpc",
15+
},
16+
})
17+
if err != nil {
18+
t.Error(err)
19+
}
20+
defer nginxC.Terminate(ctx)
21+
ip, err := nginxC.GetIPAddress(ctx)
22+
if err != nil {
23+
t.Error(err)
24+
}
25+
resp, err := http.Get(fmt.Sprintf("http://%s", ip))
26+
if err != nil {
27+
t.Error(err)
28+
}
29+
if resp.StatusCode != http.StatusOK {
30+
t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode)
31+
}
32+
}

0 commit comments

Comments
 (0)