-
Notifications
You must be signed in to change notification settings - Fork 119
Add Cloud Events support for #55 #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 49 commits
86723ec
b0cd71a
f1ebf4a
64d8e3d
bbf6d35
3bcb69d
42ea896
4b3a10e
a1dddbb
4407259
47acff6
d40f4a0
1d83d3c
7c46f60
8832081
7a16331
16a6858
bdbf5e2
ea2e28c
9e8d874
06ffc2d
ecb0b61
fa727f1
209a8d6
f573a1e
7212640
974d52b
9b87a6e
8d33033
c297097
5176b8c
f499790
973beaf
30b9259
48e939e
2532cec
7928d5d
a38b480
154de1c
2a92d9c
a2b3c67
dbc44a3
eafa781
6ce6f63
18432e8
c2ba64d
6889f54
271e976
7cb1d88
2e34e8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
# Python Functions Frameworks Examples | ||
|
||
* [`cloud_run_http`](./cloud_run_http/) - Deploying an HTTP function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework | ||
* [`cloud_run_event`](./cloud_run_event/) - Deploying a CloudEvent function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework | ||
* [`cloud_run_event`](./cloud_run_event/) - Deploying a [Google Cloud Functions Event](https://cloud.google.com/functions/docs/concepts/events-triggers#events) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework | ||
joelgerard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* [`cloud_run_cloudevents`](./cloud_run_cloudevents/) - Deploying a [CloudEvent](https://github.com/cloudevents/sdk-python) function to [Cloud Run](http://cloud.google.com/run) with the Functions Framework | ||
|
||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Here and in the main README there >1 blank lines in the file. Remove these extra blank lines. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Use the official Python image. | ||
# https://hub.docker.com/_/python | ||
FROM python:3.7-slim | ||
|
||
# Copy local code to the container image. | ||
ENV APP_HOME /app | ||
WORKDIR $APP_HOME | ||
COPY . . | ||
|
||
# Install production dependencies. | ||
RUN pip install gunicorn cloudevents functions-framework | ||
RUN pip install -r requirements.txt | ||
|
||
# Run the web service on container startup. | ||
CMD exec functions-framework --target=hello --signature-type=cloudevent |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Deploying a CloudEvent function to Cloud Run with the Functions Framework | ||
This sample uses the [Cloud Events SDK](https://github.com/cloudevents/sdk-python) to send and receive a CloudEvent on Cloud Run. | ||
|
||
## How to run this locally | ||
Build the Docker image: | ||
|
||
```commandline | ||
docker build --tag ff_example . | ||
``` | ||
|
||
Run the image and bind the correct ports: | ||
|
||
```commandline | ||
docker run -p:8080:8080 ff_example | ||
``` | ||
|
||
Send an event to the container: | ||
|
||
```python | ||
from cloudevents.sdk import converters | ||
from cloudevents.sdk import marshaller | ||
from cloudevents.sdk.converters import structured | ||
from cloudevents.sdk.event import v1 | ||
import requests | ||
import json | ||
|
||
def run_structured(event, url): | ||
http_marshaller = marshaller.NewDefaultHTTPMarshaller() | ||
structured_headers, structured_data = http_marshaller.ToRequest( | ||
event, converters.TypeStructured, json.dumps | ||
) | ||
print("structured CloudEvent") | ||
print(structured_data.getvalue()) | ||
|
||
response = requests.post(url, | ||
headers=structured_headers, | ||
data=structured_data.getvalue()) | ||
response.raise_for_status() | ||
|
||
event = ( | ||
v1.Event() | ||
.SetContentType("application/json") | ||
.SetData('{"name":"john"}') | ||
.SetEventID("my-id") | ||
.SetSource("from-galaxy-far-far-away") | ||
.SetEventTime("tomorrow") | ||
.SetEventType("cloudevent.greet.you") | ||
) | ||
|
||
run_structured(event, "http://0.0.0.0:8080/") | ||
|
||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# 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. | ||
|
||
# This sample creates a function that accepts a Cloud Event per | ||
# https://github.com/cloudevents/sdk-python | ||
import sys | ||
|
||
|
||
def hello(cloudevent): | ||
print("Received event with ID: %s" % cloudevent.EventID(), file=sys.stdout, flush=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Optionally include additional dependencies here |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Google Cloud Functions Events Example | ||
This example demonstrates how to write an event function. Note that you can also use [CloudEvents](https://github.com/cloudevents/sdk-python) | ||
([example](../cloud_run_cloudevents)), which is a different construct. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,13 +12,19 @@ | |
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import functools | ||
import enum | ||
import importlib.util | ||
import io | ||
import json | ||
import os.path | ||
import pathlib | ||
import sys | ||
import types | ||
|
||
import cloudevents.sdk | ||
import cloudevents.sdk.event | ||
import cloudevents.sdk.event.v1 | ||
import cloudevents.sdk.marshaller | ||
import flask | ||
import werkzeug | ||
|
||
|
@@ -35,6 +41,12 @@ | |
DEFAULT_SIGNATURE_TYPE = "http" | ||
|
||
|
||
class _EventType(enum.Enum): | ||
LEGACY = 1 | ||
CLOUDEVENT_BINARY = 2 | ||
CLOUDEVENT_TEXT = 3 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned before, CloudEvent Text is not an event type. The logic used here is for a CloudEvent Structured event.
|
||
|
||
|
||
class _Event(object): | ||
"""Event passed to background functions.""" | ||
|
||
|
@@ -67,38 +79,83 @@ def view_func(path): | |
return view_func | ||
|
||
|
||
def _is_binary_cloud_event(request): | ||
return ( | ||
def _get_cloudevent_version(): | ||
return cloudevents.sdk.event.v1.Event() | ||
|
||
|
||
def _run_legacy_event(function, request): | ||
event_data = request.get_json() | ||
if not event_data: | ||
flask.abort(400) | ||
event_object = _Event(**event_data) | ||
data = event_object.data | ||
context = Context(**event_object.context) | ||
function(data, context) | ||
|
||
|
||
def _run_binary_cloudevent(function, request, cloudevent_def): | ||
data = io.BytesIO(request.get_data()) | ||
http_marshaller = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() | ||
event = http_marshaller.FromRequest( | ||
cloudevent_def, request.headers, data, json.load | ||
) | ||
|
||
function(event) | ||
|
||
|
||
def _run_text_cloudevent(function, request, cloudevent_def): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be |
||
data = io.StringIO(request.get_data(as_text=True)) | ||
m = cloudevents.sdk.marshaller.NewDefaultHTTPMarshaller() | ||
event = m.FromRequest(cloudevent_def, request.headers, data, json.loads) | ||
function(event) | ||
|
||
|
||
def _get_event_type(request): | ||
if ( | ||
request.headers.get("ce-type") | ||
and request.headers.get("ce-specversion") | ||
and request.headers.get("ce-source") | ||
and request.headers.get("ce-id") | ||
) | ||
): | ||
return _EventType.CLOUDEVENT_BINARY | ||
elif request.headers.get("Content-Type") == "application/cloudevents+json": | ||
return _EventType.CLOUDEVENT_TEXT | ||
else: | ||
return _EventType.LEGACY | ||
|
||
|
||
def _event_view_func_wrapper(function, request): | ||
def view_func(path): | ||
if _is_binary_cloud_event(request): | ||
# Support CloudEvents in binary content mode, with data being the | ||
# whole request body and context attributes retrieved from request | ||
# headers. | ||
data = request.get_data() | ||
context = Context( | ||
eventId=request.headers.get("ce-eventId"), | ||
timestamp=request.headers.get("ce-timestamp"), | ||
eventType=request.headers.get("ce-eventType"), | ||
resource=request.headers.get("ce-resource"), | ||
if _get_event_type(request) == _EventType.LEGACY: | ||
_run_legacy_event(function, request) | ||
else: | ||
# here for defensive backwards compatibility in case we make a mistake in rollout. | ||
flask.abort( | ||
400, | ||
description="The FUNCTION_SIGNATURE_TYPE for this function is set to event " | ||
"but no Google Cloud Functions Event was given. If you are using CloudEvents set " | ||
"FUNCTION_SIGNATURE_TYPE=cloudevent", | ||
) | ||
function(data, context) | ||
|
||
return "OK" | ||
|
||
return view_func | ||
|
||
|
||
def _cloudevent_view_func_wrapper(function, request): | ||
def view_func(path): | ||
cloudevent_def = _get_cloudevent_version() | ||
event_type = _get_event_type(request) | ||
if event_type == _EventType.CLOUDEVENT_TEXT: | ||
_run_text_cloudevent(function, request, cloudevent_def) | ||
elif event_type == _EventType.CLOUDEVENT_BINARY: | ||
_run_binary_cloudevent(function, request, cloudevent_def) | ||
else: | ||
# This is a regular CloudEvent | ||
event_data = request.get_json() | ||
if not event_data: | ||
flask.abort(400) | ||
event_object = _Event(**event_data) | ||
data = event_object.data | ||
context = Context(**event_object.context) | ||
function(data, context) | ||
flask.abort( | ||
400, | ||
description="Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent " | ||
" but it did not receive a cloudevent as a request.", | ||
) | ||
|
||
return "OK" | ||
|
||
|
@@ -179,19 +236,27 @@ def create_app(target=None, source=None, signature_type=None): | |
app.url_map.add(werkzeug.routing.Rule("/<path:path>", endpoint="run")) | ||
app.view_functions["run"] = _http_view_func_wrapper(function, flask.request) | ||
app.view_functions["error"] = lambda: flask.abort(404, description="Not Found") | ||
elif signature_type == "event": | ||
elif signature_type == "event" or signature_type == "cloudevent": | ||
app.url_map.add( | ||
werkzeug.routing.Rule( | ||
"/", defaults={"path": ""}, endpoint="run", methods=["POST"] | ||
"/", defaults={"path": ""}, endpoint=signature_type, methods=["POST"] | ||
) | ||
) | ||
app.url_map.add( | ||
werkzeug.routing.Rule("/<path:path>", endpoint="run", methods=["POST"]) | ||
werkzeug.routing.Rule( | ||
"/<path:path>", endpoint=signature_type, methods=["POST"] | ||
) | ||
) | ||
app.view_functions["run"] = _event_view_func_wrapper(function, flask.request) | ||
|
||
# Add a dummy endpoint for GET / | ||
app.url_map.add(werkzeug.routing.Rule("/", endpoint="get", methods=["GET"])) | ||
app.view_functions["get"] = lambda: "" | ||
|
||
# Add the view functions | ||
app.view_functions["event"] = _event_view_func_wrapper(function, flask.request) | ||
app.view_functions["cloudevent"] = _cloudevent_view_func_wrapper( | ||
function, flask.request | ||
) | ||
else: | ||
raise FunctionsFrameworkException( | ||
"Invalid signature type: {signature_type}".format( | ||
|
Uh oh!
There was an error while loading. Please reload this page.