From a849dee6791045c42330bcc382b9bf9c025b2f88 Mon Sep 17 00:00:00 2001 From: Merlin Mathesius Date: Mar 25 2019 13:38:51 +0000 Subject: Add 'retire' command supporting both packages and modules There was previously a 'retire' command in 'fedpkg' supporting only packages. This brings the 'retire' command into 'rpkg' with added support for a module-specific 'dead.module' marker file. Signed-off-by: Merlin Mathesius --- diff --git a/etc/bash_completion.d/rpkg.bash b/etc/bash_completion.d/rpkg.bash index c564afc..18cdf47 100644 --- a/etc/bash_completion.d/rpkg.bash +++ b/etc/bash_completion.d/rpkg.bash @@ -35,7 +35,7 @@ _rpkg() local options="--help -v -q" local options_value="--dist --release --user --path" local commands="build chain-build ci clean clog clone co container-build container-build-config commit compile copr-build diff flatpak-build \ - gimmespec giturl help gitbuildhash import install lint local mockbuild mock-config new new-sources patch prep pull push scratch-build sources \ + gimmespec giturl help gitbuildhash import install lint local mockbuild mock-config new new-sources patch prep pull push retire scratch-build sources \ srpm switch-branch tag unused-patches upload verify-files verrel" # parse main options and get command @@ -194,6 +194,9 @@ _rpkg() push) options="--force" ;; + retire) + after_more=true + ;; scratch-build) options="--nowait --background --md5" options_target="--target" diff --git a/pyrpkg/__init__.py b/pyrpkg/__init__.py index d3a9d46..1ae534c 100644 --- a/pyrpkg/__init__.py +++ b/pyrpkg/__init__.py @@ -213,6 +213,8 @@ class Commands(object): self._repo_name = None # API URL for the module build server self.module_api_url = None + # Namespaces for which retirement is blocked by default. + self.block_retire_ns = ['rpms'] # Define properties here # Properties allow us to "lazy load" various attributes, which also means @@ -3113,6 +3115,40 @@ class Commands(object): cmd.extend([project, srpm_name]) self._run_command(cmd) + def retire(self, message): + """Delete all tracked files and commit a new dead.package file for rpms + or dead.module file for modules. + + Use optional message in commit. + + Runs the commands and returns nothing + """ + if self.ns in self.block_retire_ns: + raise rpkgError('Retirement not allowed in the %s namespace.' + ' Please check for documentation describing the' + ' proper policies and process.' + % self.ns) + + if self.ns in ('modules'): + marker = 'dead.module' + else: + marker = 'dead.package' + + cmd = ['git'] + if self.quiet: + cmd.append('--quiet') + cmd.extend(['rm', '-rf', '.']) + self._run_command(cmd, cwd=self.path) + + fd = open(os.path.join(self.path, marker), 'w') + fd.write(message + '\n') + fd.close() + + cmd = ['git', 'add', os.path.join(self.path, marker)] + self._run_command(cmd, cwd=self.path) + + self.commit(message=message) + def module_build_cancel(self, build_id, auth_method, oidc_id_provider=None, oidc_client_id=None, oidc_client_secret=None, oidc_scopes=None): diff --git a/pyrpkg/cli.py b/pyrpkg/cli.py index c5c29a9..a2c5b45 100644 --- a/pyrpkg/cli.py +++ b/pyrpkg/cli.py @@ -472,6 +472,7 @@ class cliClient(object): self.register_prep() self.register_pull() self.register_push() + self.register_retire() self.register_scratch_build() self.register_sources() self.register_srpm() @@ -1245,6 +1246,19 @@ defined, packages will be built sequentially.""" % {'name': self.name}) push_parser.add_argument('--force', '-f', help='Force push', action='store_true') push_parser.set_defaults(command=self.push) + def register_retire(self): + """Register the retire target""" + + retire_parser = self.subparsers.add_parser( + 'retire', help='Retire a package/module', + description='This command will remove all files from the repo, ' + 'leave a dead.package file for rpms or dead.module ' + 'file for modules, and push the changes.' + ) + retire_parser.add_argument('reason', + help='Reason for retiring the package/module') + retire_parser.set_defaults(command=self.retire) + def register_scratch_build(self): """Register the scratch-build target""" @@ -2293,6 +2307,20 @@ see API KEY section of copr-cli(1) man page. 'credential.useHttpPath': 'true'} self.cmd.push(getattr(self.args, 'force', False), extra_config) + def retire(self): + # Skip if package/module is already retired... + if os.path.isfile(os.path.join(self.cmd.path, 'dead.package')): + self.log.warn('dead.package found, package probably already ' + 'retired - will not remove files from git or ' + 'overwrite existing dead.package file') + elif os.path.isfile(os.path.join(self.cmd.path, 'dead.module')): + self.log.warn('dead.module found, module probably already ' + 'retired - will not remove files from git or ' + 'overwrite existing dead.module file') + else: + self.cmd.retire(self.args.reason) + self.push() + def scratch_build(self): # A scratch build is just a build with --scratch self.args.scratch = True diff --git a/tests/test_retire.py b/tests/test_retire.py new file mode 100644 index 0000000..3f3a0b2 --- /dev/null +++ b/tests/test_retire.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- + +import os +import shutil +import mock +import tempfile +import subprocess + +from six.moves import configparser +import pyrpkg.cli +from pyrpkg.errors import rpkgError + +# For running tests with Python 2.6 +try: + import unittest2 as unittest +except ImportError: + import unittest + + +TEST_CONFIG = os.path.join(os.path.dirname(__file__), 'fixtures', 'rpkg-ns.conf') + + +class RetireTestCase(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + self.log = mock.Mock() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def _setup_repo(self, origin): + cmds = ( + ['git', 'init'], + ['git', 'config', 'user.name', 'John Doe'], + ['git', 'config', 'user.email', 'jdoe@example.com'], + ['git', 'remote', 'add', 'origin', origin], + ['touch', 'rpkg.spec'], + ['git', 'add', '.'], + ['git', 'commit', '-m', 'Initial commit'], + ) + for cmd in cmds: + subprocess.check_call( + cmd, + cwd=self.tmpdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + def _get_latest_commit(self): + proc = subprocess.Popen(['git', 'log', '-n', '1', '--pretty=%s'], + cwd=self.tmpdir, stdout=subprocess.PIPE, + universal_newlines=True) + out, err = proc.communicate() + return out.strip() + + def _fake_client(self, args): + config = configparser.SafeConfigParser() + config.read(TEST_CONFIG) + with mock.patch('sys.argv', new=args): + client = pyrpkg.cli.cliClient(config, name='rpkg') + client.do_imports() + client.setupLogging(self.log) + + client.parse_cmdline() + client.args.path = self.tmpdir + client.cmd.push = mock.Mock() + return client + + def new_client(self, repo, args): + origin = 'ssh://git@pkgs.example.com/{0}'.format(repo) + self._setup_repo(origin) + return self._fake_client(args) + + def assertRetiredPackage(self, reason): + self.assertTrue(os.path.isfile(os.path.join(self.tmpdir, + 'dead.package'))) + self.assertFalse(os.path.isfile(os.path.join(self.tmpdir, + 'rpkg.spec'))) + self.assertEqual(self._get_latest_commit(), reason) + + def assertRetiredModule(self, reason): + self.assertTrue(os.path.isfile(os.path.join(self.tmpdir, + 'dead.module'))) + self.assertFalse(os.path.isfile(os.path.join(self.tmpdir, + 'rpkg.spec'))) + self.assertEqual(self._get_latest_commit(), reason) + + +class TestPackageRetirement(RetireTestCase): + + def setUp(self): + super(TestPackageRetirement, self).setUp() + + def tearDown(self): + super(TestPackageRetirement, self).tearDown() + + def test_package_retire_with_namespace_disallowed(self): + args = ['rpkg', '--dist=master', 'retire', 'my reason'] + client = self.new_client('rpms/rpkg', args) + # retirement of packages is disabled by default + self.assertRaises(rpkgError, client.retire) + + def test_package_retire_with_namespace_allowed(self): + args = ['rpkg', '--dist=master', 'retire', 'my reason'] + client = self.new_client('rpms/rpkg', args) + # un-block retirement of packages + client.cmd.block_retire_ns.remove('rpms') + client.retire() + + self.assertRetiredPackage('my reason') + self.assertEqual(len(client.cmd.push.call_args_list), 1) + + def test_package_retire_without_namespace_disallowed(self): + args = ['rpkg', '--dist=master', 'retire', 'my reason'] + client = self.new_client('rpkg', args) + # retirement of packages is disabled by default + self.assertRaises(rpkgError, client.retire) + + def test_package_retire_without_namespace_allowed(self): + args = ['rpkg', '--dist=master', 'retire', 'my reason'] + client = self.new_client('rpkg', args) + # un-block retirement of packages + client.cmd.block_retire_ns.remove('rpms') + client.retire() + + self.assertRetiredPackage('my reason') + self.assertEqual(len(client.cmd.push.call_args_list), 1) + + def test_package_is_retired_already(self): + args = ['rpkg', '--release=master', 'retire', 'my reason'] + client = self.new_client('rpkg', args) + with open(os.path.join(self.tmpdir, 'dead.package'), 'w') as f: + f.write('dead package') + client.log = mock.Mock() + client.retire() + args, kwargs = client.log.warn.call_args + self.assertIn('dead.package found, package probably already retired', + args[0]) + + +class TestModuleRetirement(RetireTestCase): + + def setUp(self): + super(TestModuleRetirement, self).setUp() + + def tearDown(self): + super(TestModuleRetirement, self).tearDown() + + def test_module_retire_with_namespace_allowed(self): + args = ['rpkg', '--dist=master', 'retire', 'my reason'] + client = self.new_client('modules/rpkg', args) + # retirement of modules is enabled by default + client.retire() + + self.assertRetiredModule('my reason') + self.assertEqual(len(client.cmd.push.call_args_list), 1) + + def test_module_retire_with_namespace_disallowed(self): + args = ['rpkg', '--dist=master', 'retire', 'my reason'] + client = self.new_client('modules/rpkg', args) + # block retirement of modules + client.cmd.block_retire_ns.append('modules') + self.assertRaises(rpkgError, client.retire) + + def test_module_is_retired_already(self): + args = ['rpkg', '--release=master', 'retire', 'my reason'] + client = self.new_client('rpkg', args) + with open(os.path.join(self.tmpdir, 'dead.module'), 'w') as f: + f.write('dead module') + client.log = mock.Mock() + client.retire() + args, kwargs = client.log.warn.call_args + self.assertIn('dead.module found, module probably already retired', + args[0])