1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16
17 18 19
20 package dockertest
21
22 import (
23 "bufio"
24 "bytes"
25 "compress/gzip"
26 "database/sql"
27 "encoding/json"
28 "errors"
29 "fmt"
30 "io"
31 "log"
32 "net/http"
33 "os"
34 "os/exec"
35 "strings"
36 "testing"
37 "time"
38
39 "perkeep.org/internal/netutil"
40 )
41
42
43 var Debug bool
44
45
46
47 func runLongTest(t *testing.T, image string) {
48 if testing.Short() {
49 t.Skip("skipping in short mode")
50 }
51 if !haveDocker() {
52 t.Skip("skipping test; 'docker' command not found")
53 }
54 if ok, err := haveImage(image); !ok || err != nil {
55 if err != nil {
56 t.Skipf("Error running docker to check for %s: %v", image, err)
57 }
58 log.Printf("Pulling docker image %s ...", image)
59 if strings.HasPrefix(image, "camlistore/") {
60 if err := loadCamliHubImage(image); err != nil {
61 t.Skipf("Error pulling %s: %v", image, err)
62 }
63 return
64 }
65 if err := Pull(image); err != nil {
66 t.Skipf("Error pulling %s: %v", image, err)
67 }
68 }
69 }
70
71
72
73 func loadCamliHubImage(image string) error {
74 if !strings.HasPrefix(image, "camlistore/") {
75 return fmt.Errorf("not an image hosted on camlistore-docker")
76 }
77 imgURL := camliHub + strings.TrimPrefix(image, "camlistore/") + ".tar.gz"
78 resp, err := http.Get(imgURL)
79 if err != nil {
80 return fmt.Errorf("error fetching image %s: %v", image, err)
81 }
82 defer resp.Body.Close()
83
84 dockerLoad := exec.Command("docker", "load")
85 dockerLoad.Stderr = os.Stderr
86 tar, err := dockerLoad.StdinPipe()
87 if err != nil {
88 return err
89 }
90 errc1 := make(chan error)
91 errc2 := make(chan error)
92 go func() {
93 defer tar.Close()
94 zr, err := gzip.NewReader(resp.Body)
95 if err != nil {
96 errc1 <- fmt.Errorf("gzip reader error for image %s: %v", image, err)
97 return
98 }
99 defer zr.Close()
100 if _, err = io.Copy(tar, zr); err != nil {
101 errc1 <- fmt.Errorf("error gunzipping image %s: %v", image, err)
102 return
103 }
104 errc1 <- nil
105 }()
106 go func() {
107 if err := dockerLoad.Run(); err != nil {
108 errc2 <- fmt.Errorf("error running docker load %v: %v", image, err)
109 return
110 }
111 errc2 <- nil
112 }()
113 select {
114 case err := <-errc1:
115 if err != nil {
116 return err
117 }
118 return <-errc2
119 case err := <-errc2:
120 if err != nil {
121 return err
122 }
123 return <-errc1
124 }
125 }
126
127
128 func haveDocker() bool {
129 _, err := exec.LookPath("docker")
130 return err == nil
131 }
132
133
134
135 func haveImage(name string) (ok bool, err error) {
136 out, err := exec.Command("docker", "images", "--no-trunc").Output()
137 if err != nil {
138 return
139 }
140 fields := strings.Split(name, ":")
141 if len(fields) < 2 {
142 return bytes.Contains(out, []byte(name)), nil
143 }
144 tag := fields[1]
145 image := fields[0]
146 sc := bufio.NewScanner(bytes.NewBuffer(out))
147 for sc.Scan() {
148 l := sc.Text()
149 if !strings.HasPrefix(l, image) {
150 continue
151 }
152 if strings.HasPrefix(strings.TrimSpace(strings.TrimPrefix(l, image)), tag) {
153 return true, nil
154 }
155 }
156 return false, sc.Err()
157 }
158
159 func run(args ...string) (containerID string, err error) {
160 cmd := exec.Command("docker", append([]string{"run"}, args...)...)
161 var stdout, stderr bytes.Buffer
162 cmd.Stdout, cmd.Stderr = &stdout, &stderr
163 if err = cmd.Run(); err != nil {
164 err = fmt.Errorf("%v%v", stderr.String(), err)
165 return
166 }
167 containerID = strings.TrimSpace(stdout.String())
168 if containerID == "" {
169 return "", errors.New("unexpected empty output from `docker run`")
170 }
171 return
172 }
173
174 func KillContainer(container string) error {
175 return exec.Command("docker", "kill", container).Run()
176 }
177
178
179 func Pull(image string) error {
180 var stdout, stderr bytes.Buffer
181 cmd := exec.Command("docker", "pull", image)
182 cmd.Stdout = &stdout
183 cmd.Stderr = &stderr
184 err := cmd.Run()
185 out := stdout.String()
186
187
188
189 if err != nil || stderr.Len() != 0 || strings.Contains(out, "Authentication is required") {
190 return fmt.Errorf("docker pull failed: stdout: %s, stderr: %s, err: %v", out, stderr.String(), err)
191 }
192 return nil
193 }
194
195
196 func IP(containerID string) (string, error) {
197 out, err := exec.Command("docker", "inspect", containerID).Output()
198 if err != nil {
199 return "", err
200 }
201 type networkSettings struct {
202 IPAddress string
203 }
204 type container struct {
205 NetworkSettings networkSettings
206 }
207 var c []container
208 if err := json.NewDecoder(bytes.NewReader(out)).Decode(&c); err != nil {
209 return "", err
210 }
211 if len(c) == 0 {
212 return "", errors.New("no output from docker inspect")
213 }
214 if ip := c[0].NetworkSettings.IPAddress; ip != "" {
215 return ip, nil
216 }
217 return "", errors.New("could not find an IP. Not running?")
218 }
219
220 type ContainerID string
221
222 func (c ContainerID) IP() (string, error) {
223 return IP(string(c))
224 }
225
226 func (c ContainerID) Kill() error {
227 if string(c) == "" {
228 return nil
229 }
230 return KillContainer(string(c))
231 }
232
233
234 func (c ContainerID) Remove() error {
235 if Debug {
236 return nil
237 }
238 if string(c) == "" {
239 return nil
240 }
241 return exec.Command("docker", "rm", "-v", string(c)).Run()
242 }
243
244
245
246 func (c ContainerID) KillRemove(t *testing.T) {
247 if err := c.Kill(); err != nil {
248 t.Log(err)
249 return
250 }
251 if err := c.Remove(); err != nil {
252 t.Log(err)
253 }
254 }
255
256
257
258 func (c ContainerID) lookup(port int, timeout time.Duration) (ip string, err error) {
259 ip, err = c.IP()
260 if err != nil {
261 err = fmt.Errorf("error getting IP: %v", err)
262 return
263 }
264 addr := fmt.Sprintf("%s:%d", ip, port)
265 err = netutil.AwaitReachable(addr, timeout)
266 return
267 }
268
269
270
271
272
273 func setupContainer(t *testing.T, image string, port int, timeout time.Duration,
274 start func() (string, error)) (c ContainerID, ip string) {
275 runLongTest(t, image)
276
277 containerID, err := start()
278 if err != nil {
279 t.Fatalf("docker run: %v", err)
280 }
281 c = ContainerID(containerID)
282 ip, err = c.lookup(port, timeout)
283 if err != nil {
284 c.KillRemove(t)
285 t.Skipf("Skipping test for container %v: %v", c, err)
286 }
287 return
288 }
289
290 const (
291 mongoImage = "mpl7/mongo"
292 mysqlImage = "mysql:8"
293 MySQLUsername = "root"
294 MySQLPassword = "root"
295 postgresImage = "nornagon/postgres"
296 PostgresUsername = "docker"
297 PostgresPassword = "docker"
298 camliHub = "https://storage.googleapis.com/camlistore-docker/"
299 )
300
301
302
303
304
305 func SetupMongoContainer(t *testing.T) (c ContainerID, ip string) {
306 return setupContainer(t, mongoImage, 27017, 10*time.Second, func() (string, error) {
307 return run("-d", mongoImage, "--nojournal")
308 })
309 }
310
311
312
313
314
315 func SetupMySQLContainer(t *testing.T, dbname string) (c ContainerID, ip string) {
316 return setupContainer(t, mysqlImage, 3306, 20*time.Second, func() (string, error) {
317 return run("-d", "-e", "MYSQL_ROOT_PASSWORD="+MySQLPassword, "-e", "MYSQL_DATABASE="+dbname, mysqlImage)
318 })
319 }
320
321
322
323
324
325 func SetupPostgreSQLContainer(t *testing.T, dbname string) (c ContainerID, ip string) {
326 c, ip = setupContainer(t, postgresImage, 5432, 15*time.Second, func() (string, error) {
327 return run("-d", postgresImage)
328 })
329 cleanupAndDie := func(err error) {
330 c.KillRemove(t)
331 t.Fatal(err)
332 }
333 rootdb, err := sql.Open("postgres",
334 fmt.Sprintf("user=%s password=%s host=%s dbname=postgres sslmode=disable", PostgresUsername, PostgresPassword, ip))
335 if err != nil {
336 cleanupAndDie(fmt.Errorf("Could not open postgres rootdb: %v", err))
337 }
338 if _, err := sqlExecRetry(rootdb,
339 "CREATE DATABASE "+dbname+" LC_COLLATE = 'C' TEMPLATE = template0",
340 50); err != nil {
341 cleanupAndDie(fmt.Errorf("Could not create database %v: %v", dbname, err))
342 }
343 return
344 }
345
346
347
348
349
350 func sqlExecRetry(db *sql.DB, stmt string, maxTry int) (sql.Result, error) {
351 if maxTry <= 0 {
352 return nil, errors.New("did not try at all")
353 }
354 interval := 100 * time.Millisecond
355 try := 0
356 var err error
357 var result sql.Result
358 for {
359 result, err = db.Exec(stmt)
360 if err == nil {
361 return result, nil
362 }
363 try++
364 if try == maxTry {
365 break
366 }
367 time.Sleep(interval)
368 interval *= 2
369 }
370 return result, fmt.Errorf("failed %v times: %v", try, err)
371 }