Quarkus + JKube: Qute template with markdown processing from different sources
Introduction
Quarkus provides its very own templating engine, Qute. In this post, I will show you how to use it to render Markdown from different sources using flexmark-java.
Besides showcasing Qute, another reason for this post is to show complex assembly configurations to build container images using Eclipse JKube. You can learn more about JKube and how to get started here.
I designed the project to be run in Kubernetes, so some of the features won’t be available if the application is run locally. The application renders a template with markdown fragments loaded from different locations, highlighting how these fragments can be assembled into the container image using different techniques and ultimately packaged with Eclipse JKube.
Quarkus application
This is a really simple application with a single Template and resource.
Main template
The main template file is located in src/main/resources/templates/main.html
, Qute loads templates located in this directory.
The file contains a few placeholders/values that will be replaced with markdown content parsed using flexmark-java.
1<!-- ... -->
2<div>{markdownInResources.raw}</div>
3<div>{markdownInJKubeAssembly.raw}</div>
4<div class="debug-info">{markdownFiltered.raw}</div>
5<!-- ... -->
Qute will replace each of the placeholders with the content provided in the data
method for the loaded template. The .raw
suffix prevents Qute from escaping the HTML provided by flexmark-java.
Main resource class
1@Path("/")
2public class MainResource {
3 /* ... */
4 @GET
5 @Produces(MediaType.TEXT_HTML)
6 public TemplateInstance get() throws IOException {
7 return main
8 .data("title", "Hello Quarkus, you're so qute!")
9 .data("markdownInResources", md.render(MainResource.class.getResourceAsStream("/main.md")))
10 // Following files will only be available if application was deployed using Eclipse JKube
11 .data("markdownInJKubeAssembly", md.render(new File(OPT_STATIC,"main.md")))
12 .data("markdownFiltered", md.render(new File(OPT_STATIC, "filtered-fragment.md")));
13 }
14
15 @GET
16 @Path("static/{fileName:.+}")
17 @Produces(MediaType.APPLICATION_OCTET_STREAM)
18 public File getStaticFile(@PathParam("fileName") String fileName) {
19 // File only available if application was deployed using Eclipse JKube
20 return new File(OPT_STATIC, fileName);
21 }
22}
I implemented the MainResource class to expose two endpoints.
The first endpoint delivers the processed template file and is accessible in the root path of the web application. I use the data
method to replace 4 placeholders. The markdownInResources is located in a packaged resource, so it can always be loaded. The rest of the markdown resources depend on the assembly I describe later on. So if you don’t deploy the application using JKube, an error will be displayed instead of the markdown fragment.
The second endpoint delivers files located in the /opt/static
directory. The regex pattern in the @Path
annotation allows us to retrieve the file name of the requested file from the URL path (e.g. static/javaee-api-8.0.jar
). Again, this only makes sense in the JKube deployed version of the application.
Project pom.xml
The Maven project pom.xml file is divided into several sections, let’s analyze the most important ones.
Properties
1<properties>
2 <maven.compiler.source>11</maven.compiler.source>
3 <maven.compiler.target>11</maven.compiler.target>
4 <buildTimestamp>${maven.build.timestamp}</buildTimestamp>
5 <version.quarkus>1.7.0.Final</version.quarkus>
6 <version.flexmark>0.62.2</version.flexmark>
7 <version.jkube>1.0.0</version.jkube>
8 <jkube.enricher.jkube-service.type>NodePort</jkube.enricher.jkube-service.type>
9</properties>
In the properties element, I’ve included the versions of the project dependencies that will be used later on and the Java target and sources version.
Besides, there is a buildTimestamp
property that will be used in one of the filtered resources to replace a property placeholder in a Markdown file.
Note the JKube specific property too, this one is used to define the Kubernetes Service port type so that the service can be exposed later on using minikube service example
.
Dependencies
1<dependencies>
2 <dependency>
3 <groupId>io.quarkus</groupId>
4 <artifactId>quarkus-resteasy-qute</artifactId>
5 <version>${version.quarkus}</version>
6 </dependency>
7 <dependency>
8 <groupId>com.vladsch.flexmark</groupId>
9 <artifactId>flexmark-all</artifactId>
10 <version>${version.flexmark}</version>
11 </dependency>
12 <dependency>
13 <!-- Random dependency to include in the image with preassemble.xml, Maven Assembly Plugin and JKube -->
14 <groupId>javax</groupId>
15 <artifactId>javaee-api</artifactId>
16 <version>8.0</version>
17 <scope>provided</scope>
18 </dependency>
19</dependencies>
The dependencies section, as usual, contains the project dependencies required to build and run the application.
However, I’ve included an additional random dependency with scope provided
. As I’ll explain later on, I configured the Maven Assembly Plugin to process it.
Build: Resources
1<build>
2 <resources>
3 <resource>
4 <directory>src/main/resources</directory>
5 </resource>
6 <resource>
7 <directory>${project.basedir}/src/main/filtered-resources</directory>
8 <targetPath>${project.basedir}/target/filtered-resources</targetPath>
9 <filtering>true</filtering>
10 </resource>
11 </resources>
12 <!-- ... -->
13</build>
In addition to the standard convention src/main/resources
directory for resources, I’m going to add an additional src/main/filtered-resources
directory. This directory is configured to be filtered, which means that any property placeholder (e.g. ${project.artifactId }
) will be replaced with the applicable Maven property when processed.
This directory contains a single file filtered-fragment.md
with several properties including the buildTimestamp
described earlier.
Build: Maven Assembly Plugin
1<build>
2 <!-- ... -->
3 <plugins>
4 <plugin>
5 <artifactId>maven-assembly-plugin</artifactId>
6 <configuration>
7 <outputDirectory>target</outputDirectory>
8 <archiveBaseDirectory>/</archiveBaseDirectory>
9 <appendAssemblyId>false</appendAssemblyId>
10 <finalName>preassembled</finalName>
11 <descriptors>
12 <descriptor>preassemble.xml</descriptor>
13 </descriptors>
14 </configuration>
15 <executions>
16 <execution>
17 <id>preassemble-files</id>
18 <phase>package</phase>
19 <goals>
20 <goal>single</goal>
21 </goals>
22 </execution>
23 </executions>
24 </plugin>
25 <!-- ... -->
26 </plugins>
27 <!-- ... -->
28</build>
I’m using the Maven Assembly Plugin to show how to include a dependencySet
in the container image.
The plugin configuration references an assembly descriptor preassemble.xml
, that will be processed with its output placed in the target
directory. The finalName
and appendAssemblyId
configurations force the assembly to be placed in the resulting directory target/preassembled
.
I’ve also bound the execution of the plugin to the package phase. The plugin execution will be triggered whenever the Maven package lifecycle phase runs.
Following is the content of the preassemble.xml file:
1<assembly>
2 <id>preassembled</id>
3 <formats>
4 <format>dir</format>
5 </formats>
6 <includeBaseDirectory>false</includeBaseDirectory>
7 <dependencySets>
8 <dependencySet>
9 <useProjectArtifact>false</useProjectArtifact>
10 <outputDirectory>/</outputDirectory>
11 <scope>provided</scope>
12 </dependencySet>
13 </dependencySets>
14</assembly>
I’ve configured the assembly to define a single dependencySet
that will copy all dependencies with scope provided and their transitive dependencies to the assembly directory root (target/preassembled
). Given the current combination of dependencies and assembly configuration, this directory should contain the following files when you package the project:
activation-1.1.jar
javaee-api-8.0.jar
javax.mail-1.6.0.jar
Build: Eclipse JKube
1<build>
2 <!-- ... -->
3 <plugins>
4 <!-- ... -->
5 <plugin>
6 <groupId>org.eclipse.jkube</groupId>
7 <artifactId>kubernetes-maven-plugin</artifactId>
8 <version>${version.jkube}</version>
9 <configuration>
10 <images>
11 <image>
12 <name>%g/%a:%l</name>
13 <build>
14 <from>adoptopenjdk/openjdk11:jre-11.0.8_10-ubi</from>
15 <ports>8080</ports>
16 <entryPoint>
17 <exec>
18 <arg>java</arg>
19 <arg>-jar</arg>
20 <arg>-Dquarkus.http.host=0.0.0.0</arg>
21 <arg>/deployments/${project.artifactId}-${project.version}-runner.jar</arg>
22 </exec>
23 </entryPoint>
24 <assembly>
25 <targetDir>/</targetDir>
26 <excludeFinalOutputArtifact>true</excludeFinalOutputArtifact>
27 <inline>
28 <fileSets>
29 <fileSet>
30 <directory>src/main/static</directory>
31 <outputDirectory>opt/static</outputDirectory>
32 </fileSet>
33 <fileSet>
34 <directory>target/filtered-resources</directory>
35 <outputDirectory>opt/static</outputDirectory>
36 </fileSet>
37 <fileSet>
38 <directory>target/preassembled</directory>
39 <outputDirectory>opt/static</outputDirectory>
40 </fileSet>
41 <fileSet>
42 <directory>target</directory>
43 <outputDirectory>deployments/</outputDirectory>
44 <includes>
45 <include>*-runner.jar</include>
46 </includes>
47 </fileSet>
48 <fileSet>
49 <directory>target/lib</directory>
50 <outputDirectory>deployments/lib</outputDirectory>
51 <includes>
52 <include>*.jar</include>
53 </includes>
54 </fileSet>
55 </fileSets>
56 </inline>
57 </assembly>
58 </build>
59 </image>
60 </images>
61 </configuration>
62 </plugin>
63 </plugins>
64</build>
For this project, I’ve configured Eclipse JKube’s Kubernetes Maven Plugin to build a customized image.
Please note that there are other opinionated configuration-less approaches to build a Quarkus image. But in this case, since I want to demonstrate the power of assemblies I chose this procedure instead.
In the build section of the image I’ve defined the base image (from
), the exposed port and the image entry point with the standard values.
Next comes the most important part, assembly
configuration. The targetDir
defines the directory under which files will be copied within the container. In this case, since I want to copy files to several locations, I used the root directory /
. Later on, I can define specific directories for each fileSet
.
The first fileSet
configures JKube to copy all files in src/main/static
to the container’s /opt/static
directory.
The second fileSet
configures JKube to grab the files previously processed and filtered by the Maven Resources Plugin (build:resources) and copy them to the /opt/static
directory too.
The third fileSet
configures JKube to copy the JAR files processed by the Maven Assembly Plugin to the /opt/static
directory.
You can probably see the pattern now. We use other Maven plugins to gather files in the package phase and then JKube will assemble those files in the container image during the k8s:build
goal.
The fourth and fifth fileSet
configure JKube to copy the Quarkus generated artifact and dependencies to the /deployments
and /deployments/lib
directory.
Running the Quarkus application
For simplicity purposes and to avoid pushing the generated image to a shared registry I will be using Minikube.
To avoid having to push the image to a shared registry accessible from the cluster, I will share Minikube’s Docker daemon with my local environment:
1eval $(minikube docker-env)
Next, I’ll instruct Maven to package the application and to run JKube’s Kubernetes Maven Plugin goals:
1mvn package k8s:build k8s:resource k8s:apply
We can now open the application in our browser by running:
1minikube service example
If everything goes well, we will see a browser window with content similar to the previous image.
As you can see, the three markdown fragments have been replaced and loaded correctly with their appropriate styles and markup. The fragment with the debug information shows up with the placeholders correctly replaced with Maven properties. If you click on the Go!
link, the JAR preassembled from the provided dependency should be downloaded too.
Conclusion
In this article, I showed you how to use Quarkus’ Qute template engine to render Markdown from different sources using flexmark-java. I’ve also explained how you can assemble these sources using different Maven Plugins and Eclipse JKube.
You can find the full source code for this post at GitHub.
You can also check out our team’s Quarkus+JKube demos on Eclipse’s virtual booth at J4K 2020.