lunes, 22 de abril de 2024

Kubernetes: Despliegue conjunto de Quarkus y BBDD

Hemos hecho hace poco un post donde explicábamos como montar un cluster de Kubernetes en local, aquí. Donde también vimos conceptos básicos de Kubernetes, que nos ayudaron a comprender mejor el despliegue. 

Hoy veremos cómo desplegar dos contenedores distintos y entablar una conexión entre ambos. Para ello seguiremos con un ejemplo de un microservicio Quarkus y su conexión con una base de datos MariaDB. Pero además también veremos nuevos conceptos de Kubernetes que ampliarán nuestro conocimiento del mismo. 

Empezaremos montando el servicio de MariaDB donde además necesitaremos de la ayuda de un configMap y un configSecret. 

  • ConfigMap: Es otro tipo de objeto disponible en Kubernetes. Utilizado para almacenar datos no confidenciales en el formato clave-valor. Pueden ser utilizados como variables de entorno, argumentos de la linea de comandos o como ficheros de configuración en un Volumen.
  • Secrets:  Otro objeto que permite almacenar y administrar información confidencial. Permitiendo flexibilidad para añadir palabras claves sin necesidad de configurarlas en los pods. 

Por un lado crearemos un ConfigMap que nos permita añadir un script.sql inicial y que se ejecute al inicializar la base de datos. Muy util para entornos de pruebas. 

apiVersion: v1
kind: ConfigMap
metadata:
name: mariadb-initdb-config
data:
initdb.sql: |
CREATE DATABASE library;
GRANT ALL PRIVILEGES ON library.* TO 'username-default'@'%' IDENTIFIED BY 'my_cool_secret';
USE `library`;
CREATE TABLE `BOOK` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`NAME` varchar(45) DEFAULT NULL,
`AUTHOR` varchar(45) DEFAULT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
INSERT INTO BOOK (NAME, AUTHOR) VALUES('Ender Game', 'Orson S. Card');
INSERT INTO BOOK (NAME, AUTHOR) VALUES('The stars my destination', 'Alfred Bester');
INSERT INTO BOOK (NAME, AUTHOR) VALUES('Dune', 'Frank Herbert');

Por otro, crearemos el Secret para poder almacenar de forma segura las contraseñas de usuario y administrador de la base de datos. Las claves que se incluyan en los secrets deben de estar codificados en base 64. 

apiVersion: v1
kind: Secret
metadata:
name: mariadb-secret
data:
user.pass: bXlfY29vbF9zZWNyZXQ=
root.pass: bXktc2VjcmV0LXB3

Lo siguiente es la configuración de la base de datos. Lo crearemos como un objeto deployment donde tendremos toda la configuración. A través de variables de entorno, que hacen referencia a los secrets almacenados, configuraremos los valores de las contraseñas del administrador y usuario. Y a través la configuración de los volúmenes podremos indicar el script que debe ejecutar al inicio. 

apiVersion: apps/v1
kind: Deployment # what to create?
metadata:
name: mariadb
spec: # specification for deployment resource
replicas: 1 # how many replicas of pods we want to create
selector:
matchLabels:
app: mariadb
strategy:
type: Recreate
template: # blueprint for pods
metadata:
labels:
app: mariadb # service will look for this label
spec: # specification for pods
containers: # we can have one or more containers
- name: mariadb
image: mariadb
ports:
- containerPort: 3306 #port exposed in cluster
env: # allows to define environment variables
- name: MARIADB_ROOT_PASSWORD
valueFrom:
secretKeyRef: # allows to refer to a secret configured
name: mariadb-secret #name of the secret
key: root.pass #key of the secret that you want to use
- name: MARIADB_USER
value: username-default
- name: MARIADB_PASSWORD
valueFrom:
secretKeyRef:
name: mariadb-secret
key: user.pass
volumeMounts:
- name: mariadb-initdb #configuration of the volumes
mountPath: /docker-entrypoint-initdb.d
volumes:
- name: mariadb-initdb #volumen to configure
configMap:
name: mariadb-initdb-config #refer key of the config map

El siguiente paso será configurar el microservicio Quarkus. En otro post entraremos más en las configuración de Quarkus con Kubernetes o Docker. Pero en esté solo indicaremos cómo configurar la base de datos para ver también cómo podemos realizar la conexión entre ambos contenedores. 

## bbdd testing conf
quarkus.datasource.devservices.image-name=mariadb:10.3.6
quarkus.datasource.jdbc.driver=org.mariadb.jdbc.Driver

