Allow including scratch module builds

JIRA: RHELCMP-439
Signed-off-by: Haibo Lin <hlin@redhat.com>
This commit is contained in:
Haibo Lin 2020-07-06 11:02:55 +08:00
parent f5e33950c1
commit f7167fa3b6
6 changed files with 183 additions and 1 deletions

View File

@ -190,6 +190,11 @@ Options
(*str*) -- Allows to set default compose type. Type set via a command-line (*str*) -- Allows to set default compose type. Type set via a command-line
option overwrites this. option overwrites this.
**mbs_api_url**
(*str*) -- URL to Module Build Service (MBS) API.
For example ``https://mbs.example.com/module-build-service/2``.
This is required by ``pkgset_scratch_modules``.
Example Example
------- -------
:: ::
@ -548,6 +553,10 @@ Options
(*dict*) -- A mapping of architectures to repositories with RPMs: ``{arch: (*dict*) -- A mapping of architectures to repositories with RPMs: ``{arch:
[repo]}``. Only use when ``pkgset_source = "repos"``. [repo]}``. Only use when ``pkgset_source = "repos"``.
**pkgset_scratch_modules**
(*dict*) -- A mapping of variants to scratch module builds: ``{variant:
[N:S:V:C]}``. Requires ``mbs_api_url``.
**pkgset_exclusive_arch_considers_noarch** = True **pkgset_exclusive_arch_considers_noarch** = True
(*bool*) -- If a package includes ``noarch`` in its ``ExclusiveArch`` tag, (*bool*) -- If a package includes ``noarch`` in its ``ExclusiveArch`` tag,
it will be included in all architectures since ``noarch`` is compatible it will be included in all architectures since ``noarch`` is compatible

View File

