From a46a4ec4703068ae95cdd0e76dcc5c90c6a4f93c Mon Sep 17 00:00:00 2001 From: Merlin Mathesius Date: Mar 19 2019 11:56:47 +0000 Subject: Accept modulemd for scratch module builds as a parameter in the submitted JSON. Signed-off-by: Merlin Mathesius --- diff --git a/module_build_service/views.py b/module_build_service/views.py index 50221ac..2e3f481 100644 --- a/module_build_service/views.py +++ b/module_build_service/views.py @@ -31,6 +31,7 @@ import module_build_service.auth from flask import request, url_for from flask.views import MethodView from six import string_types +from io import BytesIO from module_build_service import app, conf, log, models, db, version, api_version as max_api_version from module_build_service.utils import ( @@ -172,10 +173,11 @@ class ModuleBuildAPI(AbstractQueryableBuildAPI): # Additional POST and DELETE handlers for modules follow. @validate_api_version() def post(self, api_version): - if hasattr(request, 'files') and "yaml" in request.files: - handler = YAMLFileHandler(request) + data = _dict_from_request(request) + if "modulemd" in data or (hasattr(request, "files") and "yaml" in request.files): + handler = YAMLFileHandler(request, data) else: - handler = SCMHandler(request) + handler = SCMHandler(request, data) if conf.no_auth is True and handler.username == "anonymous" and "owner" in handler.data: handler.username = handler.data["owner"] @@ -312,16 +314,9 @@ class ImportModuleAPI(MethodView): class BaseHandler(object): - def __init__(self, request): + def __init__(self, request, data=None): self.username, self.groups = module_build_service.auth.get_user(request) - if "multipart/form-data" in request.headers.get("Content-Type", ""): - self.data = request.form.to_dict() - else: - try: - self.data = json.loads(request.get_data().decode("utf-8")) - except Exception: - log.error('Invalid JSON submitted') - raise ValidationError('Invalid JSON submitted') + self.data = data or _dict_from_request(request) # canonicalize and validate scratch option if 'scratch' in self.data and str_to_bool(str(self.data['scratch'])): @@ -342,7 +337,8 @@ class BaseHandler(object): @property def optional_params(self): - return {k: v for k, v in self.data.items() if k not in ["owner", "scmurl", "branch"]} + return {k: v for k, v in self.data.items() if k not in + ["owner", "scmurl", "branch", "modulemd", "module_name"]} def _validate_dep_overrides_format(self, key): """ @@ -367,7 +363,13 @@ class BaseHandler(object): def validate_optional_params(self): module_build_columns = set([col.key for col in models.ModuleBuild.__table__.columns]) other_params = set([ - 'branch', 'rebuild_strategy', 'buildrequire_overrides', 'require_overrides']) + 'branch', + 'buildrequire_overrides', + 'modulemd', + 'module_name', + 'rebuild_strategy', + 'require_overrides', + ]) valid_params = other_params | module_build_columns forbidden_params = [k for k in self.data if k not in valid_params] @@ -427,23 +429,43 @@ class SCMHandler(BaseHandler): class YAMLFileHandler(BaseHandler): - def __init__(self, request): - super(YAMLFileHandler, self).__init__(request) + def __init__(self, request, data=None): + super(YAMLFileHandler, self).__init__(request, data) if not self.data['scratch'] and not conf.yaml_submit_allowed: raise Forbidden("YAML submission is not enabled") def validate(self): - if "yaml" not in request.files: + if ("modulemd" not in self.data and + (not hasattr(request, "files") or "yaml" not in request.files)): log.error('Invalid file submitted') raise ValidationError('Invalid file submitted') self.validate_optional_params() def post(self): - handle = request.files["yaml"] + if "modulemd" in self.data: + handle = BytesIO(self.data["modulemd"].encode("utf-8")) + if "module_name" in self.data and self.data["module_name"]: + handle.filename = self.data["module_name"] + else: + handle.filename = "unnamed" + else: + handle = request.files["yaml"] return submit_module_build_from_yaml(self.username, handle, optional_params=self.optional_params) +def _dict_from_request(request): + if "multipart/form-data" in request.headers.get("Content-Type", ""): + data = request.form.to_dict() + else: + try: + data = json.loads(request.get_data().decode("utf-8")) + except Exception: + log.error('Invalid JSON submitted') + raise ValidationError('Invalid JSON submitted') + return data + + def register_api(): """ Registers the MBS API. """ module_view = ModuleBuildAPI.as_view('module_builds') diff --git a/tests/test_views/test_views.py b/tests/test_views/test_views.py index 535c4bf..cc5260e 100644 --- a/tests/test_views/test_views.py +++ b/tests/test_views/test_views.py @@ -26,10 +26,9 @@ from datetime import datetime import module_build_service.scm from mock import patch, PropertyMock -from werkzeug.datastructures import FileStorage from shutil import copyfile from os import path, mkdir -from os.path import dirname +from os.path import basename, dirname, splitext from requests.utils import quote import hashlib import pytest @@ -1703,8 +1702,8 @@ class TestViews: @patch('module_build_service.scm.SCM') @patch('module_build_service.config.Config.modules_allow_scratch', new_callable=PropertyMock, return_value=True) - def test_submit_scratch_build(self, mocked_allow_scratch, mocked_scm, mocked_get_user, - api_version): + def test_submit_scratch_build( + self, mocked_allow_scratch, mocked_scm, mocked_get_user, api_version): FakeSCM(mocked_scm, 'testmodule', 'testmodule.yaml', '620ec77321b2ea7b0d67d82992dda3e1d67055b4') @@ -1760,9 +1759,8 @@ class TestViews: @patch('module_build_service.scm.SCM') @patch('module_build_service.config.Config.modules_allow_scratch', new_callable=PropertyMock, return_value=False) - def test_submit_scratch_build_not_allowed(self, mocked_allow_scratch, - mocked_scm, mocked_get_user, - api_version): + def test_submit_scratch_build_not_allowed( + self, mocked_allow_scratch, mocked_scm, mocked_get_user, api_version): FakeSCM(mocked_scm, 'testmodule', 'testmodule.yaml', '620ec77321b2ea7b0d67d82992dda3e1d67055b4') @@ -1789,21 +1787,21 @@ class TestViews: new_callable=PropertyMock, return_value=True) @patch('module_build_service.config.Config.yaml_submit_allowed', new_callable=PropertyMock, return_value=True) - def test_submit_scratch_build_with_mmd(self, mocked_allow_yaml, - mocked_allow_scratch, - mocked_get_user, - api_version): + def test_submit_scratch_build_with_mmd( + self, mocked_allow_yaml, mocked_allow_scratch, mocked_get_user, api_version): base_dir = path.abspath(path.dirname(__file__)) mmd_path = path.join(base_dir, '..', 'staged_data', 'testmodule.yaml') post_url = '/module-build-service/{0}/module-builds/'.format(api_version) with open(mmd_path, 'rb') as f: - yaml_file = FileStorage(f) - post_data = { - 'branch': 'master', - 'scratch': True, - 'yaml': yaml_file, - } - rv = self.client.post(post_url, content_type='multipart/form-data', data=post_data) + modulemd = f.read().decode('utf-8') + + post_data = { + 'branch': 'master', + 'scratch': True, + 'modulemd': modulemd, + 'module_name': str(splitext(basename(mmd_path))[0]), + } + rv = self.client.post(post_url, data=json.dumps(post_data)) data = json.loads(rv.data) if api_version >= 2: @@ -1857,13 +1855,22 @@ class TestViews: mmd_path = path.join(base_dir, '..', 'staged_data', 'testmodule.yaml') post_url = '/module-build-service/{0}/module-builds/'.format(api_version) with open(mmd_path, 'rb') as f: - yaml_file = FileStorage(f) - post_data = { - 'branch': 'master', - 'scratch': True, - 'yaml': yaml_file, - } - rv = self.client.post(post_url, content_type='multipart/form-data', data=post_data) + modulemd = f.read().decode('utf-8') + + post_data = { + 'branch': 'master', + 'scratch': True, + 'modulemd': modulemd, + 'module_name': str(splitext(basename(mmd_path))[0]), + } + rv = self.client.post(post_url, data=json.dumps(post_data)) + data = json.loads(rv.data) + + if api_version >= 2: + assert isinstance(data, list) + assert len(data) == 1 + data = data[0] + # this test is the same as the previous except YAML_SUBMIT_ALLOWED is False, # but it should still succeed since yaml is always allowed for scratch builds assert rv.status_code == 201