En la primera parte de Spring Cloud Consul vimos qué es Consul, cómo se puede utilizar para descubrir microservicios y cómo montar un sistema entre Consul y Spring Cloud para hacer uso de esta funcionalidad.

En esta segunda entrega, analizaremos la capacidad de Consul de almacenar pares clave/valor y su integración con Spring Cloud, lo que proporciona la potencia suficiente para gestionar la configuración de forma centralizada.

También veremos cómo funciona Consul KV Store y cómo interactuar con él. Para ello, aprovecharemos el ejemplo del artículo anterior, tanto para ver un servicio externo como uno registrado en Consul y recuperar así su configuración sin tenerla almacenada en el propio servicio usando Spring Cloud, y que ésta configuración sea gestionada desde un repositorio Git para tenerla versionada y controlada.

Al final del artículo dejaré enlaces al código fuente generado para que se descargue si se quiere jugar con él.

Consul KV Store

Para definir qué proporciona la KV Store de Consul vamos a recurrir a su web oficial:

Consul provides an easy to use KV store. This can be used to hold dynamic configuration, assist in service coordination, build leader election, and enable anything else a developer can think to build.

La estructura de almacenamiento de esta información es en árbol y sus diferentes nodos se representan con una barra* ‘ / ‘**;* así que, por ejemplo, para dar de alta una hoja en el nodo *app *se representa app/name. Hay que tener en cuenta que los nodos también son ítems en el KV store, así que los paths app/name y app/name/ son diferentes y ambos tienen un valor diferente.

Cuando se hace una solicitud de almacenamiento de KV a Consul, este, internamente, inserta la información en BoltDB, una BD embebida para Go (el lenguaje en el que está escrito Consul); la cual, además de las KV, almacena toda la información que necesita persistencia y los logs.

El tamaño máximo aceptado para un valor a almacenar es 512kb. En la versión enterprise de Consul se puede configurar el agente para que eventualmente haga volcados de la información en snapshots, pero para el resto de versiones existen comandos para hacerlo de forma manual. Cuando hace el snapshot trunca el log de la BD.

Si se añade un nodo servidor al cluster de Consul, se replica la información a su BD local. Al igual que con el descubrimiento de servicios, se puede atacar directamente a un agente Consul server o a un Consul client, el cual redirigirá la solicitud al cluster de Consul servers.

Cada datacenter tiene su propio almacenamiento K/V y no comparte esta información con otros datacenter por defecto. El proyecto Consul Replicate permite manejar la configuración desde un datacenter central, haciendo una replicación de la información de forma asíncrona y robusta contra errores de red.

Consul ofrece tres formas de interactuar con su funcionalidad de almacenamiento K/V, mediante la UI, con un cliente específico (Consul KV CLI) y su API HTTP.

Consul UI

La interfaz gráfica de Consul permite la creación, modificación, consulta y eliminación de pares clave/valor de una forma muy sencilla e intuitiva. Además, permite comprobar, en el caso de que así sea, que el JSON que se incluye en el valor de la clave tenga un formato válido.

Consul KV CLI

Expone comandos de alto nivel para interactuar contra el KV store de Consul. La sintaxis es la siguiente: consul kv [opciones] [argumentos].

Los comandos que acepta son los siguientes:


 ./consul kv put -http-addr=172.17.0.2:8500 app/name prueba


 ./consul kv get -http-addr=172.17.0.2:8500 app/name


 ./consul kv delete -http-addr=172.17.0.2:8500 app/name


 ./consul kv export -http-addr=172.17.0.2:8500 app/name > keys.json


 ./consul kv import -http-addr=172.17.0.2:8500 @keys.json

Además, tiene una amplia lista de opciones para parametrizarlos. Algunas de ellas son generales de Consul y comunes a todos los comandos, como por ejemplo -http-addr o -datacenter (dirección y puerto del agente consul que se va a consultar y datacenter al que enviar las peticiones respectivamente).

O específicas del comando como puede ser -recurse en el borrado (elimina de forma recursiva las claves indicadas) o -flags en la creación (valor de formato entero que Consul ignora, puede ser usado para incluir metadatos por los clientes).

API HTTP

El API HTTP permite hacer operaciones CRUD sobre el KV Store, pero además amplía las capacidades que ofrecía el CLI e incluye la posibilidad de hacer operaciones con múltiples claves dentro de una única transacción atómica. Lo que provoca que si alguna de estas operaciones falla, se haga rollback de toda la operativa.

