From 5921c109f72a0abf2bd58aa917a2dd104a1fdd97 Mon Sep 17 00:00:00 2001 From: Luiz Carvalho Date: Jan 18 2019 15:58:26 +0000 Subject: Add retire command to mbs-manager With this command, admins can retire module builds that should no longer be used as a dependency for other module builds. Fixes #1021 Signed-off-by: Luiz Carvalho --- diff --git a/module_build_service/manage.py b/module_build_service/manage.py index 091e573..b4f7507 100755 --- a/module_build_service/manage.py +++ b/module_build_service/manage.py @@ -22,7 +22,7 @@ # Written by Matt Prahl except for the test functions from __future__ import print_function -from flask_script import Manager +from flask_script import Manager, prompt_bool from functools import wraps import flask_migrate import logging @@ -167,6 +167,55 @@ def build_module_locally(local_build_nsvs=None, yaml_file=None, stream=None, ski raise RuntimeError('Module build failed') +@manager.option('identifier', metavar='NAME:STREAM[:VERSION[:CONTEXT]]', + help='Identifier for selecting module builds to retire') +@manager.option('--confirm', action='store_true', default=False, + help='Perform retire operation without prompting') +def retire(identifier, confirm=False): + """ Retire module build(s) by placing them into 'garbage' state. + """ + # Parse identifier and build query + parts = identifier.split(':') + if len(parts) < 2: + raise ValueError('Identifier must contain at least NAME:STREAM') + if len(parts) >= 5: + raise ValueError('Too many parts in identifier') + + filter_by_kwargs = { + 'state': models.BUILD_STATES['ready'], + 'name': parts[0], + 'stream': parts[1], + } + + if len(parts) >= 3: + filter_by_kwargs['version'] = parts[2] + if len(parts) >= 4: + filter_by_kwargs['context'] = parts[3] + + # Find module builds to retire + module_builds = db.session.query(models.ModuleBuild).filter_by(**filter_by_kwargs).all() + + if not module_builds: + logging.info('No module builds found.') + return + + logging.info('Found %d module builds:', len(module_builds)) + for build in module_builds: + logging.info('\t%s', ':'.join((build.name, build.stream, build.version, build.context))) + + # Prompt for confirmation + is_confirmed = confirm or prompt_bool('Retire {} module builds?'.format(len(module_builds))) + if not is_confirmed: + logging.info('Module builds were NOT retired.') + return + + # Retire module builds + for build in module_builds: + build.transition(conf, models.BUILD_STATES['garbage'], 'Module build retired') + db.session.commit() + logging.info('Module builds retired.') + + @console_script_help @manager.command def run(host=None, port=None, debug=None): diff --git a/tests/test_manage.py b/tests/test_manage.py new file mode 100644 index 0000000..b9327cd --- /dev/null +++ b/tests/test_manage.py @@ -0,0 +1,114 @@ +# Copyright (c) 2019 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import pytest +from mock import patch + +from module_build_service import conf +from module_build_service.manage import retire +from module_build_service.models import BUILD_STATES, ModuleBuild, make_session +from tests.test_models import init_data + + +class TestMBSManage: + def setup_method(self, test_method): + init_data() + + @pytest.mark.parametrize(('identifier', 'is_valid'), ( + ('', False), + ('spam', False), + ('spam:bacon', True), + ('spam:bacon:eggs', True), + ('spam:bacon:eggs:ham', True), + ('spam:bacon:eggs:ham:sausage', False), + )) + def test_retire_identifier_validation(self, identifier, is_valid): + if is_valid: + retire(identifier) + else: + with pytest.raises(ValueError): + retire(identifier) + + @pytest.mark.parametrize(('overrides', 'identifier', 'changed_count'), ( + ({'name': 'pickme'}, 'pickme:eggs', 1), + ({'stream': 'pickme'}, 'spam:pickme', 1), + ({'version': 'pickme'}, 'spam:eggs:pickme', 1), + ({'context': 'pickme'}, 'spam:eggs:ham:pickme', 1), + + ({}, 'spam:eggs', 3), + ({'version': 'pickme'}, 'spam:eggs', 3), + ({'context': 'pickme'}, 'spam:eggs:ham', 3), + )) + @patch('module_build_service.manage.prompt_bool') + def test_retire_build(self, prompt_bool, overrides, identifier, changed_count): + prompt_bool.return_value = True + + with make_session(conf) as session: + module_builds = session.query(ModuleBuild).filter_by(state=BUILD_STATES['ready']).all() + # Verify our assumption of the amount of ModuleBuilds in database + assert len(module_builds) == 3 + + for x, build in enumerate(module_builds): + build.name = 'spam' + build.stream = 'eggs' + build.version = 'ham' + build.context = str(x) + + for attr, value in overrides.items(): + setattr(module_builds[0], attr, value) + + session.commit() + + retire(identifier) + retired_module_builds = ( + session.query(ModuleBuild).filter_by(state=BUILD_STATES['garbage']).all()) + + assert len(retired_module_builds) == changed_count + for x in range(changed_count): + assert retired_module_builds[x].id == module_builds[x].id + assert retired_module_builds[x].state == BUILD_STATES['garbage'] + + @pytest.mark.parametrize(('confirm_prompt', 'confirm_arg', 'confirm_expected'), ( + (True, False, True), + (True, True, True), + (False, False, False), + (False, True, True), + )) + @patch('module_build_service.manage.prompt_bool') + def test_retire_build_confirm_prompt(self, prompt_bool, confirm_prompt, confirm_arg, + confirm_expected): + prompt_bool.return_value = confirm_prompt + + with make_session(conf) as session: + module_builds = session.query(ModuleBuild).filter_by(state=BUILD_STATES['ready']).all() + # Verify our assumption of the amount of ModuleBuilds in database + assert len(module_builds) == 3 + + for x, build in enumerate(module_builds): + build.name = 'spam' + build.stream = 'eggs' + + session.commit() + + retire('spam:eggs', confirm_arg) + retired_module_builds = ( + session.query(ModuleBuild).filter_by(state=BUILD_STATES['garbage']).all()) + + expected_changed_count = 3 if confirm_expected else 0 + assert len(retired_module_builds) == expected_changed_count