Simple Kotlin docker builder for tests.
Dokker is a lightweight kotlin wrapper around a docker container and allows you to build, start, stop, execute commands on and remove your docker containers.
If you want a container for tests but need a more Kotlin way to do things, Dokker provides an alternative to libraries like TestContainers.
You must have docker command line installed on your system:
https://docs.docker.com/get-docker/
Dokker is distributed through Maven Central.
<dependency>
<groupId>io.github.corbym</groupId>
<artifactId>dokker</artifactId>
<version>0.3.0</version>
<type>module</type>
<scope>test</scope>
</dependency>
dependencies {
testImplementation("io.github.corbym:dokker:0.3.0")
}
The dokker builder builds a docker command line and executes it via java.lang.ProcessBuilder
process. This is encapsulated in a DokkerContainer class.
e.g:
val myContainer = dokker {
name("my-container")
detach()
expose("9092", "29092", "9101")
image { "my/container" }
version { "1.1" }
publish("12300" to "12300")
network("local-network")
env(
"MY_CONFIG_PROP" to "hello"
)
}
Calling start()
on the resulting object attempts to docker run
the container using ProcessBuilder
to run the docker
command line client.
Calling stop()
on the resulting object attempts to docker stop
the container using ProcessBuilder
to run the docker
command line client.
Calling remove()
on the resulting object attempts to docker rm
the container using ProcessBuilder
to run the docker
command line client.
DokkerContainer
implements DokkerLifeCycle
so you can manage containers in sets.
You can use the DockerContainer::String.runCommand
independently too:
e.g:
"docker ps".runCommand()`
You can get the parameters that were used to run the docker command, e.g:
val containerPublishedPorts = myContainer.publishedPorts
val exposedPorts = myContainer.expose
val image = myContainer.image
.. etc ..
Executes the health check given in the configuration with an initial delay, timeout and polling interval. E.g.
val dockerContainer = dokker {
healthCheck {
timeout(Duration.ofSeconds(30))
pollingInterval(TEN_SECONDS)
initialDelay(TEN_SECONDS)
checking { "curl -i --fail http://localhost:8080/health?ready=1" to "HTTP/1.1 200 OK" }
}
onStartup { container, _ ->
.. commands to run on docker etc..
container.executeHealthCheck()
}
}
Note: this does not use docker's built in healthCheck functionality.
The DokkerContainer
object reflects most of the commands you can execute on the command line, including:
- start
- stop
- exec
- execWithSpacedParameter - this allows you to pass ProcessBuilder a command parameter which may contain spaces, and is a workaround really.
Running a network in docker is slightly different to starting a container. To help with this, a there is a special object for a network called DokkerNetwork
:
val myNetwork = DokkerNetwork("some-network").also { it.start() }
DokkerNetwork
also implements the DokkerLifeCycle
so it can be managed with containers (see below).
Every DokkerContainer
and DokkerNetwork
implements the DokkerLifecycle
interface:
interface DokkerLifecycle {
val name: String
fun start()
fun stop()
fun remove()
fun hasStarted(): Boolean
}
You can extend a test with the junit5 @ExtendWith
annotation and create your own DokkerProvider
.
See ExampleDokkerProvider.kt for more details.
The docker container provider lifecycle is as follows:
BeforeAll:
- Start up validation
- Run the container
- Start up execution
JVM Shutdown:
- Tear down container
If the container exists but is not running, the framework will not start it and error. In this case, you should remove the container manually.
If the container with the same name is already running, the framework will not try to start the container, and will not try to stop it when the JVM exits.
The principle being, "if its not mine, don't touch it."
The framework will attempt to run the container with docker run
.
After the container has been run, any executions you specify are performed. You can specify this with
onStartup { container, _ ->
it.waitForHealthCheck()
}
The containers are only torn down when the JVM executes a shutdown hook.
If the container fails to start or was already running with the same container name, the framework will NOT run the shutdown hook, and leave the container up.
The DokkerExtension
class was created so that the Junit5 @RegisterExtension
function can be taken advantage of, and is available from 0.2.0 onwards:
import io.github.corbym.dokker.junit5.DokkerExtension
import io.github.corbym.dokker.junit5.dokkerExtension
...
import io.github.corbym.dokker.junit5.findFreePort
import org.junit.jupiter.api.extension.RegisterExtension
class ExampleJUnit5RegisteredDokkerTest {
companion object {
val couchbasePort = findFreePort()
@JvmStatic
@RegisterExtension
val server: DokkerExtension = dokkerExtension {
container {
dokker {
name("couchbase")
detach()
debug()
expose(couchbasePort)
publish(couchbasePort to couchbasePort)
image { "arungupta/couchbase" }
version { "latest" }
}
doNotStop()
}
}
}
... rest of test ...
}
This extension starts the container on the BeforeAll
lifecycle of the test, and will stop the container in the AfterAll
phase. You can prevent shutdown and removal respectively by specifying doNotStop()
and doNotRemove
in the dokkerExtension
builder.
Note that a random available port was used by calling the utility function io.github.corbym.dokker.junit5.findFreePort
in the above example. See ExampleJUnit5RegisteredDokkerTest
for more information.
Please open an issue or fork and a PR for any changes you wish to be considered.