[mqtt] Implements mqtt publish tooling

This commit is contained in:
2019-11-30 21:49:19 +01:00
parent 1b62903474
commit 17beea7e8a
921 changed files with 306092 additions and 0 deletions

View File

@ -0,0 +1,2 @@
debug.test
vendor

View File

@ -0,0 +1,19 @@
language: go
go:
- 1.11.4
install: true
services:
- docker
env:
- GO111MODULE=on
script:
- go mod verify
- go mod tidy
- go fmt ./...
- go vet ./...
- go test ./...

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017-2019 Gianluca Arbezzano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,118 @@
[![Build Status](https://travis-ci.org/testcontainers/testcontainers-go.svg?branch=master)](https://travis-ci.org/testcontainers/testcontainers-go)
When I was working on a Zipkin PR I discovered a nice Java library called
[testcontainers](https://www.testcontainers.org/).
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:
```go
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 and image and run a container from a Dockerfile.
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:
```go
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](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...
```go
req := ContainerRequest{
Image: "alpine",
WaitingFor: wait.ForAll(
wait.ForLog("command override!"),
),
Cmd: []string{"echo", "command override!"},
}
```

View File

@ -0,0 +1,162 @@
package testcontainers
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"
)
// DeprecatedContainer shows methods that were supported before, but are now deprecated
// Deprecated: Use Container
type DeprecatedContainer interface {
GetHostEndpoint(ctx context.Context, port string) (string, string, error)
GetIPAddress(ctx context.Context) (string, error)
LivenessCheckPorts(ctx context.Context) (nat.PortSet, error)
Terminate(ctx context.Context) error
}
// ContainerProvider allows the creation of containers on an arbitrary system
type ContainerProvider interface {
CreateContainer(context.Context, ContainerRequest) (Container, error) // create a container without starting it
RunContainer(context.Context, ContainerRequest) (Container, error) // create a container and start it
}
// Container allows getting info about and controlling a single container instance
type Container interface {
GetContainerID() string // get the container id from the provider
Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the first exposed port
PortEndpoint(context.Context, nat.Port, string) (string, error) // get proto://ip:port string for the given exposed port
Host(context.Context) (string, error) // get host where the container port is exposed
MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port
Ports(context.Context) (nat.PortMap, error) // get all exposed ports
SessionID() string // get session id
Start(context.Context) error // start the container
Terminate(context.Context) error // terminate the container
Logs(context.Context) (io.ReadCloser, error) // Get logs of the container
Name(context.Context) (string, error) // get container name
Networks(context.Context) ([]string, error) // get container networks
NetworkAliases(context.Context) (map[string][]string, error) // get container network aliases for a network
Exec(ctx context.Context, cmd []string) (int, error)
}
// ImageBuildInfo defines what is needed to build an image
type ImageBuildInfo interface {
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
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
type ContainerRequest struct {
FromDockerfile
Image string
Env map[string]string
ExposedPorts []string // allow specifying protocol info
Cmd []string
Labels map[string]string
BindMounts map[string]string
RegistryCred string
WaitingFor wait.Strategy
Name string // for specifying container name
Privileged bool // for starting privileged container
Networks []string // for specifying network names
NetworkAliases map[string][]string // for specifying network aliases
SkipReaper bool // indicates whether we skip setting up a reaper for this
}
// ProviderType is an enum for the possible providers
type ProviderType int
// possible provider types
const (
ProviderDocker ProviderType = iota // Docker is default = 0
)
// GetProvider provides the provider implementation for a certain type
func (t ProviderType) GetProvider() (GenericProvider, error) {
switch t {
case ProviderDocker:
provider, err := NewDockerProvider()
if err != nil {
return nil, errors.Wrap(err, "failed to create Docker provider")
}
return provider, nil
}
return nil, errors.New("unknown provider")
}
// Validate ensures that the ContainerRequest does not have invalid paramters configured to it
// ex. make sure you are not specifying both an image as well as a context
func (c *ContainerRequest) Validate() error {
validationMethods := []func() error{
c.validateContextAndImage,
c.validateContexOrImageIsSpecified,
}
var err error
for _, validationMethod := range validationMethods {
err = validationMethod()
if err != nil {
return err
}
}
return nil
}
// GetContext retrieve the build context for the request
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"
func (c *ContainerRequest) GetDockerfile() string {
f := c.FromDockerfile.Dockerfile
if f == "" {
return "Dockerfile"
}
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
}

View File

@ -0,0 +1,612 @@
package testcontainers
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"os/exec"
"strings"
"time"
"github.com/cenkalti/backoff"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
"github.com/testcontainers/testcontainers-go/wait"
)
// Implement interfaces
var _ Container = (*DockerContainer)(nil)
// DockerContainer represents a container started using Docker
type DockerContainer struct {
// Container ID from Docker
ID string
WaitingFor wait.Strategy
// Cache to retrieve container infromation without re-fetching them from dockerd
raw *types.ContainerJSON
provider *DockerProvider
sessionID uuid.UUID
terminationSignal chan bool
skipReaper bool
}
func (c *DockerContainer) GetContainerID() string {
return c.ID
}
// Endpoint gets proto://host:port string for the first exposed port
// Will returns just host:port if proto is ""
func (c *DockerContainer) Endpoint(ctx context.Context, proto string) (string, error) {
ports, err := c.Ports(ctx)
if err != nil {
return "", err
}
// get first port
var firstPort nat.Port
for p := range ports {
firstPort = p
break
}
return c.PortEndpoint(ctx, firstPort, proto)
}
// PortEndpoint gets proto://host:port string for the given exposed port
// Will returns just host:port if proto is ""
func (c *DockerContainer) PortEndpoint(ctx context.Context, port nat.Port, proto string) (string, error) {
host, err := c.Host(ctx)
if err != nil {
return "", err
}
outerPort, err := c.MappedPort(ctx, port)
if err != nil {
return "", err
}
protoFull := ""
if proto != "" {
protoFull = fmt.Sprintf("%s://", proto)
}
return fmt.Sprintf("%s%s:%s", protoFull, host, outerPort.Port()), nil
}
// Host gets host (ip or name) of the docker daemon where the container port is exposed
// Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel
// You can use the "TC_HOST" env variable to set this yourself
func (c *DockerContainer) Host(ctx context.Context) (string, error) {
host, err := c.provider.daemonHost()
if err != nil {
return "", err
}
return host, nil
}
// MappedPort gets externally mapped port for a container port
func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Port, error) {
ports, err := c.Ports(ctx)
if err != nil {
return "", err
}
for k, p := range ports {
if k.Port() != port.Port() {
continue
}
if port.Proto() != "" && k.Proto() != port.Proto() {
continue
}
return nat.NewPort(k.Proto(), p[0].HostPort)
}
return "", errors.New("port not found")
}
// Ports gets the exposed ports for the container.
func (c *DockerContainer) Ports(ctx context.Context) (nat.PortMap, error) {
inspect, err := c.inspectContainer(ctx)
if err != nil {
return nil, err
}
return inspect.NetworkSettings.Ports, nil
}
// SessionID gets the current session id
func (c *DockerContainer) SessionID() string {
return c.sessionID.String()
}
// Start will start an already created container
func (c *DockerContainer) Start(ctx context.Context) error {
if err := c.provider.client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil {
return err
}
// if a Wait Strategy has been specified, wait before returning
if c.WaitingFor != nil {
if err := c.WaitingFor.WaitUntilReady(ctx, c); err != nil {
return err
}
}
return nil
}
// Terminate is used to kill the container. It is usally triggered by as defer function.
func (c *DockerContainer) Terminate(ctx context.Context) error {
err := c.provider.client.ContainerRemove(ctx, c.GetContainerID(), types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
})
if err == nil {
c.sessionID = uuid.UUID{}
c.raw = nil
}
return err
}
func (c *DockerContainer) inspectContainer(ctx context.Context) (*types.ContainerJSON, error) {
if c.raw != nil {
return c.raw, nil
}
inspect, err := c.provider.client.ContainerInspect(ctx, c.ID)
if err != nil {
return nil, err
}
c.raw = &inspect
return c.raw, nil
}
// Logs will fetch both STDOUT and STDERR from the current container. Returns a
// ReadCloser and leaves it up to the caller to extract what it wants.
func (c *DockerContainer) Logs(ctx context.Context) (io.ReadCloser, error) {
options := types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
}
return c.provider.client.ContainerLogs(ctx, c.ID, options)
}
// Name gets the name of the container.
func (c *DockerContainer) Name(ctx context.Context) (string, error) {
inspect, err := c.inspectContainer(ctx)
if err != nil {
return "", err
}
return inspect.Name, nil
}
// Networks gets the names of the networks the container is attached to.
func (c *DockerContainer) Networks(ctx context.Context) ([]string, error) {
inspect, err := c.inspectContainer(ctx)
if err != nil {
return []string{}, err
}
networks := inspect.NetworkSettings.Networks
n := []string{}
for k := range networks {
n = append(n, k)
}
return n, nil
}
// NetworkAliases gets the aliases of the container for the networks it is attached to.
func (c *DockerContainer) NetworkAliases(ctx context.Context) (map[string][]string, error) {
inspect, err := c.inspectContainer(ctx)
if err != nil {
return map[string][]string{}, err
}
networks := inspect.NetworkSettings.Networks
a := map[string][]string{}
for k := range networks {
a[k] = networks[k].Aliases
}
return a, nil
}
func (c *DockerContainer) Exec(ctx context.Context, cmd []string) (int, error) {
cli := c.provider.client
response, err := cli.ContainerExecCreate(ctx, c.ID, types.ExecConfig{
Cmd: cmd,
Detach: false,
})
if err != nil {
return 0, err
}
err = cli.ContainerExecStart(ctx, response.ID, types.ExecStartCheck{
Detach: false,
})
if err != nil {
return 0, err
}
var exitCode int
for {
execResp, err := cli.ContainerExecInspect(ctx, response.ID)
if err != nil {
return 0, err
}
if !execResp.Running {
exitCode = execResp.ExitCode
break
}
time.Sleep(100 * time.Millisecond)
}
return exitCode, nil
}
// DockerNetwork represents a network started using Docker
type DockerNetwork struct {
ID string // Network ID from Docker
Driver string
Name string
provider *DockerProvider
terminationSignal chan bool
}
// Remove is used to remove the network. It is usually triggered by as defer function.
func (n *DockerNetwork) Remove(_ context.Context) error {
if n.terminationSignal != nil {
n.terminationSignal <- true
}
return nil
}
// DockerProvider implements the ContainerProvider interface
type DockerProvider struct {
client *client.Client
hostCache string
}
var _ ContainerProvider = (*DockerProvider)(nil)
// NewDockerProvider creates a Docker provider with the EnvClient
func NewDockerProvider() (*DockerProvider, error) {
client, err := client.NewEnvClient()
if err != nil {
return nil, err
}
client.NegotiateAPIVersion(context.Background())
p := &DockerProvider{
client: client,
}
return p, nil
}
// BuildImage will build and image from context and Dockerfile, then return the tag
func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (string, error) {
repo := uuid.NewV4()
tag := uuid.NewV4()
repoTag := fmt.Sprintf("%s:%s", repo, tag)
buildContext, err := img.GetContext()
if err != nil {
return "", err
}
buildOptions := types.ImageBuildOptions{
Dockerfile: img.GetDockerfile(),
Context: buildContext,
Tags: []string{repoTag},
}
resp, err := p.client.ImageBuild(ctx, buildContext, buildOptions)
if err != nil {
return "", err
}
// need to read the response from Docker, I think otherwise the image
// might not finish building before continuing to execute here
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(resp.Body)
if err != nil {
return "", err
}
resp.Body.Close()
return repoTag, nil
}
// CreateContainer fulfills a request for a container without starting it
func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerRequest) (Container, error) {
exposedPortSet, exposedPortMap, err := nat.ParsePortSpecs(req.ExposedPorts)
if err != nil {
return nil, err
}
env := []string{}
for envKey, envVar := range req.Env {
env = append(env, envKey+"="+envVar)
}
if req.Labels == nil {
req.Labels = make(map[string]string)
}
sessionID := uuid.NewV4()
var termSignal chan bool
if !req.SkipReaper {
r, err := NewReaper(ctx, sessionID.String(), p)
if err != nil {
return nil, errors.Wrap(err, "creating reaper failed")
}
termSignal, err = r.Connect()
if err != nil {
return nil, errors.Wrap(err, "connecting to reaper failed")
}
for k, v := range r.Labels() {
if _, ok := req.Labels[k]; !ok {
req.Labels[k] = v
}
}
}
if err = req.Validate(); err != nil {
return nil, err
}
var tag string
if req.ShouldBuildImage() {
tag, err = p.BuildImage(ctx, &req)
if err != nil {
return nil, err
}
} else {
tag = req.Image
_, _, err = p.client.ImageInspectWithRaw(ctx, tag)
if err != nil {
if client.IsErrNotFound(err) {
pullOpt := types.ImagePullOptions{}
if req.RegistryCred != "" {
pullOpt.RegistryAuth = req.RegistryCred
}
var pull io.ReadCloser
err := backoff.Retry(func() error {
var err error
pull, err = p.client.ImagePull(ctx, tag, pullOpt)
return err
}, backoff.NewExponentialBackOff())
if err != nil {
return nil, err
}
defer pull.Close()
// download of docker image finishes at EOF of the pull request
_, err = ioutil.ReadAll(pull)
if err != nil {
return nil, err
}
} else {
return nil, err
}
}
}
dockerInput := &container.Config{
Image: tag,
Env: env,
ExposedPorts: exposedPortSet,
Labels: req.Labels,
Cmd: req.Cmd,
}
// prepare mounts
bindMounts := []mount.Mount{}
for hostPath, innerPath := range req.BindMounts {
bindMounts = append(bindMounts, mount.Mount{
Type: mount.TypeBind,
Source: hostPath,
Target: innerPath,
})
}
hostConfig := &container.HostConfig{
PortBindings: exposedPortMap,
Mounts: bindMounts,
AutoRemove: true,
Privileged: req.Privileged,
}
endpointConfigs := map[string]*network.EndpointSettings{}
for _, n := range req.Networks {
nw, err := p.GetNetwork(ctx, NetworkRequest{
Name: n,
})
if err == nil {
endpointSetting := network.EndpointSettings{
Aliases: req.NetworkAliases[n],
NetworkID: nw.ID,
}
endpointConfigs[n] = &endpointSetting
}
}
networkingConfig := network.NetworkingConfig{
EndpointsConfig: endpointConfigs,
}
resp, err := p.client.ContainerCreate(ctx, dockerInput, hostConfig, &networkingConfig, req.Name)
if err != nil {
return nil, err
}
c := &DockerContainer{
ID: resp.ID,
WaitingFor: req.WaitingFor,
sessionID: sessionID,
provider: p,
terminationSignal: termSignal,
skipReaper: req.SkipReaper,
}
return c, nil
}
// RunContainer takes a RequestContainer as input and it runs a container via the docker sdk
func (p *DockerProvider) RunContainer(ctx context.Context, req ContainerRequest) (Container, error) {
c, err := p.CreateContainer(ctx, req)
if err != nil {
return nil, err
}
if err := c.Start(ctx); err != nil {
return c, errors.Wrap(err, "could not start container")
}
return c, nil
}
// daemonHost gets the host or ip of the Docker daemon where ports are exposed on
// Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel
// You can use the "TC_HOST" env variable to set this yourself
func (p *DockerProvider) daemonHost() (string, error) {
if p.hostCache != "" {
return p.hostCache, nil
}
host, exists := os.LookupEnv("TC_HOST")
if exists {
p.hostCache = host
return p.hostCache, nil
}
// infer from Docker host
url, err := url.Parse(p.client.DaemonHost())
if err != nil {
return "", err
}
switch url.Scheme {
case "http", "https", "tcp":
p.hostCache = url.Hostname()
case "unix", "npipe":
if inAContainer() {
ip, err := getGatewayIp()
if err != nil {
return "", err
}
p.hostCache = ip
} else {
p.hostCache = "localhost"
}
default:
return "", errors.New("Could not determine host through env or docker host")
}
return p.hostCache, nil
}
// CreateNetwork returns the object representing a new network identified by its name
func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) (Network, error) {
if req.Labels == nil {
req.Labels = make(map[string]string)
}
nc := types.NetworkCreate{
Driver: req.Driver,
CheckDuplicate: req.CheckDuplicate,
Internal: req.Internal,
EnableIPv6: req.EnableIPv6,
Attachable: req.Attachable,
Labels: req.Labels,
}
sessionID := uuid.NewV4()
var termSignal chan bool
if !req.SkipReaper {
r, err := NewReaper(ctx, sessionID.String(), p)
if err != nil {
return nil, errors.Wrap(err, "creating network reaper failed")
}
termSignal, err = r.Connect()
if err != nil {
return nil, errors.Wrap(err, "connecting to network reaper failed")
}
for k, v := range r.Labels() {
if _, ok := req.Labels[k]; !ok {
req.Labels[k] = v
}
}
}
response, err := p.client.NetworkCreate(ctx, req.Name, nc)
if err != nil {
return &DockerNetwork{}, err
}
n := &DockerNetwork{
ID: response.ID,
Driver: req.Driver,
Name: req.Name,
terminationSignal: termSignal,
}
return n, nil
}
// GetNetwork returns the object representing the network identified by its name
func (p *DockerProvider) GetNetwork(ctx context.Context, req NetworkRequest) (types.NetworkResource, error) {
networkResource, err := p.client.NetworkInspect(ctx, req.Name, types.NetworkInspectOptions{
Verbose: true,
})
if err != nil {
return types.NetworkResource{}, err
}
return networkResource, err
}
func inAContainer() bool {
// see https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L15
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
return false
}
func getGatewayIp() (string, error) {
// see https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L27
cmd := exec.Command("sh", "-c", "ip route|awk '/default/ { print $3 }'")
stdout, err := cmd.Output()
if err != nil {
return "", errors.New("Failed to detect docker host")
}
ip := strings.TrimSpace(string(stdout))
if len(ip) == 0 {
return "", errors.New("Failed to parse default gateway IP")
}
return string(ip), nil
}

