Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8b569cb

Browse files
dinagravesTakashi Matsuo
andauthored
Markdown tutorial (GoogleCloudPlatform#3547)
* Default templates load properly * Fully integrated markdown service * Adding tests * End to end tests * Improving the tests and adding region tags * Renaming tests * Moving e2e tests * Removing unused dependency * Adding empty requirements.txt for test driver * Sanitizing input * Added test for missing url * Reformatting the yaml and sanitizing markdown html * Removing pytest from requirements.txt * correctly tearing down * add a noxfile for e2e test * Creating unique service names for every test run * use uuid * activate service account * set gcloud project Co-authored-by: Takashi Matsuo <[email protected]>
1 parent 87c3f9d commit 8b569cb

19 files changed

+810
-0
lines changed

run/markdown-preview/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Markdown Preview
2+
3+
This sample application consists of two services: a "markdown editor" and a separate "markdown renderer". The environment variable, `EDITOR_UPSTREAM_RENDER_URL`, is used to point the "markdown editor" to the "markdown renderer".
4+
5+
Read more about how to deploy and work with these services in https://cloud.google.com/run/docs/tutorials/secure-services.
6+

run/markdown-preview/e2e_test.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
# This sample creates a secure two-service application running on Cloud Run.
16+
# This test builds and deploys the two secure services
17+
# to test that they interact properly together.
18+
19+
import json
20+
import subprocess
21+
from urllib import request
22+
import uuid
23+
24+
import pytest
25+
26+
27+
@pytest.fixture()
28+
def services():
29+
# Unique suffix to create distinct service names
30+
suffix = uuid.uuid4().hex
31+
32+
# Build and Deploy Cloud Run Services
33+
subprocess.run(
34+
[
35+
"gcloud",
36+
"builds",
37+
"submit",
38+
"--substitutions",
39+
f"_SUFFIX={suffix}",
40+
"--config",
41+
"e2e_test_setup.yaml",
42+
"--quiet",
43+
], check=True
44+
)
45+
46+
# Get the URL for the editor and the token
47+
editor = subprocess.run(
48+
[
49+
"gcloud",
50+
"run",
51+
"--platform=managed",
52+
"--region=us-central1",
53+
"services",
54+
"describe",
55+
f"editor-{suffix}",
56+
"--format=value(status.url)",
57+
],
58+
stdout=subprocess.PIPE,
59+
check=True
60+
).stdout.strip()
61+
62+
token = subprocess.run(
63+
["gcloud", "auth", "print-identity-token"], stdout=subprocess.PIPE,
64+
check=True
65+
).stdout.strip()
66+
67+
yield editor, token
68+
69+
subprocess.run(
70+
["gcloud", "run", "services", "delete", f"editor-{suffix}",
71+
"--platform", "managed", "--region", "us-central1", "--quiet"],
72+
check=True
73+
)
74+
subprocess.run(
75+
["gcloud", "run", "services", "delete", f"renderer-{suffix}",
76+
"--platform", "managed", "--region", "us-central1", "--quiet"],
77+
check=True
78+
)
79+
80+
81+
def test_end_to_end(services):
82+
editor = services[0].decode() + "/render"
83+
token = services[1].decode()
84+
data = json.dumps({"data": "**strong text**"})
85+
86+
req = request.Request(
87+
editor,
88+
data=data.encode(),
89+
headers={
90+
"Authorization": f"Bearer {token}",
91+
"Content-Type": "application/json",
92+
},
93+
)
94+
95+
response = request.urlopen(req)
96+
assert response.status == 200
97+
98+
body = response.read()
99+
assert "<p><strong>strong text</strong></p>" in body.decode()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
steps:
16+
- # Build the renderer image
17+
name: gcr.io/cloud-builders/docker:latest
18+
args: ['build', '--tag=gcr.io/$PROJECT_ID/renderer-${_SUFFIX}', 'renderer/.']
19+
20+
- # Push the container image to Container Registry
21+
name: gcr.io/cloud-builders/docker
22+
args: ['push', 'gcr.io/$PROJECT_ID/renderer-${_SUFFIX}']
23+
24+
- # Deploy to Cloud Run
25+
name: gcr.io/cloud-builders/gcloud
26+
args:
27+
- run
28+
- deploy
29+
- renderer-${_SUFFIX}
30+
- --image
31+
- gcr.io/$PROJECT_ID/renderer-${_SUFFIX}
32+
- --region
33+
- us-central1
34+
- --platform
35+
- managed
36+
- --no-allow-unauthenticated
37+
38+
- # Get the Renderer URL and save to workspace
39+
name: gcr.io/cloud-builders/gcloud
40+
entrypoint: /bin/bash
41+
args:
42+
- -c
43+
- |
44+
get_url() {
45+
gcloud run --platform managed --region us-central1 services describe \
46+
renderer-${_SUFFIX} --format='value(status.url)'
47+
}
48+
echo $(get_url) > /workspace/renderer_url.txt
49+
url=$(< /workspace/renderer_url.txt) && if [ -z $url ]; \
50+
then echo 'Missing Renderer URL'; exit 1; fi
51+
52+
- # Build the editor image
53+
name: gcr.io/cloud-builders/docker:latest
54+
args: ['build', '--tag=gcr.io/$PROJECT_ID/editor-${_SUFFIX}', 'editor/.']
55+
56+
- # Push the container image to Container Registry
57+
name: gcr.io/cloud-builders/docker
58+
args: ['push', 'gcr.io/$PROJECT_ID/editor-${_SUFFIX}']
59+
60+
- # Deploy with renderer url environment variable
61+
name: gcr.io/cloud-builders/gcloud
62+
entrypoint: /bin/bash
63+
args:
64+
- -c
65+
- "gcloud run deploy editor-${_SUFFIX} --image gcr.io/$PROJECT_ID/editor-${_SUFFIX} \
66+
--region us-central1 --platform managed \
67+
--set-env-vars EDITOR_UPSTREAM_RENDER_URL=$(cat /workspace/renderer_url.txt) \
68+
--no-allow-unauthenticated"
69+
70+
71+
images:
72+
- 'gcr.io/$PROJECT_ID/renderer-${_SUFFIX}'
73+
- 'gcr.io/$PROJECT_ID/editor-${_SUFFIX}'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
# Use the official Python image.
16+
# https://hub.docker.com/_/python
17+
FROM python:3.8-slim
18+
19+
# Copy application dependency manifests to the container image.
20+
# Copying this separately prevents re-running pip install on every code change.
21+
COPY requirements.txt .
22+
23+
# Install production dependencies.
24+
RUN pip install -r requirements.txt
25+
26+
# Copy local code to the container image.
27+
ENV APP_HOME /app
28+
WORKDIR $APP_HOME
29+
COPY . .
30+
31+
# Run the web service on container startup.
32+
# Use gunicorn webserver with one worker process and 8 threads.
33+
# For environments with multiple CPU cores, increase the number of workers
34+
# to be equal to the cores available.
35+
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app

run/markdown-preview/editor/main.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
from flask import Flask, render_template, request
16+
import os
17+
18+
import render
19+
20+
app = Flask(__name__)
21+
22+
23+
@app.route("/", methods=["GET"])
24+
def index():
25+
# Render the default template
26+
f = open("templates/markdown.md")
27+
return render_template("index.html", default=f.read())
28+
29+
30+
# [START run_secure_request_do]
31+
@app.route("/render", methods=["POST"])
32+
def render_handler():
33+
body = request.get_json()
34+
if not body:
35+
raise Exception("Invalid JSON")
36+
37+
data = body["data"]
38+
parsed_markdown = render.new_request(data)
39+
return parsed_markdown
40+
# [END run_secure_request_do]
41+
42+
43+
if __name__ == "__main__":
44+
PORT = int(os.getenv("PORT")) if os.getenv("PORT") else 8080
45+
46+
# This is used when running locally. Gunicorn is used to run the
47+
# application on Cloud Run. See entrypoint in Dockerfile.
48+
app.run(host="127.0.0.1", port=PORT, debug=True)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
import main
16+
import os
17+
import json
18+
import pytest
19+
20+
21+
@pytest.fixture
22+
def client():
23+
main.app.testing = True
24+
return main.app.test_client()
25+
26+
27+
def test_editor_handler(client):
28+
os.environ["EDITOR_UPSTREAM_RENDER_URL"] = "http://testing.local"
29+
r = client.get("/")
30+
body = r.data.decode()
31+
32+
assert r.status_code == 200
33+
assert "<title>Markdown Editor</title>" in body
34+
assert "This UI allows a user to write Markdown text" in body
35+
36+
37+
def test_render_handler_errors(client):
38+
r = client.get("/render")
39+
assert r.status_code == 405
40+
41+
with pytest.raises(Exception) as e:
42+
client.post("/render", data="**markdown**")
43+
assert "Invalid JSON" in str(e.value)
44+
45+
46+
def test_missing_upstream_url(client):
47+
del os.environ["EDITOR_UPSTREAM_RENDER_URL"]
48+
with pytest.raises(Exception) as e:
49+
client.post("/render",
50+
data=json.dumps({"data": "**strong text**"}),
51+
headers={"Content-Type": "application/json"})
52+
assert "EDITOR_UPSTREAM_RENDER_URL missing" in str(e.value)

run/markdown-preview/editor/render.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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_secure_request]
16+
import os
17+
import sys
18+
import urllib
19+
20+
21+
def new_request(data):
22+
"""
23+
new_request creates a new HTTP request with IAM ID Token credential.
24+
This token is automatically handled by private Cloud Run (fully managed)
25+
and Cloud Functions.
26+
"""
27+
28+
url = os.environ.get("EDITOR_UPSTREAM_RENDER_URL")
29+
if not url:
30+
raise Exception("EDITOR_UPSTREAM_RENDER_URL missing")
31+
32+
unauthenticated = os.environ.get("EDITOR_UPSTREAM_UNAUTHENTICATED", False)
33+
34+
req = urllib.request.Request(url, data=data.encode())
35+
36+
if not unauthenticated:
37+
token = get_token(url)
38+
req.add_header("Authorization", f"Bearer {token}")
39+
40+
sys.stdout.flush()
41+
42+
response = urllib.request.urlopen(req)
43+
return response.read()
44+
45+
46+
def get_token(url):
47+
"""
48+
Retrieves the IAM ID Token credential for the url.
49+
"""
50+
token_url = (
51+
f"http://metadata.google.internal/computeMetadata/v1/instance/"
52+
f"service-accounts/default/identity?audience={url}"
53+
)
54+
token_req = urllib.request.Request(
55+
token_url, headers={"Metadata-Flavor": "Google"}
56+
)
57+
token_response = urllib.request.urlopen(token_req)
58+
token = token_response.read()
59+
return token.decode()
60+
# [END run_secure_request]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest==5.3.2
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Flask==1.1.1
2+
gunicorn==20.0.4

0 commit comments

Comments
 (0)