Skip to content

Commit

Permalink
Add option to create/edit updates side tags to CLI
Browse files Browse the repository at this point in the history
Fixes #2325

Signed-off-by: Michal Konečný <mkonecny@redhat.com>
  • Loading branch information
Zlopez authored and mergify[bot] committed Aug 2, 2019
1 parent cdb3db5 commit 0f84645
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 12 deletions.
52 changes: 42 additions & 10 deletions bodhi/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ def _set_logging_debug(
type=click.Choice(['logout', 'reboot'])),
click.option('--display-name',
help='The name of the update'),
click.option('--from-tag', help='Use builds from a Koji tag instead of specifying '
'them individually.',
is_flag=True),
staging_option]


Expand Down Expand Up @@ -395,7 +398,7 @@ def require_severity_for_security_update(type: str, severity: str):
@click.option('--type', default='bugfix', help='Update type', required=True,
type=click.Choice(['security', 'bugfix', 'enhancement', 'newpackage']))
@add_options(new_edit_options)
@click.argument('builds')
@click.argument('builds_or_tag')
@click.option('--file', help='A text file containing all the update details')
@handle_errors
@openid_option
Expand All @@ -419,10 +422,20 @@ def new(user: str, password: str, url: str, debug: bool, openid_api: str, **kwar
openid_api: A URL for an OpenID API to use to authenticate to Bodhi.
kwargs: Other keyword arguments passed to us by click.
"""

client = bindings.BodhiClient(base_url=url, username=user, password=password,
staging=kwargs['staging'], openid_api=openid_api)

# Because bodhi.server.services.updates expects from_tag to be string
# copy builds to from_tag and remove builds
if kwargs['from_tag']:
if len(kwargs['builds_or_tag'].split(' ')) > 1:
click.echo("ERROR: Can't specify more than one tag.", err=True)
sys.exit(1)
kwargs['from_tag'] = kwargs.pop('builds_or_tag')
else:
kwargs['builds'] = kwargs.pop('builds_or_tag')
del kwargs['from_tag']

if kwargs['file'] is None:
updates = [kwargs]

Expand All @@ -432,7 +445,7 @@ def new(user: str, password: str, url: str, debug: bool, openid_api: str, **kwar
kwargs['notes'] = _get_notes(**kwargs)

if not kwargs['notes'] and not kwargs['file']:
click.echo("ERROR: must specify at least one of --file, --notes, or --notes-file")
click.echo("ERROR: must specify at least one of --file, --notes, or --notes-file", err=True)
sys.exit(1)

for update in updates:
Expand Down Expand Up @@ -517,13 +530,32 @@ def edit(user: str, password: str, url: str, debug: bool, openid_api: str, **kwa

kwargs['builds'] = [b['nvr'] for b in former_update['builds']]
kwargs['edited'] = former_update['alias']
if kwargs['addbuilds']:
for build in kwargs['addbuilds'].split(','):
if build not in kwargs['builds']:
kwargs['builds'].append(build)
if kwargs['removebuilds']:
for build in kwargs['removebuilds'].split(','):
kwargs['builds'].remove(build)
# Because bodhi.server.services.updates expects from_tag to be string
# copy builds to from_tag and remove builds
if kwargs['from_tag']:
if not former_update.get('from_tag', None):
click.echo(
"ERROR: This update was not created from a tag."
" Please remove --from_tag and try again.", err=True
)
sys.exit(1)
if kwargs['addbuilds'] or kwargs['removebuilds']:
click.echo(
"ERROR: The --from-tag option can't be used together with"
" --addbuilds or --removebuilds.", err=True
)
sys.exit(1)
kwargs['from_tag'] = former_update['from_tag']
del kwargs['builds']
else:
kwargs.pop('from_tag')
if kwargs['addbuilds']:
for build in kwargs['addbuilds'].split(','):
if build not in kwargs['builds']:
kwargs['builds'].append(build)
if kwargs['removebuilds']:
for build in kwargs['removebuilds'].split(','):
kwargs['builds'].remove(build)
del kwargs['addbuilds']
del kwargs['removebuilds']

Expand Down
2 changes: 2 additions & 0 deletions bodhi/client/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ def save(self, **kwargs) -> 'munch.Munch':
update have been confirmed by testers.
require_testcases (bool): A boolean to require that this update passes
all test cases before reaching stable.
from_tag (str): The name of a Koji tag from which to pull builds
instead of providing them manually in `builds`.
Returns:
The Bodhi server's response to the request.
"""
Expand Down
186 changes: 185 additions & 1 deletion bodhi/tests/client/test___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,63 @@ def test_display_name_flag(self, send_request):
self.assertEqual(send_request.mock_calls, calls)
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',
return_value=client_test_data.EXAMPLE_UPDATE_MUNCH, autospec=True)
def test_from_tag_flag(self, send_request):
"""
Assert correct behavior with the --from-tag flag.
"""
runner = testing.CliRunner()

