/
entrypoint.py
181 lines (146 loc) · 6.33 KB
/
entrypoint.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# Copyright Swiss Data Science Center (SDSC). A partnership between
# École Polytechnique Fédérale de Lausanne (EPFL) and
# Eidgenössische Technische Hochschule Zürich (ETHZ).
#
# 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.
"""Renku service entry point."""
import logging
import os
import traceback
import uuid
import sentry_sdk
from flask import Flask, Response, jsonify, request, url_for
from jwt import InvalidTokenError
from prometheus_flask_exporter.multiprocess import GunicornPrometheusMetrics
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.rq import RqIntegration
from renku.core.util.util import is_test_session_running
from renku.ui.service.cache import cache
from renku.ui.service.config import CACHE_DIR, MAX_CONTENT_LENGTH, SENTRY_ENABLED, SENTRY_SAMPLERATE, SERVICE_PREFIX
from renku.ui.service.errors import (
ProgramHttpMethodError,
ProgramHttpMissingError,
ProgramHttpRequestError,
ProgramHttpServerError,
ProgramHttpTimeoutError,
ServiceError,
)
from renku.ui.service.logger import service_log
from renku.ui.service.serializers.headers import JWT_TOKEN_SECRET
from renku.ui.service.utils.json_encoder import SvcJSONEncoder
from renku.ui.service.views import error_response
from renku.ui.service.views.apispec import apispec_blueprint
from renku.ui.service.views.cache import cache_blueprint
from renku.ui.service.views.config import config_blueprint
from renku.ui.service.views.datasets import dataset_blueprint
from renku.ui.service.views.graph import graph_blueprint
from renku.ui.service.views.jobs import jobs_blueprint
from renku.ui.service.views.project import project_blueprint
from renku.ui.service.views.templates import templates_blueprint
from renku.ui.service.views.version import version_blueprint
from renku.ui.service.views.versions_list import versions_list_blueprint
from renku.ui.service.views.workflow_plans import workflow_plans_blueprint
logging.basicConfig(level=os.getenv("SERVICE_LOG_LEVEL", "WARNING"))
if SENTRY_ENABLED:
sentry_sdk.init(
dsn=os.getenv("SENTRY_DSN"),
environment=os.getenv("SENTRY_ENV"),
traces_sample_rate=float(SENTRY_SAMPLERATE),
integrations=[FlaskIntegration(), RqIntegration(), RedisIntegration()],
)
def create_app(custom_exceptions=True):
"""Creates a Flask app with a necessary configuration."""
app = Flask(__name__)
app.secret_key = os.getenv("RENKU_SVC_SERVICE_KEY", uuid.uuid4().hex)
app.json_encoder = SvcJSONEncoder
app.config["UPLOAD_FOLDER"] = CACHE_DIR
app.config["MAX_CONTENT_LENGTH"] = MAX_CONTENT_LENGTH
app.config["cache"] = cache
if not is_test_session_running():
GunicornPrometheusMetrics(app)
build_routes(app)
@app.route(SERVICE_PREFIX)
def root():
"""Root shows basic service information."""
import renku
return jsonify({"service_version": renku.__version__, "spec_url": url_for("apispec.openapi")})
@app.route("/health")
def health():
"""Service health check."""
import renku
return f"renku repository service version {renku.__version__}\n"
if custom_exceptions:
register_exceptions(app)
return app
def register_exceptions(app):
"""Register the exceptions handler."""
@app.errorhandler(Exception)
def exceptions(e):
"""Exception handler that manages Flask/Werkzeug exceptions.
For the other exception handlers check ``service/decorators.py``
"""
# NOTE: add log entry
str(getattr(e, "code", "unavailable"))
log_error_code = str(getattr(e, "code", "unavailable"))
service_log.error(
f"{request.remote_addr} {request.method} {request.scheme} {request.full_path}\n"
f"Error code: {log_error_code}\n"
f"Stack trace: {traceback.format_exc()}"
)
# NOTE: craft user messages
if hasattr(e, "code"):
code = int(e.code)
# NOTE: return an http error for methods with no body allowed. This prevents undesired exceptions.
NO_PAYLOAD_METHODS = "HEAD"
if request.method in NO_PAYLOAD_METHODS:
return Response(status=code)
error: ServiceError
if code == 400:
error = ProgramHttpRequestError(e)
elif code == 404:
error = ProgramHttpMissingError(e)
elif code == 405:
error = ProgramHttpMethodError(e)
elif code == 408:
error = ProgramHttpTimeoutError(e)
else:
error = ProgramHttpServerError(e, code)
return error_response(error)
# NOTE: Werkzeug exceptions should be covered above, the following line is for
# unexpected HTTP server errors.
return error_response(e)
def build_routes(app):
"""Register routes to given app instance."""
app.register_blueprint(workflow_plans_blueprint)
app.register_blueprint(cache_blueprint)
app.register_blueprint(config_blueprint)
app.register_blueprint(dataset_blueprint)
app.register_blueprint(graph_blueprint)
app.register_blueprint(jobs_blueprint)
app.register_blueprint(project_blueprint)
app.register_blueprint(templates_blueprint)
app.register_blueprint(version_blueprint)
app.register_blueprint(apispec_blueprint)
app.register_blueprint(versions_list_blueprint)
app = create_app()
@app.after_request
def after_request(response):
"""After request handler."""
service_log.info(f"{request.remote_addr} {request.method} {request.scheme} {request.full_path} {response.status}")
return response
if __name__ == "__main__":
if len(JWT_TOKEN_SECRET) < 32:
raise InvalidTokenError("web token must be greater or equal to 32 bytes")
app.logger.handlers.extend(service_log.handlers)
app.run()