%prod.quarkus.datasource.db-kind=mariadb
%prod.quarkus.datasource.username=username-default
%prod.quarkus.datasource.password=my_cool_secret
%prod.quarkus.datasource.jdbc.url=jdbc:mariadb://mariadb:3306/library
%prod.quarkus.datasource.jdbc.max-size=5

Como se puede apreciar, simplemente vale con indicar la etiqueta indicada en el deployment. Y los datos del usuario de BBDD que hemos indicado en la creación del pod. 

Y esto ha sido todo. Espero que os haya servido para aprender un poco más sobre Kubernetes, los ConfigMap, Secrets y Quarkus.

jueves, 22 de febrero de 2024

Kubernetes: Como desplegar un cluster en local

En este post vamos a ver de forma sencilla como poder crear tu propio cluster de Kubernetes en local. Y así poder adentrarnos un poco en esta tecnología. En el mundo del desarrollo cada vez es más necesario tener al menos conocimientos básicos de DevOps. 

Y de eso trata este post de poder tener al menos una noción de lo que significan determinados conceptos. Aunque el ejemplo es sencillo y en un par de ficheros tendremos lo que deseamos, necesitaremos de más teoría de la que normalmente indicamos, para poder llevarlo a cabo. La idea es poderarrancar un cluster, con dos instancias de nuestro microservicio que contendrá un sencillo health y poder probar desde fuera del cluster su funcionamiento. 

Empecemos con conceptos básicos de la tecnología a utilizar:

  • Kubernetes: Es un sistema de código abierto para automatizar el despliegue, el escalado y la gestión de aplicaciones en contenedores. 
  • Kind: es una herramienta para ejecutar clusters locales de Kubernetes utilizando "nodos" contenedores Docker. Aunque nació con otro objetivo se puede usar para el despliegue en local. 
  • Kubectl: Herramienta para manejar mediante comandos tu cluster de Kubernetes. 
Por tanto lo primero es instalar tanto Docker o Podman, como Kind y Kubectl si es que aún no lo tenemos. Y antes de mostrar la configuración del cluster debemos tener otro par de conceptos claros sobre arquitectura de Kubernetes. Estos son:

  • Nodo o worker: Elemento del cluster, puede ser una máquina física o virtual, que ejecutan aplicaciones en contenedores. 
  • Control Plane: Gestiona los worker y los Pods en el cluster.
Sabiendo esto, ya tenemos podremos entender un poco más el fichero de configuración del cluster. Este se almacenará en un fichero YAML al igual que podemos hacer con cualquier otro objeto de Kubernetes.

apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
    listenAddress: "0.0.0.0" # Optional, defaults to "0.0.0.0"
    protocol: tcp # Optional, defaults to tcp
- role: worker

En este fichero, podemos ver cómo tendremos dos nodos, el de administración y el que nos permitirá el despliegue de los contenedores. Podemos indicar tantos como queramos teniendo en cuenta los requisitos de nuestra máquina. También indicamos que vamos a exponer el puerto 30000 (el primero disponible ya que no es posible indicar puertos inferiores) desde el cluster hacia fuera. Ya veremos más adelante porque. 

Si queremos arrancar nuestro cluster debemos ejecutar este comando:

kind create cluster --config kind-config.yaml --name kind-basic

Y si queremos destruirlo, este otro:

kind delete cluster --name kind-basic

Lo siguiente será preparar los ficheros de configuración de los objetos de Kubernetes. Aunque hay más, por ahora solo necesitamos saber de estos:

  • Pods: Son las unidades desplegables más pequeñas que se pueden crear y gestionar en Kubernetes. Y que pueden estar compuesto por un solo contenedor, en un caso de uso común, o por varios.
  • Deployment: Es un objeto que describe cómo se debe implementar y actualizar una aplicación en el clúster
  • Service: Es un objeto que describe cómo se accede a las aplicaciones y que puede describir puertos y balanceadores de carga asociados al mismo.

Sabiendo esto procederemos a configurar por un lado un Deployment, el cual nos permitirá indicar que aplicación queremos desplegar, su versión, cuantas replicas desplegar en el cluster. Y un sinfín de posibilidades más. En este fichero declararemos el nombre y etiqueta asociada a nuestro deployment, y lo que es más importante, cuantas replicas queremos desplegar y cual es la imagen que debemos utilizar. 

apiVersion: apps/v1
kind: Deployment
metadata:
name: ms-k8s
labels:
app: ms-k8s
spec:
replicas: 2
selector:
matchLabels:
app: ms-k8s
template:
metadata:
labels:
app: ms-k8s
spec:
containers:
- name: ms-k8s
imagePullPolicy: IfNotPresent
image: deesebc/ms-k8s:1.0.0-SNAPSHOT

