Add module obsoletes feature

JIRA: MODULAR-113
Merges: https://pagure.io/pungi/pull-request/1578
Signed-off-by: Filip Valder <fvalder@redhat.com>
This commit is contained in:
Filip Valder 2021-12-15 10:13:23 +01:00 committed by Lubomír Sedlář
parent 42f668d969
commit fe986d68b9
11 changed files with 124 additions and 24 deletions

2
.gitignore vendored
View File

@ -14,3 +14,5 @@ htmlcov/
.idea/ .idea/
.tox .tox
.venv .venv
.kdev4/
pungi.kdev4

View File

@ -30,9 +30,17 @@ This is a shortened configuration for Fedora Radhide compose as of 2019-10-14.
module_defaults_dir = { module_defaults_dir = {
'scm': 'git', 'scm': 'git',
'repo': 'https://pagure.io/releng/fedora-module-defaults.git', 'repo': 'https://pagure.io/releng/fedora-module-defaults.git',
'branch': 'master', 'branch': 'main',
'dir': '.' 'dir': '.'
} }
# Optional module obsoletes configuration which is merged
# into the module index and gets resolved
module_obsoletes_dir = {
'scm': 'git',
'repo': 'https://pagure.io/releng/fedora-module-defaults.git',
'branch': 'main',
'dir': 'obsoletes'
}
variants_file='variants-fedora.xml' variants_file='variants-fedora.xml'
sigkeys = ['12C944D0'] sigkeys = ['12C944D0']

View File

@ -736,6 +736,7 @@ def make_schema():
"patternProperties": {".+": {"$ref": "#/definitions/strings"}}, "patternProperties": {".+": {"$ref": "#/definitions/strings"}},
"additionalProperties": False, "additionalProperties": False,
}, },
"module_obsoletes_dir": {"$ref": "#/definitions/str_or_scm_dict"},
"create_optional_isos": {"type": "boolean", "default": False}, "create_optional_isos": {"type": "boolean", "default": False},
"symlink_isos_to": {"type": "string"}, "symlink_isos_to": {"type": "string"},
"dogpile_cache_backend": {"type": "string"}, "dogpile_cache_backend": {"type": "string"},

View File

@ -377,6 +377,10 @@ class Compose(kobo.log.LoggingBase):
def has_module_defaults(self): def has_module_defaults(self):
return bool(self.conf.get("module_defaults_dir", False)) return bool(self.conf.get("module_defaults_dir", False))
@property
def has_module_obsoletes(self):
return bool(self.conf.get("module_obsoletes_dir", False))
@property @property
def config_dir(self): def config_dir(self):
return os.path.dirname(self.conf._open_file or "") return os.path.dirname(self.conf._open_file or "")

View File

