diff --git a/noxfile-template.py b/noxfile-template.py index 547e9975e90..811b1a183d9 100644 --- a/noxfile-template.py +++ b/noxfile-template.py @@ -149,6 +149,9 @@ def _setup_appengine_sdk(session): or str(Path(sample).absolute().relative_to(REPO_ROOT)).startswith( "bigquery/pandas-gbq-migration" ) + or str(Path(sample).absolute().relative_to(REPO_ROOT)).startswith( + "run/system-package" + ) ) ] NON_GAE_STANDARD_SAMPLES_PY2 = sorted( @@ -207,6 +210,7 @@ def py3(session, sample): """Runs py.test for a sample using Python 3.x""" _session_tests(session, sample) + @nox.session(python="3.6") def lint(session): session.install("flake8", "flake8-import-order") @@ -219,6 +223,7 @@ def lint(session): ] session.run("flake8", *args) + SAMPLES_WITH_GENERATED_READMES = sorted(list(_collect_dirs(".", suffix=".rst.in"))) diff --git a/noxfile.py b/noxfile.py index 267b584e297..73d365783bd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -174,6 +174,7 @@ def _setup_appengine_sdk(session): sample.startswith("./appengine/standard_python37") or sample.startswith("./functions/") or sample.startswith("./bigquery/pandas-gbq-migration") + or sample.startswith("./run/system-package") ) ] NON_GAE_STANDARD_SAMPLES_PY2 = sorted( diff --git a/run/system-package/.dockerignore b/run/system-package/.dockerignore new file mode 100644 index 00000000000..504e1d3bb7b --- /dev/null +++ b/run/system-package/.dockerignore @@ -0,0 +1,4 @@ +Dockerfile +.dockerignore +__pycache__ +.pytest_cache \ No newline at end of file diff --git a/run/system-package/Dockerfile b/run/system-package/Dockerfile new file mode 100644 index 00000000000..5d2a58f84a2 --- /dev/null +++ b/run/system-package/Dockerfile @@ -0,0 +1,42 @@ +# 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. + +# Use the official Python image. +# https://hub.docker.com/_/python +FROM python:3.7 + +# [START run_system_package_ubuntu] +RUN apt-get update -y && apt-get install -y \ + graphviz \ + && apt-get clean +# [END run_system_package_ubuntu] + +# Copy application dependency manifests to the container image. +# Copying this separately prevents re-running pip install on every code change. +COPY requirements.txt . + +# Install production dependencies. +RUN pip install -r requirements.txt + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . . + +# Run the web service on container startup. +# Use gunicorn webserver with one worker process and 8 threads. +# For environments with multiple CPU cores, increase the number of workers +# to be equal to the cores available. +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 main:app + diff --git a/run/system-package/README.md b/run/system-package/README.md new file mode 100644 index 00000000000..4b6cfa7b756 --- /dev/null +++ b/run/system-package/README.md @@ -0,0 +1,44 @@ +# Cloud Run System Package Sample + +This sample shows how to use a CLI tool installed as a system package as part of a web service. + +Use it with the [Using system packages tutorial](https://cloud.google.com/run/docs/tutorials/system-packages). + + +[![Run in Google Cloud][run_img]][run_link] + +[run_img]: https://storage.googleapis.com/cloudrun/button.svg +[run_link]: https://console.cloud.google.com/cloudshell/editor?shellonly=true&cloudshell_image=gcr.io/cloudrun/button&cloudshell_git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&cloudshell_working_dir=run/system-package + +## Build + +``` +docker build --tag graphviz:python . +``` + +## Run Locally + +``` +docker run --rm -p 9090:8080 graphviz:python +``` + +## Test + +``` +pytest +``` + +_Note: you may need to install `pytest` using `pip install pytest`._ + +## Deploy + +```sh +# Set an environment variable with your GCP Project ID +export GOOGLE_CLOUD_PROJECT= + +# Submit a build using Google Cloud Build +gcloud builds submit --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/graphviz + +# Deploy to Cloud Run +gcloud beta run deploy graphviz --image gcr.io/${GOOGLE_CLOUD_PROJECT}/graphviz +``` diff --git a/run/system-package/main.py b/run/system-package/main.py new file mode 100644 index 00000000000..5bc6cc2e431 --- /dev/null +++ b/run/system-package/main.py @@ -0,0 +1,85 @@ +# 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. + +from flask import Flask, make_response, request +import os +import subprocess +import sys + + +app = Flask(__name__) + + +# [START run_system_package_handler] +@app.route("/diagram.png", methods=["GET"]) +def index(): + # Takes an HTTP GET request with query param dot and + # returns a png with the rendered DOT diagram in a HTTP response. + try: + image = create_diagram(request.args.get("dot")) + response = make_response(image) + response.headers.set("Content-Type", "image/png") + return response + + except Exception as e: + print("error: {}".format(e)) + + # Flush the stdout to avoid log buffering. + sys.stdout.flush() + + # If no graphviz definition or bad graphviz def, return 400 + if "syntax" in str(e): + return "Bad Request: {}".format(e), 400 + + return "Internal Server Error", 500 + + +# [END run_system_package_handler] + + +# [START run_system_package_exec] +def create_diagram(dot): + # Generates a diagram based on a graphviz DOT diagram description. + if not dot: + raise Exception("syntax: no graphviz definition provided") + + dot_args = [ # These args add a watermark to the dot graphic. + "-Glabel=Made on Cloud Run", + "-Gfontsize=10", + "-Glabeljust=right", + "-Glabelloc=bottom", + "-Gfontcolor=gray", + "-Tpng", + ] + + # Uses local `dot` binary from Graphviz: + # https://graphviz.gitlab.io + image = subprocess.run( + ["dot"] + dot_args, input=dot.encode("utf-8"), stdout=subprocess.PIPE + ).stdout + + if not image: + raise Exception("syntax: bad graphviz definition provided") + return image + + +# [END run_system_package_exec] + + +if __name__ == "__main__": + PORT = int(os.getenv("PORT")) if os.getenv("PORT") else 8080 + + # This is used when running locally. Gunicorn is used to run the + # application on Cloud Run. See entrypoint in Dockerfile. + app.run(host="127.0.0.1", port=PORT, debug=True) diff --git a/run/system-package/main_test.py b/run/system-package/main_test.py new file mode 100644 index 00000000000..f19e54332ce --- /dev/null +++ b/run/system-package/main_test.py @@ -0,0 +1,46 @@ +# 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. + +# NOTE: +# To pass these tests locally, run `brew install graphviz` + + +import main +import pytest + + +@pytest.fixture +def client(): + main.app.testing = True + return main.app.test_client() + + +def test_empty_query_string(client): + r = client.get("/diagram.png") + assert r.status_code == 400 + + +def test_empty_dot_parameter(client): + r = client.get("/diagram.png?dot=") + assert r.status_code == 400 + + +def test_bad_dot_parameter(client): + r = client.get("/diagram.png?dot=digraph") + assert r.status_code == 400 + + +def test_good_dot_parameter(client): + r = client.get("/diagram.png?dot=digraph G { A -> {B, C, D} -> {F} }") + assert r.content_type == "image/png" diff --git a/run/system-package/requirements.txt b/run/system-package/requirements.txt new file mode 100644 index 00000000000..d166fe0cc7f --- /dev/null +++ b/run/system-package/requirements.txt @@ -0,0 +1,3 @@ +Flask==1.1.1 +pytest==5.1.3 +gunicorn==19.9.0