diff --git a/rida/views.py b/rida/views.py index 5c4eb33d..eb2cec82 100644 --- a/rida/views.py +++ b/rida/views.py @@ -28,6 +28,7 @@ This is the implementation of the orchestrator's public RESTful API. """ from flask import request, jsonify +from flask.views import View import json import logging import modulemd @@ -43,147 +44,164 @@ from rida.utils import pagination_metadata, filter_module_builds from errors import (ValidationError, Unauthorized, UnprocessableEntity, Conflict, NotFound) - -@app.route("/rida/module-builds/", methods=["POST"]) -def submit_build(): +class SubmitBuild(View): """Handles new module build submissions.""" - username = rida.auth.get_username(request.environ) - rida.auth.assert_is_packager(username, fas_kwargs=dict( - base_url=conf.fas_url, - username=conf.fas_username, - password=conf.fas_password)) + def dispatch_request(self): + username = rida.auth.get_username(request.environ) + rida.auth.assert_is_packager(username, fas_kwargs=dict( + base_url=conf.fas_url, + username=conf.fas_username, + password=conf.fas_password)) - try: - r = json.loads(request.get_data().decode("utf-8")) - except: - raise ValidationError('Invalid JSON submitted') - - if "scmurl" not in r: - raise ValidationError('Missing scmurl') - - url = r["scmurl"] - urlallowed = False - - for prefix in conf.scmurls: - - if url.startswith(prefix): - urlallowed = True - break - - if not urlallowed: - raise Unauthorized('The submitted scmurl isn\'t allowed') - - yaml = str() - td = None - try: - td = tempfile.mkdtemp() - scm = rida.scm.SCM(url, conf.scmurls) - cod = scm.checkout(td) - cofn = os.path.join(cod, (scm.name + ".yaml")) - - with open(cofn, "r") as mmdfile: - yaml = mmdfile.read() - finally: try: - if td is not None: - shutil.rmtree(td) - except Exception as e: - log.warning( - "Failed to remove temporary directory {!r}: {}".format( - td, str(e))) + r = json.loads(request.get_data().decode("utf-8")) + except: + raise ValidationError('Invalid JSON submitted') - mmd = modulemd.ModuleMetadata() - try: - mmd.loads(yaml) - except: - raise UnprocessableEntity('Invalid modulemd') + if "scmurl" not in r: + raise ValidationError('Missing scmurl') - if models.ModuleBuild.query.filter_by(name=mmd.name, version=mmd.version, release=mmd.release).first(): - raise Conflict('Module already exists') + url = r["scmurl"] + if not any(url.startswith(prefix) for prefix in conf.scmurls): + raise Unauthorized("The submitted scmurl is not allowed") - module = models.ModuleBuild.create( - db.session, - conf, - name=mmd.name, - version=mmd.version, - release=mmd.release, - modulemd=yaml, - scmurl=url, - username=username - ) - - for pkgname, pkg in mmd.components.rpms.packages.items(): + yaml = "" + td = None 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 + td = tempfile.mkdtemp() + scm = rida.scm.SCM(url, conf.scmurls) + cod = scm.checkout(td) + cofn = os.path.join(cod, (scm.name + ".yaml")) - full_url = pkg["repository"] + "?#" + pkg["commit"] + with open(cofn, "r") as mmdfile: + yaml = mmdfile.read() + finally: + try: + if td is not None: + shutil.rmtree(td) + except Exception as e: + log.warning( + "Failed to remove temporary directory {!r}: {}".format( + td, str(e))) - if not rida.scm.SCM(full_url).is_available(): - raise UnprocessableEntity("Cannot checkout %s" % pkgname) + mmd = modulemd.ModuleMetadata() + try: + mmd.loads(yaml) + except: + raise UnprocessableEntity('Invalid modulemd') - build = models.ComponentBuild( - module_id=module.id, - package=pkgname, - format="rpms", - scmurl=full_url, + if models.ModuleBuild.query.filter_by(name=mmd.name, + version=mmd.version, + release=mmd.release).first(): + raise Conflict('Module already exists') + + module = models.ModuleBuild.create( + db.session, + conf, + name=mmd.name, + version=mmd.version, + release=mmd.release, + modulemd=yaml, + scmurl=url, + username=username ) - db.session.add(build) - module.modulemd = mmd.dumps() - module.transition(conf, models.BUILD_STATES["wait"]) - db.session.add(module) - db.session.commit() - logging.info("%s submitted build of %s-%s-%s", username, mmd.name, - mmd.version, mmd.release) - return jsonify(module.json()), 201 + for pkgname, pkg in mmd.components.rpms.packages.items(): + 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"] -@app.route("/rida/module-builds/", methods=["GET"]) -def query_builds(): + if not rida.scm.SCM(full_url).is_available(): + raise UnprocessableEntity("Cannot checkout %s" % pkgname) + + build = models.ComponentBuild( + module_id=module.id, + package=pkgname, + format="rpms", + scmurl=full_url, + ) + db.session.add(build) + + module.modulemd = mmd.dumps() + module.transition(conf, models.BUILD_STATES["wait"]) + db.session.add(module) + db.session.commit() + logging.info("%s submitted build of %s-%s-%s", username, mmd.name, + mmd.version, mmd.release) + return jsonify(module.json()), 201 + +class QueryBuilds(View): """Lists all tracked module builds.""" - p_query = filter_module_builds(request) - json_data = { - 'meta': pagination_metadata(p_query) - } + def dispatch_builds_request(self): + """Lists all tracked module builds.""" + p_query = filter_module_builds(request) - verbose_flag = request.args.get('verbose', 'false') + json_data = { + 'meta': pagination_metadata(p_query) + } - if verbose_flag.lower() == 'true' or verbose_flag == '1': - json_data['items'] = [item.api_json() for item in p_query.items] - else: - json_data['items'] = [{'id': item.id, 'state': item.state} for item in p_query.items] + verbose_flag = request.args.get('verbose', 'false') - return jsonify(json_data), 200 + if verbose_flag.lower() == 'true' or verbose_flag == '1': + json_data['items'] = [item.api_json() for item in p_query.items] + else: + json_data['items'] = [{'id': item.id, 'state': item.state} for item in p_query.items] + return jsonify(json_data), 200 -@app.route("/rida/module-builds/", methods=["GET"]) -def query_build(id): - """Lists details for the specified module builds.""" - module = models.ModuleBuild.query.filter_by(id=id).first() + def dispatch_build_request(self, id): + """Lists details for the specified module builds.""" - if module: - return jsonify(module.api_json()), 200 - else: - raise NotFound('No such module found.') + module = models.ModuleBuild.query.filter_by(id=id).first() + + if module: + return jsonify(module.api_json()), 200 + else: + raise NotFound('No such module found.') + + def dispatch_request(self, id): + if id is None: + return self.dispatch_builds_request() + else: + return self.dispatch_build_request(id) + +def register_v1_api(): + """ Registers version 1 of Rida API. """ + + query_builds = QueryBuilds.as_view("query-builds") + module_builds = SubmitBuild.as_view("module-builds") + + app.add_url_rule('/rida/1/module-builds/', + view_func=module_builds, + methods=['POST']) + app.add_url_rule('/rida/1/module-builds/', + defaults={'id': None}, view_func=query_builds, + methods=['GET']) + app.add_url_rule('/rida/1/module-builds/', + view_func=query_builds, + methods=['GET']) + +register_v1_api() diff --git a/tests/test_views/test_views.py b/tests/test_views/test_views.py index ce4b64a6..34bc6e63 100644 --- a/tests/test_views/test_views.py +++ b/tests/test_views/test_views.py @@ -37,7 +37,7 @@ class TestViews(unittest.TestCase): init_data() def test_query_build(self): - rv = self.client.get('/rida/module-builds/1') + rv = self.client.get('/rida/1/module-builds/1') data = json.loads(rv.data) self.assertEquals(data['id'], 1) self.assertEquals(data['name'], 'nginx') @@ -52,29 +52,29 @@ class TestViews(unittest.TestCase): self.assertEquals(data['time_submitted'], '2016-09-03T11:23:20Z') def test_pagination_metadata(self): - rv = self.client.get('/rida/module-builds/?per_page=8&page=2') + rv = self.client.get('/rida/1/module-builds/?per_page=8&page=2') meta_data = json.loads(rv.data)['meta'] self.assertTrue( - 'rida/module-builds/?per_page=8&page=1' in meta_data['prev']) + 'rida/1/module-builds/?per_page=8&page=1' in meta_data['prev']) self.assertTrue( - 'rida/module-builds/?per_page=8&page=3' in meta_data['next']) + 'rida/1/module-builds/?per_page=8&page=3' in meta_data['next']) self.assertTrue( - 'rida/module-builds/?per_page=8&page=4' in meta_data['last']) + 'rida/1/module-builds/?per_page=8&page=4' in meta_data['last']) self.assertTrue( - 'rida/module-builds/?per_page=8&page=1' in meta_data['first']) + 'rida/1/module-builds/?per_page=8&page=1' in meta_data['first']) self.assertEquals(meta_data['total'], 30) self.assertEquals(meta_data['per_page'], 8) self.assertEquals(meta_data['pages'], 4) self.assertEquals(meta_data['page'], 2) def test_query_builds(self): - rv = self.client.get('/rida/module-builds/?per_page=2') + rv = self.client.get('/rida/1/module-builds/?per_page=2') items = json.loads(rv.data)['items'] self.assertEquals(items, [{u'state': 3, u'id': 1}, {u'state': 3, u'id': 2}]) def test_query_builds_verbose(self): - rv = self.client.get('/rida/module-builds/?per_page=2&verbose=True') + rv = self.client.get('/rida/1/module-builds/?per_page=2&verbose=True') item = json.loads(rv.data)['items'][1] self.assertEquals(item['id'], 2) self.assertEquals(item['name'], 'postgressql') @@ -90,67 +90,67 @@ class TestViews(unittest.TestCase): self.assertEquals(item['time_submitted'], '2016-09-03T12:25:33Z') def test_query_builds_filter_name(self): - rv = self.client.get('/rida/module-builds/?name=nginx') + rv = self.client.get('/rida/1/module-builds/?name=nginx') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 10) def test_query_builds_filter_completed_before(self): rv = self.client.get( - '/rida/module-builds/?completed_before=2016-09-03T11:30:00Z') + '/rida/1/module-builds/?completed_before=2016-09-03T11:30:00Z') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 2) def test_query_builds_filter_completed_after(self): rv = self.client.get( - '/rida/module-builds/?completed_after=2016-09-03T12:25:00Z') + '/rida/1/module-builds/?completed_after=2016-09-03T12:25:00Z') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 8) def test_query_builds_filter_submitted_before(self): rv = self.client.get( - '/rida/module-builds/?submitted_before=2016-09-03T12:25:00Z') + '/rida/1/module-builds/?submitted_before=2016-09-03T12:25:00Z') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 7) def test_query_builds_filter_submitted_after(self): rv = self.client.get( - '/rida/module-builds/?submitted_after=2016-09-03T12:25:00Z') + '/rida/1/module-builds/?submitted_after=2016-09-03T12:25:00Z') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 23) def test_query_builds_filter_modified_before(self): rv = self.client.get( - '/rida/module-builds/?modified_before=2016-09-03T12:25:00Z') + '/rida/1/module-builds/?modified_before=2016-09-03T12:25:00Z') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 6) def test_query_builds_filter_modified_after(self): rv = self.client.get( - '/rida/module-builds/?modified_after=2016-09-03T12:25:00Z') + '/rida/1/module-builds/?modified_after=2016-09-03T12:25:00Z') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 24) def test_query_builds_filter_owner(self): rv = self.client.get( - '/rida/module-builds/?owner=Moe%20Szyslak') + '/rida/1/module-builds/?owner=Moe%20Szyslak') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 10) def test_query_builds_filter_state(self): rv = self.client.get( - '/rida/module-builds/?state=3') + '/rida/1/module-builds/?state=3') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 20) def test_query_builds_two_filters(self): - rv = self.client.get('/rida/module-builds/?owner=Moe%20Szyslak' + rv = self.client.get('/rida/1/module-builds/?owner=Moe%20Szyslak' '&modified_after=2016-09-03T12:25:00Z') data = json.loads(rv.data) self.assertEquals(data['meta']['total'], 4) def test_query_builds_filter_invalid_date(self): rv = self.client.get( - '/rida/module-builds/?modified_after=2016-09-03T12:25:00-05:00') + '/rida/1/module-builds/?modified_after=2016-09-03T12:25:00-05:00') data = json.loads(rv.data) self.assertEquals(data['error'], 'Bad Request') self.assertEquals(data['message'], 'An invalid Zulu ISO 8601 timestamp' @@ -173,7 +173,7 @@ class TestViews(unittest.TestCase): mocked_scm.return_value.checkout = mocked_scm_checkout mocked_scm.return_value.name = 'fakemodule' - rv = self.client.post('/rida/module-builds/', data=json.dumps( + rv = self.client.post('/rida/1/module-builds/', data=json.dumps( {'scmurl': 'git://pkgs.stg.fedoraproject.org/modules/' 'testmodule.git?#68932c90de214d9d13feefbd35246a81b6cb8d49'})) data = json.loads(rv.data) @@ -194,7 +194,7 @@ class TestViews(unittest.TestCase): self.assertEquals(data['state_name'], 'wait') def test_submit_build_cert_error(self): - rv = self.client.post('/rida/module-builds/', data=json.dumps( + rv = self.client.post('/rida/1/module-builds/', data=json.dumps( {'scmurl': 'git://pkgs.stg.fedoraproject.org/modules/' 'testmodule.git?#48932b90de214d9d13feefbd35246a81b6cb8d49'})) data = json.loads(rv.data) @@ -209,11 +209,11 @@ class TestViews(unittest.TestCase): @patch('rida.auth.assert_is_packager') def test_submit_build_scm_url_error(self, mocked_assert_is_packager, mocked_get_username): - rv = self.client.post('/rida/module-builds/', data=json.dumps( + rv = self.client.post('/rida/1/module-builds/', data=json.dumps( {'scmurl': 'git://badurl.com'})) data = json.loads(rv.data) self.assertEquals( - data['message'], 'The submitted scmurl isn\'t allowed') + data['message'], 'The submitted scmurl is not allowed') self.assertEquals(data['status'], 401) self.assertEquals(data['error'], 'Unauthorized') @@ -223,7 +223,7 @@ class TestViews(unittest.TestCase): def test_submit_build_bad_modulemd(self, mocked_scm, mocked_assert_is_packager, mocked_get_username): - rv = self.client.post('/rida/module-builds/', data=json.dumps( + rv = self.client.post('/rida/1/module-builds/', data=json.dumps( {'scmurl': 'git://badurl.com'})) def mocked_scm_checkout(temp_dir): scm_dir = path.join(temp_dir, 'fakemodule') @@ -237,7 +237,7 @@ class TestViews(unittest.TestCase): mocked_scm.return_value.checkout = mocked_scm_checkout mocked_scm.return_value.name = 'fakemodule' - rv = self.client.post('/rida/module-builds/', data=json.dumps( + rv = self.client.post('/rida/1/module-builds/', data=json.dumps( {'scmurl': 'git://pkgs.stg.fedoraproject.org/modules/' 'testmodule.git?#68932c90de214d9d13feefbd35246a81b6cb8d49'})) data = json.loads(rv.data)