@ -25,9 +25,10 @@ except (ImportError, ValueError):
Modulemd = None Modulemd = None
def iter_module_defaults(path): def iter_module_defaults_or_obsoletes(path, obsoletes=False):
"""Given a path to a directory with yaml files, yield each module default """Given a path to a directory with yaml files, yield each module default
in there as a pair (module_name, ModuleDefaults instance). in there as a pair (module_name, ModuleDefaults instance).
The same happens for module obsoletes if the obsoletes switch is True.
""" """
# It is really tempting to merge all the module indexes into a single one # It is really tempting to merge all the module indexes into a single one
# and work with it. However that does not allow for detecting conflicting # and work with it. However that does not allow for detecting conflicting
@ -41,7 +42,10 @@ def iter_module_defaults(path):
index = Modulemd.ModuleIndex() index = Modulemd.ModuleIndex()
index.update_from_file(file, strict=False) index.update_from_file(file, strict=False)
for module_name in index.get_module_names(): for module_name in index.get_module_names():
yield module_name, index.get_module(module_name).get_defaults() if obsoletes:
yield module_name, index.get_module(module_name).get_obsoletes()
else:
yield module_name, index.get_module(module_name).get_defaults()
def collect_module_defaults( def collect_module_defaults(
@ -69,3 +73,21 @@ def collect_module_defaults(
mod_index.add_defaults(defaults) mod_index.add_defaults(defaults)
return mod_index return mod_index
def collect_module_obsoletes(obsoletes_dir, modules_to_load, mod_index=None):
"""Load module obsoletes into index.
This works in a similar fashion as collect_module_defaults except the overrides_dir
feature.
"""
mod_index = mod_index or Modulemd.ModuleIndex()
for module_name, obsoletes in iter_module_defaults_or_obsoletes(
obsoletes_dir, obsoletes=True
):
for obsolete in obsoletes:
if not modules_to_load or module_name in modules_to_load:
mod_index.add_obsoletes(obsoletes)
return mod_index

View File

@ -509,6 +509,16 @@ class WorkPaths(object):
makedirs(path) makedirs(path)
return path return path
def module_obsoletes_dir(self, create_dir=True):
"""
Example:
work/global/module_obsoletes
"""
path = os.path.join(self.topdir(create_dir=create_dir), "module_obsoletes")
if create_dir:
makedirs(path)
return path
def pkgset_file_cache(self, pkgset_name): def pkgset_file_cache(self, pkgset_name):
""" """
Returns the path to file in which the cached version of Returns the path to file in which the cached version of

View File

@ -29,7 +29,7 @@ import productmd.rpms
from kobo.shortcuts import relative_path, run from kobo.shortcuts import relative_path, run
from kobo.threads import ThreadPool, WorkerThread from kobo.threads import ThreadPool, WorkerThread
from ..module_util import Modulemd, collect_module_defaults from ..module_util import Modulemd, collect_module_defaults, collect_module_obsoletes
from ..util import ( from ..util import (
get_arch_variant_data, get_arch_variant_data,
read_single_module_stream_from_file, read_single_module_stream_from_file,
@ -266,6 +266,9 @@ def create_variant_repo(
defaults_dir, module_names, mod_index, overrides_dir=overrides_dir defaults_dir, module_names, mod_index, overrides_dir=overrides_dir
) )
obsoletes_dir = compose.paths.work.module_obsoletes_dir()
collect_module_obsoletes(obsoletes_dir, module_names, mod_index)
# Add extra modulemd files # Add extra modulemd files
if variant.uid in compose.conf.get("createrepo_extra_modulemd", {}): if variant.uid in compose.conf.get("createrepo_extra_modulemd", {}):
compose.log_debug("Adding extra modulemd for %s.%s", variant.uid, arch) compose.log_debug("Adding extra modulemd for %s.%s", variant.uid, arch)

View File

@ -33,7 +33,11 @@ except ImportError:
import pungi.wrappers.kojiwrapper import pungi.wrappers.kojiwrapper
from pungi.arch import get_compatible_arches, split_name_arch from pungi.arch import get_compatible_arches, split_name_arch
from pungi.compose import get_ordered_variant_uids from pungi.compose import get_ordered_variant_uids
from pungi.module_util import Modulemd, collect_module_defaults from pungi.module_util import (
Modulemd,
collect_module_defaults,
collect_module_obsoletes,
)
from pungi.phases.base import PhaseBase from pungi.phases.base import PhaseBase
from pungi.phases.createrepo import add_modular_metadata from pungi.phases.createrepo import add_modular_metadata
from pungi.util import get_arch_data, get_arch_variant_data, get_variant_data, makedirs from pungi.util import get_arch_data, get_arch_variant_data, get_variant_data, makedirs
@ -698,6 +702,8 @@ def _make_lookaside_repo(compose, variant, arch, pkg_map, package_sets=None):
collect_module_defaults( collect_module_defaults(
defaults_dir, module_names, mod_index, overrides_dir=overrides_dir defaults_dir, module_names, mod_index, overrides_dir=overrides_dir
) )
obsoletes_dir = compose.paths.work.module_obsoletes_dir()
collect_module_obsoletes(obsoletes_dir, module_names, mod_index)
log_file = compose.paths.log.log_file( log_file = compose.paths.log.log_file(
arch, "lookaside_repo_modules_%s" % (variant.uid) arch, "lookaside_repo_modules_%s" % (variant.uid)

View File

@ -24,7 +24,7 @@ from kobo.threads import run_in_threads
from pungi.phases.base import PhaseBase from pungi.phases.base import PhaseBase
from pungi.phases.gather import write_prepopulate_file from pungi.phases.gather import write_prepopulate_file
from pungi.util import temp_dir from pungi.util import temp_dir
from pungi.module_util import iter_module_defaults from pungi.module_util import iter_module_defaults_or_obsoletes
from pungi.wrappers.comps import CompsWrapper from pungi.wrappers.comps import CompsWrapper
from pungi.wrappers.createrepo import CreaterepoWrapper from pungi.wrappers.createrepo import CreaterepoWrapper
from pungi.wrappers.scm import get_dir_from_scm, get_file_from_scm from pungi.wrappers.scm import get_dir_from_scm, get_file_from_scm
@ -68,10 +68,18 @@ class InitPhase(PhaseBase):
# download module defaults # download module defaults
if self.compose.has_module_defaults: if self.compose.has_module_defaults:
write_module_defaults(self.compose) write_module_defaults(self.compose)
validate_module_defaults( validate_module_defaults_or_obsoletes(
self.compose.paths.work.module_defaults_dir(create_dir=False) self.compose.paths.work.module_defaults_dir(create_dir=False)
) )
# download module obsoletes
if self.compose.has_module_obsoletes:
write_module_obsoletes(self.compose)
validate_module_defaults_or_obsoletes(
self.compose.paths.work.module_obsoletes_dir(create_dir=False),
obsoletes=True,
)
# write prepopulate file # write prepopulate file
write_prepopulate_file(self.compose) write_prepopulate_file(self.compose)
@ -218,28 +226,53 @@ def write_module_defaults(compose):
) )
def validate_module_defaults(path): def write_module_obsoletes(compose):
scm_dict = compose.conf["module_obsoletes_dir"]
if isinstance(scm_dict, dict):
if scm_dict["scm"] == "file":
scm_dict["dir"] = os.path.join(compose.config_dir, scm_dict["dir"])
else:
scm_dict = os.path.join(compose.config_dir, scm_dict)
with temp_dir(prefix="moduleobsoletes_") as tmp_dir:
get_dir_from_scm(scm_dict, tmp_dir, compose=compose)
compose.log_debug("Writing module obsoletes")
shutil.copytree(
tmp_dir,
compose.paths.work.module_obsoletes_dir(create_dir=False),
ignore=shutil.ignore_patterns(".git"),
)
def validate_module_defaults_or_obsoletes(path, obsoletes=False):
"""Make sure there are no conflicting defaults. Each module name can only """Make sure there are no conflicting defaults. Each module name can only
have one default stream. have one default stream or module obsolete.
:param str path: directory with cloned module defaults :param str path: directory with cloned module defaults/obsoletes
""" """
seen_defaults = collections.defaultdict(set) seen = collections.defaultdict(set)
mmd_type = "obsoletes" if obsoletes else "defaults"
for module_name, defaults in iter_module_defaults(path): for module_name, defaults_or_obsoletes in iter_module_defaults_or_obsoletes(
seen_defaults[module_name].add(defaults.get_default_stream()) path, obsoletes
):
if obsoletes:
for obsolete in defaults_or_obsoletes:
seen[obsolete.props.module_name].add(obsolete)
else:
seen[module_name].add(defaults_or_obsoletes.get_default_stream())
errors = [] errors = []
for module_name, defaults in seen_defaults.items(): for module_name, defaults_or_obsoletes in seen.items():
if len(defaults) > 1: if len(defaults_or_obsoletes) > 1:
errors.append( errors.append(
"Module %s has multiple defaults: %s" "Module %s has multiple %s: %s"
% (module_name, ", ".join(sorted(defaults))) % (module_name, mmd_type, ", ".join(sorted(defaults_or_obsoletes)))
) )
if errors: if errors:
raise RuntimeError( raise RuntimeError(
"There are duplicated module defaults:\n%s" % "\n".join(errors) "There are duplicated module %s:\n%s" % (mmd_type, "\n".join(errors))
) )

View File

@ -28,7 +28,11 @@ from pungi.util import (
PartialFuncWorkerThread, PartialFuncWorkerThread,
PartialFuncThreadPool, PartialFuncThreadPool,
) )
from pungi.module_util import Modulemd, collect_module_defaults from pungi.module_util import (
Modulemd,
collect_module_defaults,
collect_module_obsoletes,
)
from pungi.phases.createrepo import add_modular_metadata from pungi.phases.createrepo import add_modular_metadata
@ -159,6 +163,9 @@ def _create_arch_repo(worker_thread, args, task_num):
mod_index = collect_module_defaults( mod_index = collect_module_defaults(
compose.paths.work.module_defaults_dir(), names, overrides_dir=overrides_dir compose.paths.work.module_defaults_dir(), names, overrides_dir=overrides_dir
) )
mod_index = collect_module_obsoletes(
compose.paths.work.module_obsoletes_dir(), names, mod_index
)
for x in mmd: for x in mmd:
mod_index.add_module_stream(x) mod_index.add_module_stream(x)
add_modular_metadata( add_modular_metadata(

View File

@ -24,7 +24,7 @@ from tests.helpers import (
@mock.patch("pungi.phases.init.run_in_threads", new=fake_run_in_threads) @mock.patch("pungi.phases.init.run_in_threads", new=fake_run_in_threads)
@mock.patch("pungi.phases.init.validate_comps") @mock.patch("pungi.phases.init.validate_comps")
@mock.patch("pungi.phases.init.validate_module_defaults") @mock.patch("pungi.phases.init.validate_module_defaults_or_obsoletes")
@mock.patch("pungi.phases.init.write_module_defaults") @mock.patch("pungi.phases.init.write_module_defaults")
@mock.patch("pungi.phases.init.write_global_comps") @mock.patch("pungi.phases.init.write_global_comps")
@mock.patch("pungi.phases.init.write_arch_comps") @mock.patch("pungi.phases.init.write_arch_comps")
@ -46,6 +46,7 @@ class TestInitPhase(PungiTestCase):
compose = DummyCompose(self.topdir, {}) compose = DummyCompose(self.topdir, {})
compose.has_comps = True compose.has_comps = True
compose.has_module_defaults = False compose.has_module_defaults = False
compose.has_module_obsoletes = False
compose.setup_optional() compose.setup_optional()
phase = init.InitPhase(compose) phase = init.InitPhase(compose)
phase.run() phase.run()
@ -100,6 +101,7 @@ class TestInitPhase(PungiTestCase):
compose = DummyCompose(self.topdir, {}) compose = DummyCompose(self.topdir, {})
compose.has_comps = True compose.has_comps = True
compose.has_module_defaults = False compose.has_module_defaults = False
compose.has_module_obsoletes = False
compose.variants["Everything"].groups = [] compose.variants["Everything"].groups = []
compose.variants["Everything"].modules = [] compose.variants["Everything"].modules = []
phase = init.InitPhase(compose) phase = init.InitPhase(compose)
@ -156,6 +158,7 @@ class TestInitPhase(PungiTestCase):
compose = DummyCompose(self.topdir, {}) compose = DummyCompose(self.topdir, {})
compose.has_comps = False compose.has_comps = False
compose.has_module_defaults = False compose.has_module_defaults = False
compose.has_module_obsoletes = False
phase = init.InitPhase(compose) phase = init.InitPhase(compose)
phase.run() phase.run()
@ -182,6 +185,7 @@ class TestInitPhase(PungiTestCase):
compose = DummyCompose(self.topdir, {}) compose = DummyCompose(self.topdir, {})
compose.has_comps = False compose.has_comps = False
compose.has_module_defaults = True compose.has_module_defaults = True
compose.has_module_obsoletes = False
phase = init.InitPhase(compose) phase = init.InitPhase(compose)
phase.run() phase.run()
@ -620,13 +624,13 @@ class TestValidateModuleDefaults(PungiTestCase):
def test_valid_files(self): def test_valid_files(self):
self._write_defaults({"httpd": ["1"], "python": ["3.6"]}) self._write_defaults({"httpd": ["1"], "python": ["3.6"]})
init.validate_module_defaults(self.topdir) init.validate_module_defaults_or_obsoletes(self.topdir)
def test_duplicated_stream(self): def test_duplicated_stream(self):
self._write_defaults({"httpd": ["1"], "python": ["3.6", "3.5"]}) self._write_defaults({"httpd": ["1"], "python": ["3.6", "3.5"]})
with self.assertRaises(RuntimeError) as ctx: with self.assertRaises(RuntimeError) as ctx:
init.validate_module_defaults(self.topdir) init.validate_module_defaults_or_obsoletes(self.topdir)
self.assertIn( self.assertIn(
"Module python has multiple defaults: 3.5, 3.6", str(ctx.exception) "Module python has multiple defaults: 3.5, 3.6", str(ctx.exception)
@ -636,7 +640,7 @@ class TestValidateModuleDefaults(PungiTestCase):
self._write_defaults({"httpd": ["1", "2"], "python": ["3.6", "3.5"]}) self._write_defaults({"httpd": ["1", "2"], "python": ["3.6", "3.5"]})
with self.assertRaises(RuntimeError) as ctx: with self.assertRaises(RuntimeError) as ctx:
init.validate_module_defaults(self.topdir) init.validate_module_defaults_or_obsoletes(self.topdir)
self.assertIn("Module httpd has multiple defaults: 1, 2", str(ctx.exception)) self.assertIn("Module httpd has multiple defaults: 1, 2", str(ctx.exception))
self.assertIn( self.assertIn(
@ -661,7 +665,7 @@ class TestValidateModuleDefaults(PungiTestCase):
), ),
) )
init.validate_module_defaults(self.topdir) init.validate_module_defaults_or_obsoletes(self.topdir)
@mock.patch("pungi.phases.init.CompsWrapper") @mock.patch("pungi.phases.init.CompsWrapper")