View File

@ -0,0 +1,41 @@
package testcontainers
import (
"context"
"github.com/pkg/errors"
)
// GenericContainerRequest represents parameters to a generic container
type GenericContainerRequest struct {
ContainerRequest // embedded request for provider
Started bool // whether to auto-start the container
ProviderType ProviderType // which provider to use, Docker if empty
}
// GenericContainer creates a generic container with parameters
func GenericContainer(ctx context.Context, req GenericContainerRequest) (Container, error) {
provider, err := req.ProviderType.GetProvider()
if err != nil {
return nil, err
}
c, err := provider.CreateContainer(ctx, req.ContainerRequest)
if err != nil {
return nil, errors.Wrap(err, "failed to create container")
}
if req.Started {
if err := c.Start(ctx); err != nil {
return c, errors.Wrap(err, "failed to start container")
}
}
return c, nil
}
// GenericProvider represents an abstraction for container and network providers
type GenericProvider interface {
ContainerProvider
NetworkProvider
}

View File

@ -0,0 +1,36 @@
module github.com/testcontainers/testcontainers-go
replace github.com/docker/docker => github.com/docker/engine v0.0.0-20190717161051-705d9623b7c1
require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Microsoft/go-winio v0.4.11 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc // indirect
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible // indirect
github.com/docker/docker v0.7.3-0.20190506211059-b20a14b54661
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.3.3 // indirect
github.com/go-redis/redis v6.15.6+incompatible
github.com/go-sql-driver/mysql v1.4.1
github.com/gogo/protobuf v1.2.0 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v0.1.1 // indirect
github.com/pkg/errors v0.8.1
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.2.0 // indirect
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/grpc v1.17.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gotest.tools v0.0.0-20181223230014-1083505acf35 // indirect
)
go 1.13

