Repository: fedora-infra/bodhi

Issue:
Add a --wait flag to bodhi overrides edit/save
We could add a ```--wait``` (or similar name, name isn't important) flag to ```bodhi overrides edit/save``` which would cause bodhi to run the ```koji wait-repo``` command in a subshell and wait for it to exit. This would be a nice convenience for our users.

Pull request https://github.com/fedora-infra/bodhi/pull/1805 added a feature where these commands can print the ```koji wait-repo``` command that would make sense to run for the given override, so we could piggy back off that work and just have subshell execute the generated command if the user requests it.

I also considered making this the default behavior, having a --no-wait flag (or similar) for users who don't want bodhi to do that for them. I'm kind of on the fence about which default is better, and welcome discussion.


Discussion:

Resolving patch:
diff --git a/bodhi/client/__init__.py b/bodhi/client/__init__.py
--- a/bodhi/client/__init__.py
+++ b/bodhi/client/__init__.py
@@ -123,6 +123,8 @@ def _set_logging_debug(ctx, param, value):
                  help='Notes on why this override is in place.'),
     click.option('--user'),
     click.option('--password', hide_input=True),
+    click.option('--wait', is_flag=True, default=False,
+                 help='Wait and ensure that the override is active'),
     staging_option,
     url_option,
     debug_option]
@@ -250,7 +252,18 @@ def _save_override(url, user, password, staging, edit=False, **kwargs):
                                 notes=kwargs['notes'],
                                 edit=edit,
                                 expired=kwargs.get('expire', False))
-    print_resp(resp, client)
+
+    if kwargs['wait']:
+        print_resp(resp, client, override_hint=False)
+        command = _generate_wait_repo_command(resp, client)
+        if command:
+            click.echo("\n\nRunning {}\n".format(' '.join(command)))
+            ret = subprocess.call(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+            if ret:
+                click.echo("WARNING: ensuring active override failed for {}".format(resp.build.nvr))
+                sys.exit(ret)
+    else:
+        print_resp(resp, client, override_hint=True)
 
 
 @click.group()
@@ -919,6 +932,25 @@ def edit_buildroot_overrides(user, password, url, staging, **kwargs):
     _save_override(url=url, user=user, password=password, staging=staging, edit=True, **kwargs)
 
 
+def _generate_wait_repo_command(override, client):
+    """
+    Generate and return a koji wait-repo command for the given override, if possible.
+
+    Args:
+        override (munch.Munch): A Munch of the Override we want to print a hint about.
+        client (bodhi.client.bindings.BodhiClient): A BodhiClient that we can use to query the
+            server for Releases.
+    Returns:
+        tuple or None: If we know the release for the override's build, we return a tuple suitable
+            for passing to subprocess.Popen for a koji command that will wait on the repo. If we
+            can't we return None.
+    """
+    if 'release_id' in override.build:
+        release = client.get_releases(ids=[override.build.release_id])['releases'][0]
+        return ('koji', 'wait-repo', '{}-build'.format(release.dist_tag),
+                '--build={}'.format(override.build.nvr))
+
+
 def _print_override_koji_hint(override, client):
     """
     Print a human readable hint about how to use koji wait-repo to monitor an override, if possible.
@@ -933,15 +965,14 @@ def _print_override_koji_hint(override, client):
         client (bodhi.client.bindings.BodhiClient): A BodhiClient that we can use to query the
             server for Releases.
     """
-    if 'release_id' in override.build:
-        release = client.get_releases(ids=[override.build.release_id])['releases'][0]
+    command = _generate_wait_repo_command(override, client)
+    if command:
         click.echo(
             '\n\nUse the following to ensure the override is active:\n\n'
-            '\t$ koji wait-repo {}-build --build={}\n'.format(
-                release.dist_tag, override.build.nvr))
+            '\t$ {}\n'.format(' '.join(command)))
 
 
-def print_resp(resp, client, verbose=False):
+def print_resp(resp, client, verbose=False, override_hint=True):
     """
     Print a human readable rendering of the given server response to the terminal.
 
@@ -949,6 +980,8 @@ def print_resp(resp, client, verbose=False):
         resp (munch.Munch): The response from the server.
         client (bodhi.client.bindings.BodhiClient): A BodhiClient.
         verbose (bool): If True, show more detailed output. Defaults to False.
+        override_hint (bool): If True, show a hint to the user about how to wait on a buildroot
+            override. Defaults to True.
     """
     if 'updates' in resp:
         if len(resp.updates) == 1:
