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