From fbe1beee542f6c9daa7e27689e116d08245818ad Mon Sep 17 00:00:00 2001 From: Matt Prahl Date: Wed, 24 Aug 2016 15:21:44 -0400 Subject: [PATCH] Convert errors to JSON Signed-off-by: Matt Prahl Signed-off-by: Nils Philippsen --- rida/__init__.py | 44 ++++++++++++++++++++++++++++ rida/errors.py | 31 ++++++++++++++++++++ rida/scm.py | 10 ++++--- rida/views.py | 76 ++++++++++++++++++++++-------------------------- 4 files changed, 115 insertions(+), 46 deletions(-) diff --git a/rida/__init__.py b/rida/__init__.py index 6a4949de..9e1e6107 100644 --- a/rida/__init__.py +++ b/rida/__init__.py @@ -22,6 +22,7 @@ # SOFTWARE. # # Written by Petr Ĺ abata +# Matt Prahl """The module build orchestrator for Modularity. @@ -45,6 +46,8 @@ from flask_sqlalchemy import SQLAlchemy from os import sys import rida.logger from logging import getLogger +from rida.errors import (ValidationError, Unauthorized, UnprocessableEntity, + Conflict, NotFound, Forbidden, json_error) app = Flask(__name__) app.config.from_envvar("RIDA_SETTINGS", silent=True) @@ -58,6 +61,47 @@ else: 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 conf = rida.config.from_app_config() rida.logger.init_logging(conf) diff --git a/rida/errors.py b/rida/errors.py index f44c4d99..dc974150 100644 --- a/rida/errors.py +++ b/rida/errors.py @@ -20,6 +20,37 @@ # # Written by Matt Prahl """ Defines custom exceptions and error handling functions """ +from flask import jsonify + class ValidationError(ValueError): 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 diff --git a/rida/scm.py b/rida/scm.py index 5540c8dd..b1c59948 100644 --- a/rida/scm.py +++ b/rida/scm.py @@ -36,6 +36,7 @@ import tempfile import shutil from rida import log +from rida.errors import Unauthorized, ValidationError import rida.utils @@ -62,7 +63,7 @@ class SCM(object): :param str url: The unmodified scmurl :param list allowed_scm: The list of allowed SCMs, optional - :raises: RuntimeError + :raises: Unauthorized or ValidationError """ if allowed_scm: @@ -70,7 +71,8 @@ class SCM(object): if url.startswith(allowed): break 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 @@ -79,7 +81,7 @@ class SCM(object): self.scheme = scmtype break else: - raise RuntimeError('Invalid SCM URL: %s' % url) + raise ValidationError('Invalid SCM URL: %s' % url) if self.scheme == "git": match = re.search(r"^(?P.*/(?P[^?]*))(\?#(?P.*))?", url) @@ -89,7 +91,7 @@ class SCM(object): self.name = self.name[:-4] self.commit = match.group("commit") else: - raise RuntimeError("Unhandled SCM scheme: %s" % self.scheme) + raise ValidationError("Unhandled SCM scheme: %s" % self.scheme) @staticmethod @rida.utils.retry(wait_on=RuntimeError) diff --git a/rida/views.py b/rida/views.py index 0921309d..17553cca 100644 --- a/rida/views.py +++ b/rida/views.py @@ -40,7 +40,8 @@ import tempfile from rida import app, conf, db, log from rida import models 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"]) @@ -48,15 +49,16 @@ def submit_build(): """Handles new module build submissions.""" username = rida.auth.is_packager(conf.pkgdb_api_url) 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: r = json.loads(request.get_data().decode("utf-8")) except: - return "Invalid JSON submitted", 400 + raise ValidationError('Invalid JSON submitted') if "scmurl" not in r: - return "Missing scmurl", 400 + raise ValidationError('Missing scmurl') url = r["scmurl"] urlallowed = False @@ -68,7 +70,7 @@ def submit_build(): break if not urlallowed: - return "The submitted scmurl isn't allowed", 403 + raise Unauthorized('The submitted scmurl isn\'t allowed') yaml = str() td = None @@ -80,14 +82,6 @@ def submit_build(): with open(cofn, "r") as mmdfile: 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: try: if td is not None: @@ -101,10 +95,10 @@ def submit_build(): try: mmd.loads(yaml) 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(): - return "Module already exists", 409 + raise Conflict('Module already exists') module = models.ModuleBuild.create( db.session, @@ -117,33 +111,34 @@ def submit_build(): 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(): - if pkg.get("repository") and not conf.rpms_allow_repository: - return failure("Custom component repositories aren't allowed", 403) - if pkg.get("cache") and not conf.rpms_allow_cache: - return failure("Custom component caches aren't allowed", 403) - if not pkg.get("repository"): - pkg["repository"] = conf.rpms_default_repository + pkgname - if not pkg.get("cache"): - pkg["cache"] = conf.rpms_default_cache + pkgname - if not pkg.get("commit"): - try: - pkg["commit"] = rida.scm.SCM(pkg["repository"]).get_latest() - except Exception as e: - return failure("Failed to get the latest commit: %s" % pkgname, 422) + try: + if pkg.get("repository") and not conf.rpms_allow_repository: + raise Unauthorized( + "Custom component repositories aren't allowed") + if pkg.get("cache") and not conf.rpms_allow_cache: + raise Unauthorized("Custom component caches aren't allowed") + if not pkg.get("repository"): + pkg["repository"] = conf.rpms_default_repository + pkgname + if not pkg.get("cache"): + pkg["cache"] = conf.rpms_default_cache + pkgname + if not pkg.get("commit"): + try: + 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"] 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( module_id=module.id, @@ -165,10 +160,7 @@ def submit_build(): @app.route("/rida/module-builds/", methods=["GET"]) def query_builds(): """Lists all tracked module builds.""" - try: - p_query = filter_module_builds(request) - except ValidationError as e: - return e.message, 400 + p_query = filter_module_builds(request) json_data = { 'meta': pagination_metadata(p_query) @@ -192,4 +184,4 @@ def query_build(id): if module: return jsonify(module.api_json()), 200 else: - return "No such module found.", 404 + raise NotFound('No such module found.')