View File

@ -0,0 +1,109 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible h1:dvc1KSkIYTVjZgHf/CTC2diTYC8PzhaA5sFISRfNVrE=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/engine v0.0.0-20190717161051-705d9623b7c1 h1:pKV3lCoWunXtXfyRUcqYflvdaiFU3BMxHw5izMsYDhY=
github.com/docker/engine v0.0.0-20190717161051-705d9623b7c1/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis v6.15.5+incompatible h1:pLky8I0rgiblWfa8C1EV7fPEUv0aH6vKRaYHc/YRHVk=
github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg=
github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb h1:pf3XwC90UUdNPYWZdFjhGBE7DUFuK3Ct1zWmZ65QN30=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180810170437-e96c4e24768d/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v0.0.0-20181223230014-1083505acf35 h1:zpdCK+REwbk+rqjJmHhiCN6iBIigrZ39glqSF0P3KF0=
gotest.tools v0.0.0-20181223230014-1083505acf35/go.mod h1:R//lfYlUuTOTfblYI3lGoAAAebUdzjvbmQsuB7Ykd90=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -0,0 +1,31 @@
package testcontainers
import (
"context"
"github.com/docker/docker/api/types"
)
// NetworkProvider allows the creation of networks on an arbitrary system
type NetworkProvider interface {
CreateNetwork(context.Context, NetworkRequest) (Network, error) // create a network
GetNetwork(context.Context, NetworkRequest) (types.NetworkResource, error) // get a network
}
// Network allows getting info about a single network instance
type Network interface {
Remove(context.Context) error // removes the network
}
// NetworkRequest represents the parameters used to get a network
type NetworkRequest struct {
Driver string
CheckDuplicate bool
Internal bool
EnableIPv6 bool
Name string
Labels map[string]string
Attachable bool
SkipReaper bool // indicates whether we skip setting up a reaper for this
}

