Convert errors to JSON

Signed-off-by: Matt Prahl <mprahl@redhat.com>
Signed-off-by: Nils Philippsen <nils@redhat.com>
This commit is contained in:
Matt Prahl
2016-08-24 15:21:44 -04:00
committed by Nils Philippsen
parent d809d34ed7
commit fbe1beee54
4 changed files with 115 additions and 46 deletions

View File

@@ -22,6 +22,7 @@
# SOFTWARE. # SOFTWARE.
# #
# Written by Petr Šabata <contyk@redhat.com> # Written by Petr Šabata <contyk@redhat.com>
# Matt Prahl <mprahl@redhat.com>
"""The module build orchestrator for Modularity. """The module build orchestrator for Modularity.
@@ -45,6 +46,8 @@ from flask_sqlalchemy import SQLAlchemy
from os import sys from os import sys
import rida.logger import rida.logger
from logging import getLogger from logging import getLogger
from rida.errors import (ValidationError, Unauthorized, UnprocessableEntity,
Conflict, NotFound, Forbidden, json_error)
app = Flask(__name__) app = Flask(__name__)
app.config.from_envvar("RIDA_SETTINGS", silent=True) app.config.from_envvar("RIDA_SETTINGS", silent=True)
@@ -58,6 +61,47 @@ else:
db = SQLAlchemy(app) db = SQLAlchemy(app)
@app.errorhandler(ValidationError)
def validationerror_error(e):
"""Flask error handler for ValidationError exceptions"""
return json_error(400, 'Bad Request', e.args[0])
@app.errorhandler(Unauthorized)
def unauthorized_error(e):
"""Flask error handler for NotAuthorized exceptions"""
return json_error(401, 'Unauthorized', e.args[0])
@app.errorhandler(Forbidden)
def forbidden_error(e):
"""Flask error handler for Forbidden exceptions"""
return json_error(403, 'Forbidden', e.args[0])
@app.errorhandler(RuntimeError)
def runtimeerror_error(e):
"""Flask error handler for RuntimeError exceptions"""
return json_error(500, 'Internal Server Error', e.args[0])
@app.errorhandler(UnprocessableEntity)
def unprocessableentity_error(e):
"""Flask error handler for UnprocessableEntity exceptions"""
return json_error(422, 'Unprocessable Entity', e.args[0])
@app.errorhandler(Conflict)
def conflict_error(e):
"""Flask error handler for Conflict exceptions"""
return json_error(409, 'Conflict', e.args[0])
@app.errorhandler(NotFound)
def notfound_error(e):
"""Flask error handler for Conflict exceptions"""
return json_error(404, 'Not Found', e.args[0])
import rida.config import rida.config
conf = rida.config.from_app_config() conf = rida.config.from_app_config()
rida.logger.init_logging(conf) rida.logger.init_logging(conf)

View File

@@ -20,6 +20,37 @@
# #
# Written by Matt Prahl <mprahl@redhat.com> # Written by Matt Prahl <mprahl@redhat.com>
""" Defines custom exceptions and error handling functions """ """ Defines custom exceptions and error handling functions """
from flask import jsonify
class ValidationError(ValueError): class ValidationError(ValueError):
pass pass
class Unauthorized(ValueError):
pass
class Forbidden(ValueError):
pass
class UnprocessableEntity(ValueError):
pass
class Conflict(ValueError):
pass
class NotFound(ValueError):
pass
def json_error(status, error, message):
response = jsonify(
{'status': status,
'error': error,
'message': message})
response.status_code = status
return response

View File

@@ -36,6 +36,7 @@ import tempfile
import shutil import shutil
from rida import log from rida import log
from rida.errors import Unauthorized, ValidationError
import rida.utils import rida.utils
@@ -62,7 +63,7 @@ class SCM(object):
:param str url: The unmodified scmurl :param str url: The unmodified scmurl
:param list allowed_scm: The list of allowed SCMs, optional :param list allowed_scm: The list of allowed SCMs, optional
:raises: RuntimeError :raises: Unauthorized or ValidationError
""" """
if allowed_scm: if allowed_scm:
@@ -70,7 +71,8 @@ class SCM(object):
if url.startswith(allowed): if url.startswith(allowed):
break break
else: else:
raise RuntimeError('%s is not in the list of allowed SCMs' % url) raise Unauthorized(
'%s is not in the list of allowed SCMs' % url)
self.url = url self.url = url
@@ -79,7 +81,7 @@ class SCM(object):
self.scheme = scmtype self.scheme = scmtype
break break
else: else:
raise RuntimeError('Invalid SCM URL: %s' % url) raise ValidationError('Invalid SCM URL: %s' % url)
if self.scheme == "git": if self.scheme == "git":
match = re.search(r"^(?P<repository>.*/(?P<name>[^?]*))(\?#(?P<commit>.*))?", url) match = re.search(r"^(?P<repository>.*/(?P<name>[^?]*))(\?#(?P<commit>.*))?", url)
@@ -89,7 +91,7 @@ class SCM(object):
self.name = self.name[:-4] self.name = self.name[:-4]
self.commit = match.group("commit") self.commit = match.group("commit")
else: else:
raise RuntimeError("Unhandled SCM scheme: %s" % self.scheme) raise ValidationError("Unhandled SCM scheme: %s" % self.scheme)
@staticmethod @staticmethod
@rida.utils.retry(wait_on=RuntimeError) @rida.utils.retry(wait_on=RuntimeError)

View File

