Skip to content

Commit d2e3bce

Browse files
[run] add Pub/Sub events sample (#4011)
* Initialized run/events-pubsub dir Mostly copied contents from https://github.com/gcpevents/eventsforcloudrun/tree/master/python Signed-off-by: Curtis Mason <cumason@google.com> * Edited README Signed-off-by: Curtis Mason <cumason@google.com> * Added testing for events-pubsub sample code Signed-off-by: Curtis Mason <cumason@google.com> * added dockerignore Signed-off-by: Curtis Mason <cumason@google.com> * updated requirements.txt Signed-off-by: Curtis Mason <cumason@google.com> * fixed requirements.txt Signed-off-by: Curtis Mason <cumason@google.com> * Stable sample codebase Signed-off-by: Curtis Mason <cumason@google.com> * added identifiers Signed-off-by: Curtis Mason <cumason@google.com> * Updated events-pubsub README Signed-off-by: Curtis Mason <cumason@google.com> * Removed json.dumps Signed-off-by: Curtis Mason <cumason@google.com> * Reformatted README.md with 80char ruler Signed-off-by: Curtis Mason <cumason@google.com> * cloud run events-pubsub README changed message Signed-off-by: Curtis Mason <cumason@google.com> * events-pubsub added pytest to requirements Signed-off-by: Curtis Mason <cumason@google.com> * fixing linter imports events-pubsub * lint changing import order Signed-off-by: Curtis Mason <cumason@google.com> * requirements.txt with only flask and pytest requirements.txt has flask and pytest, and their codependencies Signed-off-by: Curtis Mason <cumason@google.com> * Further minimized requirements files Signed-off-by: Curtis Mason <cumason@google.com> * added flask to requirements-test Signed-off-by: Curtis Mason <cumason@google.com> * removed requirements-test.txt Signed-off-by: Curtis Mason <cumason@google.com> * added apache license Signed-off-by: Curtis Mason <cumason@google.com> * upgraded dockerfile to 3.8 Signed-off-by: Curtis Mason <cumason@google.com> * Update run/events-pubsub/README.md Co-authored-by: Averi Kitsch <akitsch@google.com> * codebase similar to pubsub samplecode * fixed README typo Signed-off-by: Curtis Mason <cumason@google.com> * Revised README and renamed app.py to main.py Signed-off-by: Curtis Mason <cumason@google.com> * added automated subscription update to README Signed-off-by: Curtis Mason <cumason@google.com> * Added SERVICE_ACCOUNT var to readme Signed-off-by: Curtis Mason <cumason@google.com> * Added more env variables to README Signed-off-by: Curtis Mason <cumason@google.com> * using python:3.8-slim in dockerfile * removed pytest from requirements.txt Signed-off-by: Curtis Mason <cumason@google.com> * removed Flask from requirements-test.txt * removed subscription= from readme Signed-off-by: Curtis Mason <cumason@google.com> * Documented --push-endpoint workaround in readme Signed-off-by: Curtis Mason <cumason@google.com> * Fixed linter in main.py * Finalized events-pubsub readme Signed-off-by: Curtis Mason <cumason@google.com> * removed duplicate region tag Signed-off-by: Curtis Mason <cumason@google.com> * removed port 8080 from dockerfile Signed-off-by: Curtis Mason <cumason@google.com> * Added required fields testing * Readme nitpick * lint fix: reordered imports * changed readme log command * added ce-id to response in main.py * Fixed README typo Co-authored-by: Averi Kitsch <akitsch@google.com>
1 parent b7bd227 commit d2e3bce

File tree

7 files changed

+265
-0
lines changed

7 files changed

+265
-0
lines changed

run/events-pubsub/.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Dockerfile
2+
README.md
3+
*.pyc
4+
*.pyo
5+
*.pyd
6+
__pycache__

run/events-pubsub/Dockerfile

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright 2020 Google, LLC.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START run_events_pubsub_dockerfile]
16+
17+
# Use the official Python image.
18+
# https://hub.docker.com/_/python
19+
FROM python:3.8-slim
20+
21+
# Allow statements and log messages to immediately appear in the Cloud Run logs
22+
ENV PYTHONUNBUFFERED True
23+
24+
# Copy application dependency manifests to the container image.
25+
# Copying this separately prevents re-running pip install on every code change.
26+
COPY requirements.txt ./
27+
28+
# Install production dependencies.
29+
RUN pip install -r requirements.txt
30+
31+
# Copy local code to the container image.
32+
ENV APP_HOME /app
33+
WORKDIR $APP_HOME
34+
COPY . ./
35+
36+
# Run the web service on container startup.
37+
# Use gunicorn webserver with one worker process and 8 threads.
38+
# For environments with multiple CPU cores, increase the number of workers
39+
# to be equal to the cores available.
40+
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
41+
# [END run_events_pubsub_dockerfile]

