diff --git a/.github/workflows/conformance.yaml b/.github/workflows/conformance.yaml deleted file mode 100644 index fbd1ce18..00000000 --- a/.github/workflows/conformance.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: Java Conformance CI -on: - push: - branches: - - master - pull_request: -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - java: [ - 11.x - # 12.x, - # 13.x - ] - steps: - - uses: actions/checkout@v2 - - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: '1.15' - - - name: Build API with Maven - run: (cd functions-framework-api/ && mvn install) - - - name: Build invoker with Maven - run: (cd invoker/ && mvn install) - - - name: Run HTTP conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@v1.2.1 - with: - version: 'v1.2.1' - functionType: 'http' - useBuildpacks: false - cmd: "'mvn -f invoker/conformance/pom.xml function:run -Drun.functionTarget=com.google.cloud.functions.conformance.HttpConformanceFunction'" - startDelay: 10 - - - name: Run background event conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@v1.2.1 - with: - version: 'v1.2.1' - functionType: 'legacyevent' - useBuildpacks: false - validateMapping: true - cmd: "'mvn -f invoker/conformance/pom.xml function:run -Drun.functionTarget=com.google.cloud.functions.conformance.BackgroundEventConformanceFunction'" - startDelay: 10 - - - name: Run cloudevent conformance tests - uses: GoogleCloudPlatform/functions-framework-conformance/action@v1.2.1 - with: - version: 'v1.2.1' - functionType: 'cloudevent' - useBuildpacks: false - validateMapping: true - cmd: "'mvn -f invoker/conformance/pom.xml function:run -Drun.functionTarget=com.google.cloud.functions.conformance.CloudEventsConformanceFunction'" - startDelay: 10 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml deleted file mode 100644 index 82a20cfe..00000000 --- a/.github/workflows/lint.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: Java Lint CI -on: - push: - branches: - - master - pull_request: - workflow_dispatch: -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK - uses: actions/setup-java@v1 - with: - java-version: 11.x - - name: Build API with Maven - run: (cd functions-framework-api/ && mvn install) - - name: Lint Functions Framework API - run: (cd functions-framework-api/ && mvn clean verify -DskipTests -P lint) - - name: Build Invoker with Maven - run: (cd functions-framework-api/ && mvn install) - - name: Lint Invoker - run: (cd invoker/ && mvn clean verify -DskipTests -P lint) - formatting: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 # v2 minimum required - - name: Run formatter - id: formatter - uses: axel-op/googlejavaformat-action@v3 - with: - args: "--dry-run --set-exit-if-changed" - continue-on-error: true - - name: Check for failure - if: steps.formatter.outcome != 'success' - run: | - echo "Java format check failed, see 'Run formatter' step for more information." - echo "See https://github.com/google/google-java-format for options on running the formatter locally." - exit 1 \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..c540d10e --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,43 @@ +name: Maven Publish +on: + push: + branches: + - 'master' + tags: + - '*' + paths: + - '.github/workflows/**' + - 'functions-framework-api/pom.xml' + - 'functions-framework-api/src/**' + - 'functions-framework-invoker/pom.xml' + - 'functions-framework-invoker/src/main/**' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Install Java and Maven + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Publish framework API + uses: samuelmeuli/action-maven-publish@v1 + with: + directory: functions-framework-api + gpg_private_key: ${{ secrets.GPG_KEY }} + gpg_passphrase: ${{ secrets.GPG_PASSWORD }} + nexus_username: ${{ secrets.OSSRH_USER }} + nexus_password: ${{ secrets.OSSRH_PASSWORD }} + + - name: Publish framework invoker + uses: samuelmeuli/action-maven-publish@v1 + with: + directory: functions-framework-invoker + gpg_private_key: ${{ secrets.GPG_KEY }} + gpg_passphrase: ${{ secrets.GPG_PASSWORD }} + nexus_username: ${{ secrets.OSSRH_USER }} + nexus_password: ${{ secrets.OSSRH_PASSWORD }} diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml deleted file mode 100644 index 1938fc01..00000000 --- a/.github/workflows/unit.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: Java Unit CI -on: - push: - branches: - - master - pull_request: -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - java: [ - 11.x, - 17.x - ] - steps: - - uses: actions/checkout@v2 - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v2 - with: - java-version: ${{ matrix.java }} - distribution: temurin - - name: Build with Maven - run: (cd functions-framework-api/ && mvn install) - - name: Test - run: (cd invoker/ && mvn test) \ No newline at end of file diff --git a/CHANGLOG.md b/CHANGLOG.md new file mode 100644 index 00000000..8abceb2c --- /dev/null +++ b/CHANGLOG.md @@ -0,0 +1,21 @@ +## 1.2.0 / 2023-06-14 + +### Features + +- Add support for OpenFunction v1beta2 API [#15](https://github.com/OpenFunction/functions-framework-java/pull/15). + +## 1.1.0 / 2023-06-01 + +### Features + +- Use opentelemetry to collect tracing [#10](https://github.com/OpenFunction/functions-framework-java/pull/10). +- Add support for dapr state store [#11](https://github.com/OpenFunction/functions-framework-java/pull/11). + +### Others + +- Use jackson instead of gson [#12](https://github.com/OpenFunction/functions-framework-java/pull/12). + +## 1.0.0 / 2023-02-20 + +The first release version. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index f7e4114c..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,82 +0,0 @@ -# How to Contribute - -We'd love to accept your patches and contributions to this project. There are -just a few small guidelines you need to follow. - -## Contributor License Agreement - -Contributions to this project must be accompanied by a Contributor License -Agreement. You (or your employer) retain the copyright to your contribution; -this simply gives us permission to use and redistribute your contributions as -part of the project. Head over to to see -your current agreements on file or to sign a new one. - -You generally only need to submit a CLA once, so if you've already submitted one -(even if it was for a different project), you probably don't need to do it -again. - -## Code reviews - -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult -[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. - -## Community Guidelines - -This project follows [Google's Open Source Community -Guidelines](https://opensource.google.com/conduct/). - -## Developing - -This project is divided into multiple packages, primarily: - -- [`functions-framework-api`](./functions-framework-api) – The interfaces for functions. -- [`java-function-invoker`](./invoker) - - `core` - The function invoker - - `testfunction` - A set of test functions - - `function-maven-plugin` - The Maven plugin for building functions - - `conformance` - A set of functions used for conformance testing - -### Setup JDK 11 / 17 - -Install JDK 11 and 17. One way to install these is through [SDK man](https://sdkman.io/). - -```sh -sdk install java 11.0.2-open -sdk install java 17-open -sdk use java 17-open -sdk use java 11.0.2-open -``` - -Verify Java version with: - -```sh -java --version -``` - -### Setup Apache Maven - -Install `mvn`: - -https://maven.apache.org/install.html - -### Formatting -This repo follows the Google Java Style guide for formatting. You can setup the -formatting tool locally using one of the options provided at -[google/google-java-format](https://github.com/google/google-java-format#google-java-format). - -## Install and Run Invoker Tests Locally - -``` -cd invoker; -mvn test; -``` - -### Running Conformance Tests Locally - -First, install Go 1.16+, then run the conformance tests with this script: - -``` -./run_conformance_tests.sh -``` diff --git a/README.md b/README.md index ade347b4..f5d53aa5 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,21 @@ # Functions Framework for Java -[![Maven Central (functions-framework-api)](https://img.shields.io/maven-central/v/com.google.cloud.functions/functions-framework-api.svg?label=functions-framework-api)](https://search.maven.org/artifact/com.google.cloud.functions/functions-framework-api) -[![Maven Central (java-function-invoker)](https://img.shields.io/maven-central/v/com.google.cloud.functions.invoker/java-function-invoker.svg?label=java-function-invoker)](https://search.maven.org/artifact/com.google.cloud.functions.invoker/java-function-invoker) -[![Maven Central (function-maven-plugin)](https://img.shields.io/maven-central/v/com.google.cloud.functions/function-maven-plugin.svg?label=function-maven-plugin)](https://search.maven.org/artifact/com.google.cloud.functions/function-maven-plugin) +An open source FaaS (Function as a service) framework for writing portable Java functions. -[![Java Unit CI](https://github.com/GoogleCloudPlatform/functions-framework-java/actions/workflows/unit.yaml/badge.svg)](https://github.com/GoogleCloudPlatform/functions-framework-java/actions/workflows/unit.yaml) -[![Java Lint CI](https://github.com/GoogleCloudPlatform/functions-framework-java/actions/workflows/lint.yaml/badge.svg)](https://github.com/GoogleCloudPlatform/functions-framework-java/actions/workflows/lint.yaml) -[![Java Conformance CI](https://github.com/GoogleCloudPlatform/functions-framework-java/actions/workflows/conformance.yaml/badge.svg)](https://github.com/GoogleCloudPlatform/functions-framework-java/actions/workflows/conformance.yaml) - -An open source FaaS (Function as a service) framework for writing portable -Java functions -- brought to you by the Google Cloud Functions team. - -The Functions Framework lets you write lightweight functions that run in many -different environments, including: - -* [Google Cloud Functions](https://cloud.google.com/functions/) -* Your local development machine -* [Cloud Run](https://cloud.google.com/run/) and [Cloud Run for Anthos](https://cloud.google.com/anthos/run/) -* [Knative](https://github.com/knative/)-based environments - -## Installation - -The Functions Framework for Java uses -[Java](https://java.com/en/download/help/download_options.xml) and -[Maven](http://maven.apache.org/install.html) (the `mvn` command), -for building and deploying functions from source. - -However, it is also possible to build your functions using -[Gradle](https://gradle.org/), as JAR archives, that you will deploy with the -`gcloud` command-line. - -## Quickstart: Hello, World on your local machine +## How to use A function is typically structured as a Maven project. We recommend using an IDE that supports Maven to create the Maven project. Add this dependency in the `pom.xml` file of your project: ```xml - - com.google.cloud.functions - functions-framework-api - 1.0.4 - provided - + + + dev.openfunction.functions + functions-framework-api + 1.2.0 + + ``` If you are using Gradle to build your functions, you can define the Functions @@ -50,270 +23,7 @@ Framework dependency in your `build.gradle` project file as follows: ```groovy dependencies { - implementation 'com.google.cloud.functions:functions-framework-api:1.0.4' - } - -``` - -### Writing an HTTP function - -Create a file `src/main/java/com/example/HelloWorld.java` with the following -contents: - -```java -package com.example; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpResponse; - -public class HelloWorld implements HttpFunction { - @Override - public void service(HttpRequest request, HttpResponse response) - throws Exception { - response.getWriter().write("Hello, World\n"); - } -} -``` - - -## Quickstart: Create a Background Function - -There are two ways to write a Background function, which differ in how the -payload of the incoming event is represented. In a "raw" background function -this payload is presented as a JSON-encoded Java string. In a "typed" background -function the Functions Framework deserializes the JSON payload into a Plain Old -Java Object (POJO). - -### Writing a Raw Background Function - -Create a file `src/main/java/com/example/Background.java` with the following -contents: - -```java -package com.example; - -import com.google.cloud.functions.Context; -import com.google.cloud.functions.RawBackgroundFunction; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import java.util.logging.Logger; - -public class Background implements RawBackgroundFunction { - private static final Logger logger = - Logger.getLogger(Background.class.getName()); - - @Override - public void accept(String json, Context context) { - Gson gson = new Gson(); - JsonObject jsonObject = gson.fromJson(json, JsonObject.class); - logger.info("Received JSON object: " + jsonObject); - } -} -``` - -### Writing a Typed Background Function - -Create a file `src/main/java/com/example/PubSubBackground` with the following -contents: - -```java -package com.example; - -import com.google.cloud.functions.BackgroundFunction; -import com.google.cloud.functions.Context; -import java.util.Map; -import java.util.logging.Logger; - -// This is the Pub/Sub message format from the Pub/Sub emulator. -class PubSubMessage { - String data; - Map attributes; - String messageId; - String publishTime; -} - -public class PubSubBackground implements BackgroundFunction { - private static final Logger logger = - Logger.getLogger(PubSubBackground.class.getName()); - - @Override - public void accept(PubSubMessage pubSubMessage, Context context) { - logger.info("Received message with id " + context.eventId()); - } -} -``` - - -## Running a function with the Maven plugin - -The Maven plugin called `function-maven-plugin` allows you to run functions -on your development machine. - -### Configuration in `pom.xml` - -You can configure the plugin in `pom.xml`: - -```xml - - com.google.cloud.functions - function-maven-plugin - 0.10.0 - - com.example.function.Echo - - -``` - -Then run it from the command line: - -```sh -mvn function:run -``` - -### Configuration on the command line - -You can alternatively configure the plugin with properties on the command line: - -```sh - mvn com.google.cloud.functions:function-maven-plugin:0.10.0:run \ - -Drun.functionTarget=com.example.function.Echo -``` - -### Running the Functions Framework directly - -You can also run a function by using the Functions Framework jar directly. -Copy the Functions Framework jar to a local location like this: - -```sh -mvn dependency:copy \ - -Dartifact='com.google.cloud.functions.invoker:java-function-invoker:1.1.0' \ - -DoutputDirectory=. -``` - -In this example we use the current directory `.` but you can specify any other -directory to copy to. Then run your function: - -```sh -java -jar java-function-invoker-1.1.0 \ - --classpath myfunction.jar \ - --target com.example.HelloWorld -``` - - -## Running a function with Gradle - -From Gradle, similarily to running functions with the Functions Framework jar, -we can invoke the `Invoker` class with a `JavaExec` task. - -### Configuration in `build.gradle` - -```groovy -configurations { - invoker -} - -dependencies { - implementation 'com.google.cloud.functions:functions-framework-api:1.0.4' - invoker 'com.google.cloud.functions.invoker:java-function-invoker:1.1.0' -} - -tasks.register("runFunction", JavaExec) { - main = 'com.google.cloud.functions.invoker.runner.Invoker' - classpath(configurations.invoker) - inputs.files(configurations.runtimeClasspath, sourceSets.main.output) - args( - '--target', project.findProperty('run.functionTarget'), - '--port', project.findProperty('run.port') ?: 8080 - ) - doFirst { - args('--classpath', files(configurations.runtimeClasspath, sourceSets.main.output).asPath) + implementation 'dev.openfunction.functions:functions-framework-api:1.2.0' } -} -``` - -Then in your terminal or IDE, you will be able to run the function locally with: -```sh -gradle runFunction -Prun.functionTarget=com.example.HelloWorld \ - -Prun.port=8080 ``` - -Or if you use the Gradle wrapper provided by your Gradle project build: - -```sh -./gradlew runFunction -Prun.functionTarget=com.example.HelloWorld \ - -Prun.port=8080 -``` - -## Functions Framework configuration - -There are a number of options that can be used to configure the Functions -Framework, whether run directly or on the command line. - -### Which function to run - -A function is a Java class. You must specify the name of that class when running -the Functions Framework: - -``` ---target com.example.HelloWorld -com.example.HelloWorld --Drun.functionTarget=com.example.HelloWorld --Prun.functionTarget=com.example.HelloWorld -``` - -* Invoker argument: `--target com.example.HelloWorld` -* Maven `pom.xml`: `com.example.HelloWorld` -* Maven CLI argument: `-Drun.functionTarget=com.example.HelloWorld` -* Gradle CLI argument: `-Prun.functionTarget=com.example.HelloWorld` - -### Which port to listen on - -The Functions Framework is an HTTP server that directs incoming HTTP requests to -the function code. By default this server listens on port 8080. Specify an -alternative value like this: - -* Invoker argument: `--port 12345` -* Maven `pom.xml`: `12345` -* Maven CLI argument: `-Drun.port=12345` -* Gradle CLI argument: `-Prun.port=12345` - -### Function classpath - -Function code runs with a classpath that includes the function code itself and -its dependencies. The Maven plugin automatically computes the classpath based -on the dependencies expressed in `pom.xml`. When invoking the Functions -Framework directly, you must use `--classpath` to indicate how to find the code -and its dependencies. For example: - -``` -java -jar java-function-invoker-1.1.0 \ - --classpath 'myfunction.jar:/some/directory:/some/library/*' \ - --target com.example.HelloWorld -``` - -The `--classpath` option works like -[`java -classpath`](https://docs.oracle.com/en/java/javase/13/docs/specs/man/java.html#standard-options-for-java). -It is a list of entries separated by `:` (`;` on Windows), where each entry is: - -* a directory, in which case class `com.example.Foo` is looked for in a file - `com/example/Foo.class` under that directory; -* a jar file, in which case class `com.example.Foo` is looked for in a file - `com/example/Foo.class` in that jar file; -* a directory followed by `/*` (`\*` on Windows), in which case each jar file - in that directory (file called `foo.jar`) is treated the same way as if it - had been named explicitly. - -#### Simplifying the claspath - -Specifying the right classpath can be tricky. A simpler alternative is to -build the function as a "fat jar", where the function code and all its -dependencies are in a single jar file. Then `--classpath myfatfunction.jar` -is enough. An example of how this is done is the Functions Framework jar itself, -as seen -[here](https://github.com/GoogleCloudPlatform/functions-framework-java/blob/b627f28/invoker/core/pom.xml#L153). - -Alternatively, you can arrange for your jar to have its own classpath, as -described -[here](https://maven.apache.org/shared/maven-archiver/examples/classpath.html). diff --git a/functions-framework-api/pom.xml b/functions-framework-api/pom.xml index 27246b71..9e68a03b 100644 --- a/functions-framework-api/pom.xml +++ b/functions-framework-api/pom.xml @@ -13,181 +13,169 @@ See the License for the specific language governing permissions and limitations under the License. --> - - 4.0.0 + + 4.0.0 + + + io.cloudevents + cloudevents-api + 2.4.2 + compile + + + io.dapr + dapr-sdk + 1.8.0 + + + org.apache.eventmesh + eventmesh-sdk-java + 1.9.0-release + + - - org.sonatype.oss - oss-parent - 9 - + + org.sonatype.oss + oss-parent + 9 + - com.google.cloud.functions - functions-framework-api - 1.0.5-SNAPSHOT + dev.openfunction.functions + functions-framework-api + 1.3.0-SNAPSHOT - - UTF-8 - 3.8.0 - 3.1.0 - 5.3.2 - + + UTF-8 + 3.8.0 + 3.1.0 + 5.3.2 + - - - Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + - - scm:git:https://github.com/GoogleCloudPlatform/functions-framework-java.git - scm:git:git@github.com:GoogleCloudPlatform/functions-framework-java.git - https://github.com/GoogleCloudPlatform/functions-framework-java - HEAD - - - - - io.cloudevents - cloudevents-api - 2.0.0.RC2 - - - - - - - maven-compiler-plugin - ${maven-compiler-plugin.version} - - 11 - 11 - - - - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-release-plugin - 2.5.3 - - - default - - perform - - - functions-framework-api/pom.xml - - - - - - - - - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - true - true - UTF-8 - UTF-8 - UTF-8 - - -XDignore.symbol.file - - true - 8 - false - - - - attach-docs - post-integration-test - jar - - - - - - - - - sonatype-nexus-snapshots - Sonatype Nexus Snapshots - https://oss.sonatype.org/content/repositories/snapshots/ - - - sonatype-nexus-staging - Nexus Release Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - sonatype-oss-release - + - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 11 + 11 + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + + default + + perform + + + functions-framework-api/pom.xml + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.7 + true + + ossrh + https://s01.oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + - - - + + + + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + true + true + UTF-8 + UTF-8 + UTF-8 + + -XDignore.symbol.file + + true + 8 + false + + + + attach-docs + post-integration-test + + jar + + + + + + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java deleted file mode 100644 index 5052b7b6..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions; - -/** - * Represents a Cloud Function that is activated by an event and parsed into a user-supplied class. - * The payload of the event is a JSON object, which is deserialized into a user-defined class as - * described for Gson. - * - *

Here is an example of an implementation that accesses the {@code messageId} property from a - * payload that matches a user-defined {@code PubSubMessage} class: - * - * - *

- * public class Example implements{@code BackgroundFunction} {
- *   private static final Logger logger = Logger.getLogger(Example.class.getName());
- *
- *  {@code @Override}
- *   public void accept(PubSubMessage pubSubMessage, Context context) {
- *     logger.info("Got messageId " + pubSubMessage.messageId);
- *   }
- * }
- *
- * // Where PubSubMessage is a user-defined class like this:
- * public class PubSubMessage {
- *   String data;
- *  {@code Map} attributes;
- *   String messageId;
- *   String publishTime;
- * }
- * 
- * - * @param the class of payload objects that this function expects. - */ -@FunctionalInterface -public interface BackgroundFunction { - /** - * Called to service an incoming event. This interface is implemented by user code to provide the - * action for a given background function. If this method throws any exception (including any - * {@link Error}) then the HTTP response will have a 500 status code. - * - * @param payload the payload of the event, deserialized from the original JSON string. - * @param context the context of the event. This is a set of values that every event has, - * separately from the payload, such as timestamp and event type. - * @throws Exception to produce a 500 status code in the HTTP response. - */ - void accept(T payload, Context context) throws Exception; -} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/CloudEventsFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/CloudEventsFunction.java deleted file mode 100644 index 7e34ae68..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/CloudEventsFunction.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.google.cloud.functions; - -import io.cloudevents.CloudEvent; - -/** - * Represents a Cloud Function that is activated by an event and parsed into a {@link CloudEvent} - * object. - */ -@FunctionalInterface -public interface CloudEventsFunction { - /** - * Called to service an incoming event. This interface is implemented by user code to provide the - * action for a given background function. If this method throws any exception (including any - * {@link Error}) then the HTTP response will have a 500 status code. - * - * @param event the event. - * @throws Exception to produce a 500 status code in the HTTP response. - */ - void accept(CloudEvent event) throws Exception; -} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/Context.java b/functions-framework-api/src/main/java/com/google/cloud/functions/Context.java deleted file mode 100644 index 5100451e..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/Context.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions; - -import java.util.Collections; -import java.util.Map; - -/** An interface for event function context. */ -public interface Context { - /** - * Returns event ID. - * - * @return event ID - */ - String eventId(); - - /** - * Returns event timestamp. - * - * @return event timestamp - */ - String timestamp(); - - /** - * Returns event type. - * - * @return event type - */ - String eventType(); - - /** - * Returns event resource. - * - * @return event resource - */ - String resource(); - - /** - * Returns additional attributes from this event. For CloudEvents, the entries in this map will - * include the required - * attributes and may include optional - * attributes and - * extension attributes. - * - *

The map returned by this method may be empty but is never null. - * - * @return additional attributes form this event. - */ - default Map attributes() { - return Collections.emptyMap(); - } -} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpFunction.java deleted file mode 100644 index 6357724d..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpFunction.java +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions; - -/** Represents a Cloud Function that is activated by an HTTP request. */ -@FunctionalInterface -public interface HttpFunction { - /** - * Called to service an incoming HTTP request. This interface is implemented by user code to - * provide the action for a given function. If the method throws any exception (including any - * {@link Error}) then the HTTP response will have a 500 status code. - * - * @param request a representation of the incoming HTTP request. - * @param response an object that can be used to provide the corresponding HTTP response. - * @throws Exception if thrown, the HTTP response will have a 500 status code. - */ - void service(HttpRequest request, HttpResponse response) throws Exception; -} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpMessage.java b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpMessage.java deleted file mode 100644 index 24a70c9b..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpMessage.java +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** Represents an HTTP message, either an HTTP request or a part of a multipart HTTP request. */ -public interface HttpMessage { - /** - * Returns the value of the {@code Content-Type} header, if any. - * - * @return the content type, if any. - */ - Optional getContentType(); - - /** - * Returns the numeric value of the {@code Content-Length} header. - * - * @return the content length. - */ - long getContentLength(); - - /** - * Returns the character encoding specified in the {@code Content-Type} header, or {@code - * Optional.empty()} if there is no {@code Content-Type} header or it does not have the {@code - * charset} parameter. - * - * @return the character encoding for the content type, if one is specified. - */ - Optional getCharacterEncoding(); - - /** - * Returns an {@link InputStream} that can be used to read the body of this HTTP request. Every - * call to this method on the same {@link HttpMessage} will return the same object. This method is - * typically used to read binary data. If the body is text, the {@link #getReader()} method is - * more appropriate. - * - * @return an {@link InputStream} that can be used to read the body of this HTTP request. - * @throws IOException if a valid {@link InputStream} cannot be returned for some reason. - * @throws IllegalStateException if {@link #getReader()} has already been called on this instance. - */ - InputStream getInputStream() throws IOException; - - /** - * Returns a {@link BufferedReader} that can be used to read the text body of this HTTP request. - * Every call to this method on the same {@link HttpMessage} will return the same object. - * - * @return a {@link BufferedReader} that can be used to read the text body of this HTTP request. - * @throws IOException if a valid {@link BufferedReader} cannot be returned for some reason. - * @throws IllegalStateException if {@link #getInputStream()} has already been called on this - * instance. - */ - BufferedReader getReader() throws IOException; - - /** - * Returns a map describing the headers of this HTTP request, or this part of a multipart request. - * If the headers look like this... - * - *

-   *   Content-Type: text/plain
-   *   Some-Header: some value
-   *   Some-Header: another value
-   * 
- * - * ...then the returned value will map {@code "Content-Type"} to a one-element list containing - * {@code "text/plain"}, and {@code "Some-Header"} to a two-element list containing {@code "some - * value"} and {@code "another value"}. - * - * @return a map where each key is an HTTP header and the corresponding {@code List} value has one - * element for each occurrence of that header. - */ - Map> getHeaders(); - - /** - * Convenience method that returns the value of the first header with the given name. If the - * headers look like this... - * - *
-   *   Content-Type: text/plain
-   *   Some-Header: some value
-   *   Some-Header: another value
-   * 
- * - * ...then {@code getFirstHeader("Some-Header")} will return {@code Optional.of("some value")}, - * and {@code getFirstHeader("Another-Header")} will return {@code Optional.empty()}. - * - * @param name an HTTP header name. - * @return the first value of the given header, if present. - */ - default Optional getFirstHeader(String name) { - List headers = getHeaders().get(name); - if (headers == null || headers.isEmpty()) { - return Optional.empty(); - } - return Optional.of(headers.get(0)); - } -} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpRequest.java b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpRequest.java deleted file mode 100644 index d50f1cf7..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpRequest.java +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** Represents the contents of an HTTP request that is being serviced by a Cloud Function. */ -public interface HttpRequest extends HttpMessage { - /** - * The HTTP method of this request, such as {@code "POST"} or {@code "GET"}. - * - * @return the HTTP method of this request. - */ - String getMethod(); - - /** - * The full URI of this request as it arrived at the server. - * - * @return the full URI of this request. - */ - String getUri(); - - /** - * The path part of the URI for this request, without any query. If the full URI is {@code - * http://foo.com/bar/baz?this=that}, then this method will return {@code /bar/baz}. - * - * @return the path part of the URI for this request. - */ - String getPath(); - - /** - * The query part of the URI for this request. If the full URI is {@code - * http://foo.com/bar/baz?this=that}, then this method will return {@code this=that}. If there is - * no query part, the returned {@code Optional} is empty. - * - * @return the query part of the URI, if any. - */ - Optional getQuery(); - - /** - * The query parameters of this request. If the full URI is {@code - * http://foo.com/bar?thing=thing1&thing=thing2&cat=hat}, then the returned map will map {@code - * thing} to the list {@code ["thing1", "thing2"]} and {@code cat} to the list with the single - * element {@code "hat"}. - * - * @return a map where each key is the name of a query parameter and the corresponding {@code - * List} value indicates every value that was associated with that name. - */ - Map> getQueryParameters(); - - /** - * The first query parameter with the given name, if any. If the full URI is {@code - * http://foo.com/bar?thing=thing1&thing=thing2&cat=hat}, then {@code - * getFirstQueryParameter("thing")} will return {@code Optional.of("thing1")} and {@code - * getFirstQueryParameter("something")} will return {@code Optional.empty()}. This is a more - * convenient alternative to {@link #getQueryParameters}. - * - * @param name a query parameter name. - * @return the first query parameter value with the given name, if any. - */ - default Optional getFirstQueryParameter(String name) { - List parameters = getQueryParameters().get(name); - if (parameters == null || parameters.isEmpty()) { - return Optional.empty(); - } - return Optional.of(parameters.get(0)); - } - - /** - * Represents one part inside a multipart ({@code multipart/form-data}) HTTP request. Each such - * part can have its own HTTP headers, which can be retrieved with the methods inherited from - * {@link HttpMessage}. - */ - interface HttpPart extends HttpMessage { - /** - * Returns the filename associated with this part, if any. - * - * @return the filename associated with this part, if any. - */ - Optional getFileName(); - } - - /** - * Returns the parts inside this multipart ({@code multipart/form-data}) HTTP request. Each entry - * in the returned map has the name of the part as its key and the contents as the associated - * value. - * - * @return a map from part names to part contents. - * @throws IllegalStateException if the {@link #getContentType() content type} is not {@code - * multipart/form-data}. - */ - Map getParts(); -} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpResponse.java b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpResponse.java deleted file mode 100644 index c3f87ea2..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpResponse.java +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * Represents the contents of an HTTP response that is being sent by a Cloud Function in response to - * an HTTP request. - */ -public interface HttpResponse { - /** - * Sets the numeric HTTP status - * code to use in the response. Most often this will be 200, which is the OK status. The named - * constants in {@link java.net.HttpURLConnection}, such as {@link - * java.net.HttpURLConnection#HTTP_OK HTTP_OK}, can be used as an alternative to writing numbers - * in your source code. - * - * @param code the status code. - */ - void setStatusCode(int code); - - /** - * Sets the numeric HTTP status - * code and reason message to use in the response. For example
- * {@code setStatusCode(400, "Something went wrong")}. The named constants in {@link - * java.net.HttpURLConnection}, such as {@link java.net.HttpURLConnection#HTTP_BAD_REQUEST - * HTTP_BAD_REQUEST}, can be used as an alternative to writing numbers in your source code. - * - * @param code the status code. - * @param message the status message. - */ - void setStatusCode(int code, String message); - - /** - * Sets the value to use for the {@code Content-Type} header in the response. This may include a - * character encoding, for example {@code setContentType("text/plain; charset=utf-8")}. - * - * @param contentType the content type. - */ - void setContentType(String contentType); - - /** - * Returns the {@code Content-Type} that was previously set by {@link #setContentType}, or by - * {@link #appendHeader} with a header name of {@code Content-Type}. If no {@code Content-Type} - * has been set, returns {@code Optional.empty()}. - * - * @return the content type, if any. - */ - Optional getContentType(); - - /** - * Includes the given header name with the given value in the response. This method may be called - * several times for the same header, in which case the response will contain the header the same - * number of times. - * - * @param header an HTTP header, such as {@code Content-Type}. - * @param value a value to associate with that header. - */ - void appendHeader(String header, String value); - - /** - * Returns the headers that have been defined for the response so far. This will contain at least - * the headers that have been set via {@link #appendHeader} or {@link #setContentType}, and may - * contain additional headers such as {@code Date}. - * - * @return a map where each key is a header name and the corresponding {@code List} value has one - * entry for every value associated with that header. - */ - Map> getHeaders(); - - /** - * Returns an {@link OutputStream} that can be used to write the body of the response. This method - * is typically used to write binary data. If the body is text, the {@link #getWriter()} method is - * more appropriate. - * - * @return the output stream. - * @throws IOException if a valid {@link OutputStream} cannot be returned for some reason. - * @throws IllegalStateException if {@link #getWriter} has already been called on this instance. - */ - OutputStream getOutputStream() throws IOException; - - /** - * Returns a {@link BufferedWriter} that can be used to write the text body of the response. If - * the written text will not be US-ASCII, you should specify a character encoding by calling - * {@link #setContentType setContentType("text/foo; charset=bar")} or {@link #appendHeader - * appendHeader("Content-Type", "text/foo; charset=bar")} before calling this method. - * - * @return the writer. - * @throws IOException if a valid {@link BufferedWriter} cannot be returned for some reason. - * @throws IllegalStateException if {@link #getOutputStream} has already been called on this - * instance. - */ - BufferedWriter getWriter() throws IOException; -} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java deleted file mode 100644 index e27624ed..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions; - -/** - * Represents a Cloud Function that is activated by an event. The payload of the event is a JSON - * object, which can be parsed using a JSON package such as GSON. - * - *

Here is an example of an implementation that parses the JSON payload using Gson, to access its - * {@code messageId} property: - * - * - *

- * public class Example implements RawBackgroundFunction {
- *   private static final Logger logger = Logger.getLogger(Example.class.getName());
- *
- *  {@code @Override}
- *   public void accept(String json, Context context) {
- *     JsonObject jsonObject = new Gson().fromJson(json, JsonObject.class);
- *     JsonElement messageId = jsonObject.get("messageId");
- *     String messageIdString = messageId.getAsJsonString();
- *     logger.info("Got messageId " + messageIdString);
- *   }
- * }
- * 
- * - *

Here is an example of an implementation that deserializes the JSON payload into a Java object - * for simpler access, again using Gson: - * - *

- * public class Example implements RawBackgroundFunction {
- *   private static final Logger logger = Logger.getLogger(Example.class.getName());
- *
- *  {@code @Override}
- *   public void accept(String json, Context context) {
- *     PubSubMessage message = new Gson().fromJson(json, PubSubMessage.class);
- *     logger.info("Got messageId " + message.messageId);
- *   }
- * }
- *
- * // Where PubSubMessage is a user-defined class like this:
- * public class PubSubMessage {
- *   String data;
- *  {@code Map} attributes;
- *   String messageId;
- *   String publishTime;
- * }
- * 
- */ -@FunctionalInterface -public interface RawBackgroundFunction { - /** - * Called to service an incoming event. This interface is implemented by user code to provide the - * action for a given background function. If this method throws any exception (including any - * {@link Error}) then the HTTP response will have a 500 status code. - * - * @param json the payload of the event, as a JSON string. - * @param context the context of the event. This is a set of values that every event has, - * separately from the payload, such as timestamp and event type. - * @throws Exception to produce a 500 status code in the HTTP response. - */ - void accept(String json, Context context) throws Exception; -} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/BindingEvent.java b/functions-framework-api/src/main/java/dev/openfunction/functions/BindingEvent.java new file mode 100644 index 00000000..96cc80c5 --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/BindingEvent.java @@ -0,0 +1,49 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import java.nio.ByteBuffer; +import java.util.Map; + +public class BindingEvent { + /** + * The name of the input binding component. + */ + private final String name; + + private final ByteBuffer data; + + private final Map metadata; + + public BindingEvent(String name, Map metadata, ByteBuffer data) { + this.name = name; + this.metadata = metadata; + this.data = data; + } + + public ByteBuffer getData() { + return data; + } + + public Map getMetadata() { + return metadata; + } + + public String getName() { + return name; + } +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/CloudEventFunction.java b/functions-framework-api/src/main/java/dev/openfunction/functions/CloudEventFunction.java new file mode 100644 index 00000000..1aa3650f --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/CloudEventFunction.java @@ -0,0 +1,29 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import io.cloudevents.CloudEvent; + +public interface CloudEventFunction { + /** + * @param ctx Context + * @param event cloud event + * @return Error + * @throws Exception Exception + */ + Error accept(Context ctx, CloudEvent event) throws Exception; +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/Component.java b/functions-framework-api/src/main/java/dev/openfunction/functions/Component.java new file mode 100644 index 00000000..34779636 --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/Component.java @@ -0,0 +1,106 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; +import java.util.Objects; + +public class Component { + private static final String ComponentTypeBinding = "bindings"; + private static final String ComponentTypePubsub = "pubsub"; + + @Deprecated + private String uri; + private String topic; + private String componentName; + private String componentType; + private Map metadata; + private String operation; + + @Deprecated + public String getUri() { + if (!StringUtils.isBlank(uri)) { + return uri; + } else if (!StringUtils.isBlank(topic)) { + return topic; + } else { + return componentName; + } + } + + @Deprecated + public void setUri(String uri) { + this.uri = uri; + } + + public String getComponentName() { + return componentName; + } + + public void setComponentName(String componentName) { + this.componentName = componentName; + } + + public String getComponentType() { + return componentType; + } + + public void setComponentType(String componentType) { + this.componentType = componentType; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getTopic() { + if (StringUtils.isNotBlank(topic)) { + return topic; + } else if (StringUtils.isNotBlank(uri) && !Objects.equals(uri, componentName)) { + return uri; + } else { + return null; + } + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public boolean isPubsub() { + return componentType.startsWith(ComponentTypePubsub); + } + public boolean isBinding() { + return componentType.startsWith(ComponentTypeBinding); + } + +} + diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/Context.java b/functions-framework-api/src/main/java/dev/openfunction/functions/Context.java new file mode 100644 index 00000000..071b4971 --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/Context.java @@ -0,0 +1,129 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import org.apache.eventmesh.client.grpc.producer.EventMeshGrpcProducer; + +import io.cloudevents.CloudEvent; +import io.dapr.client.DaprClient; + +import java.util.Map; + +/** + * An interface for event function context. + */ +public interface Context { + /** + * send provides the ability to allow the user to send data to a specified output target. + * + * @param outputName output target name + * @param data Data String + * @return Error + */ + @Deprecated + Error send(String outputName, String data); + + /** + * getHttpRequest returns the Http request. + * + * @return HttpRequest + */ + HttpRequest getHttpRequest(); + + /** + * getHttpResponse returns the Http response. + * + * @return HttpResponse + */ + HttpResponse getHttpResponse(); + + /** + * getBindingEvent returns the binding event. + * + * @return BindingEvent GetBindingEvent(); + */ + BindingEvent getBindingEvent(); + + /** + * getTopicEvent returns the topic event. + * + * @return TopicEvent + */ + TopicEvent getTopicEvent(); + + /** + * getCloudEvent returns the cloud Event. + * + * @return CloudEvent + */ + CloudEvent getCloudEvent(); + + /** + * getName returns the function's name. + * + * @return Function Name + */ + String getName(); + + /** + * GetOut returns the returned value of function. + * + * @return Out + */ + Out getOut(); + + /** + * getHttpPattern returns the path of the server listening for http function. + * + * @return String + */ + String getHttpPattern(); + + /** + * getInputs returns the inputs of function. + * + * @return Inputs + */ + Map getInputs(); + + /** + * getOutputs returns the Outputs of function. + * + * @return Outputs + */ + Map getOutputs(); + + /** + * getStates returns the states of function. + * + * @return states + */ + Map getStates(); + + /** + * getDaprClient return a dapr client, so that use user + * can call the dapr API directly. + * Be carefully, the dapr client maybe null; + * + * @return Dapr client + */ + DaprClient getDaprClient(); + + EventMeshGrpcProducer getEventMeshProducer(); + + byte[] packageAsCloudevent(String payload); +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/Hook.java b/functions-framework-api/src/main/java/dev/openfunction/functions/Hook.java new file mode 100644 index 00000000..cf041395 --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/Hook.java @@ -0,0 +1,55 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import java.util.Map; + +public interface Hook { + /** + * name return the name of this plugin. + * + * @return Plugin name + */ + String name(); + + /** + * version return the version of this plugin. + * + * @return Plugin name + */ + String version(); + + /** + * init will create a new plugin, and execute hook in this calling. + * If you do not want to use a new plugin to execute hook, just return `this`. + * + * @return Plugin + */ + Hook init(); + + /** + * execute executes the hook. + * + * @param ctx Runtime context + * @return error + */ + Error execute(Context ctx); + + Boolean needToTracing(); + + Map tagsAddToTracing(); +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/HttpFunction.java b/functions-framework-api/src/main/java/dev/openfunction/functions/HttpFunction.java new file mode 100644 index 00000000..452f5c24 --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/HttpFunction.java @@ -0,0 +1,30 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +public interface HttpFunction { + /** + * Called to service an incoming HTTP request. This interface is implemented by user code to + * provide the action for a given function. If the method throws any exception (including any + * {@link Error}) then the HTTP response will have a 500 status code. + * + * @param request a representation of the incoming HTTP request. + * @param response an object that can be used to provide the corresponding HTTP response. + * @throws Exception if thrown, the HTTP response will have a 500 status code. + */ + void service(HttpRequest request, HttpResponse response) throws Exception; +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/HttpMessage.java b/functions-framework-api/src/main/java/dev/openfunction/functions/HttpMessage.java new file mode 100644 index 00000000..4b9ed773 --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/HttpMessage.java @@ -0,0 +1,118 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Represents an HTTP message, either an HTTP request or a part of a multipart HTTP request. + */ +public interface HttpMessage { + /** + * Returns the value of the {@code Content-Type} header, if any. + * + * @return the content type, if any. + */ + Optional getContentType(); + + /** + * Returns the numeric value of the {@code Content-Length} header. + * + * @return the content length. + */ + long getContentLength(); + + /** + * Returns the character encoding specified in the {@code Content-Type} header, or {@code + * Optional.empty()} if there is no {@code Content-Type} header or it does not have the {@code + * charset} parameter. + * + * @return the character encoding for the content type, if one is specified. + */ + Optional getCharacterEncoding(); + + /** + * Returns an {@link InputStream} that can be used to read the body of this HTTP request. Every + * call to this method on the same {@link HttpMessage} will return the same object. This method is + * typically used to read binary data. If the body is text, the {@link #getReader()} method is + * more appropriate. + * + * @return an {@link InputStream} that can be used to read the body of this HTTP request. + * @throws IOException if a valid {@link InputStream} cannot be returned for some reason. + * @throws IllegalStateException if {@link #getReader()} has already been called on this instance. + */ + InputStream getInputStream() throws IOException; + + /** + * Returns a {@link BufferedReader} that can be used to read the text body of this HTTP request. + * Every call to this method on the same {@link HttpMessage} will return the same object. + * + * @return a {@link BufferedReader} that can be used to read the text body of this HTTP request. + * @throws IOException if a valid {@link BufferedReader} cannot be returned for some reason. + * @throws IllegalStateException if {@link #getInputStream()} has already been called on this + * instance. + */ + BufferedReader getReader() throws IOException; + + /** + * Returns a map describing the headers of this HTTP request, or this part of a multipart request. + * If the headers look like this... + * + *
+     *   Content-Type: text/plain
+     *   Some-Header: some value
+     *   Some-Header: another value
+     * 
+ *

+ * ...then the returned value will map {@code "Content-Type"} to a one-element list containing + * {@code "text/plain"}, and {@code "Some-Header"} to a two-element list containing {@code "some + * value"} and {@code "another value"}. + * + * @return a map where each key is an HTTP header and the corresponding {@code List} value has one + * element for each occurrence of that header. + */ + Map> getHeaders(); + + /** + * Convenience method that returns the value of the first header with the given name. If the + * headers look like this... + * + *

+     *   Content-Type: text/plain
+     *   Some-Header: some value
+     *   Some-Header: another value
+     * 
+ *

+ * ...then {@code getFirstHeader("Some-Header")} will return {@code Optional.of("some value")}, + * and {@code getFirstHeader("Another-Header")} will return {@code Optional.empty()}. + * + * @param name an HTTP header name. + * @return the first value of the given header, if present. + */ + default Optional getFirstHeader(String name) { + List headers = getHeaders().get(name); + if (headers == null || headers.isEmpty()) { + return Optional.empty(); + } + return Optional.of(headers.get(0)); + } +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/HttpRequest.java b/functions-framework-api/src/main/java/dev/openfunction/functions/HttpRequest.java new file mode 100644 index 00000000..48f79742 --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/HttpRequest.java @@ -0,0 +1,111 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Represents the contents of an HTTP request that is being serviced by a Cloud Function. + */ +public interface HttpRequest extends HttpMessage { + /** + * The HTTP method of this request, such as {@code "POST"} or {@code "GET"}. + * + * @return the HTTP method of this request. + */ + String getMethod(); + + /** + * The full URI of this request as it arrived at the server. + * + * @return the full URI of this request. + */ + String getUri(); + + /** + * The path part of the URI for this request, without any query. If the full URI is {@code + * http://foo.com/bar/baz?this=that}, then this method will return {@code /bar/baz}. + * + * @return the path part of the URI for this request. + */ + String getPath(); + + /** + * The query part of the URI for this request. If the full URI is {@code + * http://foo.com/bar/baz?this=that}, then this method will return {@code this=that}. If there is + * no query part, the returned {@code Optional} is empty. + * + * @return the query part of the URI, if any. + */ + Optional getQuery(); + + /** + * The query parameters of this request. If the full URI is {@code + * http://foo.com/bar?thing=thing1&thing=thing2&cat=hat}, then the returned map will map {@code + * thing} to the list {@code ["thing1", "thing2"]} and {@code cat} to the list with the single + * element {@code "hat"}. + * + * @return a map where each key is the name of a query parameter and the corresponding {@code + * List} value indicates every value that was associated with that name. + */ + Map> getQueryParameters(); + + /** + * The first query parameter with the given name, if any. If the full URI is {@code + * http://foo.com/bar?thing=thing1&thing=thing2&cat=hat}, then {@code + * getFirstQueryParameter("thing")} will return {@code Optional.of("thing1")} and {@code + * getFirstQueryParameter("something")} will return {@code Optional.empty()}. This is a more + * convenient alternative to {@link #getQueryParameters}. + * + * @param name a query parameter name. + * @return the first query parameter value with the given name, if any. + */ + default Optional getFirstQueryParameter(String name) { + List parameters = getQueryParameters().get(name); + if (parameters == null || parameters.isEmpty()) { + return Optional.empty(); + } + return Optional.of(parameters.get(0)); + } + + /** + * Represents one part inside a multipart ({@code multipart/form-data}) HTTP request. Each such + * part can have its own HTTP headers, which can be retrieved with the methods inherited from + * {@link HttpMessage}. + */ + interface HttpPart extends HttpMessage { + /** + * Returns the filename associated with this part, if any. + * + * @return the filename associated with this part, if any. + */ + Optional getFileName(); + } + + /** + * Returns the parts inside this multipart ({@code multipart/form-data}) HTTP request. Each entry + * in the returned map has the name of the part as its key and the contents as the associated + * value. + * + * @return a map from part names to part contents. + * @throws IllegalStateException if the {@link #getContentType() content type} is not {@code + * multipart/form-data}. + */ + Map getParts(); +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/HttpResponse.java b/functions-framework-api/src/main/java/dev/openfunction/functions/HttpResponse.java new file mode 100644 index 00000000..b1b8befd --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/HttpResponse.java @@ -0,0 +1,121 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Represents the contents of an HTTP response that is being sent by a Cloud Function in response to + * an HTTP request. + */ +public interface HttpResponse { + /** + * Sets the numeric HTTP status + * code to use in the response. Most often this will be 200, which is the OK status. The named + * constants in {@link java.net.HttpURLConnection}, such as {@link + * java.net.HttpURLConnection#HTTP_OK HTTP_OK}, can be used as an alternative to writing numbers + * in your source code. + * + * @param code the status code. + */ + void setStatusCode(int code); + + /** + * @return http status code + */ + int getStatusCode(); + + /** + * Sets the numeric HTTP status + * code and reason message to use in the response. For example
+ * {@code setStatusCode(400, "Something went wrong")}. The named constants in {@link + * java.net.HttpURLConnection}, such as {@link java.net.HttpURLConnection#HTTP_BAD_REQUEST + * HTTP_BAD_REQUEST}, can be used as an alternative to writing numbers in your source code. + * + * @param code the status code. + * @param message the status message. + */ + void setStatusCode(int code, String message); + + /** + * Sets the value to use for the {@code Content-Type} header in the response. This may include a + * character encoding, for example {@code setContentType("text/plain; charset=utf-8")}. + * + * @param contentType the content type. + */ + void setContentType(String contentType); + + /** + * Returns the {@code Content-Type} that was previously set by {@link #setContentType}, or by + * {@link #appendHeader} with a header name of {@code Content-Type}. If no {@code Content-Type} + * has been set, returns {@code Optional.empty()}. + * + * @return the content type, if any. + */ + Optional getContentType(); + + /** + * Includes the given header name with the given value in the response. This method may be called + * several times for the same header, in which case the response will contain the header the same + * number of times. + * + * @param header an HTTP header, such as {@code Content-Type}. + * @param value a value to associate with that header. + */ + void appendHeader(String header, String value); + + /** + * Returns the headers that have been defined for the response so far. This will contain at least + * the headers that have been set via {@link #appendHeader} or {@link #setContentType}, and may + * contain additional headers such as {@code Date}. + * + * @return a map where each key is a header name and the corresponding {@code List} value has one + * entry for every value associated with that header. + */ + Map> getHeaders(); + + /** + * Returns an {@link OutputStream} that can be used to write the body of the response. This method + * is typically used to write binary data. If the body is text, the {@link #getWriter()} method is + * more appropriate. + * + * @return the output stream. + * @throws IOException if a valid {@link OutputStream} cannot be returned for some reason. + * @throws IllegalStateException if {@link #getWriter} has already been called on this instance. + */ + OutputStream getOutputStream() throws IOException; + + /** + * Returns a {@link BufferedWriter} that can be used to write the text body of the response. If + * the written text will not be US-ASCII, you should specify a character encoding by calling + * {@link #setContentType setContentType("text/foo; charset=bar")} or {@link #appendHeader + * appendHeader("Content-Type", "text/foo; charset=bar")} before calling this method. + * + * @return the writer. + * @throws IOException if a valid {@link BufferedWriter} cannot be returned for some reason. + * @throws IllegalStateException if {@link #getOutputStream} has already been called on this + * instance. + */ + BufferedWriter getWriter() throws IOException; +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/OpenFunction.java b/functions-framework-api/src/main/java/dev/openfunction/functions/OpenFunction.java new file mode 100644 index 00000000..b037526c --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/OpenFunction.java @@ -0,0 +1,30 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +public interface OpenFunction { + /** + * Called to service an incoming event. This interface is implemented by user code to provide the + * action for a given function. + * + * @param context context + * @param payload incoming event + * @return Out + * @throws Exception Exception + */ + Out accept(Context context, String payload) throws Exception; +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/Out.java b/functions-framework-api/src/main/java/dev/openfunction/functions/Out.java new file mode 100644 index 00000000..ae94ef1a --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/Out.java @@ -0,0 +1,87 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import java.nio.ByteBuffer; +import java.util.Map; + +public class Out { + /** + * code is return code of the user function + */ + private int code; + + /** + * error is the error returned by the user function. + */ + private Error error; + + /** + * data is the return data of the user function + */ + private ByteBuffer data; + /** + * metadata is the metadata of the event + */ + private Map metadata; + + public Out() { + } + + public Out(int code, Error error, ByteBuffer data, Map metadata) { + this.code = code; + this.error = error; + this.data = data; + this.metadata = metadata; + } + + public int getCode() { + return code; + } + + public Out setCode(int code) { + this.code = code; + return this; + } + + public ByteBuffer getData() { + return data; + } + + public Out setData(ByteBuffer data) { + this.data = data; + return this; + } + + public Map getMetadata() { + return metadata; + } + + public Out setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public Error getError() { + return error; + } + + public Out setError(Error error) { + this.error = error; + return this; + } +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/Plugin.java b/functions-framework-api/src/main/java/dev/openfunction/functions/Plugin.java new file mode 100644 index 00000000..0ce5a40f --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/Plugin.java @@ -0,0 +1,74 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import java.util.Map; + +@Deprecated +public interface Plugin { + /** + * name return the name of this plugin. + * + * @return Plugin name + */ + String name(); + + /** + * version return the version of this plugin. + * + * @return Plugin name + */ + String version(); + + /** + * init will create a new plugin, and execute hook in this calling. + * If you do not want to use a new plugin to execute hook, just return `this`. + * + * @return Plugin + */ + Plugin init(); + + + /** + * execPreHook executes a hook before the function called. + * + * @param ctx Runtime context + * @return error + */ + Error execPreHook(Context ctx); + + /** + * execPreHook executes a hook after the function called. + * + * @param ctx Runtime context + * @return error + */ + Error execPostHook(Context ctx); + + /** + * get return the value of the fieldName` + * + * @param fieldName Name of member + * @return Object + */ + Object getField(String fieldName); + + Boolean needToTracing(); + + Map tagsAddToTracing(); + +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/Routable.java b/functions-framework-api/src/main/java/dev/openfunction/functions/Routable.java new file mode 100644 index 00000000..b43b9125 --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/Routable.java @@ -0,0 +1,31 @@ +package dev.openfunction.functions; + +/** + * An object that can route the specified http request to the specified function. + */ +public abstract class Routable { + public static final String METHOD_DELETE = "DELETE"; + public static final String METHOD_HEAD = "HEAD"; + public static final String METHOD_GET = "GET"; + public static final String METHOD_PATCH = "PATCH"; + public static final String METHOD_POST = "POST"; + public static final String METHOD_PUT = "PUT"; + + /** + * Get the supported http methods. + * + * @return The supported http methods. + */ + public String[] getMethods() { + return new String[]{METHOD_DELETE, METHOD_GET, METHOD_PATCH, METHOD_HEAD, METHOD_PUT, METHOD_POST}; + }; + + /** + * Get the URI that will be routed. + * + * @return The URI that will be routed. + */ + public String getPath(){ + return "/"; + } +} diff --git a/functions-framework-api/src/main/java/dev/openfunction/functions/TopicEvent.java b/functions-framework-api/src/main/java/dev/openfunction/functions/TopicEvent.java new file mode 100644 index 00000000..3bbb745a --- /dev/null +++ b/functions-framework-api/src/main/java/dev/openfunction/functions/TopicEvent.java @@ -0,0 +1,113 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.functions; + +import java.nio.ByteBuffer; +import java.util.Map; + +public class TopicEvent { + /** + * The name of the pubsub the publisher sent to. + */ + private final String name; + + /** + * ID identifies the event. + */ + private final String id; + + /** + * The version of the CloudEvents specification. + */ + private final String specversion; + + /** + * The type of event related to the originating occurrence. + */ + private final String type; + + /** + * Source identifies the context in which an event happened. + */ + private final String source; + + /** + * + */ + private final String datacontenttype; + + /** + * The content of the event. + * Note, this is why the gRPC and HTTP implementations need separate structs for cloud events. + */ + private final ByteBuffer data; + + /** + * The pubsub topic which publisher sent to. + */ + private final String topic; + + private final Map extensions; + + public TopicEvent(String name, String id, String topic, String specversion, String source, String type, String datacontenttype, ByteBuffer data, Map extensions) { + this.name = name; + this.id = id; + this.topic = topic; + this.specversion = specversion; + this.source = source; + this.type = type; + this.datacontenttype = datacontenttype; + this.data = data; + this.extensions = extensions; + } + + public String getName() { + return name; + } + + public String getId() { + return id; + } + + public String getSpecversion() { + return specversion; + } + + public String getType() { + return type; + } + + public String getSource() { + return source; + } + + public String getDatacontenttype() { + return datacontenttype; + } + + public ByteBuffer getData() { + return data; + } + + public String getTopic() { + return topic; + } + + public Map getExtensions() { + return this.extensions; + } +} diff --git a/functions-framework-invoker/pom.xml b/functions-framework-invoker/pom.xml new file mode 100644 index 00000000..516f88a8 --- /dev/null +++ b/functions-framework-invoker/pom.xml @@ -0,0 +1,346 @@ + + 4.0.0 + + + org.sonatype.oss + oss-parent + 9 + + + dev.openfunction.functions + functions-framework-invoker + 1.3.0-SNAPSHOT + + + 3.8.0 + 3.1.0 + UTF-8 + 5.3.2 + 11 + 11 + 2.4.2 + 8.16.0 + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + snapshots + Maven snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots/ + + false + + + true + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.23.1 + pom + import + + + + + + + dev.openfunction.functions + functions-framework-api + 1.3.0-SNAPSHOT + + + org.apache.eventmesh + eventmesh-connector-openfunction + 1.9.0-release + + + org.apache.eventmesh + eventmesh-sdk-java + 1.9.0-release + + + io.cloudevents + cloudevents-api + ${cloudevents.sdk.version} + + + io.cloudevents + cloudevents-core + ${cloudevents.sdk.version} + + + io.cloudevents + cloudevents-protobuf + ${cloudevents.sdk.version} + + + com.google.auto.value + auto-value + 1.10.1 + provided + + + com.google.auto.value + auto-value-annotations + 1.10.1 + provided + + + org.eclipse.jetty + jetty-servlet + 11.0.14 + + + org.eclipse.jetty + jetty-server + 11.0.14 + + + + io.dapr + dapr-sdk + 1.8.0 + + + io.dapr + dapr-sdk-actors + 1.8.0 + + + io.cloudevents + cloudevents-http-basic + ${cloudevents.sdk.version} + + + org.glassfish.jersey.core + jersey-common + 3.1.1 + + + org.slf4j + slf4j-simple + 2.0.5 + + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-api + 1.23.0 + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-sdk-common + + + io.opentelemetry + opentelemetry-sdk-extension-autoconfigure + 1.23.1-alpha + + + io.opentelemetry + opentelemetry-exporter-otlp + + + io.opentelemetry + opentelemetry-exporter-zipkin + + + io.opentelemetry + opentelemetry-exporter-jaeger + + + io.opentelemetry + opentelemetry-exporter-jaeger-proto + + + io.opentelemetry + opentelemetry-exporter-jaeger-thrift + + + io.opentelemetry + opentelemetry-semconv + 1.23.1-alpha + + + + commons-beanutils + commons-beanutils + 1.9.4 + + + com.fasterxml.jackson.core + jackson-databind + 2.14.2 + + + + org.apache.skywalking + apm-toolkit-trace + ${skywalking.version} + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + + dev.openfunction.invoker.Runner + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 11 + 11 + + + + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + + default + + perform + + + functions-framework-runner/pom.xml + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.7 + true + + ossrh + https://s01.oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + + + + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + true + true + UTF-8 + UTF-8 + UTF-8 + + -XDignore.symbol.file + + true + 8 + false + + + + attach-docs + post-integration-test + + jar + + + + + + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/Callback.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/Callback.java new file mode 100644 index 00000000..fb1f59d8 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/Callback.java @@ -0,0 +1,5 @@ +package dev.openfunction.invoker; + +public interface Callback { + Error execute() throws Exception; +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/JsonEventFormat.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/JsonEventFormat.java new file mode 100644 index 00000000..d18d7785 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/JsonEventFormat.java @@ -0,0 +1,185 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; +import io.cloudevents.SpecVersion; +import io.cloudevents.core.data.BytesCloudEventData; +import io.cloudevents.core.format.EventDeserializationException; +import io.cloudevents.core.format.EventFormat; +import io.cloudevents.core.format.EventSerializationException; +import io.cloudevents.core.v03.CloudEventV03; +import io.cloudevents.core.v1.CloudEventV1; +import io.cloudevents.rw.CloudEventDataMapper; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +public class JsonEventFormat implements EventFormat { + + public static final String CONTENT_TYPE = "application/cloudevents+json"; + + public final static String ID = "id"; + public final static String SOURCE = "source"; + public final static String SPECVERSION = "specversion"; + public final static String TYPE = "type"; + public final static String TIME = "time"; + public final static String SCHEMAURL = "schemaurl"; + public final static String DATACONTENTTYPE = "datacontenttype"; + public final static String DATASCHEMA = "dataschema"; + public final static String SUBJECT = "subject"; + public final static String DATA = "data"; + public final static String EXTENSIONS = "extensions"; + public final static String TRACEPARENT = "traceparent"; + public final static String TRACEID = "traceid"; + + @Override + public byte[] serialize(@NotNull CloudEvent event) throws EventSerializationException { + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode root = objectMapper.createObjectNode(); + root.set(SPECVERSION, objectMapper.valueToTree(event.getSpecVersion().toString())); + root.set(ID, objectMapper.valueToTree(event.getId())); + root.set(TYPE, objectMapper.valueToTree(event.getType())); + root.set(SOURCE, objectMapper.valueToTree(event.getSource())); + root.set(SCHEMAURL, objectMapper.valueToTree(event.getDataSchema())); + root.set(DATACONTENTTYPE, objectMapper.valueToTree(event.getDataContentType())); + root.set(SUBJECT, objectMapper.valueToTree(event.getSubject())); + root.set(DATASCHEMA, objectMapper.valueToTree(event.getDataSchema())); + + if (event.getTime() != null) { + root.set(TIME, objectMapper.valueToTree(event.getTime().format(DateTimeFormatter.ISO_DATE_TIME))); + } + + if (event.getData() != null) { + root.set(DATA, objectMapper.valueToTree(new String(event.getData().toBytes()))); + } + + ObjectNode extensions = objectMapper.createObjectNode(); + for (String key : event.getExtensionNames()) { + root.set(key, objectMapper.valueToTree(event.getExtension(key))); + extensions.set(key, objectMapper.valueToTree(event.getExtension(key))); + } + root.set(EXTENSIONS, extensions); + + if (root.get(TRACEPARENT) != null) { + String traceparent = root.get(TRACEPARENT).asText(); + if (!Objects.equals(traceparent, "")) { + root.set(TRACEID, objectMapper.valueToTree(traceparent)); + } + } + + try { + return objectMapper.writeValueAsBytes(root); + } catch (JsonProcessingException e) { + throw new EventSerializationException(e); + } + } + + @Override + public CloudEvent deserialize(@NotNull byte[] bytes, @NotNull CloudEventDataMapper mapper) throws EventDeserializationException { + try { + String specversion = null; + String id = null; + URI source = null; + String type = null; + String datacontenttype = null; + URI schemaurl = null; + URI dataschema = null; + String subject = null; + OffsetDateTime time = null; + BytesCloudEventData data = null; + Map extensions = new HashMap<>(); + + JsonNode root = new ObjectMapper().readTree(bytes); + Iterator fields = root.fieldNames(); + while (fields.hasNext()) { + String field = fields.next(); + JsonNode node = root.get(field); + if (node.isNull()) { + continue; + } + + switch (field) { + case SPECVERSION: + specversion = node.asText(); + break; + case ID: + id = node.asText(); + break; + case SOURCE: + source = new URI(node.asText()); + break; + case TYPE: + type = node.asText(); + break; + case DATACONTENTTYPE: + datacontenttype = node.asText(); + break; + case SCHEMAURL: + schemaurl = new URI(node.asText()); + break; + case SUBJECT: + subject = node.asText(); + break; + case TIME: + time = OffsetDateTime.parse(node.asText()); + break; + case DATASCHEMA: + dataschema = new URI(node.asText()); + break; + case DATA: + data = BytesCloudEventData.wrap(node.asText().getBytes()); + break; + case EXTENSIONS: + Iterator it = node.fieldNames(); + while ( it.hasNext() ) { + String name = it.next(); + extensions.put(name, node.get(name)); + } + break; + default: + extensions.put(field, node); + break; + } + } + + if (Objects.equals(specversion, SpecVersion.V1.toString())) { + return new CloudEventV1(id, source, type, datacontenttype, dataschema, subject,time, data, extensions); + } else { + return new CloudEventV03(id, source, type, time, schemaurl, datacontenttype, subject, data, extensions); + } + } catch (Exception e) { + throw new EventDeserializationException(e); + } + } + + @Override + public String serializedContentType() { + return CONTENT_TYPE; + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/Runner.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/Runner.java new file mode 100644 index 00000000..004d24dd --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/Runner.java @@ -0,0 +1,150 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker; + + +import dev.openfunction.invoker.context.RuntimeContext; +import dev.openfunction.invoker.trigger.DaprTrigger; +import dev.openfunction.invoker.trigger.EventMeshTrigger; +import dev.openfunction.invoker.trigger.HttpTrigger; +import dev.openfunction.invoker.trigger.Trigger; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; + +public class Runner { + private static final Logger logger = Logger.getLogger(Runner.class.getName()); + + private static final String FunctionContext = "FUNC_CONTEXT"; + private static final String FunctionContextV1beta2 = "FUNC_CONTEXT_V1BETA2"; + private static final String FunctionTarget = "FUNCTION_TARGET"; + private static final String FunctionClasspath = "FUNCTION_CLASSPATH"; + + public static void main(String[] args) { + + try { + if (!System.getenv().containsKey(FunctionTarget)) { + throw new Error(FunctionTarget + " not set"); + } + String target = System.getenv(FunctionTarget); + + String functionContext = ""; + if (System.getenv().containsKey(FunctionContext)) { + functionContext = System.getenv(FunctionContext); + } + + if (System.getenv().containsKey(FunctionContextV1beta2)) { + functionContext = System.getenv(FunctionContextV1beta2); + } + + if (StringUtils.isEmpty(functionContext)) { + throw new Error("Function context not set"); + } + + String classPath = System.getenv().getOrDefault(FunctionClasspath, System.getProperty("user.dir") + "/*"); + ClassLoader functionClassLoader = new URLClassLoader(classpathToUrls(classPath)); + RuntimeContext runtimeContext = new RuntimeContext(functionContext, functionClassLoader); + + Class[] functionClasses = loadTargets(target, functionClassLoader); + Set triggers = new HashSet<>(); + if (runtimeContext.hasHttpTrigger()) { + triggers.add(new HttpTrigger(runtimeContext, functionClasses)); + } + + if (runtimeContext.hasDaprTrigger()) { + triggers.add(new DaprTrigger(runtimeContext, functionClasses)); + } + + if (runtimeContext.hasEventMeshTrigger()) { + triggers.add(new EventMeshTrigger(runtimeContext, functionClasses)); + } + + for (Trigger trigger : triggers) { + trigger.start(); + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to run function", e); + e.printStackTrace(); + } + } + + private static Class[] loadTargets(String target, ClassLoader functionClassLoader) throws ClassNotFoundException { + String[] targets = target.split(","); + Class[] classes = new Class[targets.length]; + for (int i = 0; i < targets.length; i++) { + classes[i] = functionClassLoader.loadClass(targets[i]); + } + + return classes; + } + + static URL[] classpathToUrls(String classpath) { + String[] components = classpath.split(File.pathSeparator); + List urls = new ArrayList<>(); + for (String component : components) { + if (component.endsWith(File.separator + "*")) { + urls.addAll(jarsIn(component.substring(0, component.length() - 2))); + } else { + Path path = Paths.get(component); + try { + urls.add(path.toUri().toURL()); + } catch (MalformedURLException e) { + throw new UncheckedIOException(e); + } + } + } + return urls.toArray(new URL[0]); + } + + private static List jarsIn(String dir) { + + Path path = Paths.get(dir); + if (!Files.isDirectory(path)) { + return Collections.emptyList(); + } + + try (Stream stream = Files.list(path)) { + return stream + .filter(p -> p.getFileName().toString().endsWith(".jar")) + .map( + p -> { + try { + return p.toUri().toURL(); + } catch (MalformedURLException e) { + throw new UncheckedIOException(e); + } + }) + .collect(toList()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/FunctionContext.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/FunctionContext.java new file mode 100644 index 00000000..66138cb8 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/FunctionContext.java @@ -0,0 +1,348 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.context; + +import dev.openfunction.functions.Component; + +import java.util.Map; + +class FunctionContext { + + private String name; + private String version; + private Map inputs; + private Map outputs; + private Map states; + @Deprecated + private String runtime; + @Deprecated + private String port = "8080"; + + @Deprecated + private String[] prePlugins; + @Deprecated + private String[] postPlugins; + @Deprecated + private TracingConfig pluginsTracing; + + private String[] preHooks; + private String[] postHooks; + private TracingConfig tracing; + + private Triggers triggers; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Map getInputs() { + return inputs; + } + + public void setInputs(Map inputs) { + this.inputs = inputs; + } + + public Map getOutputs() { + return outputs; + } + + public void setOutputs(Map outputs) { + this.outputs = outputs; + } + + public String getRuntime() { + return runtime; + } + + public void setRuntime(String runtime) { + this.runtime = runtime; + } + + public String getPort() { + if (triggers != null && triggers.http != null) { + return triggers.http.port; + } else { + return port; + } + } + + @Deprecated + public void setPort(String port) { + this.port = port; + } + + @Deprecated + public String[] getPrePlugins() { + return prePlugins; + } + + @Deprecated + public void setPrePlugins(String[] prePlugins) { + this.prePlugins = prePlugins; + } + + @Deprecated + public String[] getPostPlugins() { + return postPlugins; + } + + @Deprecated + public void setPostPlugins(String[] postPlugins) { + this.postPlugins = postPlugins; + } + + @Deprecated + public TracingConfig getPluginsTracing() { + return pluginsTracing; + } + + @Deprecated + public void setPluginsTracing(TracingConfig pluginsTracing) { + this.pluginsTracing = pluginsTracing; + } + + public Map getStates() { + return states; + } + + public void setStates(Map states) { + this.states = states; + } + + public String[] getPreHooks() { + return preHooks; + } + + public void setPreHooks(String[] preHooks) { + this.preHooks = preHooks; + } + + public String[] getPostHooks() { + return postHooks; + } + + public void setPostHooks(String[] postHooks) { + this.postHooks = postHooks; + } + + public TracingConfig getTracing() { + return tracing; + } + + public void setTracing(TracingConfig tracing) { + this.tracing = tracing; + } + + public Triggers getTriggers() { + return triggers; + } + + public void setTriggers(Triggers triggers) { + this.triggers = triggers; + } + + static class Triggers { + private HttpTrigger http; + private DaprTrigger[] dapr; + + private EventMeshTrigger eventMesh; + + public HttpTrigger getHttp() { + return http; + } + + public void setHttp(HttpTrigger http) { + this.http = http; + } + + public DaprTrigger[] getDapr() { + return dapr; + } + + public void setDapr(DaprTrigger[] dapr) { + this.dapr = dapr; + } + + public EventMeshTrigger getEventMesh() { + return eventMesh; + } + + public void setEventMesh(EventMeshTrigger eventMesh) { + this.eventMesh = eventMesh; + } + } + + static class HttpTrigger { + private String port; + + + public String getPort() { + return port; + } + + public void setPort(String port) { + this.port = port; + } + } + + static class DaprTrigger { + private String name; + private String type; + private String topic; + private String inputName; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getInputName() { + return inputName; + } + + public void setInputName(String inputName) { + this.inputName = inputName; + } + } + + static class EventMeshTrigger { + private String name; + + private String type; + + private String topic; + + private String eventMeshConnectorAddr; + + private String eventMeshConnectorPort; + + private String producerGroup; + + private String env; + + private String idc; + + private String sysId; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + + public String getEventMeshConnectorAddr() { + return eventMeshConnectorAddr; + } + + public void setEventMeshConnectorAddr(String eventMeshConnectorAddr) { + this.eventMeshConnectorAddr = eventMeshConnectorAddr; + } + + public String getEventMeshConnectorPort() { + return eventMeshConnectorPort; + } + + public void setEventMeshConnectorPort(String eventMeshConnectorPort) { + this.eventMeshConnectorPort = eventMeshConnectorPort; + } + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = producerGroup; + } + + public String getEnv() { + return env; + } + + public void setEnv(String env) { + this.env = env; + } + + public String getIdc() { + return idc; + } + + public void setIdc(String idc) { + this.idc = idc; + } + + public String getSysId() { + return sysId; + } + + public void setSysId(String sysId) { + this.sysId = sysId; + } + } + + +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/RuntimeContext.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/RuntimeContext.java new file mode 100644 index 00000000..f153ed45 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/RuntimeContext.java @@ -0,0 +1,269 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.context; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfunction.functions.*; +import dev.openfunction.invoker.Callback; +import dev.openfunction.invoker.JsonEventFormat; +import dev.openfunction.invoker.tracing.OpenTelemetryProvider; +import dev.openfunction.invoker.tracing.SkywalkingProvider; +import dev.openfunction.invoker.tracing.TracingProvider; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.provider.EventFormatProvider; + +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class RuntimeContext { + + private static final Logger logger = Logger.getLogger("dev.openfunction.invoker"); + + static final String PodNameEnvName = "POD_NAME"; + static final String PodNamespaceEnvName = "POD_NAMESPACE"; + + @Deprecated + public static final String SyncRuntime = "Knative"; + @Deprecated + public static final String AsyncRuntime = "Async"; + + private static final String TracingSkywalking = "skywalking"; + private static final String TracingOpentelemetry = "opentelemetry"; + + private final FunctionContext functionContext; + + private TracingProvider tracingProvider; + + private Map preHooks; + private Map postHooks; + + public RuntimeContext(String context, ClassLoader classLoader) throws Exception { + functionContext = new ObjectMapper(). + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false). + readValue(context, FunctionContext.class); + + preHooks = new HashMap<>(); + postHooks = new HashMap<>(); + + loadHooks(classLoader); + + TracingConfig tracingConfig = getTracingConfig(); + if (tracingConfig != null && tracingConfig.isEnabled() && tracingConfig.getProvider() != null) { + String provider = tracingConfig.getProvider().getName(); + if (!Objects.equals(provider, TracingSkywalking) && !Objects.equals(provider, TracingOpentelemetry)) { + throw new IllegalArgumentException("unsupported tracing provider " + provider); + } + + switch (provider) { + case TracingSkywalking: + tracingProvider = new SkywalkingProvider(tracingConfig, + functionContext.getName(), + System.getenv(RuntimeContext.PodNameEnvName), + System.getenv(RuntimeContext.PodNamespaceEnvName)); + break; + case TracingOpentelemetry: + tracingProvider = new OpenTelemetryProvider(tracingConfig, + functionContext.getName(), + System.getenv(RuntimeContext.PodNameEnvName), + System.getenv(RuntimeContext.PodNamespaceEnvName)); + break; + } + } + + EventFormatProvider.getInstance().registerFormat(new JsonEventFormat()); + } + + private TracingConfig getTracingConfig() { + TracingConfig tracingConfig = functionContext.getTracing(); + if (tracingConfig != null) { + return tracingConfig; + } + + return functionContext.getPluginsTracing(); + } + + private void loadHooks(ClassLoader classLoader) { + String[] preHookNames = functionContext.getPreHooks(); + if (ArrayUtils.isEmpty(preHookNames)) { + preHookNames = functionContext.getPrePlugins(); + } + + String[] postHookNames = functionContext.getPostHooks(); + if (ArrayUtils.isEmpty(postHookNames)) { + postHookNames = functionContext.getPostPlugins(); + } + preHooks = loadHooks(classLoader, preHookNames); + postHooks = loadHooks(classLoader, postHookNames); + } + + private Map loadHooks(ClassLoader classLoader, String[] hookNames) { + Map hooks = new HashMap<>(); + if (ArrayUtils.isEmpty(hookNames)) { + return hooks; + } + + for (String name : hookNames) { + try { + Class hookClass = classLoader.loadClass(name); + if (Hook.class.isAssignableFrom(hookClass)) { + Class hookImplClass = hookClass.asSubclass(Hook.class); + hooks.put(name, hookImplClass.getConstructor().newInstance()); + } + + if (Plugin.class.isAssignableFrom(hookClass)) { + Class pluginImplClass = hookClass.asSubclass(Plugin.class); + hooks.put(name, pluginImplClass.getConstructor().newInstance()); + } + } catch (Exception e) { + logger.log(Level.WARNING, "load hook " + name + " error, " + e.getMessage()); + e.printStackTrace(); + } + } + + return hooks; + } + + public int getPort() { + return Integer.parseInt(functionContext.getPort()); + } + + public String getName() { + return functionContext.getName(); + } + + public Map getInputs() { + return functionContext.getInputs(); + } + + public void executeWithTracing(Object obj, Callback callback) throws Exception { + if (tracingProvider != null) { + if (obj == null) { + tracingProvider.executeWithTracing(callback); + } else if (obj instanceof HttpRequest) { + tracingProvider.executeWithTracing((HttpRequest) obj, callback); + } else if (obj instanceof CloudEvent) { + tracingProvider.executeWithTracing((CloudEvent) obj, callback); + } else if (obj.getClass().isAssignableFrom(TopicEvent.class)) { + tracingProvider.executeWithTracing((TopicEvent) obj, callback); + } else if (obj.getClass().isAssignableFrom(BindingEvent.class)) { + tracingProvider.executeWithTracing((BindingEvent) obj, callback); + } else if (obj.getClass().isAssignableFrom(UserContext.class)) { + tracingProvider.executeWithTracing((UserContext) obj, callback); + } else if (obj instanceof Plugin) { + tracingProvider.executeWithTracing((Plugin) obj, callback); + } else if (obj instanceof Hook) { + tracingProvider.executeWithTracing((Hook) obj, callback); + } + } else { + Error error = callback.execute(); + if (error != null) { + logger.log(Level.WARNING, "execute failed, ", error); + } + } + } + + public Map getPreHooks() { + return preHooks; + } + + public Map getPostHooks() { + return postHooks; + } + + public boolean hasHttpTrigger() { + if (Objects.equals(functionContext.getRuntime(), SyncRuntime)) { + return true; + } + + return functionContext.getTriggers() != null && functionContext.getTriggers().getHttp() != null; + } + + public boolean hasDaprTrigger() { + if (Objects.equals(functionContext.getRuntime(), AsyncRuntime)) { + return true; + } + + return functionContext.getTriggers() != null && ArrayUtils.isNotEmpty(functionContext.getTriggers().getDapr()); + } + + public Map getDaprTrigger() { + if (Objects.equals(functionContext.getRuntime(), AsyncRuntime)) { + return functionContext.getInputs(); + } + + if (functionContext.getTriggers() != null && + ArrayUtils.isNotEmpty(functionContext.getTriggers().getDapr())) { + Map triggers = new HashMap<>(); + for (FunctionContext.DaprTrigger trigger : functionContext.getTriggers().getDapr()) { + Component component = new Component(); + component.setComponentName(trigger.getName()); + component.setComponentType(trigger.getType()); + component.setTopic(trigger.getTopic()); + triggers.put(component.getComponentName(), component); + } + + return triggers; + } + + return null; + } + + + public boolean hasEventMeshTrigger() { + return functionContext.getTriggers() != null && functionContext.getTriggers().getEventMesh() != null; + } + + public Component getEventMeshTrigger() { + if (functionContext.getTriggers() != null && + functionContext.getTriggers().getEventMesh() != null) { + FunctionContext.EventMeshTrigger trigger = functionContext.getTriggers().getEventMesh(); + + Component component = new Component(); + component.setComponentName(trigger.getName()); + component.setComponentType(trigger.getType()); + component.setTopic(trigger.getTopic()); + Map metaDataMap = new HashMap<>(); + metaDataMap.put("eventMeshConnectorAddr", trigger.getEventMeshConnectorAddr()); + metaDataMap.put("eventMeshConnectorPort", trigger.getEventMeshConnectorPort()); + metaDataMap.put("producerGroup", trigger.getProducerGroup()); + metaDataMap.put("env", trigger.getEnv()); + metaDataMap.put("idc", trigger.getIdc()); + metaDataMap.put("sysId", trigger.getSysId()); + + component.setMetadata(metaDataMap); + + return component; + } + + return null; + } + + public FunctionContext getFunctionContext() { + return functionContext; + } + + public boolean needToCreateDaprClient() { + return (MapUtils.isNotEmpty(functionContext.getInputs())) || + (MapUtils.isNotEmpty(functionContext.getOutputs())) || + (MapUtils.isNotEmpty(functionContext.getStates())); + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/TracingConfig.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/TracingConfig.java new file mode 100644 index 00000000..b015a5cf --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/TracingConfig.java @@ -0,0 +1,131 @@ +package dev.openfunction.invoker.context; + +import java.time.Duration; +import java.util.Map; + +public class TracingConfig { + private boolean enabled; + private Provider provider; + private Map tags; + private Map baggage; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Provider getProvider() { + return provider; + } + + public void setProvider(Provider provider) { + this.provider = provider; + } + + public Map getTags() { + return tags; + } + + public void setTags(Map tags) { + this.tags = tags; + } + + public Map getBaggage() { + return baggage; + } + + public void setBaggage(Map baggage) { + this.baggage = baggage; + } + + public static class Provider { + private String name; + private String oapServer; + private Exporter exporter; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Exporter getExporter() { + return exporter; + } + + public void setExporter(Exporter exporter) { + this.exporter = exporter; + } + + public String getOapServer() { + return oapServer; + } + + public void setOapServer(String oapServer) { + this.oapServer = oapServer; + } + } + + public static class Exporter { + private String name; + private String endpoint; + private Map headers; + private String compression; + private Duration timeout; + private String protocol; + + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public Duration getTimeout() { + return timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public String getCompression() { + return compression; + } + + public void setCompression(String compression) { + this.compression = compression; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/UserContext.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/UserContext.java new file mode 100644 index 00000000..82521bae --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/context/UserContext.java @@ -0,0 +1,321 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.context; + +import dev.openfunction.functions.*; +import dev.openfunction.invoker.Callback; +import dev.openfunction.invoker.JsonEventFormat; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.v03.CloudEventBuilder; +import io.dapr.client.DaprClient; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.eventmesh.client.grpc.producer.EventMeshGrpcProducer; + +import java.net.URI; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class UserContext implements Context { + + private static final Logger logger = Logger.getLogger("dev.openfunction.invoker"); + + private static final Set MiddlewaresCloudEventFormatRequired = Set.of( + "bindings.kafka", + "bindings.kubemq", + "bindings.mqtt3", + "bindings.rabbitmq", + "bindings.redis", + "bindings.gcp.pubsub", + "bindings.azure.eventhubs" + ); + + private final RuntimeContext runtimeContext; + private DaprClient daprClient; + + private EventMeshGrpcProducer eventMeshGrpcProducer; + + private Out out; + + private BindingEvent bindingEvent; + private TopicEvent topicEvent; + private CloudEvent cloudEvent; + + private HttpRequest httpRequest; + private HttpResponse httpResponse; + + private Object function; + + public UserContext(RuntimeContext runtimeContext, DaprClient daprClient) { + this.runtimeContext = runtimeContext; + this.daprClient = daprClient; + } + + public UserContext(RuntimeContext runtimeContext, EventMeshGrpcProducer eventMeshGrpcProducer) { + this.runtimeContext = runtimeContext; + this.eventMeshGrpcProducer = eventMeshGrpcProducer; + } + + public UserContext withHttp(HttpRequest httpRequest, HttpResponse httpResponse) { + this.httpRequest = httpRequest; + this.httpResponse = httpResponse; + return this; + } + + public UserContext withBindingEvent(BindingEvent event) { + this.bindingEvent = event; + return this; + } + + public UserContext withTopicEvent(TopicEvent event) { + this.topicEvent = event; + return this; + } + + @Override + @Deprecated + public Error send(String outputName, String data) { + if (data == null) { + return null; + } + Map outputs = runtimeContext.getFunctionContext().getOutputs(); + if (outputs.isEmpty()) { + return new Error("no output"); + } + + Component output = outputs.get(outputName); + if (output == null) { + return new Error("output " + outputName + " not found"); + } + + if (output.isPubsub()) { + daprClient.publishEvent(output.getComponentName(), output.getTopic(), data); + } else if (output.isBinding()) { + // If a middleware supports both binding and pubsub, then the data send to + // binding must be in CloudEvent format, otherwise pubsub cannot parse the data. + byte[] payload = data.getBytes(); + if (MiddlewaresCloudEventFormatRequired.contains(output.getComponentType())) { + payload = packageAsCloudevent(data); + } + + daprClient.invokeBinding(output.getComponentName(), output.getOperation(), payload).block(); + } else { + return new Error("unsupported output type " + output.getComponentType()); + } + + return null; + } + + @Override + public HttpRequest getHttpRequest() { + return httpRequest; + } + + @Override + public HttpResponse getHttpResponse() { + return httpResponse; + } + + @Override + public BindingEvent getBindingEvent() { + return bindingEvent; + } + + @Override + public TopicEvent getTopicEvent() { + return topicEvent; + } + + @Override + public CloudEvent getCloudEvent() { + return cloudEvent; + } + + @Override + public String getName() { + return runtimeContext.getFunctionContext().getName(); + } + + @Override + public Out getOut() { + return out; + } + + @Override + public String getHttpPattern() { + return null; + } + + @Override + public Map getOutputs() { + return runtimeContext.getFunctionContext().getOutputs(); + } + + @Override + public Map getStates() { + return runtimeContext.getFunctionContext().getStates(); + } + + @Override + public DaprClient getDaprClient() { + return daprClient; + } + + @Override + public EventMeshGrpcProducer getEventMeshProducer() { + return eventMeshGrpcProducer; + } + + @Override + public byte[] packageAsCloudevent(String payload) { + CloudEvent event = new CloudEventBuilder() + .withId(UUID.randomUUID().toString()) + .withType("dapr.invoke") + .withSource(URI.create("openfunction/invokeBinding")) + .withData(payload.getBytes()) + .withDataContentType(JsonEventFormat.CONTENT_TYPE) + .build(); + return new JsonEventFormat().serialize(event); + } + + @Override + public Map getInputs() { + return runtimeContext.getInputs(); + } + + public Class getFunctionClass() { + return function.getClass(); + } + + private void executeHooks(boolean pre) throws Exception { + Map hooks; + if (pre) { + hooks = runtimeContext.getPreHooks(); + } else { + hooks = runtimeContext.getPostHooks(); + } + for (String name : hooks.keySet()) { + Object obj = hooks.get(name); + if (Hook.class.isAssignableFrom(obj.getClass())) { + executeHook(((Hook) obj).init()); + } + + if (Plugin.class.isAssignableFrom(obj.getClass())) { + executePlugin(((Plugin) obj).init(), pre); + } + } + } + + private void executePlugin(Plugin plugin, boolean pre) throws Exception { + if (plugin.needToTracing()) { + runtimeContext.executeWithTracing(plugin, () -> { + Error error; + if (pre) { + error = plugin.execPreHook(UserContext.this); + } else { + error = plugin.execPostHook(UserContext.this); + } + if (error != null) { + logger.log(Level.SEVERE, "execute plugin " + plugin.name() + ":" + plugin.version() + " error", error); + } + + return error; + }); + } else { + Error error; + if (pre) { + error = plugin.execPreHook(UserContext.this); + } else { + error = plugin.execPostHook(UserContext.this); + } + if (error != null) { + logger.log(Level.SEVERE, "execute plugin " + plugin.name() + ":" + plugin.version() + " error", error); + } + } + } + + private void executeHook(Hook hook) throws Exception { + if (hook.needToTracing()) { + runtimeContext.executeWithTracing(hook, () -> { + Error error = hook.execute(UserContext.this); + if (error != null) { + logger.log(Level.SEVERE, "execute hook " + hook.name() + ":" + hook.version() + " error", error); + } + + return error; + }); + } else { + Error error = hook.execute(UserContext.this); + if (error != null) { + logger.log(Level.SEVERE, "execute hook " + hook.name() + ":" + hook.version() + " error", error); + } + } + } + + public void executeFunction(HttpFunction function) throws Exception { + this.function = function; + executeFunction(() -> { + function.service(this.httpRequest, this.httpResponse); + return null; + }); + } + + public void executeFunction(CloudEventFunction function, CloudEvent event) throws Exception { + this.function = function; + this.cloudEvent = event; + executeFunction(() -> { + Error err = function.accept(UserContext.this, event); + if (err == null) { + httpResponse.setStatusCode(HttpServletResponse.SC_OK); + httpResponse.getOutputStream().write(out == null || out.getData() == null ? "Success".getBytes() : out.getData().array()); + } else { + httpResponse.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + httpResponse.getOutputStream().write(err.getMessage().getBytes()); + } + return null; + }); + } + + public void executeFunction(OpenFunction function, String payload) throws Exception { + this.function = function; + executeFunction(() -> { + out = function.accept(UserContext.this, payload); + if (httpResponse != null) { + if (out == null || out.getError() == null) { + httpResponse.setStatusCode(HttpServletResponse.SC_OK); + httpResponse.getOutputStream().write(out == null || out.getData() == null ? "Success".getBytes() : out.getData().array()); + } else { + httpResponse.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + httpResponse.getOutputStream().write(out.getError().getMessage().getBytes()); + } + } + + return out == null ? null : out.getError(); + }); + } + + private void executeFunction(Callback callBack) throws Exception { + runtimeContext.executeWithTracing(this, + () -> { + executeHooks(true); + runtimeContext.executeWithTracing(null, callBack); + executeHooks(false); + return null; + }); + } +} diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/http/HttpRequestImpl.java similarity index 78% rename from invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java rename to functions-framework-invoker/src/main/java/dev/openfunction/invoker/http/HttpRequestImpl.java index db53936e..d599b4ce 100644 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/http/HttpRequestImpl.java @@ -1,40 +1,33 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker.http; +/* +Copyright 2022 The OpenFunction Authors. -import static java.util.stream.Collectors.toMap; +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.http; -import com.google.cloud.functions.HttpRequest; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UncheckedIOException; +import dev.openfunction.functions.HttpRequest; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.Part; +import java.io.*; import java.util.AbstractMap.SimpleEntry; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.Part; + +import static java.util.stream.Collectors.toMap; public class HttpRequestImpl implements HttpRequest { private final HttpServletRequest request; diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/http/HttpResponseImpl.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/http/HttpResponseImpl.java new file mode 100644 index 00000000..e0a46516 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/http/HttpResponseImpl.java @@ -0,0 +1,108 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.http; + +import dev.openfunction.functions.HttpResponse; + +import jakarta.servlet.http.HttpServletResponse; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.util.AbstractMap.SimpleEntry; +import java.util.*; + +import static java.util.stream.Collectors.toMap; + +public class HttpResponseImpl implements HttpResponse { + private final HttpServletResponse response; + + private int code; + + public HttpResponseImpl(HttpServletResponse response) { + this.response = response; + this.code = HttpServletResponse.SC_OK; + } + + @Override + public void setStatusCode(int code) { + this.code = code; + response.setStatus(code); + } + + @Override + @SuppressWarnings("deprecation") + public void setStatusCode(int code, String message) { + this.code = code; + response.setStatus(code, message); + } + + @Override + public void setContentType(String contentType) { + response.setContentType(contentType); + } + + @Override + public Optional getContentType() { + return Optional.ofNullable(response.getContentType()); + } + + @Override + public void appendHeader(String key, String value) { + response.addHeader(key, value); + } + + @Override + public Map> getHeaders() { + return response.getHeaderNames().stream() + .map(header -> new SimpleEntry<>(header, list(response.getHeaders(header)))) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static List list(Collection collection) { + return (collection instanceof List) ? (List) collection : new ArrayList<>(collection); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return response.getOutputStream(); + } + + private BufferedWriter writer; + + @Override + public synchronized BufferedWriter getWriter() throws IOException { + if (writer == null) { + // Unfortunately this means that we get two intermediate objects between the object we return + // and the underlying Writer that response.getWriter() wraps. We could try accessing the + // PrintWriter.out field via reflection, but that sort of access to non-public fields of + // platform classes is now frowned on and may draw warnings or even fail in subsequent + // versions. + // We could instead wrap the OutputStream, but that would require us to deduce the appropriate + // Charset, using logic like this: + // https://github.com/eclipse/jetty.project/blob/923ec38adf/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java#L731 + // We may end up doing that if performance is an issue. + writer = new BufferedWriter(response.getWriter()); + } + return writer; + } + + @Override + public int getStatusCode() { + return code; + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/tracing/OpenTelemetryProvider.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/tracing/OpenTelemetryProvider.java new file mode 100644 index 00000000..781bd398 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/tracing/OpenTelemetryProvider.java @@ -0,0 +1,299 @@ +package dev.openfunction.invoker.tracing; + +import dev.openfunction.functions.*; +import dev.openfunction.invoker.Callback; +import dev.openfunction.invoker.context.TracingConfig; +import dev.openfunction.invoker.context.UserContext; +import io.cloudevents.CloudEvent; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter; +import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporterBuilder; +import io.opentelemetry.exporter.jaeger.thrift.JaegerThriftSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.OpenTelemetrySdkBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class OpenTelemetryProvider implements TracingProvider { + private static final String OTEL_LIBRARY_NAME = "opentelemetry-java"; + private static final String OTEL_LIBRARY_VERSION = "1.23.1"; + + private static final String Protocol_HTTP = "http"; + + private final String functionName; + private Map tags; + private final Map baggage; + private final TextMapGetter> getter; + + public OpenTelemetryProvider(TracingConfig config, String functionName, String pod, String namespace) throws Exception { + this.functionName = functionName; + + OpenTelemetrySdkBuilder openTelemetrySdkBuilder = OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())); + TracingConfig.Exporter exporter = config.getProvider().getExporter(); + if (exporter != null) { + SpanExporter spanExporter; + String exporterName = config.getProvider().getExporter().getName(); + switch (exporterName) { + case "otlp": + spanExporter = createOtlpExporter(exporter); + break; + case "jaeger": + spanExporter = createJaegerExporter(exporter); + break; + case "zipkin": + ZipkinSpanExporterBuilder builder = ZipkinSpanExporter.builder().setEndpoint(exporter.getEndpoint()); + if (exporter.getTimeout() != null && !exporter.getTimeout().isZero()) { + builder.setReadTimeout(exporter.getTimeout()); + } + if (exporter.getCompression() != null && !Objects.equals(exporter.getCompression(), "")) { + builder.setCompression(exporter.getCompression()); + } + spanExporter = builder.build(); + break; + default: + throw new Exception("opentelemetry export not set, use default"); + } + + Resource resource = Resource.getDefault() + .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, functionName))); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build()) + .setResource(resource) + .build(); + openTelemetrySdkBuilder.setTracerProvider(sdkTracerProvider); + } + + openTelemetrySdkBuilder.buildAndRegisterGlobal(); + + getter = new TextMapGetter<>() { + @Override + public Iterable keys(@NotNull Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, @NotNull String key) { + return carrier.get(key); + } + }; + + tags = config.getTags(); + if (tags == null) { + tags = new HashMap<>(); + } + if (!Objects.equals(pod, "")) { + tags.put("instance", pod); + } + if (!Objects.equals(namespace, "")) { + tags.put("namespace", pod); + } + + baggage = config.getBaggage(); + } + + private static SpanExporter createOtlpExporter(TracingConfig.Exporter exporter) { + String protocol = exporter.getProtocol(); + if (protocol != null && Objects.equals(protocol, Protocol_HTTP)) { + OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder().setEndpoint(exporter.getEndpoint()); + if (exporter.getTimeout() != null && !exporter.getTimeout().isZero()) { + builder.setTimeout(exporter.getTimeout()); + } + if (exporter.getCompression() != null && !Objects.equals(exporter.getCompression(), "")) { + builder.setCompression(exporter.getCompression()); + } + if (exporter.getHeaders() != null) { + for (String key : exporter.getHeaders().keySet()) { + builder.addHeader(key, exporter.getHeaders().get(key)); + } + } + return JaegerThriftSpanExporter.builder().setEndpoint(exporter.getEndpoint()).build(); + } else { + OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder().setEndpoint(exporter.getEndpoint()); + if (exporter.getTimeout() != null && !exporter.getTimeout().isZero()) { + builder.setTimeout(exporter.getTimeout()); + } + if (exporter.getCompression() != null && !Objects.equals(exporter.getCompression(), "")) { + builder.setCompression(exporter.getCompression()); + } + if (exporter.getHeaders() != null) { + for (String key : exporter.getHeaders().keySet()) { + builder.addHeader(key, exporter.getHeaders().get(key)); + } + } + return builder.build(); + } + } + + private static SpanExporter createJaegerExporter(TracingConfig.Exporter exporter) { + String protocol = exporter.getProtocol(); + if (protocol != null && Objects.equals(protocol, Protocol_HTTP)) { + return JaegerThriftSpanExporter.builder().setEndpoint(exporter.getEndpoint()).build(); + } else { + JaegerGrpcSpanExporterBuilder builder = JaegerGrpcSpanExporter.builder().setEndpoint(exporter.getEndpoint()); + if (exporter.getTimeout() != null && !exporter.getTimeout().isZero()) { + builder.setTimeout(exporter.getTimeout()); + } + if (exporter.getCompression() != null && !Objects.equals(exporter.getCompression(), "")) { + builder.setCompression(exporter.getCompression()); + } + + return builder.build(); + } + } + + @Override + public void executeWithTracing(HttpRequest httpRequest, Callback callback) throws Exception { + Map carrier = new HashMap<>(); + for (String key : httpRequest.getHeaders().keySet()) { + carrier.put(key, httpRequest.getHeaders().get(key).get(0)); + } + + executeWithTracing(carrier, callback); + } + + @Override + public void executeWithTracing(CloudEvent event, Callback callback) throws Exception { + Map carrier = new HashMap<>(); + for (String key : event.getExtensionNames()) { + Object obj = event.getExtension(key); + carrier.put(key, obj == null ? "" : obj.toString()); + } + + executeWithTracing(carrier, callback); + } + + @Override + public void executeWithTracing(TopicEvent event, Callback callback) throws Exception { + executeWithTracing(new HashMap<>(), callback); + } + + @Override + public void executeWithTracing(BindingEvent event, Callback callback) throws Exception { + executeWithTracing(new HashMap<>(), callback); + } + + @Override + public void executeWithTracing(Callback callback) throws Exception { + executeWithTracing("function", SpanKind.INTERNAL, null, callback); + } + + @Override + public void executeWithTracing(Plugin plugin, Callback callback) throws Exception { + Map tags = new HashMap<>(); + tags.put("kind", "Plugin"); + tags.put("name", plugin.name()); + tags.put("version", plugin.version()); + if (plugin.tagsAddToTracing() != null) { + tags.putAll(plugin.tagsAddToTracing()); + } + + executeWithTracing(plugin.name(), SpanKind.INTERNAL, tags, callback); + } + + @Override + public void executeWithTracing(Hook hook, Callback callback) throws Exception { + Map tags = new HashMap<>(); + tags.put("kind", "Hook"); + tags.put("name", hook.name()); + tags.put("version", hook.version()); + if (hook.tagsAddToTracing() != null) { + tags.putAll(hook.tagsAddToTracing()); + } + + executeWithTracing(hook.name(), SpanKind.INTERNAL, tags, callback); + } + + @Override + public void executeWithTracing(UserContext ctx, Callback callback) throws Exception { + SpanKind kind = SpanKind.SERVER; + Map tags = new HashMap<>(); + tags.put("function", ctx.getFunctionClass().getName()); + + executeWithTracing(ctx.getFunctionClass().getSimpleName(), kind, tags, callback); + } + + private void executeWithTracing(Map carrier, Callback callback) throws Exception { + TextMapPropagator propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); + Context parentContext = propagator.extract(Context.root(), carrier, getter); + Tracer tracer = GlobalOpenTelemetry.getTracer(OTEL_LIBRARY_NAME, OTEL_LIBRARY_VERSION); + Span span = tracer.spanBuilder(functionName) + .setParent(parentContext) + .setSpanKind(SpanKind.SERVER) + .startSpan(); + + setGlobalAttribute(span); + try (Scope ignored = span.makeCurrent()) { + endSpan(span, callback.execute()); + } + } + + private void executeWithTracing(String name, SpanKind kind, Map tags, Callback callback) throws + Exception { + Tracer tracer = GlobalOpenTelemetry.getTracer(OTEL_LIBRARY_NAME, OTEL_LIBRARY_VERSION); + Span span = tracer.spanBuilder(name) + .setSpanKind(kind) + .startSpan(); + span.setAttribute(SemanticAttributes.FAAS_INVOKED_NAME, this.functionName); + span.setAttribute(SemanticAttributes.FAAS_INVOKED_PROVIDER, "OpenFunction"); + + if (tags != null) { + for (String key : tags.keySet()) { + span.setAttribute(key, tags.get(key)); + } + } + + setGlobalAttribute(span); + + try (Scope ignored = span.makeCurrent()) { + endSpan(span, callback.execute()); + } + } + + private void endSpan(Span span, Error error) { + if (error != null) { + span.setStatus(StatusCode.ERROR, error.getMessage()); + } + + span.end(); + } + + private void setGlobalAttribute(Span span) { + span.setAttribute(SemanticAttributes.FAAS_INVOKED_NAME, this.functionName); + span.setAttribute(SemanticAttributes.FAAS_INVOKED_PROVIDER, "OpenFunction"); + + if (tags != null) { + for (String key : tags.keySet()) { + span.setAttribute(AttributeKey.stringKey(key), tags.get(key)); + } + } + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/tracing/SkywalkingProvider.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/tracing/SkywalkingProvider.java new file mode 100644 index 00000000..d99abd40 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/tracing/SkywalkingProvider.java @@ -0,0 +1,181 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.tracing; + +import dev.openfunction.functions.*; +import dev.openfunction.invoker.Callback; +import dev.openfunction.invoker.context.TracingConfig; +import dev.openfunction.invoker.context.UserContext; +import io.cloudevents.CloudEvent; +import org.apache.skywalking.apm.toolkit.trace.*; + +import java.util.*; + +public class SkywalkingProvider implements TracingProvider { + /** + * ... + */ +// private static final int componentIDOpenFunction = 5013; + + private final String functionName; + private Map tags; + private final Map baggage; + + public SkywalkingProvider(TracingConfig config, String functionName, String pod, String namespace) { + this.functionName = functionName; + + tags = config.getTags(); + tags = config.getTags(); + if (tags == null) { + tags = new HashMap<>(); + } + if (!Objects.equals(pod, "")) { + tags.put("instance", pod); + } + if (!Objects.equals(namespace, "")) { + tags.put("namespace", pod); + } + baggage = config.getBaggage(); + } + + @Override + public void executeWithTracing(HttpRequest httpRequest, Callback callback) throws Exception { + Map carrier = new HashMap<>(); + Map> headers = httpRequest.getHeaders(); + if (headers != null) { + for (String key : headers.keySet()) { + if (headers.get(key).size() > 0) { + carrier.put(key, headers.get(key).get(0)); + } + } + } + + HashMap newTags = new HashMap<>(tags); + newTags.put("Method", httpRequest.getMethod()); + newTags.put("URI", httpRequest.getUri()); + + executeWithTracing(carrier, newTags, callback); + } + + @Override + public void executeWithTracing(CloudEvent event, Callback callback) throws Exception { + Map carrier = new HashMap<>(); + for (String key : event.getExtensionNames()) { + Object obj = event.getExtension(key); + carrier.put(key, obj == null ? "" : obj.toString()); + } + executeWithTracing(carrier, tags, callback); + } + + @Override + public void executeWithTracing(BindingEvent event, Callback callback) throws Exception { + executeWithTracing(new HashMap<>(), tags, callback); + } + + @Override + public void executeWithTracing(TopicEvent event, Callback callback) throws Exception { + executeWithTracing(new HashMap<>(), tags, callback); + } + + private void executeWithTracing(Map carrier, Map tags, Callback callback) throws Exception { + ContextCarrierRef contextCarrierRef = new ContextCarrierRef(); + if (carrier != null) { + CarrierItemRef next = contextCarrierRef.items(); + while (next.hasNext()) { + next = next.next(); + if (carrier.get(next.getHeadKey()) != null) { + next.setHeadValue(carrier.get(next.getHeadKey())); + } + } + } + + SpanRef span = Tracer.createEntrySpan(functionName, contextCarrierRef); + if (tags != null) { + for (String key : tags.keySet()) { + span.tag(key, tags.get(key)); + } + } + + if (baggage != null) { + for (String key : baggage.keySet()) { + TraceContext.putCorrelation(key, baggage.get(key)); + } + } + + Error err = callback.execute(); + if (err != null) { + ActiveSpan.error(err); + } + Tracer.stopSpan(); + } + + @Override + public void executeWithTracing(Callback callback) throws Exception { + executeWithTracing("function", null, callback); + } + + @Override + public void executeWithTracing(Plugin plugin, Callback callback) throws Exception { + Map tags = new HashMap<>(); + tags.put("kind", "Plugin"); + tags.put("name", plugin.name()); + tags.put("version", plugin.version()); + if (plugin.tagsAddToTracing() != null) { + tags.putAll(plugin.tagsAddToTracing()); + } + + executeWithTracing(plugin.name(), tags, callback); + } + + @Override + public void executeWithTracing(Hook hook, Callback callback) throws Exception { + Map tags = new HashMap<>(); + tags.put("kind", "Hook"); + tags.put("name", hook.name()); + tags.put("version", hook.version()); + if (hook.tagsAddToTracing() != null) { + tags.putAll(hook.tagsAddToTracing()); + } + + executeWithTracing(hook.name(), tags, callback); + } + + @Override + public void executeWithTracing(UserContext ctx, Callback callback) throws Exception { + Map tags = new HashMap<>(); + tags.put("function", ctx.getFunctionClass().getName()); + + executeWithTracing(ctx.getFunctionClass().getSimpleName(), tags, callback); + } + + private void executeWithTracing(String name, Map tags, Callback callback) throws Exception { + SpanRef span = Tracer.createLocalSpan(name); + + if (tags != null) { + for (String key : tags.keySet()) { + span.tag(key, tags.get(key)); + } + } + + Error err = callback.execute(); + if (err != null) { + ActiveSpan.error(err); + } + + Tracer.stopSpan(); + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/tracing/TracingProvider.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/tracing/TracingProvider.java new file mode 100644 index 00000000..efaf50c5 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/tracing/TracingProvider.java @@ -0,0 +1,26 @@ +package dev.openfunction.invoker.tracing; + +import dev.openfunction.functions.*; +import dev.openfunction.invoker.Callback; +import dev.openfunction.invoker.context.UserContext; +import io.cloudevents.CloudEvent; + +import java.util.Map; + +public interface TracingProvider { + void executeWithTracing(HttpRequest httpRequest, Callback callback) throws Exception; + + void executeWithTracing(CloudEvent event, Callback callback) throws Exception; + + void executeWithTracing(TopicEvent event, Callback callback) throws Exception; + + void executeWithTracing(BindingEvent event, Callback callback) throws Exception; + + void executeWithTracing(Callback callback)throws Exception; + + void executeWithTracing(Plugin plugin, Callback callback)throws Exception; + + void executeWithTracing(Hook hook, Callback callback)throws Exception; + + void executeWithTracing(UserContext ctx, Callback callback)throws Exception; +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/DaprTrigger.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/DaprTrigger.java new file mode 100644 index 00000000..6f5abffc --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/DaprTrigger.java @@ -0,0 +1,209 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.trigger; + +import com.google.protobuf.Value; +import dev.openfunction.functions.BindingEvent; +import dev.openfunction.functions.Component; +import dev.openfunction.functions.OpenFunction; +import dev.openfunction.functions.TopicEvent; +import dev.openfunction.invoker.context.RuntimeContext; +import dev.openfunction.invoker.context.UserContext; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.v1.AppCallbackGrpc; +import io.dapr.v1.DaprAppCallbackProtos; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.stub.StreamObserver; +import org.apache.commons.collections.MapUtils; + +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Executes the user's asynchronous function. + */ +public final class DaprTrigger implements Trigger { + private static final Logger logger = Logger.getLogger("dev.openfunction.invoker"); + + private final RuntimeContext runtimeContext; + + private final ArrayList functions; + + private final Service service; + + public DaprTrigger(RuntimeContext runtimeContext, Class[] functionClasses) { + this.runtimeContext = runtimeContext; + + functions = new ArrayList<>(); + for (Class c : functionClasses) { + if (!OpenFunction.class.isAssignableFrom(c)) { + throw new Error("Unsupported function " + c.getName()); + } + + try { + Class openFunctionClass = c.asSubclass(OpenFunction.class); + functions.add(openFunctionClass.getConstructor().newInstance()); + } catch (ReflectiveOperationException e) { + throw new Error("Could not construct an instance of " + c.getName(), e); + } + } + + service = new Service(); + } + + @Override + public void start() throws Exception { + if (MapUtils.isEmpty(runtimeContext.getDaprTrigger())) { + throw new Error("no dapr trigger defined for the function"); + } + + this.service.start(runtimeContext.getPort()); + } + + @Override + public void close() { + + } + + private class Service extends AppCallbackGrpc.AppCallbackImplBase { + + private Server daprServer; + private DaprClient daprClient; + + public void start(int port) throws Exception { + daprServer = ServerBuilder + .forPort(port) + .addService(Service.this) + .build() + .start(); + + daprClient = new DaprClientBuilder().build(); + daprClient.waitForSidecar(WaitDaprSidecarTimeout); + + // Now we handle ctrl+c (or any other JVM shutdown) + java.lang.Runtime.getRuntime().addShutdownHook(new Thread(() -> { + daprClient.shutdown(); + daprServer.shutdown(); + }) + ); + + daprServer.awaitTermination(); + } + + @Override + public void listInputBindings(com.google.protobuf.Empty request, + io.grpc.stub.StreamObserver responseObserver) { + + List bindings = new ArrayList<>(); + for (String key : runtimeContext.getDaprTrigger().keySet()) { + Component component = runtimeContext.getDaprTrigger().get(key); + if (component.isBinding()) { + bindings.add(component.getComponentName()); + } + } + + responseObserver.onNext(DaprAppCallbackProtos.ListInputBindingsResponse.newBuilder().addAllBindings(bindings).build()); + responseObserver.onCompleted(); + } + + @Override + public void onBindingEvent(DaprAppCallbackProtos.BindingEventRequest request, + StreamObserver responseObserver) { + BindingEvent event = new BindingEvent(request.getName(), request.getMetadataMap(), request.getData().asReadOnlyByteBuffer()); + + try { + runtimeContext.executeWithTracing(event, () -> { + for (OpenFunction function : functions) { + new UserContext(runtimeContext, daprClient). + withBindingEvent(event). + executeFunction(function, request.getData().toStringUtf8()); + } + responseObserver.onNext(DaprAppCallbackProtos.BindingEventResponse.getDefaultInstance()); + responseObserver.onCompleted(); + return null; + } + ); + } catch (Exception e) { + logger.log(Level.INFO, "catch exception when execute function " + runtimeContext.getName()); + e.printStackTrace(); + responseObserver.onError(e); + } + } + + @Override + public void listTopicSubscriptions(com.google.protobuf.Empty request, + io.grpc.stub.StreamObserver responseObserver) { + List subscriptions = new ArrayList<>(); + for (String key : runtimeContext.getDaprTrigger().keySet()) { + Component component = runtimeContext.getDaprTrigger().get(key); + if (component.isPubsub()) { + subscriptions.add(DaprAppCallbackProtos.TopicSubscription.newBuilder().setTopic(component.getTopic()).setPubsubName(component.getComponentName()).build()); + } + } + responseObserver.onNext(DaprAppCallbackProtos.ListTopicSubscriptionsResponse.newBuilder().addAllSubscriptions(subscriptions).build()); + responseObserver.onCompleted(); + } + + @Override + public void onTopicEvent(DaprAppCallbackProtos.TopicEventRequest request, + io.grpc.stub.StreamObserver responseObserver) { + TopicEvent event = new TopicEvent(request.getPubsubName(), + request.getId(), + request.getTopic(), + request.getSpecVersion(), + request.getSource(), + request.getType(), + request.getDataContentType(), + request.getData().asReadOnlyByteBuffer(), + getExtensions(request)); + + try { + runtimeContext.executeWithTracing(event, () -> { + for (OpenFunction function : functions) { + new UserContext(runtimeContext, daprClient). + withTopicEvent(event). + executeFunction(function, request.getData().toStringUtf8()); + } + responseObserver.onNext(DaprAppCallbackProtos.TopicEventResponse.getDefaultInstance()); + responseObserver.onCompleted(); + return null; + } + ); + } catch (Exception e) { + logger.log(Level.INFO, "catch exception when execute function " + runtimeContext.getName()); + e.printStackTrace(); + responseObserver.onError(e); + } + } + } + + private Map getExtensions(DaprAppCallbackProtos.TopicEventRequest request) { + Map extensions = new HashMap<>(); + Map fields = request.getExtensions().getFieldsMap(); + for (String key : fields.keySet()) { + Value value = fields.get(key); + if (value.hasStringValue()) { + extensions.put(key, value.getStringValue()); + } + } + + return extensions; + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/EventMeshTrigger.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/EventMeshTrigger.java new file mode 100644 index 00000000..d7433d98 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/EventMeshTrigger.java @@ -0,0 +1,162 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.trigger; + +import org.apache.commons.collections.MapUtils; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.cloudevents.core.format.EventFormat; +import io.cloudevents.core.provider.EventFormatProvider; +import io.cloudevents.protobuf.ProtobufFormat; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.stub.StreamObserver; + +import dev.openfunction.functions.CloudEventFunction; +import dev.openfunction.functions.Component; +import dev.openfunction.functions.OpenFunction; +import dev.openfunction.invoker.context.RuntimeContext; +import dev.openfunction.invoker.context.UserContext; + +import org.apache.eventmesh.client.grpc.config.EventMeshGrpcClientConfig; +import org.apache.eventmesh.client.grpc.producer.EventMeshGrpcProducer; +import org.apache.eventmesh.common.protocol.grpc.cloudevents.CloudEvent; +import org.apache.eventmesh.connector.openfunction.client.CallbackServiceGrpc; + +/** + * Executes the user's asynchronous function. + */ +public final class EventMeshTrigger implements Trigger { + + private static final Logger logger = Logger.getLogger("dev.openfunction.invoker"); + + private static final EventFormat eventFormat = EventFormatProvider.getInstance().resolveFormat(ProtobufFormat.PROTO_CONTENT_TYPE); + + private final RuntimeContext runtimeContext; + + private final ArrayList functions; + + private final Service service; + + public EventMeshTrigger(RuntimeContext runtimeContext, Class[] functionClasses) { + this.runtimeContext = runtimeContext; + + functions = new ArrayList<>(); + for (Class c : functionClasses) { + try { + if (OpenFunction.class.isAssignableFrom(c)) { + Class openFunctionClass = c.asSubclass(OpenFunction.class); + functions.add(openFunctionClass.getConstructor().newInstance()); + } else { + throw new Error("Unsupported function " + c.getName()); + } + + } catch (ReflectiveOperationException e) { + throw new Error("Could not construct an instance of " + c.getName(), e); + } + } + + service = new Service(); + } + + @Override + public void start() throws Exception { + if (runtimeContext.getEventMeshTrigger() == null) { + throw new Error("no eventmesh trigger defined for the function"); + } + + this.service.start(runtimeContext.getPort()); + } + + @Override + public void close() { + + } + + private class Service extends CallbackServiceGrpc.CallbackServiceImplBase { + + private Server eventMeshServer; + + private EventMeshGrpcProducer eventMeshGrpcProducer; + + public void start(int port) throws Exception { + eventMeshServer = ServerBuilder + .forPort(port) + .addService(Service.this) + .build() + .start(); + + // this map only have one trigger + Component eventMeshTrigger = runtimeContext.getEventMeshTrigger(); + Map metaDataMap = eventMeshTrigger.getMetadata(); + EventMeshGrpcClientConfig eventMeshGrpcClientConfig = EventMeshGrpcClientConfig.builder() + .serverAddr(metaDataMap.get("eventMeshConnectorAddr")) + .serverPort(Integer.parseInt(metaDataMap.get("eventMeshConnectorPort"))) + .producerGroup(metaDataMap.get("producerGroup")) + .env(metaDataMap.get("env")) + .idc(metaDataMap.get("idc")) + .sys(metaDataMap.get("sysId")) + .build(); + + eventMeshGrpcProducer = new EventMeshGrpcProducer(eventMeshGrpcClientConfig); + + // Now we handle ctrl+c (or any other JVM shutdown) + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + eventMeshGrpcProducer.close(); + eventMeshServer.shutdown(); + }) + ); + + eventMeshServer.awaitTermination(); + } + + @Override + public void onTopicEvent(CloudEvent cloudEvent, StreamObserver responseObserver) { + + assert eventFormat != null; + io.cloudevents.CloudEvent event = eventFormat.deserialize(cloudEvent.toByteArray()); + + try { + runtimeContext.executeWithTracing(event, () -> { + for (Object function : functions) { + runtimeContext.executeWithTracing(event, () -> { + new UserContext(runtimeContext, eventMeshGrpcProducer). + executeFunction((OpenFunction) function, + new String(Objects.requireNonNull(event.getData()).toBytes(), StandardCharsets.UTF_8)); + return null; + } + ); + } + responseObserver.onNext(CloudEvent.getDefaultInstance()); + responseObserver.onCompleted(); + return null; + } + ); + } catch (Exception e) { + logger.log(Level.INFO, "catch exception when execute function " + runtimeContext.getName()); + e.printStackTrace(); + responseObserver.onError(e); + } + } + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/HttpTrigger.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/HttpTrigger.java new file mode 100644 index 00000000..b7bc80ac --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/HttpTrigger.java @@ -0,0 +1,167 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.trigger; + +import dev.openfunction.functions.CloudEventFunction; +import dev.openfunction.functions.HttpFunction; +import dev.openfunction.functions.OpenFunction; +import dev.openfunction.functions.Routable; +import dev.openfunction.invoker.context.RuntimeContext; +import dev.openfunction.invoker.context.UserContext; +import dev.openfunction.invoker.http.HttpRequestImpl; +import dev.openfunction.invoker.http.HttpResponseImpl; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.message.MessageReader; +import io.cloudevents.http.HttpMessageFactory; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Executes the user's synchronize method. + */ +public class HttpTrigger extends HttpServlet implements Trigger { + private static final Logger logger = Logger.getLogger("dev.openfunction..invoker"); + + private final Class[] functionClasses; + + private final RuntimeContext runtimeContext; + + private DaprClient daprClient; + + public HttpTrigger(RuntimeContext runtimeContext, Class[] functionClasses) { + this.runtimeContext = runtimeContext; + this.functionClasses = functionClasses; + } + + @Override + public void start() throws Exception { + if (runtimeContext.needToCreateDaprClient()) { + daprClient = new DaprClientBuilder().build(); + daprClient.waitForSidecar(Trigger.WaitDaprSidecarTimeout); + } + + ServletContextHandler handler = new ServletContextHandler(); + handler.setContextPath("/"); + for (Class c : functionClasses) { + Object function; + if (CloudEventFunction.class.isAssignableFrom(c)) { + Class cloudEventFunctionClass = c.asSubclass(CloudEventFunction.class); + function = cloudEventFunctionClass.getConstructor().newInstance(); + } else if (HttpFunction.class.isAssignableFrom(c)) { + Class httpFunctionClass = c.asSubclass(HttpFunction.class); + function = httpFunctionClass.getConstructor().newInstance(); + } else if (OpenFunction.class.isAssignableFrom(c)) { + Class openFunctionClass = c.asSubclass(OpenFunction.class); + function = openFunctionClass.getConstructor().newInstance(); + } else { + throw new Error("Unsupported function " + c.getName()); + } + + String path = "/*"; + if (Routable.class.isAssignableFrom(c)) { + path = ((Routable) function).getPath(); + } + handler.addServlet(new ServletHolder(new OpenFunctionServlet(function)), path); + } + + Server server = new Server(runtimeContext.getPort()); + server.setHandler(handler); + server.start(); + server.join(); + } + + @Override + public void close() { + } + + class OpenFunctionServlet extends HttpServlet { + private final Object function; + + public OpenFunctionServlet(Object function) { + this.function = function; + } + + /** + * Executes the user's method, can handle all HTTP type methods. + */ + @Override + public void service(HttpServletRequest req, HttpServletResponse res) { + HttpRequestImpl reqImpl = new HttpRequestImpl(req); + HttpResponseImpl respImpl = new HttpResponseImpl(res); + try { + if (Routable.class.isAssignableFrom(function.getClass())) { + List methods = Arrays.asList((((Routable) function).getMethods())); + if (methods.stream().noneMatch(req.getMethod()::equalsIgnoreCase)) { + respImpl.setStatusCode(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + } + + UserContext userContext = new UserContext(runtimeContext, daprClient). + withHttp(reqImpl, respImpl); + if (HttpFunction.class.isAssignableFrom(function.getClass())) { + runtimeContext.executeWithTracing(reqImpl, () -> { + userContext.executeFunction(((HttpFunction) function)); + return null; + } + ); + } else if (CloudEventFunction.class.isAssignableFrom(function.getClass())) { + MessageReader messageReader = HttpMessageFactory.createReaderFromMultimap(reqImpl.getHeaders(), reqImpl.getInputStream().readAllBytes()); + CloudEvent event = messageReader.toEvent(); + runtimeContext.executeWithTracing(event, () -> { + userContext.executeFunction((CloudEventFunction) function, event); + return null; + }); + } else if (OpenFunction.class.isAssignableFrom(function.getClass())) { + runtimeContext.executeWithTracing(reqImpl, () -> { + userContext.executeFunction((OpenFunction) function, new String(reqImpl.getInputStream().readAllBytes())); + return null; + } + ); + } + } catch (Throwable t) { + logger.log(Level.SEVERE, "Failed to execute function", t); + res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } finally { + try { + // We can't use HttpServletResponse.flushBuffer() because we wrap the PrintWriter + // returned by HttpServletResponse in our own BufferedWriter to match our API. + // So we have to flush whichever of getWriter() or getOutputStream() works. + try { + respImpl.getOutputStream().flush(); + } catch (IllegalStateException e) { + respImpl.getWriter().flush(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/Trigger.java b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/Trigger.java new file mode 100644 index 00000000..e82f2265 --- /dev/null +++ b/functions-framework-invoker/src/main/java/dev/openfunction/invoker/trigger/Trigger.java @@ -0,0 +1,26 @@ +/* +Copyright 2022 The OpenFunction Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dev.openfunction.invoker.trigger; + +public interface Trigger { + + int WaitDaprSidecarTimeout = 60000; + + void start() throws Exception; + + void close(); +} diff --git a/functions-framework-invoker/src/main/resources/nocalhost/components.yaml b/functions-framework-invoker/src/main/resources/nocalhost/components.yaml new file mode 100644 index 00000000..70663101 --- /dev/null +++ b/functions-framework-invoker/src/main/resources/nocalhost/components.yaml @@ -0,0 +1,48 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: cron-input +spec: + type: bindings.cron + version: v1 + metadata: + - name: schedule + value: "@every 2s" +--- +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: kafka-output +spec: + type: bindings.kafka + version: v1 + metadata: + - name: brokers + value: "kafka-server-kafka-brokers:9092" + - name: topics + value: "topic-test" + - name: consumerGroup + value: "topic-test" + - name: publishTopic + value: "topic-test" + - name: authRequired + value: "false" +--- +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: msg +spec: + type: pubsub.kafka + version: v1 + metadata: + - name: brokers + value: "kafka-server-kafka-brokers:9092" + - name: consumerGroup + value: "group1" + - name: authRequired + value: "false" + - name: allowedTopics + value: "topic-test,topic-otel-1" + - name: consumerID + value: "topic-test" diff --git a/functions-framework-invoker/src/main/resources/nocalhost/config.yaml b/functions-framework-invoker/src/main/resources/nocalhost/config.yaml new file mode 100644 index 00000000..1944670a --- /dev/null +++ b/functions-framework-invoker/src/main/resources/nocalhost/config.yaml @@ -0,0 +1,55 @@ +# This is the runtime configuration which stored in K8s cluster. Modifications +# to the development configuration will take effect the next time you enter +# the DevMode, and modification will share with all those who use this cluster. +# +# If you want to customized personal configuration, you can create a configuration +# file named config.yaml in the root directory of your project under the +# folder .nocalhost (/.nocalhost/config.yaml). It will become part of your +# project, you can easily share configuration with other developers, or +# develop on any other devices using this personal configuration. +# +# Tips: You can paste the configuration follow into +# /root/IdeaProjects/functions-framework-java/functions-framework-invoker/.nocalhost/config.yaml +# +# In addition, if you want to config multi service in same config.yaml, or use +# the Server-version of Nocalhost, you can also configure under the definition +# of the application, such as: +# https://nocalhost.dev/docs/config/config-deployment-quickstart +# +name: sample-java +serviceType: deployment +containers: + - name: function + dev: + gitUrl: "" + image: nocalhost-docker.pkg.coding.net/nocalhost/dev-images/java:11 + shell: "" + workDir: "" + storageClass: "" + resources: null + persistentVolumeDirs: [] + command: + run: + - ./src/main/resources/nocalhost/run.sh + debug: + - ./src/main/resources/nocalhost/debug.sh + debug: + remoteDebugPort: 5005 + language: java + hotReload: false + sync: + type: send + filePattern: [] + ignoreFilePattern: [] + env: + - name: FUNCTION_TARGET + value: dev.openfunction.samples.AsyncFunctionImpl + - name: FUNC_CONTEXT + #value: "{\"name\":\"sample-pubsub\",\"version\":\"v2.0.0\",\"inputs\":{\"sub\":{\"uri\":\"sample-topic\",\"componentName\":\"msg\",\"componentType\":\"pubsub.kafka\"}},\"outputs\":{},\"runtime\":\"Async\",\"port\":\"8080\",\"prePlugins\":[\"dev.openfunction.samples.plugin.ExamplePlugin\"],\"postPlugins\":[\"dev.openfunction.samples.plugin.ExamplePlugin\"]}" + # function context for binding with tracing + #value:"{\"name\":\"sample-binding\",\"version\":\"v2.0.0\",\"inputs\":{\"cron\":{\"componentName\":\"cron-input\",\"componentType\":\"bindings.cron\"}},\"outputs\":{\t\t\t\t\"kafka\":{\t\t\t\t\"uri\":\"topic-test\",\t\t\t\t\"componentName\":\"kafka-output\",\"componentType\":\"bindings.kafka\",\"operation\":\"create\"\t\t\t\t}},\"runtime\":\"Async\",\"port\":\"8080\",\"prePlugins\":[\"dev.openfunction.samples.plugins.ExamplePlugin\"],\"postPlugins\":[\"dev.openfunction.samples.plugins.ExamplePlugin\"],\"pluginsTracing\":{\"enabled\":true,\"provider\":{\"name\":\"opentelemetry\"},\"tags\":{\"func\":\"sample-binding\",\"layer\":\"faas\"},\"baggage\":{\"key\":\"opentelemetry\",\"value\":\"v1.23.0\"}}}" + portForward: [] + patches: + - patch: '{"spec":{"template":{"metadata":{"annotations":{"dapr.io/app-id":"cron-input-kafka-output-default", "dapr.io/app-port": "8080", "dapr.io/app-protocol":"grpc","dapr.io/enabled":"true","dapr.io/log-as-json": "true","dapr.io/log-level":"debug"}}}}}' + type: strategic + diff --git a/functions-framework-invoker/src/main/resources/nocalhost/debug.sh b/functions-framework-invoker/src/main/resources/nocalhost/debug.sh new file mode 100755 index 00000000..0ee86a14 --- /dev/null +++ b/functions-framework-invoker/src/main/resources/nocalhost/debug.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +mvn clean compile dependency:copy-dependencies +mvn exec:exec -Dexec.executable="java" -Dexec.args="-javaagent:./skywalking-agent/skywalking-agent.jar -classpath samples-1.0-SNAPSHOT.jar:target/classes/:target/dependency/* -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 dev.openfunction.invoker.Runner" diff --git a/functions-framework-invoker/src/main/resources/nocalhost/deployment.yaml b/functions-framework-invoker/src/main/resources/nocalhost/deployment.yaml new file mode 100644 index 00000000..610fb73d --- /dev/null +++ b/functions-framework-invoker/src/main/resources/nocalhost/deployment.yaml @@ -0,0 +1,47 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: sample-java + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: sample-java + template: + metadata: + labels: + app.kubernetes.io/name: sample-java + spec: + containers: + - name: nocalhost-dev + image: 'nocalhost-docker.pkg.coding.net/nocalhost/dev-images/java:11' + command: + - /bin/sh + - '-c' + - tail -f /dev/null + workingDir: /home/nocalhost-dev + ports: + - name: function-port + containerPort: 8080 + protocol: TCP + env: + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + value: default + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + imagePullPolicy: Always + restartPolicy: Always + terminationGracePeriodSeconds: 30 + dnsPolicy: ClusterFirst + securityContext: {} + schedulerName: default-scheduler + strategy: + type: Recreate + revisionHistoryLimit: 10 + progressDeadlineSeconds: 600 diff --git a/functions-framework-invoker/src/main/resources/nocalhost/run.sh b/functions-framework-invoker/src/main/resources/nocalhost/run.sh new file mode 100755 index 00000000..34db50ab --- /dev/null +++ b/functions-framework-invoker/src/main/resources/nocalhost/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +mvn clean compile dependency:copy-dependencies +mvn exec:exec -Dexec.executable="java" -Dexec.args="-javaagent:./skywalking-agent/skywalking-agent.jar -classpath samples-1.0-SNAPSHOT.jar:target/classes/:target/dependency/* dev.openfunction.invoker.Runner" diff --git a/invoker/conformance/pom.xml b/invoker/conformance/pom.xml deleted file mode 100644 index 189f656f..00000000 --- a/invoker/conformance/pom.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - 4.0.0 - - java-function-invoker-parent - com.google.cloud.functions.invoker - 1.1.1-SNAPSHOT - - - com.google.cloud.functions.invoker - conformance - 1.1.1-SNAPSHOT - - GCF Confromance Tests - - A GCF project used to validate conformance to the Functions Framework contract - using the Functions Framework Conformance tools. - - https://github.com/GoogleCloudPlatform/functions-framework-conformance - - - UTF-8 - 11 - 11 - - - - - com.google.cloud.functions - functions-framework-api - - - com.google.code.gson - gson - 2.8.6 - - - io.cloudevents - cloudevents-core - 2.2.0 - - - io.cloudevents - cloudevents-json-jackson - 2.2.0 - - - - - - - - com.google.cloud.functions - function-maven-plugin - 0.10.1-SNAPSHOT - - - - - diff --git a/invoker/conformance/src/main/java/com/google/cloud/functions/conformance/BackgroundEventConformanceFunction.java b/invoker/conformance/src/main/java/com/google/cloud/functions/conformance/BackgroundEventConformanceFunction.java deleted file mode 100644 index b21e68c8..00000000 --- a/invoker/conformance/src/main/java/com/google/cloud/functions/conformance/BackgroundEventConformanceFunction.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.google.cloud.functions.conformance; - -import com.google.cloud.functions.Context; -import com.google.cloud.functions.RawBackgroundFunction; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import java.io.BufferedWriter; -import java.io.FileWriter; - -/** - * This class is used by the Functions Framework Conformance Tools to validate the framework's - * Background Event API. It can be run with the following command: - * - *
{@code
- * $ functions-framework-conformance-client \
- *   -cmd="mvn function:run -Drun.functionTarget=com.google.cloud.functions.conformance.BackgroundEventConformanceFunction" \
- *   -type=legacyevent \
- *   -buildpacks=false \
- *   -validate-mapping=false \
- *   -start-delay=10
- * }
- */ -public class BackgroundEventConformanceFunction implements RawBackgroundFunction { - - private static final Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create(); - - @Override - public void accept(String data, Context context) throws Exception { - try (BufferedWriter writer = new BufferedWriter(new FileWriter("function_output.json"))) { - writer.write(serialize(data, context)); - } - } - - /** Create a structured JSON representation of the request context and data */ - private String serialize(String data, Context context) { - JsonObject contextJson = new JsonObject(); - contextJson.addProperty("eventId", context.eventId()); - contextJson.addProperty("timestamp", context.timestamp()); - contextJson.addProperty("eventType", context.eventType()); - - if (context.resource().startsWith("{")) { - JsonElement resource = gson.fromJson(context.resource(), JsonElement.class); - contextJson.add("resource", resource); - } else { - contextJson.addProperty("resource", context.resource()); - } - - JsonObject dataJson = gson.fromJson(data, JsonObject.class); - - JsonObject json = new JsonObject(); - json.add("data", dataJson); - json.add("context", contextJson); - return gson.toJson(json); - } -} diff --git a/invoker/conformance/src/main/java/com/google/cloud/functions/conformance/CloudEventsConformanceFunction.java b/invoker/conformance/src/main/java/com/google/cloud/functions/conformance/CloudEventsConformanceFunction.java deleted file mode 100644 index 7faa079c..00000000 --- a/invoker/conformance/src/main/java/com/google/cloud/functions/conformance/CloudEventsConformanceFunction.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.conformance; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.cloud.functions.CloudEventsFunction; -import io.cloudevents.CloudEvent; -import io.cloudevents.core.format.EventFormat; -import io.cloudevents.core.provider.EventFormatProvider; -import io.cloudevents.jackson.JsonFormat; -import java.io.BufferedWriter; -import java.io.FileWriter; - -/** - * This class is used by the Functions Framework Conformance Tools to validate the framework's Cloud - * Events API. It can be run with the following command: - * - *
{@code
- * $ functions-framework-conformance-client \
- *   -cmd="mvn function:run -Drun.functionTarget=com.google.cloud.functions.conformance.CloudEventsConformanceFunction" \
- *   -type=cloudevent \
- *   -buildpacks=false \
- *   -validate-mapping=false \
- *   -start-delay=5
- * }
- */ -public class CloudEventsConformanceFunction implements CloudEventsFunction { - - @Override - public void accept(CloudEvent event) throws Exception { - try (BufferedWriter writer = new BufferedWriter(new FileWriter("function_output.json"))) { - EventFormat format = EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE); - writer.write(new String(format.serialize(event), UTF_8)); - } - } -} diff --git a/invoker/conformance/src/main/java/com/google/cloud/functions/conformance/HttpConformanceFunction.java b/invoker/conformance/src/main/java/com/google/cloud/functions/conformance/HttpConformanceFunction.java deleted file mode 100644 index 46eafd09..00000000 --- a/invoker/conformance/src/main/java/com/google/cloud/functions/conformance/HttpConformanceFunction.java +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.conformance; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpResponse; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; - -/** - * This class is used by the Functions Framework Conformance Tools to validate the framework's HTTP - * API. It can be run with the following command: - * - *
{@code
- * $ functions-framework-conformance-client \
- *   -cmd="mvn function:run -Drun.functionTarget=com.google.cloud.functions.conformance.HttpConformanceFunction" \
- *   -type=http \
- *   -buildpacks=false \
- *   -validate-mapping=false \
- *   -start-delay=5
- * }
- */ -public class HttpConformanceFunction implements HttpFunction { - - @Override - public void service(HttpRequest request, HttpResponse response) throws IOException { - try (BufferedReader reader = request.getReader(); - BufferedWriter writer = new BufferedWriter(new FileWriter("function_output.json"))) { - int c; - while ((c = reader.read()) != -1) { - writer.write(c); - } - } - } -} diff --git a/invoker/core/pom.xml b/invoker/core/pom.xml deleted file mode 100644 index b751e6a2..00000000 --- a/invoker/core/pom.xml +++ /dev/null @@ -1,186 +0,0 @@ - - 4.0.0 - - - com.google.cloud.functions.invoker - java-function-invoker-parent - 1.1.1-SNAPSHOT - - - com.google.cloud.functions.invoker - java-function-invoker - 1.1.1-SNAPSHOT - GCF Java Invoker - - Application that invokes a GCF Java function. This application is a - complete HTTP server that interprets incoming HTTP requests appropriately - and forwards them to the function code. - - - - UTF-8 - 5.3.2 - 11 - 11 - 2.0.0.RC2 - - - - - Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - scm:git:https://github.com/GoogleCloudPlatform/functions-framework-java.git - scm:git:git@github.com:GoogleCloudPlatform/functions-framework-java.git - https://github.com/GoogleCloudPlatform/functions-framework-java - HEAD - - - - - com.google.cloud.functions - functions-framework-api - - - javax.servlet - javax.servlet-api - 3.1.0 - - - io.cloudevents - cloudevents-core - ${cloudevents.sdk.version} - - - io.cloudevents - cloudevents-http-basic - ${cloudevents.sdk.version} - - - io.cloudevents - cloudevents-json-jackson - ${cloudevents.sdk.version} - - - com.google.code.gson - gson - 2.8.6 - - - com.ryanharter.auto.value - auto-value-gson - 1.3.0 - provided - - - com.ryanharter.auto.value - auto-value-gson-annotations - 0.8.0 - provided - - - com.google.auto.value - auto-value - 1.7 - provided - - - com.google.auto.value - auto-value-annotations - 1.7 - provided - - - org.eclipse.jetty - jetty-servlet - 9.4.45.v20220203 - - - org.eclipse.jetty - jetty-server - 9.4.45.v20220203 - - - com.beust - jcommander - 1.82 - - - - - com.google.cloud.functions.invoker - java-function-invoker-testfunction - 1.1.1-SNAPSHOT - test-jar - test - - - org.mockito - mockito-core - 3.2.4 - test - - - junit - junit - 4.13.1 - test - - - com.google.re2j - re2j - 1.6 - - - com.google.truth - truth - 1.0.1 - test - - - com.google.truth.extensions - truth-java8-extension - 1.0.1 - test - - - org.eclipse.jetty - jetty-client - 9.4.26.v20200117 - test - - - - - - - maven-jar-plugin - 3.1.2 - - - - com.google.cloud.functions.invoker.runner.Invoker - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.1 - - - package - - shade - - - - - - - diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java deleted file mode 100644 index 98b9bc8a..00000000 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java +++ /dev/null @@ -1,396 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker; - -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; - -import com.google.cloud.functions.BackgroundFunction; -import com.google.cloud.functions.CloudEventsFunction; -import com.google.cloud.functions.Context; -import com.google.cloud.functions.RawBackgroundFunction; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.TypeAdapter; -import io.cloudevents.CloudEvent; -import io.cloudevents.core.message.MessageReader; -import io.cloudevents.http.HttpMessageFactory; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.TreeMap; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** Executes the user's background function. */ -public final class BackgroundFunctionExecutor extends HttpServlet { - private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker"); - - private final FunctionExecutor functionExecutor; - - private BackgroundFunctionExecutor(FunctionExecutor functionExecutor) { - this.functionExecutor = functionExecutor; - } - - private enum FunctionKind { - BACKGROUND(BackgroundFunction.class), - RAW_BACKGROUND(RawBackgroundFunction.class), - CLOUD_EVENTS(CloudEventsFunction.class); - - static final List VALUES = Arrays.asList(values()); - - final Class functionClass; - - FunctionKind(Class functionClass) { - this.functionClass = functionClass; - } - - /** Returns the {@link FunctionKind} that the given class implements, if any. */ - static Optional forClass(Class functionClass) { - return VALUES.stream() - .filter(v -> v.functionClass.isAssignableFrom(functionClass)) - .findFirst(); - } - } - - /** - * Optionally makes a {@link BackgroundFunctionExecutor} for the given class, if it implements one - * of {@link BackgroundFunction}, {@link RawBackgroundFunction}, or {@link CloudEventsFunction}. - * Otherwise returns {@link Optional#empty()}. - * - * @param functionClass the class of a possible background function implementation. - * @throws RuntimeException if the given class does implement one of the required interfaces, but - * we are unable to construct an instance using its no-arg constructor. - */ - public static Optional maybeForClass(Class functionClass) { - Optional maybeFunctionKind = FunctionKind.forClass(functionClass); - if (!maybeFunctionKind.isPresent()) { - return Optional.empty(); - } - return Optional.of(forClass(functionClass, maybeFunctionKind.get())); - } - - /** - * Makes a {@link BackgroundFunctionExecutor} for the given class. - * - * @throws RuntimeException if either the class does not implement one of {@link - * BackgroundFunction}, {@link RawBackgroundFunction}, or {@link CloudEventsFunction}; or we - * are unable to construct an instance using its no-arg constructor. - */ - public static BackgroundFunctionExecutor forClass(Class functionClass) { - Optional maybeFunctionKind = FunctionKind.forClass(functionClass); - if (!maybeFunctionKind.isPresent()) { - List classNames = - FunctionKind.VALUES.stream().map(v -> v.functionClass.getName()).collect(toList()); - throw new RuntimeException( - "Class " - + functionClass.getName() - + " must implement one of these interfaces: " - + String.join(", ", classNames)); - } - return forClass(functionClass, maybeFunctionKind.get()); - } - - private static BackgroundFunctionExecutor forClass( - Class functionClass, FunctionKind functionKind) { - Object instance; - try { - instance = functionClass.getConstructor().newInstance(); - } catch (ReflectiveOperationException e) { - throw new RuntimeException( - "Could not construct an instance of " + functionClass.getName() + ": " + e, e); - } - FunctionExecutor executor; - switch (functionKind) { - case RAW_BACKGROUND: - executor = new RawFunctionExecutor((RawBackgroundFunction) instance); - break; - case BACKGROUND: - BackgroundFunction backgroundFunction = (BackgroundFunction) instance; - @SuppressWarnings("unchecked") - Class> c = - (Class>) backgroundFunction.getClass(); - Optional maybeTargetType = backgroundFunctionTypeArgument(c); - if (!maybeTargetType.isPresent()) { - // This is probably because the user implemented just BackgroundFunction rather than - // BackgroundFunction. - throw new RuntimeException( - "Could not determine the payload type for BackgroundFunction of type " - + instance.getClass().getName() - + "; must implement BackgroundFunction for some T"); - } - executor = new TypedFunctionExecutor<>(maybeTargetType.get(), backgroundFunction); - break; - case CLOUD_EVENTS: - executor = new CloudEventFunctionExecutor((CloudEventsFunction) instance); - break; - default: // can't happen, we've listed all the FunctionKind values already. - throw new AssertionError(functionKind); - } - return new BackgroundFunctionExecutor(executor); - } - - /** - * Returns the {@code T} of a concrete class that implements {@link BackgroundFunction - * BackgroundFunction}. Returns an empty {@link Optional} if {@code T} can't be determined. - */ - static Optional backgroundFunctionTypeArgument( - Class> functionClass) { - // If this is BackgroundFunction then the user must have implemented a method - // accept(Foo, Context), so we look for that method and return the type of its first argument. - // We must be careful because the compiler will also have added a synthetic method - // accept(Object, Context). - return Arrays.stream(functionClass.getMethods()) - .filter( - m -> - m.getName().equals("accept") - && m.getParameterCount() == 2 - && m.getParameterTypes()[1] == Context.class - && m.getParameterTypes()[0] != Object.class) - .map(m -> m.getGenericParameterTypes()[0]) - .findFirst(); - } - - private static Event parseLegacyEvent(HttpServletRequest req) throws IOException { - try (BufferedReader bodyReader = req.getReader()) { - return parseLegacyEvent(bodyReader); - } - } - - static Event parseLegacyEvent(Reader reader) throws IOException { - // A Type Adapter is required to set the type of the JsonObject because CloudFunctionsContext - // is abstract and Gson default behavior instantiates the type provided. - TypeAdapter typeAdapter = CloudFunctionsContext.typeAdapter(new Gson()); - Gson gson = - new GsonBuilder() - .registerTypeAdapter(CloudFunctionsContext.class, typeAdapter) - .registerTypeAdapter(Event.class, new Event.EventDeserializer()) - .create(); - return gson.fromJson(reader, Event.class); - } - - private static Context contextFromCloudEvent(CloudEvent cloudEvent) { - OffsetDateTime timestamp = - Optional.ofNullable(cloudEvent.getTime()).orElse(OffsetDateTime.now()); - String timestampString = DateTimeFormatter.ISO_INSTANT.format(timestamp); - // We don't have an obvious replacement for the Context.resource field, which with legacy events - // corresponded to a value present for some proprietary Google event types. - String resource = "{}"; - Map attributesMap = - cloudEvent.getAttributeNames().stream() - .collect(toMap(a -> a, a -> String.valueOf(cloudEvent.getAttribute(a)))); - return CloudFunctionsContext.builder() - .setEventId(cloudEvent.getId()) - .setEventType(cloudEvent.getType()) - .setResource(resource) - .setTimestamp(timestampString) - .setAttributes(attributesMap) - .build(); - } - - /** - * A background function, either "raw" or "typed". A raw background function is one where the user - * code receives a String parameter that is the JSON payload of the triggering event. A typed - * background function is one where the payload is deserialized into a user-provided class whose - * field names correspond to the keys of the JSON object. - * - *

In addition to these two flavours, events can be either "legacy events" or "CloudEvents". - * Legacy events are the only kind that GCF originally supported, and use proprietary encodings - * for the various triggers. CloudEvents are ones that follow the standards defined by cloudevents.io. - * - * @param the type to be used in the {@link Unmarshallers} call when - * unmarshalling this event, if it is a CloudEvent. - */ - private abstract static class FunctionExecutor { - private final Class functionClass; - - FunctionExecutor(Class functionClass) { - this.functionClass = functionClass; - } - - final String functionName() { - return functionClass.getCanonicalName(); - } - - final ClassLoader functionClassLoader() { - return functionClass.getClassLoader(); - } - - abstract void serviceLegacyEvent(Event legacyEvent) throws Exception; - - abstract void serviceCloudEvent(CloudEvent cloudEvent) throws Exception; - } - - private static class RawFunctionExecutor extends FunctionExecutor> { - private static Gson gson = new GsonBuilder().serializeNulls().create(); - private final RawBackgroundFunction function; - - RawFunctionExecutor(RawBackgroundFunction function) { - super(function.getClass()); - this.function = function; - } - - @Override - void serviceLegacyEvent(Event legacyEvent) throws Exception { - function.accept(gson.toJson(legacyEvent.getData()), legacyEvent.getContext()); - } - - @Override - void serviceCloudEvent(CloudEvent cloudEvent) throws Exception { - serviceLegacyEvent(CloudEvents.convertToLegacyEvent(cloudEvent)); - } - } - - private static class TypedFunctionExecutor extends FunctionExecutor { - private final Type type; // T - private final BackgroundFunction function; - - private TypedFunctionExecutor(Type type, BackgroundFunction function) { - super(function.getClass()); - this.type = type; - this.function = function; - } - - static TypedFunctionExecutor of(Type type, BackgroundFunction instance) { - @SuppressWarnings("unchecked") - BackgroundFunction function = (BackgroundFunction) instance; - return new TypedFunctionExecutor<>(type, function); - } - - @Override - void serviceLegacyEvent(Event legacyEvent) throws Exception { - T payload = new Gson().fromJson(legacyEvent.getData(), type); - function.accept(payload, legacyEvent.getContext()); - } - - @Override - void serviceCloudEvent(CloudEvent cloudEvent) throws Exception { - if (cloudEvent.getData() != null) { - serviceLegacyEvent(CloudEvents.convertToLegacyEvent(cloudEvent)); - } else { - throw new IllegalStateException("Event has no \"data\" component"); - } - } - } - - private static class CloudEventFunctionExecutor extends FunctionExecutor { - private final CloudEventsFunction function; - - CloudEventFunctionExecutor(CloudEventsFunction function) { - super(function.getClass()); - this.function = function; - } - - @Override - void serviceLegacyEvent(Event legacyEvent) throws Exception { - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - function.accept(cloudEvent); - } - - @Override - void serviceCloudEvent(CloudEvent cloudEvent) throws Exception { - function.accept(cloudEvent); - } - } - - /** Executes the user's background function. This can handle all HTTP methods. */ - @Override - public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { - String contentType = req.getContentType(); - try { - if ((contentType != null && contentType.startsWith("application/cloudevents+json")) - || req.getHeader("ce-specversion") != null) { - serviceCloudEvent(req); - } else { - serviceLegacyEvent(req); - } - res.setStatus(HttpServletResponse.SC_OK); - } catch (Throwable t) { - res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - logger.log(Level.SEVERE, "Failed to execute " + functionExecutor.functionName(), t); - } - } - - private enum CloudEventKind { - BINARY, - STRUCTURED - } - - /** - * Service a CloudEvent. - * - * @param a fake type parameter, which corresponds to the type parameter of {@link - * FunctionExecutor}. - */ - private void serviceCloudEvent(HttpServletRequest req) throws Exception { - @SuppressWarnings("unchecked") - FunctionExecutor executor = (FunctionExecutor) functionExecutor; - byte[] body = req.getInputStream().readAllBytes(); - MessageReader reader = HttpMessageFactory.createReaderFromMultimap(headerMap(req), body); - // It's important not to set the context ClassLoader earlier, because MessageUtils will use - // ServiceLoader.load(EventFormat.class) to find a handler to deserialize a binary CloudEvent - // and if it finds something from the function ClassLoader then that something will implement - // the EventFormat interface as defined by that ClassLoader rather than ours. Then - // ServiceLoader.load - // will throw ServiceConfigurationError. At this point we're still running with the default - // context ClassLoader, which is the system ClassLoader that has loaded the code here. - runWithContextClassLoader(() -> executor.serviceCloudEvent(reader.toEvent(data -> data))); - // The data->data is a workaround for a bug fixed since Milestone 4 of the SDK, in - // https://github.com/cloudevents/sdk-java/pull/259. - } - - private static Map> headerMap(HttpServletRequest req) { - Map> headerMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - for (String header : Collections.list(req.getHeaderNames())) { - for (String value : Collections.list(req.getHeaders(header))) { - headerMap.computeIfAbsent(header, unused -> new ArrayList<>()).add(value); - } - } - return headerMap; - } - - private void serviceLegacyEvent(HttpServletRequest req) throws Exception { - Event event = parseLegacyEvent(req); - runWithContextClassLoader(() -> functionExecutor.serviceLegacyEvent(event)); - } - - private void runWithContextClassLoader(ContextClassLoaderTask task) throws Exception { - ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); - try { - Thread.currentThread().setContextClassLoader(functionExecutor.functionClassLoader()); - task.run(); - } finally { - Thread.currentThread().setContextClassLoader(oldLoader); - } - } - - @FunctionalInterface - private interface ContextClassLoaderTask { - void run() throws Exception; - } -} diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/CloudEvents.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/CloudEvents.java deleted file mode 100644 index 0021d44e..00000000 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/CloudEvents.java +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker; - -import static java.util.Map.entry; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.re2j.Matcher; -import com.google.re2j.Pattern; -import io.cloudevents.CloudEvent; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Map; -import java.util.Optional; - -/** Conversions from CloudEvents events to GCF Background Events. */ -class CloudEvents { - private static final String PUB_SUB_MESSAGE_TYPE = - "type.googleapis.com/google.pubsub.v1.PubsubMessage"; - - private static final Map EVENT_TYPE_MAPPING = - Map.ofEntries( - entry( - "google.cloud.pubsub.topic.v1.messagePublished", - new PubSubEventAdapter("google.pubsub.topic.publish")), - entry( - "google.cloud.storage.object.v1.finalized", - new StorageEventAdapter("google.storage.object.finalize")), - entry( - "google.cloud.storage.object.v1.deleted", - new StorageEventAdapter("google.storage.object.delete")), - entry( - "google.cloud.storage.object.v1.archived", - new StorageEventAdapter("google.storage.object.archive")), - entry( - "google.cloud.storage.object.v1.metadataUpdated", - new StorageEventAdapter("google.storage.object.metadataUpdate")), - entry( - "google.cloud.firestore.document.v1.written", - new EventAdapter("providers/cloud.firestore/eventTypes/document.write")), - entry( - "google.cloud.firestore.document.v1.created", - new EventAdapter("providers/cloud.firestore/eventTypes/document.create")), - entry( - "google.cloud.firestore.document.v1.updated", - new EventAdapter("providers/cloud.firestore/eventTypes/document.update")), - entry( - "google.cloud.firestore.document.v1.deleted", - new EventAdapter("providers/cloud.firestore/eventTypes/document.delete")), - entry( - "google.firebase.analytics.log.v1.written", - new EventAdapter("providers/google.firebase.analytics/eventTypes/event.log")), - entry( - "google.firebase.auth.user.v1.created", - new FirebaseAuthEventAdapter("providers/firebase.auth/eventTypes/user.create")), - entry( - "google.firebase.auth.user.v1.deleted", - new FirebaseAuthEventAdapter("providers/firebase.auth/eventTypes/user.delete")), - entry( - "google.firebase.database.ref.v1.created", - new FirebaseDatabaseEventAdapter( - "providers/google.firebase.database/eventTypes/ref.create")), - entry( - "google.firebase.database.ref.v1.written", - new FirebaseDatabaseEventAdapter( - "providers/google.firebase.database/eventTypes/ref.write")), - entry( - "google.firebase.database.ref.v1.updated", - new FirebaseDatabaseEventAdapter( - "providers/google.firebase.database/eventTypes/ref.update")), - entry( - "google.firebase.database.ref.v1.deleted", - new FirebaseDatabaseEventAdapter( - "providers/google.firebase.database/eventTypes/ref.delete")), - entry( - "google.cloud.storage.object.v1.changed", - new StorageEventAdapter("providers/cloud.storage/eventTypes/object.change"))); - - private static final Gson GSON = new GsonBuilder().serializeNulls().create(); - - /** - * Converts a CloudEvent to the legacy event format. - * - * @param cloudEvent the CloudEvent to convert - * @return the legacy event representation of the Cloud Event - */ - static Event convertToLegacyEvent(CloudEvent cloudEvent) { - String eventType = cloudEvent.getType(); - EventAdapter eventAdapter = EVENT_TYPE_MAPPING.get(eventType); - if (eventAdapter == null) { - throw new IllegalArgumentException("Unrecognized CloudEvent type \"" + eventType + "\""); - } - return eventAdapter.convertToLegacyEvent(cloudEvent); - } - - private static class EventAdapter { - private final String legacyEventType; - private static Pattern sourcePattern = Pattern.compile("//([^/]+)/(.+)"); - - protected class ParsedCloudEvent { - public final String Resource; - public final String Service; - public final String Name; - - public ParsedCloudEvent(String resource, String service, String name) { - this.Resource = resource; - this.Service = service; - this.Name = name; - } - } - ; - - /** - * Creates an adapter to convert from the CloudEvent to a legacy event. - * - * @param legacyEventType the event type of the legacy event being created - */ - EventAdapter(String legacyEventType) { - this.legacyEventType = legacyEventType; - } - - /** - * Converts a CloudEvent to the legacy event format. - * - * @param cloudEvent the CloudEvent to convert - * @return the legacy event representation of the Cloud Event - */ - final Event convertToLegacyEvent(CloudEvent cloudEvent) { - /* - Ex 1: "//firebaseauth.googleapis.com/projects/my-project-id" - m.group(0): "//firebaseauth.googleapis.com/projects/my-project-id" - m.group(1): "firebaseauth.googleapis.com" - m.group(2): "projects/my-project-id" - - Ex 2: "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test" - m.group(0): "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test" - m.group(1): "pubsub.googleapis.com" - m.group(2): "projects/sample-project/topics/gcf-test" - */ - Matcher m = sourcePattern.matcher(cloudEvent.getSource().toString()); - if (!m.find() || m.groupCount() != 2) { - throw new IllegalArgumentException( - String.format( - "Invalid CloudEvent source '%s', unable to parse into resource service and name", - cloudEvent.getSource().toString())); - } - - String service = m.group(1); - String name = m.group(2); - String resource = String.format("%s/%s", name, cloudEvent.getSubject()); - ParsedCloudEvent parsed = new ParsedCloudEvent(resource, service, name); - - OffsetDateTime timestamp = - Optional.ofNullable(cloudEvent.getTime()).orElse(OffsetDateTime.now()); - - CloudFunctionsContext.Builder ctxBuilder = - CloudFunctionsContext.builder() - .setEventId(cloudEvent.getId()) - .setEventType(this.legacyEventType) - .setResource(resource) - .setTimestamp(DateTimeFormatter.ISO_INSTANT.format(timestamp)); - - JsonObject data = - GSON.fromJson( - new String(cloudEvent.getData().toBytes(), java.nio.charset.StandardCharsets.UTF_8), - JsonObject.class); - return createLegacyEvent(parsed, ctxBuilder, data); - } - - /** - * Provides a hook to furither modify the converted event for specific event adapter subclasses. - * - * @param event convenient information parsed from the original CloudEvent - * @param builder the builder for the converted legacy event's context, pre-populated with - * defaults from the original CloudEvent - * @param data the data for the converted legacy event's data, pre-populated with defaults from - * the original CloudEvent - * @return the fully converted legacy event - */ - Event createLegacyEvent( - ParsedCloudEvent event, CloudFunctionsContext.Builder builder, JsonObject data) { - return Event.of(data, builder.build()); - } - } - - private static class PubSubEventAdapter extends EventAdapter { - PubSubEventAdapter(String legacyEventType) { - super(legacyEventType); - } - - @Override - Event createLegacyEvent( - ParsedCloudEvent event, CloudFunctionsContext.Builder builder, JsonObject data) { - JsonObject resource = new JsonObject(); - resource.addProperty("service", event.Service); - resource.addProperty("name", event.Name); - resource.addProperty("type", PUB_SUB_MESSAGE_TYPE); - builder.setResource(GSON.toJson(resource)); - - // Lift the "message" field into the main "data" field. - if (data.has("message")) { - JsonElement message = data.get("message"); - if (message.isJsonObject()) { - data = message.getAsJsonObject(); - } - } - - data.remove("messageId"); - data.remove("publishTime"); - - return Event.of(data, builder.build()); - } - } - - private static class FirebaseAuthEventAdapter extends EventAdapter { - FirebaseAuthEventAdapter(String legacyEventType) { - super(legacyEventType); - } - - @Override - Event createLegacyEvent( - ParsedCloudEvent event, CloudFunctionsContext.Builder builder, JsonObject data) { - builder.setResource(event.Name); - - if (data.has("metadata")) { - JsonElement meta = data.get("metadata"); - if (meta.isJsonObject()) { - JsonObject metaObj = meta.getAsJsonObject(); - - JsonElement createTime = metaObj.get("createTime"); - if (createTime != null) { - metaObj.add("createdAt", createTime); - metaObj.remove("createTime"); - } - - JsonElement lastSignInTime = metaObj.get("lastSignInTime"); - if (lastSignInTime != null) { - metaObj.add("lastSignedInAt", lastSignInTime); - metaObj.remove("lastSignInTime"); - } - } - } - return Event.of(data, builder.build()); - } - } - - private static class FirebaseDatabaseEventAdapter extends EventAdapter { - private static Pattern resourcePattern = Pattern.compile("/locations/[^/]+"); - - FirebaseDatabaseEventAdapter(String legacyEventType) { - super(legacyEventType); - } - - @Override - Event createLegacyEvent( - ParsedCloudEvent event, CloudFunctionsContext.Builder builder, JsonObject data) { - builder.setResource(resourcePattern.matcher(event.Resource).replaceAll("")); - return Event.of(data, builder.build()); - } - } - - private static class StorageEventAdapter extends EventAdapter { - StorageEventAdapter(String legacyEventType) { - super(legacyEventType); - } - - @Override - Event createLegacyEvent( - ParsedCloudEvent event, CloudFunctionsContext.Builder builder, JsonObject data) { - JsonObject resource = new JsonObject(); - resource.addProperty("service", event.Service); - resource.addProperty("name", event.Resource); - if (data.has("kind")) { - resource.addProperty("type", data.get("kind").getAsString()); - } - - builder.setResource(GSON.toJson(resource)); - return Event.of(data, builder.build()); - } - } -} diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java deleted file mode 100644 index 65df5411..00000000 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker; - -import com.google.auto.value.AutoValue; -import com.google.cloud.functions.Context; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.TypeAdapter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Collections; -import java.util.Map; - -/** Event context (metadata) for events handled by Cloud Functions. */ -@AutoValue -abstract class CloudFunctionsContext implements Context { - // AutoValue recognizes any annotation called @Nullable, so no need to import this from anywhere. - @Retention(RetentionPolicy.SOURCE) - @interface Nullable {} - - @Override - @Nullable - public abstract String eventId(); - - @Override - @Nullable - public abstract String timestamp(); - - @Override - @Nullable - public abstract String eventType(); - - @Override - @Nullable - public abstract String resource(); - - // TODO: expose this in the Context interface (as a default method). - abstract Map params(); - - @Nullable - abstract String domain(); - - @Override - public abstract Map attributes(); - - public static TypeAdapter typeAdapter(Gson gson) { - return new AutoValue_CloudFunctionsContext.GsonTypeAdapter(gson); - } - - static Builder builder() { - return new AutoValue_CloudFunctionsContext.Builder() - .setParams(Collections.emptyMap()) - .setAttributes(Collections.emptyMap()); - } - - @AutoValue.Builder - abstract static class Builder { - abstract Builder setEventId(String x); - - abstract Builder setTimestamp(String x); - - abstract Builder setEventType(String x); - - abstract Builder setResource(String x); - - abstract Builder setParams(Map x); - - abstract Builder setAttributes(Map value); - - abstract Builder setDomain(String x); - - abstract CloudFunctionsContext build(); - } - - /** - * Depending on the event type, the {@link Context#resource()} field is either a JSON string - * (complete with encosing quotes) or a JSON object. This class allows us to redeserialize that - * JSON representation into its components. - */ - @AutoValue - abstract static class Resource { - abstract @Nullable String service(); - - abstract String name(); - - abstract @Nullable String type(); - - static TypeAdapter typeAdapter(Gson gson) { - return new AutoValue_CloudFunctionsContext_Resource.GsonTypeAdapter(gson); - } - - static Resource from(String s) { - if (s.startsWith("{") && (s.endsWith("}") || s.endsWith("}\n"))) { - TypeAdapter typeAdapter = typeAdapter(new Gson()); - Gson gson = new GsonBuilder().registerTypeAdapter(Resource.class, typeAdapter).create(); - return gson.fromJson(s, Resource.class); - } - return builder().setName(s).build(); - } - - static Builder builder() { - return new AutoValue_CloudFunctionsContext_Resource.Builder(); - } - - @AutoValue.Builder - abstract static class Builder { - abstract Builder setService(String x); - - abstract Builder setName(String x); - - abstract Builder setType(String x); - - abstract Resource build(); - } - } -} diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/Event.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/Event.java deleted file mode 100644 index 642e5118..00000000 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/Event.java +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker; - -import com.google.auto.value.AutoValue; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import java.lang.reflect.Type; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; - -/** - * Represents an event that should be handled by a background function. This is an internal format - * which is later converted to actual background function parameter types. - */ -@AutoValue -abstract class Event { - static Event of(JsonElement data, CloudFunctionsContext context) { - return new AutoValue_Event(data, context); - } - - abstract JsonElement getData(); - - abstract CloudFunctionsContext getContext(); - - /** Custom deserializer that supports both GCF beta and GCF GA event formats. */ - static class EventDeserializer implements JsonDeserializer { - - @Override - public Event deserialize( - JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) - throws JsonParseException { - JsonObject root = jsonElement.getAsJsonObject(); - - JsonElement data = root.get("data"); - CloudFunctionsContext context; - - if (root.has("context")) { - JsonObject contextCopy = root.getAsJsonObject("context").deepCopy(); - context = - jsonDeserializationContext.deserialize( - adjustContextResource(contextCopy), CloudFunctionsContext.class); - } else if (isPubSubEmulatorPayload(root)) { - JsonObject message = root.getAsJsonObject("message"); - - String timestampString = - message.has("publishTime") - ? message.get("publishTime").getAsString() - : DateTimeFormatter.ISO_INSTANT.format(OffsetDateTime.now()); - - context = - CloudFunctionsContext.builder() - .setEventType("google.pubsub.topic.publish") - .setTimestamp(timestampString) - .setEventId(message.get("messageId").getAsString()) - .setResource( - "{" - + "\"name\":null," - + "\"service\":\"pubsub.googleapis.com\"," - + "\"type\":\"type.googleapis.com/google.pubsub.v1.PubsubMessage\"" - + "}") - .build(); - - JsonObject marshalledData = new JsonObject(); - marshalledData.addProperty("@type", "type.googleapis.com/google.pubsub.v1.PubsubMessage"); - marshalledData.add("data", message.get("data")); - if (message.has("attributes")) { - marshalledData.add("attributes", message.get("attributes")); - } - data = marshalledData; - } else { - JsonObject rootCopy = root.deepCopy(); - rootCopy.remove("data"); - context = - jsonDeserializationContext.deserialize( - adjustContextResource(rootCopy), CloudFunctionsContext.class); - } - return Event.of(data, context); - } - - private boolean isPubSubEmulatorPayload(JsonObject root) { - if (root.has("subscription") && root.has("message") && root.get("message").isJsonObject()) { - JsonObject message = root.getAsJsonObject("message"); - return message.has("data") && message.has("messageId"); - } - return false; - } - - /** - * Replaces 'resource' member from context JSON with its string equivalent. The original - * 'resource' member can be a JSON object itself while {@link CloudFunctionsContext} requires it - * to be a string. - */ - private JsonObject adjustContextResource(JsonObject contextObject) { - if (contextObject.has("resource")) { - JsonElement resourceElement = contextObject.get("resource"); - if (resourceElement.isJsonObject()) { - contextObject.addProperty("resource", resourceElement.toString()); - } - } - return contextObject; - } - } -} diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/GcfEvents.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/GcfEvents.java deleted file mode 100644 index d78365dc..00000000 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/GcfEvents.java +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Map.entry; - -import com.google.auto.value.AutoValue; -import com.google.cloud.functions.invoker.CloudFunctionsContext.Nullable; -import com.google.cloud.functions.invoker.CloudFunctionsContext.Resource; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; -import io.cloudevents.CloudEvent; -import io.cloudevents.core.builder.CloudEventBuilder; -import java.net.URI; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Map; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** Conversions from GCF events to CloudEvents. */ -class GcfEvents { - private static final String FIREBASE_SERVICE = "firebase.googleapis.com"; - private static final String FIREBASE_AUTH_SERVICE = "firebaseauth.googleapis.com"; - private static final String FIREBASE_DB_SERVICE = "firebasedatabase.googleapis.com"; - private static final String FIRESTORE_SERVICE = "firestore.googleapis.com"; - private static final String PUB_SUB_SERVICE = "pubsub.googleapis.com"; - private static final String STORAGE_SERVICE = "storage.googleapis.com"; - - private static final String PUB_SUB_MESSAGE_PUBLISHED = - "google.cloud.pubsub.topic.v1.messagePublished"; - - private static final Map EVENT_TYPE_MAPPING = - Map.ofEntries( - entry("google.pubsub.topic.publish", new PubSubEventAdapter(PUB_SUB_MESSAGE_PUBLISHED)), - entry( - "google.storage.object.finalize", - new StorageEventAdapter("google.cloud.storage.object.v1.finalized")), - entry( - "google.storage.object.delete", - new StorageEventAdapter("google.cloud.storage.object.v1.deleted")), - entry( - "google.storage.object.archive", - new StorageEventAdapter("google.cloud.storage.object.v1.archived")), - entry( - "google.storage.object.metadataUpdate", - new StorageEventAdapter("google.cloud.storage.object.v1.metadataUpdated")), - entry( - "providers/cloud.firestore/eventTypes/document.write", - new FirestoreFirebaseEventAdapter( - "google.cloud.firestore.document.v1.written", FIRESTORE_SERVICE)), - entry( - "providers/cloud.firestore/eventTypes/document.create", - new FirestoreFirebaseEventAdapter( - "google.cloud.firestore.document.v1.created", FIRESTORE_SERVICE)), - entry( - "providers/cloud.firestore/eventTypes/document.update", - new FirestoreFirebaseEventAdapter( - "google.cloud.firestore.document.v1.updated", FIRESTORE_SERVICE)), - entry( - "providers/cloud.firestore/eventTypes/document.delete", - new FirestoreFirebaseEventAdapter( - "google.cloud.firestore.document.v1.deleted", FIRESTORE_SERVICE)), - entry( - "providers/firebase.auth/eventTypes/user.create", - new FirebaseAuthEventAdapter("google.firebase.auth.user.v1.created")), - entry( - "providers/firebase.auth/eventTypes/user.delete", - new FirebaseAuthEventAdapter("google.firebase.auth.user.v1.deleted")), - entry( - "providers/google.firebase.analytics/eventTypes/event.log", - new FirestoreFirebaseEventAdapter( - "google.firebase.analytics.log.v1.written", FIREBASE_SERVICE)), - entry( - "providers/google.firebase.database/eventTypes/ref.create", - new FirebaseDatabaseEventAdapter("google.firebase.database.ref.v1.created")), - entry( - "providers/google.firebase.database/eventTypes/ref.write", - new FirebaseDatabaseEventAdapter("google.firebase.database.ref.v1.written")), - entry( - "providers/google.firebase.database/eventTypes/ref.update", - new FirebaseDatabaseEventAdapter("google.firebase.database.ref.v1.updated")), - entry( - "providers/google.firebase.database/eventTypes/ref.delete", - new FirebaseDatabaseEventAdapter("google.firebase.database.ref.v1.deleted")), - entry( - "providers/cloud.pubsub/eventTypes/topic.publish", - new PubSubEventAdapter(PUB_SUB_MESSAGE_PUBLISHED)), - entry( - "providers/cloud.storage/eventTypes/object.change", - new StorageEventAdapter("google.cloud.storage.object.v1.changed"))); - - private static final Gson GSON = new GsonBuilder().serializeNulls().create(); - - static CloudEvent convertToCloudEvent(Event legacyEvent) { - String eventType = legacyEvent.getContext().eventType(); - EventAdapter eventAdapter = EVENT_TYPE_MAPPING.get(eventType); - if (eventAdapter == null) { - throw new IllegalArgumentException("Unrecognized event type \"" + eventType + "\""); - } - return eventAdapter.convertToCloudEvent(legacyEvent); - } - - @AutoValue - abstract static class SourceAndSubject { - /** The source URI, without the initial {@code ///}. */ - abstract String source(); - - abstract @Nullable String subject(); - - static SourceAndSubject of(String source, String subject) { - return new AutoValue_GcfEvents_SourceAndSubject(source, subject); - } - } - - private abstract static class EventAdapter { - private final String cloudEventType; - private final String defaultService; - - EventAdapter(String cloudEventType, String defaultService) { - this.cloudEventType = cloudEventType; - this.defaultService = defaultService; - } - - final CloudEvent convertToCloudEvent(Event legacyEvent) { - String jsonData = GSON.toJson(legacyEvent.getData()); - jsonData = maybeReshapeData(legacyEvent, jsonData); - Resource resource = Resource.from(legacyEvent.getContext().resource()); - String service = Optional.ofNullable(resource.service()).orElse(defaultService); - String resourceName = resource.name(); - SourceAndSubject sourceAndSubject = - convertResourceToSourceAndSubject(resourceName, legacyEvent); - URI source = URI.create("//" + service + "/" + sourceAndSubject.source()); - OffsetDateTime timestamp = - Optional.ofNullable(legacyEvent.getContext().timestamp()) - .map(s -> OffsetDateTime.parse(s, DateTimeFormatter.ISO_DATE_TIME)) - .orElse(null); - return CloudEventBuilder.v1() - .withData(jsonData.getBytes(UTF_8)) - .withDataContentType("application/json") - .withId(legacyEvent.getContext().eventId()) - .withSource(source) - .withSubject(sourceAndSubject.subject()) - .withTime(timestamp) - .withType(cloudEventType) - .build(); - } - - String maybeReshapeData(Event legacyEvent, String jsonData) { - return jsonData; - } - - SourceAndSubject convertResourceToSourceAndSubject(String resourceName, Event legacyEvent) { - return SourceAndSubject.of(resourceName, null); - } - } - - private static class PubSubEventAdapter extends EventAdapter { - PubSubEventAdapter(String cloudEventType) { - super(cloudEventType, PUB_SUB_SERVICE); - } - - @Override - String maybeReshapeData(Event legacyEvent, String jsonData) { - JsonObject jsonObject = GSON.fromJson(jsonData, JsonObject.class); - jsonObject.addProperty("messageId", legacyEvent.getContext().eventId()); - jsonObject.addProperty("publishTime", legacyEvent.getContext().timestamp()); - JsonObject wrapped = new JsonObject(); - wrapped.add("message", jsonObject); - return GSON.toJson(wrapped); - } - } - - private static class StorageEventAdapter extends EventAdapter { - private static final Pattern STORAGE_RESOURCE_PATTERN = - Pattern.compile("^(projects/_/buckets/[^/]+)/(objects/.*?)(?:#\\d+)?$"); - - StorageEventAdapter(String cloudEventType) { - super(cloudEventType, STORAGE_SERVICE); - } - - @Override - SourceAndSubject convertResourceToSourceAndSubject(String resourceName, Event legacyEvent) { - Matcher matcher = STORAGE_RESOURCE_PATTERN.matcher(resourceName); - if (matcher.matches()) { - String resource = matcher.group(1); - String subject = matcher.group(2); - return SourceAndSubject.of(resource, subject); - } - return super.convertResourceToSourceAndSubject(resourceName, legacyEvent); - } - } - - private static class FirestoreFirebaseEventAdapter extends EventAdapter { - private static final Pattern FIRESTORE_RESOURCE_PATTERN = - Pattern.compile("^(projects/.+)/((documents|refs)/.+)$"); - - FirestoreFirebaseEventAdapter(String cloudEventType, String defaultService) { - super(cloudEventType, defaultService); - } - - @Override - SourceAndSubject convertResourceToSourceAndSubject(String resourceName, Event legacyEvent) { - Matcher matcher = FIRESTORE_RESOURCE_PATTERN.matcher(resourceName); - if (matcher.matches()) { - String resource = matcher.group(1); - String subject = matcher.group(2); - return SourceAndSubject.of(resource, subject); - } - return super.convertResourceToSourceAndSubject(resourceName, legacyEvent); - } - - @Override - String maybeReshapeData(Event legacyEvent, String jsonData) { - // The reshaping code is disabled for now, because the specification for how the legacy - // "params" - // field should be represented in a CloudEvent is in flux. - if (true || legacyEvent.getContext().params().isEmpty()) { - return jsonData; - } - JsonObject jsonObject = GSON.fromJson(jsonData, JsonObject.class); - JsonObject wildcards = new JsonObject(); - legacyEvent.getContext().params().forEach((k, v) -> wildcards.addProperty(k, v)); - jsonObject.add("wildcards", wildcards); - return GSON.toJson(jsonObject); - } - } - - private static class FirebaseDatabaseEventAdapter extends EventAdapter { - private static final Pattern FIREBASE_DB_RESOURCE_PATTERN = - Pattern.compile("^projects/_/(instances/[^/]+)/((documents|refs)/.+)$"); - - FirebaseDatabaseEventAdapter(String cloudEventType) { - super(cloudEventType, FIREBASE_DB_SERVICE); - } - - @Override - SourceAndSubject convertResourceToSourceAndSubject(String resourceName, Event legacyEvent) { - Matcher matcher = FIREBASE_DB_RESOURCE_PATTERN.matcher(resourceName); - String location = parseLocation(legacyEvent); - if (matcher.matches() && location != null) { - String resource = String.format("projects/_/locations/%s/%s", location, matcher.group(1)); - String subject = matcher.group(2); - return SourceAndSubject.of(resource, subject); - } - return super.convertResourceToSourceAndSubject(resourceName, legacyEvent); - } - - private String parseLocation(Event legacyEvent) { - String domain = legacyEvent.getContext().domain(); - if (domain == null) { - return null; - } - // The default location for firebaseio.com is us-central1 - if ("firebaseio.com".equals(domain)) { - return "us-central1"; - } - // Otherwise the location can be inferred from the first subdomain - String[] subdomains = domain.split("\\."); - if (subdomains.length > 1) { - return subdomains[0]; - } - return null; - } - } - - private static class FirebaseAuthEventAdapter extends EventAdapter { - FirebaseAuthEventAdapter(String cloudEventType) { - super(cloudEventType, FIREBASE_AUTH_SERVICE); - } - - @Override - SourceAndSubject convertResourceToSourceAndSubject(String resourceName, Event legacyEvent) { - String subject = null; - JsonObject data = legacyEvent.getData().getAsJsonObject(); - if (data.has("uid")) { - subject = "users/" + data.get("uid").getAsString(); - } - return SourceAndSubject.of(resourceName, subject); - } - - @Override - String maybeReshapeData(Event legacyEvent, String jsonData) { - JsonObject jsonObject = GSON.fromJson(jsonData, JsonObject.class); - if (!jsonObject.has("metadata")) { - return jsonData; - } - JsonObject metadata = jsonObject.getAsJsonObject("metadata"); - if (metadata.has("createdAt")) { - metadata.add("createTime", metadata.get("createdAt")); - metadata.remove("createdAt"); - } - if (metadata.has("lastSignedInAt")) { - metadata.add("lastSignInTime", metadata.get("lastSignedInAt")); - metadata.remove("lastSignedInAt"); - } - return GSON.toJson(jsonObject); - } - } -} diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java deleted file mode 100644 index 7a66fefd..00000000 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.invoker.http.HttpRequestImpl; -import com.google.cloud.functions.invoker.http.HttpResponseImpl; -import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** Executes the user's method. */ -public class HttpFunctionExecutor extends HttpServlet { - private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker"); - - private final HttpFunction function; - - private HttpFunctionExecutor(HttpFunction function) { - this.function = function; - } - - /** - * Makes a {@link HttpFunctionExecutor} for the given class. - * - * @throws RuntimeException if either the given class does not implement {@link HttpFunction} or - * we are unable to construct an instance using its no-arg constructor. - */ - public static HttpFunctionExecutor forClass(Class functionClass) { - if (!HttpFunction.class.isAssignableFrom(functionClass)) { - throw new RuntimeException( - "Class " - + functionClass.getName() - + " does not implement " - + HttpFunction.class.getName()); - } - Class httpFunctionClass = functionClass.asSubclass(HttpFunction.class); - try { - HttpFunction httpFunction = httpFunctionClass.getConstructor().newInstance(); - return new HttpFunctionExecutor(httpFunction); - } catch (ReflectiveOperationException e) { - throw new RuntimeException( - "Could not construct an instance of " + functionClass.getName() + ": " + e, e); - } - } - - /** Executes the user's method, can handle all HTTP type methods. */ - @Override - public void service(HttpServletRequest req, HttpServletResponse res) { - HttpRequestImpl reqImpl = new HttpRequestImpl(req); - HttpResponseImpl respImpl = new HttpResponseImpl(res); - ClassLoader oldContextLoader = Thread.currentThread().getContextClassLoader(); - try { - Thread.currentThread().setContextClassLoader(function.getClass().getClassLoader()); - function.service(reqImpl, respImpl); - } catch (Throwable t) { - logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t); - res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } finally { - Thread.currentThread().setContextClassLoader(oldContextLoader); - try { - // We can't use HttpServletResponse.flushBuffer() because we wrap the PrintWriter - // returned by HttpServletResponse in our own BufferedWriter to match our API. - // So we have to flush whichever of getWriter() or getOutputStream() works. - try { - respImpl.getOutputStream().flush(); - } catch (IllegalStateException e) { - respImpl.getWriter().flush(); - } - } catch (IOException e) { - // Too bad, can't flush. - } - } - } -} diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/JsonLogHandler.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/JsonLogHandler.java deleted file mode 100644 index 9c94b92a..00000000 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/JsonLogHandler.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.google.cloud.functions.invoker.gcf; - -import java.io.PrintStream; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogRecord; - -/** - * A log handler that publishes log messages in a json format. This is StackDriver's "structured logging" format. - */ -public final class JsonLogHandler extends Handler { - private static final String SOURCE_LOCATION_KEY = "\"logging.googleapis.com/sourceLocation\": "; - - private static final String DEBUG = "DEBUG"; - private static final String INFO = "INFO"; - private static final String WARNING = "WARNING"; - private static final String ERROR = "ERROR"; - private static final String DEFAULT = "DEFAULT"; - - private final PrintStream out; - private final boolean closePrintStreamOnClose; - - public JsonLogHandler(PrintStream out, boolean closePrintStreamOnClose) { - this.out = out; - this.closePrintStreamOnClose = closePrintStreamOnClose; - } - - @Override - public void publish(LogRecord record) { - // We avoid String.format and String.join even though they would simplify the code. - // Logging code often shows up in profiling so we want to make this fast and StringBuilder is - // more performant. - StringBuilder json = new StringBuilder("{"); - appendSeverity(json, record); - appendSourceLocation(json, record); - appendMessage(json, record); // must be last, see appendMessage - json.append("}"); - // We must output the log all at once (should only call println once per call to publish) - out.println(json); - } - - private static void appendMessage(StringBuilder json, LogRecord record) { - // This must be the last item in the JSON object, because it has no trailing comma. JSON is - // unforgiving about commas and you can't have one just before }. - json.append("\"message\": \"").append(escapeString(record.getMessage())); - if (record.getThrown() != null) { - json.append("\\n").append(escapeString(getStackTraceAsString(record.getThrown()))); - } - json.append("\""); - } - - private static void appendSeverity(StringBuilder json, LogRecord record) { - json.append("\"severity\": \"").append(levelToSeverity(record.getLevel())).append("\", "); - } - - private static String levelToSeverity(Level level) { - int intLevel = (level == null) ? 0 : level.intValue(); - switch (intLevel) { - case 300: // FINEST - case 400: // FINER - case 500: // FINE - return DEBUG; - case 700: // CONFIG - case 800: // INFO - // Java's CONFIG is lower than its INFO, while Stackdriver's NOTICE is greater than its - // INFO. So despite the similarity, we don't try to use NOTICE for CONFIG. - return INFO; - case 900: // WARNING - return WARNING; - case 1000: // SEVERE - return ERROR; - default: - return DEFAULT; - } - } - - private static void appendSourceLocation(StringBuilder json, LogRecord record) { - if (record.getSourceClassName() == null && record.getSourceMethodName() == null) { - return; - } - List entries = new ArrayList<>(); - if (record.getSourceClassName() != null) { - // TODO: Handle nested classes. If the source class name is com.example.Foo$Bar then the - // source file is com/example/Foo.java, not com/example/Foo$Bar.java. - String fileName = record.getSourceClassName().replace('.', '/') + ".java"; - entries.add("\"file\": \"" + escapeString(fileName) + "\""); - } - if (record.getSourceMethodName() != null) { - entries.add("\"method\": \"" + escapeString(record.getSourceMethodName()) + "\""); - } - json.append(SOURCE_LOCATION_KEY).append("{").append(String.join(", ", entries)).append("}, "); - } - - private static String escapeString(String s) { - return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); - } - - private static String getStackTraceAsString(Throwable t) { - StringWriter stringWriter = new StringWriter(); - t.printStackTrace(new PrintWriter(stringWriter)); - return stringWriter.toString(); - } - - @Override - public void flush() { - out.flush(); - } - - @Override - public void close() throws SecurityException { - if (closePrintStreamOnClose) { - out.close(); - } - } -} diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java deleted file mode 100644 index 41a96e36..00000000 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker.http; - -import static java.util.stream.Collectors.toMap; - -import com.google.cloud.functions.HttpResponse; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.util.AbstractMap.SimpleEntry; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import javax.servlet.http.HttpServletResponse; - -public class HttpResponseImpl implements HttpResponse { - private final HttpServletResponse response; - - public HttpResponseImpl(HttpServletResponse response) { - this.response = response; - } - - @Override - public void setStatusCode(int code) { - response.setStatus(code); - } - - @Override - @SuppressWarnings("deprecation") - public void setStatusCode(int code, String message) { - response.setStatus(code, message); - } - - @Override - public void setContentType(String contentType) { - response.setContentType(contentType); - } - - @Override - public Optional getContentType() { - return Optional.ofNullable(response.getContentType()); - } - - @Override - public void appendHeader(String key, String value) { - response.addHeader(key, value); - } - - @Override - public Map> getHeaders() { - return response.getHeaderNames().stream() - .map(header -> new SimpleEntry<>(header, list(response.getHeaders(header)))) - .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - private static List list(Collection collection) { - return (collection instanceof List) ? (List) collection : new ArrayList<>(collection); - } - - @Override - public OutputStream getOutputStream() throws IOException { - return response.getOutputStream(); - } - - private BufferedWriter writer; - - @Override - public synchronized BufferedWriter getWriter() throws IOException { - if (writer == null) { - // Unfortunately this means that we get two intermediate objects between the object we return - // and the underlying Writer that response.getWriter() wraps. We could try accessing the - // PrintWriter.out field via reflection, but that sort of access to non-public fields of - // platform classes is now frowned on and may draw warnings or even fail in subsequent - // versions. - // We could instead wrap the OutputStream, but that would require us to deduce the appropriate - // Charset, using logic like this: - // https://github.com/eclipse/jetty.project/blob/923ec38adf/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java#L731 - // We may end up doing that if performance is an issue. - writer = new BufferedWriter(response.getWriter()); - } - return writer; - } -} diff --git a/invoker/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java b/invoker/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java deleted file mode 100644 index b03b8c63..00000000 --- a/invoker/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java +++ /dev/null @@ -1,513 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker.runner; - -import static java.util.stream.Collectors.toList; - -import com.beust.jcommander.JCommander; -import com.beust.jcommander.Parameter; -import com.beust.jcommander.ParameterException; -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.invoker.BackgroundFunctionExecutor; -import com.google.cloud.functions.invoker.HttpFunctionExecutor; -import com.google.cloud.functions.invoker.gcf.JsonLogHandler; -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Stream; -import javax.servlet.MultipartConfigElement; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; - -/** - * Java server that runs the user's code (a jar file) on HTTP request and an HTTP response is sent - * once the user's function is completed. The server accepts HTTP requests at '/' for executing the - * user's function, handles all HTTP methods. - * - *

This class requires the following environment variables: - * - *

    - *
  • PORT - defines the port on which this server listens to HTTP requests. - *
  • FUNCTION_TARGET - defines the name of the class defining the function. - *
  • FUNCTION_SIGNATURE_TYPE - determines whether the loaded code defines an HTTP or event - * function. - *
- */ -public class Invoker { - private static final Logger rootLogger = Logger.getLogger(""); - private static final Logger logger = Logger.getLogger(Invoker.class.getName()); - - static { - if (isGcf()) { - // If we're running with Google Cloud Functions, we'll get better-looking logs if we arrange - // for them to be formatted using StackDriver's "structured logging" JSON format. Remove the - // JDK's standard logger and replace it with the JSON one. - for (Handler handler : rootLogger.getHandlers()) { - rootLogger.removeHandler(handler); - } - rootLogger.addHandler(new JsonLogHandler(System.out, false)); - } - } - - private static class Options { - @Parameter(description = "Port on which to listen for HTTP requests.", names = "--port") - private String port = System.getenv().getOrDefault("PORT", "8080"); - - @Parameter( - description = "Name of function class to execute when servicing incoming requests.", - names = "--target") - private String target = System.getenv().getOrDefault("FUNCTION_TARGET", "Function"); - - @Parameter( - description = - "List of files or directories where the compiled Java classes making up the function" - + " will be found. This functions like the -classpath option to the java command." - + " It is a list of filenames separated by '${path.separator}'. If an entry in the" - + " list names a directory then the class foo.bar.Baz will be looked for in" - + " foo${file.separator}bar${file.separator}Baz.class under that directory. If an" - + " entry in the list names a file and that file is a jar file then class" - + " foo.bar.Baz will be looked for in an entry foo/bar/Baz.class in that jar file." - + " If an entry is a directory followed by '${file.separator}*' then every file in" - + " the directory whose name ends with '.jar' will be searched for classes.", - names = "--classpath") - private String classPath = null; - - @Parameter(names = "--help", help = true) - private boolean help = false; - } - - public static void main(String[] args) throws Exception { - Optional invoker = makeInvoker(args); - if (invoker.isPresent()) { - invoker.get().startServer(); - } - } - - static Optional makeInvoker(String... args) { - return makeInvoker(System.getenv(), args); - } - - static Optional makeInvoker(Map environment, String... args) { - Options options = new Options(); - JCommander jCommander = JCommander.newBuilder().addObject(options).build(); - try { - jCommander.parse(args); - } catch (ParameterException e) { - usage(jCommander); - throw e; - } - - if (options.help) { - usage(jCommander); - return Optional.empty(); - } - - int port; - try { - port = Integer.parseInt(options.port); - } catch (NumberFormatException e) { - System.err.println("--port value should be an integer: " + options.port); - usage(jCommander); - throw e; - } - String functionTarget = options.target; - Path standardFunctionJarPath = Paths.get("function/function.jar"); - Optional functionClasspath = - Arrays.asList( - options.classPath, - environment.get("FUNCTION_CLASSPATH"), - Files.exists(standardFunctionJarPath) ? standardFunctionJarPath.toString() : null) - .stream() - .filter(Objects::nonNull) - .findFirst(); - ClassLoader functionClassLoader = makeClassLoader(functionClasspath); - Invoker invoker = - new Invoker( - port, functionTarget, environment.get("FUNCTION_SIGNATURE_TYPE"), functionClassLoader); - return Optional.of(invoker); - } - - private static void usage(JCommander jCommander) { - StringBuilder usageBuilder = new StringBuilder(); - jCommander.getUsageFormatter().usage(usageBuilder); - String usage = - usageBuilder - .toString() - .replace("${file.separator}", File.separator) - .replace("${path.separator}", File.pathSeparator); - jCommander.getConsole().println(usage); - } - - private static ClassLoader makeClassLoader(Optional functionClasspath) { - ClassLoader runtimeLoader = Invoker.class.getClassLoader(); - if (functionClasspath.isPresent()) { - ClassLoader parent = new OnlyApiClassLoader(runtimeLoader); - return new FunctionClassLoader(classpathToUrls(functionClasspath.get()), parent); - } - return runtimeLoader; - } - - // This is a subclass just so we can identify it from its toString(). - private static class FunctionClassLoader extends URLClassLoader { - FunctionClassLoader(URL[] urls, ClassLoader parent) { - super(urls, parent); - } - } - - private final Integer port; - private final String functionTarget; - private final String functionSignatureType; - private final ClassLoader functionClassLoader; - - private Server server; - - public Invoker( - Integer port, - String functionTarget, - String functionSignatureType, - ClassLoader functionClassLoader) { - this.port = port; - this.functionTarget = functionTarget; - this.functionSignatureType = functionSignatureType; - this.functionClassLoader = functionClassLoader; - } - - Integer getPort() { - return port; - } - - String getFunctionTarget() { - return functionTarget; - } - - String getFunctionSignatureType() { - return functionSignatureType; - } - - ClassLoader getFunctionClassLoader() { - return functionClassLoader; - } - - /** - * This will start the server and wait (join) for function calls. To start the server inside a - * unit or integration test, use {@link #startTestServer()} instead. - * - * @see #stopServer() - * @throws Exception - */ - public void startServer() throws Exception { - startServer(true); - } - - /** - * This will start the server and return. - * - *

This method is designed to be used for unit or integration testing only. For other use cases - * use {@link #startServer()}. - * - *

Inside a test a typical usage will be: - * - *

{@code
-   * // Create an invoker
-   * Invoker invoker = new Invoker(
-   *         8081,
-   *         "org.example.MyHttpFunction",
-   *         "http",
-   *         Thread.currentThread().getContextClassLoader()
-   * );
-   *
-   * // Start the test server
-   * invoker.startTestServer();
-   *
-   * // Test the function
-   *
-   * // Stop the test server
-   * invoker.stopServer();
-   * }
- * - * @see #stopServer() - * @throws Exception - */ - public void startTestServer() throws Exception { - startServer(false); - } - - private void startServer(boolean join) throws Exception { - if (server != null) { - throw new IllegalStateException("Server already started"); - } - - server = new Server(port); - - ServletContextHandler servletContextHandler = new ServletContextHandler(); - servletContextHandler.setContextPath("/"); - server.setHandler(NotFoundHandler.forServlet(servletContextHandler)); - - Class functionClass = loadFunctionClass(); - - HttpServlet servlet; - if (functionSignatureType == null) { - servlet = servletForDeducedSignatureType(functionClass); - } else { - switch (functionSignatureType) { - case "http": - servlet = HttpFunctionExecutor.forClass(functionClass); - break; - case "event": - case "cloudevent": - servlet = BackgroundFunctionExecutor.forClass(functionClass); - break; - default: - String error = - String.format( - "Function signature type %s is unknown; should be \"http\", \"event\"," - + " or \"cloudevent\"", - functionSignatureType); - throw new RuntimeException(error); - } - } - ServletHolder servletHolder = new ServletHolder(servlet); - servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement("")); - servletContextHandler.addServlet(servletHolder, "/*"); - - server.start(); - logServerInfo(); - if (join) { - server.join(); - } - } - - /** - * Stop the server. - * - * @see #startServer() - * @see #startTestServer() - * @throws Exception - */ - public void stopServer() throws Exception { - if (server == null) { - throw new IllegalStateException("Server not yet started"); - } - - server.stop(); - // setting the server to null, so it can be started again - server = null; - } - - private Class loadFunctionClass() throws ClassNotFoundException { - String target = functionTarget; - ClassNotFoundException firstException = null; - while (true) { - try { - return functionClassLoader.loadClass(target); - } catch (ClassNotFoundException e) { - if (firstException == null) { - firstException = e; - } - // This might be a nested class like com.example.Foo.Bar. That will actually appear as - // com.example.Foo$Bar as far as Class.forName is concerned. So we try to replace every dot - // from the last to the first with a $ in the hope of finding a class we can load. - int lastDot = target.lastIndexOf('.'); - if (lastDot < 0) { - throw firstException; - } - target = target.substring(0, lastDot) + '$' + target.substring(lastDot + 1); - } - } - } - - private HttpServlet servletForDeducedSignatureType(Class functionClass) { - if (HttpFunction.class.isAssignableFrom(functionClass)) { - return HttpFunctionExecutor.forClass(functionClass); - } - Optional maybeExecutor = - BackgroundFunctionExecutor.maybeForClass(functionClass); - if (maybeExecutor.isPresent()) { - return maybeExecutor.get(); - } - String error = - String.format( - "Could not determine function signature type from target %s. Either this should be a" - + " class implementing one of the interfaces in com.google.cloud.functions, or the" - + " environment variable FUNCTION_SIGNATURE_TYPE should be set to \"http\" or" - + " \"event\".", - functionTarget); - throw new RuntimeException(error); - } - - static URL[] classpathToUrls(String classpath) { - String[] components = classpath.split(File.pathSeparator); - List urls = new ArrayList<>(); - for (String component : components) { - if (component.endsWith(File.separator + "*")) { - urls.addAll(jarsIn(component.substring(0, component.length() - 2))); - } else { - Path path = Paths.get(component); - try { - urls.add(path.toUri().toURL()); - } catch (MalformedURLException e) { - throw new UncheckedIOException(e); - } - } - } - return urls.toArray(new URL[0]); - } - - private static List jarsIn(String dir) { - Path path = Paths.get(dir); - if (!Files.isDirectory(path)) { - return Collections.emptyList(); - } - Stream stream; - try { - stream = Files.list(path); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return stream - .filter(p -> p.getFileName().toString().endsWith(".jar")) - .map( - p -> { - try { - return p.toUri().toURL(); - } catch (MalformedURLException e) { - throw new UncheckedIOException(e); - } - }) - .collect(toList()); - } - - private void logServerInfo() { - if (!isGcf()) { - logger.log(Level.INFO, "Serving function..."); - logger.log(Level.INFO, "Function: {0}", functionTarget); - logger.log(Level.INFO, "URL: http://localhost:{0,number,#}/", port); - } - } - - private static boolean isGcf() { - // This environment variable is set in the GCF environment but won't be set when invoking - // the Functions Framework directly. We don't use its value, just whether it is set. - return System.getenv("K_SERVICE") != null; - } - - /** - * Wrapper that intercepts requests for {@code /favicon.ico} and {@code /robots.txt} and causes - * them to produce a 404 status. Otherwise they would be sent to the function code, like any other - * URL, meaning that someone testing their function by using a browser as an HTTP client can see - * two requests, one for {@code /favicon.ico} and one for {@code /} (or whatever). - */ - private static class NotFoundHandler extends HandlerWrapper { - static NotFoundHandler forServlet(ServletContextHandler servletHandler) { - NotFoundHandler handler = new NotFoundHandler(); - handler.setHandler(servletHandler); - return handler; - } - - private static final Set NOT_FOUND_PATHS = - new HashSet<>(Arrays.asList("/favicon.ico", "/robots.txt")); - - @Override - public void handle( - String target, - Request baseRequest, - HttpServletRequest request, - HttpServletResponse response) - throws IOException, ServletException { - if (NOT_FOUND_PATHS.contains(request.getRequestURI())) { - response.sendError(HttpStatus.NOT_FOUND_404, "Not Found"); - } - super.handle(target, baseRequest, request, response); - } - } - - /** - * A loader that only loads GCF API classes. Those are classes whose package is exactly {@code - * com.google.cloud.functions}. The package can't be a subpackage, such as {@code - * com.google.cloud.functions.invoker}. - * - *

This loader allows us to load the classes from a user function, without making the runtime - * classes visible to them. We will make this loader the parent of the {@link URLClassLoader} that - * loads the user code in order to filter out those runtime classes. - * - *

The reason we do need to share the API classes between the runtime and the user function is - * so that the runtime can instantiate the function class and cast it to {@link - * com.google.cloud.functions.HttpFunction} or whatever. - */ - private static class OnlyApiClassLoader extends ClassLoader { - private final ClassLoader runtimeClassLoader; - - OnlyApiClassLoader(ClassLoader runtimeClassLoader) { - super(getSystemOrBootstrapClassLoader()); - this.runtimeClassLoader = runtimeClassLoader; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - String prefix = "com.google.cloud.functions."; - if ((name.startsWith(prefix) && Character.isUpperCase(name.charAt(prefix.length()))) - || name.startsWith("javax.servlet.") - || isCloudEventsApiClass(name)) { - return runtimeClassLoader.loadClass(name); - } - return super.findClass(name); // should throw ClassNotFoundException - } - - private static final String CLOUD_EVENTS_API_PREFIX = "io.cloudevents."; - private static final int CLOUD_EVENTS_API_PREFIX_LENGTH = CLOUD_EVENTS_API_PREFIX.length(); - - private static boolean isCloudEventsApiClass(String name) { - return name.startsWith(CLOUD_EVENTS_API_PREFIX) - && Character.isUpperCase(name.charAt(CLOUD_EVENTS_API_PREFIX_LENGTH)); - } - - private static ClassLoader getSystemOrBootstrapClassLoader() { - try { - // We're still building against the Java 8 API, so we have to use reflection for now. - Method getPlatformClassLoader = ClassLoader.class.getMethod("getPlatformClassLoader"); - return (ClassLoader) getPlatformClassLoader.invoke(null); - } catch (ReflectiveOperationException e) { - return null; - } - } - } -} diff --git a/invoker/core/src/test/java/PackagelessHelloWorld.java b/invoker/core/src/test/java/PackagelessHelloWorld.java deleted file mode 100644 index e590fef1..00000000 --- a/invoker/core/src/test/java/PackagelessHelloWorld.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// A function in the default package. - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpResponse; - -public class PackagelessHelloWorld implements HttpFunction { - @Override - public void service(HttpRequest request, HttpResponse response) throws Exception { - response.setContentType("text/plain; charset=utf-8"); - response.getWriter().write("hello, world\n"); - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutorTest.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutorTest.java deleted file mode 100644 index d00b0b4f..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutorTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.google.cloud.functions.invoker; - -import static com.google.cloud.functions.invoker.BackgroundFunctionExecutor.backgroundFunctionTypeArgument; -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; - -import com.google.cloud.functions.BackgroundFunction; -import com.google.cloud.functions.Context; -import com.google.gson.JsonObject; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class BackgroundFunctionExecutorTest { - private static class PubSubMessage { - String data; - Map attributes; - String messageId; - String publishTime; - } - - private static class PubSubFunction implements BackgroundFunction { - @Override - public void accept(PubSubMessage payload, Context context) {} - } - - @Test - public void backgroundFunctionTypeArgument_simple() { - assertThat(backgroundFunctionTypeArgument(PubSubFunction.class)).hasValue(PubSubMessage.class); - } - - private abstract static class Parent implements BackgroundFunction {} - - private static class Child extends Parent { - @Override - public void accept(PubSubMessage payload, Context context) {} - } - - @Test - public void backgroundFunctionTypeArgument_superclass() { - assertThat(backgroundFunctionTypeArgument(Child.class)).hasValue(PubSubMessage.class); - } - - private interface GenericParent extends BackgroundFunction {} - - private static class GenericChild implements GenericParent { - @Override - public void accept(PubSubMessage payload, Context context) {} - } - - @Test - public void backgroundFunctionTypeArgument_genericInterface() { - assertThat(backgroundFunctionTypeArgument(GenericChild.class)).hasValue(PubSubMessage.class); - } - - @SuppressWarnings("rawtypes") - private static class ForgotTypeParameter implements BackgroundFunction { - @Override - public void accept(Object payload, Context context) {} - } - - @Test - public void backgroundFunctionTypeArgument_raw() { - @SuppressWarnings("unchecked") - Class> c = - (Class>) (Class) ForgotTypeParameter.class; - assertThat(backgroundFunctionTypeArgument(c)).isEmpty(); - } - - @Test - public void parseLegacyEventPubSub() throws IOException { - try (Reader reader = - new InputStreamReader(getClass().getResourceAsStream("/pubsub_background.json"))) { - Event event = BackgroundFunctionExecutor.parseLegacyEvent(reader); - - Context context = event.getContext(); - assertThat(context.eventType()).isEqualTo("google.pubsub.topic.publish"); - assertThat(context.eventId()).isEqualTo("1"); - assertThat(context.timestamp()).isEqualTo("2021-06-28T05:46:32.390Z"); - - JsonObject data = event.getData().getAsJsonObject(); - assertThat(data.get("data").getAsString()).isEqualTo("eyJmb28iOiJiYXIifQ=="); - String attr = data.get("attributes").getAsJsonObject().get("test").getAsString(); - assertThat(attr).isEqualTo("123"); - } - } - - @Test - public void parseLegacyEventPubSubEmulator() throws IOException { - try (Reader reader = - new InputStreamReader(getClass().getResourceAsStream("/pubsub_emulator.json"))) { - Event event = BackgroundFunctionExecutor.parseLegacyEvent(reader); - - Context context = event.getContext(); - assertThat(context.eventType()).isEqualTo("google.pubsub.topic.publish"); - assertThat(context.eventId()).isEqualTo("1"); - assertThat(context.timestamp()).isNotNull(); - ; - - JsonObject data = event.getData().getAsJsonObject(); - assertThat(data.get("data").getAsString()).isEqualTo("eyJmb28iOiJiYXIifQ=="); - String attr = data.get("attributes").getAsJsonObject().get("test").getAsString(); - assertThat(attr).isEqualTo("123"); - } - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/CloudEventsTest.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/CloudEventsTest.java deleted file mode 100644 index 3e964d7b..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/CloudEventsTest.java +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; -import static java.nio.charset.StandardCharsets.UTF_8; - -import io.cloudevents.CloudEvent; -import io.cloudevents.jackson.JsonFormat; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import org.junit.Test; - -public class CloudEventsTest { - @Test - public void firebaseFirestoreTest() throws Exception { - CloudEvent cloudEvent = cloudEventForResource("firestore_complex-cloudevent-input.json"); - Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); - - Event expEvent = legacyEventForResource("firestore_complex-legacy-output.json"); - assertThat(actualEvent).isEqualTo(expEvent); - } - - @Test - public void pubSubTest() throws Exception { - CloudEvent cloudEvent = cloudEventForResource("pubsub_text-cloudevent-input.json"); - Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); - - Event expEvent = legacyEventForResource("pubsub_text-legacy-output.json"); - assertThat(actualEvent).isEqualTo(expEvent); - } - - @Test - public void firebaseAuthTest() throws Exception { - CloudEvent cloudEvent = cloudEventForResource("firebase-auth-cloudevent-input.json"); - Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); - - Event expEvent = legacyEventForResource("firebase-auth-legacy-output.json"); - assertThat(actualEvent).isEqualTo(expEvent); - } - - @Test - public void firebaseDb1Test() throws Exception { - CloudEvent cloudEvent = cloudEventForResource("firebase-db1-cloudevent-input.json"); - Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); - - Event expEvent = legacyEventForResource("firebase-db1-legacy-output.json"); - assertThat(actualEvent).isEqualTo(expEvent); - } - - @Test - public void firebaseDb2Test() throws Exception { - CloudEvent cloudEvent = cloudEventForResource("firebase-db2-cloudevent-input.json"); - Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); - - Event expEvent = legacyEventForResource("firebase-db2-legacy-output.json"); - assertThat(actualEvent).isEqualTo(expEvent); - } - - @Test - public void storageTest() throws Exception { - CloudEvent cloudEvent = cloudEventForResource("storage-cloudevent-input.json"); - Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); - - Event expEvent = legacyEventForResource("storage-legacy-output.json"); - assertThat(actualEvent).isEqualTo(expEvent); - } - - private CloudEvent cloudEventForResource(String resourceName) throws IOException { - try (InputStream in = getClass().getResourceAsStream("/" + resourceName)) { - assertWithMessage("No such resource /%s", resourceName).that(in).isNotNull(); - byte[] req = in.readAllBytes(); - return io.cloudevents.core.provider.EventFormatProvider.getInstance() - .resolveFormat(JsonFormat.CONTENT_TYPE) - .deserialize(req); - } - } - - private Event legacyEventForResource(String resourceName) throws IOException { - try (InputStream in = getClass().getResourceAsStream("/" + resourceName)) { - assertWithMessage("No such resource /%s", resourceName).that(in).isNotNull(); - String legacyEventString = new String(in.readAllBytes(), UTF_8); - return BackgroundFunctionExecutor.parseLegacyEvent(new StringReader(legacyEventString)); - } - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/GcfEventsTest.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/GcfEventsTest.java deleted file mode 100644 index 24939fff..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/GcfEventsTest.java +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.truth.Expect; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import io.cloudevents.CloudEvent; -import io.cloudevents.SpecVersion; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; - -public class GcfEventsTest { - @Rule public Expect expect = Expect.create(); - - private static final String[][] EVENT_DATA = { - { - "storage.json", - "google.cloud.storage.object.v1.finalized", - "//storage.googleapis.com/projects/_/buckets/some-bucket", - "objects/folder/Test.cs" - }, - { - "legacy_storage_change.json", - "google.cloud.storage.object.v1.changed", - "//storage.googleapis.com/projects/_/buckets/sample-bucket", - "objects/MyFile" - }, - { - "firestore_simple.json", - "google.cloud.firestore.document.v1.written", - "//firestore.googleapis.com/projects/project-id/databases/(default)", - "documents/gcf-test/2Vm2mI1d0wIaK2Waj5to" - }, - { - "pubsub_text.json", - "google.cloud.pubsub.topic.v1.messagePublished", - "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test", - null - }, - { - "legacy_pubsub.json", - "google.cloud.pubsub.topic.v1.messagePublished", - "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test", - null - }, - { - "firebase-db1.json", - "google.firebase.database.ref.v1.written", - "//firebasedatabase.googleapis.com/projects/_/locations/us-central1/instances/my-project-id", - "refs/gcf-test/xyz" - }, - { - "firebase-db2.json", - "google.firebase.database.ref.v1.written", - "//firebasedatabase.googleapis.com/projects/_/locations/europe-west1/instances/my-project-id", - "refs/gcf-test/xyz" - }, - { - "firebase-auth1.json", - "google.firebase.auth.user.v1.created", - "//firebaseauth.googleapis.com/projects/my-project-id", - "users/UUpby3s4spZre6kHsgVSPetzQ8l2" - }, - { - "firebase-auth2.json", - "google.firebase.auth.user.v1.deleted", - "//firebaseauth.googleapis.com/projects/my-project-id", - "users/UUpby3s4spZre6kHsgVSPetzQ8l2" - }, - }; - - @Test - public void convertGcfEvent() throws IOException { - for (String[] eventData : EVENT_DATA) { - Event legacyEvent = legacyEventForResource(eventData[0]); - convertGcfEvent(legacyEvent, eventData[1], eventData[2], eventData[3]); - } - } - - private void convertGcfEvent( - Event legacyEvent, String expectedType, String expectedSource, String expectedSubject) { - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - expect.that(cloudEvent.getType()).isEqualTo(expectedType); - expect.that(cloudEvent.getSource().toString()).isEqualTo(expectedSource); - expect.that(cloudEvent.getSubject()).isEqualTo(expectedSubject); - } - - // Checks everything we know about a single event. - @Test - public void checkAllProperties() throws IOException { - Event legacyEvent = legacyEventForResource("storage.json"); - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - assertThat(cloudEvent.getDataContentType()).isEqualTo("application/json"); - assertThat(cloudEvent.getId()).isEqualTo("1147091835525187"); - assertThat(cloudEvent.getType()).isEqualTo("google.cloud.storage.object.v1.finalized"); - assertThat(cloudEvent.getTime()) - .isEqualTo(OffsetDateTime.of(2020, 4, 23, 7, 38, 57, 772_000_000, ZoneOffset.UTC)); - assertThat(cloudEvent.getSource().toString()) - .isEqualTo("//storage.googleapis.com/projects/_/buckets/some-bucket"); - assertThat(cloudEvent.getSubject()).isEqualTo("objects/folder/Test.cs"); - assertThat(cloudEvent.getSpecVersion()).isEqualTo(SpecVersion.V1); - assertThat(cloudEvent.getDataSchema()).isNull(); - } - - // The next set of tests checks the result of using Gson to deserialize the JSON "data" field of - // the - // CloudEvent that we get from converting a legacy event. For the most part we're not testing much - // here, - // since the "data" field is essentially copied from the input legacy event. In some cases we - // adjust it, - // though. - // Later, when we have support for handling these types properly in Java, we can change the tests - // to use - // that. See https://github.com/googleapis/google-cloudevents-java - - @Test - public void storageData() throws IOException { - Event legacyEvent = legacyEventForResource("storage.json"); - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - Map data = cloudEventDataJson(cloudEvent); - assertThat(data) - .containsAtLeast( - "bucket", "some-bucket", - "timeCreated", "2020-04-23T07:38:57.230Z", - "generation", "1587627537231057", - "metageneration", "1", - "size", "352"); - } - - @Test - public void firestoreSimpleData() throws IOException { - Event legacyEvent = legacyEventForResource("firestore_simple.json"); - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - Map data = cloudEventDataJson(cloudEvent); - Map expectedValue = - Map.of( - "name", - "projects/project-id/databases/(default)/documents/gcf-test/2Vm2mI1d0wIaK2Waj5to", - "createTime", "2020-04-23T09:58:53.211035Z", - "updateTime", "2020-04-23T12:00:27.247187Z", - "fields", - Map.of( - "another test", Map.of("stringValue", "asd"), - "count", Map.of("integerValue", "4"), - "foo", Map.of("stringValue", "bar"))); - Map expectedOldValue = - Map.of( - "name", - "projects/project-id/databases/(default)/documents/gcf-test/2Vm2mI1d0wIaK2Waj5to", - "createTime", "2020-04-23T09:58:53.211035Z", - "updateTime", "2020-04-23T12:00:27.247187Z", - "fields", - Map.of( - "another test", Map.of("stringValue", "asd"), - "count", Map.of("integerValue", "3"), - "foo", Map.of("stringValue", "bar"))); - assertThat(data) - .containsAtLeast( - "value", expectedValue, - "oldValue", expectedOldValue, - "updateMask", Map.of("fieldPaths", List.of("count"))); - } - - @Test - public void firestoreComplexData() throws IOException { - Event legacyEvent = legacyEventForResource("firestore_complex.json"); - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - Map data = cloudEventDataJson(cloudEvent); - Map value = (Map) data.get("value"); - Map fields = (Map) value.get("fields"); - Map expectedFields = - Map.of( - "arrayValue", - Map.of( - "arrayValue", - Map.of( - "values", - List.of(Map.of("integerValue", "1"), Map.of("integerValue", "2")))), - "booleanValue", Map.of("booleanValue", true), - "geoPointValue", - Map.of("geoPointValue", Map.of("latitude", 51.4543, "longitude", -0.9781)), - "intValue", Map.of("integerValue", "50"), - "doubleValue", Map.of("doubleValue", 5.5), - "nullValue", Collections.singletonMap("nullValue", null), - "referenceValue", - Map.of( - "referenceValue", - "projects/project-id/databases/(default)/documents/foo/bar/baz/qux"), - "stringValue", Map.of("stringValue", "text"), - "timestampValue", Map.of("timestampValue", "2020-04-23T14:23:53.241Z"), - "mapValue", - Map.of( - "mapValue", - Map.of( - "fields", - Map.of( - "field1", - Map.of("stringValue", "x"), - "field2", - Map.of( - "arrayValue", - Map.of( - "values", - List.of( - Map.of("stringValue", "x"), - Map.of("integerValue", "1")))))))); - assertThat(fields).containsExactlyEntriesIn(expectedFields); - } - - @Test - public void pubSubTextData() throws IOException { - Event legacyEvent = legacyEventForResource("pubsub_text.json"); - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - Map data = cloudEventDataJson(cloudEvent); - - Map message = (Map) data.get("message"); - assertThat(message).isNotNull(); - assertThat(message).containsKey("data"); - // Later we should provide support for doing this more simply and test that: - String base64 = (String) message.get("data"); - byte[] bytes = Base64.getDecoder().decode(base64); - String text = new String(bytes, UTF_8); - assertThat(text).isEqualTo("test message 3"); - - assertThat(message).containsEntry("attributes", Map.of("attr1", "attr1-value")); - } - - @Test - public void pubSubBinaryData() throws IOException { - Event legacyEvent = legacyEventForResource("pubsub_binary.json"); - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - Map data = cloudEventDataJson(cloudEvent); - - Map message = (Map) data.get("message"); - assertThat(message).isNotNull(); - assertThat(message).containsKey("data"); - // Later we should provide support for doing this more simply and test that: - String base64 = (String) message.get("data"); - byte[] bytes = Base64.getDecoder().decode(base64); - assertThat(bytes).isEqualTo(new byte[] {1, 2, 3, 4}); - - assertThat(message).doesNotContainKey("attributes"); - } - - // Checks that a PubSub event correctly gets its payload wrapped in a "message" dictionary. - @Test - public void pubSubWrapping() throws IOException { - Event legacyEvent = legacyEventForResource("legacy_pubsub.json"); - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - assertThat(new String(cloudEvent.getData().toBytes(), UTF_8)) - .isEqualTo( - "{\"message\":{\"@type\":\"type.googleapis.com/google.pubsub.v1.PubsubMessage\"," - + "\"attributes\":{\"attribute1\":\"value1\"}," - + "\"data\":\"VGhpcyBpcyBhIHNhbXBsZSBtZXNzYWdl\"," - + "\"messageId\":\"1215011316659232\"," - + "\"publishTime\":\"2020-05-18T12:13:19.209Z\"}}"); - } - - // Checks that a Firestore event correctly gets an extra "wildcards" property in its CloudEvent - // data - // reflecting the "params" field in the legacy event. - // This test is currently ignored because the final representation of the "params" field is in - // flux. - @Test - @Ignore - public void firestoreWildcards() throws IOException { - Event legacyEvent = legacyEventForResource("firestore_simple.json"); - CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent); - JsonObject payload = - new Gson().fromJson(new String(cloudEvent.getData().toBytes(), UTF_8), JsonObject.class); - JsonObject wildcards = payload.getAsJsonObject("wildcards"); - assertThat(wildcards.keySet()).containsExactly("doc"); - assertThat(wildcards.getAsJsonPrimitive("doc").getAsString()).isEqualTo("2Vm2mI1d0wIaK2Waj5to"); - } - - private Event legacyEventForResource(String resourceName) throws IOException { - try (InputStream in = getClass().getResourceAsStream("/" + resourceName)) { - assertWithMessage("No such resource /%s", resourceName).that(in).isNotNull(); - String legacyEventString = new String(in.readAllBytes(), UTF_8); - return BackgroundFunctionExecutor.parseLegacyEvent(new StringReader(legacyEventString)); - } - } - - private static Map cloudEventDataJson(CloudEvent cloudEvent) { - String data = new String(cloudEvent.getData().toBytes(), UTF_8); - @SuppressWarnings("unchecked") - Map map = new Gson().fromJson(data, Map.class); - return map; - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java deleted file mode 100644 index 335cc7de..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java +++ /dev/null @@ -1,773 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.stream.Collectors.toList; - -import com.google.auto.value.AutoValue; -import com.google.cloud.functions.invoker.runner.Invoker; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import com.google.common.io.Resources; -import com.google.common.truth.Expect; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import io.cloudevents.CloudEvent; -import io.cloudevents.core.builder.CloudEventBuilder; -import io.cloudevents.core.format.EventFormat; -import io.cloudevents.core.provider.EventFormatProvider; -import io.cloudevents.http.HttpMessageFactory; -import io.cloudevents.jackson.JsonFormat; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UncheckedIOException; -import java.net.ServerSocket; -import java.net.URI; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.TreeMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Pattern; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentProvider; -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.util.BytesContentProvider; -import org.eclipse.jetty.client.util.MultiPartContentProvider; -import org.eclipse.jetty.client.util.StringContentProvider; -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpStatus; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.rules.TestName; - -/** - * Integration test that starts up a web server running the Function Framework and sends HTTP - * requests to it. - */ -public class IntegrationTest { - @Rule public final Expect expect = Expect.create(); - @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - @Rule public final TestName testName = new TestName(); - - private static final String SERVER_READY_STRING = "Started ServerConnector"; - - private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(); - - private static String sampleLegacyEvent(File snoopFile) { - return "{\n" - + " \"data\": {\n" - + " \"a\": 2,\n" - + " \"b\": 3,\n" - + " \"targetFile\": \"" - + snoopFile - + "\"" - + " },\n" - + " \"context\": {\n" - + " \"eventId\": \"B234-1234-1234\",\n" - + " \"timestamp\": \"2018-04-05T17:31:00Z\",\n" - + " \"eventType\": \"google.pubsub.topic.publish\",\n" - + " \"resource\": {\n" - + " \"service\":\"pubsub.googleapis.com\",\n" - + " \"name\":\"projects/sample-project/topics/gcf-test\",\n" - + " \"type\":\"type.googleapis.com/google.pubsub.v1.PubsubMessage\"\n" - + " }\n" - + " }\n" - + "}"; - } - - private static CloudEvent sampleCloudEvent(File snoopFile) { - return CloudEventBuilder.v1() - .withId("B234-1234-1234") - .withSource(URI.create("//pubsub.googleapis.com/projects/sample-project/topics/gcf-test")) - .withSubject("documents/gcf-test/2Vm2mI1d0wIaK2Waj5to") - .withType("google.cloud.pubsub.topic.v1.messagePublished") - .withDataSchema(URI.create("/schema")) - .withDataContentType("application/json") - .withData(("{\"a\": 2, \"b\": 3, \"targetFile\": \"" + snoopFile + "\"}").getBytes(UTF_8)) - .withTime(OffsetDateTime.of(2018, 4, 5, 17, 31, 0, 0, ZoneOffset.UTC)) - .build(); - } - - private static JsonObject expectedCloudEventAttributes() { - JsonObject attributes = new JsonObject(); - attributes.addProperty("datacontenttype", "application/json"); - attributes.addProperty("specversion", "1.0"); - attributes.addProperty("id", "B234-1234-1234"); - attributes.addProperty("source", "/source"); - attributes.addProperty("time", "2018-04-05T17:31Z"); - attributes.addProperty("type", "com.example.someevent.new"); - attributes.addProperty("dataschema", "/schema"); - return attributes; - } - - private static int serverPort; - - /** - * Each test method will start up a server on the same port, make one or more HTTP requests to - * that port, then kill the server. So the port should be free when the next test method runs. - */ - @BeforeClass - public static void allocateServerPort() throws IOException { - try (ServerSocket serverSocket = new ServerSocket(0)) { - serverPort = serverSocket.getLocalPort(); - } - } - - /** - * Description of a test case. When we send an HTTP POST to the given {@link #url()} in the - * server, with the given {@link #requestContent()} ()} as the body of the POST, then we expect to - * get back the given {@link #expectedResponseText()} in the body of the response. - */ - @AutoValue - abstract static class TestCase { - - abstract String url(); - - abstract ContentProvider requestContent(); - - abstract int expectedResponseCode(); - - abstract Optional expectedResponseText(); - - abstract Optional expectedJson(); - - abstract Optional expectedContentType(); - - abstract Optional expectedOutput(); - - abstract Optional httpContentType(); - - abstract ImmutableMap httpHeaders(); - - abstract Optional snoopFile(); - - static Builder builder() { - return new AutoValue_IntegrationTest_TestCase.Builder() - .setUrl("/") - .setRequestText("") - .setExpectedResponseCode(HttpStatus.OK_200) - .setExpectedResponseText("") - .setHttpContentType("text/plain") - .setHttpHeaders(ImmutableMap.of()); - } - - @AutoValue.Builder - abstract static class Builder { - - abstract Builder setUrl(String x); - - abstract Builder setRequestContent(ContentProvider x); - - Builder setRequestText(String text) { - return setRequestContent(new StringContentProvider(text)); - } - - abstract Builder setExpectedResponseCode(int x); - - abstract Builder setExpectedResponseText(String x); - - abstract Builder setExpectedResponseText(Optional x); - - abstract Builder setExpectedContentType(String x); - - abstract Builder setExpectedOutput(String x); - - abstract Builder setExpectedJson(JsonObject x); - - abstract Builder setHttpContentType(String x); - - abstract Builder setHttpContentType(Optional x); - - abstract Builder setHttpHeaders(ImmutableMap x); - - abstract Builder setSnoopFile(File x); - - abstract TestCase build(); - } - } - - private static String fullTarget(String nameWithoutPackage) { - return "com.google.cloud.functions.invoker.testfunctions." + nameWithoutPackage; - } - - private static final TestCase FAVICON_TEST_CASE = - TestCase.builder() - .setUrl("/favicon.ico?foo=bar") - .setExpectedResponseCode(HttpStatus.NOT_FOUND_404) - .setExpectedResponseText(Optional.empty()) - .build(); - - private static final TestCase ROBOTS_TXT_TEST_CASE = - TestCase.builder() - .setUrl("/robots.txt?foo=bar") - .setExpectedResponseCode(HttpStatus.NOT_FOUND_404) - .setExpectedResponseText(Optional.empty()) - .build(); - - @Test - public void helloWorld() throws Exception { - testHttpFunction( - fullTarget("HelloWorld"), - ImmutableList.of( - TestCase.builder().setExpectedResponseText("hello\n").build(), - FAVICON_TEST_CASE, - ROBOTS_TXT_TEST_CASE)); - } - - @Test - public void exceptionHttp() throws Exception { - String exceptionExpectedOutput = - "\"severity\": \"ERROR\", \"logging.googleapis.com/sourceLocation\": {\"file\":" - + " \"com/google/cloud/functions/invoker/HttpFunctionExecutor.java\", \"method\":" - + " \"service\"}, \"message\": \"Failed to execute" - + " com.google.cloud.functions.invoker.testfunctions.ExceptionHttp\\n" - + "java.lang.RuntimeException: exception thrown for test"; - testHttpFunction( - fullTarget("ExceptionHttp"), - ImmutableList.of( - TestCase.builder() - .setExpectedResponseCode(500) - .setExpectedOutput(exceptionExpectedOutput) - .build())); - } - - @Test - public void exceptionBackground() throws Exception { - String exceptionExpectedOutput = - "\"severity\": \"ERROR\", \"logging.googleapis.com/sourceLocation\": {\"file\":" - + " \"com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java\", \"method\":" - + " \"service\"}, \"message\": \"Failed to execute" - + " com.google.cloud.functions.invoker.testfunctions.ExceptionBackground\\n" - + "java.lang.RuntimeException: exception thrown for test"; - - File snoopFile = snoopFile(); - String gcfRequestText = sampleLegacyEvent(snoopFile); - - testFunction( - SignatureType.BACKGROUND, - fullTarget("ExceptionBackground"), - ImmutableList.of(), - ImmutableList.of( - TestCase.builder() - .setRequestText(gcfRequestText) - .setExpectedResponseCode(500) - .setExpectedOutput(exceptionExpectedOutput) - .build())); - } - - @Test - public void echo() throws Exception { - String testText = "hello\nworld\n"; - testHttpFunction( - fullTarget("Echo"), - ImmutableList.of( - TestCase.builder() - .setRequestText(testText) - .setExpectedResponseText(testText) - .setExpectedContentType("text/plain") - .build(), - TestCase.builder() - .setHttpContentType("application/octet-stream") - .setRequestText(testText) - .setExpectedResponseText(testText) - .setExpectedContentType("application/octet-stream") - .build())); - } - - @Test - public void echoUrl() throws Exception { - String[] testUrls = {"/", "/foo/bar", "/?foo=bar&baz=buh", "/foo?bar=baz"}; - List testCases = - Arrays.stream(testUrls) - .map(url -> TestCase.builder().setUrl(url).setExpectedResponseText(url + "\n").build()) - .collect(toList()); - testHttpFunction(fullTarget("EchoUrl"), testCases); - } - - @Test - public void stackDriverLogging() throws Exception { - String simpleExpectedOutput = - "{\"severity\": \"INFO\", " - + "\"logging.googleapis.com/sourceLocation\": " - + "{\"file\": \"com/google/cloud/functions/invoker/testfunctions/Log.java\"," - + " \"method\": \"service\"}," - + " \"message\": \"blim\"}"; - TestCase simpleTestCase = - TestCase.builder().setUrl("/?message=blim").setExpectedOutput(simpleExpectedOutput).build(); - String quotingExpectedOutput = "\"message\": \"foo\\nbar\\\""; - TestCase quotingTestCase = - TestCase.builder() - .setUrl("/?message=" + URLEncoder.encode("foo\nbar\"", "UTF-8")) - .setExpectedOutput(quotingExpectedOutput) - .build(); - String exceptionExpectedOutput = - "{\"severity\": \"ERROR\", " - + "\"logging.googleapis.com/sourceLocation\": " - + "{\"file\": \"com/google/cloud/functions/invoker/testfunctions/Log.java\", " - + "\"method\": \"service\"}, " - + "\"message\": \"oops\\njava.lang.Exception: disaster\\n" - + " at com.google.cloud.functions.invoker.testfunctions.Log.service(Log.java:"; - TestCase exceptionTestCase = - TestCase.builder() - .setUrl("/?message=oops&level=severe&exception=disaster") - .setExpectedOutput(exceptionExpectedOutput) - .build(); - testHttpFunction( - fullTarget("Log"), ImmutableList.of(simpleTestCase, quotingTestCase, exceptionTestCase)); - } - - private static int getJavaVersion() { - String version = System.getProperty("java.version"); - if (version.startsWith("1.")) { - version = version.substring(2, 3); - } else { - int dot = version.indexOf("."); - if (dot != -1) { - version = version.substring(0, dot); - } - } - return Integer.parseInt(version); - } - - @Test - public void background() throws Exception { - // TODO: Only enable background tests for < 17 - if (getJavaVersion() < 17) { - backgroundTest("BackgroundSnoop"); - } - } - - @Test - public void typedBackground() throws Exception { - // TODO: Only enable background tests for < 17 - if (getJavaVersion() < 17) { - backgroundTest("TypedBackgroundSnoop"); - } - } - - private void backgroundTest(String target) throws Exception { - File snoopFile = snoopFile(); - String gcfRequestText = sampleLegacyEvent(snoopFile); - JsonObject expectedJson = new Gson().fromJson(gcfRequestText, JsonObject.class); - TestCase gcfTestCase = - TestCase.builder() - .setRequestText(gcfRequestText) - .setSnoopFile(snoopFile) - .setExpectedJson(expectedJson) - .build(); - - // A CloudEvent using the "structured content mode", where both the metadata and the payload - // are in the body of the HTTP request. - EventFormat jsonFormat = - EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE); - String cloudEventRequestText = - new String(jsonFormat.serialize(sampleCloudEvent(snoopFile)), UTF_8); - // For CloudEvents, we don't currently populate Context#getResource with anything interesting, - // so we excise that from the expected text we would have with legacy events. - JsonObject cloudEventExpectedJson = new Gson().fromJson(gcfRequestText, JsonObject.class); - TestCase cloudEventsStructuredTestCase = - TestCase.builder() - .setSnoopFile(snoopFile) - .setRequestText(cloudEventRequestText) - .setHttpContentType("application/cloudevents+json; charset=utf-8") - .setExpectedJson(cloudEventExpectedJson) - .build(); - - // A CloudEvent using the "binary content mode", where the metadata is in HTTP headers and the - // payload is the body of the HTTP request. - Map headers = new TreeMap<>(); - AtomicReference bodyRef = new AtomicReference<>(); - HttpMessageFactory.createWriter(headers::put, bodyRef::set) - .writeBinary(sampleCloudEvent(snoopFile)); - TestCase cloudEventsBinaryTestCase = - TestCase.builder() - .setSnoopFile(snoopFile) - .setRequestText(new String(bodyRef.get(), UTF_8)) - .setHttpContentType(headers.get("Content-Type")) - .setHttpHeaders(ImmutableMap.copyOf(headers)) - .setExpectedJson(cloudEventExpectedJson) - .build(); - - backgroundTest( - SignatureType.BACKGROUND, - fullTarget(target), - ImmutableList.of(gcfTestCase, cloudEventsStructuredTestCase, cloudEventsBinaryTestCase)); - } - - /** - * Tests a CloudEvent being handled by a CloudEvent handler (no translation to or from legacy). - */ - @Test - public void nativeCloudEvent() throws Exception { - File snoopFile = snoopFile(); - CloudEvent cloudEvent = sampleCloudEvent(snoopFile); - EventFormat jsonFormat = - EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE); - String cloudEventJson = new String(jsonFormat.serialize(cloudEvent), UTF_8); - - // A CloudEvent using the "structured content mode", where both the metadata and the payload - // are in the body of the HTTP request. - JsonObject cloudEventJsonObject = new Gson().fromJson(cloudEventJson, JsonObject.class); - TestCase cloudEventsStructuredTestCase = - TestCase.builder() - .setSnoopFile(snoopFile) - .setRequestText(cloudEventJson) - .setHttpContentType("application/cloudevents+json; charset=utf-8") - .setExpectedJson(cloudEventJsonObject) - .build(); - - // A CloudEvent using the "binary content mode", where the metadata is in HTTP headers and the - // payload is the body of the HTTP request. - Map headers = new TreeMap<>(); - AtomicReference bodyRef = new AtomicReference<>(); - HttpMessageFactory.createWriter(headers::put, bodyRef::set) - .writeBinary(sampleCloudEvent(snoopFile)); - TestCase cloudEventsBinaryTestCase = - TestCase.builder() - .setSnoopFile(snoopFile) - .setRequestText(new String(bodyRef.get(), UTF_8)) - .setHttpContentType(headers.get("Content-Type")) - .setHttpHeaders(ImmutableMap.copyOf(headers)) - .setExpectedJson(cloudEventJsonObject) - .build(); - - backgroundTest( - SignatureType.CLOUD_EVENT, - fullTarget("CloudEventSnoop"), - ImmutableList.of(cloudEventsStructuredTestCase, cloudEventsBinaryTestCase)); - } - - @Test - public void nested() throws Exception { - String testText = "sic transit gloria mundi"; - testHttpFunction( - fullTarget("Nested.Echo"), - ImmutableList.of( - TestCase.builder().setRequestText(testText).setExpectedResponseText(testText).build())); - } - - @Test - public void packageless() throws Exception { - testHttpFunction( - "PackagelessHelloWorld", - ImmutableList.of(TestCase.builder().setExpectedResponseText("hello, world\n").build())); - } - - @Test - public void multipart() throws Exception { - MultiPartContentProvider multiPartProvider = new MultiPartContentProvider(); - byte[] bytes = new byte[17]; - multiPartProvider.addFieldPart("bytes", new BytesContentProvider(bytes), new HttpFields()); - String string = "1234567890"; - multiPartProvider.addFieldPart("string", new StringContentProvider(string), new HttpFields()); - String expectedResponse = - "part bytes type application/octet-stream length 17\n" - + "part string type text/plain;charset=UTF-8 length 10\n"; - testHttpFunction( - fullTarget("Multipart"), - ImmutableList.of( - TestCase.builder() - .setHttpContentType(Optional.empty()) - .setRequestContent(multiPartProvider) - .setExpectedResponseText(expectedResponse) - .build())); - } - - private File snoopFile() throws IOException { - return temporaryFolder.newFile(testName.getMethodName() + ".txt"); - } - - /** Any runtime class that user code shouldn't be able to see. */ - private static final Class INTERNAL_CLASS = CloudFunctionsContext.class; - - private String functionJarString() throws IOException { - Path functionJarTargetDir = Paths.get("../testfunction/target"); - Pattern functionJarPattern = - Pattern.compile("java-function-invoker-testfunction-.*-tests\\.jar"); - List functionJars = - Files.list(functionJarTargetDir) - .map(path -> path.getFileName().toString()) - .filter(s -> functionJarPattern.matcher(s).matches()) - .map(s -> functionJarTargetDir.resolve(s)) - .collect(toList()); - assertWithMessage("Number of jars in %s matching %s", functionJarTargetDir, functionJarPattern) - .that(functionJars) - .hasSize(1); - return Iterables.getOnlyElement(functionJars).toString(); - } - - /** - * Tests that if we launch an HTTP function with {@code --classpath}, then the function code - * cannot see the classes from the runtime. This is allows us to avoid conflicts between versions - * of libraries that we use in the runtime and different versions of the same libraries that the - * function might use. - */ - @Test - public void classpathOptionHttp() throws Exception { - TestCase testCase = - TestCase.builder() - .setUrl("/?class=" + INTERNAL_CLASS.getName()) - .setExpectedResponseText("OK") - .build(); - testFunction( - SignatureType.HTTP, - "com.example.functionjar.Foreground", - ImmutableList.of("--classpath", functionJarString()), - ImmutableList.of(testCase)); - } - - /** Like {@link #classpathOptionHttp} but for background functions. */ - @Test - public void classpathOptionBackground() throws Exception { - Gson gson = new Gson(); - URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json"); - assertThat(resourceUrl).isNotNull(); - String originalJson = Resources.toString(resourceUrl, StandardCharsets.UTF_8); - JsonObject json = gson.fromJson(originalJson, JsonObject.class); - JsonObject jsonData = json.getAsJsonObject("data"); - jsonData.addProperty("class", INTERNAL_CLASS.getName()); - testFunction( - SignatureType.BACKGROUND, - "com.example.functionjar.Background", - ImmutableList.of("--classpath", functionJarString()), - ImmutableList.of(TestCase.builder().setRequestText(json.toString()).build())); - } - - // In these tests, we test a number of different functions that express the same functionality - // in different ways. Each function is invoked with a complete HTTP body that looks like a real - // event. We start with a fixed body and insert into its JSON an extra property that tells the - // function where to write what it received. We have to do this since background functions, by - // design, don't return a value. - private void backgroundTest( - SignatureType signatureType, String functionTarget, List testCases) - throws Exception { - for (TestCase testCase : testCases) { - File snoopFile = testCase.snoopFile().get(); - snoopFile.delete(); - testFunction(signatureType, functionTarget, ImmutableList.of(), ImmutableList.of(testCase)); - String snooped = new String(Files.readAllBytes(snoopFile.toPath()), StandardCharsets.UTF_8); - Gson gson = new Gson(); - JsonObject snoopedJson = gson.fromJson(snooped, JsonObject.class); - JsonObject expectedJson = testCase.expectedJson().get(); - expect - .withMessage( - "Testing %s with %s\nGOT %s\nNOT %s", - functionTarget, testCase, snoopedJson, expectedJson) - .that(snoopedJson) - .isEqualTo(expectedJson); - } - } - - private void checkSnoopFile(TestCase testCase) throws IOException { - File snoopFile = testCase.snoopFile().get(); - JsonObject expectedJson = testCase.expectedJson().get(); - String snooped = new String(Files.readAllBytes(snoopFile.toPath()), StandardCharsets.UTF_8); - Gson gson = new Gson(); - JsonObject snoopedJson = gson.fromJson(snooped, JsonObject.class); - expect.withMessage("Testing with %s", testCase).that(snoopedJson).isEqualTo(expectedJson); - } - - private void testHttpFunction(String target, List testCases) throws Exception { - testFunction(SignatureType.HTTP, target, ImmutableList.of(), testCases); - } - - private void testFunction( - SignatureType signatureType, - String target, - ImmutableList extraArgs, - List testCases) - throws Exception { - ServerProcess serverProcess = startServer(signatureType, target, extraArgs); - try { - HttpClient httpClient = new HttpClient(); - httpClient.start(); - for (TestCase testCase : testCases) { - testCase.snoopFile().ifPresent(File::delete); - String uri = "http://localhost:" + serverPort + testCase.url(); - Request request = httpClient.POST(uri); - testCase - .httpContentType() - .ifPresent(contentType -> request.header(HttpHeader.CONTENT_TYPE, contentType)); - testCase.httpHeaders().forEach((header, value) -> request.header(header, value)); - request.content(testCase.requestContent()); - ContentResponse response = request.send(); - expect - .withMessage("Response to %s is %s %s", uri, response.getStatus(), response.getReason()) - .that(response.getStatus()) - .isEqualTo(testCase.expectedResponseCode()); - testCase - .expectedResponseText() - .ifPresent(text -> expect.that(response.getContentAsString()).isEqualTo(text)); - testCase - .expectedContentType() - .ifPresent(type -> expect.that(response.getMediaType()).isEqualTo(type)); - if (testCase.snoopFile().isPresent()) { - checkSnoopFile(testCase); - } - } - } finally { - serverProcess.close(); - } - for (TestCase testCase : testCases) { - testCase - .expectedOutput() - .ifPresent(output -> expect.that(serverProcess.output()).contains(output)); - } - // Wait for the output monitor task to terminate. If it threw an exception, we will get an - // ExecutionException here. - serverProcess.outputMonitorResult().get(); - } - - private enum SignatureType { - HTTP("http"), - BACKGROUND("event"), - CLOUD_EVENT("cloudevent"); - - private final String name; - - SignatureType(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - } - - private static class ServerProcess implements AutoCloseable { - private final Process process; - private final Future outputMonitorResult; - private final StringBuilder output; - - ServerProcess(Process process, Future outputMonitorResult, StringBuilder output) { - this.process = process; - this.outputMonitorResult = outputMonitorResult; - this.output = output; - } - - Process process() { - return process; - } - - Future outputMonitorResult() { - return outputMonitorResult; - } - - String output() { - synchronized (output) { - return output.toString(); - } - } - - @Override - public void close() { - process().destroy(); - try { - process().waitFor(); - } catch (InterruptedException e) { - // Should not happen. - } - } - } - - private ServerProcess startServer( - SignatureType signatureType, String target, ImmutableList extraArgs) - throws IOException, InterruptedException { - File javaHome = new File(System.getProperty("java.home")); - assertThat(javaHome.exists()).isTrue(); - File javaBin = new File(javaHome, "bin"); - File javaCommand = new File(javaBin, "java"); - assertThat(javaCommand.exists()).isTrue(); - String myClassPath = System.getProperty("java.class.path"); - assertThat(myClassPath).isNotNull(); - ImmutableList command = - ImmutableList.builder() - .add(javaCommand.toString(), "-classpath", myClassPath, Invoker.class.getName()) - .addAll(extraArgs) - .build(); - ProcessBuilder processBuilder = new ProcessBuilder().command(command).redirectErrorStream(true); - Map environment = - ImmutableMap.of( - "PORT", - String.valueOf(serverPort), - "K_SERVICE", - "test-function", - "FUNCTION_SIGNATURE_TYPE", - signatureType.toString(), - "FUNCTION_TARGET", - target); - processBuilder.environment().putAll(environment); - Process serverProcess = processBuilder.start(); - CountDownLatch ready = new CountDownLatch(1); - StringBuilder output = new StringBuilder(); - Future outputMonitorResult = - EXECUTOR.submit(() -> monitorOutput(serverProcess.getInputStream(), ready, output)); - boolean serverReady = ready.await(5, TimeUnit.SECONDS); - if (!serverReady) { - serverProcess.destroy(); - throw new AssertionError("Server never became ready"); - } - return new ServerProcess(serverProcess, outputMonitorResult, output); - } - - private void monitorOutput( - InputStream processOutput, CountDownLatch ready, StringBuilder output) { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(processOutput))) { - String line; - while ((line = reader.readLine()) != null) { - if (line.contains(SERVER_READY_STRING)) { - ready.countDown(); - } - System.out.println(line); - synchronized (output) { - output.append(line).append('\n'); - } - if (line.contains("WARNING")) { - throw new AssertionError("Found warning in server output:\n" + line); - } - } - } catch (IOException e) { - e.printStackTrace(); - throw new UncheckedIOException(e); - } - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java deleted file mode 100644 index 9ef51b2a..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java +++ /dev/null @@ -1,511 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions.invoker.http; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; -import static org.junit.Assert.fail; - -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpRequest.HttpPart; -import com.google.cloud.functions.HttpResponse; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.ServerSocket; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.TreeMap; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import javax.servlet.MultipartConfigElement; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.util.BytesContentProvider; -import org.eclipse.jetty.client.util.MultiPartContentProvider; -import org.eclipse.jetty.client.util.StringContentProvider; -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.junit.BeforeClass; -import org.junit.Test; - -public class HttpTest { - - private static final String TEST_BODY = - "In the reign of James the Second\n" - + "It was generally reckoned\n" - + "As a rather serious crime\n" - + "To marry two wives at a time.\n"; - - private static final byte[] RANDOM_BYTES = new byte[1024]; - - static { - new Random().nextBytes(RANDOM_BYTES); - } - - private static int serverPort; - - /** - * Each test method will start up a server on the same port, make one or more HTTP requests to - * that port, then kill the server. So the port should be free when the next test method runs. - */ - @BeforeClass - public static void allocateServerPort() throws IOException { - ServerSocket serverSocket = new ServerSocket(0); - serverPort = serverSocket.getLocalPort(); - serverSocket.close(); - } - - /** - * Wrapper class that allows us to start a Jetty server with a single servlet for {@code /*} - * within a try-with-resources statement. The servlet will be configured to support multipart - * requests. - */ - private static class SimpleServer implements AutoCloseable { - private final Server server; - - SimpleServer(HttpServlet servlet) throws Exception { - this.server = new Server(serverPort); - ServletContextHandler context = new ServletContextHandler(); - context.setContextPath("/"); - server.setHandler(context); - ServletHolder servletHolder = new ServletHolder(servlet); - servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement("tiddly")); - context.addServlet(servletHolder, "/*"); - server.start(); - } - - @Override - public void close() throws Exception { - server.stop(); - } - } - - @FunctionalInterface - private interface HttpRequestTest { - void test(HttpRequest request) throws Exception; - } - - /** - * Tests methods on the {@link HttpRequest} object while the request is being serviced. We are not - * guaranteed that the underlying {@link HttpServletRequest} object will still be valid when the - * request completes, and in fact in Jetty it isn't. So we perform the checks in the context of - * the servlet, and report any exception back to the test method. - */ - @Test - public void httpRequestMethods() throws Exception { - AtomicReference testReference = new AtomicReference<>(); - AtomicReference exceptionReference = new AtomicReference<>(); - HttpRequestServlet testServlet = new HttpRequestServlet(testReference, exceptionReference); - try (SimpleServer server = new SimpleServer(testServlet)) { - httpRequestMethods(testReference, exceptionReference); - } - } - - private void httpRequestMethods( - AtomicReference testReference, AtomicReference exceptionReference) - throws Exception { - HttpClient httpClient = new HttpClient(); - httpClient.start(); - String uri = "http://localhost:" + serverPort + "/foo/bar?baz=buh&baz=xxx&blim=blam&baz=what"; - HttpRequestTest[] tests = { - request -> assertThat(request.getMethod()).isEqualTo("POST"), - request -> assertThat(request.getMethod()).isEqualTo("POST"), - request -> assertThat(request.getUri()).isEqualTo(uri), - request -> assertThat(request.getPath()).isEqualTo("/foo/bar"), - request -> assertThat(request.getQuery()).hasValue("baz=buh&baz=xxx&blim=blam&baz=what"), - request -> { - Map> expectedQueryParameters = new TreeMap<>(); - expectedQueryParameters.put("baz", Arrays.asList("buh", "xxx", "what")); - expectedQueryParameters.put("blim", Arrays.asList("blam")); - assertThat(request.getQueryParameters()).isEqualTo(expectedQueryParameters); - }, - request -> assertThat(request.getFirstQueryParameter("baz")).hasValue("buh"), - request -> assertThat(request.getFirstQueryParameter("something")).isEmpty(), - request -> - assertThat(request.getContentType().get()) - .ignoringCase() - .isEqualTo("text/plain; charset=utf-8"), - request -> assertThat(request.getContentLength()).isEqualTo(TEST_BODY.length()), - request -> assertThat(request.getCharacterEncoding()).isPresent(), - request -> assertThat(request.getCharacterEncoding().get()).ignoringCase().isEqualTo("utf-8"), - request -> { - try (BufferedReader reader = request.getReader()) { - validateReader(reader); - assertThat(request.getReader()).isSameInstanceAs(reader); - } - try { - request.getInputStream(); - fail("Did not get expected exception"); - } catch (IllegalStateException expected) { - } - }, - request -> { - try (InputStream inputStream = request.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { - validateReader(reader); - assertThat(request.getInputStream()).isSameInstanceAs(inputStream); - } - }, - request -> { - Map> expectedHeaders = new TreeMap<>(); - expectedHeaders.put( - HttpHeader.CONTENT_LENGTH.asString(), - Arrays.asList(String.valueOf(TEST_BODY.length()))); - expectedHeaders.put("foo", Arrays.asList("bar", "baz")); - assertThat(request.getHeaders()).containsAtLeastEntriesIn(expectedHeaders); - }, - request -> assertThat(request.getFirstHeader("foo")).hasValue("bar"), - request -> { - try { - request.getParts(); - fail("Did not get expected exception"); - } catch (IllegalStateException expected) { - } - } - }; - for (HttpRequestTest test : tests) { - testReference.set(test); - Request request = - httpClient - .POST(uri) - .header(HttpHeader.CONTENT_TYPE, "text/plain; charset=utf-8") - .header("foo", "bar") - .header("foo", "baz") - .content(new StringContentProvider(TEST_BODY)); - ContentResponse response = request.send(); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); - throwIfNotNull(exceptionReference.get()); - } - } - - @Test - public void emptyRequest() throws Exception { - HttpClient httpClient = new HttpClient(); - httpClient.start(); - String uri = "http://localhost:" + serverPort; - HttpRequestTest test = - request -> { - assertThat(request.getUri()).isEqualTo(uri + "/"); - assertThat(request.getPath()).isEqualTo("/"); - assertThat(request.getQuery()).isEmpty(); - assertThat(request.getQueryParameters()).isEmpty(); - assertThat(request.getContentType()).isEmpty(); - assertThat(request.getContentLength()).isEqualTo(0L); - }; - AtomicReference exceptionReference = new AtomicReference<>(); - AtomicReference testReference = new AtomicReference<>(test); - HttpRequestServlet testServlet = new HttpRequestServlet(testReference, exceptionReference); - try (SimpleServer server = new SimpleServer(testServlet)) { - ContentResponse response = httpClient.POST(uri).send(); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); - throwIfNotNull(exceptionReference.get()); - } - } - - private void validateReader(BufferedReader reader) { - String text = reader.lines().collect(Collectors.joining("\n", "", "\n")); - assertThat(text).isEqualTo(TEST_BODY); - } - - @Test - public void multiPartRequest() throws Exception { - AtomicReference testReference = new AtomicReference<>(); - AtomicReference exceptionReference = new AtomicReference<>(); - HttpRequestServlet testServlet = new HttpRequestServlet(testReference, exceptionReference); - HttpClient httpClient = new HttpClient(); - httpClient.start(); - String uri = "http://localhost:" + serverPort + "/"; - MultiPartContentProvider multiPart = new MultiPartContentProvider(); - HttpFields textHttpFields = new HttpFields(); - textHttpFields.add("foo", "bar"); - multiPart.addFieldPart("text", new StringContentProvider(TEST_BODY), textHttpFields); - HttpFields bytesHttpFields = new HttpFields(); - bytesHttpFields.add("foo", "baz"); - bytesHttpFields.add("foo", "buh"); - assertThat(bytesHttpFields.getValuesList("foo")).containsExactly("baz", "buh"); - multiPart.addFilePart( - "binary", "/tmp/binary.x", new BytesContentProvider(RANDOM_BYTES), bytesHttpFields); - HttpRequestTest test = - request -> { - // The Content-Type header will also have a boundary=something attribute. - assertThat(request.getContentType().get()).startsWith("multipart/form-data"); - assertThat(request.getParts().keySet()).containsExactly("text", "binary"); - HttpPart textPart = request.getParts().get("text"); - assertThat(textPart.getFileName()).isEmpty(); - assertThat(textPart.getContentLength()).isEqualTo(TEST_BODY.length()); - assertThat(textPart.getContentType().get()).startsWith("text/plain"); - assertThat(textPart.getCharacterEncoding()).isPresent(); - assertThat(textPart.getCharacterEncoding().get()).ignoringCase().isEqualTo("utf-8"); - assertThat(textPart.getHeaders()).containsAtLeast("foo", Arrays.asList("bar")); - assertThat(textPart.getFirstHeader("foo")).hasValue("bar"); - validateReader(textPart.getReader()); - HttpPart bytesPart = request.getParts().get("binary"); - assertThat(bytesPart.getFileName()).hasValue("/tmp/binary.x"); - assertThat(bytesPart.getContentLength()).isEqualTo(RANDOM_BYTES.length); - assertThat(bytesPart.getContentType()).hasValue("application/octet-stream"); - // We only see ["buh"] here, not ["baz", "buh"], apparently due to a Jetty bug. - // Repeated headers on multi-part content are not a big problem anyway. - List foos = bytesPart.getHeaders().get("foo"); - assertThat(foos).contains("buh"); - byte[] bytes = new byte[RANDOM_BYTES.length]; - try (InputStream inputStream = bytesPart.getInputStream()) { - assertThat(inputStream.read(bytes)).isEqualTo(bytes.length); - assertThat(inputStream.read()).isEqualTo(-1); - assertThat(bytes).isEqualTo(RANDOM_BYTES); - } - }; - try (SimpleServer server = new SimpleServer(testServlet)) { - testReference.set(test); - Request request = httpClient.POST(uri).header("foo", "oof").content(multiPart); - ContentResponse response = request.send(); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); - throwIfNotNull(exceptionReference.get()); - } - } - - private static class HttpRequestServlet extends HttpServlet { - private final AtomicReference testReference; - private final AtomicReference exceptionReference; - - private HttpRequestServlet( - AtomicReference testReference, - AtomicReference exceptionReference) { - this.testReference = testReference; - this.exceptionReference = exceptionReference; - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) { - try { - testReference.get().test(new HttpRequestImpl(req)); - } catch (Throwable t) { - exceptionReference.set(t); - } - } - } - - @FunctionalInterface - private interface HttpResponseTest { - void test(HttpResponse response) throws Exception; - } - - /** - * Tests interactions with the {@link HttpResponse} object while the request is still ongoing. For - * example, if we append a header then we should see that header in {@link - * HttpResponse#getHeaders()}. - */ - @Test - public void httpResponseSetAndGet() throws Exception { - AtomicReference testReference = new AtomicReference<>(); - AtomicReference exceptionReference = new AtomicReference<>(); - HttpResponseServlet testServlet = new HttpResponseServlet(testReference, exceptionReference); - try (SimpleServer server = new SimpleServer(testServlet)) { - httpResponseSetAndGet(testReference, exceptionReference); - } - } - - private void httpResponseSetAndGet( - AtomicReference testReference, - AtomicReference exceptionReference) - throws Exception { - HttpResponseTest[] tests = { - response -> assertThat(response.getContentType()).isEmpty(), - response -> { - response.setContentType("text/plain; charset=utf-8"); - assertThat(response.getContentType().get()).matches("(?i)text/plain;\\s*charset=utf-8"); - }, - response -> { - response.appendHeader("Content-Type", "application/octet-stream"); - assertThat(response.getContentType()).hasValue("application/octet-stream"); - assertThat(response.getHeaders()) - .containsAtLeast("Content-Type", Arrays.asList("application/octet-stream")); - }, - response -> { - Map> initialHeaders = response.getHeaders(); - // The servlet spec says this should be empty, but actually we get a Date header here. - // So we just check that we can add our own headers. - response.appendHeader("foo", "bar"); - response.appendHeader("wibbly", "wobbly"); - response.appendHeader("foo", "baz"); - Map> updatedHeaders = new TreeMap<>(response.getHeaders()); - updatedHeaders.keySet().removeAll(initialHeaders.keySet()); - assertThat(updatedHeaders) - .containsExactly("foo", Arrays.asList("bar", "baz"), "wibbly", Arrays.asList("wobbly")); - }, - }; - for (HttpResponseTest test : tests) { - testReference.set(test); - HttpClient httpClient = new HttpClient(); - httpClient.start(); - String uri = "http://localhost:" + serverPort; - Request request = httpClient.POST(uri); - ContentResponse response = request.send(); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); - throwIfNotNull(exceptionReference.get()); - } - } - - private static class HttpResponseServlet extends HttpServlet { - private final AtomicReference testReference; - private final AtomicReference exceptionReference; - - private HttpResponseServlet( - AtomicReference testReference, - AtomicReference exceptionReference) { - this.testReference = testReference; - this.exceptionReference = exceptionReference; - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) { - try { - testReference.get().test(new HttpResponseImpl(resp)); - } catch (Throwable t) { - exceptionReference.set(t); - } - } - } - - @FunctionalInterface - private interface ResponseCheck { - void test(ContentResponse response); - } - - private static class ResponseTest { - final HttpResponseTest responseOperation; - final ResponseCheck responseCheck; - - private ResponseTest(HttpResponseTest responseOperation, ResponseCheck responseCheck) { - this.responseOperation = responseOperation; - this.responseCheck = responseCheck; - } - } - - private static ResponseTest responseTest( - HttpResponseTest responseOperation, ResponseCheck responseCheck) { - return new ResponseTest(responseOperation, responseCheck); - } - - /** - * Tests that operations on the {@link HttpResponse} have the appropriate effect on the HTTP - * response that ends up being sent. Here, for each check, we have two operations: the operation - * on the {@link HttpResponse}, which happens inside the servlet, and the operation to check the - * HTTP result, which happens in the client thread. - */ - @Test - public void httpResponseEffects() throws Exception { - AtomicReference testReference = new AtomicReference<>(); - AtomicReference exceptionReference = new AtomicReference<>(); - HttpResponseServlet testServlet = new HttpResponseServlet(testReference, exceptionReference); - try (SimpleServer server = new SimpleServer(testServlet)) { - httpResponseEffects(testReference, exceptionReference); - } - } - - private void httpResponseEffects( - AtomicReference testReference, - AtomicReference exceptionReference) - throws Exception { - ResponseTest[] tests = { - responseTest( - response -> {}, - response -> assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200)), - responseTest( - response -> response.setStatusCode(HttpStatus.OK_200), - response -> assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200)), - responseTest( - response -> response.setStatusCode(HttpStatus.IM_A_TEAPOT_418), - response -> assertThat(response.getStatus()).isEqualTo(HttpStatus.IM_A_TEAPOT_418)), - responseTest( - response -> response.setStatusCode(HttpStatus.IM_A_TEAPOT_418, "Je suis une théière"), - response -> { - assertThat(response.getStatus()).isEqualTo(HttpStatus.IM_A_TEAPOT_418); - assertThat(response.getReason()).isEqualTo("Je suis une théière"); - }), - responseTest( - response -> response.setContentType("application/noddy"), - response -> assertThat(response.getMediaType()).isEqualTo("application/noddy")), - responseTest( - response -> { - response.appendHeader("foo", "bar"); - response.appendHeader("blim", "blam"); - response.appendHeader("foo", "baz"); - }, - response -> { - assertThat(response.getHeaders().getValuesList("foo")).containsExactly("bar", "baz"); - assertThat(response.getHeaders().getValuesList("blim")).containsExactly("blam"); - }), - responseTest( - response -> { - response.setContentType("text/plain"); - try (BufferedWriter writer = response.getWriter()) { - writer.write(TEST_BODY); - } - }, - response -> { - assertThat(response.getMediaType()).isEqualTo("text/plain"); - assertThat(response.getContentAsString()).isEqualTo(TEST_BODY); - }), - responseTest( - response -> { - response.setContentType("application/octet-stream"); - try (OutputStream outputStream = response.getOutputStream()) { - outputStream.write(RANDOM_BYTES); - } - }, - response -> { - assertThat(response.getMediaType()).isEqualTo("application/octet-stream"); - assertThat(response.getContent()).isEqualTo(RANDOM_BYTES); - }), - }; - for (ResponseTest test : tests) { - testReference.set(test.responseOperation); - HttpClient httpClient = new HttpClient(); - httpClient.start(); - String uri = "http://localhost:" + serverPort; - Request request = httpClient.POST(uri); - ContentResponse response = request.send(); - throwIfNotNull(exceptionReference.get()); - test.responseCheck.test(response); - } - } - - private static void throwIfNotNull(Throwable t) throws Exception { - if (t != null) { - if (t instanceof Error) { - throw (Error) t; - } else if (t instanceof Exception) { - throw (Exception) t; - } else { - // Some kind of mutant Throwable that is neither an Exception nor an Error. - throw new AssertionError(t); - } - } - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/runner/InvokerTest.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/runner/InvokerTest.java deleted file mode 100644 index b3569e4e..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/runner/InvokerTest.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.google.cloud.functions.invoker.runner; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; -import static com.google.common.truth.Truth8.assertThat; -import static java.util.stream.Collectors.joining; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class InvokerTest { - @Test - public void help() throws IOException { - String help = - captureOutput( - () -> { - Optional invoker = Invoker.makeInvoker("--help"); - assertThat(invoker).isEmpty(); - }); - assertThat(help).contains("Usage:"); - assertThat(help).contains("--target"); - assertThat(help).containsMatch("separated\\s+by\\s+'" + File.pathSeparator + "'"); - } - - @Test - public void defaultPort() { - Optional invoker = Invoker.makeInvoker(); - assertThat(invoker.get().getPort()).isEqualTo(8080); - } - - @Test - public void explicitPort() { - Optional invoker = Invoker.makeInvoker("--port", "1234"); - assertThat(invoker.get().getPort()).isEqualTo(1234); - } - - @Test - public void defaultTarget() { - Optional invoker = Invoker.makeInvoker(); - assertThat(invoker.get().getFunctionTarget()).isEqualTo("Function"); - } - - @Test - public void explicitTarget() { - Optional invoker = Invoker.makeInvoker("--target", "com.example.MyFunction"); - assertThat(invoker.get().getFunctionTarget()).isEqualTo("com.example.MyFunction"); - } - - @Test - public void defaultSignatureType() { - Optional invoker = Invoker.makeInvoker(); - assertThat(invoker.get().getFunctionSignatureType()).isNull(); - } - - @Test - public void explicitSignatureType() { - Map env = Collections.singletonMap("FUNCTION_SIGNATURE_TYPE", "http"); - Optional invoker = Invoker.makeInvoker(env); - assertThat(invoker.get().getFunctionSignatureType()).isEqualTo("http"); - } - - @Test - public void defaultClasspath() { - Optional invoker = Invoker.makeInvoker(); - assertThat(invoker.get().getClass().getClassLoader()) - .isSameInstanceAs(Invoker.class.getClassLoader()); - } - - private static final String FAKE_CLASSPATH = - "/foo/bar/baz.jar" + File.pathSeparator + "/some/directory"; - - @Test - public void explicitClasspathViaEnvironment() { - Map env = Collections.singletonMap("FUNCTION_CLASSPATH", FAKE_CLASSPATH); - Optional invoker = Invoker.makeInvoker(env); - assertThat(invokerClasspath(invoker.get())).isEqualTo(FAKE_CLASSPATH); - } - - @Test - public void explicitClasspathViaOption() { - Optional invoker = Invoker.makeInvoker("--classpath", FAKE_CLASSPATH); - assertThat(invokerClasspath(invoker.get())).isEqualTo(FAKE_CLASSPATH); - } - - private static String invokerClasspath(Invoker invoker) { - URLClassLoader urlClassLoader = (URLClassLoader) invoker.getFunctionClassLoader(); - return Arrays.stream(urlClassLoader.getURLs()) - .map(URL::getPath) - .collect(joining(File.pathSeparator)); - } - - @Test - public void classpathToUrls() throws Exception { - String classpath = - "../testfunction/target/test-classes" + File.pathSeparator + "../testfunction/target/lib/*"; - URL[] urls = Invoker.classpathToUrls(classpath); - assertWithMessage(Arrays.toString(urls)).that(urls.length).isGreaterThan(2); - File classesDir = new File(urls[0].toURI()); - assertWithMessage(classesDir.toString()).that(classesDir.isDirectory()).isTrue(); - for (int i = 1; i < urls.length; i++) { - URL url = urls[i]; - assertThat(url.toString()).endsWith(".jar"); - assertWithMessage(url.toString()).that(new File(url.toURI()).isFile()).isTrue(); - } - } - - private static String captureOutput(Runnable operation) throws IOException { - PrintStream originalOut = System.out; - PrintStream originalErr = System.err; - ByteArrayOutputStream byteCapture = new ByteArrayOutputStream(); - try (PrintStream capture = new PrintStream(byteCapture)) { - System.setOut(capture); - System.setErr(capture); - operation.run(); - } finally { - System.setOut(originalOut); - System.setErr(originalErr); - } - return new String(byteCapture.toByteArray(), StandardCharsets.UTF_8); - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java deleted file mode 100644 index 0a6dba42..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.Context; -import com.google.cloud.functions.RawBackgroundFunction; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.UncheckedIOException; - -/** - * Extract the targetFile property from the data of the JSON payload, and write to it a JSON - * encoding of this payload and the context. The JSON format is chosen to be identical to the - * EventFlow format that we currently use in GCF, and the file that we write should in fact be - * identical to the JSON payload that the Functions Framework received from the client in the test. - * This will need to be rewritten when we switch to CloudEvents. - */ -public class BackgroundSnoop implements RawBackgroundFunction { - @Override - public void accept(String json, Context context) { - Gson gson = new Gson(); - JsonObject jsonObject = gson.fromJson(json, JsonObject.class); - String targetFile = jsonObject.get("targetFile").getAsString(); - if (targetFile == null) { - throw new IllegalArgumentException("Expected targetFile in JSON payload"); - } - JsonObject resourceJson = gson.fromJson(context.resource(), JsonObject.class); - JsonObject contextJson = new JsonObject(); - contextJson.addProperty("eventId", context.eventId()); - contextJson.addProperty("timestamp", context.timestamp()); - contextJson.addProperty("eventType", context.eventType()); - contextJson.add("resource", resourceJson); - JsonObject contextAndPayloadJson = new JsonObject(); - contextAndPayloadJson.add("data", jsonObject); - contextAndPayloadJson.add("context", contextJson); - try (FileWriter fileWriter = new FileWriter(targetFile); - PrintWriter writer = new PrintWriter(fileWriter)) { - writer.println(contextAndPayloadJson); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/CloudEventSnoop.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/CloudEventSnoop.java deleted file mode 100644 index 439e712d..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/CloudEventSnoop.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.cloud.functions.CloudEventsFunction; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import io.cloudevents.CloudEvent; -import io.cloudevents.core.format.EventFormat; -import io.cloudevents.core.provider.EventFormatProvider; -import io.cloudevents.jackson.JsonFormat; -import java.io.FileOutputStream; - -public class CloudEventSnoop implements CloudEventsFunction { - @Override - public void accept(CloudEvent event) throws Exception { - String payloadJson = new String(event.getData().toBytes(), UTF_8); - Gson gson = new Gson(); - JsonObject jsonObject = gson.fromJson(payloadJson, JsonObject.class); - String targetFile = jsonObject.get("targetFile").getAsString(); - if (targetFile == null) { - throw new IllegalArgumentException("Expected targetFile in JSON payload"); - } - EventFormat jsonFormat = - EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE); - byte[] bytes = jsonFormat.serialize(event); - try (FileOutputStream out = new FileOutputStream(targetFile)) { - out.write(bytes); - } - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java deleted file mode 100644 index 6cde3152..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpResponse; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.stream.Collectors; - -public class Echo implements HttpFunction { - @Override - public void service(HttpRequest request, HttpResponse response) throws Exception { - boolean binary = "application/octet-stream".equals(request.getContentType().orElse(null)); - if (binary) { - response.setContentType("application/octet-stream"); - byte[] buf = new byte[1024]; - InputStream in = request.getInputStream(); - OutputStream out = response.getOutputStream(); - int n; - while ((n = in.read(buf)) > 0) { - out.write(buf, 0, n); - } - } else { - String body = request.getReader().lines().collect(Collectors.joining("\n")) + "\n"; - response.setContentType("text/plain"); - response.getWriter().write(body); - } - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java deleted file mode 100644 index 7b446a59..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpResponse; - -public class EchoUrl implements HttpFunction { - @Override - public void service(HttpRequest request, HttpResponse response) throws Exception { - StringBuilder url = new StringBuilder(request.getPath()); - request.getQuery().ifPresent(q -> url.append("?").append(q)); - url.append("\n"); - response.getWriter().write(url.toString()); - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/ExceptionBackground.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/ExceptionBackground.java deleted file mode 100644 index d087b395..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/ExceptionBackground.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.Context; -import com.google.cloud.functions.RawBackgroundFunction; - -public class ExceptionBackground implements RawBackgroundFunction { - @Override - public void accept(String json, Context context) { - throw new RuntimeException("exception thrown for test"); - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/ExceptionHttp.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/ExceptionHttp.java deleted file mode 100644 index b6dee04d..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/ExceptionHttp.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpResponse; - -public class ExceptionHttp implements HttpFunction { - @Override - public void service(HttpRequest request, HttpResponse response) throws Exception { - throw new RuntimeException("exception thrown for test"); - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java deleted file mode 100644 index 2caaad40..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpResponse; - -public class HelloWorld implements HttpFunction { - @Override - public void service(HttpRequest request, HttpResponse response) throws Exception { - response.getWriter().write("hello\n"); - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Log.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Log.java deleted file mode 100644 index 4b1e94fe..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Log.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpResponse; -import java.lang.reflect.Field; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** Emit log messages with configurable level, message, and exception. */ -public class Log implements HttpFunction { - private static final Logger logger = Logger.getLogger(Log.class.getName()); - - @Override - public void service(HttpRequest request, HttpResponse response) throws Exception { - String message = request.getFirstQueryParameter("message").orElse("Default message"); - String levelString = request.getFirstQueryParameter("level").orElse("info"); - Optional exceptionString = request.getFirstQueryParameter("exception"); - Field levelField = Level.class.getField(levelString.toUpperCase()); - Level level = (Level) levelField.get(null); - if (exceptionString.isPresent()) { - logger.log(level, message, new Exception(exceptionString.get())); - } else { - logger.log(level, message); - } - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Multipart.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Multipart.java deleted file mode 100644 index 37102bbc..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Multipart.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpRequest.HttpPart; -import com.google.cloud.functions.HttpResponse; -import java.io.PrintWriter; -import java.util.NavigableMap; -import java.util.TreeMap; - -/** - * A simple proof-of-concept function for multipart handling. - * - *

{@code HttpTest} contains more detailed testing, but this function is part of the integration - * test that shows that we can indeed access the multipart API from a function. - */ -public class Multipart implements HttpFunction { - @Override - public void service(HttpRequest request, HttpResponse response) throws Exception { - response.setContentType("text/plain"); - String contentType = request.getContentType().orElse(""); - if (!contentType.startsWith("multipart/form-data")) { - response.getWriter().write("Content-Type is " + contentType + " not multipart/form-data"); - return; - } - PrintWriter writer = new PrintWriter(response.getWriter()); - NavigableMap parts = new TreeMap<>(request.getParts()); - parts.forEach( - (name, contents) -> { - writer.printf( - "part %s type %s length %d\n", - name, contents.getContentType().get(), contents.getContentLength()); - }); - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java deleted file mode 100644 index 62710891..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.HttpFunction; -import com.google.cloud.functions.HttpRequest; -import com.google.cloud.functions.HttpResponse; -import java.util.stream.Collectors; - -public class Nested { - public static class Echo implements HttpFunction { - @Override - public void service(HttpRequest request, HttpResponse response) throws Exception { - String body = request.getReader().lines().collect(Collectors.joining("\n")); - response.setContentType("text/plain"); - response.getWriter().write(body); - response.getWriter().flush(); - } - } -} diff --git a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/TypedBackgroundSnoop.java b/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/TypedBackgroundSnoop.java deleted file mode 100644 index c1e489b4..00000000 --- a/invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/TypedBackgroundSnoop.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.google.cloud.functions.invoker.testfunctions; - -import com.google.cloud.functions.BackgroundFunction; -import com.google.cloud.functions.Context; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.UncheckedIOException; - -/** - * Extract the targetFile property from the data of the JSON payload, and write to it a JSON - * encoding of this payload and the context. The JSON format is chosen to be identical to the - * EventFlow format that we currently use in GCF, and the file that we write should in fact be - * identical to the JSON payload that the Functions Framework received from the client in the test. - * This will need to be rewritten when we switch to CloudEvents. - */ -public class TypedBackgroundSnoop implements BackgroundFunction { - public static class Payload { - public int a; - public int b; - public String targetFile; - } - - @Override - public void accept(Payload payload, Context context) { - Gson gson = new Gson(); - String targetFile = payload.targetFile; - if (targetFile == null) { - throw new IllegalArgumentException("Expected targetFile in JSON payload"); - } - JsonObject resourceJson = gson.fromJson(context.resource(), JsonObject.class); - JsonObject contextJson = new JsonObject(); - contextJson.addProperty("eventId", context.eventId()); - contextJson.addProperty("timestamp", context.timestamp()); - contextJson.addProperty("eventType", context.eventType()); - contextJson.add("resource", resourceJson); - JsonObject contextAndPayloadJson = new JsonObject(); - contextAndPayloadJson.add("data", gson.toJsonTree(payload)); - contextAndPayloadJson.add("context", contextJson); - try (FileWriter fileWriter = new FileWriter(targetFile); - PrintWriter writer = new PrintWriter(fileWriter)) { - writer.println(contextAndPayloadJson); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } -} diff --git a/invoker/core/src/test/resources/adder_gcf_ga_event.json b/invoker/core/src/test/resources/adder_gcf_ga_event.json deleted file mode 100644 index 4762f613..00000000 --- a/invoker/core/src/test/resources/adder_gcf_ga_event.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "data": { - "a": 2, - "b": 3 - }, - "context": { - "eventId": "B234-1234-1234", - "timestamp": "2018-04-05T17:31:00Z", - "eventType": "com.example.someevent.new", - "resource": { - "service":"test-service", - "name":"test-name", - "type":"test-type" - } - } -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/firebase-auth-cloudevent-input.json b/invoker/core/src/test/resources/firebase-auth-cloudevent-input.json deleted file mode 100644 index 285878e9..00000000 --- a/invoker/core/src/test/resources/firebase-auth-cloudevent-input.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "specversion": "1.0", - "type": "google.firebase.auth.user.v1.created", - "source": "//firebaseauth.googleapis.com/projects/my-project-id", - "subject": "users/UUpby3s4spZre6kHsgVSPetzQ8l2", - "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "time": "2020-09-29T11:32:00.123Z", - "datacontenttype": "application/json", - "data": { - "email": "test@nowhere.com", - "metadata": { - "createTime": "2020-05-26T10:42:27Z", - "lastSignInTime": "2020-10-24T11:00:00Z" - }, - "providerData": [ - { - "email": "test@nowhere.com", - "providerId": "password", - "uid": "test@nowhere.com" - } - ], - "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" - } -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/firebase-auth-legacy-output.json b/invoker/core/src/test/resources/firebase-auth-legacy-output.json deleted file mode 100644 index cf0e572f..00000000 --- a/invoker/core/src/test/resources/firebase-auth-legacy-output.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "data": { - "email": "test@nowhere.com", - "metadata": { - "createdAt": "2020-05-26T10:42:27Z", - "lastSignedInAt": "2020-10-24T11:00:00Z" - }, - "providerData": [ - { - "email": "test@nowhere.com", - "providerId": "password", - "uid": "test@nowhere.com" - } - ], - "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" - }, - "context": { - "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "eventType": "providers/firebase.auth/eventTypes/user.create", - "resource": "projects/my-project-id", - "timestamp": "2020-09-29T11:32:00.123Z" - } -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/firebase-auth1.json b/invoker/core/src/test/resources/firebase-auth1.json deleted file mode 100644 index bb623341..00000000 --- a/invoker/core/src/test/resources/firebase-auth1.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "data": { - "email": "test@nowhere.com", - "metadata": { - "createdAt": "2020-05-26T10:42:27Z" - }, - "providerData": [ - { - "email": "test@nowhere.com", - "providerId": "password", - "uid": "test@nowhere.com" - } - ], - "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" - }, - "eventId": "4423b4fa-c39b-4f79-b338-977a018e9b55", - "eventType": "providers/firebase.auth/eventTypes/user.create", - "notSupported": { - }, - "resource": "projects/my-project-id", - "timestamp": "2020-05-26T10:42:27.088Z" -} diff --git a/invoker/core/src/test/resources/firebase-auth2.json b/invoker/core/src/test/resources/firebase-auth2.json deleted file mode 100644 index 0a702902..00000000 --- a/invoker/core/src/test/resources/firebase-auth2.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "data": { - "email": "test@nowhere.com", - "metadata": { - "createdAt": "2020-05-26T10:42:27Z" - }, - "providerData": [ - { - "email": "test@nowhere.com", - "providerId": "password", - "uid": "test@nowhere.com" - } - ], - "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" - }, - "eventId": "5fd71bdc-4955-421f-9fc3-552ac3abead8", - "eventType": "providers/firebase.auth/eventTypes/user.delete", - "notSupported": { - }, - "resource": "projects/my-project-id", - "timestamp": "2020-05-26T10:47:14.205Z" -} diff --git a/invoker/core/src/test/resources/firebase-db1-cloudevent-input.json b/invoker/core/src/test/resources/firebase-db1-cloudevent-input.json deleted file mode 100644 index 0d9783c3..00000000 --- a/invoker/core/src/test/resources/firebase-db1-cloudevent-input.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "specversion": "1.0", - "type": "google.firebase.database.ref.v1.written", - "source": "//firebasedatabase.googleapis.com/projects/_/locations/us-central1/instances/my-project-id", - "subject": "refs/gcf-test/xyz", - "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "time": "2020-09-29T11:32:00.123Z", - "datacontenttype": "application/json", - "data": { - "data": null, - "delta": { - "grandchild": "other" - } - } -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/firebase-db1-legacy-output.json b/invoker/core/src/test/resources/firebase-db1-legacy-output.json deleted file mode 100644 index 50190855..00000000 --- a/invoker/core/src/test/resources/firebase-db1-legacy-output.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": { - "data": null, - "delta": { - "grandchild": "other" - } - }, - "context": { - "resource": "projects/_/instances/my-project-id/refs/gcf-test/xyz", - "timestamp": "2020-09-29T11:32:00.123Z", - "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "eventType": "providers/google.firebase.database/eventTypes/ref.write" - } -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/firebase-db1.json b/invoker/core/src/test/resources/firebase-db1.json deleted file mode 100644 index d6d6a015..00000000 --- a/invoker/core/src/test/resources/firebase-db1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "eventType": "providers/google.firebase.database/eventTypes/ref.write", - "params": { - "child": "xyz" - }, - "auth": { - "admin": true - }, - "domain": "firebaseio.com", - "data": { - "data": null, - "delta": { - "grandchild": "other" - } - }, - "resource": "projects/_/instances/my-project-id/refs/gcf-test/xyz", - "timestamp": "2020-05-21T11:15:34.178Z", - "eventId": "/SnHth9OSlzK1Puj85kk4tDbF90=" -} diff --git a/invoker/core/src/test/resources/firebase-db2-cloudevent-input.json b/invoker/core/src/test/resources/firebase-db2-cloudevent-input.json deleted file mode 100644 index 71974d42..00000000 --- a/invoker/core/src/test/resources/firebase-db2-cloudevent-input.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "specversion": "1.0", - "type": "google.firebase.database.ref.v1.written", - "source": "//firebasedatabase.googleapis.com/projects/_/locations/europe-west1/instances/my-project-id", - "subject": "refs/gcf-test/xyz", - "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "time": "2020-09-29T11:32:00.123Z", - "datacontenttype": "application/json", - "data": { - "data": { - "grandchild": "other" - }, - "delta": { - "grandchild": "other changed" - } - } -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/firebase-db2-legacy-output.json b/invoker/core/src/test/resources/firebase-db2-legacy-output.json deleted file mode 100644 index 402868fa..00000000 --- a/invoker/core/src/test/resources/firebase-db2-legacy-output.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "data": { - "data": { - "grandchild": "other" - }, - "delta": { - "grandchild": "other changed" - } - }, - "context": { - "resource": "projects/_/instances/my-project-id/refs/gcf-test/xyz", - "timestamp": "2020-09-29T11:32:00.123Z", - "eventType": "providers/google.firebase.database/eventTypes/ref.write", - "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc" - } -} diff --git a/invoker/core/src/test/resources/firebase-db2.json b/invoker/core/src/test/resources/firebase-db2.json deleted file mode 100644 index 371ea00e..00000000 --- a/invoker/core/src/test/resources/firebase-db2.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "eventType": "providers/google.firebase.database/eventTypes/ref.write", - "params": { - "child": "xyz" - }, - "auth": { - "admin": true - }, - "domain":"europe-west1.firebasedatabase.app", - "data": { - "data": { - "grandchild": "other" - }, - "delta": { - "grandchild": "other changed" - } - }, - "resource": "projects/_/instances/my-project-id/refs/gcf-test/xyz", - "timestamp": "2020-09-29T11:32:00.000Z", - "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc" -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/firestore_complex-cloudevent-input.json b/invoker/core/src/test/resources/firestore_complex-cloudevent-input.json deleted file mode 100644 index fd5aadfb..00000000 --- a/invoker/core/src/test/resources/firestore_complex-cloudevent-input.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "specversion": "1.0", - "type": "google.cloud.firestore.document.v1.written", - "source": "//firestore.googleapis.com/projects/project-id/databases/(default)", - "subject": "documents/gcf-test/IH75dRdeYJKd4uuQiqch", - "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "time": "2020-09-29T11:32:00.123Z", - "datacontenttype": "application/json", - "data": { - "oldValue": {}, - "updateMask": {}, - "value": { - "createTime": "2020-04-23T14:25:05.349632Z", - "fields": { - "arrayValue": { - "arrayValue": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - } - ] - } - }, - "booleanValue": { - "booleanValue": true - }, - "doubleValue": { - "doubleValue": 5.5 - }, - "geoPointValue": { - "geoPointValue": { - "latitude": 51.4543, - "longitude": -0.9781 - } - }, - "intValue": { - "integerValue": "50" - }, - "mapValue": { - "mapValue": { - "fields": { - "field1": { - "stringValue": "x" - }, - "field2": { - "arrayValue": { - "values": [ - { - "stringValue": "x" - }, - { - "integerValue": "1" - } - ] - } - } - } - } - }, - "nullValue": { - "nullValue": null - }, - "referenceValue": { - "referenceValue": "projects/project-id/databases/(default)/documents/foo/bar/baz/qux" - }, - "stringValue": { - "stringValue": "text" - }, - "timestampValue": { - "timestampValue": "2020-04-23T14:23:53.241Z" - } - }, - "name": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", - "updateTime": "2020-04-23T14:25:05.349632Z" - } - } -} diff --git a/invoker/core/src/test/resources/firestore_complex-legacy-output.json b/invoker/core/src/test/resources/firestore_complex-legacy-output.json deleted file mode 100644 index dc7a26b4..00000000 --- a/invoker/core/src/test/resources/firestore_complex-legacy-output.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "data": { - "oldValue": {}, - "updateMask": {}, - "value": { - "createTime": "2020-04-23T14:25:05.349632Z", - "fields": { - "arrayValue": { - "arrayValue": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - } - ] - } - }, - "booleanValue": { - "booleanValue": true - }, - "doubleValue": { - "doubleValue": 5.5 - }, - "geoPointValue": { - "geoPointValue": { - "latitude": 51.4543, - "longitude": -0.9781 - } - }, - "intValue": { - "integerValue": "50" - }, - "mapValue": { - "mapValue": { - "fields": { - "field1": { - "stringValue": "x" - }, - "field2": { - "arrayValue": { - "values": [ - { - "stringValue": "x" - }, - { - "integerValue": "1" - } - ] - } - } - } - } - }, - "nullValue": { - "nullValue": null - }, - "referenceValue": { - "referenceValue": "projects/project-id/databases/(default)/documents/foo/bar/baz/qux" - }, - "stringValue": { - "stringValue": "text" - }, - "timestampValue": { - "timestampValue": "2020-04-23T14:23:53.241Z" - } - }, - "name": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", - "updateTime": "2020-04-23T14:25:05.349632Z" - } - }, - "context": { - "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "eventType": "providers/cloud.firestore/eventTypes/document.write", - "resource": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", - "timestamp": "2020-09-29T11:32:00.123Z" - } -} diff --git a/invoker/core/src/test/resources/firestore_complex.json b/invoker/core/src/test/resources/firestore_complex.json deleted file mode 100644 index 231e2a22..00000000 --- a/invoker/core/src/test/resources/firestore_complex.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "data": { - "oldValue": {}, - "updateMask": {}, - "value": { - "createTime": "2020-04-23T14:25:05.349632Z", - "fields": { - "arrayValue": { - "arrayValue": { - "values": [ - { - "integerValue": "1" - }, - { - "integerValue": "2" - } - ] - } - }, - "booleanValue": { - "booleanValue": true - }, - "doubleValue": { - "doubleValue": 5.5 - }, - "geoPointValue": { - "geoPointValue": { - "latitude": 51.4543, - "longitude": -0.9781 - } - }, - "intValue": { - "integerValue": "50" - }, - "mapValue": { - "mapValue": { - "fields": { - "field1": { - "stringValue": "x" - }, - "field2": { - "arrayValue": { - "values": [ - { - "stringValue": "x" - }, - { - "integerValue": "1" - } - ] - } - } - } - } - }, - "nullValue": { - "nullValue": null - }, - "referenceValue": { - "referenceValue": "projects/project-id/databases/(default)/documents/foo/bar/baz/qux" - }, - "stringValue": { - "stringValue": "text" - }, - "timestampValue": { - "timestampValue": "2020-04-23T14:23:53.241Z" - } - }, - "name": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", - "updateTime": "2020-04-23T14:25:05.349632Z" - } - }, - "eventId": "9babded5-e5f2-41af-a46a-06ba6bd84739-0", - "eventType": "providers/cloud.firestore/eventTypes/document.write", - "notSupported": {}, - "params": { - "doc": "IH75dRdeYJKd4uuQiqch" - }, - "resource": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", - "timestamp": "2020-04-23T14:25:05.349632Z" -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/firestore_simple.json b/invoker/core/src/test/resources/firestore_simple.json deleted file mode 100644 index 14d7de48..00000000 --- a/invoker/core/src/test/resources/firestore_simple.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "data":{ - "oldValue":{ - "createTime":"2020-04-23T09:58:53.211035Z", - "fields":{ - "another test":{ - "stringValue":"asd" - }, - "count":{ - "integerValue":"3" - }, - "foo":{ - "stringValue":"bar" - } - }, - "name":"projects/project-id/databases/(default)/documents/gcf-test/2Vm2mI1d0wIaK2Waj5to", - "updateTime":"2020-04-23T12:00:27.247187Z" - }, - "updateMask":{ - "fieldPaths":[ - "count" - ] - }, - "value":{ - "createTime":"2020-04-23T09:58:53.211035Z", - "fields":{ - "another test":{ - "stringValue":"asd" - }, - "count":{ - "integerValue":"4" - }, - "foo":{ - "stringValue":"bar" - } - }, - "name":"projects/project-id/databases/(default)/documents/gcf-test/2Vm2mI1d0wIaK2Waj5to", - "updateTime":"2020-04-23T12:00:27.247187Z" - } - }, - "eventId":"7b8f1804-d38b-4b68-b37d-e2fb5d12d5a0-0", - "eventType":"providers/cloud.firestore/eventTypes/document.write", - "notSupported":{ - - }, - "params":{ - "doc":"2Vm2mI1d0wIaK2Waj5to" - }, - "resource":"projects/project-id/databases/(default)/documents/gcf-test/2Vm2mI1d0wIaK2Waj5to", - "timestamp":"2020-04-23T12:00:27.247187Z" -} diff --git a/invoker/core/src/test/resources/legacy_pubsub.json b/invoker/core/src/test/resources/legacy_pubsub.json deleted file mode 100644 index b03bfd59..00000000 --- a/invoker/core/src/test/resources/legacy_pubsub.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "eventId": "1215011316659232", - "timestamp": "2020-05-18T12:13:19.209Z", - "eventType": "providers/cloud.pubsub/eventTypes/topic.publish", - "resource": "projects/sample-project/topics/gcf-test", - "data": { - "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", - "attributes": { - "attribute1": "value1" - }, - "data": "VGhpcyBpcyBhIHNhbXBsZSBtZXNzYWdl" - } -} diff --git a/invoker/core/src/test/resources/legacy_storage_change.json b/invoker/core/src/test/resources/legacy_storage_change.json deleted file mode 100644 index 8a1e92ed..00000000 --- a/invoker/core/src/test/resources/legacy_storage_change.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "data": { - "bucket": "sample-bucket", - "crc32c": "AAAAAA==", - "etag": "COu8mb3Dn+kCEAE=", - "generation": "1588778055917163", - "id": "sample-bucket/MyFile/1588778055917163", - "kind": "storage#object", - "md5Hash": "ZDQxZDhjZDk4ZjAwYjIwNGU5ODAwOTk4ZWNmODQyN2U=", - "mediaLink": "https://www.googleapis.com/download/storage/v1/b/projectid-sample-bucket/o/MyFile?generation=1588778055917163\u0026alt=media", - "metageneration": "1", - "name": "MyFile", - "resourceState": "not_exists", - "selfLink": "https://www.googleapis.com/storage/v1/b/projectid-sample-bucket/o/MyFile", - "size": "0", - "storageClass": "MULTI_REGIONAL", - "timeCreated": "2020-05-06T15:14:15.917Z", - "timeDeleted": "2020-05-18T09:07:51.799Z", - "timeStorageClassUpdated": "2020-05-06T15:14:15.917Z", - "updated": "2020-05-06T15:14:15.917Z" - }, - "eventId": "1200401551653202", - "eventType": "providers/cloud.storage/eventTypes/object.change", - "resource": "projects/_/buckets/sample-bucket/objects/MyFile#1588778055917163", - "timestamp": "2020-05-18T09:07:51.799Z" -} diff --git a/invoker/core/src/test/resources/pubsub_background.json b/invoker/core/src/test/resources/pubsub_background.json deleted file mode 100644 index 5f9927cb..00000000 --- a/invoker/core/src/test/resources/pubsub_background.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "data": { - "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", - "data": "eyJmb28iOiJiYXIifQ==", - "attributes": { - "test": "123" - } - }, - "context": { - "eventId": "1", - "eventType": "google.pubsub.topic.publish", - "resource": { - "name": "projects/FOO/topics/BAR_TOPIC", - "service": "pubsub.googleapis.com", - "type": "type.googleapis.com/google.pubsub.v1.PubsubMessage" - }, - "timestamp": "2021-06-28T05:46:32.390Z" - } -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/pubsub_binary.json b/invoker/core/src/test/resources/pubsub_binary.json deleted file mode 100644 index d7cbb125..00000000 --- a/invoker/core/src/test/resources/pubsub_binary.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "context": { - "eventId":"1144231683168617", - "timestamp":"2020-05-06T07:33:34.556Z", - "eventType":"google.pubsub.topic.publish", - "resource":{ - "service":"pubsub.googleapis.com", - "name":"projects/sample-project/topics/gcf-test", - "type":"type.googleapis.com/google.pubsub.v1.PubsubMessage" - } - }, - "data": { - "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", - "data": "AQIDBA==" - } -} diff --git a/invoker/core/src/test/resources/pubsub_emulator.json b/invoker/core/src/test/resources/pubsub_emulator.json deleted file mode 100644 index cdfe340a..00000000 --- a/invoker/core/src/test/resources/pubsub_emulator.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "subscription": "projects/FOO/subscriptions/BAR_SUB", - "message": { - "data": "eyJmb28iOiJiYXIifQ==", - "messageId": "1", - "attributes": { - "test": "123" - } - } -} \ No newline at end of file diff --git a/invoker/core/src/test/resources/pubsub_text-cloudevent-input.json b/invoker/core/src/test/resources/pubsub_text-cloudevent-input.json deleted file mode 100644 index f4994ee8..00000000 --- a/invoker/core/src/test/resources/pubsub_text-cloudevent-input.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "specversion": "1.0", - "type": "google.cloud.pubsub.topic.v1.messagePublished", - "source": "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test", - "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "time": "2020-09-29T11:32:00.123Z", - "datacontenttype": "application/json", - "data": { - "subscription": "projects/sample-project/subscriptions/sample-subscription", - "message": { - "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", - "messageId": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "publishTime": "2020-09-29T11:32:00.123Z", - "attributes": { - "attr1":"attr1-value" - }, - "data": "dGVzdCBtZXNzYWdlIDM=" - } - } -} diff --git a/invoker/core/src/test/resources/pubsub_text-legacy-output.json b/invoker/core/src/test/resources/pubsub_text-legacy-output.json deleted file mode 100644 index e1b8b8f9..00000000 --- a/invoker/core/src/test/resources/pubsub_text-legacy-output.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "context": { - "eventId":"aaaaaa-1111-bbbb-2222-cccccccccccc", - "timestamp":"2020-09-29T11:32:00.123Z", - "eventType":"google.pubsub.topic.publish", - "resource":{ - "service":"pubsub.googleapis.com", - "name":"projects/sample-project/topics/gcf-test", - "type":"type.googleapis.com/google.pubsub.v1.PubsubMessage" - } - }, - "data": { - "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", - "attributes": { - "attr1":"attr1-value" - }, - "data": "dGVzdCBtZXNzYWdlIDM=" - } -} diff --git a/invoker/core/src/test/resources/pubsub_text.json b/invoker/core/src/test/resources/pubsub_text.json deleted file mode 100644 index 9d7ed53c..00000000 --- a/invoker/core/src/test/resources/pubsub_text.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "context": { - "eventId":"1144231683168617", - "timestamp":"2020-05-06T07:33:34.556Z", - "eventType":"google.pubsub.topic.publish", - "resource":{ - "service":"pubsub.googleapis.com", - "name":"projects/sample-project/topics/gcf-test", - "type":"type.googleapis.com/google.pubsub.v1.PubsubMessage" - } - }, - "data": { - "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", - "attributes": { - "attr1":"attr1-value" - }, - "data": "dGVzdCBtZXNzYWdlIDM=" - } -} diff --git a/invoker/core/src/test/resources/storage-cloudevent-input.json b/invoker/core/src/test/resources/storage-cloudevent-input.json deleted file mode 100644 index 2948b99f..00000000 --- a/invoker/core/src/test/resources/storage-cloudevent-input.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "specversion": "1.0", - "type": "google.cloud.storage.object.v1.finalized", - "source": "//storage.googleapis.com/projects/_/buckets/some-bucket", - "subject": "objects/folder/Test.cs", - "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "time": "2020-09-29T11:32:00.123Z", - "datacontenttype": "application/json", - "data": { - "bucket": "some-bucket", - "contentType": "text/plain", - "crc32c": "rTVTeQ==", - "etag": "CNHZkbuF/ugCEAE=", - "generation": "1587627537231057", - "id": "some-bucket/folder/Test.cs/1587627537231057", - "kind": "storage#object", - "md5Hash": "kF8MuJ5+CTJxvyhHS1xzRg==", - "mediaLink": "https://www.googleapis.com/download/storage/v1/b/some-bucket/o/folder%2FTest.cs?generation=1587627537231057\u0026alt=media", - "metageneration": "1", - "name": "folder/Test.cs", - "selfLink": "https://www.googleapis.com/storage/v1/b/some-bucket/o/folder/Test.cs", - "size": "352", - "storageClass": "MULTI_REGIONAL", - "timeCreated": "2020-04-23T07:38:57.230Z", - "timeStorageClassUpdated": "2020-04-23T07:38:57.230Z", - "updated": "2020-04-23T07:38:57.230Z" - } -} diff --git a/invoker/core/src/test/resources/storage-legacy-output.json b/invoker/core/src/test/resources/storage-legacy-output.json deleted file mode 100644 index 88a9fd57..00000000 --- a/invoker/core/src/test/resources/storage-legacy-output.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "context": { - "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc", - "timestamp": "2020-09-29T11:32:00.123Z", - "eventType": "google.storage.object.finalize", - "resource": { - "service": "storage.googleapis.com", - "name": "projects/_/buckets/some-bucket/objects/folder/Test.cs", - "type": "storage#object" - } - }, - "data": { - "bucket": "some-bucket", - "contentType": "text/plain", - "crc32c": "rTVTeQ==", - "etag": "CNHZkbuF/ugCEAE=", - "generation": "1587627537231057", - "id": "some-bucket/folder/Test.cs/1587627537231057", - "kind": "storage#object", - "md5Hash": "kF8MuJ5+CTJxvyhHS1xzRg==", - "mediaLink": "https://www.googleapis.com/download/storage/v1/b/some-bucket/o/folder%2FTest.cs?generation=1587627537231057\u0026alt=media", - "metageneration": "1", - "name": "folder/Test.cs", - "selfLink": "https://www.googleapis.com/storage/v1/b/some-bucket/o/folder/Test.cs", - "size": "352", - "storageClass": "MULTI_REGIONAL", - "timeCreated": "2020-04-23T07:38:57.230Z", - "timeStorageClassUpdated": "2020-04-23T07:38:57.230Z", - "updated": "2020-04-23T07:38:57.230Z" - } -} diff --git a/invoker/core/src/test/resources/storage.json b/invoker/core/src/test/resources/storage.json deleted file mode 100644 index baf92557..00000000 --- a/invoker/core/src/test/resources/storage.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "context": { - "eventId": "1147091835525187", - "timestamp": "2020-04-23T07:38:57.772Z", - "eventType": "google.storage.object.finalize", - "resource": { - "service": "storage.googleapis.com", - "name": "projects/_/buckets/some-bucket/objects/folder/Test.cs", - "type": "storage#object" - } - }, - "data": { - "bucket": "some-bucket", - "contentType": "text/plain", - "crc32c": "rTVTeQ==", - "etag": "CNHZkbuF/ugCEAE=", - "generation": "1587627537231057", - "id": "some-bucket/folder/Test.cs/1587627537231057", - "kind": "storage#object", - "md5Hash": "kF8MuJ5+CTJxvyhHS1xzRg==", - "mediaLink": "https://www.googleapis.com/download/storage/v1/b/some-bucket/o/folder%2FTest.cs?generation=1587627537231057\u0026alt=media", - "metageneration": "1", - "name": "folder/Test.cs", - "selfLink": "https://www.googleapis.com/storage/v1/b/some-bucket/o/folder/Test.cs", - "size": "352", - "storageClass": "MULTI_REGIONAL", - "timeCreated": "2020-04-23T07:38:57.230Z", - "timeStorageClassUpdated": "2020-04-23T07:38:57.230Z", - "updated": "2020-04-23T07:38:57.230Z" - } -} diff --git a/invoker/function-maven-plugin/pom.xml b/invoker/function-maven-plugin/pom.xml deleted file mode 100644 index bb3ee58a..00000000 --- a/invoker/function-maven-plugin/pom.xml +++ /dev/null @@ -1,88 +0,0 @@ - - 4.0.0 - - - com.google.cloud.functions.invoker - java-function-invoker-parent - 1.1.1-SNAPSHOT - - - com.google.cloud.functions - function-maven-plugin - maven-plugin - 0.10.1-SNAPSHOT - Functions Framework Plugin - A Maven plugin that allows functions to be deployed, and to be run locally - using the Java Functions Framework. - http://maven.apache.org - - - 8 - 8 - 8 - - - - - - org.apache.maven - maven-plugin-api - 3.6.3 - - - org.apache.maven - maven-core - 3.6.3 - - - org.apache.maven.plugin-tools - maven-plugin-annotations - 3.6.0 - provided - - - - com.google.cloud.functions.invoker - java-function-invoker - 1.1.1-SNAPSHOT - - - - com.google.cloud.tools - appengine-maven-plugin - 2.4.1 - jar - - - - com.google.truth - truth - 1.0.1 - test - - - junit - junit - 4.13.1 - test - - - - - - - org.apache.maven.plugins - maven-plugin-plugin - 3.6.0 - - - help-goal - - helpmojo - - - - - - - diff --git a/invoker/function-maven-plugin/src/main/java/com/google/cloud/functions/plugin/DeployFunction.java b/invoker/function-maven-plugin/src/main/java/com/google/cloud/functions/plugin/DeployFunction.java deleted file mode 100644 index 509ccf3e..00000000 --- a/invoker/function-maven-plugin/src/main/java/com/google/cloud/functions/plugin/DeployFunction.java +++ /dev/null @@ -1,376 +0,0 @@ -package com.google.cloud.functions.plugin; - -import com.google.cloud.tools.appengine.operations.CloudSdk; -import com.google.cloud.tools.appengine.operations.Gcloud; -import com.google.cloud.tools.appengine.operations.cloudsdk.CloudSdkNotFoundException; -import com.google.cloud.tools.appengine.operations.cloudsdk.CloudSdkOutOfDateException; -import com.google.cloud.tools.appengine.operations.cloudsdk.CloudSdkVersionFileException; -import com.google.cloud.tools.appengine.operations.cloudsdk.process.ProcessHandlerException; -import com.google.cloud.tools.managedcloudsdk.BadCloudSdkVersionException; -import com.google.cloud.tools.managedcloudsdk.ManagedCloudSdk; -import com.google.cloud.tools.managedcloudsdk.UnsupportedOsException; -import com.google.cloud.tools.managedcloudsdk.Version; -import com.google.cloud.tools.maven.cloudsdk.CloudSdkChecker; -import com.google.cloud.tools.maven.cloudsdk.CloudSdkDownloader; -import com.google.cloud.tools.maven.cloudsdk.CloudSdkMojo; -import com.google.common.base.Joiner; -import com.google.common.base.Strings; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugins.annotations.Execute; -import org.apache.maven.plugins.annotations.LifecyclePhase; -import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.plugins.annotations.ResolutionScope; - -/** Deploy a Java function via mvn functions:deploy with optional flags. */ -@Mojo( - name = "deploy", - defaultPhase = LifecyclePhase.GENERATE_RESOURCES, - requiresDependencyResolution = ResolutionScope.NONE, - requiresDependencyCollection = ResolutionScope.NONE) -@Execute(phase = LifecyclePhase.NONE) -public class DeployFunction extends CloudSdkMojo { - - /** The Google Cloud Platform project Id to use for this invocation. */ - @Parameter(alias = "deploy.projectId", property = "function.deploy.projectId") - protected String projectId; - - /** - * ID of the function or fully qualified identifier for the function. This property must be - * specified if any of the other arguments in this group are specified. - */ - @Parameter(alias = "deploy.name", property = "function.deploy.name", required = true) - String name; - - /** - * The Cloud region for the function. Overrides the default functions/region property value for - * this command invocation. - */ - @Parameter(alias = "deploy.region", property = "function.deploy.region") - String region; - - /** - * If set, makes this a public function. This will allow all callers, without checking - * authentication. - */ - @Parameter( - alias = "deploy.allowunauthenticated", - property = "function.deploy.allowunauthenticated", - defaultValue = "false") - boolean allowUnauthenticated; - - /** - * Name of a Google Cloud Function (as defined in source code) that will be executed. Defaults to - * the resource name suffix, if not specified. - * - *

For Java this is fully qualified class name implementing the function, for example - * `com.google.testfunction.HelloWorld`. - */ - @Parameter(alias = "deploy.functiontarget", property = "function.deploy.functiontarget") - String functionTarget; - - /** Override the .gcloudignore file and use the specified file instead. */ - @Parameter(alias = "deploy.ignorefile", property = "function.deploy.ignorefile") - String ignoreFile; - - /** - * Limit on the amount of memory the function can use. - * - *

Allowed values are: 128MB, 256MB, 512MB, 1024MB, and 2048MB. By default, a new function is - * limited to 256MB of memory. When deploying an update to an existing function, the function will - * keep its old memory limit unless you specify this flag. - */ - @Parameter(alias = "deploy.memory", property = "function.deploy.memory") - String memory; - - /** If specified, then the function will be retried in case of a failure. */ - @Parameter(alias = "deploy.retry", property = "function.deploy.retry") - String retry; - - /** - * Runtime in which to run the function. - * - *

Required when deploying a new function; optional when updating an existing function. Default - * to Java11. - */ - @Parameter( - alias = "deploy.runtime", - defaultValue = "java11", - property = "function.deploy.runtime") - String runtime = "java11"; - - /** - * The email address of the IAM service account associated with the function at runtime. The - * service account represents the identity of the running function, and determines what - * permissions the function has. - * - *

If not provided, the function will use the project's default service account. - */ - @Parameter(alias = "deploy.serviceaccount", property = "function.deploy.serviceaccount") - String serviceAccount; - - /** Location of source code to deploy. */ - @Parameter(alias = "deploy.source", property = "function.deploy.source") - String source; - - /** - * This flag's value is the name of the Google Cloud Storage bucket in which source code will be - * stored. - */ - @Parameter(alias = "deploy.stagebucket", property = "function.deploy.stagebucket") - String stageBucket; - - /** - * The function execution timeout, e.g. 30s for 30 seconds. Defaults to original value for - * existing function or 60 seconds for new functions. Cannot be more than 540s. - */ - @Parameter(alias = "deploy.timeout", property = "function.deploy.timeout") - String timeout; - - /** - * List of label KEY=VALUE pairs to update. If a label exists its value is modified, otherwise a - * new label is created. - */ - @Parameter(alias = "deploy.updatelabels", property = "function.deploy.updatelabels") - List updateLabels; - - /** - * Function will be assigned an endpoint, which you can view by using the describe command. Any - * HTTP request (of a supported type) to the endpoint will trigger function execution. Supported - * HTTP request types are: POST, PUT, GET, DELETE, and OPTIONS. - */ - @Parameter(alias = "deploy.triggerhttp", property = "function.deploy.triggerhttp") - Boolean triggerHttp; - - /** - * Name of Pub/Sub topic. Every message published in this topic will trigger function execution - * with message contents passed as input data. - */ - @Parameter(alias = "deploy.triggertopic", property = "function.deploy.triggertopic") - String triggerTopic; - - /** - * Specifies which action should trigger the function. For a list of acceptable values, call - * gcloud functions event-types list. - */ - @Parameter(alias = "deploy.triggerevent", property = "function.deploy.triggerevent") - String triggerEvent; - - /** - * Specifies which resource from {@link #triggerEvent} is being observed. E.g. if {@link - * #triggerEvent} is providers/cloud.storage/eventTypes/object.change, {@link #triggerResource} - * must be a bucket name. For a list of expected resources, run {@code gcloud functions - * event-types list}. - */ - @Parameter(alias = "deploy.triggerresource", property = "function.deploy.triggerresource") - String triggerResource; - - /** - * The VPC Access connector that the function can connect to. It can be either the fully-qualified - * URI, or the short name of the VPC Access connector resource. If the short name is used, the - * connector must belong to the same project. The format of this field is either - * projects/${PROJECT}/locations/${LOCATION}/connectors/${CONNECTOR} or ${CONNECTOR}, where - * ${CONNECTOR} is the short name of the VPC Access connector. - */ - @Parameter(alias = "deploy.vpcconnector", property = "function.deploy.vpcconnector") - String vpcConnector; - /** - * Sets the maximum number of instances for the function. A function execution that would exceed - * max-instances times out. - */ - @Parameter(alias = "deploy.maxinstances", property = "function.deploy.maxinstances") - Integer maxInstances; - /** - * List of key-value pairs to set as environment variables. All existing environment variables - * will be removed first. - */ - @Parameter(alias = "deploy.setenvvars", property = "function.deploy.setenvvars") - Map environmentVariables; - /** - * Path to a local YAML file with definitions for all environment variables. All existing - * environment variables will be removed before the new environment variables are added. - */ - @Parameter(alias = "deploy.envvarsfile", property = "function.deploy.envvarsfile") - String envVarsFile; - /** - * List of key-value pairs to set as build environment variables. All existing environment - * variables will be removed first. - */ - @Parameter(alias = "deploy.setbuildenvvars", property = "function.deploy.setbuildenvvars") - Map buildEnvironmentVariables; - /** - * Path to a local YAML file with definitions for all build environment variables. All existing - * environment variables will be removed before the new environment variables are added. - */ - @Parameter(alias = "deploy.buildenvvarsfile", property = "function.deploy.buildenvvarsfile") - String buildEnvVarsFile; - - boolean hasEnvVariables() { - return (this.environmentVariables != null && !this.environmentVariables.isEmpty()); - } - - boolean hasBuildEnvVariables() { - return (this.buildEnvironmentVariables != null && !this.buildEnvironmentVariables.isEmpty()); - } - - // Select a downloaded Cloud SDK or a user defined Cloud SDK version. - static Function newManagedSdkFactory() { - return version -> { - try { - if (Strings.isNullOrEmpty(version)) { - return ManagedCloudSdk.newManagedSdk(); - } else { - return ManagedCloudSdk.newManagedSdk(new Version(version)); - } - } catch (UnsupportedOsException | BadCloudSdkVersionException ex) { - throw new RuntimeException(ex); - } - }; - } - - CloudSdk buildCloudSdkMinimal() { - return buildCloudSdk( - (CloudSdkMojo) this, new CloudSdkChecker(), new CloudSdkDownloader(newManagedSdkFactory())); - } - - static CloudSdk buildCloudSdk( - CloudSdkMojo mojo, CloudSdkChecker cloudSdkChecker, CloudSdkDownloader cloudSdkDownloader) { - - try { - if (mojo.getCloudSdkHome() != null) { - // Check if the user has defined a specific Cloud SDK. - CloudSdk cloudSdk = new CloudSdk.Builder().sdkPath(mojo.getCloudSdkHome()).build(); - - if (mojo.getCloudSdkVersion() != null) { - cloudSdkChecker.checkCloudSdk(cloudSdk, mojo.getCloudSdkVersion()); - } - - return cloudSdk; - } else { - - return new CloudSdk.Builder() - .sdkPath( - cloudSdkDownloader.downloadIfNecessary( - mojo.getCloudSdkVersion(), - mojo.getLog(), - Collections.emptyList(), - mojo.getMavenSession().isOffline())) - .build(); - } - } catch (CloudSdkNotFoundException - | CloudSdkOutOfDateException - | CloudSdkVersionFileException ex) { - throw new RuntimeException(ex); - } - } - - /** Return a Gcloud instance using global configuration. */ - public Gcloud getGcloud() { - return Gcloud.builder(buildCloudSdkMinimal()) - .setMetricsEnvironment(this.getArtifactId(), this.getArtifactVersion()) - .setCredentialFile(this.getServiceAccountKeyFile()) - .build(); - } - - /** Return the list of command parameters to give to the Cloud SDK for execution */ - public List getCommands() { - List commands = new ArrayList<>(); - - commands.add("functions"); - commands.add("deploy"); - commands.add(name); - if (region != null) { - commands.add("--region=" + region); - } - if (triggerResource == null && triggerTopic == null && triggerEvent == null) { - commands.add("--trigger-http"); - } - if (triggerResource != null) { - commands.add("--trigger-resource=" + triggerResource); - } - if (triggerTopic != null) { - commands.add("--trigger-topic=" + triggerTopic); - } - if (triggerEvent != null) { - commands.add("--trigger-event=" + triggerEvent); - } - if (allowUnauthenticated) { - commands.add("--allow-unauthenticated"); - } - if (functionTarget != null) { - commands.add("--entry-point=" + functionTarget); - } - if (ignoreFile != null) { - commands.add("--ignore-file=" + ignoreFile); - } - if (memory != null) { - commands.add("--memory=" + memory); - } - if (retry != null) { - commands.add("--retry=" + retry); - } - if (serviceAccount != null) { - commands.add("--service-account=" + serviceAccount); - } - if (source != null) { - commands.add("--source=" + source); - } - if (stageBucket != null) { - commands.add("--stage-bucket=" + stageBucket); - } - if (timeout != null) { - commands.add("--timeout=" + timeout); - } - if (updateLabels != null && !updateLabels.isEmpty()) { - commands.add("--update-labels=" + String.join(",", updateLabels)); - } - - if (vpcConnector != null) { - commands.add("--vpc-connector=" + vpcConnector); - } - if (maxInstances != null) { - commands.add("--max-instances=" + maxInstances); - } - - if (hasEnvVariables()) { - Joiner.MapJoiner mapJoiner = Joiner.on(",").withKeyValueSeparator("="); - commands.add("--set-env-vars=" + mapJoiner.join(environmentVariables)); - } - if (envVarsFile != null) { - commands.add("--env-vars-file=" + envVarsFile); - } - if (hasBuildEnvVariables()) { - Joiner.MapJoiner mapJoiner = Joiner.on(",").withKeyValueSeparator("="); - commands.add("--set-build-env-vars=" + mapJoiner.join(buildEnvironmentVariables)); - } - if (buildEnvVarsFile != null) { - commands.add("--build-env-vars-file=" + buildEnvVarsFile); - } - commands.add("--runtime=" + runtime); - - if (projectId != null) { - commands.add("--project=" + projectId); - } - return Collections.unmodifiableList(commands); - } - - @Override - public void execute() throws MojoExecutionException { - try { - Gcloud gcloud = getGcloud(); - List params = getCommands(); - System.out.println("Executing Cloud SDK command: gcloud " + String.join(" ", params)); - gcloud.runCommand(params); - } catch (CloudSdkNotFoundException | IOException | ProcessHandlerException ex) { - Logger.getLogger(DeployFunction.class.getName()).log(Level.SEVERE, null, ex); - } - } -} diff --git a/invoker/function-maven-plugin/src/main/java/com/google/cloud/functions/plugin/RunFunction.java b/invoker/function-maven-plugin/src/main/java/com/google/cloud/functions/plugin/RunFunction.java deleted file mode 100644 index c79fcd05..00000000 --- a/invoker/function-maven-plugin/src/main/java/com/google/cloud/functions/plugin/RunFunction.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.google.cloud.functions.plugin; - -import com.google.cloud.functions.invoker.runner.Invoker; -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.apache.maven.plugin.AbstractMojo; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugins.annotations.Execute; -import org.apache.maven.plugins.annotations.LifecyclePhase; -import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.plugins.annotations.ResolutionScope; - -/** - * Runs a function using the Java Functions Framework. Typically this plugin is configured in one of - * two ways. Either in the pom.xml file, like this... - * - *

{@code
- * 
- *   com.google.cloud.functions
- *   function-maven-plugin
- *   1.0.0-alpha-2-rc3
- *   
- *     com.example.function.Echo
- *   
- * 
- * }
- * - * ...and then run using {@code mvn function:run}. Or using properties on the command line, like - * this...
- * - *
{@code
- * mvn com.google.cloud.functions:function:1.0.0-alpha-2-rc3:run \
- *     -Drun.functionTarget=com.example.function.Echo
- * }
- */ -@Mojo( - name = "run", - defaultPhase = LifecyclePhase.GENERATE_RESOURCES, - requiresDependencyResolution = ResolutionScope.RUNTIME, - requiresDependencyCollection = ResolutionScope.RUNTIME) -@Execute(phase = LifecyclePhase.COMPILE) -public class RunFunction extends AbstractMojo { - - /** - * The name of the function to run. This is the name of a class that implements one of the - * interfaces in {@code com.google.cloud.functions}. - */ - @Parameter(property = "run.functionTarget") - private String functionTarget; - - /** The port on which the HTTP server wrapping the function should listen. */ - @Parameter(property = "run.port", defaultValue = "8080") - private Integer port; - - /** - * Used to determine what classpath needs to be used to load the function. This parameter is - * injected by Maven and can't be set explicitly in a pom.xml file. - */ - @Parameter(defaultValue = "${project.runtimeClasspathElements}", readonly = true, required = true) - private List runtimePath; - - public void execute() throws MojoExecutionException { - String classpath = String.join(File.pathSeparator, runtimePath); - List args = new ArrayList<>(); - args.addAll(Arrays.asList("--classpath", classpath)); - if (functionTarget != null) { - args.addAll(Arrays.asList("--target", functionTarget)); - } - if (port != null) { - args.addAll(Arrays.asList("--port", String.valueOf(port))); - } - try { - getLog().info("Calling Invoker with " + args); - Invoker.main(args.toArray(new String[0])); - } catch (Exception e) { - getLog().error("Could not invoke function: " + e, e); - throw new MojoExecutionException("Could not invoke function", e); - } - } -} diff --git a/invoker/function-maven-plugin/src/test/java/com/google/cloud/functions/plugin/DeployFunctionTest.java b/invoker/function-maven-plugin/src/test/java/com/google/cloud/functions/plugin/DeployFunctionTest.java deleted file mode 100644 index e5a2913d..00000000 --- a/invoker/function-maven-plugin/src/test/java/com/google/cloud/functions/plugin/DeployFunctionTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.google.cloud.functions.plugin; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class DeployFunctionTest { - - @Test - public void testDeployFunctionCommandLine() { - DeployFunction mojo = new DeployFunction(); - mojo.envVarsFile = "myfile"; - mojo.buildEnvVarsFile = "myfile2"; - mojo.functionTarget = "function"; - mojo.ignoreFile = "ff"; - mojo.maxInstances = new Integer(3); - mojo.memory = "234"; - mojo.name = "a name"; - mojo.region = "a region"; - mojo.retry = "44"; - mojo.source = "a source"; - mojo.stageBucket = "a bucket"; - mojo.timeout = "timeout"; - mojo.vpcConnector = "a connector"; - mojo.triggerHttp = true; - mojo.allowUnauthenticated = true; - mojo.environmentVariables = ImmutableMap.of("env1", "a", "env2", "b"); - mojo.buildEnvironmentVariables = ImmutableMap.of("env1", "a", "env2", "b"); - List expected = - ImmutableList.of( - "functions", - "deploy", - "a name", - "--region=a region", - "--trigger-http", - "--allow-unauthenticated", - "--entry-point=function", - "--ignore-file=ff", - "--memory=234", - "--retry=44", - "--source=a source", - "--stage-bucket=a bucket", - "--timeout=timeout", - "--vpc-connector=a connector", - "--max-instances=3", - "--set-env-vars=env1=a,env2=b", - "--env-vars-file=myfile", - "--set-build-env-vars=env1=a,env2=b", - "--build-env-vars-file=myfile2", - "--runtime=java11"); - assertThat(mojo.getCommands()).isEqualTo(expected); - } -} diff --git a/invoker/pom.xml b/invoker/pom.xml deleted file mode 100644 index 87b43b62..00000000 --- a/invoker/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - 4.0.0 - - org.sonatype.oss - oss-parent - 9 - - - com.google.cloud.functions.invoker - java-function-invoker-parent - 1.1.1-SNAPSHOT - pom - GCF Java Invoker Parent - - Parent POM for the GCF Java Invoker. The project is structured like this so - that we can have modules that build jar files for use in tests. - - https://github.com/GoogleCloudPlatform/functions-framework-java/tree/master/invoker - - - http://github.com/GoogleCloudPlatform/functions-framework-java - scm:git:git://github.com/GoogleCloudPlatform/functions-framework-java.git - scm:git:ssh://git@github.com/GoogleCloudPlatform/functions-framework-java.git - HEAD - - - - core - testfunction - function-maven-plugin - conformance - - - - UTF-8 - 3.8.1 - 11 - 11 - - - - - - com.google.cloud.functions - functions-framework-api - 1.0.4 - - - - diff --git a/invoker/testfunction/pom.xml b/invoker/testfunction/pom.xml deleted file mode 100644 index 6dada9d4..00000000 --- a/invoker/testfunction/pom.xml +++ /dev/null @@ -1,95 +0,0 @@ - - 4.0.0 - - - com.google.cloud.functions.invoker - java-function-invoker-parent - 1.1.1-SNAPSHOT - - - com.google.cloud.functions.invoker - java-function-invoker-testfunction - 1.1.1-SNAPSHOT - Example GCF Function Jar - - An example of a GCF function packaged into a jar. We use this in tests. - - - - - com.google.cloud.functions - functions-framework-api - - - - com.google.escapevelocity - escapevelocity - 0.9.1 - - - com.google.guava - guava - 29.0-jre - - - com.google.code.gson - gson - 2.8.6 - - - - - - - maven-jar-plugin - 3.1.2 - - - - true - lib - - - - - - - test-jar - - test-compile - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - test-compile - - copy-dependencies - - - ${project.build.directory}/lib - - - - - - org.apache.maven.plugins - maven-deploy-plugin - 3.0.0-M1 - - true - - - - - diff --git a/invoker/testfunction/src/test/java/com/example/functionjar/Background.java b/invoker/testfunction/src/test/java/com/example/functionjar/Background.java deleted file mode 100644 index 6b18b54c..00000000 --- a/invoker/testfunction/src/test/java/com/example/functionjar/Background.java +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.example.functionjar; - -import com.google.cloud.functions.Context; -import com.google.cloud.functions.RawBackgroundFunction; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; - -/** - * @author emcmanus@google.com (Éamonn McManus) - */ -public class Background implements RawBackgroundFunction { - @Override - public void accept(String json, Context context) { - try { - test(json); - } catch (Throwable e) { - e.printStackTrace(); - throw e; - } - } - - private void test(String jsonString) { - Gson gson = new Gson(); - JsonObject json = gson.fromJson(jsonString, JsonObject.class); - JsonPrimitive jsonRuntimeClassName = json.getAsJsonPrimitive("class"); - String runtimeClassName = jsonRuntimeClassName.getAsString(); - new Checker().serviceOrAssert(runtimeClassName); - } -} diff --git a/invoker/testfunction/src/test/java/com/example/functionjar/Checker.java b/invoker/testfunction/src/test/java/com/example/functionjar/Checker.java deleted file mode 100644 index 2a4d3920..00000000 --- a/invoker/testfunction/src/test/java/com/example/functionjar/Checker.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.example.functionjar; - -import com.google.escapevelocity.Template; - -class Checker { - void serviceOrAssert(String runtimeClassName) { - // Check that the context class loader is the loader that loaded this class. - if (getClass().getClassLoader() != Thread.currentThread().getContextClassLoader()) { - throw new AssertionError( - String.format( - "ClassLoader mismatch: mine %s; context %s", - getClass().getClassLoader(), Thread.currentThread().getContextClassLoader())); - } - - ClassLoader myLoader = getClass().getClassLoader(); - Class