@@ -966,7 +999,8 @@ def print_resp(resp, client, verbose=False):
     elif 'overrides' in resp:
         if len(resp.overrides) == 1:
             click.echo(client.override_str(resp.overrides[0], minimal=False))
-            _print_override_koji_hint(resp.overrides[0], client)
+            if override_hint:
+                _print_override_koji_hint(resp.overrides[0], client)
         else:
             for override in resp.overrides:
                 click.echo(client.override_str(override).strip())
@@ -974,7 +1008,8 @@ def print_resp(resp, client, verbose=False):
             '%s overrides found (%d shown)' % (resp.total, len(resp.overrides)))
     elif 'build' in resp:
         click.echo(client.override_str(resp, minimal=False))
-        _print_override_koji_hint(resp, client)
+        if override_hint:
+            _print_override_koji_hint(resp, client)
     elif 'comment' in resp:
         click.echo('The following comment was added to %s' % resp.comment['update'].title)
         click.echo(resp.comment.text)


Test patch:
diff --git a/bodhi/tests/client/test___init__.py b/bodhi/tests/client/test___init__.py
--- a/bodhi/tests/client/test___init__.py
+++ b/bodhi/tests/client/test___init__.py
@@ -1224,7 +1224,7 @@ def test_url_flag(self, send_request):
         self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
 
 
-class TestSaveBuilrootOverrides(unittest.TestCase):
+class TestSaveBuildrootOverrides(unittest.TestCase):
     """
     Test the save_buildroot_overrides() function.
     """