Por  último debemos configurar el servicio. A través del cual podremos exponer nuestra aplicación al exterior. Hasta aquí la configuración no tenia dificultad, mas allá de conocer cómo Kubernetes necesita que se configuren sus objetos. Pero para el servicio necesitamos saber qué tipos de servicios existen y cual es el que requerimos para nuestro ejemplo. 

  • ClusterIP: Opción por defecto. Expone el Service en una dirección IP interna del clúster asociado a un determinado puerto, también interno. 
  • NodePort: Permite exponer el Service asociado en cada IP del nodo en un puerto estático. Permitiéndonos acceder desde forma externa a través de dicho puerto. 
  • LoadBalancer: Permite exponer el servicio a través de un LoadBalancer de un proveedor en la nube. La opción a utilizar para producción. 
  • ExternalName: Funciona de forma similar a cualquier otro tipo, pero accedes a ese servicio en vez de devolver la IP asociada al mismo, devuelve el registro CNAME con el valor indicado. 
Por tanto, y teniendo en cuenta que queremos que sea accesible desde el exterior, y que al ser un ejemplo a desplegar de forma local. La opción de configuración necesaria para nuestro ejemplo es el NodePort. Asociándolo al puerto de salida que indicamos anteriormente en la creación del cluster, el 30000. 

apiVersion: v1
kind: Service
metadata:
name: ms-k8s-service
spec:
type: NodePort
selector:
app: ms-k8s
ports:
- protocol: TCP
port: 8080
targetPort: 8080
nodePort: 30000

No hace falta tener las dos configuraciones separadas, podemos incluirlas en el mismo fichero siempre que tengamos una linea con el texto '---' entre ambas configuraciones. Y si queremos desplegarlas en nuestro cluster, debemos ejecutar el comando:

kubectl apply -f configuration.yaml

Ahora solo nos quedará probarlo, para ello debemos ejecutar el siguiente comando: 

curl --location 'http://localhost:30000/q/health/live'      

Y esto ha sido todo, amigos. Espero que os haya servido para tener un conocimiento mínimo de Kubernetes y de cómo realizar una configuración básica. 

domingo, 7 de enero de 2024

Wiremock y Quarkus: Como configurarlo

Wiremock es una gran herramienta que puede ayudarte en la realización de pruebas de calidad de tu código. Un paso fundamental a la hora de desarrollar pruebas de aplicaciones o servicios que invocan a otros. Las pruebas son un paso fundamental en el desarrollo de software. Y Quarkus es un framework Java diseñado especialmente para el desarrollo de aplicaciones en contenedores, sin servidor y en la nube, especialmente para Kubernetes. De ambos hemos hablado varias veces ya.

Hoy veremos cómo podemos usarlos juntos, puesto que su uso no es tan sencillo, como pudiera ser con otra aplicación realizada con otro framework Java. Y para lograrlo haremos uso de la anotación @QuarkusTestResource. En una clase de testing debemos añadir dicha anotación asociada a una clase que implemente a su vez la interfaz QuarkusTestResourceLifecycleManager. Esto nos permitirá inicializar un servicio externo antes de la ejecución de las propias pruebas. En nuestro caso, nos permitirá arrancar el servidor Wiremock.

public class WiremockTestResource implements QuarkusTestResourceLifecycleManager {
  public static WireMockServer wireMockServer;
  @Override
  public Map<String, String> start() {
    wireMockServer = new WireMockServer(57001);
    wireMockServer.start();
    return new HashMap<String, String>();
  }
  @Override
  public void stop() {
    if (wireMockServer != null) {
      wireMockServer.stop();
    }
  }
}

Una vez creada la clase que permite arrancar Wiremock, haremos uso de ella a través de la anotación @QuarkusTestResource. De esta forma ya solo nos quedará indicar los stubs asociados a nuestros tests.

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class) //Resource that call external service
@QuarkusTestResource(WiremockTestResource.class)
public class WiremockResourceBasicTest {
  @Test
  public void getById() {
    Log.info("WiremockResourceBasicTest - getById");
    ObjectMapper mapper = new ObjectMapper();
    Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
    Log.info("WM port: " + WiremockTestResource.wireMockServer.getOptions().portNumber());
    WiremockTestResource.wireMockServer.stubFor(
      WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse()
      .withJsonBody(mapper.valueToTree(movie))
      .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));
    when().get("/1").andReturn().then().statusCode(200)
      .body("id", is(1)).body("name", is("Dune")).body("director",
      is("Denis Villeneuve"));
  }
}

