diff --git a/.github/workflows/ci-actions.yml b/.github/workflows/ci-actions.yml
index eb56fd935b323..3248a51271fb6 100644
--- a/.github/workflows/ci-actions.yml
+++ b/.github/workflows/ci-actions.yml
@@ -108,13 +108,13 @@ jobs:
java :
- { name: Java8,
java-version: 8,
- maven_args: "-pl !integration-tests/vault-app,!integration-tests/vault-agroal,!integration-tests/vault,!integration-tests/google-cloud-functions-http,!integration-tests/gradle,!integration-tests/google-cloud-functions"
+ maven_args: "-pl !integration-tests/vault-app,!integration-tests/vault-agroal,!integration-tests/vault,!integration-tests/google-cloud-functions-http,!integration-tests/gradle,!integration-tests/google-cloud-functions,!integration-tests/funqy-google-cloud-functions"
}
- {
name: "Java 8 - 242",
java-version: 8,
release: "jdk8u242-b08",
- maven_args: "-pl !integration-tests/google-cloud-functions-http,!integration-tests/gradle,!integration-tests/google-cloud-functions"
+ maven_args: "-pl !integration-tests/google-cloud-functions-http,!integration-tests/gradle,!integration-tests/google-cloud-functions,!integration-tests/funqy-google-cloud-functions"
}
- {
name: Java 11,
diff --git a/.github/workflows/native-cron-build.yml b/.github/workflows/native-cron-build.yml
index fde4700d89e6b..7cb55e2f7fa0f 100644
--- a/.github/workflows/native-cron-build.yml
+++ b/.github/workflows/native-cron-build.yml
@@ -54,7 +54,7 @@ jobs:
run: mvn -B install -DskipTests -DskipITs -Dformat.skip
- name: Run integration tests in native
- run: mvn -B --settings .github/mvn-settings.xml verify -f integration-tests/pom.xml --fail-at-end -Dno-format -Ddocker -Dnative -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-native-image:20.1.0-java${{ matrix.java }} -Dtest-postgresql -Dtest-elasticsearch -Dtest-mysql -Dtest-db2 -Dtest-amazon-services -Dtest-vault -Dtest-neo4j -Dtest-keycloak -Dtest-kafka -Dtest-mssql -Dtest-mariadb -Dmariadb.url="jdbc:mariadb://localhost:3308/hibernate_orm_test" -pl '!io.quarkus:quarkus-integration-test-google-cloud-functions-http,!io.quarkus:quarkus-integration-test-google-cloud-functions'
+ run: mvn -B --settings .github/mvn-settings.xml verify -f integration-tests/pom.xml --fail-at-end -Dno-format -Ddocker -Dnative -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-native-image:20.1.0-java${{ matrix.java }} -Dtest-postgresql -Dtest-elasticsearch -Dtest-mysql -Dtest-db2 -Dtest-amazon-services -Dtest-vault -Dtest-neo4j -Dtest-keycloak -Dtest-kafka -Dtest-mssql -Dtest-mariadb -Dmariadb.url="jdbc:mariadb://localhost:3308/hibernate_orm_test" -pl '!io.quarkus:quarkus-integration-test-google-cloud-functions-http,!io.quarkus:quarkus-integration-test-google-cloud-functions,!integration-tests/funqy-google-cloud-function'
- name: Report
if: always()
diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml
index a7a53a1174423..ddc4eb0410e3f 100644
--- a/bom/runtime/pom.xml
+++ b/bom/runtime/pom.xml
@@ -1344,7 +1344,12 @@
io.quarkus
- quarkus-funqy-amazon-lambda-deployment
+ quarkus-funqy-google-cloud-functions
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-funqy-google-cloud-functions-deployment
${project.version}
diff --git a/docs/src/main/asciidoc/funqy-gcp-functions.adoc b/docs/src/main/asciidoc/funqy-gcp-functions.adoc
new file mode 100644
index 0000000000000..50df2cb7fec52
--- /dev/null
+++ b/docs/src/main/asciidoc/funqy-gcp-functions.adoc
@@ -0,0 +1,268 @@
+////
+This guide is maintained in the main Quarkus repository
+and pull requests should be submitted there:
+https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc
+////
+= Quarkus - Funqy Google Cloud Functions
+:extension-status: experimental
+
+include::./attributes.adoc[]
+
+The guide walks through quickstart code to show you how you can deploy Funqy functions to Google Cloud Functions.
+
+As the Google Cloud Function Java engine is a new Beta feature of Google Cloud, this extension is flagged as experimental.
+
+include::./status-include.adoc[]
+
+== Prerequisites
+
+To complete this guide, you need:
+
+* less than 30 minutes
+* JDK 11 (Google Cloud Functions requires JDK 11)
+* Apache Maven {maven-version}
+* https://cloud.google.com/[A Google Cloud Account]. Free accounts work.
+* https://cloud.google.com/sdk[Cloud SDK CLI Installed]
+
+== Login to Google Cloud
+
+Login to Google Cloud is necessary for deploying the application and it can be done as follows:
+
+[source, subs=attributes+]
+----
+gcloud auth login
+----
+
+At the time of this writing, Cloud Functions are still in beta so make sure to install the `beta` command group.
+
+[source, subs=attributes+]
+----
+gcloud components install beta
+----
+
+== The Code
+
+There is nothing special about the code and more importantly nothing Google Cloud specific. Funqy functions can be deployed to many different
+environments and Google Cloud Functions is one of them.
+
+[[choose]]
+== Choose Your Function
+
+Only one Funqy function can be exported per Google Cloud Functions deployment. If you only have one method
+annotated with `@Funq` in your project, then there is no worries. If you have multiple functions defined
+within your project, then you will need to choose the function within your Quarkus `application.properties`:
+
+[source, subs=attributes+]
+----
+quarkus.funqy.export=greet
+----
+
+Alternatively, you can set the `QUARKUS_FUNQY_EXPORT` environment variable when you create the Google Cloud Function using the `gcloud` cli.
+
+== Build and Deploy
+
+Build the project using maven.
+
+[source, subs=attributes+]
+----
+./mvnw clean package
+----
+
+This will compile and package your code.
+
+
+== Create the function
+
+In this example, we will create two background functions. Background functions allow to
+react to Google Cloud events like PubSub messages, Cloud Storage events, Firestore events, ...
+
+[source,java]
+----
+import javax.inject.Inject;
+
+import io.quarkus.funqy.Funq;
+import io.quarkus.funqy.gcp.functions.event.PubsubMessage;
+import io.quarkus.funqy.gcp.functions.event.StorageEvent;
+
+public class GreetingFunctions {
+
+ @Inject
+ GreetingService service;
+
+ @Funq // <1>
+ public void helloPubSubWorld(PubsubMessage pubSubEvent) {
+ String message = service.hello("world");
+ System.out.println(pubSubEvent.messageId + " - " + message);
+ }
+
+ @Funq // <2>
+ public void helloGCSWorld(StorageEvent storageEvent) {
+ String message = service.hello("world");
+ System.out.println(storageEvent.name + " - " + message);
+ }
+
+}
+----
+
+NOTE: Function return type can also be Mutiny reactive types.
+
+1. This is a background function that takes as parameter a `io.quarkus.funqy.gcp.functions.event.PubsubMessage`,
+this is a convenient class to deserialize a PubSub message.
+2. This is a background function that takes as parameter a `io.quarkus.funqy.gcp.functions.event.StorageEvent`,
+this is a convenient class to deserialize a Google Storage event.
+
+NOTE: we provide convenience class to deserialize common Google Cloud event inside the `io.quarkus.funqy.gcp.functions.event` package.
+They are not mandatory to use, you can use any object you want.
+
+As our project contains multiple function, we need to specify which function needs to be deployed via the following property inside our `application.properties` :
+
+[source,property]
+----
+quarkus.funqy.export=helloHttpWorld
+----
+
+== Build and Deploy to Google Cloud
+
+To build your application, you can package your application via `mvn clean package`.
+You will have a single JAR inside the `target/deployment` repository that contains your classes and all your dependencies in it.
+
+Then you will be able to use `gcloud` to deploy your function to Google Cloud, the `gcloud` command will be different depending from which event you want to be triggered.
+
+[WARNING]
+====
+The first time you launch the `gcloud beta functions deploy`, you can have the following error message:
+```
+ERROR: (gcloud.beta.functions.deploy) OperationError: code=7, message=Build Failed: Cloud Build has not been used in project before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudbuild.googleapis.com/overview?project= then retry.
+```
+This means that Cloud Build is not activated yet. To overcome this error, open the URL shown in the error, follow the instructions and then wait a few minutes before retrying the command.
+====
+
+=== Background Functions - PubSub
+
+Use this command to deploy to Google Cloud Functions:
+
+[source]
+----
+gcloud beta functions deploy quarkus-example-funky-pubsub \
+ --entry-point=io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction \
+ --runtime=java11 --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish \
+ --source=target/deployment
+----
+
+The entry point always needs to be `io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction` as it will be this class
+that will bootstrap Quarkus.
+
+The `--trigger-resource` option defines the name of the PubSub topic, and the `--trigger-event google.pubsub.topic.publish` option
+define that this function will be triggered by all message publication inside the topic.
+
+To trigger an event to this function, you can use the `gcloud functions call` command:
+
+[source]
+----
+gcloud functions call quarkus-example-funky-pubsub --data '{"data":"Hello, Pub/Sub"}'
+----
+
+The `--data '{"data":"Hello, Pub/Sub"}'` option allow to specify the message to be send to PubSub.
+
+=== Background Functions - Cloud Storage
+
+Before deploying your function, you need to create a bucket.
+
+[source]
+----
+gsutil mb gs://quarkus-hello
+----
+
+Then, use this command to deploy to Google Cloud Functions:
+
+[source]
+----
+gcloud beta functions deploy quarkus-example-funky-storage \
+ --entry-point=io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction \
+ --runtime=java11 --trigger-resource quarkus-hello --trigger-event google.storage.object.finalize \
+ --source=target/deployment
+----
+
+The entry point always needs to be `io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction` as it will be this class
+that will bootstrap Quarkus.
+
+The `--trigger-resource` option defines the name of the Cloud Storage bucket, and the `--trigger-event google.storage.object.finalize` option
+define that this function will be triggered by all new file inside this bucket.
+
+To trigger an event to this function, you can use the `gcloud functions call` command:
+
+[source]
+----
+gcloud functions call quarkus-example-funky-pubsub --data '{"name":"test.txt"}'
+----
+
+The `--data '{"name":"test.txt"}'` option allow to specify a fake file name, a fake Cloud Storage event will be created for this name.
+
+You can also simply add a file to Cloud Storage using the command line of the web console.
+
+== Testing locally
+
+The easiest way to locally test your function is using the Cloud Function invoker JAR.
+
+You can download it via Maven using the following command:
+
+[source]
+----
+mvn dependency:copy \
+ -Dartifact='com.google.cloud.functions.invoker:java-function-invoker:1.0.0-beta1' \
+ -DoutputDirectory=.
+----
+
+Then you can use it to launch your function locally, again, the command depends on the type of function and the type of events.
+
+=== Background Functions - PubSub
+
+For background functions, you launch the invoker with a target class of `io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction`.
+
+[source]
+----
+java -jar java-function-invoker-1.0.0-beta1.jar \
+ --classpath target/funqy-google-cloud-functions-1.0-SNAPSHOT-runner.jar \
+ --target io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction
+----
+
+Then you can call your background function via an HTTP call with a payload containing the event:
+
+[source]
+----
+curl localhost:8080 -d '{"data":{"data":"hello"}}'
+----
+
+This will call your PubSub background function with a PubSubMessage `{"data":"hello"}`.
+
+=== Background Functions - Cloud Storage
+
+For background functions, you launch the invoker with a target class of `io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction`.
+
+[source]
+----
+java -jar java-function-invoker-1.0.0-beta1.jar \
+ --classpath target/funqy-google-cloud-functions-1.0-SNAPSHOT-runner.jar \
+ --target io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction
+----
+
+Then you can call your background function via an HTTP call with a payload containing the event:
+
+[source]
+----
+curl localhost:8080 -d '{"data":{"name":"text"}}'
+----
+
+This will call your PubSub background function with a Cloud Storage event `{"name":"file.txt"}`, so an event on the `file.txt` file.
+
+
+== Deploying HTTP Functions via Funqy
+
+You can use link:funqy-http[Funqy HTTP] on Google Cloud Functions.
+This allows you to invoke on multiple Funqy functions using HTTP deployed as one Google Cloud Function.
+
+For this you need to include both `quarkus-funqy-http` and `quarkus-google-cloud-functions` extension.
+
+== What's next?
+
+If you are looking for JAX-RS, Servlet or Vert.x support for Google Cloud Functions, we have it thanks to our link:gcp-functions-http[Google Cloud Functions HTTP binding].
diff --git a/docs/src/main/asciidoc/gcp-functions-http.adoc b/docs/src/main/asciidoc/gcp-functions-http.adoc
index c2f444ca993ca..e5ed26d5baaec 100644
--- a/docs/src/main/asciidoc/gcp-functions-http.adoc
+++ b/docs/src/main/asciidoc/gcp-functions-http.adoc
@@ -203,3 +203,8 @@ java -jar java-function-invoker-1.0.0-beta1.jar \
----
Your endpoints will be available on http://localhost:8080.
+
+== What's next?
+
+You can use our link:funqy-gcp-functions[Google Cloud Functions Funqy binding] to use Funqy,
+a provider agnostic function as a service framework, that allow to deploy HTTP function or Background function to Google Cloud.
diff --git a/extensions/funqy/funqy-google-cloud-functions/deployment/pom.xml b/extensions/funqy/funqy-google-cloud-functions/deployment/pom.xml
new file mode 100644
index 0000000000000..22dddb34c8a97
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/deployment/pom.xml
@@ -0,0 +1,48 @@
+
+
+
+ quarkus-funqy-google-cloud-functions-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-funqy-google-cloud-functions-deployment
+ Quarkus - Funqy Google Cloud Functions - Deployment
+
+
+
+ io.quarkus
+ quarkus-funqy-server-common-deployment
+
+
+ io.quarkus
+ quarkus-jackson-deployment
+
+
+ io.quarkus
+ quarkus-funqy-google-cloud-functions
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
+
diff --git a/extensions/funqy/funqy-google-cloud-functions/deployment/src/main/java/io/quarkus/funqy/gcp/functions/deployment/bindings/CloudFunctionsDeploymentBuildStep.java b/extensions/funqy/funqy-google-cloud-functions/deployment/src/main/java/io/quarkus/funqy/gcp/functions/deployment/bindings/CloudFunctionsDeploymentBuildStep.java
new file mode 100644
index 0000000000000..c612499472289
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/deployment/src/main/java/io/quarkus/funqy/gcp/functions/deployment/bindings/CloudFunctionsDeploymentBuildStep.java
@@ -0,0 +1,50 @@
+package io.quarkus.funqy.gcp.functions.deployment.bindings;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+
+import io.quarkus.builder.BuildException;
+import io.quarkus.deployment.IsNormal;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem;
+import io.quarkus.deployment.pkg.builditem.JarBuildItem;
+import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
+import io.quarkus.deployment.pkg.builditem.UberJarRequiredBuildItem;
+import io.quarkus.deployment.pkg.steps.NativeBuild;
+
+public class CloudFunctionsDeploymentBuildStep {
+
+ @BuildStep
+ public UberJarRequiredBuildItem forceUberJar() {
+ // Google Cloud Function needs a single JAR inside a dedicated directory
+ return new UberJarRequiredBuildItem();
+ }
+
+ /**
+ * Creates a target/deployment dir and copy the uber jar in it.
+ * This facilitates the usage of the 'glcoud' command.
+ */
+ @BuildStep(onlyIf = IsNormal.class, onlyIfNot = NativeBuild.class)
+ public ArtifactResultBuildItem functionDeployment(OutputTargetBuildItem target, JarBuildItem jar)
+ throws BuildException, IOException {
+ if (!jar.isUberJar()) {
+ throw new BuildException("Google Cloud Function deployment need to use a uberjar, " +
+ "please set 'quarkus.package.uber-jar=true' inside your application.properties",
+ Collections.EMPTY_LIST);
+ }
+
+ Path deployment = target.getOutputDirectory().resolve("deployment");
+ if (Files.notExists(deployment)) {
+ Files.createDirectory(deployment);
+ }
+
+ Path jarPath = jar.getPath();
+ Path targetJarPath = deployment.resolve(jarPath.getFileName());
+ Files.deleteIfExists(targetJarPath);
+ Files.copy(jarPath, targetJarPath);
+
+ return new ArtifactResultBuildItem(targetJarPath, "function", Collections.EMPTY_MAP);
+ }
+}
diff --git a/extensions/funqy/funqy-google-cloud-functions/deployment/src/main/java/io/quarkus/funqy/gcp/functions/deployment/bindings/FunqyCloudFunctionsBuildStep.java b/extensions/funqy/funqy-google-cloud-functions/deployment/src/main/java/io/quarkus/funqy/gcp/functions/deployment/bindings/FunqyCloudFunctionsBuildStep.java
new file mode 100644
index 0000000000000..81a328b6f1924
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/deployment/src/main/java/io/quarkus/funqy/gcp/functions/deployment/bindings/FunqyCloudFunctionsBuildStep.java
@@ -0,0 +1,50 @@
+package io.quarkus.funqy.gcp.functions.deployment.bindings;
+
+import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
+import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.quarkus.arc.deployment.BeanContainerBuildItem;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem;
+import io.quarkus.funqy.deployment.FunctionBuildItem;
+import io.quarkus.funqy.deployment.FunctionInitializedBuildItem;
+import io.quarkus.funqy.gcp.functions.FunqyCloudFunctionsBindingRecorder;
+import io.quarkus.funqy.runtime.FunqyConfig;
+
+public class FunqyCloudFunctionsBuildStep {
+ private static final String FEATURE_NAME = "funqy-google-cloud-functions";
+
+ @BuildStep
+ public FeatureBuildItem feature() {
+ return new FeatureBuildItem(FEATURE_NAME);
+ }
+
+ @BuildStep
+ public RunTimeConfigurationDefaultBuildItem disableBanner() {
+ // the banner is not displayed well inside the Google Cloud Function logs
+ return new RunTimeConfigurationDefaultBuildItem("quarkus.banner.enabled", "false");
+ }
+
+ @BuildStep
+ @Record(STATIC_INIT)
+ public void init(List functions,
+ FunqyCloudFunctionsBindingRecorder recorder,
+ Optional hasFunctions,
+ BeanContainerBuildItem beanContainer) throws Exception {
+ if (!hasFunctions.isPresent() || hasFunctions.get() == null)
+ return;
+
+ recorder.init(beanContainer.getValue());
+ }
+
+ @BuildStep
+ @Record(RUNTIME_INIT)
+ public void choose(FunqyConfig config, FunqyCloudFunctionsBindingRecorder recorder) {
+ recorder.chooseInvoker(config);
+ }
+}
diff --git a/extensions/funqy/funqy-google-cloud-functions/pom.xml b/extensions/funqy/funqy-google-cloud-functions/pom.xml
new file mode 100644
index 0000000000000..f6e615b817ab8
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ quarkus-build-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../../../build-parent/pom.xml
+
+ 4.0.0
+
+ quarkus-funqy-google-cloud-functions-parent
+ Quarkus - Funqy Google Cloud Functions Binding
+ pom
+
+ runtime
+ deployment
+
+
+
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/pom.xml b/extensions/funqy/funqy-google-cloud-functions/runtime/pom.xml
new file mode 100644
index 0000000000000..144a5646165e8
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/pom.xml
@@ -0,0 +1,53 @@
+
+
+
+ quarkus-funqy-google-cloud-functions-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-funqy-google-cloud-functions
+ Quarkus - Funqy Google Cloud Functions - Runtime
+ Google Cloud Functions Binding for Quarkus Funqy framework
+
+
+
+ io.quarkus
+ quarkus-funqy-server-common
+
+
+ io.quarkus
+ quarkus-jackson
+
+
+ com.google.cloud.functions
+ functions-framework-api
+ compile
+
+
+
+
+
+
+ io.quarkus
+ quarkus-bootstrap-maven-plugin
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyBackgroundFunction.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyBackgroundFunction.java
new file mode 100644
index 0000000000000..8e612af8c7704
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyBackgroundFunction.java
@@ -0,0 +1,57 @@
+package io.quarkus.funqy.gcp.functions;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import com.google.cloud.functions.Context;
+import com.google.cloud.functions.RawBackgroundFunction;
+
+import io.quarkus.runtime.Application;
+
+public class FunqyBackgroundFunction implements RawBackgroundFunction {
+ protected static final String deploymentStatus;
+ protected static boolean started = false;
+
+ static {
+ StringWriter error = new StringWriter();
+ PrintWriter errorWriter = new PrintWriter(error, true);
+ if (Application.currentApplication() == null) { // were we already bootstrapped? Needed for mock unit testing.
+ // For GCP functions, we need to set the TCCL to the QuarkusHttpFunction classloader then restore it.
+ // Without this, we have a lot of classloading issues (ClassNotFoundException on existing classes)
+ // during static init.
+ ClassLoader currentCl = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(FunqyBackgroundFunction.class.getClassLoader());
+ Class> appClass = Class.forName("io.quarkus.runner.ApplicationImpl");
+ String[] args = {};
+ Application app = (Application) appClass.newInstance();
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ @Override
+ public void run() {
+ app.stop();
+ }
+ });
+ app.start(args);
+ errorWriter.println("Quarkus bootstrapped successfully.");
+ started = true;
+ } catch (Exception ex) {
+ errorWriter.println("Quarkus bootstrap failed.");
+ ex.printStackTrace(errorWriter);
+ } finally {
+ Thread.currentThread().setContextClassLoader(currentCl);
+ }
+ } else {
+ errorWriter.println("Quarkus bootstrapped successfully.");
+ started = true;
+ }
+ deploymentStatus = error.toString();
+ }
+
+ @Override
+ public void accept(String event, Context context) {
+ if (!started) {
+ throw new RuntimeException(deploymentStatus);
+ }
+ FunqyCloudFunctionsBindingRecorder.handle(event, context);
+ }
+}
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudFunctionsBindingRecorder.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudFunctionsBindingRecorder.java
new file mode 100644
index 0000000000000..5c73cad4a35b9
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyCloudFunctionsBindingRecorder.java
@@ -0,0 +1,111 @@
+package io.quarkus.funqy.gcp.functions;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.google.cloud.functions.Context;
+
+import io.quarkus.arc.ManagedContext;
+import io.quarkus.arc.runtime.BeanContainer;
+import io.quarkus.funqy.runtime.FunctionConstructor;
+import io.quarkus.funqy.runtime.FunctionInvoker;
+import io.quarkus.funqy.runtime.FunctionRecorder;
+import io.quarkus.funqy.runtime.FunqyConfig;
+import io.quarkus.funqy.runtime.FunqyServerResponse;
+import io.quarkus.funqy.runtime.RequestContextImpl;
+import io.quarkus.runtime.annotations.Recorder;
+
+/**
+ * Provides the runtime methods to bootstrap Quarkus Funqy
+ */
+@Recorder
+public class FunqyCloudFunctionsBindingRecorder {
+ private static FunctionInvoker invoker;
+ private static BeanContainer beanContainer;
+ private static ObjectMapper objectMapper;
+ private static ObjectReader reader;
+ private static ObjectWriter writer;
+
+ public void init(BeanContainer bc) {
+ beanContainer = bc;
+ objectMapper = beanContainer.instance(ObjectMapper.class);
+
+ for (FunctionInvoker invoker : FunctionRecorder.registry.invokers()) {
+ if (invoker.hasInput()) {
+ ObjectReader reader = objectMapper.readerFor(invoker.getInputType());
+ invoker.getBindingContext().put(ObjectReader.class.getName(), reader);
+ }
+ if (invoker.hasOutput()) {
+ ObjectWriter writer = objectMapper.writerFor(invoker.getOutputType());
+ invoker.getBindingContext().put(ObjectWriter.class.getName(), writer);
+ }
+ }
+
+ FunctionConstructor.CONTAINER = bc;
+ }
+
+ public void chooseInvoker(FunqyConfig config) {
+ // this is done at Runtime so that we can change it with an environment variable.
+ if (config.export.isPresent()) {
+ invoker = FunctionRecorder.registry.matchInvoker(config.export.get());
+ if (invoker == null) {
+ throw new RuntimeException("quarkus.funqy.export does not match a function: " + config.export.get());
+ }
+ } else if (FunctionRecorder.registry.invokers().size() == 0) {
+ throw new RuntimeException("There are no functions to process lambda");
+
+ } else if (FunctionRecorder.registry.invokers().size() > 1) {
+ throw new RuntimeException("Too many functions. You need to set quarkus.funqy.export");
+ } else {
+ invoker = FunctionRecorder.registry.invokers().iterator().next();
+ }
+ if (invoker.hasInput()) {
+ reader = (ObjectReader) invoker.getBindingContext().get(ObjectReader.class.getName());
+ }
+ if (invoker.hasOutput()) {
+ writer = (ObjectWriter) invoker.getBindingContext().get(ObjectWriter.class.getName());
+ }
+ }
+
+ /**
+ * Handle RawBackgroundFunction
+ *
+ * @param event
+ * @param context
+ */
+ public static void handle(String event, Context context) {
+ //TODO allow to access the context from the function somehow.
+ try {
+ Object input = null;
+ if (invoker.hasInput()) {
+ input = reader.readValue(event);
+ }
+ FunqyServerResponse response = dispatch(input);
+
+ Object value = response.getOutput().await().indefinitely();
+ if (value != null) {
+ throw new RuntimeException("A background function cannot return a value");
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static FunqyServerResponse dispatch(Object input) {
+ ManagedContext requestContext = beanContainer.requestContext();
+ requestContext.activate();
+ try {
+ FunqyRequestImpl funqyRequest = new FunqyRequestImpl(new RequestContextImpl(), input);
+ FunqyResponseImpl funqyResponse = new FunqyResponseImpl();
+ invoker.invoke(funqyRequest, funqyResponse);
+ return funqyResponse;
+ } finally {
+ if (requestContext.isActive()) {
+ requestContext.terminate();
+ }
+ }
+ }
+
+}
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyRequestImpl.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyRequestImpl.java
new file mode 100644
index 0000000000000..8b0854f525d1a
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyRequestImpl.java
@@ -0,0 +1,24 @@
+package io.quarkus.funqy.gcp.functions;
+
+import io.quarkus.funqy.runtime.FunqyServerRequest;
+import io.quarkus.funqy.runtime.RequestContext;
+
+public class FunqyRequestImpl implements FunqyServerRequest {
+ protected RequestContext requestContext;
+ protected Object input;
+
+ public FunqyRequestImpl(RequestContext requestContext, Object input) {
+ this.requestContext = requestContext;
+ this.input = input;
+ }
+
+ @Override
+ public RequestContext context() {
+ return requestContext;
+ }
+
+ @Override
+ public Object extractInput(Class inputClass) {
+ return input;
+ }
+}
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyResponseImpl.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyResponseImpl.java
new file mode 100644
index 0000000000000..c4ea04726a157
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/FunqyResponseImpl.java
@@ -0,0 +1,18 @@
+package io.quarkus.funqy.gcp.functions;
+
+import io.quarkus.funqy.runtime.FunqyServerResponse;
+import io.smallrye.mutiny.Uni;
+
+public class FunqyResponseImpl implements FunqyServerResponse {
+ protected Uni> output;
+
+ @Override
+ public Uni> getOutput() {
+ return output;
+ }
+
+ @Override
+ public void setOutput(Uni> output) {
+ this.output = output;
+ }
+}
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/FirestoreEvent.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/FirestoreEvent.java
new file mode 100644
index 0000000000000..e534cca9b4827
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/FirestoreEvent.java
@@ -0,0 +1,26 @@
+package io.quarkus.funqy.gcp.functions.event;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * Background function event for Firestore
+ *
+ * @see Firestore event structure
+ */
+public class FirestoreEvent {
+ public Document oldValue;
+ public Document value;
+ public UpdateMask updateMask;
+
+ public static class Document {
+ public LocalDateTime createTime;
+ public String fields;
+ public String name;
+ public LocalDateTime updateTime;
+ }
+
+ public static class UpdateMask {
+ public List fieldPaths;
+ }
+}
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/PubsubMessage.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/PubsubMessage.java
new file mode 100644
index 0000000000000..b863c388e18f7
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/PubsubMessage.java
@@ -0,0 +1,15 @@
+package io.quarkus.funqy.gcp.functions.event;
+
+import java.util.Map;
+
+/**
+ * Background function event for Pubsub
+ *
+ * @see PubsubMessage
+ */
+public class PubsubMessage {
+ public String data;
+ public Map attributes;
+ public String messageId;
+ public String publishTime;
+}
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/StorageEvent.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/StorageEvent.java
new file mode 100644
index 0000000000000..8919192991e50
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/StorageEvent.java
@@ -0,0 +1,51 @@
+package io.quarkus.funqy.gcp.functions.event;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * Background function event for Storage
+ *
+ * @see Storage resource object
+ */
+public class StorageEvent {
+ public String id;
+ public String selfLink;
+ public String name;
+ public String bucket;
+ public long generation;
+ public long metageneration;
+ public String contentType;
+ public LocalDateTime timeCreated;
+ public LocalDateTime updated;
+ public LocalDateTime timeDeleted;
+ public boolean temporaryHold;
+ public boolean eventBasedHold;
+ public LocalDateTime retentionExpirationTime;
+ public String storageClass;
+ public LocalDateTime timeStorageClassUpdated;
+ public long size;
+ public String md5Hash;
+ public String mediaLink;
+ public String contentEncoding;
+ public String contentDisposition;
+ public String contentLanguage;
+ public String cacheControl;
+ public Map metadata;
+ public Owner owner;
+ public String crc32c;
+ public int componentCount;
+ public String etag;
+ public CustomerEncryption customerEncryption;
+ public String kmsKeyName;
+
+ public static class Owner {
+ public String entity;
+ public String entityId;
+ }
+
+ public static class CustomerEncryption {
+ public String encryptionAlgorithm;
+ public String keySha256;
+ }
+}
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/package-info.java b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/package-info.java
new file mode 100644
index 0000000000000..b9f675fd3dd23
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/java/io/quarkus/funqy/gcp/functions/event/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * This package contains helper class to deserialize the Google Cloud Function events for background functions.
+ *
+ * Usage of these POJO is not mandatory, you can create your own version if you prefer not to deserialize all fields of the
+ * events.
+ *
+ * @see Google Functions events triggers
+ */
+package io.quarkus.funqy.gcp.functions.event;
\ No newline at end of file
diff --git a/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 0000000000000..f829017a6ee6c
--- /dev/null
+++ b/extensions/funqy/funqy-google-cloud-functions/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,14 @@
+---
+name: "Funqy Google Cloud Functions"
+metadata:
+ keywords:
+ - "google cloud"
+ - "gcloud"
+ - "gcp"
+ - "function"
+ - "funqy"
+ - "cloud event"
+ categories:
+ - "cloud"
+ guide: "https://quarkus.io/guides/funqy-gcp-functions"
+ status: "experimental"
\ No newline at end of file
diff --git a/extensions/funqy/pom.xml b/extensions/funqy/pom.xml
index 66e10e320db8c..03c07028e2872 100644
--- a/extensions/funqy/pom.xml
+++ b/extensions/funqy/pom.xml
@@ -19,5 +19,6 @@
funqy-http
funqy-amazon-lambda
funqy-knative-events
+ funqy-google-cloud-functions
\ No newline at end of file
diff --git a/integration-tests/funqy-google-cloud-functions/.gitignore b/integration-tests/funqy-google-cloud-functions/.gitignore
new file mode 100644
index 0000000000000..af9542f46e26a
--- /dev/null
+++ b/integration-tests/funqy-google-cloud-functions/.gitignore
@@ -0,0 +1 @@
+deployment/
diff --git a/integration-tests/funqy-google-cloud-functions/README.md b/integration-tests/funqy-google-cloud-functions/README.md
new file mode 100644
index 0000000000000..ec44bd91b8c66
--- /dev/null
+++ b/integration-tests/funqy-google-cloud-functions/README.md
@@ -0,0 +1,58 @@
+# Google Cloud Functions - Funqy Binding
+
+This integration test has no automated test, it needs to be launched manually.
+
+## Build the artifact
+
+First, you need to log in to Google Cloud:
+
+```shell script
+gcloud auth login
+```
+
+Then you need to use Maven to build the artifact, the build will automatically copy it inside `target/deployment`.
+
+```shell script
+mvn clean package
+```
+
+Finally, you need to use `gcloud` to deploy the function to Google Cloud. The `gcloud` command is different for each
+Background function so the set of instructions differs for each.
+
+This example contains multiple Funqy functions, if you want to test a different function that the one defined inside
+your `application.properties`, you can use the `--set-env-vars` option of `gcloud` to define the name of the function via the
+`QUARKUS_FUNQY_EXPORT` environment variable.
+
+## Background function
+
+### PubSub event
+
+To deploy a background function that listen to PubSub event, you can use the following `gcloud` command:
+
+```shell script
+gcloud beta functions deploy quarkus-funqy-pubsub --entry-point=io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction \
+ --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish \
+ --runtime=java11 --source=target/deployment
+```
+
+You can then invoke your function via `gcloud`:
+
+```shell script
+gcloud functions call quarkus-example-pubsub --data '{"data":"HelloWorld"}'
+```
+
+### Storage event
+
+To deploy a background function that listen to Storage event, you can use the following `gcloud` command:
+
+```shell script
+gcloud beta functions deploy quarkus-funqy-storage --entry-point=io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction \
+ --trigger-resource my_java11_gcs_bucket --trigger-event google.storage.object.finalize \
+ --runtime=java11 --source=target/deployment
+```
+
+You can then invoke your function via `gcloud`:
+
+```shell script
+gcloud functions call quarkus-example-storage --data '{"name":"hello.txt"}'
+```
\ No newline at end of file
diff --git a/integration-tests/funqy-google-cloud-functions/pom.xml b/integration-tests/funqy-google-cloud-functions/pom.xml
new file mode 100644
index 0000000000000..45118a1ac5db9
--- /dev/null
+++ b/integration-tests/funqy-google-cloud-functions/pom.xml
@@ -0,0 +1,115 @@
+
+
+
+ quarkus-integration-tests-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-integration-test-funqy-google-cloud-functions
+ Quarkus - Integration Tests - Funqy Google Cloud Functions
+ Module that contains Google Cloud Functions related tests
+
+
+ io.quarkus
+ quarkus-funqy-google-cloud-functions
+
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+ maven-surefire-plugin
+
+ always
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+ build
+
+
+
+
+
+
+
+
+
+ native-image
+
+
+ native
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}-runner
+
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+ native-image
+
+ native-image
+
+
+ false
+ true
+ true
+ ${graalvmHome}
+ false
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/Greeting.java b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/Greeting.java
new file mode 100644
index 0000000000000..a008fe0c0ef4a
--- /dev/null
+++ b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/Greeting.java
@@ -0,0 +1,56 @@
+package io.quarkus.funqy.gcp.functions.test;
+
+import java.util.Objects;
+
+public class Greeting {
+ private String name;
+ private String message;
+
+ public Greeting() {
+ }
+
+ public Greeting(String name, String message) {
+ this.name = name;
+ this.message = message;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ Greeting greeting = (Greeting) o;
+ return Objects.equals(name, greeting.name) &&
+ Objects.equals(message, greeting.message);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, message);
+ }
+
+ @Override
+ public String toString() {
+ return "Greeting{" +
+ "name='" + name + '\'' +
+ ", message='" + message + '\'' +
+ '}';
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+}
diff --git a/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingFunctions.java b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingFunctions.java
new file mode 100644
index 0000000000000..9ec3c7ee68dcf
--- /dev/null
+++ b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingFunctions.java
@@ -0,0 +1,26 @@
+package io.quarkus.funqy.gcp.functions.test;
+
+import javax.inject.Inject;
+
+import io.quarkus.funqy.Funq;
+import io.quarkus.funqy.gcp.functions.event.PubsubMessage;
+import io.quarkus.funqy.gcp.functions.event.StorageEvent;
+
+public class GreetingFunctions {
+
+ @Inject
+ GreetingService service;
+
+ @Funq
+ public void helloPubSubWorld(PubsubMessage pubSubEvent) {
+ String message = service.hello("world");
+ System.out.println(pubSubEvent.messageId + " - " + message);
+ }
+
+ @Funq
+ public void helloGCSWorld(StorageEvent storageEvent) {
+ String message = service.hello("world");
+ System.out.println(storageEvent.name + " - " + message);
+ }
+
+}
diff --git a/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingService.java b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingService.java
new file mode 100644
index 0000000000000..5d6d0f329c6b4
--- /dev/null
+++ b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/GreetingService.java
@@ -0,0 +1,21 @@
+package io.quarkus.funqy.gcp.functions.test;
+
+import javax.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class GreetingService {
+ private String greeting = "Hello";
+ private String punctuation = "!";
+
+ public void setGreeting(String greet) {
+ greeting = greet;
+ }
+
+ public void setPunctuation(String punctuation) {
+ this.punctuation = punctuation;
+ }
+
+ public String hello(String val) {
+ return greeting + " " + val + punctuation;
+ }
+}
diff --git a/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/Identity.java b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/Identity.java
new file mode 100644
index 0000000000000..20451a663db1c
--- /dev/null
+++ b/integration-tests/funqy-google-cloud-functions/src/main/java/io/quarkus/funqy/gcp/functions/test/Identity.java
@@ -0,0 +1,13 @@
+package io.quarkus.funqy.gcp.functions.test;
+
+public class Identity {
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/integration-tests/funqy-google-cloud-functions/src/main/resources/application.properties b/integration-tests/funqy-google-cloud-functions/src/main/resources/application.properties
new file mode 100644
index 0000000000000..f1a7e7fba521f
--- /dev/null
+++ b/integration-tests/funqy-google-cloud-functions/src/main/resources/application.properties
@@ -0,0 +1,4 @@
+quarkus.funqy.export=helloHttpWorld
+#quarkus.funqy.export=helloHttpWorldAsync
+#quarkus.funqy.export=helloPubSubWorld
+#quarkus.funqy.export=helloGCSWorld
\ No newline at end of file
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index 8ba2cdf4cf0b3..e3f274a9dc6fb 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -23,6 +23,7 @@
funqy-amazon-lambda
+ funqy-google-cloud-functions
class-transformer
shared-library
hibernate-validator