diff --git a/.travis.yml b/.travis.yml index 3a92630..0e1b422 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,9 @@ sudo: required language: go go: - - 1.5 - - 1.6 + - 1.8 install: - - go get golang.org/x/tools/cmd/vet - go get github.com/mattn/goveralls - go get golang.org/x/tools/cmd/cover - go get github.com/golang/lint/golint @@ -24,4 +22,4 @@ script: after_failure: - docker logs ms - - docker logs pg \ No newline at end of file + - docker logs pg diff --git a/compose/compose.go b/compose/compose.go index 5bcc4af..e349602 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -43,13 +43,15 @@ import ( "log" "os" "regexp" + "sort" "strings" ) // Compose is the main type exported by the package, used to interact with a running Docker Compose configuration. type Compose struct { - fileName string - Containers map[string]*Container + fileName string + composeProjectName string + Containers map[string]*Container } var ( @@ -58,15 +60,26 @@ var ( composeUpRegexp = regexp.MustCompile("(?m:docker start <- \\(u'(.*)'\\)$)") ) -const ( - composeProjectName = "compose" -) - // Start starts a Docker Compose configuration. +// Fixes the Docker Compose project name to a known value so existing containers can be killed. // If forcePull is true, it attempts do pull newer versions of the images. // If rmFirst is true, it attempts to kill and delete containers before starting new ones. func Start(dockerComposeYML string, forcePull, rmFirst bool) (*Compose, error) { + return StartProject(dockerComposeYML, forcePull, rmFirst, "compose") +} + +// StartParallel starts a Docker Compose configuration and is suitable for concurrent usage. +// The project name is defined at random to ensure multiple instances can be run. +// Note: that the docker services should not bind to localhost ports. +func StartParallel(dockerComposeYML string, forcePull bool) (*Compose, error) { + return StartProject(dockerComposeYML, forcePull, false, randStringBytes(9)) +} + +// StartProject starts a Docker Compose configuration, giving fine grained control of all of the properties. +func StartProject(dockerComposeYML string, forcePull, rmFirst bool, projectName string) (*Compose, error) { + logger.Println("initializing...") + dockerComposeYML = replaceEnv(dockerComposeYML) fName, err := writeTmp(dockerComposeYML) @@ -74,7 +87,7 @@ func Start(dockerComposeYML string, forcePull, rmFirst bool) (*Compose, error) { return nil, err } - ids, err := composeStart(fName, forcePull, rmFirst) + ids, err := composeStart(fName, projectName, forcePull, rmFirst) if err != nil { return nil, err } @@ -92,7 +105,7 @@ func Start(dockerComposeYML string, forcePull, rmFirst bool) (*Compose, error) { containers[container.Name[1:]] = container } - return &Compose{fileName: fName, Containers: containers}, nil + return &Compose{fileName: fName, composeProjectName: projectName, Containers: containers}, nil } // MustStart is like Start, but panics on error. @@ -104,9 +117,18 @@ func MustStart(dockerComposeYML string, forcePull, killFirst bool) *Compose { return compose } +// MustStartParallel is like StartParallel, but panics on error. +func MustStartParallel(dockerComposeYML string, forcePull bool) *Compose { + compose, err := StartParallel(dockerComposeYML, forcePull) + if err != nil { + panic(err) + } + return compose +} + // Kill kills any running containers for the current configuration. func (c *Compose) Kill() error { - return composeKill(c.fileName) + return composeKill(c.fileName, c.composeProjectName) } // MustKill is like Kill, but panics on error. @@ -116,6 +138,36 @@ func (c *Compose) MustKill() { } } +// GetPublicIPAddressForService returns the IPAddress of a service +func (c *Compose) GetPublicIPAddressForService(serviceName string) (bool, string) { + + //iterate containers + for _, container := range c.Containers { + // look in NetworkSettings.Networks + for _, network := range container.NetworkSettings.Networks { + + // iterate aliases looking for the service + sort.Strings(network.Aliases) + i := sort.SearchStrings(network.Aliases, serviceName) + + if i < len(network.Aliases) { + return true, network.IPAddress + } + + } + } + return false, "" +} + +// MustGetPublicIPAddressForService is like GetPublicIPAddressForService, but panics if not found +func (c *Compose) MustGetPublicIPAddressForService(serviceName string) string { + found, ipAddress := c.GetPublicIPAddressForService(serviceName) + if !found { + panic(fmt.Errorf("ipaddress for service %s not found", serviceName)) + } + return ipAddress +} + func replaceEnv(dockerComposeYML string) string { return replaceEnvRegexp.ReplaceAllStringFunc(dockerComposeYML, replaceEnvFunc) } @@ -124,25 +176,25 @@ func replaceEnvFunc(s string) string { return os.Getenv(strings.TrimSpace(s[2 : len(s)-1])) } -func composeStart(fName string, forcePull, rmFirst bool) ([]string, error) { +func composeStart(fName, composeProjectName string, forcePull, rmFirst bool) ([]string, error) { if forcePull { logger.Println("pulling images...") - if _, err := composeRun(fName, "pull"); err != nil { + if _, err := composeRun(fName, composeProjectName, "pull"); err != nil { return nil, fmt.Errorf("compose: error pulling images: %v", err) } } if rmFirst { - if err := composeKill(fName); err != nil { + if err := composeKill(fName, composeProjectName); err != nil { return nil, err } - if err := composeRm(fName); err != nil { + if err := composeRm(fName, composeProjectName); err != nil { return nil, err } } logger.Println("starting containers...") - out, err := composeRun(fName, "--verbose", "up", "-d") + out, err := composeRun(fName, composeProjectName, "--verbose", "up", "-d") if err != nil { return nil, fmt.Errorf("compose: error starting containers: %v", err) } @@ -157,25 +209,25 @@ func composeStart(fName string, forcePull, rmFirst bool) ([]string, error) { return ids, nil } -func composeKill(fName string) error { +func composeKill(fName, composeProjectName string) error { logger.Println("killing stale containers...") - _, err := composeRun(fName, "kill") + _, err := composeRun(fName, composeProjectName, "kill") if err != nil { return fmt.Errorf("compose: error killing stale containers: %v", err) } return err } -func composeRm(fName string) error { +func composeRm(fName, composeProjectName string) error { logger.Println("removing stale containers...") - _, err := composeRun(fName, "rm", "--force") + _, err := composeRun(fName, composeProjectName, "rm", "--force") if err != nil { return fmt.Errorf("compose: error removing stale containers: %v", err) } return err } -func composeRun(fName string, otherArgs ...string) (string, error) { +func composeRun(fName, composeProjectName string, otherArgs ...string) (string, error) { args := []string{"-f", fName, "-p", composeProjectName} args = append(args, otherArgs...) return runCmd("docker-compose", args...) diff --git a/compose/compose_test.go b/compose/compose_test.go index ccb2338..eb451f4 100644 --- a/compose/compose_test.go +++ b/compose/compose_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "os" + "sync" "testing" ) @@ -82,6 +83,58 @@ func TestMustConnectWithDefaults(t *testing.T) { }) } +func TestParallelMustConnectWithDefaults(t *testing.T) { + + // NOTE that the services don't bind to local port + parallelYML := ` +version: '2' +services: + one: + image: jamesdbloom/mockserver + two: + image: jamesdbloom/mockserver +` + + compose1 := MustStartParallel(parallelYML, false) + defer compose1.MustKill() + compose2 := MustStartParallel(parallelYML, false) + defer compose2.MustKill() + + // get the URL for the service 'one' in the first docker-compose cluster + mockServer1URL := fmt.Sprintf("http://%s:%d", compose1.MustGetPublicIPAddressForService("one"), 1080) + + // get the URL for the service 'two' in the second docker-compose cluster + mockServer2URL := fmt.Sprintf("http://%s:%d", compose2.MustGetPublicIPAddressForService("two"), 1080) + + fmt.Println(mockServer1URL, mockServer2URL) + + wg := sync.WaitGroup{} + wg.Add(2) + + MustConnectWithDefaults(func() error { + logger.Print("attempting to connect to mockserver1...") + _, err := http.Get(mockServer1URL) + if err == nil { + logger.Print("connected to mockserver1") + wg.Done() + } + return err + }) + + MustConnectWithDefaults(func() error { + logger.Print("attempting to connect to mockserver2...") + _, err := http.Get(mockServer2URL) + if err == nil { + logger.Print("connected to mockserver2") + wg.Done() + } + return err + }) + + wg.Wait() + +} + func TestInspectUnknownContainer(t *testing.T) { _, err := Inspect("bad") if err == nil { diff --git a/compose/container.go b/compose/container.go index df7ae24..3e30e92 100644 --- a/compose/container.go +++ b/compose/container.go @@ -44,7 +44,8 @@ type State struct { // NetworkSettings models the network settings section of the `docker inspect` command. type NetworkSettings struct { - Ports map[string][]PortBinding `json:"Ports,omitempty"` + Ports map[string][]PortBinding `json:"Ports,omitempty"` + Networks map[string]Network `json:"Networks"` } // PortBinding models a port binding in the network settings section of the `docker inspect command. @@ -53,6 +54,12 @@ type PortBinding struct { HostPort string `json:"HostPort,omitempty"` } +// Network models the network section of the `docker inspect` command. +type Network struct { + IPAddress string `json:"IPAddress"` + Aliases []string `json:"Aliases"` +} + // Inspect inspects a container using the `docker inspect` command and returns a parsed version of its output. func Inspect(id string) (*Container, error) { out, err := runCmd("docker", "inspect", id) diff --git a/compose/utils.go b/compose/utils.go index 457b3e1..f1895e2 100644 --- a/compose/utils.go +++ b/compose/utils.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io/ioutil" + "math/rand" "os" "os/exec" "regexp" @@ -37,6 +38,7 @@ func MustInferDockerHost() string { func runCmd(name string, args ...string) (string, error) { var outBuf bytes.Buffer + cmd := exec.Command(name, args...) cmd.Stdout = &outBuf cmd.Stderr = &outBuf @@ -50,6 +52,7 @@ func runCmd(name string, args ...string) (string, error) { func writeTmp(content string) (string, error) { f, err := ioutil.TempFile("", "docker-compose-") + if err != nil { return "", fmt.Errorf("compose: error creating temp file: %v", err) } @@ -61,3 +64,13 @@ func writeTmp(content string) (string, error) { return f.Name(), nil } + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func randStringBytes(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +}