diff --git a/CHANGES/+distribution-version.feature b/CHANGES/+distribution-version.feature new file mode 100644 index 000000000..279c8a4f8 --- /dev/null +++ b/CHANGES/+distribution-version.feature @@ -0,0 +1 @@ +Added the `--version` option to rpm and file distribution create and update commands to pin a specific repository version for distribution. diff --git a/CHANGES/pulp-glue/+distribution-version.feature b/CHANGES/pulp-glue/+distribution-version.feature new file mode 100644 index 000000000..fd8edb926 --- /dev/null +++ b/CHANGES/pulp-glue/+distribution-version.feature @@ -0,0 +1 @@ +Added support for setting `repository_version` on rpm and file distributions via a `version` parameter. diff --git a/pulp-glue/src/pulp_glue/file/context.py b/pulp-glue/src/pulp_glue/file/context.py index 76f88cf07..baeb47fc5 100644 --- a/pulp-glue/src/pulp_glue/file/context.py +++ b/pulp-glue/src/pulp_glue/file/context.py @@ -12,6 +12,7 @@ PulpRepositoryVersionContext, api_spec_quirk, ) +from pulp_glue.common.exceptions import PulpException from pulp_glue.common.i18n import get_translation translation = get_translation(__package__) @@ -87,6 +88,23 @@ def preprocess_entity(self, body: EntityDefinition, partial: bool = False) -> En body["publication"] = None if "repository" not in body and "publication" in body: body["repository"] = None + + version = body.pop("version", None) + if version is not None: + self.pulp_ctx.needs_plugin(PluginRequirement("core", specifier=">=3.106.0")) + repository_href = body.pop("repository", None) + if repository_href is None and partial: + repository_href = self.entity.get("repository") + if repository_href is None: + raise PulpException(_("--repository must be provided")) + body["repository_version"] = f"{repository_href}versions/{version}/" + body["repository"] = None + body["publication"] = None + elif "repository" in body and self.pulp_ctx.has_plugin( + PluginRequirement("core", specifier=">=3.106.0") + ): + body["repository_version"] = None + return body diff --git a/pulp-glue/src/pulp_glue/rpm/context.py b/pulp-glue/src/pulp_glue/rpm/context.py index ca7c9d50a..d85a7cb26 100644 --- a/pulp-glue/src/pulp_glue/rpm/context.py +++ b/pulp-glue/src/pulp_glue/rpm/context.py @@ -15,6 +15,7 @@ PulpViewSetContext, api_spec_quirk, ) +from pulp_glue.common.exceptions import PulpException from pulp_glue.common.i18n import get_translation translation = get_translation(__package__) @@ -86,6 +87,23 @@ def preprocess_entity(self, body: EntityDefinition, partial: bool = False) -> En body["publication"] = None if "repository" not in body and "publication" in body: body["repository"] = None + + version = body.pop("version", None) + if version is not None: + self.pulp_ctx.needs_plugin(PluginRequirement("core", specifier=">=3.106.0")) + repository_href = body.pop("repository", None) + if repository_href is None and partial: + repository_href = self.entity.get("repository") + if repository_href is None: + raise PulpException(_("--repository must be provided")) + body["repository_version"] = f"{repository_href}versions/{version}/" + body["repository"] = None + body["publication"] = None + elif "repository" in body and self.pulp_ctx.has_plugin( + PluginRequirement("core", specifier=">=3.106.0") + ): + body["repository_version"] = None + if body.get("generate_repo_config") is False: self.pulp_ctx.needs_plugin( PluginRequirement( diff --git a/pulp-glue/tests/test_distribution_preprocess.py b/pulp-glue/tests/test_distribution_preprocess.py new file mode 100644 index 000000000..f39f8901c --- /dev/null +++ b/pulp-glue/tests/test_distribution_preprocess.py @@ -0,0 +1,111 @@ +import json +import typing as t + +import pytest + +from pulp_glue.common import context as context_module +from pulp_glue.common.context import PulpContext, PulpDistributionContext +from pulp_glue.common.exceptions import PulpException +from pulp_glue.common.openapi import OpenAPI +from pulp_glue.file.context import PulpFileDistributionContext +from pulp_glue.rpm.context import PulpRpmDistributionContext + +pytestmark = pytest.mark.glue + +REPO_HREF = "/pulp/api/v3/repositories/rpm/rpm/01234567-0123-0123-0123-0123456789ab/" +REPO_VERSION_HREF = REPO_HREF + "versions/5/" +PUBLICATION_HREF = "/pulp/api/v3/publications/rpm/rpm/01234567-0123-0123-0123-0123456789ab/" + + +@pytest.fixture +def modern_pulp_ctx(monkeypatch: pytest.MonkeyPatch) -> PulpContext: + spec = json.dumps( + { + "openapi": "3.0.3", + "info": { + "title": "test", + "version": "0.0.0", + "x-pulp-app-versions": {"core": "3.106.0", "rpm": "3.30.0", "file": "3.75.0"}, + }, + "paths": {}, + } + ) + monkeypatch.setattr(context_module, "_patch_api_hook", lambda spec: spec) + monkeypatch.setattr(OpenAPI, "load_api", lambda self, refresh_cache: self._parse_api(spec)) + monkeypatch.setattr( + OpenAPI, + "_send_request", + lambda *args, **kwargs: pytest.fail("No API calls allowed in unit tests."), + ) + settings: dict[str, t.Any] = {"base_url": "nowhere"} + return PulpContext.from_config(settings) + + +@pytest.mark.parametrize( + "ctx_cls", + [PulpRpmDistributionContext, PulpFileDistributionContext], + ids=["rpm", "file"], +) +class TestDistributionPreprocess: + def test_version_with_repository( + self, modern_pulp_ctx: PulpContext, ctx_cls: type[PulpDistributionContext] + ) -> None: + ctx = ctx_cls(modern_pulp_ctx) + body = ctx.preprocess_entity({"version": 5, "repository": REPO_HREF}, partial=False) + assert body["repository_version"] == REPO_VERSION_HREF + assert body["repository"] is None + assert body["publication"] is None + assert "version" not in body + + def test_repository_without_version( + self, modern_pulp_ctx: PulpContext, ctx_cls: type[PulpDistributionContext] + ) -> None: + ctx = ctx_cls(modern_pulp_ctx) + body = ctx.preprocess_entity({"repository": REPO_HREF}, partial=False) + assert body["repository"] == REPO_HREF + assert body["repository_version"] is None + assert body["publication"] is None + + def test_publication_without_repository( + self, modern_pulp_ctx: PulpContext, ctx_cls: type[PulpDistributionContext] + ) -> None: + ctx = ctx_cls(modern_pulp_ctx) + body = ctx.preprocess_entity({"publication": PUBLICATION_HREF}, partial=False) + assert body["publication"] == PUBLICATION_HREF + assert body["repository"] is None + assert body["repository_version"] is None + + def test_version_without_repository_raises( + self, modern_pulp_ctx: PulpContext, ctx_cls: type[PulpDistributionContext] + ) -> None: + ctx = ctx_cls(modern_pulp_ctx) + with pytest.raises(PulpException, match="--repository"): + ctx.preprocess_entity({"version": 5}, partial=False) + + def test_version_without_repository_partial_raises( + self, modern_pulp_ctx: PulpContext, ctx_cls: type[PulpDistributionContext] + ) -> None: + ctx = ctx_cls(modern_pulp_ctx) + ctx._entity = {"repository": None, "repository_version": None} + with pytest.raises(PulpException, match="--repository"): + ctx.preprocess_entity({"version": 5}, partial=True) + + def test_partial_version_infers_repository( + self, modern_pulp_ctx: PulpContext, ctx_cls: type[PulpDistributionContext] + ) -> None: + ctx = ctx_cls(modern_pulp_ctx) + ctx._entity = {"repository": REPO_HREF, "repository_version": None} + body = ctx.preprocess_entity({"version": 3}, partial=True) + assert body["repository_version"] == REPO_HREF + "versions/3/" + assert body["repository"] is None + assert body["publication"] is None + + def test_unrelated_field_no_nullification( + self, modern_pulp_ctx: PulpContext, ctx_cls: type[PulpDistributionContext] + ) -> None: + ctx = ctx_cls(modern_pulp_ctx) + body = ctx.preprocess_entity({"base_path": "/new/path"}, partial=True) + assert body.get("base_path") == "/new/path" + assert "repository" not in body + assert "repository_version" not in body + assert "publication" not in body diff --git a/src/pulpcore/cli/file/distribution.py b/src/pulpcore/cli/file/distribution.py index 397dba963..6ea5724bf 100644 --- a/src/pulpcore/cli/file/distribution.py +++ b/src/pulpcore/cli/file/distribution.py @@ -63,6 +63,15 @@ def distribution() -> None: ), ), repository_option, + pulp_option( + "--version", + type=int, + help=_( + "The repository version number to distribute." + " When unset, the latest version of the repository will be auto-distributed." + ), + needs_plugins=[PluginRequirement("core", specifier=">=3.106.0")], + ), content_guard_option, pulp_labels_option, pulp_option( diff --git a/src/pulpcore/cli/rpm/distribution.py b/src/pulpcore/cli/rpm/distribution.py index 5d25a3d9c..0bd9fc1d3 100644 --- a/src/pulpcore/cli/rpm/distribution.py +++ b/src/pulpcore/cli/rpm/distribution.py @@ -64,6 +64,15 @@ def distribution() -> None: help=_("Option specifying whether ``*.repo`` files will be generated and served."), ), repository_option, + pulp_option( + "--version", + type=int, + help=_( + "The repository version number to distribute." + " When unset, the latest version of the repository will be auto-distributed." + ), + needs_plugins=[PluginRequirement("core", specifier=">=3.106.0")], + ), content_guard_option, pulp_labels_option, pulp_option(