@ -786,6 +786,14 @@ def make_schema():
"type": "boolean", "type": "boolean",
"default": True, "default": True,
}, },
"pkgset_scratch_modules": {
"type": "object",
"patternProperties": {
"^.+$": {"$ref": "#/definitions/list_of_strings"}
},
"additionalProperties": False,
},
"mbs_api_url": {"type": "string"},
"disc_types": {"type": "object", "default": {}}, "disc_types": {"type": "object", "default": {}},
"paths_module": {"type": "string"}, "paths_module": {"type": "string"},
"skip_phases": { "skip_phases": {
@ -1258,6 +1266,7 @@ CONFIG_DEPS = {
"conflicts": ((lambda x: not x, ["base_product_name", "base_product_short"]),), "conflicts": ((lambda x: not x, ["base_product_name", "base_product_short"]),),
}, },
"product_id": {"conflicts": [(lambda x: not x, ["product_id_allow_missing"])]}, "product_id": {"conflicts": [(lambda x: not x, ["product_id_allow_missing"])]},
"pkgset_scratch_modules": {"requires": ((lambda x: x, ["mbs_api_url"]),)},
"pkgset_source": { "pkgset_source": {
"requires": [(lambda x: x == "repos", ["pkgset_repos"])], "requires": [(lambda x: x == "repos", ["pkgset_repos"])],
"conflicts": [ "conflicts": [

View File

@ -490,6 +490,8 @@ class WorkPaths(object):
def module_defaults_dir(self, create_dir=True): def module_defaults_dir(self, create_dir=True):
""" """
Example:
work/global/module_defaults
""" """
path = os.path.join(self.topdir(create_dir=create_dir), "module_defaults") path = os.path.join(self.topdir(create_dir=create_dir), "module_defaults")
if create_dir: if create_dir:

View File

@ -26,9 +26,10 @@ from kobo.shortcuts import force_list
import pungi.wrappers.kojiwrapper import pungi.wrappers.kojiwrapper
from pungi.wrappers.comps import CompsWrapper from pungi.wrappers.comps import CompsWrapper
from pungi.wrappers.mbs import MBSWrapper
import pungi.phases.pkgset.pkgsets import pungi.phases.pkgset.pkgsets
from pungi.arch import getBaseArch from pungi.arch import getBaseArch
from pungi.util import retry, get_arch_variant_data from pungi.util import retry, get_arch_variant_data, get_variant_data
from pungi.module_util import Modulemd from pungi.module_util import Modulemd
from pungi.phases.pkgset.common import MaterializedPackageSet, get_all_arches from pungi.phases.pkgset.common import MaterializedPackageSet, get_all_arches
@ -272,6 +273,51 @@ def _add_module_to_variant(
return nsvc return nsvc
def _add_scratch_modules_to_variant(
compose, variant, scratch_modules, variant_tags, tag_to_mmd
):
if compose.compose_type != "test" and scratch_modules:
compose.log_warning("Only test composes could include scratch module builds")
return
mbs = MBSWrapper(compose.conf["mbs_api_url"])
for nsvc in scratch_modules:
module_build = mbs.get_module_build_by_nsvc(nsvc)
if not module_build:
continue
try:
final_modulemd = mbs.final_modulemd(module_build["id"])
except Exception:
compose.log_error("Unable to get modulemd for build %s" % module_build)
raise
tag = module_build["koji_tag"]
variant_tags[variant].append(tag)
tag_to_mmd.setdefault(tag, {})
for arch in variant.arches:
try:
mmd = Modulemd.ModuleStream.read_string(
final_modulemd[arch], strict=True
)
variant.arch_mmds.setdefault(arch, {})[nsvc] = mmd
except KeyError:
continue
tag_to_mmd[tag].setdefault(arch, set()).add(mmd)
if tag_to_mmd[tag]:
compose.log_info(
"Module '%s' in variant '%s' will use Koji tag '%s' "
"(as a result of querying module '%s')",
nsvc,
variant,
tag,
module_build["name"],
)
# Store mapping NSVC --> koji_tag into variant. This is needed
# in createrepo phase where metadata is exposed by productmd
variant.module_uid_to_koji_tag[nsvc] = tag
def _is_filtered_out(compose, variant, arch, module_name, module_stream): def _is_filtered_out(compose, variant, arch, module_name, module_stream):
"""Check if module with given name and stream is filter out from this stream. """Check if module with given name and stream is filter out from this stream.
""" """
@ -618,6 +664,14 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
compose, koji_wrapper, event, variant, variant_tags, tag_to_mmd compose, koji_wrapper, event, variant, variant_tags, tag_to_mmd
) )
variant_scratch_modules = get_variant_data(
compose.conf, "pkgset_scratch_modules", variant
)
if variant_scratch_modules:
_add_scratch_modules_to_variant(
compose, variant, variant_scratch_modules, variant_tags, tag_to_mmd
)
# Ensure that every tag added to `variant_tags` is added also to # Ensure that every tag added to `variant_tags` is added also to
# `compose_tags`. # `compose_tags`.
for variant_tag in variant_tags[variant]: for variant_tag in variant_tags[variant]:

43
pungi/wrappers/mbs.py Normal file
View File

@ -0,0 +1,43 @@
import os
import requests
class MBSWrapper(object):
def __init__(self, api_url):
"""
:param string api_url: e.g. https://example.com/module-build-service/2
"""
self.api_url = api_url
def _get(self, resource, params=None):
"""Get specified resource.
:param string resource: e.g. module-builds, final-modulemd
:param dict data:
"""
url = os.path.join(self.api_url, resource)
try:
resp = requests.get(url, params=params)
except Exception as e:
raise Exception(
"Failed to query URL %s with params %s - %s" % (url, params, str(e))
)
resp.raise_for_status()
return resp
def module_builds(self, filters=None):
return self._get("module-builds", filters).json()
def get_module_build_by_nsvc(self, nsvc):
nsvc_list = nsvc.split(":")
if len(nsvc_list) != 4:
raise ValueError("Invalid N:S:V:C - %s" % nsvc)
filters = dict(zip(["name", "stream", "version", "context"], nsvc_list))
resp = self.module_builds(filters)
if resp["items"]:
return resp["items"][0]
else:
return None
def final_modulemd(self, module_build_id):
return self._get("final-modulemd/%s" % module_build_id).json()

View File

@ -881,3 +881,68 @@ class TestIsModuleFiltered(helpers.PungiTestCase):
self.assertIsFiltered("foo", "master") self.assertIsFiltered("foo", "master")
self.assertIsNotFiltered("bar", "master") self.assertIsNotFiltered("bar", "master")
self.assertIsNotFiltered("foo", "stable") self.assertIsNotFiltered("foo", "stable")
class MockMBS(object):
def __init__(self, api_url):
self.api_url = api_url
def get_module_build_by_nsvc(self, nsvc):
return {"id": 1, "koji_tag": "scratch-module-tag", "name": "scratch-module"}
def final_modulemd(self, module_build_id):
return {"x86_64": ""}
class MockMmd(object):
def __init__(self, mmd, strict=True):
pass
@mock.patch("pungi.phases.pkgset.sources.source_koji.MBSWrapper", new=MockMBS)
@unittest.skipIf(Modulemd is None, "Skipping tests, no module support")
class TestAddScratchModuleToVariant(helpers.PungiTestCase):
def setUp(self):
super(TestAddScratchModuleToVariant, self).setUp()
self.compose = helpers.DummyCompose(
self.topdir, {"mbs_api_url": "http://mbs.local/module-build-service/2"}
)
self.nsvc = "scratch-module:master:20200710:abcdef"
@mock.patch(
"pungi.phases.pkgset.sources.source_koji.Modulemd.ModuleStream.read_string"
)
def test_adding_scratch_module(self, mock_mmd):
variant = mock.Mock(
arches=["armhfp", "x86_64"],
arch_mmds={},
modules=[],
module_uid_to_koji_tag={},
)
variant_tags = {variant: []}
tag_to_mmd = {}
scratch_modules = [self.nsvc]
source_koji._add_scratch_modules_to_variant(
self.compose, variant, scratch_modules, variant_tags, tag_to_mmd
)
self.assertEqual(variant_tags, {variant: ["scratch-module-tag"]})
self.assertEqual(
variant.arch_mmds, {"x86_64": {self.nsvc: mock_mmd.return_value}}
)
self.assertEqual(
tag_to_mmd, {"scratch-module-tag": {"x86_64": {mock_mmd.return_value}}}
)
self.assertEqual(variant.modules, [])
def test_adding_scratch_module_nontest_compose(self):
self.compose.compose_type = "production"
scratch_modules = [self.nsvc]
source_koji._add_scratch_modules_to_variant(
self.compose, mock.Mock(), scratch_modules, {}, {}
)
self.compose.log_warning.assert_called_once_with(
"Only test composes could include scratch module builds"
)