wtorek, 17 sierpnia 2021

Expose MongoDB as REST service using Camel Quarkus

In this blog post I'll show you how to create a REST service which exposes CRUD functionality from a MongoDB database using Camel Quarkus framework. 

Why Camel Quarkus?

As a Java developer I prefer to use Java frameworks to develop my code. With Apache Camel I can develop faster integration logic without need to take care of low level boilerplate coding and I can focus mostly on business logic. With Camel XML dsl I can follow low code approach using XML instead of Java, which is even faster and less error prone!

With Quarkus I can create super thin application binaries and container images which allows me to run applications and containers with minimal resources footprint compared to regular JVM and start them super fast in milliseconds.

Camel Quarkus combines the best of both worlds: Java integration developer experience of Camel with resource consumption optimization of Quarkus.

Let's get started!

The first step is to create Camel Quarkus maven project on your local machine

mvn \
io.quarkus:quarkus-maven-plugin:1.13.7.Final:create \
-DprojectGroupId=org.redhat \
-DprojectArtifactId=camel-quarkus-mongodb-client \
-DplatformGroupId=io.quarkus \
-DplatformVersion=1.13.7.Final \
-Dextensions=camel-quarkus-xml-io-dsl,camel-quarkus-direct,camel-quarkus-mongodb,camel-quarkus-log,camel-quarkus-jackson,camel-quarkus-http,camel-quarkus-rest,camel-quarkus-bean

Next we must edit our application configuration and add required parameters. Please check Quarkus documentation for more details about their meaning.

$ cd camel-quarkus-mongodb-client

$ vi src/main/resources/application.properties

quarkus.package.type=uber-jar

camel.context.name = hotelsdb-client
camel.rest.component = platform-http

camel.main.routes-include-pattern = classpath:/camel-routes.xml,classpath:/camel-rests.xml


quarkus.mongodb.connection-string = mongodb://localhost:27017
quarkus.mongodb.database = hotelsdb

batchLimit = 100

quarkus.http.port = 8080
quarkus.http.host = 0.0.0.0

As you might have noticed in above configuration our business logic will be placed in two Camel files: camel-routes.xml and camel-rests.xml

First we'll edit camel-rests.xml where we'll define our REST services endpoints

$ vi src/main/resources/camel-rests.xml

<?xml version="1.0" encoding="UTF-8"?>
<rests xmlns="http://camel.apache.org/schema/spring">
    <rest id="cache" path="/camel">
         <post id="putToCache" consumes="application/json" produces="application/json" uri="/v1/cache/{cid}">
            <route>
                <doTry>
                    <to uri="direct:putToCache"/>
                    <doCatch>
                        <exception>java.lang.Exception</exception>
                        <to uri="direct:logError"/>
                    </doCatch>
                </doTry>
            </route>
         </post>
         <get id="getFromCache" produces="application/json" uri="/v1/cache/{cid}/{limit}">
            <route>
                <doTry>
                    <to uri="direct:getFromCache"/>
                    <doCatch>
                        <exception>java.lang.Exception</exception>
                        <to uri="direct:logError"/>
                    </doCatch>
                </doTry>
            </route>
        </get>
    </rest>
</rests>

This REST services endpoints will send requests to the routes which must be defined in the second camel file camel-routes.xml

$ vi src/main/resources/camel-routes.xml

<?xml version="1.0" encoding="UTF-8"?>
<routes id="DBClient" xmlns="http://camel.apache.org/schema/spring">
    <route id="Put to collection">
        <from uri="direct:putToCache"/>
        <log loggingLevel="INFO" message="Inserting to collection ${header.cid} 1 document..."/>
        <convertBodyTo type="java.lang.String"/>
        <to uri="direct:insertRecord"/>
        <log loggingLevel="INFO" message="Done"/>
        <setBody>
            <simple>{"count": 1}</simple>
        </setBody>
        <removeHeaders pattern="*"/>
    </route>
    <route id="Insert record to collection">
        <from uri="direct:insertRecord"/>
        <recipientList>
            <simple>mongodb:camelMongoClient?database={{quarkus.mongodb.database}}&amp;collection=${header.cid}&amp;operation=save</simple>
        </recipientList>
        <log loggingLevel="INFO" message="Inserted to collection ${header.cid} document with id ${header.CamelMongoOid}."/>
    </route>
    <route id="Get from collection">
        <from uri="direct:getFromCache"/>
        <validate>
            <simple>${header.limit} range '1..{{batchLimit}}'</simple>
        </validate>
        <log loggingLevel="INFO" message="Get all from cache ${header.cid} with limit ${header.limit}"/>
        <setHeader name="CamelMongoDbSortBy">
            <!--  descending by _id -->
            <constant>{"_id" : -1}</constant>
        </setHeader>
        <setHeader name="CamelMongoDbLimit">
            <simple>${header.limit}</simple>
        </setHeader>
        <setHeader name="CamelMongoDbBatchSize">
            <constant>{{batchLimit}}</constant>
        </setHeader>
        <recipientList>
            <simple>mongodb:camelMongoClient?database={{quarkus.mongodb.database}}&amp;collection=${header.cid}&amp;operation=findAll</simple>
        </recipientList>
        <to uri="direct:processOutput"/>
    </route>
    <route id="Process output">
        <from uri="direct:processOutput"/>
        <marshal>
            <json id="json" library="Jackson"/>
        </marshal>
        <removeHeaders pattern="*"/>
    </route>
    <route id="Log error">
        <from uri="direct:logError"/>
        <log logName="net.gmsworld.server.camel" loggingLevel="ERROR" message="Operation failed with exception: ${exception.stacktrace}"/>
        <setBody>
            <simple>{"error" : "Operation failed"}</simple>
        </setBody>
        <removeHeaders pattern="*"/>
        <setHeader name="CamelHttpResponseCode">
            <constant>500</constant>
        </setHeader>
    </route>
</routes>

Finally we are going to modify the automatically generated JUnit test case source file. Of course you can create your own JUnit test cases just like with a regular Java applications.

$ vi src/test/java/org/redhat/GreetingResourceTest.java

package org.redhat;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/camel/v1/cache/test/10")
          .then()
             .statusCode(200)
             .body(is("[]"));
    }
}

Now we are ready for testing!

For testing purposes let's run on the local machine a MongoDB database container. Moving forward I'll use podman for all container related actions, but if you prefer you can use any other OCI compliant tool.

$ podman run -d --name mongodb -p 27017:27017 quay.io/bitnami/mongodb:4.0

Before proceeding make sure MongoDB container is up and running

$ podman ps
CONTAINER ID  IMAGE                        COMMAND               CREATED      STATUS            PORTS                     NAMES
780b48bfd200  quay.io/bitnami/mongodb:4.0  /opt/bitnami/scri...  11 days ago  Up 2 seconds ago  0.0.0.0:27017->27017/tcp  mongodb

Now we can quickly execute JUnit test

$ mvn clean package

If it has succeeded we can start our REST service in developer mode on the local machine using maven Quarkus plugin

$ mvn clean package quarkus:dev -DskipTests=true

Once our service is up and running we can send some requests to test it

$ curl -v -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}'  http://localhost:8080/camel/v1/cache/test

$ curl -v http://localhost:8080/camel/v1/cache/test/10

Next we will compile or Camel Quarkus application to native executable using GraalVM

First we need to prepare additional configuration for native compiler to make sure required XML dsl files and Java classes are included in the output native executable.

vi src/main/resources/application.properties

#add following parameter
quarkus.native.additional-build-args =\
   -H:ResourceConfigurationFiles=resources-config.json,\
   -H:ReflectionConfigurationFiles=reflection-config.json

vi src/main/resources/resources-config.json

{
  "resources": [
    {
      "pattern": ".*\\.xml$"
    }
  ]
}

vi src/main/resources/reflection-config.json

[
  {
    "name" : "org.bson.types.ObjectId",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "allDeclaredFields" : true,
    "allPublicFields" : true
  },
  {
    "name" : "java.lang.Exception",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "allDeclaredFields" : true,
    "allPublicFields" : true
  } 
]

In order to create native executable you can either download to your local machine GraalVM and execute following maven command

$ mvn clean package -Pnative -DskipTests=true -DGRAALVM_HOME=/opt/graalvm/graalvm-ce-java11-21.0.0.2/