result = runner.invoke(
client.new,
['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'fake_tag',
'--bugs', '1234567', '--from-tag', '--url',
'http://localhost:6543', '--notes', 'No description.'])

self.assertEqual(result.exit_code, 0)
expected_output = client_test_data.EXPECTED_UPDATE_OUTPUT.replace('example.com/tests',
'localhost:6543')
self.assertTrue(compare_output(result.output, expected_output + '\n'))
bindings_client = send_request.mock_calls[0][1][0]
calls = [
mock.call(
bindings_client, 'updates/', auth=True, verb='POST',
data={
'close_bugs': False, 'stable_karma': None, 'csrf_token': 'a_csrf_token',
'staging': False, 'autokarma': True, 'autotime': False, 'stable_days': None,
'suggest': None, 'notes': 'No description.', 'request': None,
'bugs': '1234567', 'requirements': None, 'unstable_karma': None, 'file': None,
'notes_file': None, 'type': 'bugfix', 'severity': None, 'display_name': None,
'from_tag': 'fake_tag'
}
),
mock.call(
bindings_client,
'updates/FEDORA-EPEL-2016-3081a94111/get-test-results',
verb='GET'
)
]
self.assertEqual(send_request.mock_calls, calls)
self.assertEqual(bindings_client.base_url, 'http://localhost:6543/')

def test_from_tag_flag_multiple_tags(self):
"""
Assert correct behavior with the --from-tag and multiple tags.
"""
runner = testing.CliRunner()

result = runner.invoke(
client.new,
['--user', 'bowlofeggs', '--password', 's3kr3t', '--autokarma', 'fake tag',
'--bugs', '1234567', '--from-tag', '--url',
'http://localhost:6543', '--notes', 'No description.'])

self.assertEqual(result.exit_code, 1)
self.assertEqual(result.output, 'ERROR: Can\'t specify more than one tag.\n')

def test_new_update_without_notes(self):
"""
Assert providing neither --notes-file nor --notes parameters to new update request
Expand All @@ -783,7 +840,7 @@ def test_new_security_update_with_unspecified_severity(self):
'--notes', 'bla bla bla', '--type', 'security'])

self.assertEqual(result.exit_code, 2)
self.assertEqual(result.output, ('Usage: new [OPTIONS] BUILDS\n\nError: Invalid '
self.assertEqual(result.output, ('Usage: new [OPTIONS] BUILDS_OR_TAG\n\nError: Invalid '
'value for severity: must specify severity for a security update\n'))


Expand Down Expand Up @@ -1838,6 +1895,133 @@ def test_addbuilds_removebuilds(self, send_request, query):
self.assertEqual(send_request.mock_calls, calls)
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.query', autospec=True)
@mock.patch('bodhi.client.bindings.BodhiClient.send_request',
return_value=client_test_data.EXAMPLE_UPDATE_MUNCH, autospec=True)
def test_from_tag_flag(self, send_request, query):
"""
Assert correct behavior with the --from-tag flag.
"""
data = client_test_data.EXAMPLE_QUERY_MUNCH.copy()
data.updates[0]['from_tag'] = 'fake_tag'
query.return_value = data
runner = testing.CliRunner()

result = runner.invoke(
client.edit, ['FEDORA-2017-c95b33872d', '--user', 'bowlofeggs',
'--password', 's3kr3t', '--from-tag',
'--notes', 'Updated package.',
'--url', 'http://localhost:6543'])

self.assertEqual(result.exit_code, 0)
bindings_client = query.mock_calls[0][1][0]
query.assert_called_with(
bindings_client, updateid='FEDORA-2017-c95b33872d')
bindings_client = send_request.mock_calls[0][1][0]
calls = [
mock.call(
bindings_client, 'updates/', auth=True, verb='POST',
data={
'close_bugs': False, 'stable_karma': 3, 'csrf_token': 'a_csrf_token',
'autokarma': False, 'edited': 'FEDORA-2017-c95b33872d',
'suggest': 'unspecified', 'notes': 'Updated package.',
'notes_file': None, 'request': None, 'unstable_karma': -3,
'bugs': '1420605', 'requirements': '', 'type': 'newpackage',
'severity': u'low', 'display_name': None, 'autotime': False,
'stable_days': None, 'from_tag': 'fake_tag',
'staging': False,
}
),
mock.call(
bindings_client,
'updates/FEDORA-EPEL-2016-3081a94111/get-test-results',
verb='GET'
)
]
self.assertEqual(send_request.mock_calls, calls)
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.query',
return_value=client_test_data.EXAMPLE_QUERY_MUNCH, autospec=True)
def test_from_tag_flag_no_tag(self, query):
"""
Assert --from-tag bails out if the update wasn't created from a tag.
"""
runner = testing.CliRunner()

result = runner.invoke(
client.edit, ['FEDORA-2017-c95b33872d', '--user', 'bowlofeggs',
'--password', 's3kr3t', '--from-tag',
'--notes', 'Updated package.',
'--url', 'http://localhost:6543'])

self.assertEqual(result.exit_code, 1)
self.assertEqual(result.output, "ERROR: This update was not created from a tag."
" Please remove --from_tag and try again.\n")
bindings_client = query.mock_calls[0][1][0]
query.assert_called_with(
bindings_client, updateid='FEDORA-2017-c95b33872d')

@mock.patch('bodhi.client.bindings.BodhiClient.csrf',
mock.MagicMock(return_value='a_csrf_token'))
@mock.patch('bodhi.client.bindings.BodhiClient.query', autospec=True)
def test_from_tag_addbuilds(self, query):
"""
Assert --from-tag can't be used with --addbuilds.
"""
data = client_test_data.EXAMPLE_QUERY_MUNCH.copy()
data.updates[0]['from_tag'] = 'fake_tag'
query.return_value = data

runner = testing.CliRunner()

result = runner.invoke(
client.edit, ['FEDORA-2017-c95b33872d', '--user', 'bowlofeggs',
'--password', 's3kr3t', '--from-tag',
'--addbuilds', 'tar-1.29-4.fc25,nedit-5.7-1.fc25',
'--notes', 'Updated package.',
'--url', 'http://localhost:6543'])

self.assertEqual(result.exit_code, 1)
self.assertEqual(result.output, "ERROR: The --from-tag option can't be used together with"
" --addbuilds or --removebuilds.\n")
bindings_client = query.mock_calls[0][1][0]
query.assert_called_with(
bindings_client, updateid='FEDORA-2017-c95b33872d')

@mock.patch('bodhi.client.bindings.BodhiClient.csrf',
mock.MagicMock(return_value='a_csrf_token'))
@mock.patch('bodhi.client.bindings.BodhiClient.query', autospec=True)
def test_from_tag_removebuilds(self, query):
"""
Assert --from-tag can't be used with --removebuilds.
"""
data = client_test_data.EXAMPLE_QUERY_MUNCH.copy()
data.updates[0]['from_tag'] = 'fake_tag'
query.return_value = data

runner = testing.CliRunner()

result = runner.invoke(
client.edit, ['FEDORA-2017-c95b33872d', '--user', 'bowlofeggs',
'--password', 's3kr3t', '--from-tag',
'--removebuilds', 'nodejs-grunt-wrap-0.3.0-2.fc25',
'--notes', 'Updated package.',
'--url', 'http://localhost:6543'])

print(result.output)

self.assertEqual(result.exit_code, 1)
self.assertEqual(result.output, "ERROR: The --from-tag option can't be used together with"
" --addbuilds or --removebuilds.\n")
bindings_client = query.mock_calls[0][1][0]
query.assert_called_with(
bindings_client, updateid='FEDORA-2017-c95b33872d')

def test_notes_and_notes_file(self):
"""
Assert providing both --notes-file and --notes parameters to an otherwise successful
Expand Down
13 changes: 12 additions & 1 deletion docs/user/man_pages/bodhi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ The ``updates`` command allows users to interact with bodhi updates.
You can specify an architecture of packages to download. "all" will download packages for all architectures.
Omitting this option will download packages for the architecture you are currently running.

``bodhi updates new [options] <builds>``
``bodhi updates new [options] <builds_or_tag>``

Create a new bodhi update containing the builds, given as a comma separated list of NVRs. The
``new`` subcommand supports the following options:
Expand Down Expand Up @@ -271,6 +271,11 @@ The ``updates`` command allows users to interact with bodhi updates.

The name of the update

``--from-tag``

If this flag is provided, ``<builds_or_tag>`` will be interpreted as a Koji tag and expand
to all latest builds in it. Only a single tag can be provided.

``bodhi updates edit [options] <update>``

Edit an existing bodhi update, given an update id or an update title. The
Expand Down Expand Up @@ -334,6 +339,12 @@ The ``updates`` command allows users to interact with bodhi updates.

The name of the update

``--from-tag``

If given, for updates that were created from a Koji tag, this will update
the builds to the latest ones in the tag.


``bodhi updates query [options]``

Query the bodhi server for updates.
Expand Down

0 comments on commit 0f84645

Please sign in to comment.