diff --git a/README.md b/README.md index ef8c7290b7..6faa94919a 100644 --- a/README.md +++ b/README.md @@ -158,3 +158,71 @@ if err != nil { // do something with err } ``` + +## Using Docker Compose + +Similar to generic containers support, it's also possible to run a bespoke set of services specified in a docker-compose.yml file. + +This is intended to be useful on projects where Docker Compose is already used in dev or other environments to define services that an application may be dependent upon. + +You can override Testcontainers' default behaviour and make it use a docker-compose binary installed on the local machine. This will generally yield an experience that is closer to running docker-compose locally, with the caveat that Docker Compose needs to be present on dev and CI machines. + +### Examples + +```go +composeFilePaths := "testresources/docker-compose.yml" +identifier := strings.ToLower(uuid.New().String()) + +compose := tc.NewLocalDockerCompose(composeFilePaths, identifier) +execError := compose. + WithCommand([]string{"up", "-d"}). + WithEnv(map[string]string { + "key1": "value1", + "key2": "value2", + }). + Invoke() +err := execError.Error +if err != nil { + return fmt.Errorf("Could not run compose file: %v - %v", composeFilePaths, err) +} +return nil +``` + +Note that the environment variables in the `env` map will be applied, if possible, to the existing variables declared in the docker compose file. + +In the following example, we demonstrate how to stop a Docker compose using the convenient `Down` method. + +```go +composeFilePaths := "testresources/docker-compose.yml" + +compose := tc.NewLocalDockerCompose(composeFilePaths, identifierFromExistingRunningCompose) +execError := compose.Down() +err := execError.Error +if err != nil { + return fmt.Errorf("Could not run compose file: %v - %v", composeFilePaths, err) +} +return nil +``` + +## Troubleshooting Travis + +If you want to reproduce a Travis build locally, please follow this instructions to spin up a Travis build agent locally: +```shell +export BUILDID="build-testcontainers" +export INSTANCE="travisci/ci-sardonyx:packer-1564753982-0c06deb6" +docker run --name $BUILDID -w /root/go/src/github.com/testcontainers/testcontainers-go -v /Users/mdelapenya/sourcecode/src/github.com/mdelapenya/testcontainers-go:/root/go/src/github.com/testcontainers/testcontainers-go -v /var/run/docker.sock:/var/run/docker.sock -dit $INSTANCE /sbin/init +``` + +Once the container has been created, enter it (`docker exec -ti $BUILDID bash`) and reproduce Travis steps: + +```shell +eval "$(gimme 1.11.4)" +export GO111MODULE=on +export GOPATH="/root/go" +export PATH="$GOPATH/bin:$PATH" +go get gotest.tools/gotestsum +go mod tidy +go fmt ./... +go vet ./... +gotestsum --format short-verbose ./... +``` diff --git a/compose.go b/compose.go new file mode 100644 index 0000000000..53914780aa --- /dev/null +++ b/compose.go @@ -0,0 +1,269 @@ +package testcontainers + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + + "gopkg.in/yaml.v2" +) + +const ( + envProjectName = "COMPOSE_PROJECT_NAME" + envComposeFile = "COMPOSE_FILE" +) + +// DockerCompose defines the contract for running Docker Compose +type DockerCompose interface { + Down() ExecError + Invoke() ExecError + WithCommand([]string) DockerCompose + WithEnv(map[string]string) DockerCompose +} + +// LocalDockerCompose represents a Docker Compose execution using local binary +// docker-compose or docker-compose.exe, depending on the underlying platform +type LocalDockerCompose struct { + Executable string + ComposeFilePaths []string + absComposeFilePaths []string + Identifier string + Cmd []string + Env map[string]string + Services map[string]interface{} +} + +// NewLocalDockerCompose returns an instance of the local Docker Compose, using an +// array of Docker Compose file paths and an identifier for the Compose execution. +// +// It will iterate through the array adding '-f compose-file-path' flags to the local +// Docker Compose execution. The identifier represents the name of the execution, +// which will define the name of the underlying Docker network and the name of the +// running Compose services. +func NewLocalDockerCompose(filePaths []string, identifier string) *LocalDockerCompose { + dc := &LocalDockerCompose{} + + dc.Executable = "docker-compose" + if runtime.GOOS == "windows" { + dc.Executable = "docker-compose.exe" + } + + dc.ComposeFilePaths = filePaths + + dc.absComposeFilePaths = make([]string, len(filePaths)) + for i, cfp := range dc.ComposeFilePaths { + abs, _ := filepath.Abs(cfp) + dc.absComposeFilePaths[i] = abs + } + + dc.validate() + + dc.Identifier = strings.ToLower(identifier) + + return dc +} + +// Down executes docker-compose down +func (dc *LocalDockerCompose) Down() ExecError { + return executeCompose(dc, []string{"down"}) +} + +func (dc *LocalDockerCompose) getDockerComposeEnvironment() map[string]string { + environment := map[string]string{} + + composeFileEnvVariableValue := "" + for _, abs := range dc.absComposeFilePaths { + composeFileEnvVariableValue += abs + string(os.PathListSeparator) + } + + environment[envProjectName] = dc.Identifier + environment[envComposeFile] = composeFileEnvVariableValue + + return environment +} + +// Invoke invokes the docker compose +func (dc *LocalDockerCompose) Invoke() ExecError { + return executeCompose(dc, dc.Cmd) +} + +// WithCommand assigns the command +func (dc *LocalDockerCompose) WithCommand(cmd []string) DockerCompose { + dc.Cmd = cmd + return dc +} + +// WithEnv assigns the environment +func (dc *LocalDockerCompose) WithEnv(env map[string]string) DockerCompose { + dc.Env = env + return dc +} + +// validate checks if the files to be run in the compose are valid YAML files, setting up +// references to all services in them +func (dc *LocalDockerCompose) validate() error { + type compose struct { + Services map[string]interface{} + } + + for _, abs := range dc.absComposeFilePaths { + c := compose{} + + yamlFile, err := ioutil.ReadFile(abs) + if err != nil { + return err + } + err = yaml.Unmarshal(yamlFile, &c) + if err != nil { + return err + } + + dc.Services = c.Services + } + + return nil +} + +// ExecError is super struct that holds any information about an execution error, so the client code +// can handle the result +type ExecError struct { + Command []string + Error error + Stdout error + Stderr error +} + +// execute executes a program with arguments and environment variables inside a specific directory +func execute( + dirContext string, environment map[string]string, binary string, args []string) ExecError { + + var errStdout, errStderr error + + cmd := exec.Command(binary, args...) + cmd.Dir = dirContext + cmd.Env = os.Environ() + + for key, value := range environment { + cmd.Env = append(cmd.Env, key+"="+value) + } + + stdoutIn, _ := cmd.StdoutPipe() + stderrIn, _ := cmd.StderrPipe() + + stdout := newCapturingPassThroughWriter(os.Stdout) + stderr := newCapturingPassThroughWriter(os.Stderr) + + err := cmd.Start() + if err != nil { + execCmd := []string{"Starting command", dirContext, binary} + execCmd = append(execCmd, args...) + + return ExecError{ + // add information about the CMD and arguments used + Command: execCmd, + Error: err, + Stderr: errStderr, + Stdout: errStdout, + } + } + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + _, errStdout = io.Copy(stdout, stdoutIn) + wg.Done() + }() + + _, errStderr = io.Copy(stderr, stderrIn) + wg.Wait() + + err = cmd.Wait() + + execCmd := []string{"Reading std", dirContext, binary} + execCmd = append(execCmd, args...) + + return ExecError{ + Command: execCmd, + Error: err, + Stderr: errStderr, + Stdout: errStdout, + } +} + +func executeCompose(dc *LocalDockerCompose, args []string) ExecError { + if which(dc.Executable) != nil { + return ExecError{ + Command: []string{dc.Executable}, + Error: fmt.Errorf("Local Docker Compose not found. Is %s on the PATH?", dc.Executable), + } + } + + environment := dc.getDockerComposeEnvironment() + for k, v := range dc.Env { + environment[k] = v + } + + cmds := []string{} + pwd := "." + if len(dc.absComposeFilePaths) > 0 { + pwd, _ = filepath.Split(dc.absComposeFilePaths[0]) + + for _, abs := range dc.absComposeFilePaths { + cmds = append(cmds, "-f", abs) + } + } else { + cmds = append(cmds, "-f", "docker-compose.yml") + } + cmds = append(cmds, args...) + + execErr := execute(pwd, environment, dc.Executable, cmds) + err := execErr.Error + if err != nil { + args := strings.Join(dc.Cmd, " ") + return ExecError{ + Command: []string{dc.Executable}, + Error: fmt.Errorf("Local Docker compose exited abnormally whilst running %s: [%v]. %s", dc.Executable, args, err.Error()), + } + } + + return execErr +} + +// capturingPassThroughWriter is a writer that remembers +// data written to it and passes it to w +type capturingPassThroughWriter struct { + buf bytes.Buffer + w io.Writer +} + +// newCapturingPassThroughWriter creates new capturingPassThroughWriter +func newCapturingPassThroughWriter(w io.Writer) *capturingPassThroughWriter { + return &capturingPassThroughWriter{ + w: w, + } +} + +func (w *capturingPassThroughWriter) Write(d []byte) (int, error) { + w.buf.Write(d) + return w.w.Write(d) +} + +// Bytes returns bytes written to the writer +func (w *capturingPassThroughWriter) Bytes() []byte { + return w.buf.Bytes() +} + +// Which checks if a binary is present in PATH +func which(binary string) error { + _, err := exec.LookPath(binary) + + return err +} diff --git a/compose_test.go b/compose_test.go new file mode 100644 index 0000000000..b30e16f838 --- /dev/null +++ b/compose_test.go @@ -0,0 +1,237 @@ +package testcontainers + +import ( + "fmt" + "os/exec" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +const containerNameNginx = "nginx-simple" + +func ExampleNewLocalDockerCompose() { + path := "/path/to/docker-compose.yml" + + _ = NewLocalDockerCompose([]string{path}, "my_project") +} + +func ExampleLocalDockerCompose() { + _ = LocalDockerCompose{ + Executable: "docker-compose", + ComposeFilePaths: []string{ + "/path/to/docker-compose.yml", + "/path/to/docker-compose-1.yml", + "/path/to/docker-compose-2.yml", + "/path/to/docker-compose-3.yml", + }, + Identifier: "my_project", + Cmd: []string{ + "up", "-d", + }, + Env: map[string]string{ + "FOO": "foo", + "BAR": "bar", + }, + } +} + +func ExampleLocalDockerCompose_Down() { + path := "/path/to/docker-compose.yml" + + compose := NewLocalDockerCompose([]string{path}, "my_project") + + execError := compose.WithCommand([]string{"up", "-d"}).Invoke() + if execError.Error != nil { + _ = fmt.Errorf("Failed when running: %v", execError.Command) + } + + execError = compose.Down() + if execError.Error != nil { + _ = fmt.Errorf("Failed when running: %v", execError.Command) + } +} + +func ExampleLocalDockerCompose_Invoke() { + path := "/path/to/docker-compose.yml" + + compose := NewLocalDockerCompose([]string{path}, "my_project") + + execError := compose. + WithCommand([]string{"up", "-d"}). + WithEnv(map[string]string{ + "bar": "BAR", + }). + Invoke() + if execError.Error != nil { + _ = fmt.Errorf("Failed when running: %v", execError.Command) + } +} + +func ExampleLocalDockerCompose_WithCommand() { + path := "/path/to/docker-compose.yml" + + compose := NewLocalDockerCompose([]string{path}, "my_project") + + compose.WithCommand([]string{"up", "-d"}) +} + +func ExampleLocalDockerCompose_WithEnv() { + path := "/path/to/docker-compose.yml" + + compose := NewLocalDockerCompose([]string{path}, "my_project") + + compose.WithEnv(map[string]string{ + "FOO": "foo", + "BAR": "bar", + }) +} + +func TestLocalDockerCompose(t *testing.T) { + path := "./testresources/docker-compose-simple.yml" + + identifier := strings.ToLower(uuid.New().String()) + + compose := NewLocalDockerCompose([]string{path}, identifier) + destroyFn := func() { + err := compose.Down() + checkIfError(t, err) + } + defer destroyFn() + + err := compose. + WithCommand([]string{"up", "-d"}). + Invoke() + checkIfError(t, err) +} + +func TestLocalDockerComposeComplex(t *testing.T) { + path := "./testresources/docker-compose-complex.yml" + + identifier := strings.ToLower(uuid.New().String()) + + compose := NewLocalDockerCompose([]string{path}, identifier) + destroyFn := func() { + err := compose.Down() + checkIfError(t, err) + } + defer destroyFn() + + err := compose. + WithCommand([]string{"up", "-d"}). + Invoke() + checkIfError(t, err) + + assert.Equal(t, 2, len(compose.Services)) + assert.Contains(t, compose.Services, "portal") + assert.Contains(t, compose.Services, "mysql") +} + +func TestLocalDockerComposeWithEnvironment(t *testing.T) { + path := "./testresources/docker-compose-simple.yml" + + identifier := strings.ToLower(uuid.New().String()) + + compose := NewLocalDockerCompose([]string{path}, identifier) + destroyFn := func() { + err := compose.Down() + checkIfError(t, err) + } + defer destroyFn() + + err := compose. + WithCommand([]string{"up", "-d"}). + WithEnv(map[string]string{ + "bar": "BAR", + }). + Invoke() + checkIfError(t, err) + + assert.Equal(t, 1, len(compose.Services)) + assert.Contains(t, compose.Services, "nginx") + + present := map[string]string{ + "bar": "BAR", + } + absent := map[string]string{} + assertContainerEnvironmentVariables(t, containerNameNginx, present, absent) +} + +func TestLocalDockerComposeWithMultipleComposeFiles(t *testing.T) { + composeFiles := []string{ + "testresources/docker-compose-simple.yml", + "testresources/docker-compose-override.yml", + } + + identifier := strings.ToLower(uuid.New().String()) + + compose := NewLocalDockerCompose(composeFiles, identifier) + destroyFn := func() { + err := compose.Down() + checkIfError(t, err) + } + defer destroyFn() + + err := compose. + WithCommand([]string{"up", "-d"}). + WithEnv(map[string]string{ + "bar": "BAR", + "foo": "FOO", + }). + Invoke() + checkIfError(t, err) + + assert.Equal(t, 2, len(compose.Services)) + assert.Contains(t, compose.Services, "nginx") + assert.Contains(t, compose.Services, "mysql") + + present := map[string]string{ + "bar": "BAR", + "foo": "FOO", + } + absent := map[string]string{} + assertContainerEnvironmentVariables(t, containerNameNginx, present, absent) +} + +func assertContainerEnvironmentVariables(t *testing.T, containerName string, present map[string]string, absent map[string]string) { + args := []string{"exec", containerName, "env"} + + output, err := executeAndGetOutput("docker", args) + checkIfError(t, err) + + for k, v := range present { + keyVal := k + "=" + v + assert.Contains(t, output, keyVal) + } + + for k, v := range absent { + keyVal := k + "=" + v + assert.NotContains(t, output, keyVal) + } +} + +func checkIfError(t *testing.T, err ExecError) { + if err.Error != nil { + t.Fatalf("Failed when running %v: %v", err.Command, err.Error) + } + + if err.Stdout != nil { + t.Fatalf("An error in Stdout happened when running %v: %v", err.Command, err.Stdout) + } + + if err.Stderr != nil { + t.Fatalf("An error in Stderr happened when running %v: %v", err.Command, err.Stderr) + } +} + +func executeAndGetOutput(command string, args []string) (string, ExecError) { + cmd := exec.Command(command, args...) + out, err := cmd.CombinedOutput() + if err != nil { + return "", ExecError{Error: err} + } + + return string(out), ExecError{Error: nil} +} diff --git a/container.go b/container.go index cc19399a9e..c7a5855b74 100644 --- a/container.go +++ b/container.go @@ -2,9 +2,10 @@ package testcontainers import ( "context" - "github.com/docker/docker/api/types/container" "io" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/archive" "github.com/docker/go-connections/nat" "github.com/pkg/errors" @@ -45,6 +46,7 @@ type Container interface { 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) + ContainerIP(context.Context) (string, error) // get container ip } // ImageBuildInfo defines what is needed to build an image @@ -65,24 +67,25 @@ type FromDockerfile struct { // 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 - VolumeMounts map[string]string - Tmpfs 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 - ReaperImage string // alternative reaper image - AutoRemove bool // if set to true, the container will be removed from the host when stopped - NetworkMode container.NetworkMode + Image string + Env map[string]string + ExposedPorts []string // allow specifying protocol info + Cmd []string + Labels map[string]string + BindMounts map[string]string + VolumeMounts map[string]string + Tmpfs 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 + ReaperImage string // alternative reaper image + AutoRemove bool // if set to true, the container will be removed from the host when stopped + NetworkMode container.NetworkMode + AlwaysPullImage bool // Always pull image } // ProviderType is an enum for the possible providers diff --git a/docker.go b/docker.go index 38256366d0..b13f64f84a 100644 --- a/docker.go +++ b/docker.go @@ -5,8 +5,6 @@ import ( "context" "encoding/binary" "fmt" - "github.com/cenkalti/backoff" - "github.com/docker/docker/errdefs" "io" "io/ioutil" "log" @@ -16,6 +14,9 @@ import ( "strings" "time" + "github.com/cenkalti/backoff" + "github.com/docker/docker/errdefs" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" @@ -23,8 +24,8 @@ import ( "github.com/docker/docker/client" "github.com/docker/go-connections/nat" + "github.com/google/uuid" "github.com/pkg/errors" - uuid "github.com/satori/go.uuid" "github.com/testcontainers/testcontainers-go/wait" ) @@ -242,6 +243,16 @@ func (c *DockerContainer) Networks(ctx context.Context) ([]string, error) { return n, nil } +// ContainerIP gets the IP address of the primary network within the container. +func (c *DockerContainer) ContainerIP(ctx context.Context) (string, error) { + inspect, err := c.inspectContainer(ctx) + if err != nil { + return "", err + } + + return inspect.NetworkSettings.IPAddress, 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) @@ -383,11 +394,9 @@ type DockerNetwork struct { } // 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 +func (n *DockerNetwork) Remove(ctx context.Context) error { + + return n.provider.client.NetworkRemove(ctx, n.ID) } // DockerProvider implements the ContainerProvider interface @@ -413,8 +422,8 @@ func NewDockerProvider() (*DockerProvider, error) { // 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() + repo := uuid.New() + tag := uuid.New() repoTag := fmt.Sprintf("%s:%s", repo, tag) @@ -463,7 +472,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque req.Labels = make(map[string]string) } - sessionID := uuid.NewV4() + sessionID := uuid.New() var termSignal chan bool if !req.SkipReaper { @@ -494,19 +503,28 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque } } 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 shouldPullImage bool - if err := p.attemptToPullImage(ctx, tag, pullOpt); err != nil { + if req.AlwaysPullImage { + shouldPullImage = true // If requested always attempt to pull image + } else { + _, _, err = p.client.ImageInspectWithRaw(ctx, tag) + if err != nil { + if client.IsErrNotFound(err) { + shouldPullImage = true + } else { return nil, err } + } + } - } else { + if shouldPullImage { + pullOpt := types.ImagePullOptions{} + if req.RegistryCred != "" { + pullOpt.RegistryAuth = req.RegistryCred + } + + if err := p.attemptToPullImage(ctx, tag, pullOpt); err != nil { return nil, err } } @@ -675,7 +693,7 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) Labels: req.Labels, } - sessionID := uuid.NewV4() + sessionID := uuid.New() var termSignal chan bool if !req.SkipReaper { @@ -704,6 +722,7 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) Driver: req.Driver, Name: req.Name, terminationSignal: termSignal, + provider: p, } return n, nil diff --git a/docker_test.go b/docker_test.go index afdde1eeb5..19e5859030 100644 --- a/docker_test.go +++ b/docker_test.go @@ -46,12 +46,13 @@ func TestContainerAttachedToNewNetwork(t *testing.T) { }, } - provider, err := gcr.ProviderType.GetProvider() - - newNetwork, err := provider.CreateNetwork(ctx, NetworkRequest{ - Name: networkName, - CheckDuplicate: true, + newNetwork, err := GenericNetwork(ctx, GenericNetworkRequest{ + NetworkRequest: NetworkRequest{ + Name: networkName, + CheckDuplicate: true, + }, }) + if err != nil { t.Fatal(err) } @@ -98,8 +99,12 @@ func TestContainerWithHostNetworkOptions(t *testing.T) { gcr := GenericContainerRequest{ ContainerRequest: ContainerRequest{ Image: "nginx", + Privileged: true, SkipReaper: true, NetworkMode: "host", + ExposedPorts: []string{ + "80/tcp", + }, }, Started: true, } @@ -111,12 +116,17 @@ func TestContainerWithHostNetworkOptions(t *testing.T) { defer nginxC.Terminate(ctx) - host, err := nginxC.Host(ctx) + //host, err := nginxC.Host(ctx) + //if err != nil { + // t.Errorf("Expected host %s. Got '%d'.", host, err) + //} + // + endpoint, err := nginxC.Endpoint(ctx, "http") if err != nil { - t.Errorf("Expected host %s. Got '%d'.", host, err) + t.Errorf("Expected server endpoint. Got '%v'.", err) } - _, err = http.Get("http://" + host + ":80") + _, err = http.Get(endpoint) if err != nil { t.Errorf("Expected OK response. Got '%d'.", err) } @@ -546,6 +556,13 @@ func TestContainerCreation(t *testing.T) { if resp.StatusCode != http.StatusOK { t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) } + networkIP, err := nginxC.ContainerIP(ctx) + if err != nil { + t.Fatal(err) + } + if len(networkIP) == 0 { + t.Errorf("Expected an IP address, got %v", networkIP) + } networkAliases, err := nginxC.NetworkAliases(ctx) if err != nil { t.Fatal(err) diff --git a/generic.go b/generic.go index cf39b8c1a0..5c611e0905 100644 --- a/generic.go +++ b/generic.go @@ -13,6 +13,26 @@ type GenericContainerRequest struct { ProviderType ProviderType // which provider to use, Docker if empty } +// GenericNetworkRequest represents parameters to a generic network +type GenericNetworkRequest struct { + NetworkRequest // embedded request for provider + ProviderType ProviderType // which provider to use, Docker if empty +} + +// GenericNetwork creates a generic network with parameters +func GenericNetwork(ctx context.Context, req GenericNetworkRequest) (Network, error) { + provider, err := req.ProviderType.GetProvider() + if err != nil { + return nil, err + } + network, err := provider.CreateNetwork(ctx, req.NetworkRequest) + if err != nil { + return nil, errors.Wrap(err, "failed to create network") + } + + return network, nil +} + // GenericContainer creates a generic container with parameters func GenericContainer(ctx context.Context, req GenericContainerRequest) (Container, error) { provider, err := req.ProviderType.GetProvider() diff --git a/go.mod b/go.mod index 44116a2a40..5dbe971fa4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/testcontainers/testcontainers-go -replace github.com/docker/docker => github.com/docker/engine v0.0.0-20190717161051-705d9623b7c1 +go 1.13 require ( github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect @@ -12,10 +12,11 @@ require ( 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/gin-gonic/gin v1.5.0 + github.com/gin-gonic/gin v1.6.3 github.com/go-redis/redis v6.15.7+incompatible github.com/go-sql-driver/mysql v1.5.0 github.com/gogo/protobuf v1.2.0 // indirect + github.com/google/uuid v1.1.1 github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.6.2 // indirect github.com/kr/pretty v0.1.0 // indirect @@ -26,12 +27,14 @@ require ( github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/runc v0.1.1 // indirect github.com/pkg/errors v0.9.1 - github.com/satori/go.uuid v1.2.0 github.com/sirupsen/logrus v1.2.0 // indirect + github.com/stretchr/testify v1.5.1 + golang.org/x/sys v0.0.0-20200116001909-b77594299b42 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 + gopkg.in/yaml.v2 v2.2.8 gotest.tools v0.0.0-20181223230014-1083505acf35 ) -go 1.13 +replace github.com/docker/docker => github.com/docker/engine v0.0.0-20190717161051-705d9623b7c1 diff --git a/go.sum b/go.sum index 31cb282b15..ffcb079156 100644 --- a/go.sum +++ b/go.sum @@ -25,18 +25,20 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= -github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= -github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= -github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= -github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= -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/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM= +github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= github.com/go-redis/redis v6.15.7+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/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= @@ -45,18 +47,21 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU 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/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 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/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -65,11 +70,13 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= -github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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= @@ -86,25 +93,23 @@ github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJ 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/pkg/errors v0.9.0 h1:J8lpUdobwIeCI7OiSxHqEwJUKvJwicL5+3v1oe2Yb4k= -github.com/pkg/errors v0.9.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 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= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= @@ -122,16 +127,17 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ 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/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= @@ -143,15 +149,14 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 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/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= -gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 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= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/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= diff --git a/network_test.go b/network_test.go index 02e55f9b3a..43791de3f9 100644 --- a/network_test.go +++ b/network_test.go @@ -1,12 +1,26 @@ package testcontainers -import "context" +import ( + "context" + "fmt" + "github.com/testcontainers/testcontainers-go/wait" + "testing" + "time" +) // Create a network using a provider. By default it is Docker. func ExampleNetworkProvider_CreateNetwork() { ctx := context.Background() networkName := "new-network" - gcr := GenericContainerRequest{ + net, _ := GenericNetwork(ctx, GenericNetworkRequest{ + NetworkRequest: NetworkRequest{ + Name: networkName, + CheckDuplicate: true, + }, + }) + defer net.Remove(ctx) + + nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: ContainerRequest{ Image: "nginx", ExposedPorts: []string{ @@ -16,15 +30,77 @@ func ExampleNetworkProvider_CreateNetwork() { networkName, }, }, + }) + defer nginxC.Terminate(ctx) + nginxC.GetContainerID() +} + +func Test_MultipleContainersInTheNewNetwork(t *testing.T) { + ctx := context.Background() + + networkName := "test-network" + + networkRequest := NetworkRequest{ + Driver: "bridge", + Name: networkName, + Attachable: true, + } + + env := make(map[string]string) + env["POSTGRES_PASSWORD"] = "Password1" + dbContainerRequest := ContainerRequest{ + Image: "postgres:12.2", + ExposedPorts: []string{"5432/tcp"}, + AutoRemove: true, + Env: env, + WaitingFor: wait.ForListeningPort("5432/tcp"), + Networks: []string{networkName}, } - provider, _ := gcr.ProviderType.GetProvider() - net, _ := provider.CreateNetwork(ctx, NetworkRequest{ - Name: networkName, - CheckDuplicate: true, + + net, err := GenericNetwork(ctx, GenericNetworkRequest{ + NetworkRequest: networkRequest, }) + + if err != nil { + t.Fatal("cannot create network") + } + defer net.Remove(ctx) - nginxC, _ := GenericContainer(ctx, gcr) - defer nginxC.Terminate(ctx) - nginxC.GetContainerID() + postgres, err := GenericContainer(ctx, GenericContainerRequest{ + ContainerRequest: dbContainerRequest, + Started: true, + }) + if err != nil { + t.Fatal(err) + } + + defer postgres.Terminate(ctx) + + env = make(map[string]string) + env["RABBITMQ_ERLANG_COOKIE"] = "f2a2d3d27c75" + env["RABBITMQ_DEFAULT_USER"] = "admin" + env["RABBITMQ_DEFAULT_PASS"] = "Password1" + hp := wait.ForListeningPort("5672/tcp") + hp.WithStartupTimeout(3 * time.Minute) + amqpRequest := ContainerRequest{ + Image: "rabbitmq:management-alpine", + ExposedPorts: []string{"15672/tcp", "5672/tcp"}, + Env: env, + AutoRemove: true, + WaitingFor: hp, + Networks: []string{networkName}, + } + rabbitmq, err := GenericContainer(ctx, GenericContainerRequest{ + ContainerRequest: amqpRequest, + Started: true, + }) + if err != nil { + t.Fatal(err) + return + } + + defer rabbitmq.Terminate(ctx) + fmt.Println(postgres.GetContainerID()) + fmt.Println(rabbitmq.GetContainerID()) } diff --git a/reaper.go b/reaper.go index bee6b25395..ff56be9d09 100644 --- a/reaper.go +++ b/reaper.go @@ -21,6 +21,8 @@ const ( ReaperDefaultImage = "quay.io/testcontainers/ryuk:0.2.3" ) +var reaper *Reaper // We would like to create reaper only once + // 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 { @@ -36,14 +38,19 @@ type Reaper struct { // NewReaper creates a Reaper with a sessionID to identify containers and a provider to use func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, reaperImageName string) (*Reaper, error) { - r := &Reaper{ + // If reaper already exists re-use it + if reaper != nil { + return reaper, nil + } + + // Otherwise create a new one + reaper = &Reaper{ Provider: provider, SessionID: sessionID, } listeningPort := nat.Port("8080/tcp") - // TODO: reuse reaper if there already is one req := ContainerRequest{ Image: reaperImage(reaperImageName), ExposedPorts: []string{string(listeningPort)}, @@ -68,9 +75,9 @@ func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, r if err != nil { return nil, err } - r.Endpoint = endpoint + reaper.Endpoint = endpoint - return r, nil + return reaper, nil } func reaperImage(reaperImageName string) string { diff --git a/testresources/docker-compose-complex.yml b/testresources/docker-compose-complex.yml new file mode 100644 index 0000000000..1d4f2db73a --- /dev/null +++ b/testresources/docker-compose-complex.yml @@ -0,0 +1,23 @@ +version: '3' +services: + portal: + image: liferay/portal:7.2.1-ga2 + depends_on: + - mysql + environment: + - LIFERAY_JDBC_PERIOD_DEFAULT_PERIOD_DRIVER_UPPERCASEC_LASS_UPPERCASEN_AME=com.mysql.cj.jdbc.Driver + - LIFERAY_JDBC_PERIOD_DEFAULT_PERIOD_PASSWORD=my-secret-pw + - LIFERAY_JDBC_PERIOD_DEFAULT_PERIOD_URL=jdbc:mysql://mysql:13306/lportal?characterEncoding=UTF-8&dontTrackOpenResources=true&holdResultsOpenOverStatementClose=true&serverTimezone=GMT&useFastDateParsing=false&useUnicode=true + - LIFERAY_JDBC_PERIOD_DEFAULT_PERIOD_USERNAME=root + - LIFERAY_RETRY_PERIOD_JDBC_PERIOD_ON_PERIOD_STARTUP_PERIOD_MAX_PERIOD_RETRIES=5 + - LIFERAY_RETRY_PERIOD_JDBC_PERIOD_ON_PERIOD_STARTUP_PERIOD_DELAY=5 + ports: + - "8080:8080" + - "11311:11311" + mysql: + image: mdelapenya/mysql-utf8:5.7 + environment: + - MYSQL_DATABASE=lportal + - MYSQL_ROOT_PASSWORD=my-secret-pw + ports: + - "13306:3306" \ No newline at end of file diff --git a/testresources/docker-compose-override.yml b/testresources/docker-compose-override.yml new file mode 100644 index 0000000000..36727f0e30 --- /dev/null +++ b/testresources/docker-compose-override.yml @@ -0,0 +1,13 @@ +version: '3' +services: + nginx: + image: nginx:stable-alpine + environment: + bar: ${bar} + foo: ${foo} + ports: + - "9080:80" + mysql: + image: mysql:5.6 + ports: + - "13306:3306" diff --git a/testresources/docker-compose-simple.yml b/testresources/docker-compose-simple.yml new file mode 100644 index 0000000000..f2789f5c5e --- /dev/null +++ b/testresources/docker-compose-simple.yml @@ -0,0 +1,9 @@ +version: '3' +services: + nginx: + image: nginx:stable-alpine + container_name: nginx-simple + environment: + bar: ${bar} + ports: + - "9080:80" diff --git a/wait/errors.go b/wait/errors.go new file mode 100644 index 0000000000..59e9ad0313 --- /dev/null +++ b/wait/errors.go @@ -0,0 +1,9 @@ +// +build !windows + +package wait + +import "syscall" + +func isConnRefusedErr(err error) bool { + return err == syscall.ECONNREFUSED +} diff --git a/wait/errors_windows.go b/wait/errors_windows.go new file mode 100644 index 0000000000..3ae346d8ad --- /dev/null +++ b/wait/errors_windows.go @@ -0,0 +1,9 @@ +package wait + +import ( + "golang.org/x/sys/windows" +) + +func isConnRefusedErr(err error) bool { + return err == windows.WSAECONNREFUSED +} diff --git a/wait/host_port.go b/wait/host_port.go index dd67503366..f295cee0e0 100644 --- a/wait/host_port.go +++ b/wait/host_port.go @@ -6,7 +6,6 @@ import ( "net" "os" "strconv" - "syscall" "time" "github.com/pkg/errors" @@ -74,7 +73,7 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT if err != nil { if v, ok := err.(*net.OpError); ok { if v2, ok := (v.Err).(*os.SyscallError); ok { - if v2.Err == syscall.ECONNREFUSED && ctx.Err() == nil { + if isConnRefusedErr(v2.Err) { time.Sleep(100 * time.Millisecond) continue } @@ -90,6 +89,9 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT //internal check command := buildInternalCheckCommand(hp.Port.Int()) for { + if ctx.Err() != nil { + return ctx.Err() + } exitCode, err := target.Exec(ctx, []string{"/bin/sh", "-c", command}) if err != nil { return errors.Wrapf(err, "host port waiting failed") diff --git a/wait/http_test.go b/wait/http_test.go new file mode 100644 index 0000000000..05a734eca7 --- /dev/null +++ b/wait/http_test.go @@ -0,0 +1,31 @@ +package wait_test + +import ( + "context" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +// +// https://github.com/testcontainers/testcontainers-go/issues/183 +func ExampleHTTPStrategy() { + ctx := context.Background() + req := testcontainers.ContainerRequest{ + Image: "gogs/gogs:0.11.91", + ExposedPorts: []string{"3000/tcp"}, + WaitingFor: wait.ForHTTP("/").WithPort("3000/tcp"), + } + + gogs, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + panic(err) + } + + defer gogs.Terminate(ctx) + + // Here you have a running container +}