In the previous article, we saw how to automatically launch a database using a Quarkus extension. Now, we will see how to start our service in the same way and make it use the database we just deployed. I won’t go over the points covered in the previous article, so I encourage you to read it before continuing.
We take the same ones and start again.
The creation of our container for our service happens in the same way as the database. So, we’ll start by adding the necessary configurations to our 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();
We begin by adding an option for the Docker image name that will be used to start our container. This way, the user can choose a specific version if they wish. The port is random by default but can also be specified. Finally, the label applied to the container can also be customized.
DemoExtensionProcessor
In our DemoExtensionProcessor, we add the code to initialize the container of our 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());}}
Our startDemoContainer method also takes as a parameter the result of the call to the startDatabaseContainer method, which contains an instance of a DevRunningDevService. In this instance, we stored the URL for accessing our database. I mentioned earlier that this would be useful later.
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));}}
We start with the same checks regarding the presence of Docker and the activation of dev services, then we retrieve the internal Docker connection URL for the database from our instance of RunningDevService. The method then checks if another container already exists using a new ContainerLocator:
If a container for our service is already running, the method returns a RunningDevService with the information of the existing container. Otherwise, a new RunningDevService for the new container is returned. This new RunningDevService takes the database URL as a parameter.
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;}}
Our class defines two constructors: the first that simply returns the existing RunningDevService if the container already exists, and the second that creates a new RunningDevService. Our run method first checks if a port is specified in the dev-service configuration. If it is, our container will be started on that port; otherwise, an available random port will be allocated (the default behavior).
Once the instance of our DemoApiContainer is created, we configure it by passing the expected environment variables through the withEnv method. We use our devServiceConfig to retrieve the database connection credentials and the URL passed as a parameter. Then, we configure the container to be on the same network as the database and wait for it to finish starting.
Finally, we return our RunningDevService, containing the ID of our container as well as the configuration that includes the URL for accessing our API, which will also be useful for the next steps.
DemoApiContainer
All that remains is to look at the DemoApiContainer class to complete this second post on creating a DevService with 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();}}
I won’t dwell on this class as it is identical to that of our DemoDatabaseContainer. The only difference is the wait for the event indicating the end of the process startup in the container. Here, we wait for a log in our application containing the words demo and started.
Conclusion
Our extension now launches a dockerized service along with its database, all automatically configured to work together. Since we already included the extension in the previous step, there is nothing more we need to do to benefit from this update.
In a future article, we will see how to interact with our API using the URL saved in our RunningDevService and how to provide an SDK that allows this API to be exploited through a simple dependency injection into our client application.