View File

@ -0,0 +1,118 @@
package testcontainers
import (
"bufio"
"context"
"fmt"
"net"
"strings"
"time"
"github.com/pkg/errors"
)
// TestcontainerLabel is used as a base for docker labels
const (
TestcontainerLabel = "org.testcontainers.golang"
TestcontainerLabelSessionID = TestcontainerLabel + ".sessionId"
TestcontainerLabelIsReaper = TestcontainerLabel + ".reaper"
ReaperDefaultImage = "quay.io/testcontainers/ryuk:0.2.2"
)
// ReaperProvider represents a provider for the reaper to run itself with
// The ContainerProvider interface should usually satisfy this as well, so it is pluggable
type ReaperProvider interface {
RunContainer(ctx context.Context, req ContainerRequest) (Container, error)
}
// Reaper is used to start a sidecar container that cleans up resources
type Reaper struct {
Provider ReaperProvider
SessionID string
Endpoint string
}
// NewReaper creates a Reaper with a sessionID to identify containers and a provider to use
func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) {
r := &Reaper{
Provider: provider,
SessionID: sessionID,
}
// TODO: reuse reaper if there already is one
req := ContainerRequest{
Image: ReaperDefaultImage,
ExposedPorts: []string{"8080"},
Labels: map[string]string{
TestcontainerLabel: "true",
TestcontainerLabelIsReaper: "true",
},
SkipReaper: true,
BindMounts: map[string]string{
"/var/run/docker.sock": "/var/run/docker.sock",
},
}
c, err := provider.RunContainer(ctx, req)
if err != nil {
return nil, err
}
endpoint, err := c.PortEndpoint(ctx, "8080", "")
if err != nil {
return nil, err
}
r.Endpoint = endpoint
return r, nil
}
// Connect runs a goroutine which can be terminated by sending true into the returned channel
func (r *Reaper) Connect() (chan bool, error) {
conn, err := net.DialTimeout("tcp", r.Endpoint, 10*time.Second)
if err != nil {
return nil, errors.Wrap(err, "Connecting to Ryuk on "+r.Endpoint+" failed")
}
terminationSignal := make(chan bool)
go func(conn net.Conn) {
sock := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
defer conn.Close()
labelFilters := []string{}
for l, v := range r.Labels() {
labelFilters = append(labelFilters, fmt.Sprintf("label=%s=%s", l, v))
}
retryLimit := 3
for retryLimit > 0 {
retryLimit--
sock.WriteString(strings.Join(labelFilters, "&"))
sock.WriteString("\n")
if err := sock.Flush(); err != nil {
continue
}
resp, err := sock.ReadString('\n')
if err != nil {
continue
}
if resp == "ACK\n" {
break
}
}
<-terminationSignal
}(conn)
return terminationSignal, nil
}
// Labels returns the container labels to use so that this Reaper cleans them up
func (r *Reaper) Labels() map[string]string {
return map[string]string{
TestcontainerLabel: "true",
TestcontainerLabelSessionID: r.SessionID,
}
}