run/events-pubsub/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Events for Cloud Run – Pub/Sub tutorial
2+
3+
This sample shows how to create a service that processes Pub/Sub events.
4+
5+
## Setup
6+
7+
Login to gcloud:
8+
9+
```sh
10+
gcloud auth login
11+
```
12+
13+
Configure project id:
14+
15+
```sh
16+
gcloud config set project [PROJECT-ID]
17+
```
18+
19+
Configure environment variables:
20+
21+
```sh
22+
MY_RUN_SERVICE=pubsub-service
23+
MY_RUN_CONTAINER=pubsub-container
24+
MY_TOPIC=pubsub-topic
25+
MY_PUBSUB_TRIGGER=pubsub-trigger
26+
```
27+
28+
## Quickstart
29+
30+
Deploy your Cloud Run service:
31+
32+
```sh
33+
gcloud builds submit \
34+
--tag gcr.io/$(gcloud config get-value project)/$MY_RUN_CONTAINER
35+
gcloud run deploy $MY_RUN_SERVICE \
36+
--image gcr.io/$(gcloud config get-value project)/$MY_RUN_CONTAINER \
37+
--allow-unauthenticated
38+
```
39+
40+
Create a Cloud Pub/Sub topic:
41+
42+
```sh
43+
gcloud pubsub topics create $MY_TOPIC
44+
```
45+
46+
Create a Cloud Pub/Sub trigger:
47+
48+
```sh
49+
gcloud alpha events triggers create $MY_PUBSUB_TRIGGER \
50+
--target-service $MY_RUN_SERVICE \
51+
--type com.google.cloud.pubsub.topic.publish \
52+
--parameters topic=$MY_TOPIC
53+
```
54+
55+
## Test
56+
57+
Test your Cloud Run service by publishing a message to the topic:
58+
59+
```sh
60+
gcloud pubsub topics publish $MY_TOPIC --message="John Doe"
61+
```
62+
63+
You may observe the Cloud Run service printing upon receiving an event in
64+
Cloud Logging.
65+
66+
```sh
67+
gcloud logging read "resource.type=cloud_run_revision AND \
68+
resource.labels.service_name=$MY_RUN_SERVICE" --project \
69+
$(gcloud config get-value project) --limit 30 --format 'value(textPayload)'
70+
```

run/events-pubsub/main.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2020 Google, LLC.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START run_events_pubsub_server_setup]
16+
import base64
17+
import os
18+
19+
from flask import Flask, request
20+
21+
22+
required_fields = ['Ce-Id', 'Ce-Source', 'Ce-Type', 'Ce-Specversion']
23+
24+
app = Flask(__name__)
25+
# [END run_events_pubsub_server_setup]
26+
27+
28+
# [START run_events_pubsub_handler]
29+
@app.route('/', methods=['POST'])
30+
def index():
31+
for field in required_fields:
32+
if field not in request.headers:
33+
errmsg = f'Bad Request: missing required header {field}'
34+
print(errmsg)
35+
return errmsg, 400
36+
37+
envelope = request.get_json()
38+
if not envelope:
39+
msg = 'no Pub/Sub message received'
40+
print(f'error: {msg}')
41+
return f'Bad Request: {msg}', 400
42+
43+
if not isinstance(envelope, dict) or 'message' not in envelope:
44+
msg = 'invalid Pub/Sub message format'
45+
print(f'error: {msg}')
46+
return f'Bad Request: {msg}', 400
47+
48+
pubsub_message = envelope['message']
49+
50+
name = 'World'
51+
if isinstance(pubsub_message, dict) and 'data' in pubsub_message:
52+
name = base64.b64decode(pubsub_message['data']).decode('utf-8').strip()
53+
54+
ce_id = request.headers.get('Ce-Id')
55+
resp = f'Hello, {name}! ID: {ce_id}'
56+
print(resp)
57+
return (resp, 200)
58+
# [END run_events_pubsub_handler]
59+
60+
61+
# [START run_events_pubsub_server]
62+
if __name__ == "__main__":
63+
app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
64+
# [END run_events_pubsub_server]

run/events-pubsub/main_test.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright 2020 Google, LLC.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import base64
15+
16+
import copy
17+
18+
from uuid import uuid4
19+
20+
import pytest
21+
22+
import main
23+
24+
25+
required_fields = ['Ce-Id', 'Ce-Source', 'Ce-Type', 'Ce-Specversion']
26+
27+
header_data = {field: str(uuid4()) for field in required_fields}
28+
29+
30+
@pytest.fixture
31+
def client():
32+
main.app.testing = True
33+
return main.app.test_client()
34+
35+
36+
def test_empty_payload(client):
37+
r = client.post('/', json='', headers=header_data)
38+
assert r.status_code == 400
39+
40+
41+
def test_invalid_payload(client):
42+
r = client.post('/', json={'nomessage': 'invalid'}, headers=header_data)
43+
assert r.status_code == 400
44+
45+
46+
def test_invalid_mimetype(client):
47+
r = client.post('/', json="{ message: true }", headers=header_data)
48+
assert r.status_code == 400
49+
50+
51+
def test_minimally_valid_message(client, capsys):
52+
r = client.post('/', json={'message': True}, headers=header_data)
53+
assert r.status_code == 200
54+
55+
out, _ = capsys.readouterr()
56+
ce_id = header_data['Ce-Id']
57+
assert f'Hello, World! ID: {ce_id}' in out
58+
59+
60+
def test_populated_message(client, capsys):
61+
name = str(uuid4())
62+
data = base64.b64encode(name.encode()).decode()
63+
64+
r = client.post('/', json={'message': {'data': data}}, headers=header_data)
65+
assert r.status_code == 200
66+
67+
out, _ = capsys.readouterr()
68+
ce_id = header_data['Ce-Id']
69+
assert f'Hello, {name}! ID: {ce_id}' in out
70+
71+
72+
def test_missing_required_fields(client, capsys):
73+
for field in required_fields:
74+
test_headers = copy.copy(header_data)
75+
test_headers.pop(field)
76+
77+
r = client.post('/', headers=test_headers)
78+
assert r.status_code == 400
79+
80+
out, _ = capsys.readouterr()
81+
assert f'Bad Request: missing required header {field}' in out
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest==5.4.3

run/events-pubsub/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Flask==1.1.2
2+
gunicorn==20.0.4

0 commit comments

Comments
 (0)