@@ -1275,6 +1275,69 @@ def _send_request(*args, **kwargs):
                 params={'ids': [15]}))
         self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
 
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request', autospec=True)
+    @mock.patch('bodhi.client.subprocess.call', return_value=0)
+    def test_wait_flag(self, call, send_request):
+        """
+        Assert correct behavior with the --wait flag.
+        """
+        runner = testing.CliRunner()
+        responses = [client_test_data.EXAMPLE_OVERRIDE_MUNCH,
+                     client_test_data.EXAMPLE_GET_RELEASE_15]
+
+        def _send_request(*args, **kwargs):
+            """Mock the response from send_request()."""
+            return responses.pop(0)
+
+        send_request.side_effect = _send_request
+
+        result = runner.invoke(
+            client.save_buildroot_overrides,
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', 'js-tag-it-2.0-1.fc25', '--wait'])
+
+        self.assertEqual(result.exit_code, 0)
+        expected_output = (
+            '{}\n\nRunning koji wait-repo f25-build --build=js-tag-it-2.0-1.fc25\n\n'.format(
+                client_test_data.EXPECTED_OVERRIDE_STR_OUTPUT))
+        self.assertEqual(result.output, expected_output)
+        call.assert_called_once_with(
+            ('koji', 'wait-repo', 'f25-build', '--build=js-tag-it-2.0-1.fc25'),
+            stderr=-1, stdout=-1)
+
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request', autospec=True)
+    @mock.patch('bodhi.client.subprocess.call', return_value=42)
+    def test_wait_flag_fail(self, call, send_request):
+        """
+        Assert correct behavior when the command execution due to --wait flag fails.
+        """
+        runner = testing.CliRunner()
+        responses = [client_test_data.EXAMPLE_OVERRIDE_MUNCH,
+                     client_test_data.EXAMPLE_GET_RELEASE_15]
+
+        def _send_request(*args, **kwargs):
+            """Mock the response from send_request()."""
+            return responses.pop(0)
+
+        send_request.side_effect = _send_request
+
+        result = runner.invoke(
+            client.save_buildroot_overrides,
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', 'js-tag-it-2.0-1.fc25', '--wait'])
+
+        self.assertEqual(result.exit_code, 42)
+        expected_output = (
+            '{}\n\nRunning koji wait-repo f25-build --build=js-tag-it-2.0-1.fc25\n\n'
+            'WARNING: ensuring active override failed for js-tag-it-2.0-1.fc25\n')
+        expected_output = expected_output.format(client_test_data.EXPECTED_OVERRIDE_STR_OUTPUT)
+        self.assertEqual(result.output, expected_output)
+        call.assert_called_once_with(
+            ('koji', 'wait-repo', 'f25-build', '--build=js-tag-it-2.0-1.fc25'),
+            stderr=-1, stdout=-1)
+
 
 class TestWarnIfUrlAndStagingSet(unittest.TestCase):
     """
@@ -1723,7 +1786,7 @@ def test_edit_security_update_with_unspecified_severity(self, query):
                          'value for severity: must specify severity for a security update\n'))
 
 
-class TestEditBuilrootOverrides(unittest.TestCase):
+class TestEditBuildrootOverrides(unittest.TestCase):
     """
     Test the edit_buildroot_overrides() function.
     """
@@ -1758,6 +1821,69 @@ def test_expired_override(self, send_request):
                 'csrf_token': 'a_csrf_token', 'expired': True})
         self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')
 
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request', autospec=True)
+    @mock.patch('bodhi.client.subprocess.call', return_value=0)
+    def test_wait_flag(self, call, send_request):
+        """
+        Assert correct behavior with the --wait flag.
+        """
+        runner = testing.CliRunner()
+        responses = [client_test_data.EXAMPLE_OVERRIDE_MUNCH,
+                     client_test_data.EXAMPLE_GET_RELEASE_15]
+
+        def _send_request(*args, **kwargs):
+            """Mock the response from send_request()."""
+            return responses.pop(0)
+
+        send_request.side_effect = _send_request
+
+        result = runner.invoke(
+            client.edit_buildroot_overrides,
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', 'js-tag-it-2.0-1.fc25', '--wait'])
+
+        self.assertEqual(result.exit_code, 0)
+        expected_output = (
+            '{}\n\nRunning koji wait-repo f25-build --build=js-tag-it-2.0-1.fc25\n\n'.format(
+                client_test_data.EXPECTED_OVERRIDE_STR_OUTPUT))
+        self.assertEqual(result.output, expected_output)
+        call.assert_called_once_with(
+            ('koji', 'wait-repo', 'f25-build', '--build=js-tag-it-2.0-1.fc25'),
+            stderr=-1, stdout=-1)
+
+    @mock.patch('bodhi.client.bindings.BodhiClient.csrf',
+                mock.MagicMock(return_value='a_csrf_token'))
+    @mock.patch('bodhi.client.bindings.BodhiClient.send_request', autospec=True)
+    @mock.patch('bodhi.client.subprocess.call', return_value=24)
+    def test_wait_flag_fail(self, call, send_request):
+        """
+        Assert correct behavior when the command execution due to --wait flag fails.
+        """
+        runner = testing.CliRunner()
+        responses = [client_test_data.EXAMPLE_OVERRIDE_MUNCH,
+                     client_test_data.EXAMPLE_GET_RELEASE_15]
+
+        def _send_request(*args, **kwargs):
+            """Mock the response from send_request()."""
+            return responses.pop(0)
+
+        send_request.side_effect = _send_request
+
+        result = runner.invoke(
+            client.edit_buildroot_overrides,
+            ['--user', 'bowlofeggs', '--password', 's3kr3t', 'js-tag-it-2.0-1.fc25', '--wait'])
+
+        self.assertEqual(result.exit_code, 24)
+        expected_output = (
+            '{}\n\nRunning koji wait-repo f25-build --build=js-tag-it-2.0-1.fc25\n\n'
+            'WARNING: ensuring active override failed for js-tag-it-2.0-1.fc25\n')
+        expected_output = expected_output.format(client_test_data.EXPECTED_OVERRIDE_STR_OUTPUT)
+        self.assertEqual(result.output, expected_output)
+        call.assert_called_once_with(
+            ('koji', 'wait-repo', 'f25-build', '--build=js-tag-it-2.0-1.fc25'),
+            stderr=-1, stdout=-1)
+
 
 class TestCreate(unittest.TestCase):
     """