View File

@ -0,0 +1,112 @@
package wait
import (
"context"
"fmt"
"github.com/pkg/errors"
"net"
"os"
"strconv"
"syscall"
"time"
"github.com/docker/go-connections/nat"
)
// Implement interface
var _ Strategy = (*HostPortStrategy)(nil)
type HostPortStrategy struct {
Port nat.Port
// all WaitStrategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
}
// NewHostPortStrategy constructs a default host port strategy
func NewHostPortStrategy(port nat.Port) *HostPortStrategy {
return &HostPortStrategy{
Port: port,
startupTimeout: defaultStartupTimeout(),
}
}
// fluent builders for each property
// since go has neither covariance nor generics, the return type must be the type of the concrete implementation
// this is true for all properties, even the "shared" ones like startupTimeout
// ForListeningPort is a helper similar to those in Wait.java
// https://github.com/testcontainers/testcontainers-java/blob/1d85a3834bd937f80aad3a4cec249c027f31aeb4/core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java
func ForListeningPort(port nat.Port) *HostPortStrategy {
return NewHostPortStrategy(port)
}
func (hp *HostPortStrategy) WithStartupTimeout(startupTimeout time.Duration) *HostPortStrategy {
hp.startupTimeout = startupTimeout
return hp
}
// WaitUntilReady implements Strategy.WaitUntilReady
func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
// limit context to startupTimeout
ctx, cancelContext := context.WithTimeout(ctx, hp.startupTimeout)
defer cancelContext()
ipAddress, err := target.Host(ctx)
if err != nil {
return
}
port, err := target.MappedPort(ctx, hp.Port)
if err != nil {
return
}
proto := port.Proto()
portNumber := port.Int()
portString := strconv.Itoa(portNumber)
//external check
dialer := net.Dialer{}
address := net.JoinHostPort(ipAddress, portString)
for {
conn, err := dialer.DialContext(ctx, proto, address)
defer conn.Close()
if err != nil {
if v, ok := err.(*net.OpError); ok {
if v2, ok := (v.Err).(*os.SyscallError); ok {
if v2.Err == syscall.ECONNREFUSED {
time.Sleep(100 * time.Millisecond)
continue
}
}
}
return err
}
break
}
//internal check
command := buildInternalCheckCommand(hp.Port.Int())
for {
exitCode, err := target.Exec(ctx, []string{"/bin/bash", "-c", command})
if err != nil {
return errors.Wrapf(err, "host port waiting failed")
}
if exitCode == 0 {
break
}
}
return nil
}
func buildInternalCheckCommand(internalPort int) string {
command := `(
cat /proc/net/tcp{,6} | awk '{print $2}' | grep -i :%x ||
nc -vz -w 1 localhost %d ||
/bin/bash -c '</dev/tcp/localhost/%d'
)
`
return "true && " + fmt.Sprintf(command, internalPort, internalPort, internalPort)
}

