diff --git a/pungi/phases/pkgset/sources/source_koji.py b/pungi/phases/pkgset/sources/source_koji.py index be912b77..eb900ab4 100644 --- a/pungi/phases/pkgset/sources/source_koji.py +++ b/pungi/phases/pkgset/sources/source_koji.py @@ -18,7 +18,9 @@ import os from six.moves import cPickle as pickle import json import re +from itertools import groupby from kobo.shortcuts import force_list +from kobo.rpmlib import make_nvra import pungi.wrappers.kojiwrapper from pungi.wrappers.comps import CompsWrapper @@ -185,6 +187,159 @@ def get_pkgset_from_koji(compose, koji_wrapper, path_prefix): return package_sets +def _add_module_to_variant(variant, mmd, rpms, add_to_variant_modules=False): + """ + Adds module defined by Modulemd.Module `mmd` to variant. + + :param Variant variant: Variant to add the module to. + :param Modulemd.Module: Modulemd instance defining the module. + :param list rpms: List of NEVRAs to add to variant along with a module. + :param bool add_to_variant_modules: Adds the modules also to + variant.modules. + """ + # Get the NSVC of module and handle the case where for some reason the + # name/strea/version is not set. + if not mmd.get_name() or not mmd.get_stream() or not mmd.get_version(): + raise ValueError( + "Input module %s does not name or stream or version set." + % mmd.dumps()) + nsvc_list = [mmd.get_name(), mmd.get_stream(), str(mmd.get_version())] + if mmd.get_context(): + nsvc_list.append(mmd.get_context()) + nsvc = ":".join(nsvc_list) + + # Catch the issue when build system does not contain RPMs, but + # the module definition says there should be some. + if not rpms and mmd.get_rpm_components(): + raise ValueError( + "Module %s does not have any rpms in 'rpms' in build system," + "but according to modulemd, there should be some." + % nsvc) + + # Add RPMs from build systemto modulemd, so we can track + # what RPM is in which module later in gather phase. + rpm_artifacts = mmd.get_rpm_artifacts() + for rpm_nevra in rpms: + if rpm_nevra.endswith(".rpm"): + rpm_nevra = rpm_nevra[:-len(".rpm")] + rpm_artifacts.add(str(rpm_nevra)) + mmd.set_rpm_artifacts(rpm_artifacts) + variant.mmds.append(mmd) + + if add_to_variant_modules: + variant.modules.append(nsvc) + + +def _get_modules_from_pdc(compose, session, variant, variant_tags): + """ + Loads modules for given `variant` from PDC `session`, adds them to + the `variant` and also to `variant_tags` dict. + + :param Compose compose: Compose for which the modules are found. + :param PDCClient session: PDC session. + :param Variant variant: Variant with modules to find. + :param dict variant_tags: Dict populated by this method. Key is `variant` + and value is list of Koji tags to get the RPMs from. + """ + if not session: + return + + # Find out all modules in every variant and add their Koji tags + # to variant and variant_tags list. + for module in variant.get_modules(): + pdc_module = get_module(compose, session, module["name"]) + + mmd = Modulemd.Module.new_from_string(pdc_module["modulemd"]) + mmd.upgrade() + _add_module_to_variant(variant, mmd, pdc_module["rpms"]) + + tag = pdc_module["koji_tag"] + variant_tags[variant].append(tag) + + module_msg = "Module {module} in variant {variant} will use Koji tag {tag}.".format( + variant=variant, tag=tag, module=module["name"]) + compose.log_info("%s" % module_msg) + + +def _get_modules_from_koji_tags( + compose, koji_wrapper, event_id, variant, variant_tags): + """ + Loads modules for given `variant` from Koji, adds them to + the `variant` and also to `variant_tags` dict. + + :param Compose compose: Compose for which the modules are found. + :param KojiWrapper koji_wrapper: Koji wrapper. + :param dict event_id: Koji event ID. + :param Variant variant: Variant with modules to find. + :param dict variant_tags: Dict populated by this method. Key is `variant` + and value is list of Koji tags to get the RPMs from. + """ + # Find out all modules in every variant and add their Koji tags + # to variant and variant_tags list. + koji_proxy = koji_wrapper.koji_proxy + for modular_koji_tag in variant.get_modular_koji_tags(): + tag = modular_koji_tag["name"] + + # List all the modular builds in the modular Koji tag. + # We cannot use latest=True here, because we need to get all the + # available streams of all modules. The stream is represented as + # "release" in Koji build and with latest=True, Koji would return + # only builds with highest release. + module_builds = koji_proxy.listTagged( + tag, event=event_id["id"], inherit=True, type="module") + + # Find the latest builds of all modules. This does following: + # - Sorts the module_builds descending by Koji NVR (which maps to NSV + # for modules). + # - Groups the sorted module_builds by NV (NS in modular world). + # In each resulting `ns_group`, the first item is actually build + # with the latest version (because the list is still sorted by NVR). + # - Groups the `ns_group` again by "release" ("version" in modular + # world) to just get all the "contexts" of the given NSV. This is + # stored in `nsv_builds`. + # - The `nsv_builds` contains the builds representing all the contexts + # of the latest version for give name-stream, so add them to + # `latest_builds`. + latest_builds = [] + module_builds = sorted( + module_builds, key=lambda build: build['nvr'], reverse=True) + for ns, ns_builds in groupby( + module_builds, key=lambda x: ":".join([x["name"], x["version"]])): + for nsv, nsv_builds in groupby( + ns_builds, key=lambda x: x["release"].split(".")[0]): + latest_builds += list(nsv_builds) + break + + # For each latest modular Koji build, add it to variant and + # variant_tags. + for build in latest_builds: + # Get the Build from Koji to get modulemd and module_tag. + build = koji_proxy.getBuild(build["build_id"]) + module_tag = build.get("extra", {}).get("typeinfo", {}).get( + "module", {}).get("content_koji_tag", "") + modulemd = build.get("extra", {}).get("typeinfo", {}).get( + "module", {}).get("modulemd_str", "") + if not module_tag or not modulemd: + continue + + variant_tags[variant].append(module_tag) + + # Get the list of all RPMs which are tagged in the modular + # Koji tag for this NSVC and add them to variant. + tagged_rpms = koji_proxy.listTaggedRPMS( + module_tag, event=event_id["id"], inherit=True, latest=True)[0] + rpms = [make_nvra(rpm, add_epoch=True, force_epoch=True) for rpm in + tagged_rpms] + mmd = Modulemd.Module.new_from_string(modulemd) + mmd.upgrade() + _add_module_to_variant(variant, mmd, rpms, True) + + module_msg = "Module {module} in variant {variant} will use Koji tag {tag}.".format( + variant=variant, tag=module_tag, module=build["nvr"]) + compose.log_info("%s" % module_msg) + + + def populate_global_pkgset(compose, koji_wrapper, path_prefix, event_id): all_arches = set(["src"]) for arch in compose.get_arches(): @@ -226,58 +381,41 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event_id): session = get_pdc_client_session(compose) for variant in compose.all_variants.values(): + # pkgset storing the packages belonging to this particular variant. variant.pkgset = pungi.phases.pkgset.pkgsets.KojiPackageSet( koji_wrapper, compose.conf["sigkeys"], logger=compose._logger, arches=all_arches) variant_tags[variant] = [] - pdc_module_file = os.path.join(compose.paths.work.topdir(arch="global"), - "pdc-module-%s.json" % variant.uid) - pdc_modules = [] - # Find out all modules in every variant and add their compose tags - # to compose_tags list. - if session: - for module in variant.get_modules(): - if not Modulemd: - raise ValueError( - "pygobject module or libmodulemd library is not installed, " - "support for modules is disabled, but compose contains " - "modules.") - pdc_module = get_module(compose, session, module["name"]) - pdc_modules.append(pdc_module) - mmd = Modulemd.Module.new_from_string(pdc_module["modulemd"]) - mmd.upgrade() + # Get the modules from Koji tag or from PDC, depending on + # configuration. + modular_koji_tags = variant.get_modular_koji_tags() + if (variant.modules or modular_koji_tags) and not Modulemd: + raise ValueError( + "pygobject module or libmodulemd library is not installed, " + "support for modules is disabled, but compose contains " + "modules.") - # Catch the issue when PDC does not contain RPMs, but - # the module definition says there should be some. - if not pdc_module["rpms"] and mmd.get_rpm_components(): - raise ValueError( - "Module %s does not have any rpms in 'rpms' PDC field," - "but according to modulemd, there should be some." - % pdc_module["variant_uid"]) + if modular_koji_tags: + included_modules_file = os.path.join( + compose.paths.work.topdir(arch="global"), + "koji-tag-module-%s.yaml" % variant.uid) + _get_modules_from_koji_tags( + compose, koji_wrapper, event_id, variant, variant_tags) + elif variant.modules: + included_modules_file = os.path.join( + compose.paths.work.topdir(arch="global"), + "pdc-module-%s.yaml" % variant.uid) + _get_modules_from_pdc(compose, session, variant, variant_tags) - # Add RPMs from PDC response to modulemd, so we can track - # what RPM is in which module later in gather phase. - rpm_artifacts = mmd.get_rpm_artifacts() - for rpm_nevra in pdc_module["rpms"]: - if rpm_nevra.endswith(".rpm"): - rpm_nevra = rpm_nevra[:-len(".rpm")] - rpm_artifacts.add(str(rpm_nevra)) - mmd.set_rpm_artifacts(rpm_artifacts) + # Ensure that every tag added to `variant_tags` is added also to + # `compose_tags`. + for variant_tag in variant_tags[variant]: + if not variant_tag in compose_tags: + compose_tags.append(variant_tag) - tag = pdc_module["koji_tag"] - variant.mmds.append(mmd) - variant_tags[variant].append(tag) - if tag not in compose_tags: - compose_tags.append(tag) - - module_msg = "Module {module} in variant {variant} will use Koji tag {tag}.".format( - variant=variant, tag=tag, module=module["name"]) - compose.log_info("%s" % module_msg) - - if pdc_modules: - with open(pdc_module_file, 'w') as f: - json.dump(pdc_modules, f) + if variant.mmds: + Modulemd.Module.dump_all(variant.mmds, included_modules_file) if not variant_tags[variant] and variant.modules is None: variant_tags[variant].extend(force_list(compose.conf["pkgset_koji_tag"])) diff --git a/pungi/wrappers/variants.py b/pungi/wrappers/variants.py index 34ff215a..de304b2a 100755 --- a/pungi/wrappers/variants.py +++ b/pungi/wrappers/variants.py @@ -72,6 +72,7 @@ class VariantsXmlParser(object): "arches": [str(i) for i in variant_node.xpath("arches/arch/text()")], "groups": [], "modules": None, + "modular_koji_tags": None, "environments": [], "buildinstallpackages": [], "is_empty": bool(variant_node.attrib.get("is_empty", False)), @@ -113,6 +114,14 @@ class VariantsXmlParser(object): variant_dict["modules"].append(module) + for kojitag_node in modulelist_node.xpath("kojitag"): + kojitag = { + "name": str(kojitag_node.text), + } + + variant_dict["modular_koji_tags"] = variant_dict["modular_koji_tags"] or [] + variant_dict["modular_koji_tags"].append(kojitag) + for environments_node in variant_node.xpath("environments"): for environment_node in environments_node.xpath("environment"): environment = { @@ -202,7 +211,7 @@ class VariantsXmlParser(object): class Variant(object): def __init__(self, id, name, type, arches, groups, environments=None, buildinstallpackages=None, is_empty=False, parent=None, - modules=None): + modules=None, modular_koji_tags=None): environments = environments or [] buildinstallpackages = buildinstallpackages or [] @@ -216,6 +225,9 @@ class Variant(object): self.modules = copy.deepcopy(modules) if self.modules: self.modules = sorted(self.modules, key=lambda x: x["name"]) + self.modular_koji_tags = copy.deepcopy(modular_koji_tags) + if self.modular_koji_tags: + self.modular_koji_tags = sorted(self.modular_koji_tags, key=lambda x: x["name"]) self.buildinstallpackages = sorted(buildinstallpackages) self.variants = {} self.parent = parent @@ -275,9 +287,9 @@ class Variant(object): types = types or ["self"] result = copy.deepcopy(self.groups) - for variant in self.get_variants(arch=arch, types=types, recursive=recursive): + for variant in self.get_variants(arch=arch, types=types, + recursive=recursive): if variant == self: - # XXX continue for group in variant.get_groups(arch=arch, types=types, recursive=recursive): if group not in result: @@ -295,7 +307,6 @@ class Variant(object): for variant in self.get_variants(arch=arch, types=types, recursive=recursive): if variant == self: - # XXX continue for module in variant.get_modules(arch=arch, types=types, recursive=recursive): @@ -303,6 +314,24 @@ class Variant(object): result.append(module) return result + def get_modular_koji_tags(self, arch=None, types=None, recursive=False): + """Return list of modular koji tags, default types is ["self"]""" + + if self.modular_koji_tags is None: + return [] + + types = types or ["self"] + result = copy.deepcopy(self.modular_koji_tags) + for variant in self.get_variants(arch=arch, types=types, + recursive=recursive): + if variant == self: + continue + for koji_tag in variant.get_modular_koji_tags( + arch=arch, types=types, recursive=recursive): + if koji_tag not in result: + result.append(koji_tag) + return result + def get_variants(self, arch=None, types=None, recursive=False): """ Return all variants of given arch and types. diff --git a/share/variants.dtd b/share/variants.dtd index f8e4a5f3..a9cd60e9 100644 --- a/share/variants.dtd +++ b/share/variants.dtd @@ -27,9 +27,10 @@ uservisible (true|false) #IMPLIED > - + + diff --git a/tests/helpers.py b/tests/helpers.py index 4eeabdd2..c2ab6ba1 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -52,6 +52,9 @@ class MockVariant(mock.Mock): def get_modules(self, arch=None, types=None): return [] + def get_modular_koji_tags(self, arch=None, types=None): + return [] + def add_fake_module(self, nsvc, rpm_nvrs=None): if not Modulemd: # No support for modules diff --git a/tests/test_pkgset_source_koji.py b/tests/test_pkgset_source_koji.py index bb475a3b..0c803fcf 100644 --- a/tests/test_pkgset_source_koji.py +++ b/tests/test_pkgset_source_koji.py @@ -94,7 +94,7 @@ class TestPopulateGlobalPkgset(helpers.PungiTestCase): self.compose.DEBUG = False self.koji_wrapper = mock.Mock() self.pkgset_path = os.path.join(self.topdir, 'work', 'global', 'pkgset_global.pickle') - self.pdc_module_path = os.path.join(self.topdir, 'work', 'global', 'pdc-module-Server.json') + self.pdc_module_path = os.path.join(self.topdir, 'work', 'global', 'pdc-module-Server.yaml') @mock.patch('six.moves.cPickle.dumps') @mock.patch('pungi.phases.pkgset.pkgsets.KojiPackageSet') @@ -124,22 +124,35 @@ class TestPopulateGlobalPkgset(helpers.PungiTestCase): @mock.patch('pungi.phases.pkgset.pkgsets.KojiPackageSet') @mock.patch('pungi.phases.pkgset.sources.source_koji.get_module') @mock.patch('pungi.phases.pkgset.sources.source_koji.get_pdc_client_session') - @mock.patch('pungi.phases.pkgset.sources.source_koji.Modulemd') - def test_pdc_log(self, modulemd, get_pdc_client_session, get_module, KojiPackageSet, pickle_dumps): + def test_pdc_log(self, get_pdc_client_session, get_module, KojiPackageSet, pickle_dumps): pickle_dumps.return_value = b'DATA' - get_module.return_value = {'abc': 'def', 'modulemd': 'sth', 'rpms': ['dummy'], 'koji_tag': 'taggg'} + modulemd = """ +document: modulemd +version: 2 +data: + name: foo + stream: bar + version: 1 + summary: foo + description: foo + license: + module: + - MIT +""" + + get_module.return_value = {'abc': 'def', 'modulemd': modulemd, 'rpms': [], 'koji_tag': 'taggg'} for name, variant in self.compose.variants.items(): variant.get_modules = mock.MagicMock() if name == 'Server': - variant.get_modules.return_value = [{'name': 'a'}] + variant.modules = [{'name': 'a'}] + variant.get_modules.return_value = variant.modules source_koji.populate_global_pkgset( self.compose, self.koji_wrapper, '/prefix', 123456) - with open(self.pdc_module_path, 'r') as pdc_f: - self.assertEqual(json.load(pdc_f), - [{"rpms": ["dummy"], "abc": "def", "koji_tag": "taggg", "modulemd": "sth"}]) + mmds = Modulemd.Module.new_all_from_file(self.pdc_module_path) + self.assertEqual(mmds[0].get_name(), "foo") @mock.patch('six.moves.cPickle.dumps') @mock.patch('pungi.phases.pkgset.pkgsets.KojiPackageSet')