No ofrece la posibilidad de exportar ni importar las claves a JSON directamente como el CLI, lo más parecido a esta funcionalidad son los snapshots, pero en este caso crea una fotografía de la BD completa, no solo de los KV que se le indiquen.

Otra posibilidad es utilizar el endpoint transaccional txn, pero el formato JSON que espera es diferente al generado con una petición GET.

A continuación se listan los verbos HTTP que expone el API:


 curl http://172.17.0.2:8500/v1/kv/prueba


 curl --request PUT --data @prueba.json http://172.17.0.2:8500/v1/kv/prueba


 curl --request DELETE http://172.17.0.2:8500/v1/kv/prueba?recurse=true

Al igual que en el CLI, se pueden parametrizar las solicitudes indicando éstos parámetros como query params, como se ve en alguno de los ejemplos.

Después de ver los tipos de interacciones con la KV de Consul, hay tener en cuenta que la UI de Consul no permite dar valores a keys que sean *folders (*por ejemplo *app/name/) *ni recuperar información de ellos.

Si se intenta crearlo lo que hace es generar una clave con un valor nulo asociado:

En cambio, mediante Consul KV CLI o API HTTP, sí se permite esta operativa, pudiendo asociar valores y consultarlos de manera normal:

Aprovechando las inserciones del ejemplo, vamos a consultar directamente el fichero de la base de datos del Consul server y buscar uno de los valores introducidos.

Al arrancar el docker del Consul server hemos indicado el directorio /tmp para que persista la información -data-dir=/tmp. Navegamos al directorio /tmp/raft/raft.db, que es donde está localizada la BD y consultamos con el comando strings los textos que se han introducido:

Como se puede apreciar, en la primera ejecución de *strings *se lista dos veces pruebafolder. Esto es porque la BD almacena además de los K/V los logs de Consul, y al haber insertado ese valor está persistida también la traza de ese comando en los logs.

Posteriormente he hecho varias inserciones y vuelto a ejecutar el comando y ya solamente sale una vez. Esto es debido a que el log tiene un tamaño máximo y esa línea se ha sobreescrito.

Spring Cloud Consul: Consul Configuration

Como se comentó en el primer post, Spring ha implementado librerías para utilizar esta funcionalidad de Consul de forma muy sencilla, la cual supone una buena alternativa a Config Server y Client en la gestión de la configuración centralizada de microservicios Spring Cloud.

Para dotar a una aplicación Spring Boot de estas características de una forma rápida, lo único que hay que hacer es incluir la siguiente dependencia en el proyecto:

groovy

 compile('org.springframework.cloud:spring-cloud-starter-consul-config')

Y configurar la dirección del agente Consul que va a servir de puerta de entrada al cluster Consul para que resuelva las propiedades:


 spring:
  cloud:
    consul:
    host: 172.17.0.2
    port: 8500

