Should I use Quarkus

Welcome

As mentioned in a previous post, I'm working more with the RedHat stack. Most of these tools are very interesting. Like Quarkus - another Java framework. Not boring Java framework, Quarkus is fast, working on GralVM instead of regular JVM. But the biggest advantage is native builds. We can create an app, pack it into binary, and run it inside a container. That container could be used with serverless without cold-start issues. How cool is that? But...Today I will be focused on regular OpenShift deployment and some coding.

Tools used in this episode

  • Quarkus
  • crc(OpenShift for workstations)
  • httpie
  • oc

Installing tools

I assume that you have some JDK or maybe even GralVM, I don't know. But it will be nice to show something, even about that.

GralVM

  1. Download tar with the latest release from https://www.graalvm.org/downloads/

  2. Unpack it

    1$ sudo mv graalvm-ce-java11-linux-amd64-20.2.0.tar.gz /opt
    2$ cd /opt
    3# I like have toools in /opt
    4$ sudo tar -xvzf graalvm-ce-java11-linux-amd64-20.2.0.tar.gz
    
  3. Edit your PATH

    1export PATH=$JAVA_HOME/bin:$PATH
    2export JAVA_HOME="/opt/graalvm-ce-java11-20.2.0"
    
  4. Test it

    1$ java -version  
    2openjdk version "11.0.8" 2020-07-14
    3OpenJDK Runtime Environment GraalVM CE 20.2.0 (build 11.0.8+10-jvmci-20.2-b03)
    4OpenJDK 64-Bit Server VM GraalVM CE 20.2.0 (build 11.0.8+10-jvmci-20.2-b03, mixed mode, sharing)
    

NOTE: Official docs

Maven

  1. Download tar with the latest release from https://maven.apache.org/download.cgi

  2. Unpack it

    1$ sudo mv tar xzvf apache-maven-3.6.3-bin.tar.gz /opt
    2$ cd /opt
    3# I like have toools in /opt
    4$ sudo tar -xvzf tar xzvf apache-maven-3.6.3-bin.tar.gz
    
  3. Edit your PATH

    1export PATH=$MVN_HOME/bin:$PATH
    2export MVN_HOME="/opt/apache-maven-3.6.3"
    
  4. Test it

    1$ mvn -v
    2Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
    3Maven home: /opt/apache-maven-3.6.3
    4...
    

NOTE: Official docs

CRC

WARNING: Accessing the CRC requires a Red Hat account.

  1. Download tar from https://developers.redhat.com/products/codeready-containers/overview

  2. Unpack it

    1$ sudo mv tar xzvf crc-linux-amd64.tar.xz /opt
    2$ cd /opt
    3# I like have toools in /opt
    4$ sudo tar -xvzf tar xzvf crc-linux-amd64.tar.xz
    
  3. Link crc to /usr/local/bin/crc

    1sudo ln -sf /opt/crc/crc /usr/local/bin/crc
    
  4. Setup crc

    1crc setup
    

NOTE: Official docs

Coding

OK, installing tools is boring. Maybe I need to prepare some Ansible or Bolt playbook for that. Fortunately, now we can focus on the more interesting part of this article.

Init project

1mvn "io.quarkus:quarkus-maven-plugin:1.6.0.Final:create" \
2  -DprojectGroupId="dev.sky.users" \
3  -DprojectArtifactId="tutorial-app" \
4  -DprojectVersion="0.1" \
5  -DclassName="HelloResource" \
6  -Dpath="hello"
7
8cd tutorial-app

Build it

We have a skeleton with UnitTests and dockerfiles. We can build a native app and pack it into a container(but the building process take some time)

1./mvnw -DskipTests clean package \
2    -Pnative \
3    -Dquarkus.native.container-build=true \
4    -Dquarkus.native.container-runtime=podman # only if you're using Podman

Build container

Now we can build out the image

1podman build -f src/main/docker/Dockerfile.native -t example/tutorial-app:0.1 .
1$ podman images
2REPOSITORY                                             TAG            IMAGE ID      CREATED        SIZE
3localhost/example/tutorial-app                         0.1            97fd892c0412  4 seconds ago  137 MB