View File

@ -0,0 +1,147 @@
package wait
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"strconv"
"time"
"github.com/docker/go-connections/nat"
)
// Implement interface
var _ Strategy = (*HTTPStrategy)(nil)
type HTTPStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
// additional properties
Port nat.Port
Path string
StatusCodeMatcher func(status int) bool
UseTLS bool
AllowInsecure bool
}
// NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200
func NewHTTPStrategy(path string) *HTTPStrategy {
return &HTTPStrategy{
startupTimeout: defaultStartupTimeout(),
Port: "80/tcp",
Path: path,
StatusCodeMatcher: defaultStatusCodeMatcher,
UseTLS: false,
}
}
func defaultStatusCodeMatcher(status int) bool {
return status == http.StatusOK
}
// fluent builders for each property
// since go has neither covariance nor generics, the return type must be the type of the concrete implementation
// this is true for all properties, even the "shared" ones like startupTimeout
func (ws *HTTPStrategy) WithStartupTimeout(startupTimeout time.Duration) *HTTPStrategy {
ws.startupTimeout = startupTimeout
return ws
}
func (ws *HTTPStrategy) WithPort(port nat.Port) *HTTPStrategy {
ws.Port = port
return ws
}
func (ws *HTTPStrategy) WithStatusCodeMatcher(statusCodeMatcher func(status int) bool) *HTTPStrategy {
ws.StatusCodeMatcher = statusCodeMatcher
return ws
}
func (ws *HTTPStrategy) WithTLS(useTLS bool) *HTTPStrategy {
ws.UseTLS = useTLS
return ws
}
func (ws *HTTPStrategy) WithAllowInsecure(allowInsecure bool) *HTTPStrategy {
ws.AllowInsecure = allowInsecure
return ws
}
// ForHTTP is a convenience method similar to Wait.java
// https://github.com/testcontainers/testcontainers-java/blob/1d85a3834bd937f80aad3a4cec249c027f31aeb4/core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java
func ForHTTP(path string) *HTTPStrategy {
return NewHTTPStrategy(path)
}
// WaitUntilReady implements Strategy.WaitUntilReady
func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
// limit context to startupTimeout
ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout)
defer cancelContext()
ipAddress, err := target.Host(ctx)
if err != nil {
return
}
port, err := target.MappedPort(ctx, ws.Port)
if err != nil {
return
}
if port.Proto() != "tcp" {
return errors.New("Cannot use HTTP client on non-TCP ports")
}
portNumber := port.Int()
portString := strconv.Itoa(portNumber)
address := net.JoinHostPort(ipAddress, portString)
var proto string
if ws.UseTLS {
proto = "https"
} else {
proto = "http"
}
url := fmt.Sprintf("%s://%s%s", proto, address, ws.Path)
tripper := http.DefaultTransport
if ws.AllowInsecure {
tripper.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
client := http.Client{Timeout: ws.startupTimeout, Transport: tripper}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req = req.WithContext(ctx)
Retry:
for {
select {
case <-ctx.Done():
break Retry
default:
resp, err := client.Do(req)
if err != nil || !ws.StatusCodeMatcher(resp.StatusCode) {
time.Sleep(100 * time.Millisecond)
continue
}
break Retry
}
}
return nil
}