or you can run containerized native executable builder

$ mvn package -Pnative \
-Dquarkus.native.container-build=true \
-Dquarkus.native.container-runtime=podman \
-Dquarkus.native.builder-image=registry.access.redhat.com/quarkus/mandrel-20-rhel8

Both commands should produce native executable which can be optionally analysed using following command

$ readelf -h ./target/camel-quarkus-mongodb-client-1.0.0-SNAPSHOT-runner
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64

 

Now you can run this binary and test it the same way as before. Please note this native executable will start REST service in just a couple of milliseconds

$ ./target/camel-quarkus-mongodb-client-1.0.0-SNAPSHOT-runner

2021-08-16 13:12:01,922 INFO  [io.quarkus] (main) camel-quarkus-mongodb-client 1.0.0-SNAPSHOT native (powered by Quarkus 1.13.7.Final) started in 0.059s. Listening on: http://0.0.0.0:8080
2021-08-16 13:12:01,922 INFO  [io.quarkus] (main) Profile prod activated.
2021-08-16 13:12:01,922 INFO  [io.quarkus] (main) Installed features: [camel-attachments, camel-bean, camel-core, camel-direct, camel-http, camel-jackson, camel-log, camel-mongodb, camel-platform-http, camel-rest, camel-support-common, camel-support-commons-logging, camel-support-httpclient, camel-support-mongodb, camel-xml-io-dsl, cdi, mongodb-client, mutiny, resteasy, smallrye-context-propagation, vertx, vertx-web]

If you are curious you can also check how much memory is consumed by the native executable

$ ps -o pid,rss,command -p $(pgrep -f runner)
  PID   RSS COMMAND
 2575 61956 ./target/camel-quarkus-mongodb-client-1.0.0-SNAPSHOT-runner

Less than 62MB, pretty compact compared to a regular JVM application!

Let's repeat the test executed before to make sure our Camel Quarkus application is up and running

$ curl -v -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}'  http://localhost:8080/camel/v1/cache/test

$ curl -v http://localhost:8080/camel/v1/cache/test/10

Finally, let's create a lightweight container image. For that I'll use ubi-micro base image extended by required for Quarkus native libraries created using this script. To execute this script on your local machine you'll need yet another tool for building images called buildah.

$ vi Containerfile.distroless

FROM quay.io/jstakun/ubi-micro-quarkus:latest
MAINTAINER Jaroslaw Stakun jstakun@redhat.com
LABEL quarkus-version=1.13.7.Final
COPY ./target/*-runner /application
RUN chgrp 0 /application && chmod 110 /application
USER 1001
CMD /application
EXPOSE 8080

$ podman build -f ./Containerfile.distroless -t quay.io/jstakun/camel-quarkus-mongodb-client:latest

$ podman push quay.io/jstakun/camel-quarkus-mongodb-client:latest

With the image deployed to the public container images registry you can run the container anywhere you want. Here is how you can do it in Red Hat OpenShift Container Platform.

$ oc new-project camel-quarkus-mongodb

$ oc new-app -e MONGODB_DATABASE=testdb -e MONGODB_USER=test -e MONGODB_PASSWORD=test -e MONGODB_ADMIN_PASSWORD=admin mongodb:3.6
  

#please note using environment variables you can replace values of application properties defined in source code
$ oc new-app --name=frontend -e QUARKUS_MONGODB_CONNECTION_STRING=mongodb://mongodb:27017 -e QUARKUS_MONGODB_DATABASE=testdb  -e QUARKUS_MONGODB_CREDENTIALS_USERNAME=test -e QUARKUS_MONGODB_CREDENTIALS_PASSWORD=test quay.io/jstakun/camel-quarkus-mongodb-client:latest

$ oc expose service/frontend

Finally let's test our REST service pod running in OpenShift

$ ROUTE=http://$(oc get route | grep frontend | awk '{print $2}') && echo $ROUTE

$ curl -v -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' $ROUTE/camel/v1/cache/test

$ curl -v $ROUTE/camel/v1/cache/test/10
 

You can find all source codes of this tutorial in my GitHub repository. All container images referenced in this post are available at the quay.io public container images registry.

Thanks for reading!