3sky's notes

Minimal blog about IT

Should I use Quarkus

2020-11-23 8 min read 3sky

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

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

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

    $ java -version  
    openjdk version "11.0.8" 2020-07-14
    OpenJDK Runtime Environment GraalVM CE 20.2.0 (build 11.0.8+10-jvmci-20.2-b03)
    OpenJDK 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

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

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

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

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

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

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

    crc 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

mvn "io.quarkus:quarkus-maven-plugin:1.6.0.Final:create" \
  -DprojectGroupId="dev.sky.users" \
  -DprojectArtifactId="tutorial-app" \
  -DprojectVersion="0.1" \
  -DclassName="HelloResource" \
  -Dpath="hello"

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

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

Build container

Now we can build out the image

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

Run it

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

it was fast, isn’t it?

Test it

$ http :8080/hello

HTTP/1.1 200 OK
Content-Length: 5
Content-Type: text/plain;charset=UTF-8

hello

Do you want some tests? Run it

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

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

// src/main/java/com/redhat/developers/HelloResource.java
package dev.sky.users;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.config.inject.ConfigProperty;

@Path("/hello")
public class HelloResource {

    @ConfigProperty(name = "greeting")
    String greeting;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return greeting;
    }
}

Add message to aplication.properties

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

Test it manually again

$ http :8080/hello

HTTP/1.1 200 OK
Content-Length: 25
Content-Type: text/plain;charset=UTF-8

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

// src/test/java/com/redhat/developers/HelloResourceTest.java
package dev.sky.users;

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 HelloResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("Hello from the dark side!"));
    }
}

Run tests and observe

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

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

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

Add new fruit Entity

We nice some class to represent our Fruit object.

// src/main/java/com/redhat/developers/Fruit.java
package dev.sky.users;

import javax.persistence.Entity;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Fruit extends PanacheEntity {

    public String name;

    public String season;

}

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.

// src/main/java/com/redhat/developers/FruitResource.java
package dev.sky.users;

import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/fruit")
public class FruitResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Fruit> fruits() {
        return Fruit.listAll();
    }

}

Now let’s test our new endpoint

$ http :8080/fruit

HTTP/1.1 200 OK
Content-Length: 2
Content-Type: application/json

[]

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.

// src/main/java/com/redhat/developers/FruitResource.java
package dev.sky.users;

import java.util.List;

import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

@Path("/fruit")
public class FruitResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Fruit> fruits() {
        return Fruit.listAll();
    }

    @Transactional
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response newFruit(Fruit fruit) {
        fruit.id = null;
        fruit.persist();
        return Response.status(Status.CREATED).entity(fruit).build();
    }

}

Add some fruit

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

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

Check it

$ http :8080/fruit

HTTP/1.1 200 OK
Content-Length: 44
Content-Type: application/json

[
    {
        "id": 1,
        "name": "Banana",
        "season": "Summer"
    }
]

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

-- src/main/resources/import.sql
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Mango','Spring');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Strawberry','Spring');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Orange','Winter');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Lemon','Winter');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Blueberry','Summer');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Banana','Summer');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Watermelon','Summer');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Apple','Fall');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Pear','Fall');

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

$ http :8080/fruit

HTTP/1.1 200 OK
Content-Length: 390
Content-Type: application/json

[
    {
        "id": 1,
        "name": "Mango",
        "season": "Spring"
    },
    {
        "id": 2,
        "name": "Strawberry",
        "season": "Spring"
    },
...

Add custom finder

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

package dev.sky.users;

import java.util.List;

import javax.persistence.Entity;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Fruit extends PanacheEntity {

    public String name;

    public String season;

    public static List<Fruit> findBySeason(String season) {
        return find("season", season).list();
    }

}

Update Endpoint to return QueryParam

package dev.sky.users;

import java.util.List;

import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

@Path("/fruit")
public class FruitResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Fruit> fruits(@QueryParam("season") String season) {
        if (season != null) {
            return Fruit.findBySeason(season);
        }
        return Fruit.listAll();
    }

    @Transactional
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response newFruit(Fruit fruit) {
        fruit.id = null;
        fruit.persist();
        return Response.status(Status.CREATED).entity(fruit).build();
    }

}

And the result is

$ http :8080/fruit \
season==Summer

HTTP/1.1 200 OK
Content-Length: 137
Content-Type: application/json

[
    {
        "id": 5,
        "name": "Blueberry",
        "season": "Summer"
    },
    {
        "id": 6,
        "name": "Banana",
        "season": "Summer"
    },
    {
        "id": 7,
        "name": "Watermelon",
        "season": "Summer"
    }
]

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.

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

Make sure that crc is running

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

One command to deploy your app

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

oc expose svc/tutorial-app
# save route variable
HOST=$(oc get route tutorial-app -n default --template='{{ .spec.host }}')

# Test the app
$ http $HOST/hello

HTTP/1.1 200 OK
cache-control: private
content-length: 25
content-type: text/plain;charset=UTF-8
set-cookie: ff25dce5274413fb672a5a430cbde1ab=49a44cac2ad73eb3031d1718e1137501; path=/; HttpOnly

Hello 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:

mvn "io.quarkus:quarkus-maven-plugin:1.6.0.Final:create" \
  -DprojectGroupId="dev.sky.users" \
  -DprojectArtifactId="tutorial-app" \
  -DprojectVersion="0.1" \
  -DclassName="HelloResource" \
  -Dpath="hello"

cd tutorial-app
./mvnw quarkus:add-extension -Dextensions="openshift"
./mvnw clean package -Dquarkus.kubernetes.deploy=true
oc 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

oc delete project quarkus
crc 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.