Skip to content

Commit

Permalink
Merge pull request #463 from zZHorizonZz/devservices
Browse files Browse the repository at this point in the history
feature(devservices): added firestore dev service
  • Loading branch information
loicmathieu authored Jul 7, 2023
2 parents f215c0b + 6862c03 commit 967113f
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 64 deletions.
24 changes: 21 additions & 3 deletions docs/modules/ROOT/pages/firestore.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Be sure to have read the https://quarkiverse.github.io/quarkiverse-docs/quarkus-

== Bootstrapping the project

First, we need a new project. Create a new project with the following command (replace the version placeholder with the correct one):
First, we need a new project.
Create a new project with the following command (replace the version placeholder with the correct one):

[source,shell script]
----
Expand Down Expand Up @@ -38,8 +39,7 @@ This will add the following to your pom.xml:

== Some example

This is an example usage of the extension: we create a REST resource with a single endpoint that creates a 'persons' collection,
inserts three persons in it, then search for persons with last name Doe and returns them.
This is an example usage of the extension: we create a REST resource with a single endpoint that creates a 'persons' collection, inserts three persons in it, then search for persons with last name Doe and returns them.

[source,java]
----
Expand Down Expand Up @@ -92,3 +92,21 @@ public class FirestoreResource {
NOTE: Here we let Firestore serialize the `Person` object, Firestore will use reflection for this.
So if you deploy your application as a GraalVM native image you will need to register the `Person` class for reflection.
This can be done by annotating it with `@RegisterForReflection`.

== Dev Service

=== Configuring the Dev Service

The extension provides a Dev Service that can be used to run a local Firestore emulator.
This is useful for testing purposes, so you don't have to rely on a real Firestore instance.
By default, the Dev Service is disabled, but you can enable it by setting the

* `quarkus.google.cloud.firestore.devservice.enabled` property to `true`

You can also set the

* `quarkus.google.cloud.firestore.devservice.port` property to change the port on which the emulator will be started (by default there is no port set, so the emulator will use a random port)

=== Using the Dev Service

Once the Dev Service is enabled, the Firestore client which you can `@Inject` in your application will be configured to use the emulator.
6 changes: 5 additions & 1 deletion firestore/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
<groupId>io.quarkiverse.googlecloudservices</groupId>
<artifactId>quarkus-google-cloud-firestore</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>gcloud</artifactId>
</dependency>
</dependencies>

<build>
Expand All @@ -47,4 +51,4 @@
</plugins>
</build>

</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import io.quarkus.deployment.builditem.FeatureBuildItem;

public class FirestoreBuildSteps {
private static final String FEATURE = "google-cloud-firestore";
protected static final String FEATURE = "google-cloud-firestore";

@BuildStep
public FeatureBuildItem feature() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkiverse.googlecloudservices.firestore.deployment;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;

/**
* Root configuration class for Firestore that operates at build time.
* This class provides a nested structure for configuration, including
* a separate group for the development service configuration.
*/
@ConfigRoot(name = "google.cloud.firestore", phase = ConfigPhase.BUILD_TIME)
public class FirestoreBuildTimeConfig {

/**
* Configuration for the Firestore dev service.
* These settings will be used when Firestore service is being configured
* for development purposes.
*/
@ConfigItem
public FirestoreDevServiceConfig devservice;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkiverse.googlecloudservices.firestore.deployment;

import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;

/**
* Configuration group for the Firestore dev service. This class holds all the configuration properties
* related to the Google Cloud Firestore service for development environments.
* <p>
* Here is an example of how to configure these properties:
* <p>
*
* <pre>
* quarkus.firestore-dev-service.enabled = true
* quarkus.firestore-dev-service.image-name = gcr.io/google.com/cloudsdktool/google-cloud-cli # optional
* quarkus.firestore-dev-service.emulatorPort = 8080 # optional
* </pre>
*/
@ConfigGroup
public class FirestoreDevServiceConfig {

/**
* Indicates whether the Firestore service should be enabled or not.
* The default value is 'false'.
*/
@ConfigItem(defaultValue = "false")
public boolean enabled;

/**
* Sets the Docker image name for the Google Cloud SDK.
* This image is used to emulate the Firestore service in the development environment.
* The default value is 'gcr.io/google.com/cloudsdktool/google-cloud-cli'.
*/
@ConfigItem(defaultValue = "gcr.io/google.com/cloudsdktool/google-cloud-cli")
public String imageName;

/**
* Specifies the emulatorPort on which the Firestore service should run in the development environment.
*/
@ConfigItem
public Optional<Integer> emulatorPort = Optional.empty();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package io.quarkiverse.googlecloudservices.firestore.deployment;

import java.time.Duration;
import java.util.List;
import java.util.Optional;

import org.jboss.logging.Logger;
import org.testcontainers.containers.FirestoreEmulatorContainer;
import org.testcontainers.utility.DockerImageName;

import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.builditem.*;
import io.quarkus.deployment.console.ConsoleInstalledBuildItem;
import io.quarkus.deployment.console.StartupLogCompressor;
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
import io.quarkus.deployment.logging.LoggingSetupBuildItem;

/**
* Processor responsible for managing Firestore Dev Services.
* <p>
* The processor starts the Firestore service in case it's not running.
*/
@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class)
public class FirestoreDevServiceProcessor {

private static final Logger LOGGER = Logger.getLogger(FirestoreDevServiceProcessor.class.getName());

// Running dev service instance
private static volatile DevServicesResultBuildItem.RunningDevService devService;
// Configuration for the Firestore Dev service
private static volatile FirestoreDevServiceConfig config;

@BuildStep
public DevServicesResultBuildItem start(DockerStatusBuildItem dockerStatusBuildItem,
FirestoreBuildTimeConfig buildTimeConfig,
List<DevServicesSharedNetworkBuildItem> devServicesSharedNetworkBuildItem,
Optional<ConsoleInstalledBuildItem> consoleInstalledBuildItem,
CuratedApplicationShutdownBuildItem closeBuildItem,
LaunchModeBuildItem launchMode,
LoggingSetupBuildItem loggingSetupBuildItem,
GlobalDevServicesConfig globalDevServicesConfig) {
// If dev service is running and config has changed, stop the service
if (devService != null && !buildTimeConfig.devservice.equals(config)) {
stopContainer();
} else if (devService != null) {
return devService.toBuildItem();
}

// Set up log compressor for startup logs
StartupLogCompressor compressor = new StartupLogCompressor(
(launchMode.isTest() ? "(test) " : "") + "Google Cloud Firestore Dev Services Starting:",
consoleInstalledBuildItem,
loggingSetupBuildItem);

// Try starting the container if conditions are met
try {
devService = startContainerIfAvailable(dockerStatusBuildItem, buildTimeConfig.devservice,
globalDevServicesConfig.timeout);
} catch (Throwable t) {
LOGGER.warn("Unable to start Firestore dev service", t);
// Dump captured logs in case of an error
compressor.closeAndDumpCaptured();
return null;
} finally {
compressor.close();
}

return devService == null ? null : devService.toBuildItem();
}

/**
* Start the container if conditions are met.
*
* @param dockerStatusBuildItem, Docker status
* @param config, Configuration for the Firestore service
* @param timeout, Optional timeout for starting the service
* @return Running service item, or null if the service couldn't be started
*/
private DevServicesResultBuildItem.RunningDevService startContainerIfAvailable(DockerStatusBuildItem dockerStatusBuildItem,
FirestoreDevServiceConfig config,
Optional<Duration> timeout) {
if (!config.enabled) {
// Firestore service explicitly disabled
LOGGER.debug("Not starting Dev Services for Firestore as it has been disabled in the config");
return null;
}

if (!dockerStatusBuildItem.isDockerAvailable()) {
LOGGER.warn("Not starting devservice because docker is not available");
return null;
}

return startContainer(dockerStatusBuildItem, config, timeout);
}

/**
* Starts the Firestore emulator container with provided configuration.
*
* @param dockerStatusBuildItem, Docker status
* @param config, Configuration for the Firestore service
* @param timeout, Optional timeout for starting the service
* @return Running service item, or null if the service couldn't be started
*/
private DevServicesResultBuildItem.RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem,
FirestoreDevServiceConfig config,
Optional<Duration> timeout) {
// Create and configure Firestore emulator container
FirestoreEmulatorContainer emulatorContainer = new QuarkusFirestoreContainer(
DockerImageName.parse(config.imageName).asCompatibleSubstituteFor("gcr.io/google.com/cloudsdktool/cloud-sdk"),
config.emulatorPort.orElse(null));

// Set container startup timeout if provided
timeout.ifPresent(emulatorContainer::withStartupTimeout);
emulatorContainer.start();

// Set the config for the started container
FirestoreDevServiceProcessor.config = config;

// Return running service item with container details
return new DevServicesResultBuildItem.RunningDevService(FirestoreBuildSteps.FEATURE,
emulatorContainer.getContainerId(),
emulatorContainer::close, "quarkus.google.cloud.firestore.host-override",
emulatorContainer.getEmulatorEndpoint());
}

/**
* Stops the running Firestore emulator container.
*/
private void stopContainer() {
if (devService != null && devService.isOwner()) {
try {
// Try closing the running dev service
devService.close();
} catch (Throwable e) {
LOGGER.error("Failed to stop firestore container", e);
} finally {
devService = null;
}
}
}

/**
* Class for creating and configuring a Firestore emulator container.
*/
private static class QuarkusFirestoreContainer extends FirestoreEmulatorContainer {

private final Integer fixedExposedPort;
private static final int INTERNAL_PORT = 8080;

private QuarkusFirestoreContainer(DockerImageName dockerImageName, Integer fixedExposedPort) {
super(dockerImageName);
this.fixedExposedPort = fixedExposedPort;
}

/**
* Configures the Firestore emulator container.
*/
@Override
public void configure() {
super.configure();

// Expose Firestore emulatorPort
if (fixedExposedPort != null) {
addFixedExposedPort(fixedExposedPort, INTERNAL_PORT);
} else {
addExposedPort(INTERNAL_PORT);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package io.quarkiverse.googlecloudservices.firestore.runtime;

import java.io.IOException;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import org.threeten.bp.Duration;

import com.google.api.gax.retrying.RetrySettings;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreOptions;

import io.quarkiverse.googlecloudservices.common.GcpBootstrapConfiguration;
import io.quarkiverse.googlecloudservices.common.GcpConfigHolder;
import io.quarkiverse.googlecloudservices.firestore.runtime.FirestoreConfiguration.RetryConfiguration;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.threeten.bp.Duration;

import java.io.IOException;

@ApplicationScoped
public class FirestoreProducer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
# We use a dummy test project id in test for the emulators to work
%test.quarkus.google.cloud.project-id=test-project
%test.quarkus.google.cloud.storage.host-override=http://localhost:8089
%test.quarkus.google.cloud.firestore.host-override=localhost:8080
%test.quarkus.google.cloud.spanner.emulator-host=http://localhost:9010

# Disable authentication for Bigtable on tests
Expand All @@ -14,6 +13,7 @@
# Use pubsub emulator
%test.pubsub.use-emulator=true
%test.quarkus.google.cloud.pubsub.devservice.enabled=true
%test.quarkus.google.cloud.firestore.devservice.enabled=true

# Secret Manager Demo
# You can load secrets from Google Cloud Secret Manager with the ${sm//<SECRET_ID>} syntax.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,12 @@

import static io.restassured.RestAssured.given;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.FirestoreEmulatorContainer;
import org.testcontainers.utility.DockerImageName;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class FirestoreResourceTest {
private static final FirestoreEmulatorContainer EMULATOR = new FirestoreEmulatorContainer(
DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk"));

@BeforeAll
public static void startGcloudContainer() {
List<String> portBindings = new ArrayList<>();
portBindings.add("8080:8080");
EMULATOR.setPortBindings(portBindings);
EMULATOR.start();
}

@AfterAll
public static void stopGcloudContainer() {
EMULATOR.stop();
}

@Test
public void testFirestore() {
Expand Down
Loading

0 comments on commit 967113f

Please sign in to comment.