2017-02-26 21:53:19 +00:00
|
|
|
package dockertest
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2018-01-26 23:51:00 +00:00
|
|
|
"io/ioutil"
|
2017-02-26 21:53:19 +00:00
|
|
|
"os"
|
2018-01-26 23:51:00 +00:00
|
|
|
"path/filepath"
|
2017-02-26 21:53:19 +00:00
|
|
|
"runtime"
|
2017-04-17 15:17:06 +00:00
|
|
|
"strings"
|
2018-01-26 23:51:00 +00:00
|
|
|
"time"
|
2017-04-17 15:17:06 +00:00
|
|
|
|
2017-02-26 21:53:19 +00:00
|
|
|
"github.com/cenk/backoff"
|
|
|
|
dc "github.com/fsouza/go-dockerclient"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Pool represents a connection to the docker API and is used to create and remove docker images.
|
|
|
|
type Pool struct {
|
|
|
|
Client *dc.Client
|
|
|
|
MaxWait time.Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resource represents a docker container.
|
|
|
|
type Resource struct {
|
|
|
|
Container *dc.Container
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetPort returns a resource's published port. You can use it to connect to the service via localhost, e.g. tcp://localhost:1231/
|
|
|
|
func (r *Resource) GetPort(id string) string {
|
|
|
|
if r.Container == nil {
|
|
|
|
return ""
|
|
|
|
} else if r.Container.NetworkSettings == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
m, ok := r.Container.NetworkSettings.Ports[dc.Port(id)]
|
|
|
|
if !ok {
|
|
|
|
return ""
|
|
|
|
} else if len(m) == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return m[0].HostPort
|
|
|
|
}
|
|
|
|
|
2017-09-05 22:06:47 +00:00
|
|
|
func (r *Resource) GetBoundIP(id string) string {
|
|
|
|
if r.Container == nil {
|
|
|
|
return ""
|
|
|
|
} else if r.Container.NetworkSettings == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
m, ok := r.Container.NetworkSettings.Ports[dc.Port(id)]
|
|
|
|
if !ok {
|
|
|
|
return ""
|
|
|
|
} else if len(m) == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return m[0].HostIP
|
|
|
|
}
|
|
|
|
|
2017-04-17 15:17:06 +00:00
|
|
|
// NewTLSPool creates a new pool given an endpoint and the certificate path. This is required for endpoints that
|
2017-02-26 21:53:19 +00:00
|
|
|
// require TLS communication.
|
|
|
|
func NewTLSPool(endpoint, certpath string) (*Pool, error) {
|
|
|
|
ca := fmt.Sprintf("%s/ca.pem", certpath)
|
|
|
|
cert := fmt.Sprintf("%s/cert.pem", certpath)
|
|
|
|
key := fmt.Sprintf("%s/key.pem", certpath)
|
2017-04-17 15:17:06 +00:00
|
|
|
|
2017-02-26 21:53:19 +00:00
|
|
|
client, err := dc.NewTLSClient(endpoint, cert, key, ca)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "")
|
|
|
|
}
|
2017-04-17 15:17:06 +00:00
|
|
|
|
2017-02-26 21:53:19 +00:00
|
|
|
return &Pool{
|
|
|
|
Client: client,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewPool creates a new pool. You can pass an empty string to use the default, which is taken from the environment
|
2018-01-26 23:51:00 +00:00
|
|
|
// variable DOCKER_HOST and DOCKER_URL, or from docker-machine if the environment variable DOCKER_MACHINE_NAME is set,
|
2017-02-26 21:53:19 +00:00
|
|
|
// or if neither is defined a sensible default for the operating system you are on.
|
2018-01-26 23:51:00 +00:00
|
|
|
// TLS pools are automatically configured if the DOCKER_CERT_PATH environment variable exists.
|
2017-02-26 21:53:19 +00:00
|
|
|
func NewPool(endpoint string) (*Pool, error) {
|
|
|
|
if endpoint == "" {
|
2018-01-26 23:51:00 +00:00
|
|
|
if os.Getenv("DOCKER_HOST") != "" {
|
|
|
|
endpoint = os.Getenv("DOCKER_HOST")
|
|
|
|
} else if os.Getenv("DOCKER_URL") != "" {
|
2017-02-26 21:53:19 +00:00
|
|
|
endpoint = os.Getenv("DOCKER_URL")
|
|
|
|
} else if os.Getenv("DOCKER_MACHINE_NAME") != "" {
|
|
|
|
client, err := dc.NewClientFromEnv()
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Pool{Client: client}, nil
|
|
|
|
} else if runtime.GOOS == "windows" {
|
|
|
|
endpoint = "http://localhost:2375"
|
|
|
|
} else {
|
|
|
|
endpoint = "unix:///var/run/docker.sock"
|
|
|
|
}
|
|
|
|
}
|
2017-04-17 15:17:06 +00:00
|
|
|
|
2018-01-26 23:51:00 +00:00
|
|
|
if os.Getenv("DOCKER_CERT_PATH") == "" && shouldPreferTls(endpoint) {
|
|
|
|
return NewTLSPool(endpoint, os.Getenv("DOCKER_CERT_PATH"))
|
|
|
|
}
|
|
|
|
|
2017-02-26 21:53:19 +00:00
|
|
|
client, err := dc.NewClient(endpoint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Pool{
|
|
|
|
Client: client,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2018-01-26 23:51:00 +00:00
|
|
|
func shouldPreferTls(endpoint string) bool {
|
|
|
|
return !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "unix://")
|
|
|
|
}
|
|
|
|
|
2017-04-17 15:17:06 +00:00
|
|
|
// RunOptions is used to pass in optional parameters when running a container.
|
|
|
|
type RunOptions struct {
|
2018-01-26 23:51:00 +00:00
|
|
|
Hostname string
|
|
|
|
Name string
|
2017-07-18 14:15:54 +00:00
|
|
|
Repository string
|
|
|
|
Tag string
|
|
|
|
Env []string
|
|
|
|
Entrypoint []string
|
|
|
|
Cmd []string
|
|
|
|
Mounts []string
|
|
|
|
Links []string
|
|
|
|
ExposedPorts []string
|
2017-09-05 22:06:47 +00:00
|
|
|
Auth dc.AuthConfiguration
|
2018-01-26 23:51:00 +00:00
|
|
|
PortBindings map[dc.Port][]dc.PortBinding
|
|
|
|
}
|
|
|
|
|
|
|
|
// BuildAndRunWithOptions builds and starts a docker container
|
|
|
|
func (d *Pool) BuildAndRunWithOptions(dockerfilePath string, opts *RunOptions) (*Resource, error) {
|
|
|
|
// Set the Dockerfile folder as build context
|
|
|
|
dir, file := filepath.Split(dockerfilePath)
|
|
|
|
|
|
|
|
err := d.Client.BuildImage(dc.BuildImageOptions{
|
|
|
|
Name: opts.Name,
|
|
|
|
Dockerfile: file,
|
|
|
|
OutputStream: ioutil.Discard,
|
|
|
|
ContextDir: dir,
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
opts.Repository = opts.Name
|
|
|
|
|
|
|
|
return d.RunWithOptions(opts)
|
|
|
|
}
|
|
|
|
|
|
|
|
// BuildAndRun builds and starts a docker container
|
|
|
|
func (d *Pool) BuildAndRun(name, dockerfilePath string, env []string) (*Resource, error) {
|
|
|
|
return d.BuildAndRunWithOptions(dockerfilePath, &RunOptions{Name: name, Env: env})
|
2017-04-17 15:17:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// RunWithOptions starts a docker container.
|
2017-02-26 21:53:19 +00:00
|
|
|
//
|
2017-04-17 15:17:06 +00:00
|
|
|
// pool.Run(&RunOptions{Repository: "mongo", Cmd: []string{"mongod", "--smallfiles"}})
|
|
|
|
func (d *Pool) RunWithOptions(opts *RunOptions) (*Resource, error) {
|
|
|
|
repository := opts.Repository
|
|
|
|
tag := opts.Tag
|
|
|
|
env := opts.Env
|
|
|
|
cmd := opts.Cmd
|
2017-07-18 14:15:54 +00:00
|
|
|
ep := opts.Entrypoint
|
|
|
|
var exp map[dc.Port]struct{}
|
|
|
|
|
|
|
|
if len(opts.ExposedPorts) > 0 {
|
|
|
|
exp = map[dc.Port]struct{}{}
|
|
|
|
for _, p := range opts.ExposedPorts {
|
|
|
|
exp[dc.Port(p)] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
2017-04-17 15:17:06 +00:00
|
|
|
|
|
|
|
mounts := []dc.Mount{}
|
|
|
|
|
|
|
|
for _, m := range opts.Mounts {
|
|
|
|
sd := strings.Split(m, ":")
|
|
|
|
if len(sd) == 2 {
|
|
|
|
mounts = append(mounts, dc.Mount{
|
|
|
|
Source: sd[0],
|
|
|
|
Destination: sd[1],
|
|
|
|
RW: true,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
return nil, errors.Wrap(fmt.Errorf("invalid mount format: got %s, expected <src>:<dst>", m), "")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-26 21:53:19 +00:00
|
|
|
if tag == "" {
|
|
|
|
tag = "latest"
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := d.Client.InspectImage(fmt.Sprintf("%s:%s", repository, tag))
|
|
|
|
if err != nil {
|
|
|
|
if err := d.Client.PullImage(dc.PullImageOptions{
|
|
|
|
Repository: repository,
|
|
|
|
Tag: tag,
|
2017-09-05 22:06:47 +00:00
|
|
|
}, opts.Auth); err != nil {
|
2017-02-26 21:53:19 +00:00
|
|
|
return nil, errors.Wrap(err, "")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
c, err := d.Client.CreateContainer(dc.CreateContainerOptions{
|
2018-01-26 23:51:00 +00:00
|
|
|
Name: opts.Name,
|
2017-02-26 21:53:19 +00:00
|
|
|
Config: &dc.Config{
|
2018-01-26 23:51:00 +00:00
|
|
|
Hostname: opts.Hostname,
|
2017-07-18 14:15:54 +00:00
|
|
|
Image: fmt.Sprintf("%s:%s", repository, tag),
|
|
|
|
Env: env,
|
|
|
|
Entrypoint: ep,
|
|
|
|
Cmd: cmd,
|
|
|
|
Mounts: mounts,
|
|
|
|
ExposedPorts: exp,
|
2017-02-26 21:53:19 +00:00
|
|
|
},
|
|
|
|
HostConfig: &dc.HostConfig{
|
|
|
|
PublishAllPorts: true,
|
2017-04-17 15:17:06 +00:00
|
|
|
Binds: opts.Mounts,
|
2017-07-18 14:15:54 +00:00
|
|
|
Links: opts.Links,
|
2018-01-26 23:51:00 +00:00
|
|
|
PortBindings: opts.PortBindings,
|
2017-02-26 21:53:19 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := d.Client.StartContainer(c.ID, nil); err != nil {
|
|
|
|
return nil, errors.Wrap(err, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
c, err = d.Client.InspectContainer(c.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Resource{
|
|
|
|
Container: c,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2017-04-17 15:17:06 +00:00
|
|
|
// Run starts a docker container.
|
|
|
|
//
|
|
|
|
// pool.Run("mysql", "5.3", []string{"FOO=BAR", "BAR=BAZ"})
|
|
|
|
func (d *Pool) Run(repository, tag string, env []string) (*Resource, error) {
|
|
|
|
return d.RunWithOptions(&RunOptions{Repository: repository, Tag: tag, Env: env})
|
|
|
|
}
|
|
|
|
|
2017-02-26 21:53:19 +00:00
|
|
|
// Purge removes a container and linked volumes from docker.
|
|
|
|
func (d *Pool) Purge(r *Resource) error {
|
|
|
|
if err := d.Client.KillContainer(dc.KillContainerOptions{ID: r.Container.ID}); err != nil {
|
|
|
|
return errors.Wrap(err, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := d.Client.RemoveContainer(dc.RemoveContainerOptions{ID: r.Container.ID, Force: true, RemoveVolumes: true}); err != nil {
|
|
|
|
return errors.Wrap(err, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retry is an exponential backoff retry helper. You can use it to wait for e.g. mysql to boot up.
|
|
|
|
func (d *Pool) Retry(op func() error) error {
|
|
|
|
if d.MaxWait == 0 {
|
|
|
|
d.MaxWait = time.Minute
|
|
|
|
}
|
|
|
|
bo := backoff.NewExponentialBackOff()
|
|
|
|
bo.MaxInterval = time.Second * 5
|
|
|
|
bo.MaxElapsedTime = d.MaxWait
|
|
|
|
return backoff.Retry(op, bo)
|
|
|
|
}
|