├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── .jvmopts
├── .scalafmt.conf
├── LICENSE
├── README.md
├── build.sbt
├── core
└── src
│ └── main
│ └── scala
│ └── com
│ └── whisk
│ └── docker
│ └── testkit
│ ├── BaseContainer.scala
│ ├── Container.scala
│ ├── ContainerCommandExecutor.scala
│ ├── ContainerPort.scala
│ ├── ContainerSpec.scala
│ ├── DockerContainerManager.scala
│ ├── DockerReadyChecker.scala
│ ├── DockerTestTimeouts.scala
│ └── package.scala
├── notes
├── 0.10.0.markdown
├── 0.11.0.markdown
├── 0.3.0.markdown
├── 0.4.0.markdown
├── 0.5.4.markdown
├── 0.6.1.markdown
├── 0.9.0.markdown
├── 0.9.5.markdown
├── 0.9.9.markdown
└── about.markdown
├── project
├── build.properties
└── plugins.sbt
├── samples
└── src
│ └── main
│ └── scala
│ └── com
│ └── whisk
│ └── docker
│ └── testkit
│ ├── DockerClickhouseService.scala
│ ├── DockerElasticsearchService.scala
│ ├── DockerMongodbService.scala
│ ├── DockerMysqlService.scala
│ └── DockerPostgresService.scala
├── scalatest
└── src
│ └── main
│ └── scala
│ └── com
│ └── whisk
│ └── docker
│ └── testkit
│ └── scalatest
│ └── DockerTestKitForAll.scala
└── tests
└── src
└── test
└── scala
└── com
└── whisk
└── docker
└── testkit
└── test
├── ClickhouseServiceTest.scala
├── ElasticsearchServiceTest.scala
├── MongodbServiceTest.scala
├── MultiContainerTest.scala
├── MysqlServiceTest.scala
└── PostgresServiceTest.scala
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | [ push, workflow_dispatch ]
4 | jobs:
5 | test:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: Checkout
9 | uses: actions/checkout@v2
10 | - name: Setup JDK
11 | uses: actions/setup-java@v2
12 | with:
13 | distribution: temurin
14 | java-version: 11
15 | - name: Build and Test
16 | run: sbt -v +test
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | target-shaded/
3 | project/sbt-launch-*.jar
4 | .idea/
5 | .DS_Store
6 | .bsp/
7 |
--------------------------------------------------------------------------------
/.jvmopts:
--------------------------------------------------------------------------------
1 | -Xms512M
2 | -Xmx4096M
3 | -Xss2M
4 | -XX:MaxMetaspaceSize=1024M
5 |
--------------------------------------------------------------------------------
/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version = "2.6.4"
2 | style = default
3 | maxColumn = 100
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 WhiskLabs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | docker-it-scala
2 | =============
3 |
4 | [](https://github.com/whisklabs/docker-it-scala/actions/workflows/ci.yaml)
5 | [](https://maven-badges.herokuapp.com/maven-central/com.whisk/docker-testkit-core_2.12)
6 | [](https://gitter.im/whisklabs/docker-it-scala?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
7 |
8 | Set of utility classes to make integration testing with dockerised services in Scala easy.
9 |
10 | You can read about reasoning behind it at [Finely Distributed](https://finelydistributed.io/integration-testing-with-docker-and-scala-85659d037740#.8mbrg311p).
11 |
12 | ## Setup
13 |
14 | docker-it-scala works with Spotify's docker-client to communicate to docker engine through *REST API* or *unix socket*.
15 | - [Spotify's docker-client](https://github.com/spotify/docker-client) (used in Whisk)
16 |
17 | ```scala
18 | libraryDependencies ++= Seq(
19 | "com.whisk" %% "docker-testkit-scalatest" % "0.11.0" % "test"
20 | ```
21 |
22 | ### Configuration
23 |
24 | You should be able to provide configuration purely through environment variables.
25 |
26 | Examples:
27 |
28 | ```
29 | export DOCKER_HOST=tcp://127.0.0.1:2375
30 | ```
31 |
32 | ```
33 | export DOCKER_HOST=unix:///var/run/docker.sock
34 | ```
35 |
36 |
37 | # Sample Services
38 |
39 | - [Elasticsearch](https://github.com/whisklabs/docker-it-scala/blob/master/samples/src/main/scala/com/whisk/docker/testkit/DockerElasticsearchService.scala)
40 | - [Mongodb](https://github.com/whisklabs/docker-it-scala/blob/master/samples/src/main/scala/com/whisk/docker/testkit/DockerMongodbService.scala)
41 | - [MySQL](https://github.com/whisklabs/docker-it-scala/blob/master/samples/src/main/scala/com/whisk/docker/testkit/DockerMysqlService.scala)
42 | - [Postgres](https://github.com/whisklabs/docker-it-scala/blob/master/samples/src/main/scala/com/whisk/docker/testkit/DockerPostgresService.scala)
43 | - [Clickhouse](https://github.com/whisklabs/docker-it-scala/blob/master/samples/src/main/scala/com/whisk/docker/testkit/DockerClickhouseService.scala)
44 | - [Multi container test](https://github.com/whisklabs/docker-it-scala/blob/master/tests/src/test/scala/com/whisk/docker/testkit/test/MultiContainerTest.scala)
45 | # Defining Containers
46 |
47 | There are two ways to define a docker container.
48 |
49 | Code based definitions and via `typesafe-config`.
50 |
51 | ## Code based definitions
52 |
53 | ```scala
54 | import com.whisk.docker.testkit.scalatest.DockerTestKitForAll
55 | import org.scalatest.Suite
56 |
57 | trait DockerMongodbService extends DockerTestKitForAll {
58 | self: Suite =>
59 |
60 | val DefaultMongodbPort = 27017
61 |
62 | val mongodbContainer = ContainerSpec("mongo:3.4.8")
63 | .withExposedPorts(DefaultMongodbPort)
64 | .withReadyChecker(DockerReadyChecker.LogLineContains("waiting for connections on port"))
65 | .toContainer
66 |
67 | override val managedContainers: ManagedContainers = mongodbContainer.toManagedContainer
68 | }
69 | ```
70 |
71 | You can check [usage example](https://github.com/whisklabs/docker-it-scala/blob/master/tests/src/test/scala/com/whisk/docker/testkit/test/MongodbServiceTest.scala)
72 |
73 | ### Container Paths
74 |
75 | - Elasticsearch => `docker.elasticsearch`
76 | - Mongodb => `docker.mongo`
77 | - Neo4j => `docker.mysql`
78 | - Postgres => `docker.postgres`
79 | - Clickhouse => `docker.clickhouse`
80 | ### Fields
81 |
82 | - `image-name` required (String)
83 | - `container-name` optional (String)
84 | - `command` optional (Array of Strings)
85 | - `entrypoint` optional (Array of Strings)
86 | - `environmental-variables` optional (Array of Strings)
87 | - `ready-checker` optional structure
88 | - `log-line` optional (String)
89 | - `http-response-code`
90 | - `code` optional (Int - defaults to `200`)
91 | - `port` required (Int)
92 | - `path` optional (String - defaults to `/`)
93 | - `within` optional (Int)
94 | - `looped` optional structure
95 | - `attempts` required (Int)
96 | - `delay` required (Int)
97 | - `port-maps` optional structure (list of structures)
98 | - `SOME_MAPPING_NAME`
99 | - `internal` required (Int)
100 | - `external` optional (Int)
101 | - `volume-maps` optional structure (list of structures)
102 | - `container` required (String)
103 | - `host` required (String)
104 | - `rw` optional (Boolean - default:false)
105 | - `memory` optional (Long)
106 | - `memory-reservation` optional (Long)
107 |
108 | # Testkit
109 |
110 | There are two testkits available -- one for `scalatest` and one for
111 | `specs2`.
112 |
113 | Both set up the necessary docker containers and check that they are
114 | ready **BEFORE** any test is run, and doesn't close the container
115 | until **ALL** the tests are run.
116 |
117 |
118 | ## Using in ScalaTest:
119 |
120 | ```scala
121 | class MyMongoSpec extends FlatSpec with Matchers with DockerMongodbService {
122 | ...
123 | }
124 | ```
125 |
126 | ### With Multiple containers:
127 |
128 | ```scala
129 | class MultiContainerTest
130 | extends AnyFunSuite
131 | with DockerElasticsearchService
132 | with DockerMongodbService {
133 |
134 | override val managedContainers: ContainerGroup =
135 | ContainerGroup.of(elasticsearchContainer, mongodbContainer)
136 |
137 | test("both containers should be ready") {
138 | assert(
139 | elasticsearchContainer.state().isInstanceOf[ContainerState.Ready],
140 | "elasticsearch container is ready"
141 | )
142 | assert(elasticsearchContainer.mappedPortOpt(9200).nonEmpty, "elasticsearch port is exposed")
143 |
144 | assert(mongodbContainer.state().isInstanceOf[ContainerState.Ready], "mongodb is ready")
145 | assert(mongodbContainer.mappedPortOpt(27017).nonEmpty, "port 2017 is exposed")
146 | }
147 | }
148 | ```
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | lazy val commonSettings = Seq(
2 | organization := "com.whisk",
3 | version := "0.12.0",
4 | scalaVersion := "2.13.6",
5 | crossScalaVersions := Seq("2.13.6", "2.12.15", "2.11.12", "3.0.2"),
6 | scalacOptions ++= Seq("-feature", "-deprecation"),
7 | Test / fork := true,
8 | licenses += ("MIT", url("http://opensource.org/licenses/MIT")),
9 | sonatypeProfileName := "com.whisk",
10 | publishMavenStyle := true,
11 | publishTo := Some(Opts.resolver.sonatypeStaging),
12 | pomExtra in Global := {
13 | https://github.com/whisklabs/docker-it-scala
14 |
15 | scm:git:github.com/whisklabs/docker-it-scala.git
16 | scm:git:git@github.com:whisklabs/docker-it-scala.git
17 | github.com/whisklabs/docker-it-scala.git
18 |
19 |
20 |
21 | viktortnk
22 | Viktor Taranenko
23 | https://github.com/viktortnk
24 |
25 |
26 | alari
27 | Dmitry Kurinskiy
28 | https://github.com/alari
29 |
30 |
31 | }
32 | )
33 |
34 | lazy val root =
35 | project
36 | .in(file("."))
37 | .settings(commonSettings: _*)
38 | .settings(publish := {}, publishLocal := {}, packagedArtifacts := Map.empty)
39 | .aggregate(core, scalatest, samples, coreShaded)
40 |
41 | lazy val core =
42 | project
43 | .settings(commonSettings: _*)
44 | .settings(
45 | name := "docker-testkit-core",
46 | libraryDependencies ++= Seq(
47 | "org.slf4j" % "slf4j-api" % "1.7.25",
48 | "com.spotify" % "docker-client" % "8.16.0",
49 | "com.google.code.findbugs" % "jsr305" % "3.0.1"
50 | )
51 | )
52 |
53 | lazy val scalatest =
54 | project
55 | .settings(commonSettings: _*)
56 | .settings(
57 | name := "docker-testkit-scalatest",
58 | libraryDependencies ++= Seq(
59 | "org.scalatest" %% "scalatest" % "3.2.9",
60 | "ch.qos.logback" % "logback-classic" % "1.2.3" % "test"
61 | )
62 | )
63 | .dependsOn(core)
64 |
65 | lazy val samples =
66 | project
67 | .settings(commonSettings: _*)
68 | .settings(name := "docker-testkit-samples")
69 | .dependsOn(core, scalatest)
70 |
71 | lazy val tests =
72 | project
73 | .settings(commonSettings: _*)
74 | .settings(
75 | name := "docker-testkit-tests",
76 | libraryDependencies ++= Seq(
77 | "org.postgresql" % "postgresql" % "42.1.4" % "test",
78 | "mysql" % "mysql-connector-java" % "5.1.44" % "test",
79 | "com.clickhouse" % "clickhouse-jdbc" % "0.5.0" % "test"
80 | )
81 | )
82 | .dependsOn(core, scalatest, samples % "test")
83 |
84 | lazy val coreShaded =
85 | project
86 | .in(file("core"))
87 | .settings(commonSettings: _*)
88 | .settings(
89 | name := "docker-testkit-core-shaded",
90 | libraryDependencies ++=
91 | Seq(
92 | "com.spotify" % "docker-client" % "8.16.0" classifier "shaded",
93 | "com.google.code.findbugs" % "jsr305" % "3.0.1"
94 | ),
95 | target := baseDirectory.value / "target-shaded"
96 | )
97 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/whisk/docker/testkit/BaseContainer.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import java.util.concurrent.atomic.AtomicReference
4 |
5 | import com.spotify.docker.client.messages.ContainerInfo
6 | import org.slf4j.LoggerFactory
7 |
8 | import scala.collection.JavaConverters._
9 |
10 | sealed trait ContainerState
11 |
12 | object ContainerState {
13 |
14 | trait HasId extends ContainerState {
15 | val id: String
16 | }
17 |
18 | trait IsRunning extends HasId {
19 | val info: ContainerInfo
20 | override val id: String = info.id
21 | }
22 |
23 | object NotStarted extends ContainerState
24 | case class Created(id: String) extends ContainerState with HasId
25 | case class Starting(id: String) extends ContainerState with HasId
26 | case class Running(info: ContainerInfo) extends ContainerState with IsRunning
27 | case class Ready(info: ContainerInfo) extends ContainerState with IsRunning
28 | case class Failed(id: String) extends ContainerState
29 | object Stopped extends ContainerState
30 | }
31 |
32 | abstract class BaseContainer {
33 |
34 | def spec: ContainerSpec
35 |
36 | private lazy val log = LoggerFactory.getLogger(this.getClass)
37 |
38 | private val _state = new AtomicReference[ContainerState](ContainerState.NotStarted)
39 |
40 | def state(): ContainerState = {
41 | _state.get()
42 | }
43 |
44 | private def updateState(state: ContainerState): Unit = {
45 | _state.set(state)
46 | }
47 |
48 | private[docker] def created(id: String): Unit = {
49 | updateState(ContainerState.Created(id))
50 | }
51 |
52 | private[docker] def starting(id: String): Unit = {
53 | updateState(ContainerState.Starting(id))
54 | }
55 |
56 | private[docker] def running(info: ContainerInfo): Unit = {
57 | updateState(ContainerState.Running(info))
58 | }
59 |
60 | private[docker] def ready(info: ContainerInfo): Unit = {
61 | updateState(ContainerState.Ready(info))
62 | }
63 |
64 | private def addresses(info: ContainerInfo): Seq[String] = {
65 | val addrs: Iterable[String] = for {
66 | networks <- Option(info.networkSettings().networks()).map(_.asScala).toSeq
67 | (key, network) <- networks
68 | ip <- Option(network.ipAddress)
69 | } yield {
70 | ip
71 | }
72 | addrs.toList
73 | }
74 |
75 | private def portsFrom(info: ContainerInfo): Map[Int, Int] = {
76 | info
77 | .networkSettings()
78 | .ports()
79 | .asScala
80 | .collect {
81 | case (portStr, bindings) if Option(bindings).exists(!_.isEmpty) =>
82 | val port = ContainerPort.parsed(portStr).port
83 | val hostPort = bindings.get(0).hostPort().toInt
84 | port -> hostPort
85 | }
86 | .toMap
87 | }
88 |
89 | def ipAddresses(): Seq[String] = {
90 | state() match {
91 | case s: ContainerState.IsRunning =>
92 | addresses(s.info)
93 | case _ =>
94 | throw new Exception("can't get addresses of not running container")
95 | }
96 | }
97 |
98 | def mappedPorts(): Map[Int, Int] = {
99 | state() match {
100 | case s: ContainerState.IsRunning =>
101 | portsFrom(s.info)
102 | case _ =>
103 | throw new Exception("can't get ports of not running container")
104 | }
105 | }
106 |
107 | def mappedPort(port: Int): Int = {
108 | mappedPorts().apply(port)
109 | }
110 |
111 | def mappedPortOpt(port: Int): Option[Int] = {
112 | mappedPorts().get(port)
113 | }
114 |
115 | def toManagedContainer: SingleContainer = SingleContainer(this)
116 | }
117 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/whisk/docker/testkit/Container.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | class Container(override val spec: ContainerSpec) extends BaseContainer
4 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/whisk/docker/testkit/ContainerCommandExecutor.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import java.nio.charset.StandardCharsets
4 | import java.util.concurrent.TimeUnit
5 |
6 | import com.google.common.io.Closeables
7 | import com.spotify.docker.client.DockerClient.{AttachParameter, RemoveContainerParam}
8 | import com.spotify.docker.client.messages._
9 | import com.spotify.docker.client.{DockerClient, LogMessage, LogStream}
10 |
11 | import scala.concurrent.{ExecutionContext, Future, Promise}
12 |
13 | class StartFailedException(msg: String) extends Exception(msg)
14 |
15 | class ContainerCommandExecutor(val client: DockerClient) {
16 |
17 | def createContainer(
18 | spec: ContainerSpec
19 | )(implicit ec: ExecutionContext): Future[ContainerCreation] = {
20 | Future(
21 | scala.concurrent.blocking(client.createContainer(spec.containerConfig(), spec.name.orNull))
22 | )
23 | }
24 |
25 | def startContainer(id: String)(implicit ec: ExecutionContext): Future[Unit] = {
26 | Future(scala.concurrent.blocking(client.startContainer(id)))
27 | }
28 |
29 | def runningContainer(id: String)(implicit ec: ExecutionContext): Future[ContainerInfo] = {
30 | def inspect() = {
31 | Future(scala.concurrent.blocking(client.inspectContainer(id))).flatMap { info =>
32 | val status = info.state().status()
33 | val badStates = Set("removing", "paused", "exited", "dead")
34 | if (status == "running") {
35 | Future.successful(info)
36 | } else if (badStates(status)) {
37 | Future.failed(new StartFailedException("container is in unexpected state: " + status))
38 | } else {
39 | Future.failed(new Exception("not running yet"))
40 | }
41 | }
42 | }
43 |
44 | def attempt(rest: Int): Future[ContainerInfo] = {
45 | inspect().recoverWith {
46 | case e: StartFailedException => Future.failed(e)
47 | case _ if rest > 0 =>
48 | RetryUtils.withDelay(TimeUnit.SECONDS.toMillis(1))(attempt(rest - 1))
49 | case _ =>
50 | Future.failed(new StartFailedException("failed to get container in running state"))
51 | }
52 | }
53 |
54 | attempt(10)
55 | }
56 |
57 | private def logStreamFuture(id: String, withErr: Boolean)(implicit
58 | ec: ExecutionContext
59 | ): Future[LogStream] = {
60 | val baseParams = List(AttachParameter.STDOUT, AttachParameter.STREAM, AttachParameter.LOGS)
61 | val logParams = if (withErr) AttachParameter.STDERR :: baseParams else baseParams
62 | Future(scala.concurrent.blocking(client.attachContainer(id, logParams: _*)))
63 | }
64 |
65 | def withLogStreamLines(id: String, withErr: Boolean)(
66 | f: String => Unit
67 | )(implicit ec: ExecutionContext): Unit = {
68 |
69 | logStreamFuture(id, withErr).foreach { stream =>
70 | stream.forEachRemaining(new java.util.function.Consumer[LogMessage] {
71 |
72 | override def accept(t: LogMessage): Unit = {
73 | val str = StandardCharsets.US_ASCII.decode(t.content()).toString
74 | f(s"[$id] $str")
75 | }
76 | })
77 | }
78 | }
79 |
80 | def withLogStreamLinesRequirement(id: String, withErr: Boolean)(
81 | f: String => Boolean
82 | )(implicit ec: ExecutionContext): Future[Unit] = {
83 |
84 | logStreamFuture(id, withErr).flatMap { stream =>
85 | val p = Promise[Unit]()
86 | Future {
87 | stream.forEachRemaining(new java.util.function.Consumer[LogMessage] {
88 |
89 | override def accept(t: LogMessage): Unit = {
90 | val str = StandardCharsets.US_ASCII.decode(t.content()).toString
91 | if (f(str)) {
92 | p.trySuccess(())
93 | Closeables.close(stream, true)
94 | }
95 | }
96 | })
97 | }
98 | p.future
99 | }
100 | }
101 |
102 | def remove(id: String, force: Boolean, removeVolumes: Boolean)(implicit
103 | ec: ExecutionContext
104 | ): Future[Unit] = {
105 | Future(
106 | scala.concurrent.blocking(
107 | client.removeContainer(
108 | id,
109 | RemoveContainerParam.forceKill(force),
110 | RemoveContainerParam.removeVolumes(removeVolumes)
111 | )
112 | )
113 | )
114 | }
115 |
116 | def close(): Unit = {
117 | Closeables.close(client, true)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/whisk/docker/testkit/ContainerPort.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | object PortProtocol extends Enumeration {
4 | val TCP, UDP = Value
5 | }
6 |
7 | case class ContainerPort(port: Int, protocol: PortProtocol.Value)
8 |
9 | object ContainerPort {
10 | def parsed(str: String): ContainerPort = {
11 | val Array(p, rest @ _*) = str.split("/")
12 | val proto = rest.headOption
13 | .flatMap(pr => PortProtocol.values.find(_.toString.equalsIgnoreCase(pr)))
14 | .getOrElse(PortProtocol.TCP)
15 | ContainerPort(p.toInt, proto)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/whisk/docker/testkit/ContainerSpec.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import java.util.Collections
4 |
5 | import com.spotify.docker.client.messages.{ContainerConfig, HostConfig, PortBinding}
6 | import com.spotify.docker.client.messages.HostConfig.Bind
7 |
8 | import scala.collection.JavaConverters._
9 |
10 | case class ContainerSpec(image: String) {
11 |
12 | private val builder: ContainerConfig.Builder = ContainerConfig.builder().image(image)
13 | private val hostConfigBuilder: HostConfig.Builder = HostConfig.builder()
14 |
15 | private var _readyChecker: Option[DockerReadyChecker] = None
16 | private var _name: Option[String] = None
17 |
18 | def withCommand(cmd: String*): ContainerSpec = {
19 | builder.cmd(cmd: _*)
20 | this
21 | }
22 |
23 | def withExposedPorts(ports: Int*): ContainerSpec = {
24 | val binds: Seq[(Int, PortBinding)] =
25 | ports.map(p => p -> PortBinding.randomPort("0.0.0.0")).toSeq
26 | withPortBindings(binds: _*)
27 | }
28 |
29 | def withPortBindings(ps: (Int, PortBinding)*): ContainerSpec = {
30 | val binds: Map[String, java.util.List[PortBinding]] = ps.map {
31 | case (guestPort, binding) =>
32 | guestPort.toString -> Collections.singletonList(binding)
33 | }.toMap
34 |
35 | hostConfigBuilder.portBindings(binds.asJava)
36 | builder.exposedPorts(binds.keySet.asJava)
37 | this
38 | }
39 |
40 | def withVolumeBindings(vs: Bind*): ContainerSpec = {
41 | hostConfigBuilder.binds(vs: _*)
42 | this
43 | }
44 |
45 | def withReadyChecker(checker: DockerReadyChecker): ContainerSpec = {
46 | _readyChecker = Some(checker)
47 | this
48 | }
49 |
50 | def withName(name: String): ContainerSpec = {
51 | _name = Some(name)
52 | this
53 | }
54 |
55 | def withEnv(env: String*): ContainerSpec = {
56 | builder.env(env: _*)
57 | this
58 | }
59 |
60 | def withConfiguration(
61 | withBuilder: ContainerConfig.Builder => ContainerConfig.Builder
62 | ): ContainerSpec = {
63 | withBuilder(builder)
64 | this
65 | }
66 |
67 | def withHostConfiguration(
68 | withBuilder: HostConfig.Builder => HostConfig.Builder
69 | ): ContainerSpec = {
70 | withBuilder(hostConfigBuilder)
71 | this
72 | }
73 |
74 | def name: Option[String] = _name
75 |
76 | def readyChecker: Option[DockerReadyChecker] = _readyChecker
77 |
78 | def containerConfig(): ContainerConfig = {
79 | builder.hostConfig(hostConfigBuilder.build()).build()
80 | }
81 |
82 | def toContainer: Container = new Container(this)
83 |
84 | def toManagedContainer: SingleContainer = SingleContainer(this.toContainer)
85 | }
86 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/whisk/docker/testkit/DockerContainerManager.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import java.util.concurrent.{ConcurrentHashMap, TimeUnit}
4 |
5 | import com.spotify.docker.client.exceptions.ImageNotFoundException
6 | import com.spotify.docker.client.messages.ContainerCreation
7 | import org.slf4j.LoggerFactory
8 |
9 | import scala.concurrent.{Await, ExecutionContext, Future}
10 | import scala.collection.JavaConverters._
11 | import scala.language.postfixOps
12 |
13 | trait ManagedContainers
14 |
15 | case class SingleContainer(container: BaseContainer) extends ManagedContainers
16 |
17 | case class ContainerGroup(containers: Seq[BaseContainer]) extends ManagedContainers {
18 | require(containers.nonEmpty, "container group should be non-empty")
19 | }
20 |
21 | object ContainerGroup {
22 |
23 | def of(containers: BaseContainer*): ContainerGroup = ContainerGroup(containers)
24 | }
25 |
26 | class DockerContainerManager(
27 | managedContainers: ManagedContainers,
28 | executor: ContainerCommandExecutor,
29 | dockerTestTimeouts: DockerTestTimeouts,
30 | executionContext: ExecutionContext
31 | ) {
32 |
33 | private implicit val ec: ExecutionContext = executionContext
34 |
35 | private lazy val log = LoggerFactory.getLogger(this.getClass)
36 |
37 | private val registeredContainers = new ConcurrentHashMap[String, String]()
38 |
39 | private def waitUntilReady(container: BaseContainer): Future[Unit] = {
40 | container.spec.readyChecker match {
41 | case None =>
42 | Future.successful(())
43 | case Some(checker) =>
44 | checker(container)(executor, executionContext)
45 | }
46 | }
47 |
48 | private def printWarningsIfExist(creation: ContainerCreation): Unit = {
49 | Option(creation.warnings())
50 | .map(_.asScala.toList)
51 | .getOrElse(Nil)
52 | .foreach(w => log.warn(s"creating container: $w"))
53 | }
54 |
55 | private def ensureImage(image: String): Future[Unit] = {
56 | Future(scala.concurrent.blocking(executor.client.inspectImage(image)))
57 | .map(_ => ())
58 | .recoverWith {
59 | case x: ImageNotFoundException =>
60 | log.info(s"image [$image] not found. pulling...")
61 | Future(scala.concurrent.blocking(executor.client.pull(image)))
62 | }
63 | }
64 |
65 | //TODO log listeners
66 | def startContainer(container: BaseContainer): Future[Unit] = {
67 | val image = container.spec.image
68 | val startTime = System.nanoTime()
69 | log.debug("Starting container: {}", image)
70 | for {
71 | creation <- executor.createContainer(container.spec)
72 | id = creation.id()
73 | _ = registeredContainers.put(id, image)
74 | _ = container.created(id)
75 | _ = printWarningsIfExist(creation)
76 | _ = log.info(s"starting container with id: $id")
77 | _ <- executor.startContainer(id)
78 | _ = container.starting(id)
79 | _ = log.info(s"container is starting. id=$id")
80 | runningContainer <- executor.runningContainer(id)
81 | _ = log.debug(s"container entered running state. id=$id")
82 | _ = container.running(runningContainer)
83 | _ = log.debug(s"preparing to execute ready check for container")
84 | res <- waitUntilReady(container)
85 | _ = log.debug(s"container is ready. id=$id")
86 | } yield {
87 | container.ready(runningContainer)
88 | val timeTaken = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)
89 | log.info(s"container $image is ready after ${timeTaken / 1000.0}s")
90 | res
91 | }
92 |
93 | }
94 |
95 | def start(): Unit = {
96 | log.debug("Starting containers")
97 | val containers: Seq[BaseContainer] = managedContainers match {
98 | case SingleContainer(c) => Seq(c)
99 | case ContainerGroup(cs) => cs
100 | case _ => throw new Exception("unsupported type of managed containers")
101 | }
102 |
103 | val imagesF = Future.traverse(containers.map(_.spec.image))(ensureImage)
104 | Await.result(imagesF, dockerTestTimeouts.pull)
105 |
106 | val startedContainersF = Future.traverse(containers)(startContainer)
107 |
108 | sys.addShutdownHook(
109 | stop()
110 | )
111 |
112 | try {
113 | Await.result(startedContainersF, dockerTestTimeouts.init)
114 | } catch {
115 | case e: Exception =>
116 | log.error("Exception during container initialization", e)
117 | stop()
118 | throw new RuntimeException("Cannot run all required containers")
119 | }
120 | }
121 |
122 | def stop(): Unit = {
123 | try {
124 | Await.ready(stopRmAll(), dockerTestTimeouts.stop)
125 | } catch {
126 | case e: Throwable =>
127 | log.error(e.getMessage, e)
128 | }
129 | }
130 |
131 | def stopRmAll(): Future[Unit] = {
132 | val future = Future.traverse(registeredContainers.asScala.toSeq) {
133 | case (cid, _) =>
134 | executor.remove(cid, force = true, removeVolumes = true)
135 | }
136 | future.onComplete { _ =>
137 | executor.close()
138 | }
139 | future.map(_ => ())
140 | }
141 |
142 | }
143 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/whisk/docker/testkit/DockerReadyChecker.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import java.net.{HttpURLConnection, URL}
4 | import java.sql.DriverManager
5 | import java.util.{Timer, TimerTask}
6 |
7 | import scala.concurrent.duration.FiniteDuration
8 | import scala.concurrent.{ExecutionContext, Future, Promise, TimeoutException}
9 |
10 | class FailFastCheckException(m: String) extends Exception(m)
11 |
12 | trait DockerReadyChecker {
13 |
14 | def apply(
15 | container: BaseContainer
16 | )(implicit docker: ContainerCommandExecutor, ec: ExecutionContext): Future[Unit]
17 |
18 | def within(duration: FiniteDuration): DockerReadyChecker = {
19 | DockerReadyChecker.TimeLimited(this, duration)
20 | }
21 |
22 | def looped(attempts: Int, delay: FiniteDuration): DockerReadyChecker = {
23 | DockerReadyChecker.Looped(this, attempts, delay)
24 | }
25 | }
26 |
27 | object RetryUtils {
28 |
29 | def withDelay[T](delay: Long)(f: => Future[T]): Future[T] = {
30 | val timer = new Timer()
31 | val promise = Promise[T]()
32 | timer.schedule(
33 | new TimerTask {
34 | override def run(): Unit = {
35 | promise.completeWith(f)
36 | timer.cancel()
37 | }
38 | },
39 | delay
40 | )
41 | promise.future
42 | }
43 |
44 | def runWithin[T](future: => Future[T], deadline: FiniteDuration)(implicit
45 | ec: ExecutionContext
46 | ): Future[T] = {
47 | val bail = Promise[T]()
48 | withDelay(deadline.toMillis)(
49 | bail
50 | .tryCompleteWith(Future.failed(new TimeoutException(s"timed out after $deadline")))
51 | .future
52 | )
53 | Future.firstCompletedOf(future :: bail.future :: Nil)
54 | }
55 |
56 | def looped[T](future: => Future[T], attempts: Int, delay: FiniteDuration)(implicit
57 | ec: ExecutionContext
58 | ): Future[T] = {
59 | def attempt(rest: Int): Future[T] = {
60 | future.recoverWith {
61 | case e: FailFastCheckException => Future.failed(e)
62 | case e if rest > 0 =>
63 | withDelay(delay.toMillis)(attempt(rest - 1))
64 | case e =>
65 | Future.failed(e)
66 | }
67 | }
68 |
69 | attempt(attempts)
70 | }
71 | }
72 |
73 | object DockerReadyChecker {
74 |
75 | case class And(r1: DockerReadyChecker, r2: DockerReadyChecker) extends DockerReadyChecker {
76 |
77 | override def apply(
78 | container: BaseContainer
79 | )(implicit docker: ContainerCommandExecutor, ec: ExecutionContext): Future[Unit] = {
80 | val aF = r1(container)
81 | val bF = r2(container)
82 | for {
83 | a <- aF
84 | b <- bF
85 | } yield {
86 | ()
87 | }
88 | }
89 | }
90 |
91 | object Always extends DockerReadyChecker {
92 | override def apply(
93 | container: BaseContainer
94 | )(implicit docker: ContainerCommandExecutor, ec: ExecutionContext): Future[Unit] =
95 | Future.successful(())
96 | }
97 |
98 | case class HttpResponseCode(
99 | port: Int,
100 | path: String = "/",
101 | host: Option[String] = None,
102 | code: Int = 200
103 | ) extends DockerReadyChecker {
104 |
105 | override def apply(
106 | container: BaseContainer
107 | )(implicit docker: ContainerCommandExecutor, ec: ExecutionContext): Future[Unit] = {
108 |
109 | val p = container.mappedPorts()(port)
110 | val url = new URL("http", host.getOrElse(docker.client.getHost), p, path)
111 | Future {
112 | scala.concurrent.blocking {
113 | val con = url.openConnection().asInstanceOf[HttpURLConnection]
114 | try {
115 | if (con.getResponseCode != code)
116 | throw new Exception("unexpected response code: " + con.getResponseCode)
117 | } catch {
118 | case e: java.net.ConnectException =>
119 | throw e
120 | }
121 | }
122 | }
123 | }
124 | }
125 |
126 | case class LogLineContains(str: String) extends DockerReadyChecker {
127 |
128 | override def apply(
129 | container: BaseContainer
130 | )(implicit docker: ContainerCommandExecutor, ec: ExecutionContext): Future[Unit] = {
131 | container.state() match {
132 | case ContainerState.Ready(_) =>
133 | Future.successful(())
134 | case state: ContainerState.HasId =>
135 | docker
136 | .withLogStreamLinesRequirement(state.id, withErr = true)(_.contains(str))
137 | .map(_ => ())
138 | case _ =>
139 | Future.failed(
140 | new FailFastCheckException("can't initialise LogStream to container without Id")
141 | )
142 | }
143 | }
144 | }
145 |
146 | private[docker] case class TimeLimited(underlying: DockerReadyChecker, duration: FiniteDuration)
147 | extends DockerReadyChecker {
148 |
149 | override def apply(
150 | container: BaseContainer
151 | )(implicit docker: ContainerCommandExecutor, ec: ExecutionContext): Future[Unit] = {
152 | RetryUtils.runWithin(underlying(container), duration)
153 | }
154 | }
155 |
156 | private[docker] case class Looped(
157 | underlying: DockerReadyChecker,
158 | attempts: Int,
159 | delay: FiniteDuration
160 | ) extends DockerReadyChecker {
161 |
162 | override def apply(
163 | container: BaseContainer
164 | )(implicit docker: ContainerCommandExecutor, ec: ExecutionContext): Future[Unit] = {
165 |
166 | def attempt(attemptsLeft: Int): Future[Unit] = {
167 | underlying(container)
168 | .recoverWith {
169 | case e: FailFastCheckException => Future.failed(e)
170 | case e if attemptsLeft > 0 =>
171 | RetryUtils.withDelay(delay.toMillis)(attempt(attemptsLeft - 1))
172 | case e =>
173 | Future.failed(e)
174 | }
175 | }
176 |
177 | attempt(attempts)
178 | }
179 | }
180 |
181 | private[docker] case class F(f: BaseContainer => Future[Unit]) extends DockerReadyChecker {
182 | override def apply(
183 | container: BaseContainer
184 | )(implicit docker: ContainerCommandExecutor, ec: ExecutionContext): Future[Unit] =
185 | f(container)
186 | }
187 |
188 | case class Jdbc(
189 | driverClass: String,
190 | user: String,
191 | password: Option[String],
192 | database: Option[String] = None,
193 | port: Option[Int] = None
194 | ) extends DockerReadyChecker {
195 |
196 | private val driverLower = driverClass.toLowerCase
197 | private[Jdbc] val dbms: String = if (driverLower.contains("mysql")) {
198 | "mysql"
199 | } else if (driverLower.contains("postgres")) {
200 | "postgresql"
201 | } else if (driverLower.contains("clickhouse")) {
202 | "clickhouse"
203 | } else {
204 | throw new IllegalArgumentException("unsupported database for ready check")
205 | }
206 |
207 | override def apply(
208 | container: BaseContainer
209 | )(implicit docker: ContainerCommandExecutor, ec: ExecutionContext): Future[Unit] = {
210 |
211 | Future(scala.concurrent.blocking {
212 | try {
213 | Class.forName(driverClass)
214 | val p = port match {
215 | case Some(v) => container.mappedPort(v)
216 | case None => container.mappedPorts().head._2
217 | }
218 |
219 | val url = "jdbc:" + dbms + "://" + docker.client.getHost + ":" + p + "/" + database
220 | .getOrElse("")
221 |
222 | val connection = Option(DriverManager.getConnection(url, user, password.orNull))
223 | connection.foreach(_.close())
224 | if (connection.isEmpty) {
225 | throw new Exception(s"can't establish jdbc connection to $url")
226 | }
227 | } catch {
228 | case e: ClassNotFoundException =>
229 | throw new FailFastCheckException(s"jdbc class $driverClass not found")
230 | }
231 | })
232 | }
233 | }
234 |
235 | }
236 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/whisk/docker/testkit/DockerTestTimeouts.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import scala.concurrent.duration._
4 |
5 | case class DockerTestTimeouts(
6 | pull: FiniteDuration = 5.minutes,
7 | init: FiniteDuration = 60.seconds,
8 | stop: FiniteDuration = 10.seconds
9 | )
10 |
11 | object DockerTestTimeouts {
12 |
13 | val Default: DockerTestTimeouts = DockerTestTimeouts()
14 | }
15 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/whisk/docker/testkit/package.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker
2 |
3 | import java.util.concurrent.atomic.AtomicBoolean
4 |
5 | import scala.concurrent.{Future, Promise}
6 |
7 | /**
8 | * General utility functions
9 | */
10 | package object testkit {
11 | implicit class OptionalOps[A](val content: A) extends AnyVal {
12 | def withOption[B](optional: Option[B])(f: (A, B) => A): A =
13 | optional match {
14 | case None => content
15 | case Some(x) => f(content, x)
16 | }
17 | }
18 |
19 | private[docker] class SinglePromise[T] {
20 | val promise: Promise[T] = Promise[T]()
21 |
22 | def future: Future[T] = promise.future
23 |
24 | val flag = new AtomicBoolean(false)
25 |
26 | def init(f: => Future[T]): Future[T] = {
27 | if (!flag.getAndSet(true)) {
28 | promise.tryCompleteWith(f)
29 | }
30 | future
31 | }
32 | }
33 |
34 | private[docker] object SinglePromise {
35 | def apply[T] = new SinglePromise[T]
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/notes/0.10.0.markdown:
--------------------------------------------------------------------------------
1 | #scala #docker #integration-testing
2 |
3 | ## Highlights
4 |
5 | * global inner refactoring, packages were changed
6 | * removed publishing of next artifacts:
7 | - docker-testkit-config
8 | - docker-testkit-impl-spotify
9 | - docker-testkit-impl-spotify-shaded
10 | - docker-testkit-impl-docker-java
11 | * `"com.spotify" % "docker-client" % "8.16.0"` used as default backend
12 |
--------------------------------------------------------------------------------
/notes/0.11.0.markdown:
--------------------------------------------------------------------------------
1 | #scala #docker #integration-testing
2 |
3 | ## Highlights
4 |
5 | * add cross-compilation for scala [3](https://github.com/whisklabs/docker-it-scala/pull/146)
6 | * proper [timeouts handling](https://github.com/whisklabs/docker-it-scala/pull/133)
7 | * updated [sbt with plugins](https://github.com/whisklabs/docker-it-scala/pull/147)
8 | * create Github Action [CI](https://github.com/whisklabs/docker-it-scala/pull/147)
9 | * docs and small code improvements
10 |
--------------------------------------------------------------------------------
/notes/0.3.0.markdown:
--------------------------------------------------------------------------------
1 | #scala #docker #integration-testing
2 |
3 | * [Specs2](https://etorreborre.github.io/specs2/) support (in addition to [Scalatest](http://scalatest.org/)) ([@jacobono](https://github.com/jacobono))
4 | * [Typesafe Config](https://github.com/typesafehub/config)-based containers definition ([@jacobono](https://github.com/jacobono))
5 | * publishing to Bintray
6 | * minor improvements
7 |
--------------------------------------------------------------------------------
/notes/0.4.0.markdown:
--------------------------------------------------------------------------------
1 | #scala #docker #integration-testing
2 |
3 | * All containers definitions don't require implicit `Docker` instance
4 | * Make possible to check containers definition equality
5 | * Make scalatest and specs2 module independent from `config`
6 | * Clean duplicate tests
7 |
--------------------------------------------------------------------------------
/notes/0.5.4.markdown:
--------------------------------------------------------------------------------
1 | #scala #docker #integration-testing
2 |
3 | * `DockerKafkaService` to use spotify/docker container with embedded Zookeeper
4 | * upgrade [docker-java](https://github.com/docker-java/docker-java) dependency to 2.1.4
5 | * removed dependency from `undelay` and `odelay-core` to avoid requirement on external resolver
6 | * clean duplicate tests
7 | * use docker inspect command to make getting container info more efficient
8 | * releasing to Maven Central
9 |
--------------------------------------------------------------------------------
/notes/0.6.1.markdown:
--------------------------------------------------------------------------------
1 | #scala #docker #integration-testing
2 |
3 | * moving containers definitions outside `core` module
4 | * rename `core` module published artifact
5 |
--------------------------------------------------------------------------------
/notes/0.9.0.markdown:
--------------------------------------------------------------------------------
1 | #scala #docker #integration-testing
2 |
3 | ## Highlights
4 |
5 | * introduce exectutor based on Spotify's [docker-client](https://github.com/spotify/docker-client)
6 | * separate docker exectutors library from core
7 | * support for unix-socket
8 | * support for linked containers (e.g. Zookeeper + Kafka)
9 | * support for Scala 2.12
10 |
11 |
--------------------------------------------------------------------------------
/notes/0.9.5.markdown:
--------------------------------------------------------------------------------
1 | #scala #docker #integration-testing
2 |
3 | ## Highlights
4 |
5 | * extend `HostConfig` with `memory` and `memoryReservation` settings (credit [Chris Birchall](https://github.com/cb372))
6 | * allowing to override entrypoint (credit [Chris Birchall](https://github.com/cb372))
7 | * Spotify's [docker-client](https://github.com/spotify/docker-client) updated to 8.9.0
8 | * [docker-client](https://github.com/docker-java/docker-java) updated to 3.0.13
9 | * move project to sbt 1.x
10 |
11 |
--------------------------------------------------------------------------------
/notes/0.9.9.markdown:
--------------------------------------------------------------------------------
1 | #scala #docker #integration-testing
2 |
3 | ## Highlights
4 |
5 | * add cross-compilation for scala [2.13.0](https://github.com/scala/scala/releases/tag/v2.13.0)
6 | * updated [sbt](https://github.com/sbt/sbt) to 1.2.8
7 | * updated [scalatest](https://github.com/scalatest/scalatest) to 3.0.8
8 | * updated [specs2-core](https://github.com/etorreborre/specs2) to 4.5.1
9 | * updated [ficus](https://github.com/iheartradio/ficus) to 1.4.7
10 |
--------------------------------------------------------------------------------
/notes/about.markdown:
--------------------------------------------------------------------------------
1 | [docker-it-scala](https://github.com/whisklabs/docker-it-scala) is a set of utility classes to simplify integration testing with dockerized services in Scala.
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.9.8
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.10")
2 |
3 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2")
4 |
5 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.5")
6 |
7 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.18-1")
8 |
--------------------------------------------------------------------------------
/samples/src/main/scala/com/whisk/docker/testkit/DockerClickhouseService.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import com.spotify.docker.client.messages.PortBinding
4 | import com.whisk.docker.testkit.scalatest.DockerTestKitForAll
5 | import org.scalatest.Suite
6 |
7 | import scala.concurrent.duration._
8 |
9 | trait DockerClickhouseService extends DockerTestKitForAll { self: Suite =>
10 | override val dockerTestTimeouts: DockerTestTimeouts = DockerTestTimeouts(pull = 10.minutes, init = 10.minutes, stop = 1.minutes)
11 |
12 | def ClickhouseAdvertisedPort = 8123
13 | def ClickhouseExposedPort = 8123
14 |
15 | val ClickhouseUser = "default"
16 | val ClickhousePassword = ""
17 |
18 | val clickhouseContainer = ContainerSpec("clickhouse/clickhouse-server:23.6")
19 | .withEnv(s"CLICKHOUSE_USER=$ClickhouseUser", s"CLICKHOUSE_PASSWORD=$ClickhousePassword")
20 | .withPortBindings((ClickhouseAdvertisedPort, PortBinding.of("0.0.0.0", ClickhouseExposedPort)))
21 | .withReadyChecker(
22 | DockerReadyChecker
23 | .Jdbc(
24 | driverClass = "com.clickhouse.jdbc.ClickHouseDriver",
25 | user = ClickhouseUser,
26 | password = Some(ClickhousePassword)
27 | )
28 | .looped(15, 1.second)
29 | )
30 | .toContainer
31 |
32 | override val managedContainers: ManagedContainers = clickhouseContainer.toManagedContainer
33 | }
34 |
--------------------------------------------------------------------------------
/samples/src/main/scala/com/whisk/docker/testkit/DockerElasticsearchService.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import java.util.UUID
4 |
5 | import com.whisk.docker.testkit.scalatest.DockerTestKitForAll
6 | import org.scalatest.Suite
7 |
8 | import scala.concurrent.duration._
9 |
10 | trait DockerElasticsearchService extends DockerTestKitForAll {
11 | self: Suite =>
12 |
13 | val DefaultElasticsearchHttpPort = 9200
14 | val DefaultElasticsearchClientPort = 9300
15 | val EsClusterName: String = UUID.randomUUID().toString
16 |
17 | protected val elasticsearchContainer =
18 | ContainerSpec("docker.elastic.co/elasticsearch/elasticsearch:6.2.4")
19 | .withExposedPorts(DefaultElasticsearchHttpPort, DefaultElasticsearchClientPort)
20 | .withEnv(
21 | "http.host=0.0.0.0",
22 | "xpack.security.enabled=false",
23 | "http.cors.enabled: true",
24 | "http.cors.allow-origin: \"*\"",
25 | s"cluster.name=$EsClusterName",
26 | "discovery.type=single-node",
27 | "ES_JAVA_OPTS=-Xms512m -Xmx512m"
28 | )
29 | .withReadyChecker(
30 | DockerReadyChecker
31 | .HttpResponseCode(DefaultElasticsearchHttpPort, "/")
32 | .within(100.millis)
33 | .looped(20, 1250.millis)
34 | )
35 | .toContainer
36 |
37 | override val managedContainers: ManagedContainers = elasticsearchContainer.toManagedContainer
38 | }
39 |
--------------------------------------------------------------------------------
/samples/src/main/scala/com/whisk/docker/testkit/DockerMongodbService.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import com.whisk.docker.testkit.scalatest.DockerTestKitForAll
4 | import org.scalatest.Suite
5 |
6 | trait DockerMongodbService extends DockerTestKitForAll {
7 | self: Suite =>
8 |
9 | val DefaultMongodbPort = 27017
10 |
11 | val mongodbContainer = ContainerSpec("mongo:3.4.8")
12 | .withExposedPorts(DefaultMongodbPort)
13 | .withReadyChecker(DockerReadyChecker.LogLineContains("waiting for connections on port"))
14 | .toContainer
15 |
16 | override val managedContainers: ManagedContainers = mongodbContainer.toManagedContainer
17 | }
18 |
--------------------------------------------------------------------------------
/samples/src/main/scala/com/whisk/docker/testkit/DockerMysqlService.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import com.whisk.docker.testkit.scalatest.DockerTestKitForAll
4 | import org.scalatest.Suite
5 |
6 | import scala.concurrent.duration._
7 |
8 | class MysqlContainer(image: String) extends BaseContainer {
9 |
10 | val AdvertisedPort = 3306
11 | val User = "root"
12 | val Password = "test"
13 | val Database = "test"
14 |
15 | override val spec: ContainerSpec = {
16 | ContainerSpec(image)
17 | .withExposedPorts(AdvertisedPort)
18 | .withReadyChecker(
19 | DockerReadyChecker
20 | .Jdbc(
21 | driverClass = "com.mysql.jdbc.Driver",
22 | user = User,
23 | password = Some(Password),
24 | database = Some(Database)
25 | )
26 | .looped(25, 1.second)
27 | )
28 | }
29 | }
30 |
31 | trait DockerMysqlService extends DockerTestKitForAll { self: Suite =>
32 |
33 | val mysqlContainer = new MysqlContainer("quay.io/whisk/fastboot-mysql:5.7.19")
34 |
35 | override val managedContainers: ManagedContainers = mysqlContainer.toManagedContainer
36 | }
37 |
--------------------------------------------------------------------------------
/samples/src/main/scala/com/whisk/docker/testkit/DockerPostgresService.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit
2 |
3 | import com.spotify.docker.client.messages.PortBinding
4 | import com.whisk.docker.testkit.scalatest.DockerTestKitForAll
5 | import org.scalatest.Suite
6 |
7 | import scala.concurrent.duration._
8 |
9 | trait DockerPostgresService extends DockerTestKitForAll { self: Suite =>
10 |
11 | def PostgresAdvertisedPort = 5432
12 | def PostgresExposedPort = 44444
13 | val PostgresUser = "nph"
14 | val PostgresPassword = "suitup"
15 |
16 | val postgresContainer = ContainerSpec("postgres:9.6.5")
17 | .withPortBindings((PostgresAdvertisedPort, PortBinding.of("0.0.0.0", PostgresExposedPort)))
18 | .withEnv(s"POSTGRES_USER=$PostgresUser", s"POSTGRES_PASSWORD=$PostgresPassword")
19 | .withReadyChecker(
20 | DockerReadyChecker
21 | .Jdbc(
22 | driverClass = "org.postgresql.Driver",
23 | user = PostgresUser,
24 | password = Some(PostgresPassword)
25 | )
26 | .looped(15, 1.second)
27 | )
28 | .toContainer
29 |
30 | override val managedContainers: ManagedContainers = postgresContainer.toManagedContainer
31 | }
32 |
--------------------------------------------------------------------------------
/scalatest/src/main/scala/com/whisk/docker/testkit/scalatest/DockerTestKitForAll.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit.scalatest
2 |
3 | import java.util.concurrent.ForkJoinPool
4 |
5 | import com.spotify.docker.client.{DefaultDockerClient, DockerClient}
6 | import com.whisk.docker.testkit._
7 | import org.scalatest.{Args, Status, Suite, SuiteMixin}
8 |
9 | import scala.concurrent.ExecutionContext
10 | import scala.language.implicitConversions
11 |
12 | trait DockerTestKitForAll extends SuiteMixin { self: Suite =>
13 |
14 | val dockerClient: DockerClient = DefaultDockerClient.fromEnv().build()
15 |
16 | val dockerExecutionContext: ExecutionContext = ExecutionContext.fromExecutor(new ForkJoinPool())
17 |
18 | val managedContainers: ManagedContainers
19 |
20 | val dockerTestTimeouts: DockerTestTimeouts = DockerTestTimeouts.Default
21 |
22 | implicit lazy val dockerExecutor: ContainerCommandExecutor =
23 | new ContainerCommandExecutor(dockerClient)
24 |
25 | lazy val containerManager = new DockerContainerManager(
26 | managedContainers,
27 | dockerExecutor,
28 | dockerTestTimeouts,
29 | dockerExecutionContext
30 | )
31 |
32 | abstract override def run(testName: Option[String], args: Args): Status = {
33 | containerManager.start()
34 | afterStart()
35 | try {
36 | super.run(testName, args)
37 | } finally {
38 | try {
39 | beforeStop()
40 | } finally {
41 | containerManager.stop()
42 | }
43 | }
44 | }
45 |
46 | def afterStart(): Unit = {}
47 |
48 | def beforeStop(): Unit = {}
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/tests/src/test/scala/com/whisk/docker/testkit/test/ClickhouseServiceTest.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit.test
2 |
3 | import com.whisk.docker.testkit.{ContainerState, DockerClickhouseService}
4 | import org.scalatest.funsuite.AnyFunSuite
5 |
6 | class ClickhouseServiceTest extends AnyFunSuite with DockerClickhouseService {
7 | test("test container started") {
8 | assert(clickhouseContainer.state().isInstanceOf[ContainerState.Ready], "clickhouse is ready")
9 | assert(clickhouseContainer.mappedPortOpt(ClickhouseAdvertisedPort) === Some(ClickhouseExposedPort),
10 | "clickhouse port exposed")
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/src/test/scala/com/whisk/docker/testkit/test/ElasticsearchServiceTest.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit.test
2 |
3 | import com.whisk.docker.testkit.{ContainerState, DockerElasticsearchService}
4 | import org.scalatest.funsuite.AnyFunSuite
5 |
6 | class ElasticsearchServiceTest extends AnyFunSuite with DockerElasticsearchService {
7 |
8 | test("test container started") {
9 | assert(elasticsearchContainer.state().isInstanceOf[ContainerState.Ready],
10 | "elasticsearch container is ready")
11 | assert(elasticsearchContainer.mappedPortOpt(9200).nonEmpty, "elasticsearch port is exposed")
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/src/test/scala/com/whisk/docker/testkit/test/MongodbServiceTest.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit.test
2 |
3 | import com.whisk.docker.testkit.{ContainerState, DockerMongodbService}
4 | import org.scalatest.funsuite.AnyFunSuite
5 |
6 | class MongodbServiceTest extends AnyFunSuite with DockerMongodbService {
7 |
8 | test("test container started") {
9 | assert(mongodbContainer.state().isInstanceOf[ContainerState.Ready], "mongodb is ready")
10 | assert(mongodbContainer.mappedPortOpt(27017).nonEmpty, "port 2017 is exposed")
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/src/test/scala/com/whisk/docker/testkit/test/MultiContainerTest.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit.test
2 |
3 | import com.whisk.docker.testkit._
4 | import org.scalatest.funsuite.AnyFunSuite
5 |
6 | class MultiContainerTest
7 | extends AnyFunSuite
8 | with DockerElasticsearchService
9 | with DockerMongodbService {
10 |
11 | override val managedContainers: ContainerGroup =
12 | ContainerGroup.of(elasticsearchContainer, mongodbContainer)
13 |
14 | test("both containers should be ready") {
15 | assert(
16 | elasticsearchContainer.state().isInstanceOf[ContainerState.Ready],
17 | "elasticsearch container is ready"
18 | )
19 | assert(elasticsearchContainer.mappedPortOpt(9200).nonEmpty, "elasticsearch port is exposed")
20 |
21 | assert(mongodbContainer.state().isInstanceOf[ContainerState.Ready], "mongodb is ready")
22 | assert(mongodbContainer.mappedPortOpt(27017).nonEmpty, "port 2017 is exposed")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/src/test/scala/com/whisk/docker/testkit/test/MysqlServiceTest.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit.test
2 |
3 | import com.whisk.docker.testkit.{ContainerState, DockerMysqlService}
4 | import org.scalatest.funsuite.AnyFunSuite
5 |
6 | class MysqlServiceTest extends AnyFunSuite with DockerMysqlService {
7 |
8 | test("test container started") {
9 | assert(mysqlContainer.state().isInstanceOf[ContainerState.Ready], "mysql is ready")
10 | assert(mysqlContainer.mappedPortOpt(mysqlContainer.AdvertisedPort).nonEmpty,
11 | "mysql port exposed")
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/src/test/scala/com/whisk/docker/testkit/test/PostgresServiceTest.scala:
--------------------------------------------------------------------------------
1 | package com.whisk.docker.testkit.test
2 |
3 | import com.whisk.docker.testkit.{ContainerState, DockerPostgresService}
4 | import org.scalatest.funsuite.AnyFunSuite
5 |
6 | class PostgresServiceTest extends AnyFunSuite with DockerPostgresService {
7 |
8 | test("test container started") {
9 | assert(postgresContainer.state().isInstanceOf[ContainerState.Ready], "postgres is ready")
10 | assert(postgresContainer.mappedPortOpt(PostgresAdvertisedPort) === Some(PostgresExposedPort),
11 | "postgres port exposed")
12 | }
13 | }
14 |
--------------------------------------------------------------------------------