Con esta configuración básica y dando de alta propiedades en Consul con el siguiente path ‘/****config/application/’, simplemente con hacer uso de la anotación @Value de Spring Beans se recupera la información:


 @Value("${key_1}")
 private String key1;

Esto es así porque Spring Cloud Consul tiene config como path por defecto para recuperar propiedades y además config/application como path de las propiedades comunes.

En un sistema complejo es inmanejable un único path con la configuración de todos los microservicios, así que Spring Cloud Consul Configuration permite separar por aplicación y perfilar las propiedades, siendo esto último muy útil para separar por entornos.

Al igual que Cloud Config mantiene una jerarquía de priorización de configuraciones. En caso de tener repetida una clave en varios paths, será la de mayor prioridad el valor asociado al nombre de la aplicación y que disponga de un perfil igual al indicado al arranque:


 config/<spring.application.name>,<spring.profile.active>/key
 config/<spring.application.name>/key
 config/application,<spring.profile.active>/key
 config/application/key

Internamente, Spring Cloud Consul Config genera múltiples *PropertySource *con los diferentes paths que aplican al servicio arrancado y posteriormente los inyecta en el contexto de Spring para que los resuelva:

Refresco de propiedades

Las propiedades se cargan en la fase de bootstrap al arranque del microservicio. Esto quiere decir que si se cambia algún valor en el Consul KV los cambios no llegan a repercutir en caliente sobre el mismo, siendo necesario un reinicio.

En cambio, Spring Cloud Consul proporciona una forma de solucionarlo: se apoya en los watches de Consul que se encargan de monitorizar cambios de datos lanzando queries bloqueantes contra el API HTTP.

Si existe un cambio en las propiedades, publica un evento RefreshEvent que refresca el contexto de la aplicación:

Para poder hacer uso de esta funcionalidad y tener los últimos cambios de configuración en caliente, la aplicación tiene que tener las clases que resuelven propiedades anotadas con @RefreshScope y además se debe incluir la dependencia con actuator:

groovy

 compile('org.springframework.boot:spring-boot-starter-actuator')

Ya que para que se instancie la clase de Spring Cloud Consul que usa los watches, debe haber un bean RefreshEndpoint en el classpath y actuator se encarga de ello:


 @Configuration
 @ConditionalOnClass(RefreshEndpoint.class)
 protected static class ConsulRefreshConfiguration {
    @Bean
    @ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled", matchIfMissing = true)
    public ConfigWatch configWatch(ConsulConfigProperties properties,
            ConsulPropertySourceLocator locator, ConsulClient consul) {
        return new ConfigWatch(properties, consul, locator.getContextIndexes());
    }
 }

Se puede modificar la frecuencia con que se invocan a Config Watch y el tiempo que se tiene bloqueada la query de watch, además de poder deshabilitar esta funcionalidad:


 spring:
   cloud:
    consul:
      config:
        watch:
          wait-time: 10 # Tiempo de bloqueo de peticion de watch
          delay: 1000 # Frecuencia de invocacion a Config Watch
          enabled: true # Activa o desactiva la funcionalidad consul watch

Configuración

Para arrancar una aplicación de forma rápida pueden servir los valores por defecto, pero si se quiere personalizar Spring permite hacerlo con variables de configuración.

A continuación se muestran algunas de ellas:


 spring:
   cloud:
    consul:
      config:
        prefix: apps # Cambia el path por defecto config de las propiedades
        default-context: default # Cambia el path para configuraciones comunes application
        enabled: true # Activa o desactiva la funcionalidad de configuracion centralizada
        fail-fast: true # Lanza error si Consul no está disponible para servir configuracion
        profile-separator: ; # cambia el separador de perfiles por defecto <,>
        name: consulconfig # Alternativa a spring.application.name para recuperar propiedades

En caso de incluir las propiedades de sobreescritura de paths por defecto, Spring Cloud Consul da prioridad a éstas sobre las generales. Es decir, si incluyes la propiedad name, recuperará las propiedades por el name indicado, no por el nombre del servicio aunque este esté informado también.

Hay que tener cuidado al definir estas variables, ya que no es lo mismo /apps que apps y ocurriría un error al cargar las propiedades.

Almacenamiento de claves multivalor

En el apartado anterior se ha visto la manera de gestionar claves con un único valor asociado incluido a mano en el KV.

Si se sigue esta política, en el Consul KV habría un par de claves/valor por cada propiedad de la aplicación. En este caso, se podrían dar de alta todas las propiedades importándolas mediante el Consul CLI con un fichero JSON para agilizar su creación.

Pero Spring Cloud Consul nos proporciona la posibilidad de incluir el listado de todas las propiedades que aplican en una única clave, lo que se hace más sencillo de mantener.

Por defecto esta clave única es data y mantiene la jerarquía de prioridades vista anteriormente:


 config/<spring.application.name>,<spring.profile.active>/data
 config/<spring.application.name>/data
 config/application,<spring.profile.active>/data
 config/application/data

En el microservicio habría que indicar el formato de la información de esa clave única, el cual puede ser YAML o PROPERTIES.

También se puede cambiar la clave por defecto:


 spring:
   cloud:
     consul:
       config:
         format: YAML #Formato de la informacion para cargar la configuración
         data-key: multi #Cambia la clave por defecto data para los casos de claves multivalor

Para dar de alta las propiedades se puede hacer desde la UI de Consul o atacando al API HTTP.

A continuación se expone un ejemplo haciendo uso del API con la personalización de la configuración que se ha aplicado arriba:


 curl -X PUT -H "Content-Type: multipart/form-data;" -d ```

Ya hemos conseguido tener las propiedades de una aplicación centralizadas en un único *path*, pero podemos ir un paso más allá y tenerlas separadas en ficheros físicos y versionadas en un repositorio git, simulando el comportamiento de un *Config Server*. 

Para ello, haremos uso de un proyecto llamado [git2consul](https://github.com/breser/git2consul/) que, dados ciertos ficheros con la configuración de la aplicación contenida tanto en YML o PROPERTIES, los convierte por defecto en claves con los nombres de dichos ficheros y el valor la información que contienen. 

Para ello hay que:

- **Indicar en el fichero bootstrap de la aplicación Spring Boot que el tipo de formato de recogida de propiedades es fichero**:

ruby


spring:

cloud:

consul:

config:

format: FILES


Hay que tener en cuenta que para que recoja las propiedades por perfiles, en el modo FILES se ignora la configuración *profile-separator *y hace la búsqueda por guión ‘ **- ‘**.

- **Generar un JSON con las propiedades de arranque de git2consul**, especificando la ruta al repositorio donde estará la información y la rama de la que se debe descargar:

javascript


{

"version": "1.0",

"repos" : [{

"name" : "consul_config_example",

"url" : "https://github.com/edu-paradigma/consul_config_example.git",

"branches" : ["master"],

"ignore_repo_name" : true,

"include_branch_name" : false,

"hooks": [{

"type" : "polling",

"interval" : "1"

}]

}]

}


Por defecto el path que se genera es **//**. Como queremos que el *path* sea directamente el contenido del repositorio, se han incluido dos parámetros de configuración de git2consul, **ignore_repo_name e include_branch_name**. 

Además, se ha definido un intervalo de polling al repositorio de un minuto, que hace que git2consul esté preparado a cambios de configuración en caliente y refresque el KV de Consul.

- **Arrancar un agente Consul servidor**:

bash


docker run --name=consul-server consul:0.9.3 consul agent -dev -client 172.17.0.2 -bind 172.17.0.2


- **Arrancar git2consul**. Para ello haremos uso de un contenedor docker, indicando la ruta del agente Consul destino y el fichero de configuración de git2consul. Habrá que montar un volumen en el contenedor para que pueda acceder a dicho fichero:

bash


docker run -v /home/edu/wks/consul_config_example/git2consul-config:/etc/git2consul.d cimpress/git2consul --endpoint 172.17.0.2 --port 8500 --config-file /etc/git2consul.d/config_git2consul.json


Una vez hecho esto, ya tenemos cargadas todas las propiedades para que los servicios las consuman. 

Cabe destacar que git2consul genera ciertas propiedades en el KV de Consul que le son necesarias para funcionar, no solo incluye el contenido del repositorio.

<article class="block block-image  -inside-grid "><a href="https://www.paradigmadigital.com/wp-content/uploads/2017/12/consul-7.png"target=""><img src="https://www.paradigmadigital.com/assets/img/defaults/lazy-load.svg"
          data-src="https://www.paradigmadigital.com/wp-content/uploads/2017/12/consul-7.png"
          data-srcset="https://www.paradigmadigital.com/wp-content/uploads/2017/12/consul-7.png 1920w,https://www.paradigmadigital.com/wp-content/uploads/2017/12/consul-7.png 1280w,https://www.paradigmadigital.com/wp-content/uploads/2017/12/consul-7.png 910w,https://www.paradigmadigital.com/wp-content/uploads/2017/12/consul-7.png 455w"
          class="lazy-img"  
                  sizes="(max-width: 767px) 80vw, 75vw"
                  alt="" title="undefined"/></a></article>


Con esto terminamos la segunda entrega de Consul y su uso con Spring Cloud. Como se ha podido ver, Consul es una muy buena herramienta que puede plantar cara a las opciones que ofrece Netflix en cuanto a descubrimiento de servicios (Eureka) y configuración centralizada (Config Server y Config Client). 

Adjunto debajo los enlaces a github con el código usado para escribir los artículos:

- [Consul-request-service](https://github.com/paradigmadigital/consul-request-service): microservicio de solicitudes que no está registrado en Consul y que recupera su configuración a través de él.

- [Consul-response-service](https://github.com/paradigmadigital/consul-response-service): microservicio de respuestas registrado en Consul y que recupera su configuración a través de él.
- [Consul-config-example](https://github.com/paradigmadigital/consul_config_example): contiene la configuración de los microservicios a cargar con git2consul y el JSON de configuración del propio git2consul.key_1: value_1\nkey_2: value_2' 'http://172.17.0.2:8500/v1/kv/apps/default;local/multi'

Ya hemos conseguido tener las propiedades de una aplicación centralizadas en un único path, pero podemos ir un paso más allá y tenerlas separadas en ficheros físicos y versionadas en un repositorio git, simulando el comportamiento de un Config Server.

Para ello, haremos uso de un proyecto llamado git2consul que, dados ciertos ficheros con la configuración de la aplicación contenida tanto en YML o PROPERTIES, los convierte por defecto en claves con los nombres de dichos ficheros y el valor la información que contienen.

Para ello hay que:

ruby--md-var-new-line---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line- spring--md-var-colon-symbol---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-   cloud--md-var-colon-symbol---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-     consul--md-var-colon-symbol---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-       config--md-var-colon-symbol---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-         format--md-var-colon-symbol- FILES--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-

Hay que tener en cuenta que para que recoja las propiedades por perfiles, en el modo FILES se ignora la configuración *profile-separator *y hace la búsqueda por guión ‘ - ‘.

javascript--md-var-new-line---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line- --md-var-opening-curly-brace---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-  &quot;version&quot;--md-var-colon-symbol- &quot;1--md-var-dot-symbol-0&quot;,--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-  &quot;repos&quot; --md-var-colon-symbol- --md-var-opening-squarebracket---md-var-opening-curly-brace---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-    &quot;name&quot; --md-var-colon-symbol- &quot;consul--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-config--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-example&quot;,--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-    &quot;url&quot; --md-var-colon-symbol- &quot;https--md-var-colon-symbol---md-var-slash---md-var-slash-github--md-var-dot-symbol-com--md-var-slash-edu--md-var-dash-paradigma--md-var-slash-consul--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-config--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-example--md-var-dot-symbol-git&quot;,--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-    &quot;branches&quot; --md-var-colon-symbol- --md-var-opening-squarebracket-&quot;master&quot;--md-var-closing-squarebracket-,--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-    &quot;ignore--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-repo--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-name&quot; --md-var-colon-symbol- true,--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-    &quot;include--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-branch--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-name&quot; --md-var-colon-symbol- false,--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-    &quot;hooks&quot;--md-var-colon-symbol- --md-var-opening-squarebracket---md-var-opening-curly-brace---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-      &quot;type&quot; --md-var-colon-symbol- &quot;polling&quot;,--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-      &quot;interval&quot; --md-var-colon-symbol- &quot;1&quot;--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-    --md-var-closing-curly-brace---md-var-closing-squarebracket---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-  --md-var-closing-curly-brace---md-var-closing-squarebracket---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line- --md-var-closing-curly-brace---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-

Por defecto el path que se genera es //. Como queremos que el path sea directamente el contenido del repositorio, se han incluido dos parámetros de configuración de git2consul, ignore_repo_name e include_branch_name.

Además, se ha definido un intervalo de polling al repositorio de un minuto, que hace que git2consul esté preparado a cambios de configuración en caliente y refresque el KV de Consul.

bash--md-var-new-line---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line- docker run --md-var-dash---md-var-dash-name--md-var-equal-consul--md-var-dash-server consul--md-var-colon-symbol-0--md-var-dot-symbol-9--md-var-dot-symbol-3 consul agent --md-var-dash-dev --md-var-dash-client 172--md-var-dot-symbol-17--md-var-dot-symbol-0--md-var-dot-symbol-2   --md-var-dash-bind 172--md-var-dot-symbol-17--md-var-dot-symbol-0--md-var-dot-symbol-2--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-
bash--md-var-new-line---md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line- docker run --md-var-dash-v --md-var-slash-home--md-var-slash-edu--md-var-slash-wks--md-var-slash-consul--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-config--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-example--md-var-slash-git2consul--md-var-dash-config--md-var-colon-symbol---md-var-slash-etc--md-var-slash-git2consul--md-var-dot-symbol-d cimpress--md-var-slash-git2consul --md-var-dash---md-var-dash-endpoint 172--md-var-dot-symbol-17--md-var-dot-symbol-0--md-var-dot-symbol-2 --md-var-dash---md-var-dash-port 8500 --md-var-dash---md-var-dash-config--md-var-dash-file --md-var-slash-etc--md-var-slash-git2consul--md-var-dot-symbol-d--md-var-slash-config--md-var-dash---md-var-dash-md--md-var-dash-var--md-var-dash-lodash--md-var-dash-git2consul--md-var-dot-symbol-json--md-var-lower-than-br --md-var-slash---md-var-greater-than---md-var-new-line-

Una vez hecho esto, ya tenemos cargadas todas las propiedades para que los servicios las consuman.

Cabe destacar que git2consul genera ciertas propiedades en el KV de Consul que le son necesarias para funcionar, no solo incluye el contenido del repositorio.

Con esto terminamos la segunda entrega de Consul y su uso con Spring Cloud. Como se ha podido ver, Consul es una muy buena herramienta que puede plantar cara a las opciones que ofrece Netflix en cuanto a descubrimiento de servicios (Eureka) y configuración centralizada (Config Server y Config Client).

Adjunto debajo los enlaces a github con el código usado para escribir los artículos:

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete