From 07be371c3a56e78b1d454ba3c73d8b630f535892 Mon Sep 17 00:00:00 2001 From: Tomasz Nurkiewicz Date: Wed, 17 Feb 2021 19:08:35 +0100 Subject: [PATCH 1/5] Sample output is not consistent with command (debug on vs. off) (#106) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d8d5a7e..ae6b2bf5 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ functions-framework --target hello --debug * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Debug mode: off + * Debug mode: on * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit) ``` From 147a4882218f1676ecb68fe399d23518375e8bdf Mon Sep 17 00:00:00 2001 From: sesi Date: Wed, 17 Feb 2021 13:59:01 -0500 Subject: [PATCH 2/5] Add dry-run command-line flag to README.md (#105) Co-authored-by: Dustin Ingram --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ae6b2bf5..1d0597b3 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,7 @@ You can configure the Functions Framework using command-line flags or environmen | `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http`, `event` or `cloudevent` | | `--source` | `FUNCTION_SOURCE` | The path to the file containing your function. Default: `main.py` (in the current working directory) | | `--debug` | `DEBUG` | A flag that allows to run functions-framework to run in debug mode, including live reloading. Default: `False` | +| `--dry-run` | `DRY_RUN` | A flag that allows for testing the function build from the configuration without creating a server. Default: `False` | ## Enable Google Cloud Functions Events From ad3406dde978312cecc085acb717e722f9251339 Mon Sep 17 00:00:00 2001 From: Arjun Srinivasan <69502+asriniva@users.noreply.github.com> Date: Wed, 17 Feb 2021 12:56:16 -0800 Subject: [PATCH 3/5] Add backwards-compatible logging for GCF Python 3.7 (#107) * Add backwards-compatible logging for GCF Python 3.7 * Reformatted * Update src/functions_framework/__init__.py Co-authored-by: Dustin Ingram * Restructure logging to better fit legacy behavior * Modify write behavior to account for newlines * Update LogHandler to use io.TextIOWrapper * Simplify write method * Update src/functions_framework/__init__.py Co-authored-by: Dustin Ingram Co-authored-by: Dustin Ingram --- conftest.py | 16 ++++++ src/functions_framework/__init__.py | 25 ++++++++++ tests/test_functions.py | 25 ++++++++++ .../http_check_severity/main.py | 49 +++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 tests/test_functions/http_check_severity/main.py diff --git a/conftest.py b/conftest.py index b8d44d43..21572fda 100644 --- a/conftest.py +++ b/conftest.py @@ -12,7 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import os +import sys + +from importlib import reload import pytest @@ -26,3 +30,15 @@ def isolate_environment(): finally: os.environ.clear() os.environ.update(_environ) + + +@pytest.fixture(scope="function", autouse=True) +def isolate_logging(): + "Ensure any changes to logging are isolated to individual tests" "" + try: + yield + finally: + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + logging.shutdown() + reload(logging) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 356f7a03..1e87fdb4 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -14,6 +14,7 @@ import functools import importlib.util +import io import json import os.path import pathlib @@ -65,6 +66,19 @@ def __init__( self.data = data +class _LoggingHandler(io.TextIOWrapper): + """Logging replacement for stdout and stderr in GCF Python 3.7.""" + + def __init__(self, level, stderr=sys.stderr): + io.TextIOWrapper.__init__(self, io.StringIO(), encoding=stderr.encoding) + self.level = level + self.stderr = stderr + + def write(self, out): + payload = dict(severity=self.level, message=out.rstrip("\n")) + return self.stderr.write(json.dumps(payload) + "\n") + + def _http_view_func_wrapper(function, request): def view_func(path): return function(request._get_current_object()) @@ -221,6 +235,17 @@ def handle_none(rv): app.make_response = handle_none + # Handle log severity backwards compatibility + import logging # isort:skip + + logging.info = _LoggingHandler("INFO", sys.stderr).write + logging.warn = _LoggingHandler("ERROR", sys.stderr).write + logging.warning = _LoggingHandler("ERROR", sys.stderr).write + logging.error = _LoggingHandler("ERROR", sys.stderr).write + logging.critical = _LoggingHandler("ERROR", sys.stderr).write + sys.stdout = _LoggingHandler("INFO", sys.stderr) + sys.stderr = _LoggingHandler("ERROR", sys.stderr) + # Extract the target function from the source file if not hasattr(source_module, target): raise MissingTargetException( diff --git a/tests/test_functions.py b/tests/test_functions.py index 2f0ca05b..5f746931 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -16,6 +16,7 @@ import os import pathlib import re +import sys import time import pretend @@ -495,6 +496,30 @@ def test_legacy_function_check_env(monkeypatch): assert resp.data.decode("utf-8") == target +@pytest.mark.parametrize( + "mode, expected", + [ + ("loginfo", '"severity": "INFO"'), + ("logwarn", '"severity": "ERROR"'), + ("logerr", '"severity": "ERROR"'), + ("logcrit", '"severity": "ERROR"'), + ("stdout", '"severity": "INFO"'), + ("stderr", '"severity": "ERROR"'), + ], +) +def test_legacy_function_log_severity(monkeypatch, capfd, mode, expected): + source = TEST_FUNCTIONS_DIR / "http_check_severity" / "main.py" + target = "function" + + monkeypatch.setenv("ENTRY_POINT", target) + + client = create_app(target, source).test_client() + resp = client.post("/", json={"mode": mode}) + captured = capfd.readouterr().err + assert resp.status_code == 200 + assert expected in captured + + def test_legacy_function_returns_none(monkeypatch): source = TEST_FUNCTIONS_DIR / "returns_none" / "main.py" target = "function" diff --git a/tests/test_functions/http_check_severity/main.py b/tests/test_functions/http_check_severity/main.py new file mode 100644 index 00000000..be586d8d --- /dev/null +++ b/tests/test_functions/http_check_severity/main.py @@ -0,0 +1,49 @@ +# 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. + +"""Function used in Worker tests of legacy GCF Python 3.7 logging.""" +import logging +import os +import sys + +X_GOOGLE_FUNCTION_NAME = "gcf-function" +X_GOOGLE_ENTRY_POINT = "function" +HOME = "/tmp" + + +def function(request): + """Test function which logs to the appropriate output. + + Args: + request: The HTTP request which triggered this function. Must contain name + of the requested output in the 'mode' field in JSON document + in request body. + + Returns: + Value of the mode. + """ + name = request.get_json().get("mode") + if name == "stdout": + print("log") + elif name == "stderr": + print("log", file=sys.stderr) + elif name == "loginfo": + logging.info("log") + elif name == "logwarn": + logging.warning("log") + elif name == "logerr": + logging.error("log") + elif name == "logcrit": + logging.critical("log") + return name From c247550fced1a3e6dfd4aad9e7517f402ad5e633 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 17 Feb 2021 16:06:01 -0600 Subject: [PATCH 4/5] Add example of developing multiple functions locally (#99) * Add example of developing multiple functions locally * Apply suggestions from code review --- examples/skaffold/README.md | 139 +++++++++++++++++++++++++++++ examples/skaffold/k8s/goodbye.yaml | 47 ++++++++++ examples/skaffold/k8s/hello.yaml | 47 ++++++++++ examples/skaffold/k8s/ingress.yaml | 30 +++++++ examples/skaffold/main.py | 23 +++++ examples/skaffold/requirements.txt | 1 + examples/skaffold/skaffold.yaml | 28 ++++++ 7 files changed, 315 insertions(+) create mode 100644 examples/skaffold/README.md create mode 100644 examples/skaffold/k8s/goodbye.yaml create mode 100644 examples/skaffold/k8s/hello.yaml create mode 100644 examples/skaffold/k8s/ingress.yaml create mode 100644 examples/skaffold/main.py create mode 100644 examples/skaffold/requirements.txt create mode 100644 examples/skaffold/skaffold.yaml diff --git a/examples/skaffold/README.md b/examples/skaffold/README.md new file mode 100644 index 00000000..d115e30a --- /dev/null +++ b/examples/skaffold/README.md @@ -0,0 +1,139 @@ +# Developing multiple functions on the same host using Minikube and Skaffold + +## Introduction + +This example shows you how to develop multiple Cloud Functions to a single host +using Minikube and Skaffold. + +The example will focus on: +* taking two separate Cloud Functions (defined in the same file) +* building them each individually with Cloud Buildpacks and the Functions Framework +* deploying them to a local Kubernetes cluster with `minikube` and `skaffold` +* including live reloading! + +## Install `minikube` +*Note: If on Cloud Shell, `minikube` is pre-installed.* + +Install `minikube` via the instructions for your platform at + +Confirm that `minikube` is installed: + +```bash +minikube version +``` + +You should see output similar to: + +```terminal +minikube version: v1.15.1 +commit: 23f40a012abb52eff365ff99a709501a61ac5876 +``` + +## Start `minikube` + +This starts `minikube` using the default profile: + +```bash +minikube start +``` + +This may take a few minutes. + +*Note: If on Cloud Shell, you may be asked to enable Cloud Shell to make API calls* + +You should see output similar to: + +```terminal +😄 minikube v1.15.1 on Debian 10.6 + ▪ MINIKUBE_FORCE_SYSTEMD=true + ▪ MINIKUBE_HOME=/google/minikube + ▪ MINIKUBE_WANTUPDATENOTIFICATION=false +✨ Automatically selected the docker driver +👍 Starting control plane node minikube in cluster minikube +🚜 Pulling base image ... +💾 Downloading Kubernetes v1.19.4 preload ... +🔥 Creating docker container (CPUs=2, Memory=4000MB) ... +🐳 Preparing Kubernetes v1.19.4 on Docker 19.03.13 ... +🔎 Verifying Kubernetes components... +🌟 Enabled addons: storage-provisioner, default-storageclass +🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default +``` + +## Install the `ingress` addon for `minikube` + +This allows `minikube` to handle external traffic: + +```bash +minikube addons enable ingress +``` + +You should see output similar to: + +```terminal +🔎 Verifying ingress addon... +🌟 The 'ingress' addon is enabled +``` + +## Install `skaffold` +*Note: If on Cloud Shell, `skaffold` is pre-installed.* + +Install `skaffold` via the instructions for your platform at + +Confirm that `skaffold` is installed: + +```bash +skaffold version +``` + +You should see output similar to: + +```terminal +v1.16.0 +``` + +## Start `skaffold` + +Start `skaffold` with: + +```bash +skaffold dev +``` + +You should see output similar to: + +```terminal +Starting deploy... +Waiting for deployments to stabilize... + - deployment/hello is ready. [1/2 deployment(s) still pending] + - deployment/goodbye is ready. +Deployments stabilized in 1.154162006s +Watching for changes... +``` + +This command will continue running indefinitely, watching for changes and redeploying as necessary. + +## Call your Cloud Functions + +Leaving the previous command running, in a **new terminal**, call your functions. To call the `hello` function: + +```bash +curl `minikube ip`/hello +``` + +You should see output similar to: + +```terminal +Hello, World! +``` + +To call the `goodbye` function: + +```bash +curl `minikube ip`/goodbye +``` + +You should see output similar to: + +```terminal +Goodbye, World! +``` diff --git a/examples/skaffold/k8s/goodbye.yaml b/examples/skaffold/k8s/goodbye.yaml new file mode 100644 index 00000000..6a891230 --- /dev/null +++ b/examples/skaffold/k8s/goodbye.yaml @@ -0,0 +1,47 @@ +# 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. + +apiVersion: v1 +kind: Service +metadata: + name: goodbye +spec: + ports: + - port: 8080 + name: http + type: LoadBalancer + selector: + app: goodbye +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: goodbye +spec: + selector: + matchLabels: + app: goodbye + template: + metadata: + labels: + app: goodbye + spec: + containers: + - name: goodbye + image: example-goodbye-image + env: + - name: PORT + value: "8080" + ports: + - containerPort: 8080 diff --git a/examples/skaffold/k8s/hello.yaml b/examples/skaffold/k8s/hello.yaml new file mode 100644 index 00000000..68570ff6 --- /dev/null +++ b/examples/skaffold/k8s/hello.yaml @@ -0,0 +1,47 @@ +# 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. + +apiVersion: v1 +kind: Service +metadata: + name: hello +spec: + ports: + - port: 8080 + name: http + type: LoadBalancer + selector: + app: hello +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello +spec: + selector: + matchLabels: + app: hello + template: + metadata: + labels: + app: hello + spec: + containers: + - name: hello + image: example-hello-image + env: + - name: PORT + value: "8080" + ports: + - containerPort: 8080 diff --git a/examples/skaffold/k8s/ingress.yaml b/examples/skaffold/k8s/ingress.yaml new file mode 100644 index 00000000..0abc2d6e --- /dev/null +++ b/examples/skaffold/k8s/ingress.yaml @@ -0,0 +1,30 @@ +# 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. + +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: scaffold-example-ingress +spec: + rules: + - http: + paths: + - path: /hello + backend: + serviceName: hello + servicePort: 8080 + - path: /goodbye + backend: + serviceName: goodbye + servicePort: 8080 diff --git a/examples/skaffold/main.py b/examples/skaffold/main.py new file mode 100644 index 00000000..298249be --- /dev/null +++ b/examples/skaffold/main.py @@ -0,0 +1,23 @@ +# 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. + + +def hello(request): + """Return a friendly HTTP greeting.""" + return "Hello, World!" + + +def goodbye(request): + """Return a friendly HTTP goodbye.""" + return "Goodbye, World!" diff --git a/examples/skaffold/requirements.txt b/examples/skaffold/requirements.txt new file mode 100644 index 00000000..3601409f --- /dev/null +++ b/examples/skaffold/requirements.txt @@ -0,0 +1 @@ +# Add any Python requirements here diff --git a/examples/skaffold/skaffold.yaml b/examples/skaffold/skaffold.yaml new file mode 100644 index 00000000..ca668226 --- /dev/null +++ b/examples/skaffold/skaffold.yaml @@ -0,0 +1,28 @@ +# 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. + +apiVersion: skaffold/v2beta9 +kind: Config +build: + artifacts: + - image: example-hello-image + buildpacks: + builder: "gcr.io/buildpacks/builder:v1" + env: + - "GOOGLE_FUNCTION_TARGET=hello" + - image: example-goodbye-image + buildpacks: + builder: "gcr.io/buildpacks/builder:v1" + env: + - "GOOGLE_FUNCTION_TARGET=goodbye" From 348064d8fdea14c21aec39fdada1e8907898be12 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 17 Feb 2021 17:25:28 -0600 Subject: [PATCH 5/5] Version 2.1.1 (#112) * Update changelog * Version 2.1.1 --- CHANGELOG.md | 10 +++++++++- README.md | 2 +- setup.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ae4d9d..c4ec7480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.1.1] - 2021-02-17 +### Fixed +- Add backwards-compatible logging for GCF Python 3.7 ([#107]) +- Document `--dry-run` flag ([#105]) + ## [2.1.0] - 2020-12-23 ### Added - Support Python 3.9 @@ -85,7 +90,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release -[Unreleased]: https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v2.1.0...HEAD +[Unreleased]: https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v2.1.1...HEAD +[2.1.1]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v2.1.1 [2.1.0]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v2.1.0 [2.0.0]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v2.0.0 [1.6.0]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v1.6.0 @@ -102,6 +108,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [1.0.1]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v1.0.1 [1.0.0]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v1.0.0 +[#107]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/107 +[#105]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/105 [#77]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/77 [#76]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/76 [#70]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/70 diff --git a/README.md b/README.md index 1d0597b3..312aff46 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ pip install functions-framework Or, for deployment, add the Functions Framework to your `requirements.txt` file: ``` -functions-framework==2.1.0 +functions-framework==2.1.1 ``` ## Quickstarts diff --git a/setup.py b/setup.py index e03eed6f..63176dfa 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="2.1.0", + version="2.1.1", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown",