From 598347e1b6f67e44dc12e3339953e703579aa454 Mon Sep 17 00:00:00 2001 From: Filip Valder Date: Tue, 24 Jul 2018 11:52:28 +0200 Subject: [PATCH] Import module API --- README.rst | 69 +++++++ conf/config.py | 4 + module_build_service/config.py | 6 +- module_build_service/utils/general.py | 52 +++++- module_build_service/utils/submit.py | 8 +- module_build_service/utils/views.py | 10 +- module_build_service/views.py | 66 ++++++- tests/scm_data/mariadb/HEAD | 1 + tests/scm_data/mariadb/config | 4 + tests/scm_data/mariadb/description | 1 + tests/scm_data/mariadb/info/exclude | 6 + tests/scm_data/mariadb/info/refs | 1 + tests/scm_data/mariadb/objects/info/packs | 2 + ...fbbdbf4fa07dc9cab035120eb248da930e0bd6.idx | Bin 0 -> 2584 bytes ...bbdbf4fa07dc9cab035120eb248da930e0bd6.pack | Bin 0 -> 36836 bytes tests/scm_data/mariadb/packed-refs | 2 + .../scm_data/mariadb/refs/heads/.placeholder | 0 tests/scm_data/mariadb/refs/tags/.placeholder | 0 tests/test_scm.py | 35 ++-- tests/test_views/test_views.py | 175 ++++++++++++++++++ 20 files changed, 400 insertions(+), 42 deletions(-) create mode 100644 tests/scm_data/mariadb/HEAD create mode 100644 tests/scm_data/mariadb/config create mode 100644 tests/scm_data/mariadb/description create mode 100644 tests/scm_data/mariadb/info/exclude create mode 100644 tests/scm_data/mariadb/info/refs create mode 100644 tests/scm_data/mariadb/objects/info/packs create mode 100644 tests/scm_data/mariadb/objects/pack/pack-92fbbdbf4fa07dc9cab035120eb248da930e0bd6.idx create mode 100644 tests/scm_data/mariadb/objects/pack/pack-92fbbdbf4fa07dc9cab035120eb248da930e0bd6.pack create mode 100644 tests/scm_data/mariadb/packed-refs create mode 100644 tests/scm_data/mariadb/refs/heads/.placeholder create mode 100644 tests/scm_data/mariadb/refs/tags/.placeholder diff --git a/README.rst b/README.rst index 0d5b217f..b885721a 100644 --- a/README.rst +++ b/README.rst @@ -667,6 +667,75 @@ parameters include: - ``task_id`` +Import module +------------- + +Importing of modules is done via posting the SCM URL of a repository +which contains the generated modulemd YAML file. Name, stream, version, +context and other important information must be present in the metadata. + +:: + + POST /module-build-service/1/import-module/ + +:: + + { + "scmurl": "git://pkgs.fedoraproject.org/modules/foo.git?#21f92fb05572d81d78fd9a27d313942d45055840" + } + + +If the module build is imported successfully, JSON containing the most +important information is returned from MBS. The JSON also contains log +messages collected during the import. + +:: + + HTTP 201 Created + +:: + + { + "module": { + "component_builds": [], + "context": "00000000", + "id": 3, + "koji_tag": "", + "name": "mariadb", + "owner": "mbs_import", + "rebuild_strategy": "all", + "scmurl": null, + "siblings": [], + "state": 5, + "state_name": "ready", + "state_reason": null, + "stream": "10.2", + "time_completed": "2018-07-24T12:58:14Z", + "time_modified": "2018-07-24T12:58:14Z", + "time_submitted": "2018-07-24T12:58:14Z", + "version": "20180724000000" + }, + "messages": [ + "Updating existing module build mariadb:10.2:20180724000000:00000000.", + "Module mariadb:10.2:20180724000000:00000000 imported" + ] + } + + +If the module import fails, an error message is returned. + +:: + + HTTP 422 Unprocessable Entity + +:: + + { + "error": "Unprocessable Entity", + "message": "Incomplete NSVC: None:None:0:00000000" + } + + Listing about ------------- diff --git a/conf/config.py b/conf/config.py index 117d29ed..6d92b73f 100644 --- a/conf/config.py +++ b/conf/config.py @@ -62,6 +62,8 @@ class BaseConfiguration(object): # 'modularity-wg', ]) + ALLOWED_GROUPS_TO_IMPORT_MODULE = set() + # Available backends are: console and file LOG_BACKEND = 'console' @@ -120,6 +122,8 @@ class TestConfiguration(BaseConfiguration): AUTH_METHOD = 'oidc' RESOLVER = 'db' + ALLOWED_GROUPS_TO_IMPORT_MODULE = set(['mbs-import-module']) + class ProdConfiguration(BaseConfiguration): pass diff --git a/module_build_service/config.py b/module_build_service/config.py index 494ab0b1..a6c25d0f 100644 --- a/module_build_service/config.py +++ b/module_build_service/config.py @@ -247,6 +247,10 @@ class Config(object): 'type': set, 'default': set(['packager']), 'desc': 'The set of groups allowed to submit builds.'}, + 'allowed_groups_to_import_module': { + 'type': set, + 'default': set(), + 'desc': 'The set of groups allowed to import module builds.'}, 'log_backend': { 'type': str, 'default': None, @@ -350,7 +354,7 @@ class Config(object): 'yaml_submit_allowed': { 'type': bool, 'default': False, - 'desc': 'Is it allowed to directly submit modulemd yaml file?'}, + 'desc': 'Is it allowed to directly submit build by modulemd yaml file?'}, 'num_concurrent_builds': { 'type': int, 'default': 0, diff --git a/module_build_service/utils/general.py b/module_build_service/utils/general.py index 4dee4887..8b761391 100644 --- a/module_build_service/utils/general.py +++ b/module_build_service/utils/general.py @@ -29,7 +29,8 @@ import time from datetime import datetime from module_build_service import conf, log, models -from module_build_service.errors import ValidationError, ProgrammingError +from module_build_service.errors import ( + ValidationError, ProgrammingError, UnprocessableEntity) def scm_url_schemes(terse=False): @@ -258,6 +259,10 @@ def import_mmd(session, mmd): the module, we have no idea what build_context or runtime_context is - we only know the resulting "context", but there is no way to store it into do DB. By now, we just ignore mmd.get_context() and use default 00000000 context instead. + + :return: module build (ModuleBuild), + log messages collected during import (list) + :rtype: tuple """ mmd.set_context("00000000") name = mmd.get_name() @@ -265,22 +270,33 @@ def import_mmd(session, mmd): version = str(mmd.get_version()) context = mmd.get_context() + # Log messages collected during import + msgs = [] + # NSVC is used for logging purpose later. - nsvc = ":".join([name, stream, version, context]) + try: + nsvc = ":".join([name, stream, version, context]) + except TypeError: + msg = "Incomplete NSVC: {}:{}:{}:{}".format(name, stream, version, context) + log.error(msg) + raise UnprocessableEntity(msg) # Get the koji_tag. - xmd = mmd.get_xmd() - if "mbs" in xmd.keys() and "koji_tag" in xmd["mbs"].keys(): + try: + xmd = mmd.get_xmd() koji_tag = xmd["mbs"]["koji_tag"] - else: - log.warn("'koji_tag' is not set in xmd['mbs'] for module %s", nsvc) - koji_tag = "" + except KeyError: + msg = "'koji_tag' is not set in xmd['mbs'] for module {}".format(nsvc) + log.error(msg) + raise UnprocessableEntity(msg) # Get the ModuleBuild from DB. build = models.ModuleBuild.get_build_from_nsvc( session, name, stream, version, context) if build: - log.info("Updating existing module build %s.", nsvc) + msg = "Updating existing module build {}.".format(nsvc) + log.info(msg) + msgs.append(msg) else: build = models.ModuleBuild() @@ -298,4 +314,22 @@ def import_mmd(session, mmd): build.time_completed = datetime.utcnow() session.add(build) session.commit() - log.info("Module %s imported", nsvc) + msg = "Module {} imported".format(nsvc) + log.info(msg) + msgs.append(msg) + + return build, msgs + + +def get_mmd_from_scm(url): + """ + Provided an SCM URL, fetch mmd from the corresponding module YAML + file. If ref is specified within the URL, the mmd will be returned + as of the ref. + """ + from module_build_service.utils.submit import _fetch_mmd + + mmd, _ = _fetch_mmd(url, branch=None, allow_local_url=False, + whitelist_url=False, mandatory_checks=False) + + return mmd diff --git a/module_build_service/utils/submit.py b/module_build_service/utils/submit.py index 47271928..95f30607 100644 --- a/module_build_service/utils/submit.py +++ b/module_build_service/utils/submit.py @@ -462,7 +462,8 @@ def _is_eol_in_pdc(name, stream): return not results[0]['active'] -def _fetch_mmd(url, branch=None, allow_local_url=False, whitelist_url=False): +def _fetch_mmd(url, branch=None, allow_local_url=False, whitelist_url=False, + mandatory_checks=True): # Import it here, because SCM uses utils methods # and fails to import them because of dep-chain. import module_build_service.scm @@ -477,7 +478,7 @@ def _fetch_mmd(url, branch=None, allow_local_url=False, whitelist_url=False): else: scm = module_build_service.scm.SCM(url, branch, conf.scmurls, allow_local_url) scm.checkout(td) - if not whitelist_url: + if not whitelist_url and mandatory_checks: scm.verify() cofn = scm.get_module_yaml() mmd = load_mmd(cofn, is_file=True) @@ -495,6 +496,9 @@ def _fetch_mmd(url, branch=None, allow_local_url=False, whitelist_url=False): raise ValidationError( 'Module {}:{} is marked as EOL in PDC.'.format(scm.name, scm.branch)) + if not mandatory_checks: + return mmd, scm + # If the name was set in the modulemd, make sure it matches what the scmurl # says it should be if mmd.get_name() and mmd.get_name() != scm.name: diff --git a/module_build_service/utils/views.py b/module_build_service/utils/views.py index ea0f9458..de6acd27 100644 --- a/module_build_service/utils/views.py +++ b/module_build_service/utils/views.py @@ -36,10 +36,14 @@ from .general import scm_url_schemes def get_scm_url_re(): + """ + Returns a regular expression for SCM URL extraction and validation. + """ schemes_re = '|'.join(map(re.escape, scm_url_schemes(terse=True))) - return re.compile( - r"(?P(?:(?P(" + schemes_re + r"))://(?P[^/]+))?" - r"(?P/[^\?]+))\?(?P[^#]*)#(?P.+)") + regex = ( + r"(?P(?P(?:" + schemes_re + r"))://(?P[^/]+)?" + r"(?P/[^\?]+))(?:\?(?P[^#]+)?)?#(?P.+)") + return re.compile(regex) def pagination_metadata(p_query, api_version, request_args): diff --git a/module_build_service/views.py b/module_build_service/views.py index 38ab8a00..ca2a8c8a 100644 --- a/module_build_service/views.py +++ b/module_build_service/views.py @@ -36,7 +36,8 @@ from module_build_service import app, conf, log, models, db, version, api_versio from module_build_service.utils import ( pagination_metadata, filter_module_builds, filter_component_builds, submit_module_build_from_scm, submit_module_build_from_yaml, - get_scm_url_re, cors_header, validate_api_version) + get_scm_url_re, cors_header, validate_api_version, import_mmd, + get_mmd_from_scm) from module_build_service.errors import ( ValidationError, Forbidden, NotFound, ProgrammingError) from module_build_service.backports import jsonify @@ -86,6 +87,12 @@ api_routes = { 'options': { 'methods': ['GET'] } + }, + 'import_module': { + 'url': '/module-build-service//import-module/', + 'options': { + 'methods': ['POST'], + } } } @@ -152,6 +159,12 @@ class ModuleBuildAPI(AbstractQueryableBuildAPI): query_filter = staticmethod(filter_module_builds) model = models.ModuleBuild + @staticmethod + def check_groups(username, groups, allowed_groups=conf.allowed_groups): + if allowed_groups and not (allowed_groups & groups): + raise Forbidden("%s is not in any of %r, only %r" % ( + username, allowed_groups, groups)) + # Additional POST and DELETE handlers for modules follow. @validate_api_version() def post(self, api_version): @@ -163,9 +176,7 @@ class ModuleBuildAPI(AbstractQueryableBuildAPI): if conf.no_auth is True and handler.username == "anonymous" and "owner" in handler.data: handler.username = handler.data["owner"] - if conf.allowed_groups and not (conf.allowed_groups & handler.groups): - raise Forbidden("%s is not in any of %r, only %r" % ( - handler.username, conf.allowed_groups, handler.groups)) + self.check_groups(handler.username, handler.groups) handler.validate() modules = handler.post() @@ -193,9 +204,7 @@ class ModuleBuildAPI(AbstractQueryableBuildAPI): elif username == "anonymous": username = r["owner"] - if conf.allowed_groups and not (conf.allowed_groups & groups): - raise Forbidden("%s is not in any of %r, only %r" % ( - username, conf.allowed_groups, groups)) + self.check_groups(username, groups) module = models.ModuleBuild.query.filter_by(id=id).first() if not module: @@ -268,6 +277,36 @@ class RebuildStrategies(MethodView): return jsonify({'items': items}), 200 +class ImportModuleAPI(MethodView): + + @validate_api_version() + def post(self, api_version): + # disable this API endpoint if no groups are defined + if not conf.allowed_groups_to_import_module: + log.error( + "Import module API is disabled. Set 'ALLOWED_GROUPS_TO_IMPORT_MODULE'" + " configuration value first.") + raise Forbidden( + "Import module API is disabled.") + + # auth checks + username, groups = module_build_service.auth.get_user(request) + ModuleBuildAPI.check_groups(username, groups, + allowed_groups=conf.allowed_groups_to_import_module) + + # process request using SCM handler + handler = SCMHandler(request) + handler.validate(skip_branch=True, skip_optional_params=True) + + mmd = get_mmd_from_scm(handler.data["scmurl"]) + build, messages = import_mmd(db.session, mmd) + json_data = {"module": build.json(show_tasks=False), + "messages": messages} + + # return 201 Created if we reach this point + return jsonify(json_data), 201 + + class BaseHandler(object): def __init__(self, request): self.username, self.groups = module_build_service.auth.get_user(request) @@ -310,7 +349,7 @@ class SCMHandler(BaseHandler): log.error('Invalid JSON submitted') raise ValidationError('Invalid JSON submitted') - def validate(self): + def validate(self, skip_branch=False, skip_optional_params=False): if "scmurl" not in self.data: log.error('Missing scmurl') raise ValidationError('Missing scmurl') @@ -325,11 +364,12 @@ class SCMHandler(BaseHandler): log.error("The submitted scmurl %r is not valid" % url) raise Forbidden("The submitted scmurl %s is not valid" % url) - if "branch" not in self.data: + if not skip_branch and "branch" not in self.data: log.error('Missing branch') raise ValidationError('Missing branch') - self.validate_optional_params() + if not skip_optional_params: + self.validate_optional_params() def post(self): url = self.data["scmurl"] @@ -369,6 +409,7 @@ def register_api(): component_view = ComponentBuildAPI.as_view('component_builds') about_view = AboutAPI.as_view('about') rebuild_strategies_view = RebuildStrategies.as_view('rebuild_strategies') + import_module = ImportModuleAPI.as_view('import_module') for key, val in api_routes.items(): if key.startswith('component_build'): app.add_url_rule(val['url'], @@ -390,6 +431,11 @@ def register_api(): endpoint=key, view_func=rebuild_strategies_view, **val['options']) + elif key == 'import_module': + app.add_url_rule(val['url'], + endpoint=key, + view_func=import_module, + **val['options']) else: raise NotImplementedError("Unhandled api key.") diff --git a/tests/scm_data/mariadb/HEAD b/tests/scm_data/mariadb/HEAD new file mode 100644 index 00000000..cb089cd8 --- /dev/null +++ b/tests/scm_data/mariadb/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/scm_data/mariadb/config b/tests/scm_data/mariadb/config new file mode 100644 index 00000000..07d359d0 --- /dev/null +++ b/tests/scm_data/mariadb/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/tests/scm_data/mariadb/description b/tests/scm_data/mariadb/description new file mode 100644 index 00000000..498b267a --- /dev/null +++ b/tests/scm_data/mariadb/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/scm_data/mariadb/info/exclude b/tests/scm_data/mariadb/info/exclude new file mode 100644 index 00000000..a5196d1b --- /dev/null +++ b/tests/scm_data/mariadb/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/scm_data/mariadb/info/refs b/tests/scm_data/mariadb/info/refs new file mode 100644 index 00000000..0d61723d --- /dev/null +++ b/tests/scm_data/mariadb/info/refs @@ -0,0 +1 @@ +9ab5fdeba83eb3382413ee8bc06299344ef4477d refs/heads/master diff --git a/tests/scm_data/mariadb/objects/info/packs b/tests/scm_data/mariadb/objects/info/packs new file mode 100644 index 00000000..3be3c0bd --- /dev/null +++ b/tests/scm_data/mariadb/objects/info/packs @@ -0,0 +1,2 @@ +P pack-92fbbdbf4fa07dc9cab035120eb248da930e0bd6.pack + diff --git a/tests/scm_data/mariadb/objects/pack/pack-92fbbdbf4fa07dc9cab035120eb248da930e0bd6.idx b/tests/scm_data/mariadb/objects/pack/pack-92fbbdbf4fa07dc9cab035120eb248da930e0bd6.idx new file mode 100644 index 0000000000000000000000000000000000000000..9874aa77b2ae57e54cb1fd485f646267b723b6ec GIT binary patch literal 2584 zcmcK6doK@;|F(1P)&?1%Yv2R?v>vCbFJg+02P zjQ!(BB>6?93dF5ElCW2CL7qEWtH@ibvi{4pF?(yr}dzocdvSNjUyMM7QBPA8_|~9X5P}@OzrK| zp3`ZX-No6dQW(TnMBk&zoK~7MTDFKB!4c@COgEB>kOJmRqW6=)bnT7;$73hAnDE{( z7P0!R$XN1L-sZ00fJP#^H?o*z_pSbz%@Qkg#$Q{LG`+R;(07cph1U%t%zK^(9t)2c z?Vyxk{PKLc17)US(jLv+$rugM+H>F2hm7gs);XyQnpytMh{tXy6p_pl!mOL zwL1Mnhh809;p)i}bta|n_URKUSk9}JhubH_rS_CA*tf6d9&WTTD%DQdr0LSCSeMhV zMf50M>h7Zb=7@{8&PRnkeq6sS_;lc3!AaAJ4v#`&WJ!!`Xvf_H(n`8dEqz%|PA|5l zj|Bvu*guF3UGO?vR2W0jalV>=InH5T;J;nZU$OBIcM9S=(Gv## z;Jly#c8FO=tdkY~qpu}`Jdrz^bEmuS8+?LlRiI;k(^#T)>VJgYE2_Sg5lhYKEuVZ= zougGSEH@J#zPhE2?foc~>y%E+aNj3rZ#1jH5K#8af1ffT!RBHD%;l{D5&MEi$i!2v`#Ea0 z56Q(dkwlYoZkgUuw5*6$Dn?Vld09_Ryj$5vvS8Wr0fQh;3|K{C!Jq)@9J36W)pg;xtjDiN%e#qku29@r!SYJ7LKWq95ol= zrCC=QTy+@ZX|3!uw0UarD-Q1-#J3Pv;FwB0T+(PcdYLJij?-z$iqARlZ*%!GDcIor z&cJvS{uS161T9hE6#!HfQnO{FFuM}to;UUP%$wY?cysD@zw_+G7$d>Lel5NQ0NfD( z2nKMS03Z^^0r1<$=L~>DA^^$t064={{}BLGBl!PW1M{{3_}K$++7;H50KoqMz>!`6 zNbnnoe+__3H0+1pJ*pRcX05^XCVWbbz}c|p&^Z9YG~gWg&LF^haBzMg7l0^uA3uCE zSloka0|5S}@a#nZ?2f|wVJ?UbS37v$6#zVP;Jki#AFK)10uTcC{@HNWX#iq40FaAe vAMW}}ueXjV>Un&748$4A0I-f*H!jBLQ-G7m_q-N&*S_L@l5Uu literal 0 HcmV?d00001 diff --git a/tests/scm_data/mariadb/objects/pack/pack-92fbbdbf4fa07dc9cab035120eb248da930e0bd6.pack b/tests/scm_data/mariadb/objects/pack/pack-92fbbdbf4fa07dc9cab035120eb248da930e0bd6.pack new file mode 100644 index 0000000000000000000000000000000000000000..32249ad145b4925019067222ef35ebb774996e1a GIT binary patch literal 36836 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 literal 0 HcmV?d00001 diff --git a/tests/scm_data/mariadb/packed-refs b/tests/scm_data/mariadb/packed-refs new file mode 100644 index 00000000..8cfe6fad --- /dev/null +++ b/tests/scm_data/mariadb/packed-refs @@ -0,0 +1,2 @@ +# pack-refs with: peeled fully-peeled +9ab5fdeba83eb3382413ee8bc06299344ef4477d refs/heads/master diff --git a/tests/scm_data/mariadb/refs/heads/.placeholder b/tests/scm_data/mariadb/refs/heads/.placeholder new file mode 100644 index 00000000..e69de29b diff --git a/tests/scm_data/mariadb/refs/tags/.placeholder b/tests/scm_data/mariadb/refs/tags/.placeholder new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_scm.py b/tests/test_scm.py index 0b639f3a..d79901d0 100644 --- a/tests/test_scm.py +++ b/tests/test_scm.py @@ -30,7 +30,8 @@ from mock import patch import module_build_service.scm from module_build_service.errors import ValidationError, UnprocessableEntity -repo_path = 'file://' + os.path.dirname(__file__) + "/scm_data/testrepo" +base_dir = os.path.join(os.path.dirname(__file__), 'scm_data') +repo_url = 'file://' + base_dir + '/testrepo' class TestSCMModule: @@ -45,14 +46,14 @@ class TestSCMModule: def test_simple_local_checkout(self): """ See if we can clone a local git repo. """ - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) scm.checkout(self.tempdir) files = os.listdir(self.repodir) assert 'foo' in files, "foo not in %r" % files def test_local_get_latest_is_sane(self): """ See that a hash is returned by scm.get_latest. """ - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) latest = scm.get_latest('master') target = '5481faa232d66589e660cc301179867fb00842c9' assert latest == target, "%r != %r" % (latest, target) @@ -62,7 +63,7 @@ class TestSCMModule: https://pagure.io/fm-orchestrator/issue/329 """ - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) assert scm.scheme == 'git', scm.scheme fname = tempfile.mktemp(suffix='mbs-scm-test') try: @@ -71,70 +72,70 @@ class TestSCMModule: assert not os.path.exists(fname), "%r exists! Vulnerable." % fname def test_local_extract_name(self): - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) target = 'testrepo' assert scm.name == target, '%r != %r' % (scm.name, target) def test_local_extract_name_trailing_slash(self): - scm = module_build_service.scm.SCM(repo_path + '/') + scm = module_build_service.scm.SCM(repo_url + '/') target = 'testrepo' assert scm.name == target, '%r != %r' % (scm.name, target) def test_verify(self): - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) scm.checkout(self.tempdir) scm.verify() def test_verify_unknown_branch(self): with pytest.raises(UnprocessableEntity): - module_build_service.scm.SCM(repo_path, "unknown") + module_build_service.scm.SCM(repo_url, "unknown") def test_verify_commit_in_branch(self): target = '7035bd33614972ac66559ac1fdd019ff6027ad21' - scm = module_build_service.scm.SCM(repo_path + "?#" + target, "dev") + scm = module_build_service.scm.SCM(repo_url + "?#" + target, "dev") scm.checkout(self.tempdir) scm.verify() def test_verify_commit_not_in_branch(self): target = '7035bd33614972ac66559ac1fdd019ff6027ad21' - scm = module_build_service.scm.SCM(repo_path + "?#" + target, "master") + scm = module_build_service.scm.SCM(repo_url + "?#" + target, "master") scm.checkout(self.tempdir) with pytest.raises(ValidationError): scm.verify() def test_verify_unknown_hash(self): target = '7035bd33614972ac66559ac1fdd019ff6027ad22' - scm = module_build_service.scm.SCM(repo_path + "?#" + target, "master") + scm = module_build_service.scm.SCM(repo_url + "?#" + target, "master") with pytest.raises(UnprocessableEntity): scm.checkout(self.tempdir) def test_get_module_yaml(self): - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) scm.checkout(self.tempdir) scm.verify() with pytest.raises(UnprocessableEntity): scm.get_module_yaml() def test_get_latest_incorrect_component_branch(self): - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) with pytest.raises(UnprocessableEntity): scm.get_latest('foobar') def test_get_latest_component_branch(self): ref = "5481faa232d66589e660cc301179867fb00842c9" branch = "master" - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) commit = scm.get_latest(branch) assert commit == ref def test_get_latest_component_ref(self): ref = "5481faa232d66589e660cc301179867fb00842c9" - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) commit = scm.get_latest(ref) assert commit == ref def test_get_latest_incorrect_component_ref(self): - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) with pytest.raises(UnprocessableEntity): scm.get_latest('15481faa232d66589e660cc301179867fb00842c9') @@ -146,6 +147,6 @@ class TestSCMModule: 10a651f39911a07d85fe87fcfe91999545e44ae0\trefs/remotes/origin/master """ mock_run.return_value = (0, output, '') - scm = module_build_service.scm.SCM(repo_path) + scm = module_build_service.scm.SCM(repo_url) commit = scm.get_latest(None) assert commit == '58379ef7887cbc91b215bacd32430628c92bc869' diff --git a/tests/test_views/test_views.py b/tests/test_views/test_views.py index 06a5937e..4788e49f 100644 --- a/tests/test_views/test_views.py +++ b/tests/test_views/test_views.py @@ -33,6 +33,7 @@ import hashlib import pytest from tests import app, init_data, clean_database, reuse_component_init_data +from tests.test_scm import base_dir as scm_base_dir from module_build_service.errors import UnprocessableEntity from module_build_service.models import ModuleBuild from module_build_service import db, version, Modulemd @@ -43,6 +44,7 @@ import module_build_service.scheduler.handlers.modules user = ('Homer J. Simpson', set(['packager'])) other_user = ('some_other_user', set(['packager'])) anonymous_user = ('anonymous', set(['packager'])) +import_module_user = ('Import M. King', set(['mbs-import-module'])) base_dir = dirname(dirname(__file__)) @@ -1191,3 +1193,176 @@ class TestViews: def test_cors_header_decorator(self): rv = self.client.get('/module-build-service/1/module-builds/') assert rv.headers['Access-Control-Allow-Origin'] == '*' + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=user) + @patch.object(module_build_service.config.Config, 'allowed_groups_to_import_module', + new_callable=PropertyMock, return_value=set()) + def test_import_build_disabled(self, mocked_groups, mocked_get_user, api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post(post_url) + data = json.loads(rv.data) + + assert data['error'] == 'Forbidden' + assert data['message'] == ( + 'Import module API is disabled.') + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=user) + def test_import_build_user_not_allowed(self, mocked_get_user, api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post(post_url) + data = json.loads(rv.data) + + assert data['error'] == 'Forbidden' + assert data['message'] == ( + 'Homer J. Simpson is not in any of ' + 'set([\'mbs-import-module\']), only set([\'packager\'])') + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=import_module_user) + def test_import_build_scm_invalid_json(self, mocked_get_user, api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post(post_url, data='') + data = json.loads(rv.data) + + assert data['error'] == 'Bad Request' + assert data['message'] == 'Invalid JSON submitted' + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=import_module_user) + def test_import_build_scm_url_not_allowed(self, mocked_get_user, api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post( + post_url, + data=json.dumps({'scmurl': 'file://' + scm_base_dir + '/mariadb'})) + data = json.loads(rv.data) + + assert data['error'] == 'Forbidden' + assert data['message'].startswith('The submitted scmurl ') + assert data['message'].endswith('/tests/scm_data/mariadb is not allowed') + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=import_module_user) + @patch.object(module_build_service.config.Config, 'allow_custom_scmurls', + new_callable=PropertyMock, return_value=True) + def test_import_build_scm_url_not_in_list(self, mocked_scmurls, mocked_get_user, + api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post( + post_url, + data=json.dumps({'scmurl': 'file://' + scm_base_dir + ( + '/mariadb?#b17bea85de2d03558f24d506578abcfcf467e5bc')})) + data = json.loads(rv.data) + + assert data['error'] == 'Forbidden' + assert data['message'].endswith( + '/tests/scm_data/mariadb?#b17bea85de2d03558f24d506578abcfcf467e5bc ' + 'is not in the list of allowed SCMs') + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=import_module_user) + @patch.object(module_build_service.config.Config, 'scmurls', + new_callable=PropertyMock, return_value=['file://']) + def test_import_build_scm(self, mocked_scmurls, mocked_get_user, api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post( + post_url, + data=json.dumps({'scmurl': 'file://' + scm_base_dir + ( + '/mariadb?#7cf8fb26db8dbfea075eb5f898cc053139960250')})) + data = json.loads(rv.data) + + assert 'Module mariadb:10.2:20180724000000:00000000 imported' in data['messages'] + assert data['module']['name'] == 'mariadb' + assert data['module']['stream'] == '10.2' + assert data['module']['version'] == '20180724000000' + assert data['module']['context'] == '00000000' + assert data['module']['owner'] == 'mbs_import' + assert data['module']['state'] == 5 + assert data['module']['state_reason'] is None + assert data['module']['state_name'] == 'ready' + assert data['module']['scmurl'] is None + assert data['module']['component_builds'] == [] + assert data['module']['time_submitted'] == data['module']['time_modified'] == \ + data['module']['time_completed'] + assert data['module']['koji_tag'] == 'mariadb-10.2-20180724000000-00000000' + assert data['module']['siblings'] == [] + assert data['module']['rebuild_strategy'] == 'all' + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=import_module_user) + @patch.object(module_build_service.config.Config, 'scmurls', + new_callable=PropertyMock, return_value=['file://']) + def test_import_build_scm_another_commit_hash(self, mocked_scmurls, mocked_get_user, + api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post( + post_url, + data=json.dumps({'scmurl': 'file://' + scm_base_dir + ( + '/mariadb?#1a43ea22cd32f235c2f119de1727a37902a49f20')})) + data = json.loads(rv.data) + + assert 'Module mariadb:10.2:20180724065109:00000000 imported' in data['messages'] + assert data['module']['name'] == 'mariadb' + assert data['module']['stream'] == '10.2' + assert data['module']['version'] == '20180724065109' + assert data['module']['context'] == '00000000' + assert data['module']['owner'] == 'mbs_import' + assert data['module']['state'] == 5 + assert data['module']['state_reason'] is None + assert data['module']['state_name'] == 'ready' + assert data['module']['scmurl'] is None + assert data['module']['component_builds'] == [] + assert data['module']['time_submitted'] == data['module']['time_modified'] == \ + data['module']['time_completed'] + assert data['module']['koji_tag'] == 'mariadb-10.2-20180724065109-00000000' + assert data['module']['siblings'] == [] + assert data['module']['rebuild_strategy'] == 'all' + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=import_module_user) + @patch.object(module_build_service.config.Config, 'scmurls', + new_callable=PropertyMock, return_value=['file://']) + def test_import_build_scm_incomplete_nsvc(self, mocked_scmurls, mocked_get_user, + api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post( + post_url, + data=json.dumps({'scmurl': 'file://' + scm_base_dir + ( + '/mariadb?#b17bea85de2d03558f24d506578abcfcf467e5bc')})) + data = json.loads(rv.data) + + assert data['error'] == 'Unprocessable Entity' + assert data['message'] == 'Incomplete NSVC: None:None:0:00000000' + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=import_module_user) + @patch.object(module_build_service.config.Config, 'scmurls', + new_callable=PropertyMock, return_value=['file://']) + def test_import_build_scm_yaml_is_bad(self, mocked_scmurls, mocked_get_user, + api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post( + post_url, + data=json.dumps({'scmurl': 'file://' + scm_base_dir + ( + '/mariadb?#cb7cf7069059141e0797ad2cf5a559fb673ef43d')})) + data = json.loads(rv.data) + + assert data['error'] == 'Unprocessable Entity' + assert data['message'].startswith('The following invalid modulemd was encountered') + + @pytest.mark.parametrize('api_version', [1, 2]) + @patch('module_build_service.auth.get_user', return_value=import_module_user) + @patch.object(module_build_service.config.Config, 'scmurls', + new_callable=PropertyMock, return_value=['file://']) + def test_import_build_scm_missing_koji_tag(self, mocked_scmurls, mocked_get_user, + api_version): + post_url = '/module-build-service/{0}/import-module/'.format(api_version) + rv = self.client.post( + post_url, + data=json.dumps({'scmurl': 'file://' + scm_base_dir + ( + '/mariadb?#9ab5fdeba83eb3382413ee8bc06299344ef4477d')})) + data = json.loads(rv.data) + + assert data['error'] == 'Unprocessable Entity' + assert data['message'].startswith('\'koji_tag\' is not set in xmd[\'mbs\'] for module')