Run it

1podman run -it --rm -p 8080:8080 example/tutorial-app:0.1

it was fast, isn't it?

Test it

1$ http :8080/hello
2
3HTTP/1.1 200 OK
4Content-Length: 5
5Content-Type: text/plain;charset=UTF-8
6
7hello

Do you want some tests? Run it

1./mvnw clean test

Live coding - run and forget

That's a very nice feature. We can run our app, modify it, and the application is reloaded after every saves. That makes development a bit faster than normal.

1./mvnw quarkus:dev

Maybe add some basic configuration

OK, let's add some stuff and open our code. Firstly let's add some customized hello messages.

 1// src/main/java/com/redhat/developers/HelloResource.java
 2package dev.sky.users;
 3
 4import javax.ws.rs.GET;
 5import javax.ws.rs.Path;
 6import javax.ws.rs.Produces;
 7import javax.ws.rs.core.MediaType;
 8
 9import org.eclipse.microprofile.config.inject.ConfigProperty;
10
11@Path("/hello")
12public class HelloResource {
13
14    @ConfigProperty(name = "greeting")
15    String greeting;
16
17    @GET
18    @Produces(MediaType.TEXT_PLAIN)
19    public String hello() {
20        return greeting;
21    }
22}

Add message to aplication.properties

1# src/main/resources/application.properties
2greeting=Hello from the dark side!

Test it manually again

1$ http :8080/hello
2
3HTTP/1.1 200 OK
4Content-Length: 25
5Content-Type: text/plain;charset=UTF-8
6
7Hello from the dark side!

Add unittesting

As I said we have some out-of-box tests. But after the change, we need to modify it.

 1// src/test/java/com/redhat/developers/HelloResourceTest.java
 2package dev.sky.users;
 3
 4import io.quarkus.test.junit.QuarkusTest;
 5import org.junit.jupiter.api.Test;
 6
 7import static io.restassured.RestAssured.given;
 8import static org.hamcrest.CoreMatchers.is;
 9
10@QuarkusTest
11public class HelloResourceTest {
12
13    @Test
14    public void testHelloEndpoint() {
15        given()
16          .when().get("/hello")
17          .then()
18             .statusCode(200)
19             .body(is("Hello from the dark side!"));
20    }
21}

Run tests and observe

1./mvnw clean test

Add some DB's stuff

Let's add some CRUD functions. It's nice to make something more than greet. In the beginning, we need some new extensions.

1./mvnw quarkus:add-extension -Dextension="quarkus-resteasy-jsonb, quarkus-jdbc-h2, quarkus-hibernate-orm-panache, quarkus-smallrye-openapi"

Add some H2 config

For this tutorial, we're using H2 database. So it's work only in non-container environment.

1# src/main/resources/application.properties
2...
3quarkus.datasource.jdbc.url=jdbc:h2:mem:default
4quarkus.datasource.db-kind=h2
5quarkus.hibernate-orm.database.generation=drop-and-create

Add new fruit Entity

We nice some class to represent our Fruit object.

 1// src/main/java/com/redhat/developers/Fruit.java
 2package dev.sky.users;
 3
 4import javax.persistence.Entity;
 5
 6import io.quarkus.hibernate.orm.panache.PanacheEntity;
 7
 8@Entity
 9public class Fruit extends PanacheEntity {
10
11    public String name;
12
13    public String season;
14
15}

We're using PanacheEntity so we don't need any Getter or Setter !

Add FruitResource

If we already have our sweet class we need to handle HTTP calls.

 1// src/main/java/com/redhat/developers/FruitResource.java
 2package dev.sky.users;
 3
 4import java.util.List;
 5
 6import javax.ws.rs.GET;
 7import javax.ws.rs.Path;
 8import javax.ws.rs.Produces;
 9import javax.ws.rs.core.MediaType;