Como veis es muy sencillo. Ahora veremos una opción un poco más complejas pero que permitirá crear el servidor de Wiremock con más opciones de configuración y modificarlas dinámicamente. Aunque el desarrollo es un poco lioso, debido a que tiene referencias cruzadas. 

Por un lado crearemos una anotación que tenga las propiedades que queramos modificar dinámicamente posteriormente a través de la propia anotación. Pero también le añadiremos la anotación @QuarkusTestResource que hará referencia a la clase que controla el servidor de Wiremock. 

@QuarkusTestResource(value = WiremockResourceConfigurable.class, restrictToAnnotatedClass = true)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WiremockTestAnnotation {
    String port() default "57005";
}

Ahora mostraremos la clase que configura el servidor de Wiremock. Será similar al ejemplo anterior, pero con tres añadidos. Para empezar, la interfaz que vamos a implementar será QuarkusTestResourceConfigurableLifecycleManager, y está estará asociada a la anotación creada anteriormente. Luego, a través de los atributos de la anotación, en el método init, podremos configurar dinámicamente el servidor. Y por último, con el retorno del método start, también podremos sobre escribir valores del fichero application.properties con los valores dinámicos. En este caso, a través de esta sobre escritura, podremos modificar el puerto del servicio externo pre configurado para el cliente.

public class WiremockResourceConfigurable 
  implements QuarkusTestResourceConfigurableLifecycleManager<WiremockTestAnnotation> {	
    public static WireMockServer server;
    private String port;
    @Override
    public void init(WiremockTestAnnotation params) {
        port = params.port();
    }
    @Override
    public Map<String, String> start() {
	server = new WireMockServer(Integer.valueOf(port));
        server.start();
	return  Map.of(
          "quarkus.rest-client.\"com.home.example.service.ExternalService\".url", 
          "http://localhost:"+port
          );
    }
    //...
}

Ya solo nos quedaría mostrar el test, haciendo uso de la anotación que nos permitiría modificar el puerto del servidor wiremock y del servicio externo dinámicamente. Muy similar al anterior. 

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
@WiremockTestAnnotation(port = "57005")
public class WiremockResourceTest {
    @Inject
    @ConfigProperty(name = "quarkus.rest-client.\"com.home.example.service.ExternalService\".url")
    private String serverUrl;
    @Test
    public void getById() {
        Log.info("serverUrl: " + serverUrl);
        ObjectMapper mapper = new ObjectMapper();
        Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
        WiremockResourceConfigurable.server.stubFor(
            WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse().withJsonBody(mapper.valueToTree(movie))
	    .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));	
        when().get("/1").andReturn().then().statusCode(200)
            .body("id", is(1)).body("name", is("Dune")).body("director",is("Denis Villeneuve"));
    }
}

Con esto hemos visto una particularidad más para mejorar los test en Quarkus. Y aunque Wiremock es una gran herramienta que desde hace tiempo es muy util para el desarrollo de pruebas. No quisiera omitir, que también es posible mockear el servicio externo de una forma sencilla con Quarkus.  Y esto es posible haciendo uso de las anotaciones @InjectMock y @RestClient. En el ejemplo que mostramos a continuación, ExternalService ha sido todo el tiempo el cliente web que hemos mockeado con Wiremock.  

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
public class WiremockResourceInjectTest {
    @InjectMock
    @RestClient
    ExternalService extService;
    @Test
    public void getById() {
        Mockito.when(extService.getMovieById(Mockito.anyLong())).thenReturn(new Movie(1L, "Denis Villeneuve", "Dune"));
        when().get("/1").andReturn().then().statusCode(200).body("id", is(1)).body("name", is("Dune")).body("director",
            is("Denis Villeneuve"));
    }
}

Espero que como siempre haya sido util, y si quieres puedes ver el código aquí

domingo, 14 de mayo de 2023

Quarkus & Microprofile: Como hacer un Microservicio con persistencia

 Hoy vamos a ver una nueva forma de hacer un microservicio. Esta vez utilizando Quarkus y Microprofile. Además le añadiremos una pequeña capa de persistencia. 

Quarkus ya lo hemos visto anteriormente integrado con Apache Camel, aquí, pero hoy lo veremos de forma individual. Quarkus es un framework Java pero Kubernetes-native, es decir optimizado para su uso en Graalvm y su despliegue a través de contenedores. Y como es el primero que hacemos de Quarkus en solitario, empezaremos indicando como podemos crear un proyecto, para ello usaremos el comando:

mvn com.redhat.quarkus.platform:quarkus-maven-plugin:2.13.5.Final-redhat-00002:create \
    -DprojectGroupId=com.home.example -DprojectArtifactId=ms-with-panache