View File

@ -0,0 +1,104 @@
package wait
import (
"context"
"io/ioutil"
"strings"
"time"
)
// Implement interface
var _ Strategy = (*LogStrategy)(nil)
// LogStrategy will wait until a given log entry shows up in the docker logs
type LogStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
// additional properties
Log string
PollInterval time.Duration
Occurrence int
}
// NewLogStrategy constructs a HTTP strategy waiting on port 80 and status code 200
func NewLogStrategy(log string) *LogStrategy {
return &LogStrategy{
startupTimeout: defaultStartupTimeout(),
Log: log,
PollInterval: 100 * time.Millisecond,
Occurrence: 1,
}
}
// fluent builders for each property
// since go has neither covariance nor generics, the return type must be the type of the concrete implementation
// this is true for all properties, even the "shared" ones like startupTimeout
// WithStartupTimeout can be used to change the default startup timeout
func (ws *LogStrategy) WithStartupTimeout(startupTimeout time.Duration) *LogStrategy {
ws.startupTimeout = startupTimeout
return ws
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (ws *LogStrategy) WithPollInterval(pollInterval time.Duration) *LogStrategy {
ws.PollInterval = pollInterval
return ws
}
func (ws *LogStrategy) WithOccurrence(o int) *LogStrategy {
// the number of occurence needs to be positive
if o <= 0 {
o = 1
}
ws.Occurrence = o
return ws
}
// ForLog is the default construction for the fluid interface.
//
// For Example:
// wait.
// ForLog("some text").
// WithPollInterval(1 * time.Second)
func ForLog(log string) *LogStrategy {
return NewLogStrategy(log)
}
// WaitUntilReady implements Strategy.WaitUntilReady
func (ws *LogStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
// limit context to startupTimeout
ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout)
defer cancelContext()
currentOccurence := 0
LOOP:
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
reader, err := target.Logs(ctx)
if err != nil {
time.Sleep(ws.PollInterval)
continue
}
b, err := ioutil.ReadAll(reader)
logs := string(b)
if strings.Contains(logs, ws.Log) {
currentOccurence++
if ws.Occurrence == 0 || currentOccurence >= ws.Occurrence-1 {
break LOOP
}
} else {
time.Sleep(ws.PollInterval)
continue
}
}
}
return nil
}

