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 }