Y las librerías principales a utilizar serán:

  • REST: quarkus-resteasy-reactive
  • Hibernate & Panache: quarkus-hibernate-orm-panache
  • JSON: quarkus-resteasy-reactive-jackson
Si tenéis experiencia en microservicios con Spring o JAX-RS, la parte de la generación del microservicio será sencilla. Pero aún así daremos una pequeña explicación de cómo poder generar cualquier método.
  • @Path("/book"): Nos permite indicar cual es la raiz del microservicio, en que contexto atiende a llamadas. También se usa en cada uno de los recursos, si tiene un contexto posterior diferente.
  • @GET, @DELETE... : Indica el verbo HTTP con el cual realizaremos la invocación.
  • @QueryParam : Nos permite indicar parámetros que viajan en la URL
  • @PathParam: Nos permite indicar parámetros que forman parte del contexto. Han debido indicarse en la anotación @Path. 
  •  @Produces y @Consumes: Nos permite indicar el formato de la salida y de lo que espera de entrada. 
Para la parte de la persistencia utilizaremos Panache, que pondrá a nuestra disposición dos patrones distintos para llevarla a cabo. Por un lado tendremos el patrón Repository, donde crearemos una clase intermedia que será la encargada de realizar las operaciones contra la base de datos similar a Spring Data.

@ApplicationScoped
@Transactional
public class BookRepository implements PanacheRepository<Book>{ }

Y por otro lado contaremos con el patrón Active Record, donde será la propia entidad la que pondrá a disposición del programador, los distintos métodos de persistencia. 

@Entity
@Table(name = "BOARDGAME")
public class BoardGame extends PanacheEntity {
    //warning with PanacheEntity we dont need getter/setter or Id 
    public String designer;
    public String name;    
}

A continuación un ejemplo de todo junto. En el repositorio Git podéis ver todo el código. 

@Path("/boardgame")
public class BoardGameResource {
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public BoardGame getBoardGameById(@PathParam("id") final Long id) {
        return BoardGame.findById(id);
    }
    //warning: if we don put @Transactional generates an error
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @Transactional
    public BoardGame persist(final BoardGame newBoardGame) {
        BoardGame.persist(newBoardGame);
        return newBoardGame;
    }
}

Para poder configurar las distintas características, lo podremos hacer a través del fichero application.properties podremos. Aquí indicamos algunas:
  • quarkus.http.port: Podemos indicar en que puerto admite las llamadas
  • quarkus.datasource.username/password/jdbc.url: Podemos indicar la conexión a la BBDD. 
  • quarkus.hibernate-orm: Tiene múltiples opciones para indicar si queremos o no que se genere el esquema de la BBDD al iniciar la aplicación y los ficheros para ello. 
Una vez que hemos desarrollado y configurado el proyecto, vamos a proceder a arrancarlo. Como estamos en desarrollo, utilizaremos el comando

mvn quarkus:dev

Otro punto a favor del uso de Quarkus, es que podemos recargar fácilmente la aplicación y comprobar los cambios, simplemente pulsando la tecla 's' en la consola donde lo tengamos ejecutando. 

Espero que os haya sido útil, como siempre. Y si queréis ver todo el código, está aquí

viernes, 12 de mayo de 2023

Testcontainers: Uso básico

Ya hace 3 años, sobre Agosto de 2020 vi un post sobre cómo realizar pruebas unitarias con contenedores. Y aunque tuve la intención de hacer un post, nunca llegue a hacerlo. Pero ahora tras ver cómo funcionan las pruebas con Quarkus, del cual intentaré hablar más adelante, me he interesado en sacar este post. 

En múltiples de los posts que he realizado, he utilizado siempre contenedores para poder realizar las pruebas, y poder comprobar el ejemplo práctico. Normalmente dejando un docker-compose.yml que permitiese levantar y configurar los sistemas externos con los cuales iba a trabajar la aplicación. 

Ahora vamos a hacer algo parecido Y para llevarlo a cabo utilizaremos Testcontainers. Es una librería Java que nos permite crear instancias de Docker y manejarlas en base a nuestro interés. En el ejemplo utilizaremos las siguientes librerías:

  • org.testcontainers:testcontainers:1.16.0
  • org.testcontainers:junit-jupiter:1.16.0
  • org.testcontainers:mysql:1.16.0
  • org.apache.camel:camel-test-spring-junit5:3.16.0
  • org.springframework.boot:spring-boot-starter-test:2.5.1

Para empezarla a utilizar, usaremos principalmente dos anotaciones:

  • @Testcontainers: que nos permite utilizar Testcontainers con JUnit 5.
  • @Containers: Nos permite indicar a las instancias de contenedores que queremos que sean gestionadas por Testcontainers. Aunque como veremos más adelante podemos utilizar Testcontainers sin ella. 