View File

@ -0,0 +1,47 @@
package wait
import (
"context"
"fmt"
"time"
)
// Implement interface
var _ Strategy = (*MultiStrategy)(nil)
type MultiStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
// additional properties
Strategies []Strategy
}
func (ms *MultiStrategy) WithStartupTimeout(startupTimeout time.Duration) *MultiStrategy {
ms.startupTimeout = startupTimeout
return ms
}
func ForAll(strategies ...Strategy) *MultiStrategy {
return &MultiStrategy{
startupTimeout: defaultStartupTimeout(),
Strategies: strategies,
}
}
func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
ctx, cancelContext := context.WithTimeout(ctx, ms.startupTimeout)
defer cancelContext()
if len(ms.Strategies) == 0 {
return fmt.Errorf("no wait strategy supplied")
}
for _, strategy := range ms.Strategies {
err := strategy.WaitUntilReady(ctx, target)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,24 @@
package wait
import (
"context"
"io"
"time"
"github.com/docker/go-connections/nat"
)
type Strategy interface {
WaitUntilReady(context.Context, StrategyTarget) error
}
type StrategyTarget interface {
Host(context.Context) (string, error)
MappedPort(context.Context, nat.Port) (nat.Port, error)
Logs(context.Context) (io.ReadCloser, error)
Exec(ctx context.Context, cmd []string) (int, error)
}
func defaultStartupTimeout() time.Duration {
return 60 * time.Second
}