Dans le précédent article, nous avons vu comment lancer une base de données automatiquement grâce à une extension Quarkus. Nous allons maintenant voir comment démarrer notre service de la même manière et lui faire utiliser la base de données que nous venons de déployer. Je ne reviendrai pas sur les points abordés dans le précédent article. Je vous invite donc à le lire avant de continuer.
On prend les mêmes et on recommence.
La création de notre conteneur pour notre service se passe de la même manière que la base de données. On va donc commencer par ajouter les configurations nécessaires à notre DevServicesConfig :
/**
* The container image name to use, for container based DevServices providers.
* If you want to use a specific version of demo, use:
* {@code demo:<version>}.
*/@WithDefault("scandinave/demo")StringimageName();/**
* Optional fixed port the dev service will listen to.
* <p>
* If not defined, the port will be chosen randomly.
*/OptionalIntport();/**
* 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>
*/@WithDefault("demo")StringserviceName();/**
* Environment variables that are passed to the container.
*/Map<String,String>containerEnv();
On commence par ajouter une option pour le nom de l’image docker qui servira à démarrer notre conteneur. De cette manière, l’utilisateur pourra choisir une version spécifique s’il le souhaite.
Le port est par défaut aléatoire, mais pourra également être précisé. Enfin, le label appliqué au conteneur pourra aussi être personnalisé.
DemoExtensionProcessor
Dans notre DemoExtensionProcessor, nous ajoutons le code permettant d’initialiser le conteneur de notre service.
@BuildSteps(onlyIf={GlobalDevServicesConfig.Enabled.class})publicclassDemoExtensionProcessor{/** stuff from previous article
* ...
*/privatestaticvolatileRunningDevServicedemoApiDevService;@BuildSteppublicvoidcreateContainer(DockerStatusBuildItemdockerStatusBuildItem,List<DevServicesSharedNetworkBuildItem>devServicesSharedNetworkBuildItem,DemoBuildTimeConfigbuildTimeConfig,GlobalDevServicesConfigdevServicesConfig,BuildProducer<DevServicesResultBuildItem>devServicesResult){runningConfiguration=buildTimeConfig.devservices();try{// DemoApiRunningDevService newAPIDevService = startDemoContainer...DemoApiRunningDevServicenewAPIDevService=startDemoContainer(dockerStatusBuildItem,!devServicesSharedNetworkBuildItem.isEmpty(),devServicesConfig.timeout.orElse(Duration.of(0,ChronoUnit.SECONDS)),runningConfiguration,newDatabaseDevService,errors);// Get the RunningDevService instance.demoApiDevService=newAPIDevService.getRunningDevService();}catch(Throwablet){thrownewRuntimeException(t);}Log.info("Dev Services for Demo started.");devServicesResult.produce(demoApiDevService.toBuildItem());}}
Notre méthode startDemoContainer prends en plus en paramètre le résultat de l’appel à la méthode startDatabaseContainer qui contient une instance d’un DevRunningDevService. Dans ce dernier, nous avions enregistré l’URL d’accès à notre base de données. Je vous avais dit que cela servirait plus tard.
privateDemoApiRunningDevServicestartDemoContainer(DockerStatusBuildItemdockerStatusBuildItem,booleanuseSharedNetwork,Durationtimeout,DevServicesConfigdevServicesConfig,DemoDatabaseRunningDevServicerunningService,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;}StringdbUrl=Optional.ofNullable(runningService.getRunningDevService().getConfig()).orElseThrow(()->newIllegalStateException("Database does not start properly")).get("url-internal");finalOptional<ContainerAddress>maybeExistingContainer=demoAPIDevModeContainerLocator.locateContainer(devServicesConfig.serviceName(),devServicesConfig.shared(),LaunchMode.current());StringimageName=devServicesConfig.imageName();DockerImageNamedockerImageName=DockerImageName.parse(imageName).asCompatibleSubstituteFor(imageName);returnmaybeExistingContainer.map(existingContainer->newDemoApiRunningDevService(newRunningDevService(DemoApiContainer.CONTAINER_NAME,existingContainer.getId(),null,Map.of("url",existingContainer.getHost()+":"+existingContainer.getPort())))).orElseGet(()->newDemoApiRunningDevService(useSharedNetwork,timeout,devServicesConfig,dbUrl,dockerImageName,runningService));}
On commence par les mêmes vérifications concernant la présence de docker et l’activation des dev services puis nous récupérons l’URL interne docker de connexion à la base de données depuis notre instance de RunningDevService. La méthode vérifie ensuite si autre conteneur existe déjà grâce à un nouveau ContainerLocator:
Si un conteneur de notre service 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é. Ce nouveau RunningDevService prend en paramètre l’URL de notre base de données.
publicclassDemoApiRunningDevService{privateDemoApiContainerdemoContainer;privatefinalDevServicesResultBuildItem.RunningDevServicerunningDevServiceSupplier;publicDemoApiRunningDevService(booleanuseSharedNetwork,Durationtimeout,DevServicesConfigdevServicesConfig,StringdbUrl,DockerImageNamedockerImageName,DemoDatabaseRunningDevServicerunningService){runningDevServiceSupplier=this.run(useSharedNetwork,timeout,devServicesConfig,dbUrl,dockerImageName,runningService);}publicDemoApiRunningDevService(DevServicesResultBuildItem.RunningDevServicerunningDevService){runningDevServiceSupplier=runningDevService;demoContainer=null;}publicDevServicesResultBuildItem.RunningDevServicerun(booleanuseSharedNetwork,Durationtimeout,DevServicesConfigdevServicesConfig,StringdbUrl,DockerImageNamedockerImageName,DemoDatabaseRunningDevServicerunningService){if(devServicesConfig.port().isPresent()){try(DemoApiContainercontainer=newDemoApiContainer(dockerImageName,devServicesConfig.port().getAsInt(),useSharedNetwork,devServicesConfig.shared(),devServicesConfig.serviceName())){demoContainer=container;}}else{try(DemoApiContainercontainer=newDemoApiContainer(dockerImageName,useSharedNetwork,devServicesConfig.shared(),devServicesConfig.serviceName())){demoContainer=container;}}Optional.ofNullable(timeout).ifPresent(demoContainer::withStartupTimeout);demoContainer.withEnv(devServicesConfig.containerEnv());demoContainer.withEnv("POSTGRES_USER",devServicesConfig.db().username());demoContainer.withEnv("POSTGRES_PASSWORD",devServicesConfig.db().password());demoContainer.withEnv("DB_URL",dbUrl+"/"+devServicesConfig.db().name());if(runningService.getDatabaseContainer().isPresent()){// Wait for database container to start. If container is null, it means that a database was already started so no need to wait.demoContainer.withNetwork(runningService.getDatabaseContainer().get().getNetwork());demoContainer.dependsOn(runningService.getDatabaseContainer().get());}demoContainer.start();returnnewDevServicesResultBuildItem.RunningDevService(DemoApiContainer.CONTAINER_NAME,demoContainer.getContainerId(),demoContainer::close,Map.of("url",demoContainer.getHost()+":"+demoContainer.getPort()));}publicOptional<GenericContainer<DemoApiContainer>>getDatabaseContainer(){returnOptional.ofNullable(demoContainer);}publicDevServicesResultBuildItem.RunningDevServicegetRunningDevService(){returnrunningDevServiceSupplier;}}
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 DemoApiConteneur créée, nous le configurons en lui passant les variables d’environnement qu’il attend grâce à la méthode withEnv. Nous utilisons pour cela notre devServiceConfig pour récupérer les identifiants de connexion à la base de données puis l’URL passée en paramètre. Ensuite, nous configurons le conteneur sur le même réseau que la base de données et pour attendre que celle-ci ait fini de démarrer.
Enfin, nous retournons notre RunninDevService contenant l’ID de notre conteneur ainsi que de la configuration contenant l’URL d’accès à notre API qui nous sera également utile pour la suite.
DemoApiContainer
Il ne nous reste plus qu’à voir la classe DemoApiContainer pour finir ce second post sur la création d’un DevService avec Quarkus.
importio.quarkus.devservices.common.ConfigureUtil;importorg.testcontainers.containers.GenericContainer;importorg.testcontainers.containers.wait.strategy.Wait;importorg.testcontainers.utility.DockerImageName;publicclassDemoApiContainerextendsGenericContainer<DemoApiContainer>{publicstaticfinalStringCONTAINER_NAME="demo";publicstaticfinalStringDEV_SERVICE_LABEL="quarkus-dev-service-demo";privatefinalIntegerexposedPort;publicstaticfinalintDEMO_INTERNAL_PORT=8600;privatefinalbooleanuseSharedNetwork;privatefinalStringserviceName;privateStringhostname;privatefinalbooleansharedContainer;publicDemoApiContainer(DockerImageNameimage,booleanuseSharedNetwork,booleanisShared,StringserviceName){this(image,null,useSharedNetwork,isShared,serviceName);}publicDemoApiContainer(DockerImageNameimage,IntegerexposedPort,booleanuseSharedNetwork,booleanisShared,StringserviceName){super(image);this.exposedPort=exposedPort;this.useSharedNetwork=useSharedNetwork;this.sharedContainer=isShared;this.serviceName=serviceName;}@Overrideprotectedvoidconfigure(){super.configure();if(this.useSharedNetwork){hostname=ConfigureUtil.configureSharedNetwork(this,"demo");}if(this.exposedPort!=null){addFixedExposedPort(this.exposedPort,DEMO_INTERNAL_PORT);if(useSharedNetwork){// expose random port for which we are able to ask Testcontainers for the actual mapped port at runtime// as from the host's perspective Testcontainers actually expose container ports on random host portaddExposedPort(DEMO_INTERNAL_PORT);}}else{addExposedPort(DEMO_INTERNAL_PORT);}withLabel(DEV_SERVICE_LABEL,serviceName);// Tell the dev service how to know the container is readywaitingFor(Wait.forLogMessage(".*demo.*started.*",1));}@OverridepublicStringgetHost(){if(this.useSharedNetwork){returnhostname;}returnsuper.getHost();}publicintgetPort(){if(useSharedNetwork){returnDEMO_INTERNAL_PORT;}if(exposedPort!=null){returnexposedPort;}returngetFirstMappedPort();}}
Je ne m’attarderai pas sur cette classe qui est identique à celle de notre DemoDatabaseContainer. Seule l’attente de l’événement indiquant la fin du démarrage du processus dans le conteneur est différente.
Ici, nous attendons un log dans notre application contenant les mots demo et started
Conclusion
Notre extension lance maintenant un service “dockerisé” avec sa base de données. Le tout configuré automatiquement pour fonctionner ensemble. Comme nous avions déjà inclus l’extension lors de la précédente étape, nous n’avons rien de plus à faire pour profiter de cette mise à jour.
Dans un prochain article, nous verrons comment dialoguer avec notre API grâce à l’URL sauvegardée dans notre RunningDevService et comment fournir un SDK permettant d’exploiter cette API via une simple injection de dépendances dans notre application cliente.