Skip to content

5. Containers

Michal Vavřík edited this page Sep 6, 2022 · 7 revisions

The framework also supports to deployment of third party components using container images.

First, we need to add an additional dependency in the pom.xml file:

<dependency>
	<groupId>io.quarkus.qe</groupId>
	<artifactId>quarkus-test-containers</artifactId>
    <scope>test</scope>
</dependency>

Now, we can deploy third parties for our scenario:

@QuarkusScenario
public class GreetingResourceIT {

    private static final String CUSTOM_PROPERTY = "my.property";

    @Container(image = "quay.io/bitnami/consul:1.9.3", expectedLog = "Synced node info", port = 8500)
    static final DefaultService consul = new DefaultService();

    @QuarkusApplication
    static final RestService app = new RestService();

    // ...
}

External Resources

We can use properties that require external resources using the resource:: tag. For example: .withProperty("to.property", "resource::/file.yaml");. This works in bare metal or OpenShift/Kubernetes.

The same works for secret resources: using the secret:: tag. For example: .withProperty("to.property", "secret::/file.yaml");. For baremetal, there is no difference, but when deploying on OCP and Kubernetes, one secret will be pushed instead. This only works for file system resources (secrets from classpath are not supported).

Delete Container Images on Stop

If you want to delete the images after use, you need to provide the property ts.<YOUR SERVICE NAME>.container.delete.image.on.stop=true or ts.global.container.delete.image.on.stop=true to apply this property to all the containers.

Remember that <YOUR SERVICE NAME> matches with the field name of the service, for example, in the previous example would be consul.

Custom prefix name

Add your container prefix name by adding this property "-Dts.global.docker-container-prefix" to your maven statements.

For example

mvn clean verify -Dts.global.docker-container-prefix=my_prefix

As a result of this configuration, all your docker containers will have a fixed prefix name "my_prefix".

CONTAINER ID   IMAGE                               PORTS                                                   NAMES
e7a2ef3a3e84   quay.io/keycloak/keycloak:14.0.0    8443/tcp, 0.0.0.0:50138->8080/tcp, :::50138->8080/tcp   my_prefix-2074505752

The main motivation that is behind this feature is the ability to create the concept of "namespaces" in docker. In this way, you will be able to share a docker server between several "CI" workers, and then after the job execution ends, clean the environment by running a script.

For example:

Stop&remove all 4a73c71664 containers:

docker stop $(docker ps -a | grep 4a73c71664 | awk '{print $1}')
docker rm $(docker ps -a | grep 4a73c71664 | awk '{print $1}')

Privileged Mode

Some containers require Privileged mode to run properly. This mode can be enabled on a per-container basis via property ts.<YOUR SERVICE NAME>.container.privileged-mode=true or for all containers via property ts.global.container.privileged-mode=true. This property only affects containers which are both:

  1. Deployed on bare metal, not in Kubernetes/OpenShift.
  2. Use @Container annotation, not a specialised one(@KafkaContainer, @AmqContainer, etc).

Reusable Docker containers

For heavy containers such as DB2 could be possible to reuse the same container instance in several @QuarkusScenario. This feature could be used via property ts.<YOUR SERVICE NAME>.container.reusable=true. Please take a look at this example if you want to know more.

Kafka Containers

Due to the complexity of Kafka deployments, there is a special implementation of containers for Kafka that we can use by adding the dependency:

<dependency>
    <groupId>io.quarkus.qe</groupId>
    <artifactId>quarkus-test-service-kafka</artifactId>
    <scope>test</scope>
</dependency>

And now, we can use the Kafka container in our test:

@QuarkusScenario
public class StrimziKafkaWithoutRegistryMessagingIT {

    @KafkaContainer
    static final KafkaService kafka = new KafkaService();

    @QuarkusApplication
    static final RestService app = new RestService()
            .withProperty("kafka.bootstrap.servers", kafka::getBootstrapUrl);

    // ...
}

Strimzi

By default, the KafkaContainer will use the Strimzi implementation.

Moreover, we can also configure our Kafka instance using a registry (Apicurio in Kafka Strimzi):

@QuarkusScenario
public class StrimziKafkaWithRegistryMessagingIT {

    @KafkaContainer(withRegistry = true)
    static final KafkaService kafka = new KafkaService();

    @QuarkusApplication
    static final RestService app = new RestService()
            .withProperties("strimzi-application.properties")
            .withProperty("kafka.bootstrap.servers", kafka::getBootstrapUrl)
            .withProperty("strimzi.registry.url", kafka::getRegistryUrl);

    // ...
}

We can override the registry configuration using the following properties from the @KafkaContainer annotation:

  • registryImage, this image follow the standard docker:version format as quay.io/apicurio/apicurio-registry-mem:2.0.0.Final
  • registryPath

This could be useful for some cases where the registry path has changed between Quarkus/Apicurio versions. For example:

  • For Quarkus 1.13.7.Final:
 @KafkaContainer(vendor = KafkaVendor.STRIMZI, withRegistry = true)
 static final KafkaService kafka = new KafkaService();
  • For Quarkus 2.x.Final:
@KafkaContainer(vendor = KafkaVendor.STRIMZI, withRegistry = true, registryPath = "/apis/registry/v2")
static KafkaService kafka = new KafkaService();

Confluent

We can also use the Confluent implementation of kafka by doing:

@KafkaContainer(vendor = KafkaVendor.CONFLUENT)

Note that this implementation supports also registry, but not Kubernetes and OpenShift scenarios.

Custom Kafka server configuration

We can customise the Kafka deployment using a custom server.properties and external files:

@KafkaContainer(serverProperties = "strimzi-custom-server-ssl.properties", kafkaConfigResources = { "strimzi-custom-server-ssl-keystore.p12"})

| Note that this only works for Strimzi kafka and on baremetal.

SSL protocol

// Truststore must be placed on filesystem: https://github.com/quarkusio/quarkus/issues/8573
// So, we need to have:
// - a file "strimzi-server-ssl-truststore.p12" to match the defined in the default server.properties
// - using "top-secret" for the password to match the defined in the default server.properties
// - using "PKCS12" for the type to match the defined in the default server.properties
// If you want another setup, see the scenario `StrimziKafkaWithCustomSslMessagingIT`.
private static final String TRUSTSTORE_FILE = "strimzi-server-ssl-truststore.p12";

@KafkaContainer(vendor = KafkaVendor.STRIMZI, protocol = KafkaProtocol.SSL, kafkaConfigResources = TRUSTSTORE_FILE)
static final KafkaService kafka = new KafkaService();

@QuarkusApplication
static final RestService app = new RestService()
        .withProperty("kafka.bootstrap.servers", kafka::getBootstrapUrl)
        .withProperty("kafka.security.protocol", "SSL")
        .withProperty("kafka.ssl.truststore.location", TRUSTSTORE_FILE)
        .withProperty("kafka.ssl.truststore.password", "top-secret")
        .withProperty("kafka.ssl.truststore.type", "PKCS12");

@Test
public void checkUserResourceByNormalUser() {
    Awaitility.await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
        app.given().get("/prices/poll")
                .then()
                .statusCode(HttpStatus.SC_OK);
    });
}

| Note that this only works for Strimzi kafka and on baremetal.

SASL protocol

private final static String SASL_USERNAME_VALUE = "client";
private final static String SASL_PASSWORD_VALUE = "client-secret";

@KafkaContainer(vendor = KafkaVendor.STRIMZI, protocol = KafkaProtocol.SASL)
static final KafkaService kafka = new KafkaService();

@QuarkusApplication
static final RestService app = new RestService()
        .withProperty("kafka.bootstrap.servers", kafka::getBootstrapUrl)
        .withProperty("kafka.security.protocol", "SASL_PLAINTEXT")
        .withProperty("kafka.sasl.mechanism", "PLAIN")
        .withProperty("kafka.sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required "
                + "username=\"" + SASL_USERNAME_VALUE + "\" "
                + "password=\"" + SASL_PASSWORD_VALUE + "\";");

@Test
public void checkUserResourceByNormalUser() {
    Awaitility.await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
        app.given().get("/prices/poll")
                .then()
                .statusCode(HttpStatus.SC_OK);
    });
}

| Note that this only works for Strimzi kafka and on baremetal.

AMQ Containers

Similar to Kafka, we have a default implementation of an AMQ container (Artemis vendor).

Required Maven dependency:

<dependency>
    <groupId>io.quarkus.qe</groupId>
    <artifactId>quarkus-test-service-amq</artifactId>
    <scope>test</scope>
</dependency>

Example:

@QuarkusScenario
public class AmqIT {

    @AmqContainer
    static final AmqService amq = new AmqService();

    @QuarkusApplication
    static final RestService app = new RestService()
            .withProperty("quarkus.artemis.username", amq.getAmqUser())
            .withProperty("quarkus.artemis.password", amq.getAmqPassword())
            .withProperty("quarkus.artemis.url", amq::getUrl);

We can specify a different image by setting @AmqContainer(image = XXX). This container is compatible with OpenShift, but not with Kubernetes deployments.

Jaeger Containers

Required Maven dependency:

<dependency>
    <groupId>io.quarkus.qe</groupId>
    <artifactId>quarkus-test-service-jaeger</artifactId>
    <scope>test</scope>
</dependency>

Example with quarkus-smallrye-opentracing extension and a Jaeger thrift collector:

@JaegerContainer
static final JaegerService jaeger = new JaegerService();

@QuarkusApplication
static final RestService app = new RestService().withProperty("quarkus.jaeger.endpoint", jaeger::getRestUrl);

This container is compatible with OpenShift, but not with Kubernetes deployments.

Example with quarkus-opentelemetry-exporter-otlp extension over gRPC:

@JaegerContainer(useOtlpCollector = true)
static final JaegerService jaeger = new JaegerService();

@QuarkusApplication
static final RestService app = new RestService().withProperty("quarkus.opentelemetry.tracer.exporter.otlp.endpoint", jaeger::getCollectorUrl);