PageRenderTime 132ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/docker.go

https://gitlab.com/oytunistrator/dockertest
Go | 507 lines | 396 code | 53 blank | 58 comment | 110 complexity | 44caf128414006131cc54140d94eadb3 MD5 | raw file
  1. package dockertest
  2. /*
  3. Copyright 2014 The Camlistore Authors
  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. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. */
  14. import (
  15. "bytes"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "log"
  20. "os/exec"
  21. "strconv"
  22. "strings"
  23. "time"
  24. "database/sql"
  25. "math/rand"
  26. "regexp"
  27. "github.com/mattbaird/elastigo/lib"
  28. "github.com/mediocregopher/radix.v2/redis"
  29. "github.com/pborman/uuid"
  30. "gopkg.in/mgo.v2"
  31. // Import mysql driver
  32. _ "github.com/go-sql-driver/mysql"
  33. // Import postgres driver
  34. _ "github.com/lib/pq"
  35. )
  36. /// runLongTest checks all the conditions for running a docker container
  37. // based on image.
  38. func runLongTest(image string) error {
  39. DockerMachineAvailable = false
  40. if haveDockerMachine() {
  41. DockerMachineAvailable = true
  42. if !startDockerMachine() {
  43. log.Printf(`Starting docker machine "%s" failed. This could be because the image is already running or because the image does not exist. Tests will fail if the image does not exist.`, DockerMachineName)
  44. }
  45. } else if !haveDocker() {
  46. return errors.New("Neither 'docker' nor 'docker-machine' available on this system.")
  47. }
  48. if ok, err := haveImage(image); !ok || err != nil {
  49. if err != nil {
  50. return fmt.Errorf("Error checking for docker image %s: %v", image, err)
  51. }
  52. log.Printf("Pulling docker image %s ...", image)
  53. if err := Pull(image); err != nil {
  54. return fmt.Errorf("Error pulling %s: %v", image, err)
  55. }
  56. }
  57. return nil
  58. }
  59. func runDockerCommand(command string, args ...string) *exec.Cmd {
  60. if DockerMachineAvailable {
  61. command = "/usr/local/bin/" + strings.Join(append([]string{command}, args...), " ")
  62. cmd := exec.Command("docker-machine", "ssh", DockerMachineName, command)
  63. return cmd
  64. }
  65. return exec.Command(command, args...)
  66. }
  67. // haveDockerMachine returns whether the "docker" command was found.
  68. func haveDockerMachine() bool {
  69. _, err := exec.LookPath("docker-machine")
  70. return err == nil
  71. }
  72. // startDockerMachine starts the docker machine and returns false if the command failed to execute
  73. func startDockerMachine() bool {
  74. _, err := exec.Command("docker-machine", "start", DockerMachineName).Output()
  75. return err == nil
  76. }
  77. // haveDocker returns whether the "docker" command was found.
  78. func haveDocker() bool {
  79. _, err := exec.LookPath("docker")
  80. return err == nil
  81. }
  82. func haveImage(name string) (ok bool, err error) {
  83. out, err := runDockerCommand("docker", "images", "--no-trunc").Output()
  84. if err != nil {
  85. return false, err
  86. }
  87. return bytes.Contains(out, []byte(name)), nil
  88. }
  89. func run(args ...string) (containerID string, err error) {
  90. var stdout, stderr bytes.Buffer
  91. validID := regexp.MustCompile(`^([a-zA-Z0-9]+)$`)
  92. cmd := runDockerCommand("docker", append([]string{"run"}, args...)...)
  93. cmd.Stdout, cmd.Stderr = &stdout, &stderr
  94. if err = cmd.Run(); err != nil {
  95. err = fmt.Errorf("Error running docker\nStdOut: %s\nStdErr: %s\nError: %v\n\n", stdout.String(), stderr.String(), err)
  96. return
  97. }
  98. containerID = strings.TrimSpace(string(stdout.String()))
  99. if !validID.MatchString(containerID) {
  100. return "", fmt.Errorf("Error running docker: %s", containerID)
  101. }
  102. if containerID == "" {
  103. return "", errors.New("Unexpected empty output from `docker run`")
  104. }
  105. return containerID, nil
  106. }
  107. // KillContainer runs docker kill on a container.
  108. func KillContainer(container string) error {
  109. if container != "" {
  110. return runDockerCommand("docker", "kill", container).Run()
  111. }
  112. return nil
  113. }
  114. // Pull retrieves the docker image with 'docker pull'.
  115. func Pull(image string) error {
  116. out, err := runDockerCommand("docker", "pull", image).CombinedOutput()
  117. if err != nil {
  118. err = fmt.Errorf("%v: %s", err, out)
  119. }
  120. return err
  121. }
  122. // IP returns the IP address of the container.
  123. func IP(containerID string) (string, error) {
  124. out, err := runDockerCommand("docker", "inspect", containerID).Output()
  125. if err != nil {
  126. return "", err
  127. }
  128. type networkSettings struct {
  129. IPAddress string
  130. }
  131. type container struct {
  132. NetworkSettings networkSettings
  133. }
  134. var c []container
  135. if err := json.NewDecoder(bytes.NewReader(out)).Decode(&c); err != nil {
  136. return "", err
  137. }
  138. if len(c) == 0 {
  139. return "", errors.New("no output from docker inspect")
  140. }
  141. if ip := c[0].NetworkSettings.IPAddress; ip != "" {
  142. return ip, nil
  143. }
  144. return "", errors.New("could not find an IP. Not running?")
  145. }
  146. // setupContainer sets up a container, using the start function to run the given image.
  147. // It also looks up the IP address of the container, and tests this address with the given
  148. // port and timeout. It returns the container ID and its IP address, or makes the test
  149. // fail on error.
  150. func setupContainer(image string, port int, timeout time.Duration, start func() (string, error)) (c ContainerID, ip string, err error) {
  151. err = runLongTest(image)
  152. if err != nil {
  153. return "", "", err
  154. }
  155. containerID, err := start()
  156. if err != nil {
  157. return "", "", err
  158. }
  159. c = ContainerID(containerID)
  160. ip, err = c.lookup(port, timeout)
  161. if err != nil {
  162. c.KillRemove()
  163. return "", "", err
  164. }
  165. return c, ip, nil
  166. }
  167. func randInt(min int, max int) int {
  168. rand.Seed(time.Now().UTC().UnixNano())
  169. return min + rand.Intn(max-min)
  170. }
  171. type pinger interface {
  172. Ping() error
  173. }
  174. func ping(db pinger, tries int, delay time.Duration) bool {
  175. for i := 0; i <= tries; i++ {
  176. time.Sleep(delay)
  177. if s, ok := db.(*sql.DB); ok {
  178. if _, err := s.Exec("SELECT 1"); err != nil {
  179. continue
  180. }
  181. } else if s, ok := db.(*mgo.Session); ok {
  182. if _, err := s.DatabaseNames(); err != nil {
  183. continue
  184. }
  185. }
  186. if err := db.Ping(); err == nil {
  187. return true
  188. }
  189. log.Printf("Ping try %v failed", i)
  190. }
  191. return false
  192. }
  193. // SetupMongoContainer sets up a real MongoDB instance for testing purposes,
  194. // using a Docker container. It returns the container ID and its IP address,
  195. // or makes the test fail on error.
  196. func SetupMongoContainer() (c ContainerID, ip string, port int, err error) {
  197. port = randInt(1024, 49150)
  198. forward := fmt.Sprintf("%d:%d", port, 27017)
  199. if BindDockerToLocalhost != "" {
  200. forward = "127.0.0.1:" + forward
  201. }
  202. c, ip, err = setupContainer(mongoImage, port, 10*time.Second, func() (string, error) {
  203. res, err := run("--name", uuid.New(), "-d", "-P", "-p", forward, mongoImage)
  204. return res, err
  205. })
  206. return
  207. }
  208. // SetupMySQLContainer sets up a real MySQL instance for testing purposes,
  209. // using a Docker container. It returns the container ID and its IP address,
  210. // or makes the test fail on error.
  211. func SetupMySQLContainer() (c ContainerID, ip string, port int, err error) {
  212. port = randInt(1024, 49150)
  213. forward := fmt.Sprintf("%d:%d", port, 3306)
  214. if BindDockerToLocalhost != "" {
  215. forward = "127.0.0.1:" + forward
  216. }
  217. c, ip, err = setupContainer(mysqlImage, port, 10*time.Second, func() (string, error) {
  218. return run("-d", "-p", forward, "-e", fmt.Sprintf("MYSQL_ROOT_PASSWORD=%s", MySQLPassword), mysqlImage)
  219. })
  220. return
  221. }
  222. // SetupPostgreSQLContainer sets up a real PostgreSQL instance for testing purposes,
  223. // using a Docker container. It returns the container ID and its IP address,
  224. // or makes the test fail on error.
  225. func SetupPostgreSQLContainer() (c ContainerID, ip string, port int, err error) {
  226. port = randInt(1024, 49150)
  227. forward := fmt.Sprintf("%d:%d", port, 5432)
  228. if BindDockerToLocalhost != "" {
  229. forward = "127.0.0.1:" + forward
  230. }
  231. c, ip, err = setupContainer(postgresImage, port, 15*time.Second, func() (string, error) {
  232. return run("-d", "-p", forward, "-e", fmt.Sprintf("POSTGRES_PASSWORD=%s", PostgresPassword), postgresImage)
  233. })
  234. return
  235. }
  236. // SetupElasticSearchContainer sets up a real ElasticSearch instance for testing purposes
  237. // using a Docker container. It returns the container ID and its IP address,
  238. // or makes the test fail on error.
  239. func SetupElasticSearchContainer() (c ContainerID, ip string, port int, err error) {
  240. port = randInt(1024, 49150)
  241. forward := fmt.Sprintf("%d:%d", port, 9200)
  242. if BindDockerToLocalhost != "" {
  243. forward = "127.0.0.1:" + forward
  244. }
  245. c, ip, err = setupContainer(elasticsearchImage, port, 15*time.Second, func() (string, error) {
  246. return run("--name", uuid.New(), "-d", "-P", "-p", forward, elasticsearchImage)
  247. })
  248. return
  249. }
  250. // SetupRedisContainer sets up a real Redis instance for testing purposes
  251. // using a Docker container. It returns the container ID and its IP address,
  252. // or makes the test fail on error.
  253. func SetupRedisContainer() (c ContainerID, ip string, port int, err error) {
  254. port = randInt(1024, 49150)
  255. forward := fmt.Sprintf("%d:%d", port, 6379)
  256. if BindDockerToLocalhost != "" {
  257. forward = "127.0.0.1:" + forward
  258. }
  259. c, ip, err = setupContainer(redisImage, port, 15*time.Second, func() (string, error) {
  260. return run("--name", uuid.New(), "-d", "-P", "-p", forward, redisImage)
  261. })
  262. return
  263. }
  264. // OpenPostgreSQLContainerConnection is supported for legacy reasons. Don't use it.
  265. func OpenPostgreSQLContainerConnection(tries int, delay time.Duration) (c ContainerID, db *sql.DB, err error) {
  266. c, ip, port, err := SetupPostgreSQLContainer()
  267. if err != nil {
  268. return c, nil, fmt.Errorf("Could not set up PostgreSQL container: %v", err)
  269. }
  270. for try := 0; try <= tries; try++ {
  271. time.Sleep(delay)
  272. url := fmt.Sprintf("postgres://%s:%s@%s:%d/postgres?sslmode=disable", PostgresUsername, PostgresPassword, ip, port)
  273. log.Printf("Try %d: Connecting %s", try, url)
  274. if db, err = sql.Open("postgres", url); err == nil {
  275. if ping(db, tries, delay) {
  276. log.Printf("Try %d: Successfully connected to %v", try, url)
  277. return c, db, nil
  278. }
  279. log.Printf("Try %d: Could not ping database: %v", try, err)
  280. }
  281. log.Printf("Try %d: Could not set up PostgreSQL container: %v", try, err)
  282. }
  283. return c, nil, errors.New("Could not set up PostgreSQL container.")
  284. }
  285. // OpenMongoDBContainerConnection is supported for legacy reasons. Don't use it.
  286. func OpenMongoDBContainerConnection(tries int, delay time.Duration) (c ContainerID, db *mgo.Session, err error) {
  287. c, ip, port, err := SetupMongoContainer()
  288. if err != nil {
  289. return c, nil, fmt.Errorf("Could not set up MongoDB container: %v", err)
  290. }
  291. for try := 0; try <= tries; try++ {
  292. time.Sleep(delay)
  293. url := fmt.Sprintf("%s:%d", ip, port)
  294. log.Printf("Try %d: Connecting %s", try, url)
  295. if db, err = mgo.Dial(url); err == nil {
  296. if ping(db, tries, delay) {
  297. log.Printf("Try %d: Successfully connected to %v", try, url)
  298. return c, db, nil
  299. }
  300. log.Printf("Try %d: Could not ping database: %v", try, err)
  301. }
  302. log.Printf("Try %d: Could not set up MongoDB container: %v", try, err)
  303. }
  304. return c, nil, errors.New("Could not set up MongoDB container.")
  305. }
  306. // OpenMySQLContainerConnection is supported for legacy reasons. Don't use it.
  307. func OpenMySQLContainerConnection(tries int, delay time.Duration) (c ContainerID, db *sql.DB, err error) {
  308. c, ip, port, err := SetupMySQLContainer()
  309. if err != nil {
  310. return c, nil, fmt.Errorf("Could not set up MySQL container: %v", err)
  311. }
  312. for try := 0; try <= tries; try++ {
  313. time.Sleep(delay)
  314. url := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql", MySQLUsername, MySQLPassword, ip, port)
  315. log.Printf("Try %d: Connecting %s", try, url)
  316. if db, err = sql.Open("mysql", url); err == nil {
  317. if ping(db, tries, delay) {
  318. log.Printf("Try %d: Successfully connected to %v", try, url)
  319. return c, db, nil
  320. }
  321. log.Printf("Try %d: Could not ping database: %v", try, err)
  322. }
  323. log.Printf("Try %d: Could not set up MySQL container: %v", try, err)
  324. }
  325. return c, nil, errors.New("Could not set up MySQL container.")
  326. }
  327. // OpenElasticSearchContainerConnection is supported for legacy reasons. Don't use it.
  328. func OpenElasticSearchContainerConnection(tries int, delay time.Duration) (c ContainerID, con *elastigo.Conn, err error) {
  329. c, ip, port, err := SetupElasticSearchContainer()
  330. if err != nil {
  331. return c, nil, fmt.Errorf("Could not set up ElasticSearch container: %v", err)
  332. }
  333. for try := 0; try <= tries; try++ {
  334. time.Sleep(delay)
  335. url := fmt.Sprintf("%s:%d", ip, port)
  336. log.Printf("Try %d: Connecting %s", try, url)
  337. conn := elastigo.NewConn()
  338. conn.Domain = ip
  339. conn.Port = strconv.Itoa(port)
  340. resp, err := conn.Health("")
  341. if err == nil && resp.Status != "" {
  342. log.Printf("Try %d: Successfully connected to %v", try, conn.Domain)
  343. return c, conn, nil
  344. }
  345. log.Printf("Try %d: Could not set up ElasticSearch container: %v", try, err)
  346. }
  347. return c, nil, errors.New("Could not set up ElasticSearch container.")
  348. }
  349. // OpenRedisContainerConnection is supported for legacy reasons. Don't use it.
  350. func OpenRedisContainerConnection(tries int, delay time.Duration) (c ContainerID, client *redis.Client, err error) {
  351. c, ip, port, err := SetupRedisContainer()
  352. if err != nil {
  353. return c, nil, fmt.Errorf("Could not set up Redis container: %v", err)
  354. }
  355. for try := 0; try <= tries; try++ {
  356. time.Sleep(delay)
  357. url := fmt.Sprintf("%s:%d", ip, port)
  358. log.Printf("Try %d: Connecting %s", try, url)
  359. client, err := redis.DialTimeout("tcp", url, 10*time.Second)
  360. if err == nil {
  361. log.Printf("Try %d: Successfully connected to %v", try, client.Addr)
  362. return c, client, nil
  363. }
  364. log.Printf("Try %d: Could not set up Redis container: %v", try, err)
  365. }
  366. return c, nil, errors.New("Could not set up Redis container.")
  367. }
  368. // ConnectToPostgreSQL starts a PostgreSQL image and passes the database url to the connector callback.
  369. func ConnectToPostgreSQL(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
  370. c, ip, port, err := SetupPostgreSQLContainer()
  371. if err != nil {
  372. return c, fmt.Errorf("Could not set up PostgreSQL container: %v", err)
  373. }
  374. for try := 0; try <= tries; try++ {
  375. time.Sleep(delay)
  376. url := fmt.Sprintf("postgres://%s:%s@%s:%d/postgres?sslmode=disable", PostgresUsername, PostgresPassword, ip, port)
  377. if connector(url) {
  378. return c, nil
  379. }
  380. log.Printf("Try %d failed. Retrying.", try)
  381. }
  382. return c, errors.New("Could not set up PostgreSQL container.")
  383. }
  384. // ConnectToMongoDB starts a MongoDB image and passes the database url to the connector callback.
  385. // The url will match the ip:port pattern (e.g. 123.123.123.123:4241)
  386. func ConnectToMongoDB(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
  387. c, ip, port, err := SetupMongoContainer()
  388. if err != nil {
  389. return c, fmt.Errorf("Could not set up MongoDB container: %v", err)
  390. }
  391. for try := 0; try <= tries; try++ {
  392. time.Sleep(delay)
  393. url := fmt.Sprintf("%s:%d", ip, port)
  394. if connector(url) {
  395. return c, nil
  396. }
  397. log.Printf("Try %d failed. Retrying.", try)
  398. }
  399. return c, errors.New("Could not set up MongoDB container.")
  400. }
  401. // ConnectToMySQL starts a MySQL image and passes the database url to the connector callback function.
  402. // The url will match the username:password@tcp(ip:port) pattern (e.g. `root:root@tcp(123.123.123.123:3131)`)
  403. func ConnectToMySQL(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
  404. c, ip, port, err := SetupMySQLContainer()
  405. if err != nil {
  406. return c, fmt.Errorf("Could not set up MySQL container: %v", err)
  407. }
  408. for try := 0; try <= tries; try++ {
  409. time.Sleep(delay)
  410. url := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql", MySQLUsername, MySQLPassword, ip, port)
  411. if connector(url) {
  412. return c, nil
  413. }
  414. log.Printf("Try %d failed. Retrying.", try)
  415. }
  416. return c, errors.New("Could not set up MySQL container.")
  417. }
  418. // ConnectToElasticSearch starts an ElasticSearch image and passes the database url to the connector callback function.
  419. // The url will match the ip:port pattern (e.g. 123.123.123.123:4241)
  420. func ConnectToElasticSearch(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
  421. c, ip, port, err := SetupElasticSearchContainer()
  422. if err != nil {
  423. return c, fmt.Errorf("Could not set up ElasticSearch container: %v", err)
  424. }
  425. for try := 0; try <= tries; try++ {
  426. time.Sleep(delay)
  427. url := fmt.Sprintf("%s:%d", ip, port)
  428. if connector(url) {
  429. return c, nil
  430. }
  431. log.Printf("Try %d failed. Retrying.", try)
  432. }
  433. return c, errors.New("Could not set up ElasticSearch container.")
  434. }
  435. // ConnectToRedis starts a Redis image and passes the database url to the connector callback function.
  436. // The url will match the ip:port pattern (e.g. 123.123.123.123:6379)
  437. func ConnectToRedis(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
  438. c, ip, port, err := SetupRedisContainer()
  439. if err != nil {
  440. return c, fmt.Errorf("Could not set up Redis container: %v", err)
  441. }
  442. for try := 0; try <= tries; try++ {
  443. time.Sleep(delay)
  444. url := fmt.Sprintf("%s:%d", ip, port)
  445. if connector(url) {
  446. return c, nil
  447. }
  448. log.Printf("Try %d failed. Retrying.", try)
  449. }
  450. return c, errors.New("Could not set up Redis container.")
  451. }