Antes de empezar debemos aclarar un poco el funcionamiento. Testcontainers nos permite levantar gestionar contenedores de cualquier imagen que queramos. Pudiéndole añadir a través de sus métodos una breve configuración. Pero también tiene módulos específicos para hacer uso de contenedores comunes, como puede ser de BBDD, Hashicorp, RabitMQ o Kafka.

Otro asunto importante a tener en cuenta, que aunque nosotros les indiquemos los puertos que queremos exponer, no podremos crear bindings tal y como se realiza en un docker-compose. Sino que los puertos expuestos serán enlazados con puertos aleatorios de nuestra máquina. Debido a esto, deberemos modificar antes de realizar las pruebas la configuración que tenemos por defecto. Para que podamos indicar los puertos que utilizarán los sistemas, una vez han sido arrancados con Testcontainers. 

Para ver cómo funciona, utilizaremos ejemplos hechos con Apache Camel y Spring Boot. En el primer ejemplo enviaremos y recibiremos mensajes de una cola de ActiveMQ. A continuación detallo los componentes más importantes del ejemplo:
  • La instancia de GenericContainer nos permitirá indicar la imagen de ActiveMQ a crear y los puertos a exponer.
  • Crear un método BeforeAll que permita arrancar el contenedor y el puerto vinculado externo. 
  • Modificar la configuración de los componentes que realizan la comunicación con el sistema externo. En Spring Boot lo podemos hacer fácilmente con un método anotado con @DynamicPropertySource.
@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = ApacheCamelTestApplication.class)
@Log4j2
public class ApacheActiveMqRouterTest {
    @Autowired
    ProducerTemplate producer;
    @Container
    private static GenericContainer container = new GenericContainer("rmohr/activemq").withExposedPorts(61616, 8161);
    private static Integer tcpPort;
    @BeforeAll
    public static void beforeAll() {
        container.start();
        tcpPort = container.getMappedPort(61616);
    }
    @DynamicPropertySource
    static void replaceProperties(DynamicPropertyRegistry registry) {
        registry.add("activemq.broker-url", () -> "tcp://localhost:" + tcpPort);
    }
    @Test
    public void amqTo01() throws InterruptedException {
        producer.sendBody("direct:SendToPublic", "mensaje ");
    }
}

En el segundo ejemplo, será similar en funcionamiento al anterior. Con la excepción de qué utilizaremos un módulo concreto de Testcontainers. Con él, a través de la clase MySQLContainer podremos hacer configuraciones específicas para crear el contenedor. Como indicar un script para inicializar la base de datos. 

@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = {ApacheCamelTestApplication.class})
@Slf4j
public class ApacheMySQLRouterTest{
    static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
    static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
    .withInitScript("scripts/init_mysql.sql")
    .withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
    @Autowired
    ProducerTemplate producer;
@BeforeAll public static void beforeAll() throws IOException { database.start(); } @DynamicPropertySource static void databaseProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", database::getJdbcUrl); registry.add("spring.datasource.username", database::getUsername); registry.add("spring.datasource.password", database::getPassword); } @Test public void getBookById() throws InterruptedException { producer.sendBodyAndHeader("direct:getBookById", null, "id", 1); } }

Antes de terminar dos cosas. El primero, es que durante la realización de los ejemplos he tenido problemas con distintas versiones de Testcontainers: 
  • 1.18: Genero la excepción NoSuchMethodError asociada al método optionallyMapResourceParameterAsVolume. 
  • 1.17: Genero ClassNotFoundException: asociado a la clase org.testcontainers.shaded.org.apache.commons.lang.StringUtils
Y lo segundo, es que sí debemos utilizar un varios contenedores como es en este caso podemos tener errores debido a que nuestras aplicación se intente conectar a un sistema externo y no tengamos su contenedor levantado. Por tanto como recomendación, podemos crear una clase que contenga toda la configuración de los contenedores y de la que extenderemos. 

@SpringBootTest
@Testcontainers
@CamelSpringBootTest
@Slf4j
public class TestcontainersConf {
    public static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
    static GenericContainer<?> container = new GenericContainer<>("rmohr/activemq").withExposedPorts(61616, 8161);    
    static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
    .withInitScript("scripts/init_mysql.sql")
    .withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
    @BeforeAll
    public static void beforeAll() {
        container.start();
        database.start();
    }
    @DynamicPropertySource
    static void replaceProperties(DynamicPropertyRegistry registry) {
        registry.add("activemq.broker-url", () -> "tcp://localhost:" + container.getMappedPort(61616));
        registry.add("spring.datasource.url", database::getJdbcUrl);
        registry.add("spring.datasource.username", database::getUsername);
        registry.add("spring.datasource.password", database::getPassword);
    }
}
// ............................
public class ApacheMySQLRouterTest extends TestcontainersConf{
    @Autowired
    ProducerTemplate producer;
    @Test
    public void getBookById() throws InterruptedException {
        producer.sendBodyAndHeader("direct:getBookById", null, "id", 1);
    }
}

Espero que os haya ayudado, y si os interesa, aquí tenéis todo el código fuente. 

viernes, 3 de marzo de 2023

WSO2 Secret: Creación básica de claves

WSO2 permite la creación de claves que son manejadas por su propio Secure Vault. Una buena herramienta que nos permite el manejo de claves de forma segura e implementar buenas practicas. 

Para este ejemplo hemos utilizado la imagen: docker.wso2.com/wso2mi:1.2.0

Clave estáticas

Por un lado, las configuramos en el fichero de configuración, deployment.toml, sin encriptar. Deben ir entre comillas dobles y corchetes.

[secrets]
server_secret = "[secret_1]"
synapse_secret = "[secret_2]"

Si quieres activar dicha implementación en un entorno con VM lo podemos hacer ejecutando el comando:

sh <MI_HOME>/bin/ciphertool.sh -Dconfigure

Tras ejecutarlo, tendremos que indicar la clave de nuestro keystore. Que por defecto es, wso2carbon. Tras realizar este paso, si volvemos al fichero de configuración, podremos ver cómo las variables han sido encriptadas.