10
11@Path("/fruit")
12public class FruitResource {
13
14    @GET
15    @Produces(MediaType.APPLICATION_JSON)
16    public List<Fruit> fruits() {
17        return Fruit.listAll();
18    }
19
20}

Now let's test our new endpoint

1$ http :8080/fruit
2
3HTTP/1.1 200 OK
4Content-Length: 2
5Content-Type: application/json
6
7[]

As we can expect we get an empty list. We didn't provide any data, yet.

Add some POST endpoint in our FruitResource.java

But we want to add some data. Let's start with POST request handling.

 1// src/main/java/com/redhat/developers/FruitResource.java
 2package dev.sky.users;
 3
 4import java.util.List;
 5
 6import javax.transaction.Transactional;
 7import javax.ws.rs.Consumes;
 8import javax.ws.rs.GET;
 9import javax.ws.rs.POST;
10import javax.ws.rs.Path;
11import javax.ws.rs.Produces;
12import javax.ws.rs.core.MediaType;
13import javax.ws.rs.core.Response;
14import javax.ws.rs.core.Response.Status;
15
16@Path("/fruit")
17public class FruitResource {
18
19    @GET
20    @Produces(MediaType.APPLICATION_JSON)
21    public List<Fruit> fruits() {
22        return Fruit.listAll();
23    }
24
25    @Transactional
26    @POST
27    @Consumes(MediaType.APPLICATION_JSON)
28    @Produces(MediaType.APPLICATION_JSON)
29    public Response newFruit(Fruit fruit) {
30        fruit.id = null;
31        fruit.persist();
32        return Response.status(Status.CREATED).entity(fruit).build();
33    }
34
35}

Add some fruit

When our app support POST requests, we can send some data.

1$ http POST :8080/fruit \
2    Content-Type:application/json \
3    name="Banan" \
4    season="Summer"

Check it

 1$ http :8080/fruit
 2
 3HTTP/1.1 200 OK
 4Content-Length: 44
 5Content-Type: application/json
 6
 7[
 8    {
 9        "id": 1,
10        "name": "Banana",
11        "season": "Summer"
12    }
13]

Do you want some instant testing data? Add some sql's

 1-- src/main/resources/import.sql
 2INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Mango','Spring');
 3INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Strawberry','Spring');
 4INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Orange','Winter');
 5INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Lemon','Winter');
 6INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Blueberry','Summer');
 7INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Banana','Summer');
 8INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Watermelon','Summer');
 9INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Apple','Fall');
10INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Pear','Fall');

