From fe95221f10b35d0c7ab0f56774e803c4dc734d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Sedl=C3=A1=C5=99?= Date: Tue, 8 Dec 2015 14:29:18 +0100 Subject: [PATCH] Resolve HEAD in ksurl to actual hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the image build configuration specifies kickstart URL as a HEAD of a git repo, pungi now figures out what the actual hash of that commit is and uses that hash instead. This might make logs clearer and should prevent potential problems if someone pushes to that repo during composing. Documentation is updated to mention this. Signed-off-by: Lubomír Sedlář --- doc/configuration.rst | 8 +++++- pungi/phases/image_build.py | 5 +++- pungi/util.py | 27 ++++++++++++++++++++ tests/test_util.py | 50 +++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100755 tests/test_util.py diff --git a/doc/configuration.rst b/doc/configuration.rst index cdaa6891..153cd83f 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -480,7 +480,8 @@ Image Build Settings .. note:: Config can contain anything what is accepted by - koji image-build --config configfile.ini + ``koji image-build --config configfile.ini`` + Repo is currently the only option which is being automatically transformed into a string. @@ -488,6 +489,10 @@ Image Build Settings The 'format' attr is [('image_type', 'image_suffix'), ...]. productmd should ideally contain all of image types and suffixes. + If ``ksurl`` ends with ``#HEAD``, Pungi will figure out the SHA1 hash of + current HEAD and use that instead. + + Example ------- :: @@ -501,6 +506,7 @@ Example 'target': 'koji-target-name', 'ksversion': 'F23', # value from pykickstart 'version': '23', + # correct SHA1 hash will be put into the URL below automatically 'ksurl': 'https://git.fedorahosted.org/git/spin-kickstarts.git?somedirectoryifany#HEAD', 'kickstart': "fedora-docker-base.ks", 'repo': ["http://someextrarepos.org/repo", "ftp://rekcod.oi/repo]. diff --git a/pungi/phases/image_build.py b/pungi/phases/image_build.py index 0234bb7e..aad3f985 100644 --- a/pungi/phases/image_build.py +++ b/pungi/phases/image_build.py @@ -4,7 +4,7 @@ import os import time -from pungi.util import get_arch_variant_data +from pungi.util import get_arch_variant_data, resolve_git_url from pungi.phases.base import PhaseBase from pungi.linker import Linker from pungi.paths import translate_path @@ -34,6 +34,9 @@ class ImageBuildPhase(PhaseBase): for variant in self.compose.get_variants(arch=arch): image_build_data = get_arch_variant_data(self.compose.conf, self.name, arch, variant) for image_conf in image_build_data: + # Replace possible ambiguous ref name with explicit hash. + if 'ksurl' in image_conf: + image_conf['ksurl'] = resolve_git_url(image_conf['ksurl']) image_conf["arches"] = arch # passed to get_image_build_cmd as dict image_conf["variant"] = variant # ^ image_conf["install_tree"] = translate_path(self.compose, self.compose.paths.compose.os_tree(arch, variant)) # ^ diff --git a/pungi/util.py b/pungi/util.py index 54f43e16..18ef2352 100644 --- a/pungi/util.py +++ b/pungi/util.py @@ -23,6 +23,7 @@ import hashlib import errno import pipes import re +import urlparse from kobo.shortcuts import run from productmd.common import get_major_version @@ -214,6 +215,32 @@ def get_arch_variant_data(conf, var_name, arch, variant): return result +def resolve_git_url(url): + """Given a url to a Git repo specifying HEAD as a ref, replace that + specifier with actual SHA1 of the commit. + + Otherwise, the original URL will be returned. + + Raises RuntimeError if there was an error. Most likely cause is failure to + run git command. + """ + r = urlparse.urlsplit(url) + if r.fragment != 'HEAD': + return url + + baseurl = urlparse.urlunsplit((r.scheme, r.netloc, r.path, '', '')) + _, output = run(['git', 'ls-remote', baseurl, r.fragment]) + + lines = [line for line in output.split('\n') if line] + if len(lines) != 1: + # This should never happen. HEAD can not match multiple commits in a + # single repo, and there can not be a repo without a HEAD. + raise RuntimeError('Failed to resolve %s', url) + + fragment = lines[0].split()[0] + return urlparse.urlunsplit((r.scheme, r.netloc, r.path, r.query, fragment)) + + # fomat: {arch|*: [data]} def get_arch_data(conf, var_name, arch): result = [] diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100755 index 00000000..fe13a47f --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +import mock +import os +import sys +import unittest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from pungi import util + + +class TestGitRefResolver(unittest.TestCase): + + @mock.patch('pungi.util.run') + def test_successful_resolve(self, run): + run.return_value = (0, 'CAFEBABE\tHEAD\n') + + url = util.resolve_git_url('https://git.example.com/repo.git?somedir#HEAD') + + self.assertEqual(url, 'https://git.example.com/repo.git?somedir#CAFEBABE') + run.assert_called_once_with(['git', 'ls-remote', 'https://git.example.com/repo.git', 'HEAD']) + + @mock.patch('pungi.util.run') + def test_resolve_missing_spec(self, run): + url = util.resolve_git_url('https://git.example.com/repo.git') + + self.assertEqual(url, 'https://git.example.com/repo.git') + self.assertEqual(run.mock_calls, []) + + @mock.patch('pungi.util.run') + def test_resolve_non_head_spec(self, run): + url = util.resolve_git_url('https://git.example.com/repo.git#some-tag') + + self.assertEqual(url, 'https://git.example.com/repo.git#some-tag') + self.assertEqual(run.mock_calls, []) + + @mock.patch('pungi.util.run') + def test_resolve_ambiguous(self, run): + run.return_value = (0, 'CAFEBABE\tF11\nDEADBEEF\tF10\n') + + with self.assertRaises(RuntimeError): + util.resolve_git_url('https://git.example.com/repo.git?somedir#HEAD') + + run.assert_called_once_with(['git', 'ls-remote', 'https://git.example.com/repo.git', 'HEAD']) + + +if __name__ == "__main__": + unittest.main()