diff --git a/bin/pungi-koji b/bin/pungi-koji index 80e5cc85..46f2fdc5 100755 --- a/bin/pungi-koji +++ b/bin/pungi-koji @@ -233,6 +233,7 @@ def run_compose(compose): liveimages_phase = pungi.phases.LiveImagesPhase(compose) livemedia_phase = pungi.phases.LiveMediaPhase(compose) image_build_phase = pungi.phases.ImageBuildPhase(compose) + osbs_phase = pungi.phases.OSBSPhase(compose) image_checksum_phase = pungi.phases.ImageChecksumPhase(compose) test_phase = pungi.phases.TestPhase(compose) @@ -241,7 +242,8 @@ def run_compose(compose): buildinstall_phase, productimg_phase, gather_phase, extrafiles_phase, createiso_phase, liveimages_phase, livemedia_phase, image_build_phase, image_checksum_phase, - test_phase, ostree_phase, ostree_installer_phase): + test_phase, ostree_phase, ostree_installer_phase, + osbs_phase): if phase.skip(): continue try: @@ -348,17 +350,20 @@ def run_compose(compose): image_build_phase.start() livemedia_phase.start() ostree_installer_phase.start() + osbs_phase.start() createiso_phase.stop() liveimages_phase.stop() image_build_phase.stop() livemedia_phase.stop() ostree_installer_phase.stop() + osbs_phase.stop() image_checksum_phase.start() image_checksum_phase.stop() pungi.metadata.write_compose_info(compose) + osbs_phase.dump_metadata() # TEST phase test_phase.start() diff --git a/doc/configuration.rst b/doc/configuration.rst index 06c8e797..8f871e77 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -148,6 +148,7 @@ Options * live-media * ostree * ostree-installer + * osbs .. note:: @@ -327,11 +328,13 @@ Example signing_key_password_file = '~/password_for_fedora-24_key' +.. _git-urls: + Git URLs ======== -In multiple places the config requires URL of a Git repository to download -kickstart file from. This URL is passed on to *Koji*. It is possible to which +In multiple places the config requires URL of a Git repository to download some +file from. This URL is passed on to *Koji*. It is possible to specify which commit to use using this syntax: :: git://git.example.com/git/repo-name.git?# @@ -1138,6 +1141,54 @@ Example config ] +OSBS Settings +============= + +*Pungi* can build docker images in OSBS. The build is initiated through Koji +``container-build`` plugin. The base image will be using RPMs from the current +compose and a ``Dockerfile`` from specified Git repository. + +Please note that the image is uploaded to a Docker v2 registry and not exported +into compose directory. There will be a metadata file in +``compose/metadata/osbs.json`` with details about the built images (assuming +they are not scratch builds). + +**osbs** + (*dict*) -- a mapping from variant regexes to configuration blocks. The + format should be ``{variant_uid_regex: [config_dict]}``. + + The configuration for each image must have at least these keys: + + * ``url`` -- (*str*) URL pointing to a Git repository with ``Dockerfile``. + Please see :ref:`git-urls` section for more details. + * ``target`` -- (*str*) A Koji target to build the image for. + + The configuration will pass other attributes directly to the Koji task. + This includes ``name``, ``version``, ``release``, ``scratch`` and + ``priority``. + + If ``release`` is set explicitly to ``None``, the value will be retrieved + from Koji. If this feature is used, a ``name`` key must be set as well.. + + A value for ``yum_repourls`` will be created automatically and point at a + repository in the current compose. + + +Example config +-------------- +:: + + osbs = { + "^Server$": { + "url": "git://example.com/dockerfiles.git?#HEAD", + "name": "fedora-docker-base", + "target": "f24-docker-candidate", + "version": "24", + "release": None, + } + } + + Media Checksums Settings ======================== diff --git a/pungi/phases/__init__.py b/pungi/phases/__init__.py index 1dff888d..dfc1bbd6 100644 --- a/pungi/phases/__init__.py +++ b/pungi/phases/__init__.py @@ -31,3 +31,4 @@ from image_checksum import ImageChecksumPhase # noqa from livemedia_phase import LiveMediaPhase # noqa from ostree import OSTreePhase # noqa from ostree_installer import OstreeInstallerPhase # noqa +from osbs import OSBSPhase # noqa diff --git a/pungi/phases/osbs.py b/pungi/phases/osbs.py new file mode 100644 index 00000000..428a6f33 --- /dev/null +++ b/pungi/phases/osbs.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- + +import json +import os +from kobo.threads import ThreadPool, WorkerThread + +from .base import ConfigGuardedPhase +from .. import util +from ..wrappers import kojiwrapper +from ..paths import translate_path + + +class OSBSPhase(ConfigGuardedPhase): + name = 'osbs' + + config_options = [ + { + "name": "osbs", + "expected_types": [dict], + "optional": True, + } + ] + + def __init__(self, compose): + super(OSBSPhase, self).__init__(compose) + self.pool = ThreadPool(logger=self.compose._logger) + self.pool.metadata = {} + + def run(self): + for variant in self.compose.get_variants(): + for conf in util.get_variant_data(self.compose.conf, self.name, variant): + self.pool.add(OSBSThread(self.pool)) + self.pool.queue_put((self.compose, variant, conf)) + + self.pool.start() + + def dump_metadata(self): + """Create a file with image metadata if the phase actually ran.""" + if self._skipped: + return + with open(self.compose.paths.compose.metadata('osbs.json'), 'w') as f: + json.dump(self.pool.metadata, f, indent=4, sort_keys=True, + separators=(',', ': ')) + + +class OSBSThread(WorkerThread): + def process(self, item, num): + compose, variant, config = item + self.num = num + with util.failable(compose, variant, '*', 'osbs'): + self.worker(compose, variant, config) + + def worker(self, compose, variant, config): + msg = 'OSBS phase for variant %s' % variant.uid + self.pool.log_info('[BEGIN] %s' % msg) + koji = kojiwrapper.KojiWrapper(compose.conf['koji_profile']) + koji.login() + + # Start task + try: + source = util.resolve_git_url(config.pop('url')) + target = config.pop('target') + + # Set release dynamically + if 'release' in config and config['release'] is None: + config['release'] = self._get_release(koji, target, config['name']) + except KeyError as exc: + raise RuntimeError('OSBS: missing config key %s for %s' + % (exc, variant.uid)) + priority = config.pop('priority', None) + + config['yum_repourls'] = [self._get_repo(compose, variant)] + + task_id = koji.koji_proxy.buildContainer(source, target, config, + priority=priority) + + # Wait for it to finish and capture the output into log file (even + # though there is not much there). + log_dir = os.path.join(compose.paths.log.topdir(), 'osbs') + util.makedirs(log_dir) + log_file = os.path.join(log_dir, '%s-%s-watch-task.log' + % (variant.uid, self.num)) + if koji.watch_task(task_id, log_file) != 0: + raise RuntimeError('OSBS: task %s failed: see %s for details' + % (task_id, log_file)) + + # Only real builds get the metadata. + if not config.get('scratch', False): + self._add_metadata(koji.koji_proxy, variant, task_id) + + self.pool.log_info('[DONE ] %s' % msg) + + def _add_metadata(self, koji_proxy, variant, task_id): + # Create metadata + result = koji_proxy.getTaskResult(task_id) + build_id = result['koji_builds'][0] + buildinfo = koji_proxy.getBuild(build_id) + archives = koji_proxy.listArchives(build_id) + + metadata = { + 'name': buildinfo['name'], + 'version': buildinfo['version'], + 'release': buildinfo['release'], + 'creation_time': buildinfo['creation_time'], + } + for archive in archives: + data = { + 'filename': archive['filename'], + 'size': archive['size'], + 'checksum': archive['checksum'], + } + data.update(archive['extra']) + data.update(metadata) + arch = archive['extra']['image']['arch'] + self.pool.metadata.setdefault( + variant.uid, {}).setdefault(arch, []).append(data) + + def _get_repo(self, compose, variant): + """ + Write a .repo file pointing to current variant and return URL to the + file. + """ + os_tree = compose.paths.compose.os_tree('$basearch', variant, + create_dir=False) + repo_file = os.path.join(compose.paths.work.tmp_dir(None, variant), + 'compose-rpms-%s.repo' % self.num) + + with open(repo_file, 'w') as f: + f.write('[%s]\n' % compose.compose_id) + f.write('name=Compose %s (RPMs)\n' % compose.compose_id) + f.write('baseurl=%s\n' % translate_path(compose, os_tree)) + f.write('enabled=1\n') + f.write('gpgcheck=0\n') + + return translate_path(compose, repo_file) + + def _get_release(self, koji, target, name): + """ + Get next release value based on last build. If no build has been done + yet (in given target), use 1 as initial value. + """ + latest_builds = koji.koji_proxy.getLatestBuilds(target, package=name) + try: + return koji.koji_proxy.getNextRelease(latest_builds[0]) + except IndexError: + return 1 diff --git a/pungi/wrappers/kojiwrapper.py b/pungi/wrappers/kojiwrapper.py index 4919f8ec..f45d5b41 100644 --- a/pungi/wrappers/kojiwrapper.py +++ b/pungi/wrappers/kojiwrapper.py @@ -36,7 +36,29 @@ class KojiWrapper(object): self.executable = self.profile.replace("_", "-") with self.lock: self.koji_module = koji.get_profile_module(profile) - self.koji_proxy = koji.ClientSession(self.koji_module.config.server) + session_opts = {} + for key in ('krbservice', 'timeout', 'keepalive', + 'max_retries', 'retry_interval', 'anon_retry', + 'offline_retry', 'offline_retry_interval', + 'debug', 'debug_xmlrpc', + 'use_fast_upload'): + value = getattr(self.koji_module.config, key, None) + if value is not None: + session_opts[key] = value + self.koji_proxy = koji.ClientSession(self.koji_module.config.server, session_opts) + + def login(self): + """Authenticate to the hub.""" + auth_type = self.koji_module.config.authtype + if auth_type == 'ssl' or (os.path.isfile(os.path.expanduser(self.koji_module.config.cert)) + and auth_type is None): + self.koji_proxy.ssl_login(os.path.expanduser(self.koji_module.config.cert), + os.path.expanduser(self.koji_module.config.ca), + os.path.expanduser(self.koji_module.config.serverca)) + elif auth_type == 'kerberos': + self.koji_proxy.krb_login() + else: + raise RuntimeError('Unsupported authentication type in Koji') def get_runroot_cmd(self, target, arch, command, quiet=False, use_shell=True, channel=None, packages=None, mounts=None, weight=None, task_id=True): cmd = [self.executable, "runroot"] @@ -287,6 +309,10 @@ class KojiWrapper(object): "task_id": task_id, } + def watch_task(self, task_id, log_file=None, max_retries=None): + retcode, _ = self._wait_for_task(task_id, logfile=log_file, max_retries=max_retries) + return retcode + def get_image_paths(self, task_id): """ Given an image task in Koji, get a mapping from arches to a list of diff --git a/tests/test_osbs_phase.py b/tests/test_osbs_phase.py new file mode 100755 index 00000000..a39a65d5 --- /dev/null +++ b/tests/test_osbs_phase.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +try: + import unittest2 as unittest +except ImportError: + import unittest +import mock +import json + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from tests import helpers +from pungi.phases import osbs + + +class OSBSPhaseTest(helpers.PungiTestCase): + + def test_validate(self): + compose = helpers.DummyCompose(self.topdir, { + 'osbs': {"^Server$": {}} + }) + + phase = osbs.OSBSPhase(compose) + try: + phase.validate() + except: + self.fail('Correct config must validate') + + def test_validate_bad_conf(self): + compose = helpers.DummyCompose(self.topdir, { + 'osbs': 'yes please' + }) + + phase = osbs.OSBSPhase(compose) + with self.assertRaises(ValueError): + phase.validate() + + @mock.patch('pungi.phases.osbs.ThreadPool') + def test_run(self, ThreadPool): + cfg = mock.Mock() + compose = helpers.DummyCompose(self.topdir, { + 'osbs': {'^Everything$': cfg} + }) + + pool = ThreadPool.return_value + + phase = osbs.OSBSPhase(compose) + phase.run() + + self.assertEqual(len(pool.add.call_args_list), 1) + self.assertEqual(pool.queue_put.call_args_list, + [mock.call((compose, compose.variants['Everything'], cfg))]) + + @mock.patch('pungi.phases.osbs.ThreadPool') + def test_skip_without_config(self, ThreadPool): + compose = helpers.DummyCompose(self.topdir, {}) + compose.just_phases = None + compose.skip_phases = [] + phase = osbs.OSBSPhase(compose) + self.assertTrue(phase.skip()) + + @mock.patch('pungi.phases.osbs.ThreadPool') + def test_dump_metadata(self, ThreadPool): + compose = helpers.DummyCompose(self.topdir, { + 'osbs': {'^Everything$': {}} + }) + compose.just_phases = None + compose.skip_phases = [] + compose.notifier = mock.Mock() + phase = osbs.OSBSPhase(compose) + phase.start() + phase.stop() + phase.pool.metadata = METADATA + phase.dump_metadata() + + with open(self.topdir + '/compose/metadata/osbs.json') as f: + data = json.load(f) + self.assertEqual(data, METADATA) + + @mock.patch('pungi.phases.osbs.ThreadPool') + def test_dump_metadata_after_skip(self, ThreadPool): + compose = helpers.DummyCompose(self.topdir, {}) + compose.just_phases = None + compose.skip_phases = [] + phase = osbs.OSBSPhase(compose) + phase.start() + phase.stop() + phase.dump_metadata() + + self.assertFalse(os.path.isfile(self.topdir + '/compose/metadata/osbs.json')) + + +TASK_RESULT = { + 'koji_builds': ['54321'], + 'repositories': [ + 'registry.example.com:8888/rcm/buildroot:f24-docker-candidate-20160617141632', + ] +} + +BUILD_INFO = { + 'completion_time': '2016-06-17 18:25:30', + 'completion_ts': 1466187930.0, + 'creation_event_id': 13227702, + 'creation_time': '2016-06-17 18:25:57.611172', + 'creation_ts': 1466187957.61117, + 'epoch': None, + 'extra': {'container_koji_task_id': '12345', 'image': {}}, + 'id': 54321, + 'name': 'my-name', + 'nvr': 'my-name-1.0-1', + 'owner_id': 3436, + 'owner_name': 'osbs', + 'package_id': 50072, + 'package_name': 'my-name', + 'release': '1', + 'source': 'git://example.com/repo?#BEEFCAFE', + 'start_time': '2016-06-17 18:16:37', + 'start_ts': 1466187397.0, + 'state': 1, + 'task_id': None, + 'version': '1.0', + 'volume_id': 0, + 'volume_name': 'DEFAULT' +} + +ARCHIVES = [ + {'build_id': 54321, + 'buildroot_id': 2955357, + 'checksum': 'a2922842dc80873ac782da048c54f6cc', + 'checksum_type': 0, + 'extra': { + 'docker': { + 'id': '408c4cd37a87a807bec65dd13b049a32fe090d2fa1a8e891f65e3e3e683996d7', + 'parent_id': '6c3a84d798dc449313787502060b6d5b4694d7527d64a7c99ba199e3b2df834e', + 'repositories': ['registry.example.com:8888/rcm/buildroot:1.0-1']}, + 'image': {'arch': 'x86_64'}}, + 'filename': 'docker-image-408c4cd37a87a807bec65dd13b049a32fe090d2fa1a8e891f65e3e3e683996d7.x86_64.tar.gz', + 'id': 1436049, + 'metadata_only': False, + 'size': 174038795, + 'type_description': 'Tar file', + 'type_extensions': 'tar tar.gz tar.bz2 tar.xz', + 'type_id': 4, + 'type_name': 'tar'} +] + +METADATA = { + 'Server': {'x86_64': [{ + 'name': 'my-name', + 'version': '1.0', + 'release': '1', + 'creation_time': BUILD_INFO['creation_time'], + 'filename': ARCHIVES[0]['filename'], + 'size': ARCHIVES[0]['size'], + 'docker': { + 'id': '408c4cd37a87a807bec65dd13b049a32fe090d2fa1a8e891f65e3e3e683996d7', + 'parent_id': '6c3a84d798dc449313787502060b6d5b4694d7527d64a7c99ba199e3b2df834e', + 'repositories': ['registry.example.com:8888/rcm/buildroot:1.0-1']}, + 'image': {'arch': 'x86_64'}, + 'checksum': ARCHIVES[0]['checksum'], + }]} +} + + +class OSBSThreadTest(helpers.PungiTestCase): + + def setUp(self): + super(OSBSThreadTest, self).setUp() + self.pool = mock.Mock(metadata={}) + self.t = osbs.OSBSThread(self.pool) + self.compose = helpers.DummyCompose(self.topdir, { + 'koji_profile': 'koji', + 'translate_paths': [ + (self.topdir, 'http://root'), + ] + }) + + def _setupMock(self, KojiWrapper, resolve_git_url): + resolve_git_url.return_value = 'git://example.com/repo?#BEEFCAFE' + self.wrapper = KojiWrapper.return_value + self.wrapper.koji_proxy.buildContainer.return_value = 12345 + self.wrapper.koji_proxy.getTaskResult.return_value = TASK_RESULT + self.wrapper.koji_proxy.getBuild.return_value = BUILD_INFO + self.wrapper.koji_proxy.listArchives.return_value = ARCHIVES + self.wrapper.koji_proxy.getLatestBuilds.return_value = [mock.Mock(), mock.Mock()] + self.wrapper.koji_proxy.getNextRelease.return_value = 3 + self.wrapper.watch_task.return_value = 0 + + def _assertCorrectMetadata(self): + self.maxDiff = None + self.assertEqual(self.pool.metadata, METADATA) + + def _assertCorrectCalls(self, opts, setupCalls=None): + setupCalls = setupCalls or [] + options = {'yum_repourls': ['http://root/work/global/tmp-Server/compose-rpms-1.repo']} + options.update(opts) + self.assertEqual( + self.wrapper.mock_calls, + [mock.call.login()] + setupCalls + [ + mock.call.koji_proxy.buildContainer( + 'git://example.com/repo?#BEEFCAFE', + 'f24-docker-candidate', + options, + priority=None), + mock.call.watch_task( + 12345, self.topdir + '/logs/global/osbs/Server-1-watch-task.log'), + mock.call.koji_proxy.getTaskResult(12345), + mock.call.koji_proxy.getBuild('54321'), + mock.call.koji_proxy.listArchives('54321')]) + + def _assertRepoFile(self): + with open(self.topdir + '/work/global/tmp-Server/compose-rpms-1.repo') as f: + lines = f.read().split('\n') + self.assertIn('baseurl=http://root/compose/Server/$baseurl/os', lines) + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_minimal_run(self, KojiWrapper, resolve_git_url): + cfg = { + 'url': 'git://example.com/repo?#HEAD', + 'target': 'f24-docker-candidate', + } + self._setupMock(KojiWrapper, resolve_git_url) + + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + self._assertCorrectCalls({}) + self._assertCorrectMetadata() + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_run_with_more_args(self, KojiWrapper, resolve_git_url): + cfg = { + 'url': 'git://example.com/repo?#HEAD', + 'target': 'f24-docker-candidate', + 'name': 'my-name', + 'version': '1.0', + } + self._setupMock(KojiWrapper, resolve_git_url) + + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + self._assertCorrectCalls({'name': 'my-name', 'version': '1.0'}) + self._assertCorrectMetadata() + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_run_with_missing_url(self, KojiWrapper, resolve_git_url): + cfg = { + 'target': 'f24-docker-candidate', + 'name': 'my-name', + } + self._setupMock(KojiWrapper, resolve_git_url) + + with self.assertRaises(RuntimeError) as ctx: + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + self.assertIn("missing config key 'url' for Server", str(ctx.exception)) + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_run_with_missing_target(self, KojiWrapper, resolve_git_url): + cfg = { + 'url': 'git://example.com/repo?#HEAD', + 'name': 'my-name', + } + self._setupMock(KojiWrapper, resolve_git_url) + + with self.assertRaises(RuntimeError) as ctx: + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + self.assertIn("missing config key 'target' for Server", str(ctx.exception)) + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_set_release_dynamically(self, KojiWrapper, resolve_git_url): + cfg = { + 'url': 'git://example.com/repo?#HEAD', + 'target': 'f24-docker-candidate', + 'release': None, + 'name': 'fedora-server-docker', + } + self._setupMock(KojiWrapper, resolve_git_url) + last_build = mock.Mock() + self.wrapper.koji_proxy.getLatestBuilds.return_value = [last_build, mock.Mock()] + self.wrapper.koji_proxy.getNextRelease.return_value = 3 + + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + self._assertCorrectCalls( + {'release': 3, 'name': 'fedora-server-docker'}, + [mock.call.koji_proxy.getLatestBuilds( + 'f24-docker-candidate', package='fedora-server-docker'), + mock.call.koji_proxy.getNextRelease(last_build)]) + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_set_release_dynamically_no_previous_build(self, KojiWrapper, resolve_git_url): + cfg = { + 'url': 'git://example.com/repo?#HEAD', + 'target': 'f24-docker-candidate', + 'release': None, + 'name': 'fedora-server-docker', + } + self._setupMock(KojiWrapper, resolve_git_url) + self.wrapper.koji_proxy.getLatestBuilds.return_value = [] + + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + self._assertCorrectCalls( + {'release': 1, 'name': 'fedora-server-docker'}, + [mock.call.koji_proxy.getLatestBuilds( + 'f24-docker-candidate', package='fedora-server-docker')]) + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_set_release_dynamically_missing_name(self, KojiWrapper, resolve_git_url): + cfg = { + 'url': 'git://example.com/repo?#HEAD', + 'target': 'fedora-24-docker-candidate', + 'release': None, + } + self._setupMock(KojiWrapper, resolve_git_url) + self.wrapper.koji_proxy.getLatestBuilds.return_value = [] + + with self.assertRaises(RuntimeError) as ctx: + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + self.assertIn("missing config key 'name' for Server", str(ctx.exception)) + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_failing_task(self, KojiWrapper, resolve_git_url): + cfg = { + 'url': 'git://example.com/repo?#HEAD', + 'target': 'fedora-24-docker-candidate', + } + self._setupMock(KojiWrapper, resolve_git_url) + self.wrapper.watch_task.return_value = 1 + + with self.assertRaises(RuntimeError) as ctx: + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + self.assertRegexpMatches(str(ctx.exception), r"task 12345 failed: see .+ for details") + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_failing_task_with_failable(self, KojiWrapper, resolve_git_url): + cfg = { + 'url': 'git://example.com/repo?#HEAD', + 'target': 'fedora-24-docker-candidate', + } + self._setupMock(KojiWrapper, resolve_git_url) + self.wrapper.watch_task.return_value = 1 + self.compose.conf['failable_deliverables'] = [('.*', {'*': ['osbs']})] + + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + @mock.patch('pungi.util.resolve_git_url') + @mock.patch('pungi.phases.osbs.kojiwrapper.KojiWrapper') + def test_scratch_has_no_metadata(self, KojiWrapper, resolve_git_url): + cfg = { + 'url': 'git://example.com/repo?#HEAD', + 'target': 'fedora-24-docker-candidate', + 'scratch': True, + } + self._setupMock(KojiWrapper, resolve_git_url) + + self.t.process((self.compose, self.compose.variants['Server'], cfg), 1) + + self.assertEqual(self.pool.metadata, {}) + + +if __name__ == '__main__': + unittest.main()