Add the ability to specify custom metadata on an RPM build
This adds the `--custom-user-metadata` option to the CLI `build` command. This is then stored under the "extra.custom_user_metadata" field on the resulting build. Signed-off-by: mprahl <mprahl@redhat.com>
This commit is contained in:
parent
b6b7d644b5
commit
cd1b928a5f
3 changed files with 182 additions and 10 deletions
|
|
@ -27,6 +27,7 @@ import copy
|
|||
import glob
|
||||
import grp
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
|
|
@ -962,6 +963,18 @@ class BuildTask(BaseTaskHandler):
|
|||
repo_info = None
|
||||
# we'll wait for a repo later (self.getRepo)
|
||||
self.event_id = None
|
||||
|
||||
if opts.get('custom_user_metadata'):
|
||||
if not isinstance(opts['custom_user_metadata'], dict):
|
||||
raise koji.BuildError('custom_user_metadata must be serializable to a JSON object')
|
||||
|
||||
try:
|
||||
json.dumps(opts['custom_user_metadata'])
|
||||
except TypeError:
|
||||
error_msg = 'custom_user_metadata is not JSON serializable'
|
||||
self.logger.exception(error_msg)
|
||||
raise koji.BuildError(error_msg)
|
||||
|
||||
task_info = self.session.getTaskInfo(self.id)
|
||||
target_info = None
|
||||
if target:
|
||||
|
|
@ -1019,6 +1032,10 @@ class BuildTask(BaseTaskHandler):
|
|||
data['source'] = self.source['source']
|
||||
data['extra'] = {'source': {'original_url': self.source['url']}}
|
||||
|
||||
if opts.get('custom_user_metadata'):
|
||||
data.setdefault('extra', {})
|
||||
data['extra']['custom_user_metadata'] = opts['custom_user_metadata']
|
||||
|
||||
extra_arches = None
|
||||
self.logger.info("Reading package config for %(name)s" % data)
|
||||
pkg_cfg = self.session.getPackageConfig(dest_tag, data['name'], event=self.event_id)
|
||||
|
|
|
|||
|
|
@ -494,6 +494,9 @@ def handle_build(options, session, args):
|
|||
help=_("Do not display progress of the upload"))
|
||||
parser.add_option("--background", action="store_true",
|
||||
help=_("Run the build at a lower priority"))
|
||||
parser.add_option("--custom-user-metadata", type="str",
|
||||
help=_("Provide a JSON string of custom metadata to be deserialized and "
|
||||
"stored under the build's extra.custom_user_metadata field"))
|
||||
(build_opts, args) = parser.parse_args(args)
|
||||
if len(args) != 2:
|
||||
parser.error(_("Exactly two arguments (a build target and a SCM URL or srpm file) are "
|
||||
|
|
@ -502,6 +505,17 @@ def handle_build(options, session, args):
|
|||
parser.error(_("--no-/rebuild-srpm is only allowed for --scratch builds"))
|
||||
if build_opts.arch_override and not build_opts.scratch:
|
||||
parser.error(_("--arch_override is only allowed for --scratch builds"))
|
||||
custom_user_metadata = {}
|
||||
if build_opts.custom_user_metadata:
|
||||
try:
|
||||
custom_user_metadata = json.loads(build_opts.custom_user_metadata)
|
||||
# Use ValueError instead of json.JSONDecodeError for Python 2 and 3 compatibility
|
||||
except ValueError:
|
||||
parser.error(_("--custom-user-metadata is not valid JSON"))
|
||||
|
||||
if not isinstance(custom_user_metadata, dict):
|
||||
parser.error(_("--custom-user-metadata must be a JSON object"))
|
||||
|
||||
activate_session(session, options)
|
||||
target = args[0]
|
||||
if target.lower() == "none" and build_opts.repo_id:
|
||||
|
|
@ -525,6 +539,7 @@ def handle_build(options, session, args):
|
|||
val = getattr(build_opts, key)
|
||||
if val is not None:
|
||||
opts[key] = val
|
||||
opts["custom_user_metadata"] = custom_user_metadata
|
||||
priority = None
|
||||
if build_opts.background:
|
||||
# relative to koji.PRIO_DEFAULT
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ https://docs.pagure.org/koji/HOWTO/#package-organization
|
|||
source = 'srpm'
|
||||
task_id = 1
|
||||
args = [target, source]
|
||||
opts = {'wait_builds': []}
|
||||
opts = {'custom_user_metadata': {}, 'wait_builds': []}
|
||||
priority = None
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
|
|
@ -105,7 +105,7 @@ Task info: weburl/taskinfo?taskID=1
|
|||
source = 'http://scm'
|
||||
task_id = 1
|
||||
args = [target, source]
|
||||
opts = {'wait_builds': []}
|
||||
opts = {'custom_user_metadata': {}, 'wait_builds': []}
|
||||
priority = None
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
|
|
@ -226,6 +226,10 @@ Options:
|
|||
--repo-id=REPO_ID Use a specific repo
|
||||
--noprogress Do not display progress of the upload
|
||||
--background Run the build at a lower priority
|
||||
--custom-user-metadata=CUSTOM_USER_METADATA
|
||||
Provide a JSON string of custom metadata to be
|
||||
deserialized and stored under the build's
|
||||
extra.custom_user_metadata field
|
||||
""" % (progname, progname)
|
||||
expected_stderr = ''
|
||||
self.assertMultiLineEqual(actual_stdout, expected_stdout)
|
||||
|
|
@ -242,6 +246,135 @@ Options:
|
|||
self.session.logout.assert_not_called()
|
||||
watch_tasks_mock.assert_not_called()
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=six.StringIO)
|
||||
@mock.patch('koji_cli.commands.activate_session')
|
||||
@mock.patch('koji_cli.commands.unique_path', return_value='random_path')
|
||||
@mock.patch('koji_cli.commands._running_in_bg', return_value=False)
|
||||
@mock.patch('koji_cli.commands.watch_tasks', return_value=0)
|
||||
def test_handle_build_custom_user_metadata(
|
||||
self,
|
||||
watch_tasks_mock,
|
||||
running_in_bg_mock,
|
||||
unique_path_mock,
|
||||
activate_session_mock,
|
||||
stdout):
|
||||
target = 'target'
|
||||
dest_tag = 'dest_tag'
|
||||
target_info = {'dest_tag': dest_tag}
|
||||
dest_tag_info = {'locked': False}
|
||||
source = 'http://scm'
|
||||
task_id = 1
|
||||
args = ['--custom-user-metadata={"automation-triggered-by": "yoda"}', target, source]
|
||||
opts = {'custom_user_metadata': {'automation-triggered-by': 'yoda'}, 'wait_builds': []}
|
||||
priority = None
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
self.session.getTag.return_value = dest_tag_info
|
||||
self.session.build.return_value = task_id
|
||||
# Run it and check immediate output
|
||||
# args: target, http://scm
|
||||
# expected: success
|
||||
rv = handle_build(self.options, self.session, args)
|
||||
actual = stdout.getvalue()
|
||||
expected = """Created task: 1
|
||||
Task info: weburl/taskinfo?taskID=1
|
||||
"""
|
||||
self.assertMultiLineEqual(actual, expected)
|
||||
# Finally, assert that things were called as we expected.
|
||||
activate_session_mock.assert_called_once_with(self.session, self.options)
|
||||
self.session.getBuildTarget.assert_called_once_with(target)
|
||||
self.session.getTag.assert_called_once_with(dest_tag)
|
||||
unique_path_mock.assert_not_called()
|
||||
running_in_bg_mock.assert_called_once()
|
||||
self.session.uploadWrapper.assert_not_called()
|
||||
self.session.build.assert_called_once_with(
|
||||
source, target, opts, priority=priority)
|
||||
self.session.logout.assert_called()
|
||||
watch_tasks_mock.assert_called_once_with(
|
||||
self.session, [task_id], quiet=self.options.quiet,
|
||||
poll_interval=self.options.poll_interval, topurl=self.options.topurl)
|
||||
self.assertEqual(rv, 0)
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=six.StringIO)
|
||||
@mock.patch('sys.stderr', new_callable=six.StringIO)
|
||||
@mock.patch('koji_cli.commands.activate_session')
|
||||
@mock.patch('koji_cli.commands.unique_path', return_value='random_path')
|
||||
@mock.patch('koji_cli.commands._running_in_bg', return_value=False)
|
||||
@mock.patch('koji_cli.commands.watch_tasks', return_value=0)
|
||||
def test_handle_build_custom_user_metadata_invalid_json(
|
||||
self,
|
||||
watch_tasks_mock,
|
||||
running_in_bg_mock,
|
||||
unique_path_mock,
|
||||
activate_session_mock,
|
||||
stderr,
|
||||
stdout):
|
||||
target = 'target'
|
||||
source = 'http://scm'
|
||||
args = [target, source, '--custom-user-metadata={Do or do not. There is no try.}']
|
||||
|
||||
# Run it and check immediate output
|
||||
with self.assertRaises(SystemExit) as ex:
|
||||
handle_build(self.options, self.session, args)
|
||||
self.assertExitCode(ex, 2)
|
||||
actual_stdout = stdout.getvalue()
|
||||
actual_stderr = stderr.getvalue()
|
||||
expected_stdout = ''
|
||||
expected_stderr = self.format_error_message("--custom-user-metadata is not valid JSON")
|
||||
self.assertMultiLineEqual(actual_stdout, expected_stdout)
|
||||
self.assertMultiLineEqual(actual_stderr, expected_stderr)
|
||||
|
||||
# Finally, assert that things were called as we expected.
|
||||
activate_session_mock.assert_not_called()
|
||||
self.session.getBuildTarget.assert_not_called()
|
||||
self.session.getTag.assert_not_called()
|
||||
unique_path_mock.assert_not_called()
|
||||
running_in_bg_mock.assert_not_called()
|
||||
self.session.uploadWrapper.assert_not_called()
|
||||
self.session.build.assert_not_called()
|
||||
self.session.logout.assert_not_called()
|
||||
watch_tasks_mock.assert_not_called()
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=six.StringIO)
|
||||
@mock.patch('sys.stderr', new_callable=six.StringIO)
|
||||
@mock.patch('koji_cli.commands.activate_session')
|
||||
@mock.patch('koji_cli.commands.unique_path', return_value='random_path')
|
||||
@mock.patch('koji_cli.commands._running_in_bg', return_value=False)
|
||||
@mock.patch('koji_cli.commands.watch_tasks', return_value=0)
|
||||
def test_handle_build_custom_user_metadata_not_json_object(
|
||||
self,
|
||||
watch_tasks_mock,
|
||||
running_in_bg_mock,
|
||||
unique_path_mock,
|
||||
activate_session_mock,
|
||||
stderr,
|
||||
stdout):
|
||||
target = 'target'
|
||||
source = 'http://scm'
|
||||
args = [target, source, '--custom-user-metadata="Do or do not. There is no try."']
|
||||
|
||||
# Run it and check immediate output
|
||||
with self.assertRaises(SystemExit) as ex:
|
||||
handle_build(self.options, self.session, args)
|
||||
self.assertExitCode(ex, 2)
|
||||
actual_stdout = stdout.getvalue()
|
||||
actual_stderr = stderr.getvalue()
|
||||
expected_stdout = ''
|
||||
expected_stderr = self.format_error_message("--custom-user-metadata must be a JSON object")
|
||||
self.assertMultiLineEqual(actual_stdout, expected_stdout)
|
||||
self.assertMultiLineEqual(actual_stderr, expected_stderr)
|
||||
|
||||
# Finally, assert that things were called as we expected.
|
||||
activate_session_mock.assert_not_called()
|
||||
self.session.getBuildTarget.assert_not_called()
|
||||
self.session.getTag.assert_not_called()
|
||||
unique_path_mock.assert_not_called()
|
||||
running_in_bg_mock.assert_not_called()
|
||||
self.session.uploadWrapper.assert_not_called()
|
||||
self.session.build.assert_not_called()
|
||||
self.session.logout.assert_not_called()
|
||||
watch_tasks_mock.assert_not_called()
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=six.StringIO)
|
||||
@mock.patch('sys.stderr', new_callable=six.StringIO)
|
||||
@mock.patch('koji_cli.commands.activate_session')
|
||||
|
|
@ -301,7 +434,9 @@ Options:
|
|||
task_id = 1
|
||||
repo_id = 2
|
||||
args = ['--repo-id=' + str(repo_id), target, source]
|
||||
opts = {'repo_id': repo_id, 'skip_tag': True, 'wait_builds': []}
|
||||
opts = {
|
||||
'repo_id': repo_id, 'skip_tag': True, 'wait_builds': [], 'custom_user_metadata': {}
|
||||
}
|
||||
priority = None
|
||||
|
||||
self.session.build.return_value = task_id
|
||||
|
|
@ -483,7 +618,12 @@ Task info: weburl/taskinfo?taskID=1
|
|||
'--scratch',
|
||||
target,
|
||||
source]
|
||||
opts = {'arch_override': arch_override, 'scratch': True, 'wait_builds': []}
|
||||
opts = {
|
||||
'arch_override': arch_override,
|
||||
'custom_user_metadata': {},
|
||||
'scratch': True,
|
||||
'wait_builds': [],
|
||||
}
|
||||
priority = None
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
|
|
@ -534,7 +674,7 @@ Task info: weburl/taskinfo?taskID=1
|
|||
task_id = 1
|
||||
args = ['--background', target, source]
|
||||
priority = 5
|
||||
opts = {'wait_builds': []}
|
||||
opts = {'custom_user_metadata': {}, 'wait_builds': []}
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
self.session.getTag.return_value = dest_tag_info
|
||||
|
|
@ -582,7 +722,7 @@ Task info: weburl/taskinfo?taskID=1
|
|||
source = 'srpm'
|
||||
task_id = 1
|
||||
args = [target, source]
|
||||
opts = {'wait_builds': []}
|
||||
opts = {'custom_user_metadata': {}, 'wait_builds': []}
|
||||
priority = None
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
|
|
@ -633,7 +773,7 @@ Task info: weburl/taskinfo?taskID=1
|
|||
source = 'srpm'
|
||||
task_id = 1
|
||||
args = ['--noprogress', target, source]
|
||||
opts = {'wait_builds': []}
|
||||
opts = {'custom_user_metadata': {}, 'wait_builds': []}
|
||||
priority = None
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
|
|
@ -687,7 +827,7 @@ Task info: weburl/taskinfo?taskID=1
|
|||
task_id = 1
|
||||
quiet = True
|
||||
args = ['--quiet', target, source]
|
||||
opts = {'wait_builds': []}
|
||||
opts = {'custom_user_metadata': {}, 'wait_builds': []}
|
||||
priority = None
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
|
|
@ -737,7 +877,7 @@ Task info: weburl/taskinfo?taskID=1
|
|||
task_id = 1
|
||||
quiet = None
|
||||
args = ['--wait', target, source]
|
||||
opts = {'wait_builds': []}
|
||||
opts = {'custom_user_metadata': {}, 'wait_builds': []}
|
||||
priority = None
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
|
|
@ -790,7 +930,7 @@ Task info: weburl/taskinfo?taskID=1
|
|||
source = 'srpm'
|
||||
task_id = 1
|
||||
args = ['--nowait', target, source]
|
||||
opts = {'wait_builds': []}
|
||||
opts = {'custom_user_metadata': {}, 'wait_builds': []}
|
||||
priority = None
|
||||
|
||||
self.session.getBuildTarget.return_value = target_info
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue