Quarkus + JKube: Qute template con procesado de ficheros markdown
Introducción
Quarkus proporciona su propio motor de plantillas, Qute. En esta publicación te mostraré cómo usarlo para renderizar Markdown desde diferentes fuentes usando flexmark-java.
Además de mostrar Qute, otra razón para esta publicación es demostrar configuraciones complejas de ensamblado para construir imágenes de contenedor usando Eclipse JKube. Puedes aprender más sobre JKube y cómo empezar aquí.
He diseñado el proyecto de prueba para ejecutarse en Kubernetes, por lo que algunas de las características no estarán disponibles si la aplicación se ejecuta localmente. La aplicación renderiza una plantilla con fragmentos de markdown cargados desde diferentes ubicaciones, destacando cómo estos fragmentos pueden ensamblarse en la imagen del contenedor usando diferentes técnicas y finalmente empaquetarse con Eclipse JKube.
Aplicación Quarkus
Esta es una aplicación realmente simple con una sola Plantilla y recurso.
Main template
El archivo de la plantilla principal está ubicado en src/main/resources/templates/main.html
, Qute carga plantillas ubicadas en este directorio.
El archivo contiene algunos placeholders o valores que serán reemplazados con contenido markdown procesado usando flexmark-java.
<!-- ... -->
<div>{markdownInResources.raw}</div>
<div>{markdownInJKubeAssembly.raw}</div>
<div class="debug-info">{markdownFiltered.raw}</div>
<!-- ... -->
Qute reemplazará cada uno de los placeholders con el contenido proporcionado en el método data
para la plantilla cargada. El sufijo .raw
evita que Qute escape el HTML proporcionado por flexmark-java.
Main resource class
@Path("/")
public class MainResource {
/* ... */
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get() throws IOException {
return main
.data("title", "Hello Quarkus, you're so qute!")
.data("markdownInResources", md.render(MainResource.class.getResourceAsStream("/main.md")))
// Los siguientes archivos solo estarán disponibles si la aplicación fue desplegada usando Eclipse JKube
.data("markdownInJKubeAssembly", md.render(new File(OPT_STATIC,"main.md")))
.data("markdownFiltered", md.render(new File(OPT_STATIC, "filtered-fragment.md")));
}
@GET
@Path("static/{fileName:.+}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public File getStaticFile(@PathParam("fileName") String fileName) {
// Archivo solo disponible si la aplicación fue desplegada usando Eclipse JKube
return new File(OPT_STATIC, fileName);
}
}
Implementé la clase MainResource para exponer dos endpoints.
El primer endpoint sirve la plantilla ya procesada y es accesible en la ruta raíz de la aplicación web.
Uso el método data
para reemplazar 4 marcadores de posición.
El markdownInResources
está ubicado en un recurso empaquetado, por lo que siempre puede cargarse.
El resto de los recursos markdown dependen del ensamblado que describo más adelante. Así que si no despliegas la aplicación usando JKube, se mostrará un error en lugar del fragmento markdown.
El segundo endpoint sirve los archivos ubicados en el directorio /opt/static
.
El patrón regex en la anotación @Path
nos permite recuperar el nombre del archivo solicitado desde la ruta URL (ej. static/javaee-api-8.0.jar
).
Esto solo tiene sentido en la versión desplegada con JKube de la aplicación.
Proyecto pom.xml
El archivo pom.xml del proyecto Maven está dividido en varias secciones, analicemos las más importantes.
Propiedades
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<buildTimestamp>${maven.build.timestamp}</buildTimestamp>
<version.quarkus>1.7.0.Final</version.quarkus>
<version.flexmark>0.62.2</version.flexmark>
<version.jkube>1.0.0</version.jkube>
<jkube.enricher.jkube-service.type>NodePort</jkube.enricher.jkube-service.type>
</properties>
En el elemento properties, he incluido las versiones de las dependencias del proyecto que se usarán más adelante y la versión de Java compatible con el proyecto.
Además, hay una propiedad buildTimestamp
que se usará en uno de los recursos filtrados para reemplazar un placeholder en un archivo Markdown.
Nota también la propiedad específica de JKube, esta se usa para definir el tipo de puerto del Servicio de Kubernetes para que el servicio pueda exponerse más tarde usando minikube service example
.
Dependencias
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-qute</artifactId>
<version>${version.quarkus}</version>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>${version.flexmark}</version>
</dependency>
<dependency>
<!-- Dependencia aleatoria para incluir en la imagen con preassemble.xml, Maven Assembly Plugin y JKube -->
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
La sección de dependencias, como es habitual, contiene las dependencias del proyecto requeridas para construir y ejecutar la aplicación.
Sin embargo, he incluido una dependencia aleatoria adicional con scope provided
.
Como explicaré más adelante, la configuración del Maven Assembly Plugin se encargará de procesarla.
Build: Resources
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>${project.basedir}/src/main/filtered-resources</directory>
<targetPath>${project.basedir}/target/filtered-resources</targetPath>
<filtering>true</filtering>
</resource>
</resources>
<!-- ... -->
</build>
Además del directorio estándar src/main/resources
para recursos, voy a añadir un directorio adicional src/main/filtered-resources
.
Este directorio está configurado para ser filtrado, lo que significa que cualquier placeholder (ej. ${project.artifactId}
) será reemplazado con la propiedad Maven aplicable cuando se procese.
Este directorio contiene un solo archivo filtered-fragment.md
con varias propiedades incluyendo el buildTimestamp
descrito anteriormente.
Build: Maven Assembly Plugin
<build>
<!-- ... -->
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<outputDirectory>target</outputDirectory>
<archiveBaseDirectory>/</archiveBaseDirectory>
<appendAssemblyId>false</appendAssemblyId>
<finalName>preassembled</finalName>
<descriptors>
<descriptor>preassemble.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>preassemble-files</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- ... -->
</plugins>
<!-- ... -->
</build>
Estoy usando el Maven Assembly Plugin para mostrar cómo incluir un dependencySet
en la imagen del contenedor.
La configuración del plugin hace referencia a un descriptor de ensamblado preassemble.xml
, que será procesado con su salida colocada en el directorio target
.
Las configuraciones finalName
y appendAssemblyId
fuerzan que el ensamblado se coloque en el directorio resultante target/preassembled
.
También he vinculado la ejecución del plugin a la fase package. La ejecución del plugin se activará cada vez que se ejecute la fase del ciclo de vida package de Maven.
A continuación está el contenido del archivo preassemble.xml
:
<assembly>
<id>preassembled</id>
<formats>
<format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<useProjectArtifact>false</useProjectArtifact>
<outputDirectory>/</outputDirectory>
<scope>provided</scope>
</dependencySet>
</dependencySets>
</assembly>
He configurado el ensamblado para definir un solo dependencySet
que copiará todas las dependencias con scope provided y sus dependencias transitivas a la raíz del directorio de ensamblado (target/preassembled
).
Dada la combinación actual de dependencias y configuración de ensamblado, este directorio debería contener los siguientes archivos cuando empaquetes el proyecto:
activation-1.1.jar
javaee-api-8.0.jar
javax.mail-1.6.0.jar
Build: Eclipse JKube
<build>
<!-- ... -->
<plugins>
<!-- ... -->
<plugin>
<groupId>org.eclipse.jkube</groupId>
<artifactId>kubernetes-maven-plugin</artifactId>
<version>${version.jkube}</version>
<configuration>
<images>
<image>
<name>%g/%a:%l</name>
<build>
<from>adoptopenjdk/openjdk11:jre-11.0.8_10-ubi</from>
<ports>8080</ports>
<entryPoint>
<exec>
<arg>java</arg>
<arg>-jar</arg>
<arg>-Dquarkus.http.host=0.0.0.0</arg>
<arg>/deployments/${project.artifactId}-${project.version}-runner.jar</arg>
</exec>
</entryPoint>
<assembly>
<targetDir>/</targetDir>
<excludeFinalOutputArtifact>true</excludeFinalOutputArtifact>
<inline>
<fileSets>
<fileSet>
<directory>src/main/static</directory>
<outputDirectory>opt/static</outputDirectory>
</fileSet>
<fileSet>
<directory>target/filtered-resources</directory>
<outputDirectory>opt/static</outputDirectory>
</fileSet>
<fileSet>
<directory>target/preassembled</directory>
<outputDirectory>opt/static</outputDirectory>
</fileSet>
<fileSet>
<directory>target</directory>
<outputDirectory>deployments/</outputDirectory>
<includes>
<include>*-runner.jar</include>
</includes>
</fileSet>
<fileSet>
<directory>target/lib</directory>
<outputDirectory>deployments/lib</outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
</inline>
</assembly>
</build>
</image>
</images>
</configuration>
</plugin>
</plugins>
</build>
Para este proyecto, he configurado el Kubernetes Maven Plugin de Eclipse JKube para construir una imagen personalizada.
Ten en cuenta que existen otros enfoques sin configuración y con opiniones preestablecidas para construir una imagen de Quarkus. Pero en este caso, como quiero demostrar el poder de los ensamblados, elegí este procedimiento en su lugar.
En la sección build de la imagen he definido la imagen base (from
), el puerto expuesto y el punto de entrada de la imagen con los valores estándar.
Luego viene la parte más importante, la configuración de assembly
.
El targetDir
define el directorio bajo el cual los archivos serán copiados dentro del contenedor.
En este caso, como quiero copiar archivos a varias ubicaciones, usé el directorio raíz /
.
Más adelante, puedo definir directorios específicos para cada fileSet
.
El primer fileSet
configura JKube para copiar todos los archivos en src/main/static
al directorio /opt/static
del contenedor.
El segundo fileSet
configura JKube para tomar los archivos previamente procesados y filtrados por el Maven Resources Plugin (build:resources) y copiarlos al directorio /opt/static
también.
El tercer fileSet
configura JKube para copiar los archivos JAR procesados por el Maven Assembly Plugin al directorio /opt/static
.
Probablemente, puedas ver el patrón ahora.
Usamos otros plugins de Maven para recopilar archivos en la fase package y luego JKube ensamblará esos archivos en la imagen del contenedor durante el objetivo k8s:build
.
El cuarto y quinto fileSet
configuran JKube para copiar el artefacto generado por Quarkus y las dependencias a los directorios /deployments
y /deployments/lib
.
Ejecutando la aplicación Quarkus
Por simplicidad y para evitar tener que subir la imagen generada a un registro compartido de imágenes, usaré Minikube.
Para evitar tener que subir la imagen a un registro compartido accesible desde el clúster, compartiré el daemon Docker de Minikube con mi entorno local:
eval $(minikube docker-env)
A continuación, instruiré a Maven para empaquetar la aplicación y ejecutar los targets del Kubernetes Maven Plugin de JKube:
mvn package k8s:build k8s:resource k8s:apply
Ahora podemos abrir la aplicación en nuestro navegador ejecutando:
minikube service example
Si todo va bien, veremos una ventana del navegador con contenido similar a la imagen anterior.
Como puedes ver, los tres fragmentos markdown han sido reemplazados y cargados correctamente con sus estilos y marcado apropiados.
El fragmento con la información de depuración aparece con los marcadores de posición correctamente reemplazados con propiedades de Maven.
Si haces clic en el enlace Go!
, el JAR preensamblado de la dependencia proporcionada también debería descargarse.
Conclusión
En este artículo, te he mostrado cómo usar el motor de plantillas Qute de Quarkus para renderizar Markdown desde diferentes fuentes usando flexmark-java. También he explicado cómo puedes ensamblar estas fuentes usando diferentes Plugins de Maven y Eclipse JKube.
Puedes encontrar el código fuente completo para esta publicación en GitHub.
También puedes ver las demos de nuestro equipo de Quarkus+JKube en el stand virtual de Eclipse en J4K 2020.