diff --git a/.all-contributorsrc b/.all-contributorsrc index 2b85d231..7ebbcdca 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -205,6 +205,15 @@ "contributions": [ "code" ] + }, + { + "login": "xperjon", + "name": "Jon-Erik Liw", + "avatar_url": "https://avatars.githubusercontent.com/u/965777?v=4", + "profile": "https://github.com/xperjon", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/.github/workflows/quarkus-snapshot.yaml b/.github/workflows/quarkus-snapshot.yaml index 6fd23592..3ff828ca 100644 --- a/.github/workflows/quarkus-snapshot.yaml +++ b/.github/workflows/quarkus-snapshot.yaml @@ -26,9 +26,6 @@ jobs: if: github.actor == 'quarkusbot' || github.actor == 'quarkiversebot' || github.actor == 'loicmathieu' steps: - - name: Install yq - run: sudo add-apt-repository ppa:rmescandon/yq && sudo apt update && sudo apt install yq -y - - name: Set up Java uses: actions/setup-java@v1 with: diff --git a/README.md b/README.md index 40fd1e22..d5caa3b6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Quarkiverse - Quarkus Google Cloud Services -[![All Contributors](https://img.shields.io/badge/all_contributors-22-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-23-orange.svg?style=flat-square)](#contributors-) [![version](https://img.shields.io/maven-central/v/io.quarkiverse.googlecloudservices/quarkus-google-cloud-services-bom)](https://repo1.maven.org/maven2/io/quarkiverse/googlecloudservices/) [![Build](https://github.com/quarkiverse/quarkus-google-cloud-services/workflows/Build/badge.svg)](https://github.com/quarkiverse/quarkus-google-cloud-services/actions?query=workflow%3ABuild) @@ -71,6 +71,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d wabrit
wabrit

💻 + Jon-Erik Liw
Jon-Erik Liw

📖 diff --git a/bom/pom.xml b/bom/pom.xml index 1245b542..43153c80 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -19,7 +19,7 @@ UTF-8 26.50.0 - 9.4.2 + 9.4.3 0.31.1 diff --git a/docs/modules/ROOT/pages/firebase-admin.adoc b/docs/modules/ROOT/pages/firebase-admin.adoc index 0128ad45..1a7e042c 100644 --- a/docs/modules/ROOT/pages/firebase-admin.adoc +++ b/docs/modules/ROOT/pages/firebase-admin.adoc @@ -73,7 +73,7 @@ public class FirebaseAuthResourceTest { This extension also supports Firebase Authentication, allowing you to secure your endpoints using Firebase's authentication mechanisms. This section describes how to use Firebase Authentication in your Quarkus application. -Remember that you need to enable the Firebase Authentication service in your Firebase project. `quarkus.google.cloud.firebase.auth.enable` must be set to `true` in your application configuration. +Remember that you need to enable the Firebase Authentication service in your Firebase project. `quarkus.google.cloud.firebase.auth.enabled` must be set to `true` in your application configuration. === Configuration @@ -122,4 +122,4 @@ public class FirebaseAppResource { == Configuration Reference -include::./includes/quarkus-google-cloud-firebase-admin.adoc[] \ No newline at end of file +include::./includes/quarkus-google-cloud-firebase-admin.adoc[] diff --git a/docs/modules/ROOT/pages/firebase-devservices.adoc b/docs/modules/ROOT/pages/firebase-devservices.adoc index 47fa12e3..dd2a9d96 100644 --- a/docs/modules/ROOT/pages/firebase-devservices.adoc +++ b/docs/modules/ROOT/pages/firebase-devservices.adoc @@ -131,13 +131,43 @@ If emulators are configured via the configuration options, a `firebase.json` fil * Each of the emulators must be exposed on `0.0.0.0` as host as described https://firebase.google.com/docs/emulator-suite/use_hosting#emulators-no-local-host[here]. If this is not done, the Emulators will not be reachable from the Docker host. * Emulators need to be configured to use the default ports. Customizing the ports on which they run is currently not supported (this might change in a future version). +== Details on specific Devservices + +The following sections provide documentation in interaction with specific emulators. + +=== Hosting emulator + +If you use the hosting emulator, where Quarkus is the backend, you will need to include a CORS configuration, as REST +requests will originate from another Origin (host). See the https://quarkus.io/guides/security-cors[Quarkus CORS] +documentation for more info. + +A simple setup would be +[source,properties] +---- +"%dev".quarkus.http.cors=true +"%dev".quarkus.http.cors.origins: /.*/ +---- + +Note that a redirect from the hosting emulator to the Quarkus instance is currently not supported by the emulator. + +=== Auth emulator + +You can use the features provided by Mircoprofile JWT (e.g. injecting a `@Claim` value) by including the smallrye-jwt +extension and disabling smallrye-jwt using the following property. This will prevent SmallRye JWT from handling the JWT +validation (leaving that to the Firebase Auth module and the Auth emulator), but using the provided JWT to allow injecting +of these beans. + +[source,properties] +---- +quarkus.smallrye-jwt.enabled=false +---- + == Interaction with other extensions The following extensions support Dev Services which conflicts with the Dev Services exposed by the Firebase Emulators. * Firestore * PubSub -* TODO: Verify Storage When including this module, these Dev Services will automatically be disabled, as the Firebase emulator should feature wise be on-par or more extensive than the individual emulators. diff --git a/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-devservices.adoc b/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-devservices.adoc index 95883d1d..31cd5553 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-devservices.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-devservices.adoc @@ -247,6 +247,23 @@ endif::add-copy-button-to-env-var[] a|`import-only`, `export-only`, `import-export` | +a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-devservices-firebase-emulator-cli-experiments]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-devservices-firebase-emulator-cli-experiments[`quarkus.google.cloud.devservices.firebase.emulator.cli.experiments`]## + +[.description] +-- +Indicates the set of experimental features from firebase to enable (using the firebase experiment:enable command line option). + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_DEVSERVICES_FIREBASE_EMULATOR_CLI_EXPERIMENTS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_GOOGLE_CLOUD_DEVSERVICES_FIREBASE_EMULATOR_CLI_EXPERIMENTS+++` +endif::add-copy-button-to-env-var[] +-- +|list of string +| + a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-devservices-firebase-emulator-cli-debug]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-devservices-firebase-emulator-cli-debug[`quarkus.google.cloud.devservices.firebase.emulator.cli.debug`]## [.description] diff --git a/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-devservices_quarkus.google.adoc b/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-devservices_quarkus.google.adoc index 95883d1d..31cd5553 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-devservices_quarkus.google.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-google-cloud-firebase-devservices_quarkus.google.adoc @@ -247,6 +247,23 @@ endif::add-copy-button-to-env-var[] a|`import-only`, `export-only`, `import-export` | +a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-devservices-firebase-emulator-cli-experiments]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-devservices-firebase-emulator-cli-experiments[`quarkus.google.cloud.devservices.firebase.emulator.cli.experiments`]## + +[.description] +-- +Indicates the set of experimental features from firebase to enable (using the firebase experiment:enable command line option). + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_DEVSERVICES_FIREBASE_EMULATOR_CLI_EXPERIMENTS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_GOOGLE_CLOUD_DEVSERVICES_FIREBASE_EMULATOR_CLI_EXPERIMENTS+++` +endif::add-copy-button-to-env-var[] +-- +|list of string +| + a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-devservices-firebase-emulator-cli-debug]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-devservices-firebase-emulator-cli-debug[`quarkus.google.cloud.devservices.firebase.emulator.cli.debug`]## [.description] diff --git a/docs/modules/ROOT/pages/pubsub.adoc b/docs/modules/ROOT/pages/pubsub.adoc index 5f50d6b1..5fdfa62e 100644 --- a/docs/modules/ROOT/pages/pubsub.adoc +++ b/docs/modules/ROOT/pages/pubsub.adoc @@ -155,6 +155,28 @@ public class PubSubResource { } ---- +== AdminClient beans + +If you need to perform admin actions on PubSub, you can inject the `com.google.cloud.pubsub.v1.SubscriptionAdminClient` and +`com.google.cloud.pubsub.v1.TopicAdminClient` as CDI beans as shown in the example below. This is usefull if the basic +facilities offered by `io.quarkiverse.googlecloudservices.pubsub.QuarkusPubSub` are not sufficient: + +[source, java] +---- + @Inject + SubscriptionAdminClient subscriptionAdminClient; + + @Inject + TopicAdminClient topicAdminClient; + + public someMethod() { + var pushConfig = ...; // Create com.google.pubsub.v1.PushConfig + subscriptionAdminClient.createSubscription("subscription-name", "topic-name", pushConfig, 10 /* ACK deadline */); + + var topics = topicAdminClient.listTopics("my-google-project"); + } +---- + == Dev Service === Configuring the Dev Service diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceConfig.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceConfig.java index 08b58ab6..6c2d614e 100644 --- a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceConfig.java +++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceConfig.java @@ -1,6 +1,7 @@ package io.quarkiverse.googlecloudservices.firebase.deployment; import java.util.Optional; +import java.util.Set; import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer; import io.quarkus.runtime.annotations.ConfigRoot; @@ -199,6 +200,12 @@ interface Cli { */ Optional importExport(); + /** + * Indicates the set of experimental features from firebase to enable (using the firebase experiment:enable + * command line option). + */ + Optional> experiments(); + /** * Enable firebase emulators debugging. */ diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProcessor.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProcessor.java index 9d8d46fd..52b75811 100644 --- a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProcessor.java +++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProcessor.java @@ -67,7 +67,11 @@ public DevServicesResultBuildItem start(DockerStatusBuildItem dockerStatusBuildI // Try starting the container if conditions are met try { - devService = startContainerIfAvailable(dockerStatusBuildItem, projectConfig, firebaseBuildTimeConfig, + devService = startContainerIfAvailable( + dockerStatusBuildItem, + closeBuildItem, + projectConfig, + firebaseBuildTimeConfig, globalDevServicesConfig.timeout); } catch (Throwable t) { LOGGER.warn("Unable to start Firebase dev service", t); @@ -87,9 +91,11 @@ public DevServicesResultBuildItem start(DockerStatusBuildItem dockerStatusBuildI * @param dockerStatusBuildItem, Docker status * @param config, Configuration for the Firebase service * @param timeout, Optional timeout for starting the service + * @param closeBuildItem * @return Running service item, or null if the service couldn't be started */ private DevServicesResultBuildItem.RunningDevService startContainerIfAvailable(DockerStatusBuildItem dockerStatusBuildItem, + CuratedApplicationShutdownBuildItem closeBuildItem, FirebaseDevServiceProjectConfig projectConfig, FirebaseDevServiceConfig config, Optional timeout) { @@ -111,7 +117,7 @@ private DevServicesResultBuildItem.RunningDevService startContainerIfAvailable(D return null; } - return startContainer(dockerStatusBuildItem, projectConfig, config, timeout); + return startContainer(closeBuildItem, projectConfig, config, timeout); } private boolean isEnabled(FirebaseDevServiceConfig config) { @@ -125,12 +131,12 @@ private boolean isEnabled(FirebaseDevServiceConfig config) { /** * Starts the Pub/Sub emulator container with provided configuration. * - * @param dockerStatusBuildItem, Docker status + * @param closeBuildItem The close build item to handle shutdown of the container * @param config, Configuration for the Firebase 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, + private DevServicesResultBuildItem.RunningDevService startContainer(CuratedApplicationShutdownBuildItem closeBuildItem, FirebaseDevServiceProjectConfig projectConfig, FirebaseDevServiceConfig config, Optional timeout) { @@ -155,6 +161,8 @@ private DevServicesResultBuildItem.RunningDevService startContainer(DockerStatus .forEach((e, h) -> LOGGER.info("Google Cloud emulator config property " + e + " set to " + h)); } + closeBuildItem.addCloseTask(emulatorContainer::close, true); + // Return running service item with container details return new DevServicesResultBuildItem.RunningDevService(FirebaseBuildSteps.FEATURE, emulatorContainer.getContainerId(), diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilder.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilder.java index e3ed09aa..e356965a 100644 --- a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilder.java +++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilder.java @@ -79,6 +79,7 @@ private void handleCliConfig(FirebaseDevServiceConfig.Firebase.Emulator.Cli cli, cli.javaToolOptions().ifPresent(cliConfig::withJavaToolOptions); cli.emulatorData().map(FirebaseEmulatorConfigBuilder::asPath).ifPresent(cliConfig::withEmulatorData); cli.importExport().ifPresent(cliConfig::withImportExport); + cli.experiments().ifPresent(cliConfig::withExperiments); cli.debug().ifPresent(cliConfig::withDebug); cliConfig.done(); diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/CustomFirebaseConfigReader.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/CustomFirebaseConfigReader.java index a7050bd9..ca3cbfc8 100644 --- a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/CustomFirebaseConfigReader.java +++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/CustomFirebaseConfigReader.java @@ -10,6 +10,8 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.json.Emulators; @@ -21,6 +23,8 @@ */ class CustomFirebaseConfigReader { + private static final Logger LOGGER = LoggerFactory.getLogger(CustomFirebaseConfigReader.class); + private final ObjectMapper objectMapper = new ObjectMapper(); /** @@ -110,6 +114,8 @@ private Map this.resolvePath(f, customFirebaseJson)); + LOGGER.debug("Firestore configured with rules file {}", rulesFile); + LOGGER.debug("Firestore configured with indexes file {}", indexesFile); + return new FirebaseEmulatorContainer.FirestoreConfig( rulesFile, indexesFile); @@ -152,8 +161,11 @@ private FirebaseEmulatorContainer.HostingConfig readHosting(Object hosting, Path var publicDir = Optional .ofNullable(hostingMap.get("public")) + .or(() -> Optional.ofNullable(hostingMap.get("source"))) .map(f -> this.resolvePath(f, customFirebaseJson)); + LOGGER.debug("Hosting configured with public directory {}", publicDir); + return new FirebaseEmulatorContainer.HostingConfig( publicDir); } else { @@ -170,6 +182,8 @@ private FirebaseEmulatorContainer.StorageConfig readStorage(Object storage, Path .ofNullable(storageMap.get("rules")) .map(f -> this.resolvePath(f, customFirebaseJson)); + LOGGER.debug("Storage configured with rules file {}", rulesFile); + return new FirebaseEmulatorContainer.StorageConfig( rulesFile); } else { @@ -192,6 +206,9 @@ private FirebaseEmulatorContainer.FunctionsConfig readFunctions(Object functions .map(String[].class::cast) .orElse(new String[0]); + LOGGER.debug("Functions will be read from source directory {}", functionsPath); + LOGGER.debug("Functions configured with ignore file {}", (Object) ignores); + return new FirebaseEmulatorContainer.FunctionsConfig( functionsPath, ignores); diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainer.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainer.java index 02937f8e..61d13180 100644 --- a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainer.java +++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainer.java @@ -185,6 +185,7 @@ public record DockerConfig( * @param javaToolOptions The options to pass to the java based emulators * @param emulatorData The path to the directory where to store the emulator data * @param importExport Specify whether to import, export or do both with the emulator data + * @param experiments Firebase experiments to enable on the docker image * @param debug Whether to run with the --debug flag */ public record CliArgumentsConfig( @@ -193,6 +194,7 @@ public record CliArgumentsConfig( Optional javaToolOptions, Optional emulatorData, ImportExport importExport, + Optional> experiments, boolean debug) { public static final CliArgumentsConfig DEFAULT = new CliArgumentsConfig( Optional.empty(), @@ -200,6 +202,7 @@ public record CliArgumentsConfig( Optional.empty(), Optional.empty(), ImportExport.IMPORT_EXPORT, + Optional.of(new HashSet<>()), false); } @@ -413,7 +416,7 @@ public FirebaseConfigBuilder withFirebaseConfig() { public EmulatorConfig buildConfig() { if (firebaseConfig == null) { // Try to autoload the firebase.json configuration - var defaultFirebaseJson = new File("firebase.json").getAbsoluteFile().toPath(); + var defaultFirebaseJson = new File("firebase.json").toPath(); LOGGER.info("Trying to automatically read firebase config from {}", defaultFirebaseJson); @@ -609,6 +612,7 @@ public class CliBuilder { private String javaToolOptions; private Path emulatorData; private ImportExport importExport; + private Set experiments; private boolean debug; /** @@ -620,6 +624,7 @@ private CliBuilder() { this.javaToolOptions = Builder.this.cliArguments.javaToolOptions.orElse(null); this.emulatorData = Builder.this.cliArguments.emulatorData.orElse(null); this.importExport = Builder.this.cliArguments.importExport; + this.experiments = Builder.this.cliArguments.experiments.orElse(new HashSet<>()); this.debug = Builder.this.cliArguments.debug; } @@ -690,6 +695,28 @@ public CliBuilder withDebug(boolean debug) { return this; } + /** + * Add the firebase experiments setting + * + * @param experiments The experiments to enable + * @return The builder + */ + public CliBuilder withExperiments(Set experiments) { + this.experiments = new HashSet<>(experiments); + return this; + } + + /** + * Add a single firebase experiment to the set + * + * @param experiment The experiment to add + * @return The builder + */ + public CliBuilder addExperiment(String experiment) { + this.experiments.add(experiment); + return this; + } + /** * Finish the builder * @@ -702,6 +729,7 @@ public Builder done() { Optional.ofNullable(this.javaToolOptions), Optional.ofNullable(this.emulatorData), this.importExport, + Optional.of(this.experiments), this.debug); return Builder.this; } @@ -921,8 +949,10 @@ public FirebaseEmulatorContainer(EmulatorConfig emulatorConfig) { .map(Path::toString) .orElse(new File(FirebaseJsonBuilder.FIREBASE_HOSTING_SUBPATH).getAbsolutePath()); + LOGGER.debug("Mounting {} to the container hosting path", hostingPath); + // Mount volume for static hosting content - this.withFileSystemBind(hostingPath, containerHostingPath(emulatorConfig), BindMode.READ_ONLY); + this.withFileSystemBind(hostingPath, containerHostingPath(emulatorConfig), BindMode.READ_WRITE); } if (this.services.containsKey(Emulator.CLOUD_FUNCTIONS)) { @@ -933,39 +963,62 @@ public FirebaseEmulatorContainer(EmulatorConfig emulatorConfig) { .map(Path::toString) .orElse(new File(FirebaseJsonBuilder.FIREBASE_FUNCTIONS_SUBPATH).getAbsolutePath()); + LOGGER.debug("Mounting {} to the container functions sources path", functionsPath); + // Mount volume for functions - this.withFileSystemBind(functionsPath, containerFunctionsPath(emulatorConfig), BindMode.READ_ONLY); + this.withFileSystemBind(functionsPath, containerFunctionsPath(emulatorConfig), BindMode.READ_WRITE); } } static String containerHostingPath(EmulatorConfig emulatorConfig) { - var hostingPath = emulatorConfig.firebaseConfig().hostingConfig().hostingContentDir(); - if (emulatorConfig.customFirebaseJson().isPresent()) { - var firebaseJsonDir = emulatorConfig.customFirebaseJson().get().getParent(); - hostingPath = hostingPath.map(path -> path.subpath(firebaseJsonDir.getNameCount(), path.getNameCount())); - } + var hostingPath = relativizeToFirebaseJson( + emulatorConfig.firebaseConfig().hostingConfig().hostingContentDir(), + emulatorConfig); + String containerHostingPath; if (hostingPath.isPresent()) { var path = hostingPath.get(); if (path.isAbsolute()) { - return FIREBASE_HOSTING_PATH; + containerHostingPath = FIREBASE_HOSTING_PATH; } else { - return FIREBASE_ROOT + "/" + hostingPath.get(); + containerHostingPath = FIREBASE_ROOT + "/" + hostingPath.get(); } } else { - return FIREBASE_HOSTING_PATH; + containerHostingPath = FIREBASE_HOSTING_PATH; } + + LOGGER.debug("Container hosting path is {}", containerHostingPath); + + return containerHostingPath; } static String containerFunctionsPath(EmulatorConfig emulatorConfig) { - var functionsPath = emulatorConfig.firebaseConfig().functionsConfig().functionsPath(); - if (emulatorConfig.customFirebaseJson().isPresent()) { - var firebaseJsonDir = emulatorConfig.customFirebaseJson().get().getParent(); - functionsPath = functionsPath.map(path -> path.subpath(firebaseJsonDir.getNameCount(), path.getNameCount())); - } - return FIREBASE_ROOT + "/" + functionsPath + var functionsPath = relativizeToFirebaseJson( + emulatorConfig.firebaseConfig().functionsConfig().functionsPath(), + emulatorConfig); + + var containerFunctionsPath = FIREBASE_ROOT + "/" + functionsPath .map(Path::toString) .orElse(FirebaseJsonBuilder.FIREBASE_FUNCTIONS_SUBPATH); + + LOGGER.debug("Container functions path is {}", containerFunctionsPath); + + return containerFunctionsPath; + } + + private static Optional relativizeToFirebaseJson(Optional filePath, EmulatorConfig emulatorConfig) { + if (emulatorConfig.customFirebaseJson().isPresent()) { + var firebaseJsonFile = emulatorConfig.customFirebaseJson().get(); + var nameCount = firebaseJsonFile.getParent() == null ? 0 : firebaseJsonFile.getParent().getNameCount(); + + var result = filePath.map(path -> path.subpath(nameCount, path.getNameCount())); + + LOGGER.debug("Resolved path to be {} relative to the firebase.json file", result); + + return result; + } else { + return filePath; + } } private static class FirebaseDockerBuilder { @@ -998,14 +1051,15 @@ public ImageFromDockerfile build() { this.initialSetup(); this.authenticateToFirebase(); this.setupJavaToolOptions(); - this.setupUserAndGroup(); this.downloadEmulators(); - this.addFirebaseJson(); - this.includeFirestoreFiles(); - this.includeStorageFiles(); + this.setupExperiments(); this.setupDataImportExport(); this.setupHosting(); this.setupFunctions(); + this.addFirebaseJson(); + this.includeFirestoreFiles(); + this.includeStorageFiles(); + this.setupUserAndGroup(); this.runExecutable(); return result; @@ -1036,10 +1090,14 @@ private void validateConfiguration() { } if (emulatorConfig.customFirebaseJson.isPresent()) { - var hostingDirIsAbsolute = emulatorConfig.firebaseConfig.hostingConfig.hostingContentDir + var hostingDir = emulatorConfig.firebaseConfig.hostingConfig.hostingContentDir; + + var hostingDirIsAbsolute = hostingDir .map(Path::isAbsolute) .orElse(false); + LOGGER.debug("Checking if path {} is absolute --> {}", hostingDir, hostingDirIsAbsolute); + if (hostingDirIsAbsolute) { throw new IllegalStateException( "When using a custom firebase.json, the hosting path must be relative to the firebase.json file"); @@ -1047,11 +1105,14 @@ private void validateConfiguration() { var firebasePath = emulatorConfig.customFirebaseJson.get().toAbsolutePath().getParent(); - var hostingDirIsChildOfFirebaseJsonParent = emulatorConfig.firebaseConfig.hostingConfig.hostingContentDir + var hostingDirIsChildOfFirebaseJsonParent = hostingDir .map(Path::toAbsolutePath) .map(h -> h.startsWith(firebasePath)) .orElse(true); + LOGGER.debug("Checking if the hosting path {} is relative to the firebase.json file --> {}", hostingDir, + hostingDirIsChildOfFirebaseJsonParent); + if (!hostingDirIsChildOfFirebaseJsonParent) { throw new IllegalStateException( "When using a custom firebase.json, the hosting path must be in the same subtree as the firebase.json file"); @@ -1059,10 +1120,14 @@ private void validateConfiguration() { } if (emulatorConfig.firebaseConfig.functionsConfig.functionsPath.isPresent()) { - var functionsDirIsAbsolute = emulatorConfig.firebaseConfig.functionsConfig.functionsPath + var functionsDir = emulatorConfig.firebaseConfig.functionsConfig.functionsPath; + var functionsDirIsAbsolute = functionsDir .map(Path::isAbsolute) .orElse(false); + LOGGER.debug("Checking if the functions sources dir {} is absolute --> {}", functionsDir, + functionsDirIsAbsolute); + if (functionsDirIsAbsolute) { throw new IllegalStateException("Functions path cannot be absolute"); } @@ -1117,8 +1182,36 @@ private void setupJavaToolOptions() { toolOptions -> dockerBuilder.env("JAVA_TOOL_OPTIONS", toolOptions)); } + private void setupExperiments() { + emulatorConfig.cliArguments.experiments().ifPresent( + experimentsSet -> { + var experiments = String.join(",", experimentsSet); + LOGGER.debug("Firebase experiments found, enabling experiments: {}", experiments); + dockerBuilder.env("FIREBASE_CLI_EXPERIMENTS", String.join(",", experiments)); + }); + } + private void addFirebaseJson() { - dockerBuilder.workDir(FIREBASE_ROOT); + /* + * Workaround for https://github.com/firebase/firebase-tools/issues/5903#issuecomment-1568239576 + * + * Remove the conditional and just set FIREBASE_ROOT as workdir once the upstream bug is fixed. + */ + if (isEmulatorEnabled(Emulator.FIREBASE_HOSTING)) { + var hostingPath = containerHostingPath(emulatorConfig); + + LOGGER.debug( + "Hosting emulator detected. Setting workdir to {} as a workaround for an upstream bug in firebase-tools", + hostingPath); + + dockerBuilder.workDir(hostingPath); + } else { + LOGGER.debug("No hosting emulator detected. Using default workdir"); + dockerBuilder.workDir(FIREBASE_ROOT); + } + /* + * Workaround ends <-- https://github.com/firebase/firebase-tools/issues/5903#issuecomment-1568239576 + */ emulatorConfig.customFirebaseJson().ifPresentOrElse( this::includeCustomFirebaseJson, @@ -1220,6 +1313,8 @@ private void runExecutable() { List arguments = new ArrayList<>(); arguments.add("emulators:start"); + arguments.add("--config"); + arguments.add(FIREBASE_ROOT + "/firebase.json"); emulatorConfig.cliArguments().projectId() .map(id -> "--project") @@ -1232,7 +1327,9 @@ private void runExecutable() { arguments.add("--debug"); } - if (emulatorConfig.cliArguments().importExport.isDoExport()) { + if (emulatorConfig.cliArguments().importExport.isDoImport()) { + LOGGER.debug("Import requested. Importing data on startup"); + emulatorConfig .cliArguments() .emulatorData() @@ -1251,6 +1348,8 @@ private void runExecutable() { } if (emulatorConfig.cliArguments().importExport.isDoExport()) { + LOGGER.debug("Export requested. Saving data on exit"); + emulatorConfig .cliArguments() .emulatorData() @@ -1304,11 +1403,22 @@ public void stop() { * kill (SIGKILL) command instead of a stop (SIGTERM) command. This will kill the container instantly * and prevent firebase from writing the "--export-on-exit" data to the mounted directory. */ + LOGGER.debug("Requesting to stopping the container to give export a chance to finish"); + this.getDockerClient().stopContainerCmd(this.getContainerId()).exec(); + LOGGER.debug("Stopping abd removing the container"); + super.stop(); } + @Override + public void close() { + LOGGER.debug("Emulator is being closed"); + + this.stop(); + } + /** * Configures the Pub/Sub emulator container. */ @@ -1385,7 +1495,7 @@ private void writeOutputFrame(OutputFrame frame, Level level) { private String getEmulatorEndpoint(Emulator emulator) { var endpoint = this.getHost() + ":" + emulatorPort(emulator); - if (emulator.equals(Emulator.REALTIME_DATABASE)) { + if (emulator.equals(Emulator.REALTIME_DATABASE) || emulator.equals(Emulator.CLOUD_STORAGE)) { endpoint = "http://" + endpoint; } return endpoint; diff --git a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilderTest.java b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilderTest.java index 49397aca..60febcea 100644 --- a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilderTest.java +++ b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilderTest.java @@ -5,6 +5,7 @@ import java.nio.file.Path; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,6 +38,7 @@ void setUp() { Optional.of("-Xmx"), Optional.of("data"), Optional.of(FirebaseEmulatorContainer.ImportExport.EXPORT_ONLY), + Optional.of(Set.of("webframeworks")), Optional.of(true)), Optional.empty(), new TestUI( @@ -89,6 +91,7 @@ void testBuild() { assertEquals("MY_TOKEN", emulatorConfig.cliArguments().token().orElse(null)); assertEquals("-Xmx", emulatorConfig.cliArguments().javaToolOptions().orElse(null)); assertPathEndsWith("data", emulatorConfig.cliArguments().emulatorData().orElse(null)); + assertEquals(Set.of("webframeworks"), emulatorConfig.cliArguments().experiments().orElse(null)); assertEquals(FirebaseEmulatorContainer.ImportExport.EXPORT_ONLY, emulatorConfig.cliArguments().importExport()); assertTrue(emulatorConfig.cliArguments().debug()); @@ -174,6 +177,7 @@ record TestCli( Optional javaToolOptions, Optional emulatorData, Optional importExport, + Optional> experiments, Optional debug) implements FirebaseDevServiceConfig.Firebase.Emulator.Cli { } diff --git a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerCustomConfigTest.java b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerCustomConfigTest.java index 77312a87..c0534afa 100644 --- a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerCustomConfigTest.java +++ b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerCustomConfigTest.java @@ -10,6 +10,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.Set; import org.junit.jupiter.api.Test; import org.testcontainers.junit.jupiter.Container; @@ -29,6 +30,7 @@ public class FirebaseEmulatorContainerCustomConfigTest { firebaseContainer = testContainer.testBuilder() .withCliArguments() .withEmulatorData(tempEmulatorDataDir.toPath()) + .withExperiments(Set.of("webframeworks")) .done() .readFromFirebaseJson(new File("src/test/firebase.json").toPath()) .build(); diff --git a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerIntegrationTest.java b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerIntegrationTest.java index 122cbba3..bf7a85bf 100644 --- a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerIntegrationTest.java +++ b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerIntegrationTest.java @@ -153,6 +153,11 @@ private static void validateEmulatorDataWritten() { File[] files = emulatorDataDir.listFiles(); assertNotNull(files); assertTrue(files.length > 0, "Expected files to be present in the emulator data directory"); + + // Verify storage files are written + File[] storageFiles = new File(emulatorDataDir, "storage_export/blobs").listFiles(); + assertNotNull(storageFiles); + assertTrue(storageFiles.length > 0, "Expected storage files to be present in the storage data directory"); } @Test diff --git a/integration-tests/firebase/src/main/resources/application.properties b/integration-tests/firebase/src/main/resources/application.properties index 7fdb657a..c93b2805 100644 --- a/integration-tests/firebase/src/main/resources/application.properties +++ b/integration-tests/firebase/src/main/resources/application.properties @@ -7,3 +7,4 @@ quarkus.google.cloud.devservices.firebase.auth.enabled=true quarkus.google.cloud.devservices.firebase.firestore.enabled=true quarkus.google.cloud.devservices.firebase.database.enabled=true quarkus.google.cloud.devservices.pubsub.enabled=true +quarkus.google.cloud.devservices.firebase.emulator.cli.experiments=webframeworks diff --git a/pom.xml b/pom.xml index ec8b14f1..83111766 100644 --- a/pom.xml +++ b/pom.xml @@ -14,8 +14,8 @@ 3.17.2 3.5.0 - 3.27.1 - 1.45.0-alpha + 3.27.2 + 1.46.0-alpha scm:git:git@github.com:quarkiverse/quarkus-google-cloud-services.git diff --git a/pubsub/deployment/src/main/java/io/quarkiverse/googlecloudservices/pubsub/deployment/PubSubBuildSteps.java b/pubsub/deployment/src/main/java/io/quarkiverse/googlecloudservices/pubsub/deployment/PubSubBuildSteps.java index 13e5741e..bffc8936 100644 --- a/pubsub/deployment/src/main/java/io/quarkiverse/googlecloudservices/pubsub/deployment/PubSubBuildSteps.java +++ b/pubsub/deployment/src/main/java/io/quarkiverse/googlecloudservices/pubsub/deployment/PubSubBuildSteps.java @@ -1,5 +1,6 @@ package io.quarkiverse.googlecloudservices.pubsub.deployment; +import io.quarkiverse.googlecloudservices.pubsub.PubSubProducer; import io.quarkiverse.googlecloudservices.pubsub.QuarkusPubSub; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildStep; @@ -15,6 +16,6 @@ public FeatureBuildItem feature() { @BuildStep public AdditionalBeanBuildItem producer() { - return new AdditionalBeanBuildItem(QuarkusPubSub.class); + return new AdditionalBeanBuildItem(QuarkusPubSub.class, PubSubProducer.class); } } diff --git a/pubsub/runtime/src/main/java/io/quarkiverse/googlecloudservices/pubsub/PubSubProducer.java b/pubsub/runtime/src/main/java/io/quarkiverse/googlecloudservices/pubsub/PubSubProducer.java new file mode 100644 index 00000000..92b3f764 --- /dev/null +++ b/pubsub/runtime/src/main/java/io/quarkiverse/googlecloudservices/pubsub/PubSubProducer.java @@ -0,0 +1,50 @@ +package io.quarkiverse.googlecloudservices.pubsub; + +import java.io.IOException; + +import jakarta.enterprise.inject.Disposes; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import com.google.cloud.pubsub.v1.SubscriptionAdminClient; +import com.google.cloud.pubsub.v1.TopicAdminClient; + +/** + * Producer class for PubSub beans. + */ +public class PubSubProducer { + + @Inject + QuarkusPubSub quarkusPubSub; + + /** + * Makes the subscription admin client available as CDI bean + */ + @Produces + public SubscriptionAdminClient subscriptionAdminClient() throws IOException { + return SubscriptionAdminClient.create(quarkusPubSub.subscriptionAdminSettings()); + } + + /** + * CDI Dispose method for {@link #subscriptionAdminClient()}. Shouldn't be called directly + */ + public void shutdownSubscriptionAdminClient(@Disposes SubscriptionAdminClient subscriptionAdminClient) { + subscriptionAdminClient.close(); + } + + /** + * Makes the topic admin client available as a CDI bean + */ + @Produces + public TopicAdminClient topicAdminClient() throws IOException { + return TopicAdminClient.create(quarkusPubSub.topicAdminSettings()); + } + + /** + * CDI Dispose method for {@link #topicAdminClient()}. Shouldn't be called directly + */ + public void shutdownTopicAdminClient(@Disposes TopicAdminClient topicAdminClient) { + topicAdminClient.close(); + } + +}