@@ -40,7 +40,8 @@ import tempfile
from rida import app, conf, db, log from rida import app, conf, db, log
from rida import models from rida import models
from rida.utils import pagination_metadata, filter_module_builds from rida.utils import pagination_metadata, filter_module_builds
from errors import ValidationError from errors import (ValidationError, Unauthorized, UnprocessableEntity,
Conflict, NotFound)
@app.route("/rida/module-builds/", methods=["POST"]) @app.route("/rida/module-builds/", methods=["POST"])
@@ -48,15 +49,16 @@ def submit_build():
"""Handles new module build submissions.""" """Handles new module build submissions."""
username = rida.auth.is_packager(conf.pkgdb_api_url) username = rida.auth.is_packager(conf.pkgdb_api_url)
if not username: if not username:
return "You must use your Fedora certificate when submitting a new build", 403 raise Unauthorized("You must use your Fedora certificate "
"when submitting a new build")
try: try:
r = json.loads(request.get_data().decode("utf-8")) r = json.loads(request.get_data().decode("utf-8"))
except: except:
return "Invalid JSON submitted", 400 raise ValidationError('Invalid JSON submitted')
if "scmurl" not in r: if "scmurl" not in r:
return "Missing scmurl", 400 raise ValidationError('Missing scmurl')
url = r["scmurl"] url = r["scmurl"]
urlallowed = False urlallowed = False
@@ -68,7 +70,7 @@ def submit_build():
break break
if not urlallowed: if not urlallowed:
return "The submitted scmurl isn't allowed", 403 raise Unauthorized('The submitted scmurl isn\'t allowed')
yaml = str() yaml = str()
td = None td = None
@@ -80,14 +82,6 @@ def submit_build():
with open(cofn, "r") as mmdfile: with open(cofn, "r") as mmdfile:
yaml = mmdfile.read() yaml = mmdfile.read()
except Exception as e:
if "is not in the list of allowed SCMs" in str(e):
rc = 403
elif "Invalid SCM URL" in str(e):
rc = 400
else:
rc = 500
return str(e), rc
finally: finally:
try: try:
if td is not None: if td is not None:
@@ -101,10 +95,10 @@ def submit_build():
try: try:
mmd.loads(yaml) mmd.loads(yaml)
except: except:
return "Invalid modulemd", 422 raise UnprocessableEntity('Invalid modulemd')
if models.ModuleBuild.query.filter_by(name=mmd.name, version=mmd.version, release=mmd.release).first(): if models.ModuleBuild.query.filter_by(name=mmd.name, version=mmd.version, release=mmd.release).first():
return "Module already exists", 409 raise Conflict('Module already exists')
module = models.ModuleBuild.create( module = models.ModuleBuild.create(
db.session, db.session,
@@ -117,33 +111,34 @@ def submit_build():
username=username username=username
) )
def failure(message, code):
# TODO, we should make some note of why it failed in the db..
log.exception(message)
module.transition(conf, models.BUILD_STATES["failed"])
db.session.add(module)
db.session.commit()
return message, code
for pkgname, pkg in mmd.components.rpms.packages.items(): for pkgname, pkg in mmd.components.rpms.packages.items():
if pkg.get("repository") and not conf.rpms_allow_repository: try:
return failure("Custom component repositories aren't allowed", 403) if pkg.get("repository") and not conf.rpms_allow_repository:
if pkg.get("cache") and not conf.rpms_allow_cache: raise Unauthorized(
return failure("Custom component caches aren't allowed", 403) "Custom component repositories aren't allowed")
if not pkg.get("repository"): if pkg.get("cache") and not conf.rpms_allow_cache:
pkg["repository"] = conf.rpms_default_repository + pkgname raise Unauthorized("Custom component caches aren't allowed")
if not pkg.get("cache"): if not pkg.get("repository"):
pkg["cache"] = conf.rpms_default_cache + pkgname pkg["repository"] = conf.rpms_default_repository + pkgname
if not pkg.get("commit"): if not pkg.get("cache"):
try: pkg["cache"] = conf.rpms_default_cache + pkgname
pkg["commit"] = rida.scm.SCM(pkg["repository"]).get_latest() if not pkg.get("commit"):
except Exception as e: try:
return failure("Failed to get the latest commit: %s" % pkgname, 422) pkg["commit"] = rida.scm.SCM(
pkg["repository"]).get_latest()
except Exception as e:
raise UnprocessableEntity(
"Failed to get the latest commit: %s" % pkgname)
except Exception:
module.transition(conf, models.BUILD_STATES["failed"])
db.session.add(module)
db.session.commit()
raise
full_url = pkg["repository"] + "?#" + pkg["commit"] full_url = pkg["repository"] + "?#" + pkg["commit"]
if not rida.scm.SCM(full_url).is_available(): if not rida.scm.SCM(full_url).is_available():
return failure("Cannot checkout %s" % pkgname, 422) raise UnprocessableEntity("Cannot checkout %s" % pkgname)
build = models.ComponentBuild( build = models.ComponentBuild(
module_id=module.id, module_id=module.id,
@@ -165,10 +160,7 @@ def submit_build():
@app.route("/rida/module-builds/", methods=["GET"]) @app.route("/rida/module-builds/", methods=["GET"])
def query_builds(): def query_builds():
"""Lists all tracked module builds.""" """Lists all tracked module builds."""
try: p_query = filter_module_builds(request)
p_query = filter_module_builds(request)
except ValidationError as e:
return e.message, 400
json_data = { json_data = {
'meta': pagination_metadata(p_query) 'meta': pagination_metadata(p_query)
@@ -192,4 +184,4 @@ def query_build(id):
if module: if module:
return jsonify(module.api_json()), 200 return jsonify(module.api_json()), 200
else: else:
return "No such module found.", 404 raise NotFound('No such module found.')