Now just restart your app - and results will be the following:

 1$ http :8080/fruit
 2
 3HTTP/1.1 200 OK
 4Content-Length: 390
 5Content-Type: application/json
 6
 7[
 8    {
 9        "id": 1,
10        "name": "Mango",
11        "season": "Spring"
12    },
13    {
14        "id": 2,
15        "name": "Strawberry",
16        "season": "Spring"
17    },
18...

Add custom finder

OK, that was easy. Let's complicate logic and add some filters to our app.

 1package dev.sky.users;
 2
 3import java.util.List;
 4
 5import javax.persistence.Entity;
 6
 7import io.quarkus.hibernate.orm.panache.PanacheEntity;
 8
 9@Entity
10public class Fruit extends PanacheEntity {
11
12    public String name;
13
14    public String season;
15
16    public static List<Fruit> findBySeason(String season) {
17        return find("season", season).list();
18    }
19
20}

Update Endpoint to return QueryParam

 1package dev.sky.users;
 2
 3import java.util.List;
 4
 5import javax.transaction.Transactional;
 6import javax.ws.rs.Consumes;
 7import javax.ws.rs.GET;
 8import javax.ws.rs.POST;
 9import javax.ws.rs.Path;
10import javax.ws.rs.Produces;
11import javax.ws.rs.QueryParam;
12import javax.ws.rs.core.MediaType;
13import javax.ws.rs.core.Response;
14import javax.ws.rs.core.Response.Status;
15
16@Path("/fruit")
17public class FruitResource {
18
19    @GET
20    @Produces(MediaType.APPLICATION_JSON)
21    public List<Fruit> fruits(@QueryParam("season") String season) {
22        if (season != null) {
23            return Fruit.findBySeason(season);
24        }
25        return Fruit.listAll();
26    }
27
28    @Transactional
29    @POST
30    @Consumes(MediaType.APPLICATION_JSON)
31    @Produces(MediaType.APPLICATION_JSON)
32    public Response newFruit(Fruit fruit) {
33        fruit.id = null;
34        fruit.persist();
35        return Response.status(Status.CREATED).entity(fruit).build();
36    }
37
38}

And the result is

 1$ http :8080/fruit \
 2season==Summer
 3
 4HTTP/1.1 200 OK
 5Content-Length: 137
 6Content-Type: application/json
 7
 8[
 9    {
10        "id": 5,
11        "name": "Blueberry",
12        "season": "Summer"
13    },
14    {
15        "id": 6,
16        "name": "Banana",
17        "season": "Summer"
18    },
19    {
20        "id": 7,
21        "name": "Watermelon",
22        "season": "Summer"
23    }
24]

Deploy the app

I need to notice, that H2 not working in containers. Maybe in another articule, I will add Postgresql DB.

Prepper the app - add some extension

The best thing here is that we need to add only one extension.

1 ./mvnw quarkus:add-extension -Dextensions="openshift"

Make sure that crc is running

1crc status
2# and
3oc login -u kubeadmin -p <your token> https://api.crc.testing:6443
4oc new-project quarkus  

One command to deploy your app

1./mvnw clean package -Dquarkus.kubernetes.deploy=true

Wow, that was great, we got ImageStream, DeploymentConfig, Service and Repicacontrolers. In on command, without any YAML.

Test the app

 1oc expose svc/tutorial-app
 2# save route variable
 3HOST=$(oc get route tutorial-app -n default --template='{{ .spec.host }}')
 4
 5# Test the app
 6$ http $HOST/hello
 7
 8HTTP/1.1 200 OK
 9cache-control: private
10content-length: 25
11content-type: text/plain;charset=UTF-8
12set-cookie: ff25dce5274413fb672a5a430cbde1ab=49a44cac2ad73eb3031d1718e1137501; path=/; HttpOnly
13
14Hello from the dark side!

As you can see we build an app with unittests, deploy it to OpenShift without YAMLs. If we want to be really fast and skip playing with Quarkus, we can just type:

 1mvn "io.quarkus:quarkus-maven-plugin:1.6.0.Final:create" \
 2  -DprojectGroupId="dev.sky.users" \
 3  -DprojectArtifactId="tutorial-app" \
 4  -DprojectVersion="0.1" \
 5  -DclassName="HelloResource" \
 6  -Dpath="hello"
 7
 8cd tutorial-app
 9./mvnw quarkus:add-extension -Dextensions="openshift"
10./mvnw clean package -Dquarkus.kubernetes.deploy=true
11oc expose svc/tutorial-app

Of course, we need to have Maven and configured access to OCP cluster with oc. But in general, it could be fast and really impressive for presentation purpose.

Clean app

1oc delete project quarkus
2crc stop

Summary

This is an article based on quarkus-tutorial. I fix some errors, skip the Spring parts, and focus on the deployment part. Instruction present in the article not working correctly for some reason, also some plugins versions are outdated. For me, in general, it was a very interesting read. Which pushed me to write my own post with some fixes. What about Quarkus? I will try to work more with this ecosystem. I'm not a Java programmer, but GralVM and all this native stuff look pretty awesome. Especially If I need to make some PoC or demo, just imagine the situation. You run 4 commands, without any code writing, fixing etc. After that, you get a working app with one endpoint. Ready to show processes, pipelines etc. And that's very useful for me. Before that, I just copy my basic Golang app based on Echo framework, but as you know I need to prepare some deployment steps. Now, something will do it for me. That means more time for gam... family :) Take care, the next article will be about K8S with Linode.