@fireemu/darwin-arm64 0.42.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/bin/fireemu +2 -2
- package/bin/runner-python/fireemu_runner/__init__.py +5 -0
- package/bin/runner-python/fireemu_runner/appcheck.py +255 -0
- package/bin/runner-python/fireemu_runner/bodies.py +96 -0
- package/bin/runner-python/fireemu_runner/diagnostics.py +36 -0
- package/bin/runner-python/fireemu_runner/dispatch.py +141 -0
- package/bin/runner-python/fireemu_runner/environment.py +119 -0
- package/bin/runner-python/fireemu_runner/events.py +203 -0
- package/bin/runner-python/fireemu_runner/framing.py +145 -0
- package/bin/runner-python/fireemu_runner/main.py +92 -0
- package/bin/runner-python/fireemu_runner/manifest.py +370 -0
- package/bin/runner-python/fireemu_runner/request_view.py +65 -0
- package/bin/runner-python/fireemu_runner/scheduler.py +87 -0
- package/bin/runner-python/fireemu_runner/serve.py +165 -0
- package/bin/runner-python/runner.py +19 -0
- package/package.json +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""The HTTP server for `onRequest`, `onCall`, task queue and Auth blocking functions.
|
|
2
|
+
|
|
3
|
+
The daemon proxies `/<project>/<region>/<name>/...` here with the `x-fireemu-runner-secret` header
|
|
4
|
+
(`crates/fireemu-adapter-functions/src/http.rs`, `tools/runner-node/http-admission.mjs`). The
|
|
5
|
+
function sees the path below its mount point, as it does in production, and answers through the same
|
|
6
|
+
`flask.Request` handlers the SDK wraps. An unhandled error is functions-framework's crash response.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import hmac
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import flask
|
|
15
|
+
from functions_framework import crash_handler, read_request
|
|
16
|
+
from werkzeug.serving import WSGIRequestHandler, make_server
|
|
17
|
+
|
|
18
|
+
from .diagnostics import ASYNC_HANDLER, describe, is_async_handler
|
|
19
|
+
from .environment import EnvironmentGate
|
|
20
|
+
from .manifest import DEFAULT_REGION
|
|
21
|
+
|
|
22
|
+
SECRET_ENVIRON_KEY = "HTTP_X_FIREEMU_RUNNER_SECRET"
|
|
23
|
+
# Task queue and Auth blocking functions enter through the HTTP wrapper as well (the Node runner's `signatureType`).
|
|
24
|
+
HTTP_TRIGGERS = frozenset(("http", "tasks", "blockingAuth"))
|
|
25
|
+
_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _flask_app() -> flask.Flask:
|
|
29
|
+
app = flask.Flask("fireemu-runner")
|
|
30
|
+
app.register_error_handler(500, crash_handler)
|
|
31
|
+
app.after_request(read_request)
|
|
32
|
+
|
|
33
|
+
def run(path):
|
|
34
|
+
return flask.request.environ["fireemu.function"](flask.request._get_current_object())
|
|
35
|
+
|
|
36
|
+
# "OPTIONS" is listed so that Flask does not answer a callable's preflight itself.
|
|
37
|
+
app.add_url_rule("/", endpoint="run", view_func=run, defaults={"path": ""}, methods=_METHODS)
|
|
38
|
+
app.add_url_rule("/<path:path>", endpoint="run", view_func=run, methods=_METHODS)
|
|
39
|
+
return app
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ResponseBodyFailed(Exception):
|
|
43
|
+
"""A response body failed while it was being sent. The details were logged inside the function's environment."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _log_failure(name: str, error: BaseException) -> None:
|
|
47
|
+
diagnostic, _ = describe(error)
|
|
48
|
+
sys.stderr.write(f"[functions] {name}: {diagnostic[:8192]}\n")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _HeldBody:
|
|
52
|
+
"""A response body that keeps its function's environment until the body is finished, whatever happens to it.
|
|
53
|
+
|
|
54
|
+
Closing the body, and running the callbacks `Response.call_on_close` registered, is user code: if it raises the
|
|
55
|
+
gate must still be released, or the process is locked with a secret in its environment. Errors are described
|
|
56
|
+
here, while the environment is still held, and the server sees only a plain exception.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, body, held: contextlib.ExitStack, name: str):
|
|
60
|
+
self._body = body
|
|
61
|
+
self._iterator = iter(body)
|
|
62
|
+
self._held = held
|
|
63
|
+
self._name = name
|
|
64
|
+
|
|
65
|
+
def __iter__(self):
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __next__(self):
|
|
69
|
+
try:
|
|
70
|
+
return next(self._iterator)
|
|
71
|
+
except StopIteration:
|
|
72
|
+
raise
|
|
73
|
+
except BaseException as error:
|
|
74
|
+
_log_failure(self._name, error)
|
|
75
|
+
raise ResponseBodyFailed(type(error).__name__) from None
|
|
76
|
+
|
|
77
|
+
def close(self):
|
|
78
|
+
try:
|
|
79
|
+
close = getattr(self._body, "close", None)
|
|
80
|
+
if close is not None:
|
|
81
|
+
close()
|
|
82
|
+
except BaseException as error: # the response has been sent; there is nobody to tell but the log
|
|
83
|
+
_log_failure(self._name, error)
|
|
84
|
+
finally:
|
|
85
|
+
self._held.close()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class RunnerRouter:
|
|
89
|
+
"""Admission and routing in front of the Flask app."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, functions: dict, manifest: dict, project: str, secret: str, gate: EnvironmentGate | None = None):
|
|
92
|
+
self._app = _flask_app()
|
|
93
|
+
self._gate = gate if gate is not None else EnvironmentGate({})
|
|
94
|
+
self._project = project
|
|
95
|
+
self._secret = secret.encode()
|
|
96
|
+
self._routes = {}
|
|
97
|
+
for spec in manifest["functions"]:
|
|
98
|
+
if spec["trigger"]["type"] in HTTP_TRIGGERS:
|
|
99
|
+
self._routes[spec["name"]] = (
|
|
100
|
+
spec.get("region", DEFAULT_REGION),
|
|
101
|
+
functions[spec["entryPoint"]],
|
|
102
|
+
spec["entryPoint"],
|
|
103
|
+
spec.get("platformOptions", {}).get("secrets"),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _plain(start_response, status: str, text: str):
|
|
108
|
+
body = text.encode()
|
|
109
|
+
start_response(status, [("Content-Type", "text/plain; charset=utf-8"), ("Content-Length", str(len(body)))])
|
|
110
|
+
return [body]
|
|
111
|
+
|
|
112
|
+
def __call__(self, environ, start_response):
|
|
113
|
+
presented = environ.pop(SECRET_ENVIRON_KEY, "").encode()
|
|
114
|
+
if not self._secret:
|
|
115
|
+
# A runner with no secret would accept any request without the header (an empty value equals an empty
|
|
116
|
+
# secret). The Node runner answers every request this way (`http-admission.mjs`).
|
|
117
|
+
return self._plain(start_response, "500 Internal Server Error", "FIREEMU_RUNNER_SECRET is required")
|
|
118
|
+
if not hmac.compare_digest(presented, self._secret):
|
|
119
|
+
return self._plain(start_response, "403 Forbidden", "forbidden")
|
|
120
|
+
parts = environ.get("PATH_INFO", "").split("/")
|
|
121
|
+
# ["", project, region, name, *rest]
|
|
122
|
+
if len(parts) < 4 or parts[0] != "" or parts[1] != self._project:
|
|
123
|
+
return self._plain(start_response, "404 Not Found", "no HTTP function")
|
|
124
|
+
region, name = parts[2], parts[3]
|
|
125
|
+
route = self._routes.get(name)
|
|
126
|
+
if route is None or route[0] != region:
|
|
127
|
+
return self._plain(start_response, "404 Not Found", f"no HTTP function {parts[1]}/{region}/{name}")
|
|
128
|
+
environ["SCRIPT_NAME"] = environ.get("SCRIPT_NAME", "") + "/".join(parts[:4])
|
|
129
|
+
environ["PATH_INFO"] = "/" + "/".join(parts[4:])
|
|
130
|
+
environ["fireemu.function"] = route[1]
|
|
131
|
+
if is_async_handler(route[1]):
|
|
132
|
+
# Production answers 500 as well (the coroutine is not a response); this says why.
|
|
133
|
+
sys.stderr.write(f"[functions] {name}: {ASYNC_HANDLER}\n")
|
|
134
|
+
return self._plain(start_response, "500 Internal Server Error", ASYNC_HANDLER)
|
|
135
|
+
# The environment this function is entitled to is held until its response is finished, a streamed body
|
|
136
|
+
# included: the secrets it declares are in the process environment for exactly that long.
|
|
137
|
+
held = contextlib.ExitStack()
|
|
138
|
+
held.enter_context(self._gate.hold(route[3]))
|
|
139
|
+
try:
|
|
140
|
+
# The variables a production instance has from its start name the function it serves. One process
|
|
141
|
+
# serves the whole codebase here, so they name the call that started most recently, as for events.
|
|
142
|
+
os.environ["FUNCTION_TARGET"] = route[2]
|
|
143
|
+
os.environ["FUNCTION_SIGNATURE_TYPE"] = "http"
|
|
144
|
+
os.environ["K_SERVICE"] = name
|
|
145
|
+
body = self._app(environ, start_response)
|
|
146
|
+
except BaseException as error:
|
|
147
|
+
_log_failure(name, error)
|
|
148
|
+
held.close()
|
|
149
|
+
raise ResponseBodyFailed(type(error).__name__) from None
|
|
150
|
+
return _HeldBody(body, held, name)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class _QuietHandler(WSGIRequestHandler):
|
|
154
|
+
def log_request(self, *args, **kwargs): # the daemon logs requests; the runner's stderr is for user output
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def start_server(functions: dict, manifest: dict, secret: str, gate: EnvironmentGate | None = None):
|
|
159
|
+
"""Starts the server on a free loopback port; returns `(server, port)`."""
|
|
160
|
+
router = RunnerRouter(functions, manifest, os.environ.get("GCLOUD_PROJECT", ""), secret, gate)
|
|
161
|
+
server = make_server("127.0.0.1", 0, router, threaded=True, request_handler=_QuietHandler)
|
|
162
|
+
import threading
|
|
163
|
+
|
|
164
|
+
threading.Thread(target=server.serve_forever, daemon=True, name="http").start()
|
|
165
|
+
return server, server.server_port
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Entry point of the fireemu Python runner: `python runner.py --source <dir> --codebase <name>`.
|
|
2
|
+
|
|
3
|
+
It is started by the fireemu daemon with the interpreter of the codebase's `venv` and speaks the
|
|
4
|
+
frame protocol on stdin and stdout (see `fireemu_runner/framing.py`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
+
|
|
12
|
+
from fireemu_runner.main import run # noqa: E402
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
code = run(sys.argv[1:])
|
|
16
|
+
sys.stdout.flush()
|
|
17
|
+
sys.stderr.flush()
|
|
18
|
+
# Handlers may have left non-daemon threads behind; the daemon already decided to stop us.
|
|
19
|
+
os._exit(code)
|