diff --git a/README.rst b/README.rst index 262aad4f..a351b50f 100644 --- a/README.rst +++ b/README.rst @@ -128,6 +128,8 @@ Options: rebuilt). For the available options, please look at the "Rebuild Strategies" section below. - ``require_overrides`` - the requires to override the modulemd with. The overrides must be to existing requires on the modulemd. The expected format is ``{'platform': ['f28', 'f29']}``. +- ``reuse_components_from`` - the ID or NSVC of the module build to reuse components from. If it's + not set, MBS will try to find a compatible module build to reuse components from. - ``scratch`` - a boolean indicating if a scratch module build should be performed. Only allowed to be ``True`` if the MBS setting ``MODULES_ALLOW_SCRATCH`` is ``True``. - ``srpms`` - an optional list of Koji upload URLs of SRPMs to include in a module scratch build. @@ -357,6 +359,7 @@ parameters:: "modulemd": "...." "name": "testmodule", "owner": "mprahl", + "reused_module_id": 121, "scmurl": "https://src.fedoraproject.org/modules/testmodule.git?#86d9cfe53d20118d863ae051641fc3784d91d981", "state": 5, "state_name": "ready", @@ -479,6 +482,7 @@ parameters include: - ``new_repo_task_id`` - ``owner`` - ``rebuild_strategy`` +- ``reuse_components_from`` - the compatible module that was used for component reuse - ``scmurl`` - ``state`` - Can be the state name or the state ID e.g. ``state=done``. This parameter can be given multiple times, in which case or-ing will be used. diff --git a/docs/REBUILD_STRATEGIES.rst b/docs/REBUILD_STRATEGIES.rst index c542e1c5..cdad857c 100644 --- a/docs/REBUILD_STRATEGIES.rst +++ b/docs/REBUILD_STRATEGIES.rst @@ -75,6 +75,9 @@ module with the following requirements: Additionally, if the rebuild strategy for the module being built is ``changed-and-after``, then the module to reuse components from will have a rebuild strategy of ``changed-and-after`` or ``all``. +If the user wants to specify the compatible module, they can use the ``reuse_components_from`` +parameter. + How the Rebuild Strategies Work =============================== diff --git a/module_build_service/models.py b/module_build_service/models.py index e383bd2c..8c122f48 100644 --- a/module_build_service/models.py +++ b/module_build_service/models.py @@ -877,6 +877,7 @@ class ModuleBuild(MBSBase): "build_context": self.build_context, "modulemd": self.modulemd, "ref_build_context": self.ref_build_context, + "reused_module_id": self.reused_module_id, "runtime_context": self.runtime_context, "state_trace": [ { diff --git a/module_build_service/utils/submit.py b/module_build_service/utils/submit.py index c754bdf9..e8a64b60 100644 --- a/module_build_service/utils/submit.py +++ b/module_build_service/utils/submit.py @@ -1014,6 +1014,7 @@ def submit_module_build(username, mmd, params): scmurl=params.get("scmurl"), username=username, rebuild_strategy=params.get("rebuild_strategy"), + reused_module_id=params.get("reuse_components_from"), scratch=params.get("scratch"), srpms=params.get("srpms"), ) diff --git a/module_build_service/views.py b/module_build_service/views.py index 86ad995e..f98beeb3 100644 --- a/module_build_service/views.py +++ b/module_build_service/views.py @@ -305,6 +305,7 @@ class BaseHandler(object): "module_name", "owner", "rebuild_strategy", + "reuse_components_from", "require_overrides", "scmurl", "scratch", @@ -382,6 +383,40 @@ class BaseHandler(object): self._validate_dep_overrides_format("buildrequire_overrides") self._validate_dep_overrides_format("require_overrides") + if "reuse_components_from" in self.data: + if "rebuild_strategy" in self.data and self.data["rebuild_strategy"] == "all": + raise ValidationError( + 'You cannot specify the parameter "reuse_components_from" when the ' + '"rebuild_strategy" parameter is set to "all"' + ) + + invalid_identifier_msg = ( + 'The parameter "reuse_components_from" contains an invalid module identifier') + + if isinstance(self.data["reuse_components_from"], int): + reuse_module = models.ModuleBuild.get_by_id( + db.session, self.data["reuse_components_from"]) + elif isinstance(self.data["reuse_components_from"], string_types): + try: + n, s, v, c = self.data["reuse_components_from"].split(":") + except ValueError: + raise ValidationError(invalid_identifier_msg) + reuse_module = models.ModuleBuild.get_build_from_nsvc(db.session, n, s, v, c) + else: + raise ValidationError(invalid_identifier_msg) + + if not reuse_module: + raise ValidationError( + 'The module in the parameter "reuse_components_from" could not be found') + + if reuse_module.state != models.BUILD_STATES["ready"]: + raise ValidationError( + 'The module in the parameter "reuse_components_from" must be in the ready state' + ) + + # Normalize the value so that it simplifies any code that uses this value + self.data["reuse_components_from"] = reuse_module.id + class SCMHandler(BaseHandler): def validate(self, skip_branch=False, skip_optional_params=False): diff --git a/tests/test_views/test_views.py b/tests/test_views/test_views.py index 41836a24..16b33e37 100644 --- a/tests/test_views/test_views.py +++ b/tests/test_views/test_views.py @@ -31,6 +31,7 @@ from os import path, mkdir from os.path import basename, dirname, splitext from requests.utils import quote import hashlib +import koji import pytest import re import sqlalchemy @@ -39,7 +40,7 @@ from tests import app, init_data, clean_database, reuse_component_init_data, sta from tests import read_staged_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.models import ModuleBuild, BUILD_STATES from module_build_service import db, version import module_build_service.config as mbs_config import module_build_service.scheduler.handlers.modules @@ -207,6 +208,7 @@ class TestViews: assert data["name"] == "nginx" assert data["owner"] == "Moe Szyslak" assert data["rebuild_strategy"] == "changed-and-after" + assert data["reused_module_id"] is None assert data["scmurl"] == \ "git://pkgs.domain.local/modules/nginx?#ba95886c7a443b36a9ce31abda1f9bef22f2f8c9" assert data["scratch"] is False @@ -2592,3 +2594,112 @@ class TestViews: mock_get.assert_called_once_with(expected_url, timeout=15) else: mock_get.assert_not_called() + + @pytest.mark.parametrize("reuse_components_from", (7, "testmodule:4.3.43:7:00000000")) + @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.scm.SCM") + def test_submit_build_reuse_components_from( + self, mocked_scm, mocked_get_user, reuse_components_from, + ): + """Test a build submission using the reuse_components_from parameter.""" + module_to_reuse = db.session.query(ModuleBuild).get(7) + module_to_reuse.state = BUILD_STATES["ready"] + for c in module_to_reuse.component_builds: + c.state = koji.BUILD_STATES["COMPLETE"] + db.session.commit() + + FakeSCM( + mocked_scm, "testmodule", "testmodule.yaml", "620ec77321b2ea7b0d67d82992dda3e1d67055b4") + rv = self.client.post( + "/module-build-service/1/module-builds/", + data=json.dumps({ + "branch": "master", + "reuse_components_from": reuse_components_from, + "scmurl": ( + "https://src.stg.fedoraproject.org/modules/testmodule.git?" + "#68931c90de214d9d13feefbd35246a81b6cb8d49" + ), + }), + ) + data = json.loads(rv.data) + assert data["reused_module_id"] == 7 + + @pytest.mark.parametrize( + "reuse_components_from, expected_error", + ( + ( + "testmodule:4.3.43:7", + 'The parameter "reuse_components_from" contains an invalid module identifier', + ), + ( + {}, + 'The parameter "reuse_components_from" contains an invalid module identifier', + ), + ( + 912312312, + 'The module in the parameter "reuse_components_from" could not be found', + ), + ( + 7, + 'The module in the parameter "reuse_components_from" must be in the ready state', + ) + ) + ) + @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.scm.SCM") + def test_submit_build_reuse_components_from_errors( + self, mocked_scm, mocked_get_user, reuse_components_from, expected_error, + ): + """ + Test a build submission using an invalid value for the reuse_components_from parameter. + """ + FakeSCM( + mocked_scm, "testmodule", "testmodule.yaml", "620ec77321b2ea7b0d67d82992dda3e1d67055b4") + rv = self.client.post( + "/module-build-service/1/module-builds/", + data=json.dumps({ + "branch": "master", + "reuse_components_from": reuse_components_from, + "scmurl": ( + "https://src.stg.fedoraproject.org/modules/testmodule.git?" + "#68931c90de214d9d13feefbd35246a81b6cb8d49" + ), + }), + ) + data = json.loads(rv.data) + assert rv.status_code == 400 + assert data["message"] == expected_error + + @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.scm.SCM") + @patch( + "module_build_service.config.Config.rebuild_strategy_allow_override", + new_callable=PropertyMock, + return_value=True, + ) + def test_submit_build_reuse_components_rebuild_strategy_all( + self, mock_rsao, mocked_scm, mocked_get_user, + ): + """ + Test a build submission using reuse_components_from and the rebuild_strategy of all. + """ + FakeSCM( + mocked_scm, "testmodule", "testmodule.yaml", "620ec77321b2ea7b0d67d82992dda3e1d67055b4") + rv = self.client.post( + "/module-build-service/1/module-builds/", + data=json.dumps({ + "branch": "master", + "rebuild_strategy": "all", + "reuse_components_from": 7, + "scmurl": ( + "https://src.stg.fedoraproject.org/modules/testmodule.git?" + "#68931c90de214d9d13feefbd35246a81b6cb8d49" + ), + }), + ) + data = json.loads(rv.data) + assert rv.status_code == 400 + assert data["message"] == ( + 'You cannot specify the parameter "reuse_components_from" when the "rebuild_strategy" ' + 'parameter is set to "all"' + )