/dokker

Simple Kotlin docker builder for tests

Primary LanguageKotlinMIT LicenseMIT

dokker

Simple Kotlin docker builder for tests.

What is Dokker?

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.

Who is it for?

If you want a container for tests but need a more Kotlin way to do things, Dokker provides an alternative to libraries like TestContainers.

Requirements

What software is required to use Dokker?

You must have docker command line installed on your system:

https://docs.docker.com/get-docker/

How do I install Dokker?

Dokker is distributed through Maven Central.

Maven

<dependency>
  <groupId>io.github.corbym</groupId>
  <artifactId>dokker</artifactId>
  <version>0.2.0</version>
  <type>module</type>
  <scope>test</scope>
</dependency>

Gradle

dependencies {
  testImplementation("io.github.corbym:dokker:0.2.0")
}

Getting Started

How do I create a Dokker container?

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()`

Access the command that was run

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 ..

Execute Health Check

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.

Other configuration

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.

DokkerNetwork

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).

DokkerLifecycle

Every DokkerContainer and DokkerNetwork implements the DokkerLifecycle interface:

interface DokkerLifecycle {
    val name: String
    fun start()
    fun stop()
    fun remove()
    fun hasStarted(): Boolean
}

Junit5 DokkerProvider Extension

You can extend a test with the junit5 @ExtendWith annotation and create your own DokkerProvider.

See ExampleDokkerProvider.kt for more details.

Lifecycle

The docker container provider lifecycle is as follows:

BeforeAll:

  1. Start up validation
  2. Run the container
  3. Start up execution

JVM Shutdown:

  1. Tear down container

Start up validation

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."

Run the container

The framework will attempt to run the container with docker run.

Start up execution

After the container has been run, any executions you specify are performed. You can specify this with

onStartup { container, _ ->
    it.waitForHealthCheck()
}

Tear down

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.

Junit5 DokkerExtension Extension

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.

Contributing

How can I contribute to Dokker?

Please open an issue or fork and a PR for any changes you wish to be considered.