From fcdb6ae8c386113d0ec3c37e97368ced90903ae9 Mon Sep 17 00:00:00 2001 From: dsolomakha Date: Wed, 26 Feb 2020 12:30:00 -0500 Subject: [PATCH] Initial commit of Google Function Adapter implement HttpFunction add headers processing Some refactoring [WIP] Add invoker integration test Make test classes nested within test. Add sample and refdoc Resolves #468 --- .../src/main/asciidoc/adapters/gcp-intro.adoc | 132 ++++++++++++++++ spring-cloud-function-adapters/pom.xml | 1 + .../spring-cloud-function-adapter-gcp/pom.xml | 58 +++++++ .../GcfSpringBootHttpRequestHandler.java | 73 +++++++++ .../GcfSpringBootHttpRequestHandlerTests.java | 142 ++++++++++++++++++ .../function-sample-gcp/.gitignore | 1 + .../function-sample-gcp/pom.xml | 131 ++++++++++++++++ .../java/com/example/CloudFunctionMain.java | 36 +++++ .../src/main/resources/META-INF/MANIFEST.MF | 1 + spring-cloud-function-samples/pom.xml | 1 + 10 files changed, 576 insertions(+) create mode 100644 docs/src/main/asciidoc/adapters/gcp-intro.adoc create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcloud/GcfSpringBootHttpRequestHandler.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/GcfSpringBootHttpRequestHandlerTests.java create mode 100644 spring-cloud-function-samples/function-sample-gcp/.gitignore create mode 100644 spring-cloud-function-samples/function-sample-gcp/pom.xml create mode 100644 spring-cloud-function-samples/function-sample-gcp/src/main/java/com/example/CloudFunctionMain.java create mode 100644 spring-cloud-function-samples/function-sample-gcp/src/main/resources/META-INF/MANIFEST.MF diff --git a/docs/src/main/asciidoc/adapters/gcp-intro.adoc b/docs/src/main/asciidoc/adapters/gcp-intro.adoc new file mode 100644 index 000000000..101af96d5 --- /dev/null +++ b/docs/src/main/asciidoc/adapters/gcp-intro.adoc @@ -0,0 +1,132 @@ +:branch: master + +=== Google Cloud Functions (Alpha) + +The Google Cloud Functions adapter enables Spring Cloud Function apps to run on the https://cloud.google.com/functions[Google Cloud Functions] serverless platform. +You can either run the function locally using the open source https://github.com/GoogleCloudPlatform/functions-framework-java[Google Functions Framework for Java] or on GCP. + +==== Getting Started + +Let’s start with a simple Spring Cloud Function example: + +[source, java] +---- +@SpringBootApplication +public class CloudFunctionMain { + + public static void main(String[] args) { + SpringApplication.run(CloudFunctionMain.class, args); + } + + @Bean + public Function uppercase() { + return value -> value.toUpperCase(); + } +} +---- + +===== Test locally + +Start by adding the Maven plugin provided as part of the Google Functions Framework for Java. + +[source, xml] +---- + + com.google.cloud.functions + function-maven-plugin + 0.9.1 + + org.springframework.cloud.function.adapter.gcloud.GcfSpringBootHttpRequestHandler + 8080 + + +---- + +Specify your configuration main class in `resources/META-INF/MANIFEST.MF`. + +[source] +---- +Main-Class: com.example.CloudFunctionMain +---- + +Then run the function: + +---- +mvn function:run +---- + +Invoke the HTTP function: + +---- +curl http://localhost:8080/ -d "hello" +---- + +===== Deploy to GCP + +As of March 2020, Google Cloud Functions for Java is in Alpha. +You can get on the https://docs.google.com/forms/d/e/1FAIpQLScC98jGi7CfG0n3UYlj7Xad8XScvZC8-BBOg7Pk3uSZx_2cdQ/viewform[whitelist] to try it out. + +First, add the Shade Plugin configuration to generate a fat jar when you run the `mvn package` command. + +[source, xml] +---- + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + true + deploy + gcp + + + META-INF/spring.handlers + + + META-INF/spring.factories + + + META-INF/spring.schemas + + + + com.example.CloudFunctionMain + + + + + + +---- + +Package the application. + +---- +mvn package +---- + +You should see the fat jar in `deploy` directory. + +Make sure that you have the https://cloud.google.com/sdk/install[Cloud SDK CLI] installed. + +From the project base directory run the following command to deploy. + +---- +gcloud alpha functions deploy function-sample-gcp \ +--entry-point org.springframework.cloud.function.adapter.gcloud.GcfSpringBootHttpRequestHandler \ +--runtime java11 \ +--trigger-http \ +--source deploy \ +--memory 512MB +---- + +Invoke the HTTP function: + +---- +curl https://REGION-PROJECT_ID.cloudfunctions.net/function-sample-gcp -d "hello" +---- diff --git a/spring-cloud-function-adapters/pom.xml b/spring-cloud-function-adapters/pom.xml index a09328030..853a8a53e 100644 --- a/spring-cloud-function-adapters/pom.xml +++ b/spring-cloud-function-adapters/pom.xml @@ -19,6 +19,7 @@ spring-cloud-function-adapter-aws spring-cloud-function-adapter-openwhisk spring-cloud-function-adapter-azure + spring-cloud-function-adapter-gcp diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml new file mode 100644 index 000000000..3992f19fd --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + spring-cloud-function-adapter-gcp + Spring Cloud Function Adapter GCP + Spring Cloud Function Adapter for Google Cloud Functions + + + spring-cloud-function-adapter-parent + org.springframework.cloud + 3.0.4.BUILD-SNAPSHOT + + + + UTF-8 + UTF-8 + 1.0.0-alpha-2-rc3 + 2.8.5 + + + + + com.google.cloud.functions + functions-framework-api + ${google.cloud.functions.version} + provided + + + com.google.code.gson + gson + ${gson.version} + + + org.springframework.cloud + spring-cloud-function-context + + + org.springframework.cloud + spring-cloud-starter-function-web + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.google.cloud.functions.invoker + java-function-invoker + ${google.cloud.functions.version} + test + + + diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcloud/GcfSpringBootHttpRequestHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcloud/GcfSpringBootHttpRequestHandler.java new file mode 100644 index 000000000..37b05b29d --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcloud/GcfSpringBootHttpRequestHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020-2020 the original author or 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 + * + * https://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 org.springframework.cloud.function.adapter.gcloud; + +import com.google.cloud.functions.HttpFunction; +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; +import com.google.gson.Gson; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.function.context.AbstractSpringFunctionAdapterInitializer; + +/** + * Implementation of {@link HttpFunction} for Google Cloud Function (GCF). + * This is the Spring Cloud Function adapter for GCF HTTP function. + * + * @author Dmitry Solomakha + * @author Mike Eltsufin + */ +public class GcfSpringBootHttpRequestHandler + extends AbstractSpringFunctionAdapterInitializer implements HttpFunction { + + private final Gson gson = new Gson(); + + public GcfSpringBootHttpRequestHandler() { + super(); + } + + public GcfSpringBootHttpRequestHandler(Class configurationClass) { + super(configurationClass); + } + + /** + * The implementation of a GCF {@link HttpFunction} that will be used as the entrypoint from GCF. + */ + @Override + public void service(HttpRequest httpRequest, HttpResponse httpResponse) throws Exception { + Thread.currentThread() + .setContextClassLoader(GcfSpringBootHttpRequestHandler.class.getClassLoader()); + + initialize(httpRequest); + + Publisher input; + if (getInputType() == Void.class) { + input = Mono.empty(); + } + else { + input = Mono.just(gson.fromJson(httpRequest.getReader(), getInputType())); + } + + Publisher output = this.apply(input); + + Object result = this.result(input, output); + + httpResponse.getWriter().write(gson.toJson(result)); + httpResponse.getWriter().close(); + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/GcfSpringBootHttpRequestHandlerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/GcfSpringBootHttpRequestHandlerTests.java new file mode 100644 index 000000000..92292ac53 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcloud/GcfSpringBootHttpRequestHandlerTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2020-2020 the original author or 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 + * + * https://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 org.springframework.cloud.function.adapter.gcloud; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; +import com.google.gson.Gson; +import org.junit.Test; +import org.mockito.Mockito; + +import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the HTTP functions adapter for Google Cloud Functions. + * + * @author Dmitry Solomakha + * @author Mike Eltsufin + */ +public class GcfSpringBootHttpRequestHandlerTests { + + private static final Gson gson = new Gson(); + + @Test + public void testHelloWorldSupplier() throws Exception { + testFunction(HelloWorldSupplier.class, null, "Hello World!"); + } + + @Test + public void testJsonInputFunction() throws Exception { + testFunction(JsonInputFunction.class, new IncomingRequest("hello"), + "Thank you for sending the message: hello"); + } + + @Test + public void testJsonInputOutputFunction() throws Exception { + testFunction(JsonInputOutputFunction.class, new IncomingRequest("hello"), + new OutgoingResponse("Thank you for sending the message: hello")); + } + + @Test + public void testJsonInputConsumer() throws Exception { + testFunction(JsonInputConsumer.class, new IncomingRequest("hello"), null); + } + + private void testFunction(Class configurationClass, I input, O expectedOutput) throws Exception { + GcfSpringBootHttpRequestHandler handler = new GcfSpringBootHttpRequestHandler(configurationClass); + + HttpRequest request = Mockito.mock(HttpRequest.class); + + if (input != null) { + when(request.getReader()).thenReturn(new BufferedReader(new StringReader(gson.toJson(input)))); + } + + HttpResponse response = Mockito.mock(HttpResponse.class); + StringWriter writer = new StringWriter(); + when(response.getWriter()).thenReturn(new BufferedWriter(writer)); + + handler.service(request, response); + + assertThat(writer.toString()).isEqualTo(gson.toJson(expectedOutput)); + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class HelloWorldSupplier { + @Bean + public Supplier supplier() { + return () -> "Hello World!"; + } + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class JsonInputFunction { + @Bean + public Function function() { + return (in) -> "Thank you for sending the message: " + in.message; + } + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class JsonInputOutputFunction { + @Bean + public Function function() { + return (in) -> new OutgoingResponse("Thank you for sending the message: " + in.message); + } + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class JsonInputConsumer { + @Bean + public Consumer function() { + return (in) -> System.out.println("Thank you for sending the message: " + in.message); + } + } + + private static class IncomingRequest { + String message; + + IncomingRequest(String message) { + this.message = message; + } + } + + private static class OutgoingResponse { + String message; + + OutgoingResponse(String message) { + this.message = message; + } + } +} diff --git a/spring-cloud-function-samples/function-sample-gcp/.gitignore b/spring-cloud-function-samples/function-sample-gcp/.gitignore new file mode 100644 index 000000000..ad5e6cfb6 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-gcp/.gitignore @@ -0,0 +1 @@ +deploy/ diff --git a/spring-cloud-function-samples/function-sample-gcp/pom.xml b/spring-cloud-function-samples/function-sample-gcp/pom.xml new file mode 100644 index 000000000..5debf3b91 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-gcp/pom.xml @@ -0,0 +1,131 @@ + + + 4.0.0 + + function-sample-gcp + + + spring-cloud-function-samples + org.springframework.cloud + 3.0.4.BUILD-SNAPSHOT + + + + + org.springframework.cloud + spring-cloud-function-adapter-gcp + ${project.version} + + + + + + + com.google.cloud.functions + function-maven-plugin + 0.9.1 + + org.springframework.cloud.function.adapter.gcloud.GcfSpringBootHttpRequestHandler + 8080 + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + true + deploy + gcp + + + META-INF/spring.handlers + + + META-INF/spring.factories + + + META-INF/spring.schemas + + + + com.example.CloudFunctionMain + + + + + + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/libs-release-local + + false + + + + + diff --git a/spring-cloud-function-samples/function-sample-gcp/src/main/java/com/example/CloudFunctionMain.java b/spring-cloud-function-samples/function-sample-gcp/src/main/java/com/example/CloudFunctionMain.java new file mode 100644 index 000000000..dc5f213ab --- /dev/null +++ b/spring-cloud-function-samples/function-sample-gcp/src/main/java/com/example/CloudFunctionMain.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2020 the original author or 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 + * + * https://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; + +import java.util.function.Function; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class CloudFunctionMain { + + public static void main(String[] args) { + SpringApplication.run(CloudFunctionMain.class, args); + } + + @Bean + public Function function() { + return value -> value.toUpperCase(); + } +} diff --git a/spring-cloud-function-samples/function-sample-gcp/src/main/resources/META-INF/MANIFEST.MF b/spring-cloud-function-samples/function-sample-gcp/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 000000000..6bd1d52fe --- /dev/null +++ b/spring-cloud-function-samples/function-sample-gcp/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1 @@ +Main-Class: com.example.CloudFunctionMain diff --git a/spring-cloud-function-samples/pom.xml b/spring-cloud-function-samples/pom.xml index b2dd76f92..4b5dc0b03 100644 --- a/spring-cloud-function-samples/pom.xml +++ b/spring-cloud-function-samples/pom.xml @@ -24,6 +24,7 @@ function-sample-aws-custom function-sample-azure function-sample-spring-integration + function-sample-gcp