|
1 |
| -# Copyright 2020 Google LLC |
| 1 | +# Copyright 2023 The OpenFunction Authors. |
2 | 2 | #
|
3 | 3 | # Licensed under the Apache License, Version 2.0 (the "License");
|
4 | 4 | # you may not use this file except in compliance with the License.
|
|
11 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12 | 12 | # See the License for the specific language governing permissions and
|
13 | 13 | # limitations under the License.
|
14 |
| - |
15 |
| -import functools |
16 |
| -import io |
17 |
| -import json |
18 |
| -import logging |
19 |
| -import os.path |
20 |
| -import pathlib |
21 |
| -import sys |
22 |
| - |
23 |
| -import cloudevents.exceptions as cloud_exceptions |
24 |
| -import flask |
25 |
| -import werkzeug |
26 |
| - |
27 |
| -from cloudevents.http import from_http, is_binary |
28 |
| - |
29 |
| -from functions_framework import _function_registry, event_conversion |
30 |
| -from functions_framework.background_event import BackgroundEvent |
31 |
| -from functions_framework.exceptions import ( |
32 |
| - EventConversionException, |
33 |
| - FunctionsFrameworkException, |
34 |
| - MissingSourceException, |
35 |
| -) |
36 |
| -from google.cloud.functions.context import Context |
37 |
| -from openfunction.dapr_output_middleware import dapr_output_middleware |
38 |
| -from openfunction.async_server import AsyncApp |
39 |
| - |
40 |
| -MAX_CONTENT_LENGTH = 10 * 1024 * 1024 |
41 |
| - |
42 |
| -_FUNCTION_STATUS_HEADER_FIELD = "X-Google-Status" |
43 |
| -_CRASH = "crash" |
44 |
| - |
45 |
| -_CLOUDEVENT_MIME_TYPE = "application/cloudevents+json" |
46 |
| - |
47 |
| - |
48 |
| -class _LoggingHandler(io.TextIOWrapper): |
49 |
| - """Logging replacement for stdout and stderr in GCF Python 3.7.""" |
50 |
| - |
51 |
| - def __init__(self, level, stderr=sys.stderr): |
52 |
| - io.TextIOWrapper.__init__(self, io.StringIO(), encoding=stderr.encoding) |
53 |
| - self.level = level |
54 |
| - self.stderr = stderr |
55 |
| - |
56 |
| - def write(self, out): |
57 |
| - payload = dict(severity=self.level, message=out.rstrip("\n")) |
58 |
| - return self.stderr.write(json.dumps(payload) + "\n") |
59 |
| - |
60 |
| - |
61 |
| -def cloud_event(func): |
62 |
| - """Decorator that registers cloudevent as user function signature type.""" |
63 |
| - _function_registry.REGISTRY_MAP[ |
64 |
| - func.__name__ |
65 |
| - ] = _function_registry.CLOUDEVENT_SIGNATURE_TYPE |
66 |
| - |
67 |
| - @functools.wraps(func) |
68 |
| - def wrapper(*args, **kwargs): |
69 |
| - return func(*args, **kwargs) |
70 |
| - |
71 |
| - return wrapper |
72 |
| - |
73 |
| - |
74 |
| -def http(func): |
75 |
| - """Decorator that registers http as user function signature type.""" |
76 |
| - _function_registry.REGISTRY_MAP[ |
77 |
| - func.__name__ |
78 |
| - ] = _function_registry.HTTP_SIGNATURE_TYPE |
79 |
| - |
80 |
| - @functools.wraps(func) |
81 |
| - def wrapper(*args, **kwargs): |
82 |
| - return func(*args, **kwargs) |
83 |
| - |
84 |
| - return wrapper |
85 |
| - |
86 |
| - |
87 |
| -def setup_logging(): |
88 |
| - logging.getLogger().setLevel(logging.INFO) |
89 |
| - info_handler = logging.StreamHandler(sys.stdout) |
90 |
| - info_handler.setLevel(logging.NOTSET) |
91 |
| - info_handler.addFilter(lambda record: record.levelno <= logging.INFO) |
92 |
| - logging.getLogger().addHandler(info_handler) |
93 |
| - |
94 |
| - warn_handler = logging.StreamHandler(sys.stderr) |
95 |
| - warn_handler.setLevel(logging.WARNING) |
96 |
| - logging.getLogger().addHandler(warn_handler) |
97 |
| - |
98 |
| - |
99 |
| -def setup_logging_level(debug): |
100 |
| - if debug: |
101 |
| - logging.getLogger().setLevel(logging.DEBUG) |
102 |
| - |
103 |
| - |
104 |
| -def _http_view_func_wrapper(function, request): |
105 |
| - def view_func(path): |
106 |
| - return function(request._get_current_object()) |
107 |
| - |
108 |
| - return view_func |
109 |
| - |
110 |
| - |
111 |
| -def _run_cloud_event(function, request): |
112 |
| - data = request.get_data() |
113 |
| - event = from_http(request.headers, data) |
114 |
| - function(event) |
115 |
| - |
116 |
| - |
117 |
| -def _cloud_event_view_func_wrapper(function, request): |
118 |
| - def view_func(path): |
119 |
| - ce_exception = None |
120 |
| - event = None |
121 |
| - try: |
122 |
| - event = from_http(request.headers, request.get_data()) |
123 |
| - except ( |
124 |
| - cloud_exceptions.MissingRequiredFields, |
125 |
| - cloud_exceptions.InvalidRequiredFields, |
126 |
| - ) as e: |
127 |
| - ce_exception = e |
128 |
| - |
129 |
| - if not ce_exception: |
130 |
| - function(event) |
131 |
| - return "OK" |
132 |
| - |
133 |
| - # Not a CloudEvent. Try converting to a CloudEvent. |
134 |
| - try: |
135 |
| - function(event_conversion.background_event_to_cloud_event(request)) |
136 |
| - except EventConversionException as e: |
137 |
| - flask.abort( |
138 |
| - 400, |
139 |
| - description=( |
140 |
| - "Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent but" |
141 |
| - " parsing CloudEvent failed and converting from background event to" |
142 |
| - f" CloudEvent also failed.\nGot HTTP headers: {request.headers}\nGot" |
143 |
| - f" data: {request.get_data()}\nGot CloudEvent exception: {repr(ce_exception)}" |
144 |
| - f"\nGot background event conversion exception: {repr(e)}" |
145 |
| - ), |
146 |
| - ) |
147 |
| - return "OK" |
148 |
| - |
149 |
| - return view_func |
150 |
| - |
151 |
| - |
152 |
| -def _event_view_func_wrapper(function, request): |
153 |
| - def view_func(path): |
154 |
| - if event_conversion.is_convertable_cloud_event(request): |
155 |
| - # Convert this CloudEvent to the equivalent background event data and context. |
156 |
| - data, context = event_conversion.cloud_event_to_background_event(request) |
157 |
| - function(data, context) |
158 |
| - elif is_binary(request.headers): |
159 |
| - # Support CloudEvents in binary content mode, with data being the |
160 |
| - # whole request body and context attributes retrieved from request |
161 |
| - # headers. |
162 |
| - data = request.get_data() |
163 |
| - context = Context( |
164 |
| - eventId=request.headers.get("ce-eventId"), |
165 |
| - timestamp=request.headers.get("ce-timestamp"), |
166 |
| - eventType=request.headers.get("ce-eventType"), |
167 |
| - resource=request.headers.get("ce-resource"), |
168 |
| - ) |
169 |
| - function(data, context) |
170 |
| - else: |
171 |
| - # This is a regular CloudEvent |
172 |
| - event_data = event_conversion.marshal_background_event_data(request) |
173 |
| - if not event_data: |
174 |
| - flask.abort(400) |
175 |
| - event_object = BackgroundEvent(**event_data) |
176 |
| - data = event_object.data |
177 |
| - context = Context(**event_object.context) |
178 |
| - function(data, context) |
179 |
| - |
180 |
| - return "OK" |
181 |
| - |
182 |
| - return view_func |
183 |
| - |
184 |
| - |
185 |
| -def _configure_app(app, function, signature_type, func_context): |
186 |
| - # Mount the function at the root. Support GCF's default path behavior |
187 |
| - # Modify the url_map and view_functions directly here instead of using |
188 |
| - # add_url_rule in order to create endpoints that route all methods |
189 |
| - if signature_type == _function_registry.HTTP_SIGNATURE_TYPE: |
190 |
| - app.url_map.add( |
191 |
| - werkzeug.routing.Rule("/", defaults={"path": ""}, endpoint="run") |
192 |
| - ) |
193 |
| - app.url_map.add(werkzeug.routing.Rule("/robots.txt", endpoint="error")) |
194 |
| - app.url_map.add(werkzeug.routing.Rule("/favicon.ico", endpoint="error")) |
195 |
| - app.url_map.add(werkzeug.routing.Rule("/<path:path>", endpoint="run")) |
196 |
| - app.view_functions["run"] = _http_view_func_wrapper(function, flask.request) |
197 |
| - app.view_functions["error"] = lambda: flask.abort(404, description="Not Found") |
198 |
| - app.after_request(read_request) |
199 |
| - app.after_request(dapr_output_middleware(func_context)) |
200 |
| - elif signature_type == _function_registry.BACKGROUNDEVENT_SIGNATURE_TYPE: |
201 |
| - app.url_map.add( |
202 |
| - werkzeug.routing.Rule( |
203 |
| - "/", defaults={"path": ""}, endpoint="run", methods=["POST"] |
204 |
| - ) |
205 |
| - ) |
206 |
| - app.url_map.add( |
207 |
| - werkzeug.routing.Rule("/<path:path>", endpoint="run", methods=["POST"]) |
208 |
| - ) |
209 |
| - app.view_functions["run"] = _event_view_func_wrapper(function, flask.request) |
210 |
| - # Add a dummy endpoint for GET / |
211 |
| - app.url_map.add(werkzeug.routing.Rule("/", endpoint="get", methods=["GET"])) |
212 |
| - app.view_functions["get"] = lambda: "" |
213 |
| - elif signature_type == _function_registry.CLOUDEVENT_SIGNATURE_TYPE: |
214 |
| - app.url_map.add( |
215 |
| - werkzeug.routing.Rule( |
216 |
| - "/", defaults={"path": ""}, endpoint=signature_type, methods=["POST"] |
217 |
| - ) |
218 |
| - ) |
219 |
| - app.url_map.add( |
220 |
| - werkzeug.routing.Rule( |
221 |
| - "/<path:path>", endpoint=signature_type, methods=["POST"] |
222 |
| - ) |
223 |
| - ) |
224 |
| - |
225 |
| - app.view_functions[signature_type] = _cloud_event_view_func_wrapper( |
226 |
| - function, flask.request |
227 |
| - ) |
228 |
| - else: |
229 |
| - raise FunctionsFrameworkException( |
230 |
| - "Invalid signature type: {signature_type}".format( |
231 |
| - signature_type=signature_type |
232 |
| - ) |
233 |
| - ) |
234 |
| - |
235 |
| - |
236 |
| -def read_request(response): |
237 |
| - """ |
238 |
| - Force the framework to read the entire request before responding, to avoid |
239 |
| - connection errors when returning prematurely. |
240 |
| - """ |
241 |
| - |
242 |
| - flask.request.get_data() |
243 |
| - return response |
244 |
| - |
245 |
| - |
246 |
| -def crash_handler(e): |
247 |
| - """ |
248 |
| - Return crash header to allow logging 'crash' message in logs. |
249 |
| - """ |
250 |
| - return str(e), 500, {_FUNCTION_STATUS_HEADER_FIELD: _CRASH} |
251 |
| - |
252 |
| -def create_async_app(target=None, source=None, func_context=None, debug=False): |
253 |
| - target = _function_registry.get_function_target(target) |
254 |
| - source = _function_registry.get_function_source(source) |
255 |
| - |
256 |
| - if not os.path.exists(source): |
257 |
| - raise MissingSourceException( |
258 |
| - "File {source} that is expected to define function doesn't exist".format( |
259 |
| - source=source |
260 |
| - ) |
261 |
| - ) |
262 |
| - |
263 |
| - source_module, spec = _function_registry.load_function_module(source) |
264 |
| - spec.loader.exec_module(source_module) |
265 |
| - |
266 |
| - function = _function_registry.get_user_function(source, source_module, target) |
267 |
| - |
268 |
| - setup_logging_level(debug) |
269 |
| - |
270 |
| - async_app = AsyncApp(func_context) |
271 |
| - async_app.bind(function) |
272 |
| - |
273 |
| - return async_app.app |
274 |
| - |
275 |
| - |
276 |
| -def create_app(target=None, source=None, signature_type=None, func_context=None, debug=False): |
277 |
| - target = _function_registry.get_function_target(target) |
278 |
| - source = _function_registry.get_function_source(source) |
279 |
| - |
280 |
| - # Set the template folder relative to the source path |
281 |
| - # Python 3.5: join does not support PosixPath |
282 |
| - template_folder = str(pathlib.Path(source).parent / "templates") |
283 |
| - |
284 |
| - if not os.path.exists(source): |
285 |
| - raise MissingSourceException( |
286 |
| - "File {source} that is expected to define function doesn't exist".format( |
287 |
| - source=source |
288 |
| - ) |
289 |
| - ) |
290 |
| - |
291 |
| - source_module, spec = _function_registry.load_function_module(source) |
292 |
| - |
293 |
| - # Create the application |
294 |
| - _app = flask.Flask(target, template_folder=template_folder) |
295 |
| - _app.config["MAX_CONTENT_LENGTH"] = MAX_CONTENT_LENGTH |
296 |
| - _app.register_error_handler(500, crash_handler) |
297 |
| - global errorhandler |
298 |
| - errorhandler = _app.errorhandler |
299 |
| - |
300 |
| - # Handle legacy GCF Python 3.7 behavior |
301 |
| - if os.environ.get("ENTRY_POINT"): |
302 |
| - os.environ["FUNCTION_NAME"] = os.environ.get("K_SERVICE", target) |
303 |
| - _app.make_response_original = _app.make_response |
304 |
| - |
305 |
| - def handle_none(rv): |
306 |
| - if rv is None: |
307 |
| - rv = "OK" |
308 |
| - return _app.make_response_original(rv) |
309 |
| - |
310 |
| - _app.make_response = handle_none |
311 |
| - |
312 |
| - # Handle log severity backwards compatibility |
313 |
| - sys.stdout = _LoggingHandler("INFO", sys.stderr) |
314 |
| - sys.stderr = _LoggingHandler("ERROR", sys.stderr) |
315 |
| - setup_logging() |
316 |
| - |
317 |
| - setup_logging_level(debug) |
318 |
| - |
319 |
| - # Execute the module, within the application context |
320 |
| - with _app.app_context(): |
321 |
| - spec.loader.exec_module(source_module) |
322 |
| - |
323 |
| - # Get the configured function signature type |
324 |
| - signature_type = _function_registry.get_func_signature_type(target, signature_type) |
325 |
| - function = _function_registry.get_user_function(source, source_module, target) |
326 |
| - |
327 |
| - _configure_app(_app, function, signature_type, func_context) |
328 |
| - |
329 |
| - return _app |
330 |
| - |
331 |
| - |
332 |
| -class LazyWSGIApp: |
333 |
| - """ |
334 |
| - Wrap the WSGI app in a lazily initialized wrapper to prevent initialization |
335 |
| - at import-time |
336 |
| - """ |
337 |
| - |
338 |
| - def __init__(self, target=None, source=None, signature_type=None, func_context=None, debug=False): |
339 |
| - # Support HTTP frameworks which support WSGI callables. |
340 |
| - # Note: this ability is currently broken in Gunicorn 20.0, and |
341 |
| - # environment variables should be used for configuration instead: |
342 |
| - # https://github.com/benoitc/gunicorn/issues/2159 |
343 |
| - self.target = target |
344 |
| - self.source = source |
345 |
| - self.signature_type = signature_type |
346 |
| - self.func_context = func_context |
347 |
| - self.debug = debug |
348 |
| - |
349 |
| - # Placeholder for the app which will be initialized on first call |
350 |
| - self.app = None |
351 |
| - |
352 |
| - def __call__(self, *args, **kwargs): |
353 |
| - if not self.app: |
354 |
| - self.app = create_app(self.target, self.source, self.signature_type, self.func_context, self.debug) |
355 |
| - return self.app(*args, **kwargs) |
356 |
| - |
357 |
| - |
358 |
| -app = LazyWSGIApp() |
359 |
| - |
360 |
| - |
361 |
| -class DummyErrorHandler: |
362 |
| - def __init__(self): |
363 |
| - pass |
364 |
| - |
365 |
| - def __call__(self, *args, **kwargs): |
366 |
| - return self |
367 |
| - |
368 |
| - |
369 |
| -errorhandler = DummyErrorHandler() |
0 commit comments