Aujourd’hui, on s’attaque à un gros morceau avec un cas d’usage non documenté par Quarkus, mais qui est vraiment très pratique.
Quarkus propose des extensions avec des “dev services”. Ces extensions, une fois incluses dans votre projet, lancent des services
automatiquement au démarrage du projet en mode dev. Nous allons voir comment créer notre propre “dev service” pour nos applications Quarkus.
L’idée est de fournir une extension pour chacun de nos services afin de les rendre utilisables facilement par des tiers.
Cas d’usage
Imaginons une entreprise avec plusieurs projets et plusieurs équipes. Il arrive souvent que des services soient communs à plusieurs projets. Deux solutions sont généralement mises en place.
Une instance de dev partagée
Un docker compose à lancer en parallèle du projet en mode dev.
La première solution implique une administration compliquée avec une gestion des données entres plusieurs équipes. Si une équipe casse le service, cela impact les autres équipes. Lors d’une mise à jour, l’ensemble des équipes sont également impactées.
La seconde solution est plus flexible, les services s’exécutant sur le poste du développeur. Cependant, cela implique une étape supplémentaire au lancement du projet en mode dev. Cela pose aussi la question de la mise à jour du docker compose.
Un “dev service” permet de résoudre ces problèmes. Distribuer sous la forme d’une extension Maven, ils peuvent être maintenus à jour via l’utilisation d’un bom Maven. Quarkus les démarrent automatiquement au démarrage du projet, éliminant le besoin de gérer manuellement des conteneurs docker.
L’extension
Notre extension devra pouvoir démarrer un service Quarkus avec sa base de données. L’extension devra permettre d’exécuter le service avec des paramètres par défaut et de permettre de les personnaliser.
Commençons par créer l’extension via la commande :
À la fin l’architecture de notre extension ressemblera au schéma suivant :
Création de la base de données de notre dev service
Notre service a besoin d’une base de données pour fonctionner. Nous allons donc configurer notre extension pour lancer une base de données dans un conteneur Docker. Avant cela commençons par créer quelques configurations pour notre extension afin de la rendre personnalisable.
packageinfo.techland.demo.extension.deployment;importio.quarkus.runtime.annotations.ConfigPhase;importio.quarkus.runtime.annotations.ConfigRoot;importio.smallrye.config.ConfigMapping;@ConfigMapping(prefix="quarkus.demo")@ConfigRoot(phase=ConfigPhase.BUILD_TIME)publicinterfaceDemoExtensionBuildTimeConfig{/**
* Dev services configuration.
*/DevServicesConfigdevservices();}
Cette classe enregistre le point d’entrée de la configuration de notre dev service. Notre configuration sera accessible dans l’application.properties sous la clé quarkus.demo.
packageinfo.techland.demo.extension.deployment;importjava.util.Map;importio.quarkus.runtime.annotations.ConfigGroup;importio.smallrye.config.WithDefault;@ConfigGrouppublicinterfaceDevServicesConfig{/**
* If DevServices has been explicitly enabled or disabled. DevServices is generally enabled
* by default, unless there is an existing configuration present.
* <p>
* When DevServices is enabled Quarkus will attempt to automatically configure and start
* a demo instance when running in Dev or Test mode and when Docker is running.
*/@WithDefault("true")booleanenabled();/**
* Indicates if the demo server managed by Quarkus Dev Services is shared.
* When shared, Quarkus looks for running containers using label-based service discovery.
* If a matching container is found, it is used, and so a second one is not started.
* Otherwise, Dev Services for demo starts a new container.
* <p>
* The discovery uses the {@code quarkus-dev-service-redis} label.
* The value is configured using the {@code service-name} property.
* <p>
* Container sharing is only used in dev mode.
*/@WithDefault("true")booleanshared();/**
* The value of the {@code quarkus-dev-service-demo} label attached to the started container.
* This property is used when {@code shared} is set to {@code true}.
* In this case, before starting a container, Dev Services for demo looks for a container with the
* {@code quarkus-dev-service-demo} label
* set to the configured value. If found, it will use this container instead of starting a new one. Otherwise, it
* starts a new container with the {@code quarkus-dev-service-demo} label set to the specified value.
* <p>
* This property is used when you need multiple shared demo servers.
*/@WithDefault("demo")StringserviceName();/**
* Environment variables that are passed to the container.
*/Map<String,String>containerEnv();/**
* Configuration for the postgresql database use by demo service.
* @return The database configuration
*/DatabaseConfigdb();}
Cette classe configure notre dev service avec l’accès à la configuration de notre base de données du dev service. La configuration de notre base de données sera accessible sous la clé quarkus.dev.db. Chaque entrée à une valeur par défaut afin de permettre l’utilisation de l’extension sans configuration particulière.
packageinfo.techland.demo.extension.deployment;importio.quarkus.runtime.annotations.ConfigGroup;importio.smallrye.config.WithDefault;importjava.util.Map;importjava.util.OptionalInt;@ConfigGrouppublicinterfaceDatabaseConfig{/**
* The database password used by the demo service.
*/@WithDefault("demo")Stringusername();/**
* The database password used by the demo service.
*/@WithDefault("demo")Stringpassword();/**
* The database name used by the demo service
*/@WithDefault("demo")Stringname();/**
* The number of reactive connection to use.
*/@WithDefault("20")intreactiveMaxSize();/**
* The port used by the database.
* @return The database port
*/OptionalIntport();/**
* Name of the postgresql image to use to start the database
* @return The image name.
*/@WithDefault("docker.io/postgres")StringimageName();/**
* Environment variables that are passed to the container.
*/Map<String,String>containerEnv();}
La configuration de la base de données contient les informations de connexion à la base de données PostgreSQL utilisée par défaut. Encore une fois, nous privilégions les conventions sur la configuration en appliquant des valeurs par défaut.
DemoExtensionProcessor
Pour demander à Quarkus de démarrer un conteneur, nous allons devoir configurer notre DemoExtensionProcessor. Commençons par ajouter une méthode createContainer qui fera office de @BuildStep pour démarrer et configurer notre conteneur.
@BuildSteps(onlyIf={GlobalDevServicesConfig.Enabled.class})publicclassDemoExtensionProcessor{/**
* The current configuration. either from running container instance or an instance that is about to start.
*/privatestaticvolatileDevServicesConfigrunningConfiguration;privatestaticvolatileRunningDevServicedemoDatabaseDevService;@BuildSteppublicvoidcreateContainer(DockerStatusBuildItemdockerStatusBuildItem,List<DevServicesSharedNetworkBuildItem>devServicesSharedNetworkBuildItem,DemoBuildTimeConfigbuildTimeConfig,GlobalDevServicesConfigdevServicesConfig,BuildProducer<DevServicesResultBuildItem>devServicesResult){runningConfiguration=buildTimeConfig.devservices();try{List<String>errors=newArrayList<>();DemoDatabaseRunningDevServicenewDatabaseDevService=startDatabaseContainer(dockerStatusBuildItem,!devServicesSharedNetworkBuildItem.isEmpty(),devServicesConfig.timeout.orElse(Duration.of(0,ChronoUnit.SECONDS)),runningConfiguration,errors);// Get the RunningDevService instance.demoDatabaseDevService=newDatabaseDevService.getRunningDevService();}catch(Throwablet){thrownewRuntimeException(t);}Log.info("Dev Services for Demo started.");// Call to toBuildItem method to get the DevServicesResultBuildItem need by the BuildStep producer.devServicesResult.produce(demoDatabaseDevService.toBuildItem());}}
Notre méthode se voit injecter les builds items suivants :
DockerStatusBuildItem : Permet de récupérer le statut d’une instance docker
DevServicesSharedNetworkBuildItem : Détermine si les conteneurs sont démarrés sur un réseau partagé
DemoBuildTimeConfig : La configuration de notre extension
GlobalDevServicesConfig : La configuration globale des devservices fournie par Quarkus
DevServicesResultBuildItem : Le buildItem à produire pour référencer notre conteneur auprès de Quarkus
Notre méthode appelle ensuite la méthode startDatabaseContainer chargée de créer une instance de DemoDatabaseRunningDevService. Cette dernière est en charge de fournir un
RunningDevService, via devServicesResult.produce(demoDatabaseDevService.toBuildItem()), qui sera retourné par notre @BuildStep pour demander à Quarkus de lancer un conteneur.
privateDemoDatabaseRunningDevServicestartDatabaseContainer(DockerStatusBuildItemdockerStatusBuildItem,booleanuseSharedNetwork,Durationtimeout,DevServicesConfigdevServicesConfig,List<String>errors){if(!devServicesConfig.enabled()){// explicitly disabledLog.debug("Not starting Dev Services for Demo as it has been disabled in the config");returnnull;}if(!dockerStatusBuildItem.isDockerAvailable()){Log.warn("Please get a working docker instance");errors.add("Please get a working docker instance");returnnull;}finalOptional<ContainerAddress>maybeExistingContainer=demoDBDevModeContainerLocator.locateContainer(devServicesConfig.serviceName()+"_db",devServicesConfig.shared(),LaunchMode.current());// Get the image name from the configuration.StringimageName=devServicesConfig.db().imageName();DockerImageNamedockerImageName=DockerImageName.parse(imageName).asCompatibleSubstituteFor(imageName);returnmaybeExistingContainer.map(existingContainer->newDemoDatabaseRunningDevService(newRunningDevService(DemoDatabaseContainer.CONTAINER_NAME,existingContainer.getId(),null,Map.of("url",existingContainer.getHost()+":"+existingContainer.getPort())))).orElseGet(()->newDemoDatabaseRunningDevService(DemoDatabaseContainer.CONTAINER_NAME,useSharedNetwork,timeout,devServicesConfig,dockerImageName,devServicesConfig.serviceName()+"_db"));}
On commence par quelques vérifications. Si les devservices sont désactivés de manière globale, on annule le lancement et si docker n’est pas disponible, on retourne une erreur.
La méthode vérifie ensuite si un autre conteneur existe déjà. Cela peut être le cas, si deux services Quarkus utilise la même extension. Cela se fait grâce à un ContainerLocator:
Si un conteneur de notre base de données est déjà lancé, alors la méthode retourne un RunningDevService avec les informations du conteneur existant. Sinon un nouveau RunningDevService pour le nouveau conteneur
est retourné.
Notre classe définit deux constructeurs. Un renvoyant juste le RunningDevService existant dans le cas où le conteneur existe déjà. Le deuxième, créant un nouveau RunningDevService.
Notre méthode run commence par vérifier si un port est précisé dans la configuration du dev-service. Si c’est le cas, notre conteneur sera démarré sur ce port, sinon, un port aléatoire disponible sera alloué (comportement par défaut).
Une fois l’instance de notre DemoDatabaseConteneur créée, nous le configurons en lui passant les variables d’environnement qu’il attend grâce à la méthode withEnv. Vient ensuite la configuration du réseau docker utilisé.
Dans le cas où l’utilisateur à expressement spécifié que les conteneurs devait être attachés au réseau partagé créé par TestContainer, nous utilisons la méthode withNetwork avec la valeur Network.SHARED. Dans le cas contraire, un nouveau réseau est créé pour le conteneur.
Enfin, nous retournons notre RunninDevService contenant l’ID de notre conteneur ainsi que de la configuration qui nous sera utile par la suite.
DemoDatabaseContainer
Il ne nous reste plus qu’à voir la classe DemoDatabaseContainer pour finir ce premier post sur la création d’un DevService avec Quarkus.
Cette classe étend de GenericContainer et contient la configuration utilisée par TestContainer pour démarrer notre conteneur Docker. Dans la méthode configure, on commence par regarder si le conteneur doit utiliser le Shared Network. Si c’est le cas ConfigureUtil.configureSharedNetwork se charge de tout configurer correctement. Dans le cas contraire, on configure le port (fixe ou aléatoire), on ajoute un label et on précise sur quel port écouter pour savoir quand le service a terminé de démarrer.
Conclusion
Notre extension de base de données est maintenant fonctionnelle. Cette dernière fournit une implémentation d’un GénericContainer au travers d’un RunningDevService à notre DemoExtensionProcessor. Le tout est personnalisable via la configuration classique de Quarkus.
Il nous suffit d’inclure l’extension dans une application quarkus via un import Maven pour avoir la base de données démarrée en parallèle.
Dans un prochain article, nous verrons comment lancer un service dans son propre conteneur utilisant cette base de données pour exposer une API à notre application Quarkus.