github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/pkg/test/dockertest/docker.go (about)

     1  /*
     2  Copyright 2014 The Camlistore Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  /*
    18  Package dockertest contains helper functions for setting up and tearing down docker containers to aid in testing.
    19  */
    20  package dockertest
    21  
    22  import (
    23  	"bytes"
    24  	"database/sql"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"log"
    29  	"os/exec"
    30  	"strings"
    31  	"testing"
    32  	"time"
    33  
    34  	"camlistore.org/pkg/netutil"
    35  )
    36  
    37  /// runLongTest checks all the conditions for running a docker container
    38  // based on image.
    39  func runLongTest(t *testing.T, image string) {
    40  	if testing.Short() {
    41  		t.Skip("skipping in short mode")
    42  	}
    43  	if !haveDocker() {
    44  		t.Skip("skipping test; 'docker' command not found")
    45  	}
    46  	if ok, err := haveImage(image); !ok || err != nil {
    47  		if err != nil {
    48  			t.Skipf("Error running docker to check for %s: %v", image, err)
    49  		}
    50  		log.Printf("Pulling docker image %s ...", image)
    51  		if err := Pull(image); err != nil {
    52  			t.Skipf("Error pulling %s: %v", image, err)
    53  		}
    54  	}
    55  }
    56  
    57  // haveDocker returns whether the "docker" command was found.
    58  func haveDocker() bool {
    59  	_, err := exec.LookPath("docker")
    60  	return err == nil
    61  }
    62  
    63  func haveImage(name string) (ok bool, err error) {
    64  	out, err := exec.Command("docker", "images", "--no-trunc").Output()
    65  	if err != nil {
    66  		return
    67  	}
    68  	return bytes.Contains(out, []byte(name)), nil
    69  }
    70  
    71  func run(args ...string) (containerID string, err error) {
    72  	cmd := exec.Command("docker", append([]string{"run"}, args...)...)
    73  	var stdout, stderr bytes.Buffer
    74  	cmd.Stdout, cmd.Stderr = &stdout, &stderr
    75  	if err = cmd.Run(); err != nil {
    76  		err = fmt.Errorf("%v%v", stderr.String(), err)
    77  		return
    78  	}
    79  	containerID = strings.TrimSpace(stdout.String())
    80  	if containerID == "" {
    81  		return "", errors.New("unexpected empty output from `docker run`")
    82  	}
    83  	return
    84  }
    85  
    86  func KillContainer(container string) error {
    87  	return exec.Command("docker", "kill", container).Run()
    88  }
    89  
    90  // Pull retrieves the docker image with 'docker pull'.
    91  func Pull(image string) error {
    92  	out, err := exec.Command("docker", "pull", image).CombinedOutput()
    93  	if err != nil {
    94  		err = fmt.Errorf("%v: %s", err, out)
    95  	}
    96  	return err
    97  }
    98  
    99  // IP returns the IP address of the container.
   100  func IP(containerID string) (string, error) {
   101  	out, err := exec.Command("docker", "inspect", containerID).Output()
   102  	if err != nil {
   103  		return "", err
   104  	}
   105  	type networkSettings struct {
   106  		IPAddress string
   107  	}
   108  	type container struct {
   109  		NetworkSettings networkSettings
   110  	}
   111  	var c []container
   112  	if err := json.NewDecoder(bytes.NewReader(out)).Decode(&c); err != nil {
   113  		return "", err
   114  	}
   115  	if len(c) == 0 {
   116  		return "", errors.New("no output from docker inspect")
   117  	}
   118  	if ip := c[0].NetworkSettings.IPAddress; ip != "" {
   119  		return ip, nil
   120  	}
   121  	return "", fmt.Errorf("could not find an IP for %v. Not running?", containerID)
   122  }
   123  
   124  type ContainerID string
   125  
   126  func (c ContainerID) IP() (string, error) {
   127  	return IP(string(c))
   128  }
   129  
   130  func (c ContainerID) Kill() error {
   131  	return KillContainer(string(c))
   132  }
   133  
   134  // Remove runs "docker rm" on the container
   135  func (c ContainerID) Remove() error {
   136  	return exec.Command("docker", "rm", string(c)).Run()
   137  }
   138  
   139  // KillRemove calls Kill on the container, and then Remove if there was
   140  // no error. It logs any error to t.
   141  func (c ContainerID) KillRemove(t *testing.T) {
   142  	if err := c.Kill(); err != nil {
   143  		t.Log(err)
   144  		return
   145  	}
   146  	if err := c.Remove(); err != nil {
   147  		t.Log(err)
   148  	}
   149  }
   150  
   151  // lookup retrieves the ip address of the container, and tries to reach
   152  // before timeout the tcp address at this ip and given port.
   153  func (c ContainerID) lookup(port int, timeout time.Duration) (ip string, err error) {
   154  	ip, err = c.IP()
   155  	if err != nil {
   156  		err = fmt.Errorf("Error getting container IP: %v", err)
   157  		return
   158  	}
   159  	addr := fmt.Sprintf("%s:%d", ip, port)
   160  	if err = netutil.AwaitReachable(addr, timeout); err != nil {
   161  		err = fmt.Errorf("timeout trying to reach %s for container %v: %v", addr, c, err)
   162  	}
   163  	return
   164  }
   165  
   166  // setupContainer sets up a container, using the start function to run the given image.
   167  // It also looks up the IP address of the container, and tests this address with the given
   168  // port and timeout. It returns the container ID and its IP address, or makes the test
   169  // fail on error.
   170  func setupContainer(t *testing.T, image string, port int, timeout time.Duration,
   171  	start func() (string, error)) (c ContainerID, ip string) {
   172  	runLongTest(t, image)
   173  
   174  	containerID, err := start()
   175  	if err != nil {
   176  		t.Fatalf("docker run: %v", err)
   177  	}
   178  	c = ContainerID(containerID)
   179  	ip, err = c.lookup(port, timeout)
   180  	if err != nil {
   181  		c.KillRemove(t)
   182  		t.Fatalf("container lookup: %v", err)
   183  	}
   184  	return
   185  }
   186  
   187  const (
   188  	mongoImage       = "robinvdvleuten/mongo"
   189  	mysqlImage       = "orchardup/mysql"
   190  	MySQLUsername    = "root"
   191  	MySQLPassword    = "root"
   192  	postgresImage    = "nornagon/postgres"
   193  	PostgresUsername = "docker" // set up by the dockerfile of postgresImage
   194  	PostgresPassword = "docker" // set up by the dockerfile of postgresImage
   195  )
   196  
   197  // SetupMongoContainer sets up a real MongoDB instance for testing purposes,
   198  // using a Docker container. It returns the container ID and its IP address,
   199  // or makes the test fail on error.
   200  // Currently using https://index.docker.io/u/robinvdvleuten/mongo/
   201  func SetupMongoContainer(t *testing.T) (c ContainerID, ip string) {
   202  	return setupContainer(t, mongoImage, 27017, 10*time.Second, func() (string, error) {
   203  		return run("-d", mongoImage, "--nojournal")
   204  	})
   205  }
   206  
   207  // SetupMySQLContainer sets up a real MySQL instance for testing purposes,
   208  // using a Docker container. It returns the container ID and its IP address,
   209  // or makes the test fail on error.
   210  // Currently using https://index.docker.io/u/orchardup/mysql/
   211  func SetupMySQLContainer(t *testing.T, dbname string) (c ContainerID, ip string) {
   212  	return setupContainer(t, mysqlImage, 3306, 10*time.Second, func() (string, error) {
   213  		return run("-d", "-e", "MYSQL_ROOT_PASSWORD="+MySQLPassword, "-e", "MYSQL_DATABASE="+dbname, mysqlImage)
   214  	})
   215  }
   216  
   217  // SetupPostgreSQLContainer sets up a real PostgreSQL instance for testing purposes,
   218  // using a Docker container. It returns the container ID and its IP address,
   219  // or makes the test fail on error.
   220  // Currently using https://index.docker.io/u/nornagon/postgres
   221  func SetupPostgreSQLContainer(t *testing.T, dbname string) (c ContainerID, ip string) {
   222  	c, ip = setupContainer(t, postgresImage, 5432, 15*time.Second, func() (string, error) {
   223  		return run("-d", postgresImage)
   224  	})
   225  	cleanupAndDie := func(err error) {
   226  		c.KillRemove(t)
   227  		t.Fatal(err)
   228  	}
   229  	rootdb, err := sql.Open("postgres",
   230  		fmt.Sprintf("user=%s password=%s host=%s dbname=postgres sslmode=disable", PostgresUsername, PostgresPassword, ip))
   231  	if err != nil {
   232  		cleanupAndDie(fmt.Errorf("Could not open postgres rootdb: %v", err))
   233  	}
   234  	if _, err := sqlExecRetry(rootdb,
   235  		"CREATE DATABASE "+dbname+" LC_COLLATE = 'C' TEMPLATE = template0",
   236  		50); err != nil {
   237  		cleanupAndDie(fmt.Errorf("Could not create database %v: %v", dbname, err))
   238  	}
   239  	return
   240  }
   241  
   242  // sqlExecRetry keeps calling http://golang.org/pkg/database/sql/#DB.Exec on db
   243  // with stmt until it succeeds or until it has been tried maxTry times.
   244  // It sleeps in between tries, twice longer after each new try, starting with
   245  // 100 milliseconds.
   246  func sqlExecRetry(db *sql.DB, stmt string, maxTry int) (sql.Result, error) {
   247  	if maxTry <= 0 {
   248  		return nil, errors.New("did not try at all")
   249  	}
   250  	interval := 100 * time.Millisecond
   251  	try := 0
   252  	var err error
   253  	var result sql.Result
   254  	for {
   255  		result, err = db.Exec(stmt)
   256  		if err == nil {
   257  			return result, nil
   258  		}
   259  		try++
   260  		if try == maxTry {
   261  			break
   262  		}
   263  		time.Sleep(interval)
   264  		interval *= 2
   265  	}
   266  	return result, fmt.Errorf("failed %v times: %v", try, err)
   267  }