Getting Started with Docker

Mateusz Hinc
2.0.0

Overview

In this tutorial, we will set up a simple project based on the Knot.x Starter Kit template, just as we did in the previous tutorial, but this time we will use Docker.

This is the second part of Getting started tutorials, and completion of Getting Started with Knot.x Stack tutorial is required to fully understand this tutorial.

What you’re going to learn:

  • How to build a custom Docker image containing project-specific configs and extensions using Gradle
  • How to validate the Docker image with functional / system tests

Prerequisites

You will need the following things to use Knot.x with Docker:

  • JDK 8
  • Docker

Docker

Open directory of the previous tutorial. If you don't have one, you can find it here.

Knot.x Starter Kit project builds either stack (zip distribution) or Docker image. In this tutorial, we'll use the latter.

The part responsible for building the Docker image is the docker.gradle.kts file, which uses the gradle-docker-plugin.

Let's rename the Docker image. Edit gradle.properties and change property docker.image.name:

docker.image.name=knotx-example/knotx-docker-tutorial

Build & Run

First, let's build your docker image:

$ gradlew clean build-docker

If you take a closer look at build log (you can use -i flag for a more verbose process) you can see, that not only the project is being built, but also it:

  • builds the Docker image
  • starts a container for testing
  • runs healthcheck test on the container and then executes functional tests (more on that in the next section)

But what building the Docker image means in this case?

Starter Kit Docker Image

The diagram above describes how it works. Starter Kit Docker Image is based on Knot.x image hosted at Docker Hub, which is based on a base OpenJDK image. While building the image, we copy all custom modules (JARs) and configuration to the resulting image.

Now let's run the dockerized Knot.x instance:

$ docker run -p8092:8092 knotx-example/knotx-docker-tutorial

Final result

$ curl -X GET http://localhost:8092/api/hello
{"message":"Hello World From Knot.x!"}

Tests

Now let's take a closer look at what tests we have here.

The first test is the healthcheck. Technically speaking, this is not a test, but rather an endpoint in which, we call application's endpoints within the application. If the request succeeds, we can be sure that the Docker container is up and running.

Let's take a quick peek at docker/Dockerfile:

... 

HEALTHCHECK --interval=5s --timeout=2s --retries=12 \
  CMD curl --silent --fail localhost:8092/healthcheck || exit 1

...

Docker is trying to reach localhost:8092/healthcheck with defined interval, timeouts and retries. If /healthcheck returns 200 code, we can be sure that the instance is up. For more details see this documentation.

A quick recap on how to find the class implementing the handler: in openapi.yaml endpoint /healthcheck is handled by operation healthcheck-operation, which has one handler registered with name healthcheck. This leads to the class HealthcheckHandlerFactory.java.

We'll modify it so that it will use the /api/hello endpoint.

Simply change the create method:

public Handler<RoutingContext> create(Vertx vertx, JsonObject config) {
    HealthChecks checks = HealthChecks.create(vertx);
    checks.register("API check", 200, future -> {
      WebClient webClient = WebClient.create(vertx);
      webClient.get(8092, "localhost", "/api/hello")
          .rxSend()
          .subscribe(onSuccess -> {
            JsonObject jsonResponse = onSuccess.bodyAsJsonObject();
            future.complete("Hello World From Knot.x!".equals(jsonResponse.getString("message")) ? Status.OK() : Status.KO());
          }, onError -> future
              .complete(Status.KO(new JsonObject().put("error", onError.getMessage()))));
});

Starter Kit has some example functional tests as well.

As this line suggests:

build.gradle.kts

sourceSets.named("test") {
    java.srcDir("functional/src/test/java")
}

They can be found under functional/src/test/java.

For now, we have one test that comes out of the box with the Starter Kit: ExampleApiITCase. Let's change it as well.

Again, we'll modify the test to use the /api/hello endpoint:

  @Test
  @DisplayName("Expect 200 status code from hello api.")
  void callHandlersApiEndpointAndExpectOK() {
    // @formatter:off
    given().
        port(8092).
      when().
        get("/api/hello").
      then()
        .assertThat().
        statusCode(200);
    // @formatter:on
  }

If you look closely at this file, you can see that task runFunctionalTests relies on Docker container to be up (which is ensured by healthcheck). Now that we have the application running, we can run integration tests by invoking various endpoints and asserting their responses. Here we use REST-assured for this purpose.


You can find full project implementation here.