Cuando estamos usando Maven y queremos levantar contenedores de Docker para, por ejemplo, ejecutar los tests de integración, es bastante típico usar alguno de los plugins específicos que existen. Donde a día de hoy posiblemente el más habitual es docker-maven-plugin de Fabric8.
Estos plugins suelen ser un recubrimiento sobre un cliente de Docker que al final se conecta con el Docker daemon para realizar las acciones necesarias. Si bien para casos sencillos son muy convenientes, y de hecho seguramente deberían ser la opción por defecto, el problema que tienen este tipo de plugins es que están más pensados para crear imágenes en vez de ejecutarlas, y sobre todo si queremos levantar distintos contenedores con dependencias entre ellos… la cosa se complica ?.
En este tutorial vamos a ver cómo podemos usar el simple plugin de Maven
|
Text
|
|
|---|---|
|
exec-maven-plugin
|
|
para quitarnos estos intermediarios y hacer lo que queramos directamente con el comando
|
Text
|
|
|---|---|
|
docker
|
|
.
De hecho en este tutorial utilizaremos el comando
|
Text
|
|
|---|---|
|
docker compose
|
|
que nos permitirá más flexibilidad a la hora de levantar distintos contenedores de forma simultánea.
Podéis encontrar todo el código de este tutorial en: https://github.com/alejandropg/tutorial-maven-docker-exec. Incluido un ejemplo de cómo ejecutarlo integrado con los GitHub Actions.
Índice
- Entorno
- Maven
Textpom.xml
- El script para preparar el arranque de los tests de integración:
Textdocker-compose-it-up.sh
- El script para limpiar el entorno después de los tests de integración:
Textdocker-compose-it-down.sh
- La configuración de
Textdocker compose
- Conclusiones
- Sobre el autor
1. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 16″ (Apple M1 Pro, 32GB LPDDR5, 1TB SSD).
- Sistema Operativo: macOS Monterey 12.0.1.
- Java version: 11.0.13, vendor: Azul Systems, Inc., arch: “aarch64”, family: “mac”.
- Apache Maven 3.8.3.
- Docker Desktop 4.1.1 (69879) y también probado en 4.2.0 (70708).
2. Maven
Text
pom.xml
|
Text
|
|
|---|---|
|
pom.xml
|
|
Para usar el plugin
|
Text
|
|
|---|---|
|
exec-maven-plugin
|
|
simplemente haremos la siguiente configuración en nuestro Maven
|
Text
|
|
|---|---|
|
pom.xml
|
|
.
Text ...
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${exec-maven-plugin.version}</version>
<executions>
<execution>
<id>start</id>
<phase>pre-integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<workingDirectory>${maven.multiModuleProjectDirectory}</workingDirectory>
<executable>docker-compose-it-up.sh</executable>
</configuration>
</execution>
<execution>
<id>stop</id>
<phase>post-integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<workingDirectory>${maven.multiModuleProjectDirectory}</workingDirectory>
<executable>docker-compose-it-down.sh</executable>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
Aquí se ve como simplemente nos enganchamos a la fase de
|
Text
|
|
|---|---|
|
pre-integration-test
|
|
y
|
Text
|
|
|---|---|
|
post-integration-test
|
|
para ejecutar un script para arrancar “
|
Text
|
|
|---|---|
|
up
|
|
” y parar “
|
Text
|
|
|---|---|
|
down
|
|
” el Docker compose.
He elegido usar un script y no hacerlo directamente aquí porque eso nos puede dar mucha flexibilidad a la hora de ejecutar cualquier otro tipo de configuración de arranque que necesitemos para nuestros tests de integración. Lo veremos más adelante con un ejemplo.
3. El script para preparar el arranque de los tests de integración:
Text
docker-compose-it-up.sh
|
Text
|
|
|---|---|
|
docker-compose-it-up.sh
|
|
|
Text
|
|
|---|---|
|
docker-compose-it-up.sh
|
|
es un script sh normal y corriente, por lo que tendremos a nuestro alcance toda la potencia de la shell.
Text #!/usr/bin/env sh
DOCKER_COMPOSE_FILE=docker-compose-it.yml
_docker_compose_get_image_name() {
image_name=$(fgrep --no-filename --max-count=1 image "${DOCKER_COMPOSE_FILE}" | cut -d':' -f2-)
# remove leading whitespace characters
image_name="${image_name#"${image_name%%[![:space:]]*}"}"
# remove trailing whitespace characters
image_name="${image_name%"${image_name##*[![:space:]]}"}"
echo "${image_name}"
}
_docker_compose_pull_image() {
docker image inspect "${1}" 2>&1 1>/dev/null
image_exist=$?
if [ $image_exist != 0 ]; then
echo '[INFO] Pull IT Docker images...'
docker compose -f "${DOCKER_COMPOSE_FILE}" pull
fi
}
# We don't want that the images download affect to the timeout,
# so pull images before up if they are not in the local image repository
image_name=$(_docker_compose_get_image_name)
_docker_compose_pull_image "${image_name}"
echo '[INFO] Up IT Docker containers...'
expect <<EOD
set timeout 5
spawn docker compose -f "${DOCKER_COMPOSE_FILE}" up
expect "database system is ready to accept connections"
EOD
Este script seguramente es lo más complicado de todo el tutorial. Al principio encontramos un par de funciones de ayuda donde el propósito de estas es una pequeña optimización para no hacer el
|
Text
|
|
|---|---|
|
docker compose pull
|
|
constantemente y ahorrar unos preciosos segundos cuando lo estamos ejecutando en local. Es decir, si analizamos en detalle la función
|
Text
|
|
|---|---|
|
_docker_compose_pull_image
|
|
, podemos ver que lo que hace es comprobar si la imagen ya está en local o no. De no encontrarla intentará hacer un
|
Text
|
|
|---|---|
|
pull
|
|
de todo el Docker compose.
Luego la otra parte con “truco” es el uso del
|
Text
|
|
|---|---|
|
expect
|
|
. La intención del
|
Text
|
|
|---|---|
|
expect
|
|
es no terminar la ejecución del script hasta que se encuentre en la salida la línea especificada. De esta forma garantizamos que los tests de integración no empezarán a ejecutarse hasta que esté debidamente levantado el contenedor.
Text expect "database system is ready to accept connections"
Esto sería el equivalente a, por ejemplo, cuando usando el
|
Text
|
|
|---|---|
|
docker-maven-plugin
|
|
de Fabric8 y hacemos:
Text <wait>
<log>database system is ready to accept connections</log>
</wait>
También se define un
|
Text
|
|
|---|---|
|
timeout
|
|
de 5 segundos, de forma que si en este tiempo no se encuentra la línea especificada el script terminará abruptamente. Precisamente por este timeout es por lo que separamos el
|
Text
|
|
|---|---|
|
pull
|
|
de las imágenes del
|
Text
|
|
|---|---|
|
up
|
|
del Docker compose, ya que si no lo separamos será muy fácil que salte el timeout la primera vez que lo ejecutemos, cuando intente descargar las imágenes.
4. El script para limpiar el entorno después de los tests de integración:
Text
docker-compose-it-down.sh
|
Text
|
|
|---|---|
|
docker-compose-it-down.sh
|
|
De nuevo
|
Text
|
|
|---|---|
|
docker-compose-it-down.sh
|
|
es un script sh normal donde podremos ejecutar todo lo que necesitemos para limpiar el entorno tras ejecutar los tests de integración.
Text #!/usr/bin/env sh
echo '[INFO] Down IT Docker containers...'
docker compose --file docker-compose-it.yml down
Se ve como en nuestro caso simplemente paramos el Docker compose. Además aquí son comandos normalitos y no necesitamos usar ningún “truco” como vimos con el
|
Text
|
|
|---|---|
|
expect
|
|
en el punto anterior.
5. La configuración de
Text
docker compose
|
Text
|
|
|---|---|
|
docker compose
|
|
Para configurar el Docker compose usamos un fichero de configuración normal y corriente:
|
Text
|
|
|---|---|
|
docker-compose-it.yml
|
|
Text services:
it-ddbb:
image: postgres:13.3-alpine
ports:
- 5432:5432
volumes:
- it-ddbb:/var/lib/postgresql/data:delegated
environment:
POSTGRES_PASSWORD: password
volumes:
it-ddbb:
En el ejemplo solo levantamos un servicio, pero como es un fichero normal de configuración de Docker compose podríamos hacer cualquier cosa que este permita, como levantar n servicios, con dependencias entre ellos, montar volúmenes…
6. Conclusiones
Con este ejemplo estamos yendo un poco a “bajo nivel”, pero si os fijáis tampoco tanto, puesto que hay pocas lineas que no tuviéramos que poner de todas formas: nombre de la imagen, nombre de los volúmenes, en que fase del ciclo de Maven queremos engancharlo…
Y sin embargo conseguimos un aumento de flexibilidad enorme, ya que ponemos a nuestro alcance toda la potencia de la shell, de Docker y de Docker compose.
Así que desde luego esta puede ser una buena opción a tener en cuenta si vemos que se nos complica la cosa a la hora de levantar contenedores dentro del ciclo de vida de Maven.
7. Sobre el autor
Alejandro Pérez García (@alejandropgarci).
Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster.
Socio fundador de Autentia Real Business Solutions S.L. – “Soporte a Desarrollo”.