  • Como usarlas en fichero de configuración
Para ello solo tendremos que hacer referencias a las mismas, a través del alias que le dimos.

[keystore.primary]
password = "$secret{server_secret}"

  • Como usarlas en el contexto de synapse

En este caso, lo realizaremos a través del mediator property y el alias que le dimos

<property expression="wso2:vault-lookup('synapse_secret')" name="secret"/>

Clave dinámicas

Este es el funcionamiento básico, que nos ayuda a gestionar claves privadas, pero el manejo y creación de nuevas claves require el reinicio del servidor. Lo cual puede ser un inconveniente en determinados entornos. Por lo que la creación dinámica de estas contraseñas puede ser un punto fuerte. 

Podemos indicar claves dinámicas a través de variables del entorno o variables del sistema. Para ello primero necesitaremos encriptar las contraseñas, luego setear la variable y posteriormente configurarla.

Para encriptar la variable, podemos utilizar el Micro Integrator CLI o directamente ejecutar el comando:

sh <MI_HOME>/bin/ciphertool.sh

Este comando primero nos pedirá la clave del keystore y posteriormente la contraseña a encriptar, dos veces. 

Una vez echo esto, seteamos la variable. Por ejemplo, para almacenarla en el entorno, ejecutamos:

export env_secret=<ENCRYPTED_VALUE>

Y por último solo nos quedará configurarla. Ejemplo:

[secrets]
env_secret = "$env{env_secret}"

Ya podremos utilizar como vimos anteriormente en nuestros ficheros de configuración o en el código synapse. Ejemplo:

<log level="custom">
   <property name="MSG" value="secretExample_v1_reader_api - GET - /reader/ - init"/>
   <property name="keystore_pwd" expression="wso2:vault-lookup('keystore_pwd')" />
   <property name="env_secret" expression="wso2:vault-lookup('env_secret')" />
</log>

Obteniendo la siguiente salida

wso2mi    | [2022-06-15 10:15:15,424]  INFO {LogMediator} - {api:secretExample_v1_reader_api} 
    MSG = secretExample_v1_reader_api - GET - /reader/ - init, 
    keystore_pwd = wso2carbon, env_secret = envpwd

También podemos configurarlas a través de ficheros o secrets de Docker, con las cuales podremos aumentar el dinamismo de la configuración y utilización de las claves. 


lunes, 13 de febrero de 2023

Wiremock y JUnit5

Hoy vamos a ver un sencillo post sobre cómo configurar Wiremock con JUnit 5. Algo sencillo pero que cambia respecto a cómo era con JUnit 4 rules. Para nuestro ejemplo utilizaremos las siguientes versiones:

  • junit-jupiter-api:5.9.2
  • wiremock-jre8:2.35.0

Para empezar, si queremos utilizar Wiremock de forma muy básica, simplemente nos bastará con la anotación @WireMockTest. La cual nos permitirá modificar lo siguiente:

  • httpPort: Para indicar en que puerto podremos hacer llamadas HTTP.
  • httpsEnabled y httpsPort. Para indicar que queremos hacer llamadas HTTPS y en que puerto. 
  • proxyMode. Para emular un nombre de dominio distinto a localhost. En dicho caso y usando HTTPClient deberemos usar el método useSystemProperties a la hora de crear el cliente. 
A continuación podemos ver un ejemplo de un invocación a un nombre de dominio distinto a localhost y HTTPS. Para este ultimo ya no necesitaremos crear un certificado autoafirmado como lo hacía antiguamente. 

@Log4j2
@WireMockTest(httpsEnabled = true, httpsPort = 9090, proxyMode = true)
public class WiremockBasicTest {
  private static final String BEARER_TOKEN = "Bearer 77d9b8f0-fafe-3778-addf-2755bdc53c88";
  private static final String JSON_CONTENT = "{\"hellow\":\"world\"}";

  @Test
  public void doGetAndGetResponse_proxyMode() throws Exception{
    String sEndpoint = "https://mydomain.com:9090/sample";
    Map<String, String> headers = new HashMap<>();
    headers.put(HttpHeaders.AUTHORIZATION, BEARER_TOKEN);
    stubFor(get("/sample").withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo(BEARER_TOKEN))
        .withHost(WireMock.equalTo("mydomain.com"))
        .willReturn(aResponse().withBody(JSON_CONTENT).withStatus(200)));

    String body = null;
    HttpGet get = new HttpGet(sEndpoint);
    get.setHeaders(headers.entrySet().stream().map(entry -> new BasicHeader(entry.getKey(), entry.getValue())).toArray(Header[]::new));
    try (CloseableHttpClient httpClient = createAcceptSelfSignedCertificateClient(); CloseableHttpResponse response = httpClient.execute(get)) {
        body = EntityUtils.toString(response.getEntity(), Charset.defaultCharset());
    }
    assertThat(body, equalTo(JSON_CONTENT));
  }
}

Y aunque con muy poco ya podemos hacer mucho. Puede que haya casos en los que necesitemos un poco más de configuración. Realizar las invocaciones como hacíamos antes con los JUnit 4 rules y tener acceso al método wireMockConfig

Esto lo podremos hacer a través de las extensiones de JUnit5. Pero la instancia que creemos será la misma que debemos utilizar para crear los distintos stub. Lo beneficioso de este enfoque, es que además nos permite crear distintas instancias de la extensión y usar ambas. 

public class WiremockComplexTest {
  private static final String BEARER_TOKEN = "Bearer 77d9b8f0-fafe-3778-addf-2755bdc53c88";
  private static final String JSON_CONTENT = "{\"hellow\":\"world\"}";

  @RegisterExtension
  static WireMockExtension wme = WireMockExtension.newInstance()
      .options(wireMockConfig().httpsPort(9090).port(8085)
      .notifier(new ConsoleNotifier(true))).proxyMode(true).build();

  @Test
  public void doGetAndGetResponse_proxyMode() throws Exception {
    String sEndpoint = "https://mydomain.com:9090/sample";
    Map<String, String> headers = new HashMap<>();
    headers.put(HttpHeaders.AUTHORIZATION, BEARER_TOKEN);
    wme.stubFor(get("/sample").withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo(BEARER_TOKEN)).withHost(WireMock.equalTo("mydomain.com"))
        .willReturn(aResponse().withBody(JSON_CONTENT).withStatus(200)));

    String body = null;
    HttpGet get = new HttpGet(sEndpoint);
    get.setHeaders(headers.entrySet().stream().map(entry -> new BasicHeader(entry.getKey(), entry.getValue())).toArray(Header[]::new));
    try (CloseableHttpClient httpClient = createAcceptSelfSignedCertificateClient(); CloseableHttpResponse response = httpClient.execute(get)) {
      body = EntityUtils.toString(response.getEntity(), Charset.defaultCharset());
    }
    assertThat(body, equalTo(JSON_CONTENT));
  }
}

Por último, si este enfoque es el ideal para tus pruebas. Pero no quieres estar indicando la instancia en todos los métodos de WireMock. Podemos indicar que la instancia la cree de forma estática y ya si que sería de la misma forma que cuando usábamos los JUnit 4 rules. Para ello solo tendríamos que utilizar el método configureStaticDsl(true)

@RegisterExtension
static WireMockExtension wme = WireMockExtension.newInstance()
    .options(wireMockConfig().httpsPort(9090).port(8085)
    .notifier(new ConsoleNotifier(true)))
    .configureStaticDsl(true).proxyMode(true).build();

Espero que este post haya sido útil y os ayude en la actualización de vuestro software de pruebas.