diff --git a/CHANGES/plugin_api/7606.feature b/CHANGES/plugin_api/7606.feature new file mode 100644 index 00000000000..1284ebec368 --- /dev/null +++ b/CHANGES/plugin_api/7606.feature @@ -0,0 +1,4 @@ +Added a `publish` parameter to the repository modify endpoint and the `add_and_remove` task. +Plugins can opt in by accepting `publish` in their `on_new_version` override or custom +`modify_task`. The `RepositoryVersion` context manager will pass `publish=True` to +`on_new_version` if the plugin's method signature supports it. diff --git a/CHANGES/pulp_file/7606.feature b/CHANGES/pulp_file/7606.feature new file mode 100644 index 00000000000..6e8f7d44e9f --- /dev/null +++ b/CHANGES/pulp_file/7606.feature @@ -0,0 +1,4 @@ +The file repository now supports the `publish` parameter on the modify endpoint. When +`publish=True` is passed, the repository version will be published after modification even if +`autopublish` is not enabled on the repository. Publication parameters configured on the +repository (e.g. `manifest`) will be used when publishing. diff --git a/pulp_file/app/models.py b/pulp_file/app/models.py index 6c04ced79c1..e785c4c5635 100644 --- a/pulp_file/app/models.py +++ b/pulp_file/app/models.py @@ -102,19 +102,20 @@ class Meta: ("repair_filerepository", "Can repair repository versions"), ] - def on_new_version(self, version): + def on_new_version(self, version, publish=False): """ Called when new repository versions are created. Args: version: The new repository version. + publish: Whether to publish this version. """ super().on_new_version(version) # avoid circular import issues from pulp_file.app import tasks - if self.autopublish: + if self.autopublish or publish: tasks.publish( manifest=self.manifest, repository_version_pk=version.pk, diff --git a/pulp_file/tests/functional/api/test_auto_publish.py b/pulp_file/tests/functional/api/test_auto_publish.py index 96d44401155..6e9f498433e 100644 --- a/pulp_file/tests/functional/api/test_auto_publish.py +++ b/pulp_file/tests/functional/api/test_auto_publish.py @@ -114,4 +114,75 @@ def test_auto_publish_and_distribution( distros = file_bindings.DistributionsFileApi.list( repository=nonexistent_repository_href ).results - assert len(distros) == 0 + + +@pytest.mark.parallel +def test_modify_with_publish( + file_bindings, + file_repository_factory, + file_random_content_unit, + monitor_task, +): + """Test that passing publish=True to modify creates a publication.""" + repo = file_repository_factory(manifest="TEST_MANIFEST") + repo = file_bindings.RepositoriesFileApi.read(repo.pulp_href) + + # Verify no publications exist yet + assert file_bindings.PublicationsFileApi.list(repository=repo.pulp_href).count == 0 + + # Modify the repository with publish=True + monitor_task( + file_bindings.RepositoriesFileApi.modify( + repo.pulp_href, + { + "add_content_units": [file_random_content_unit.pulp_href], + "publish": True, + }, + ).task + ) + repo = file_bindings.RepositoriesFileApi.read(repo.pulp_href) + + # A new version should have been created and a publication should exist + assert repo.latest_version_href.endswith("/versions/1/") + assert file_bindings.PublicationsFileApi.list(repository=repo.pulp_href).count == 1 + assert ( + file_bindings.PublicationsFileApi.list(repository_version=repo.latest_version_href).count + == 1 + ) + + # Verify the publication uses the custom manifest from the repository + publication = file_bindings.PublicationsFileApi.list( + repository_version=repo.latest_version_href + ).results[0] + assert publication.manifest == "TEST_MANIFEST" + + # Verify the publication's repository version contains the content unit + content = file_bindings.ContentFilesApi.list( + repository_version=repo.latest_version_href + ).results + content_hrefs = [c.pulp_href for c in content] + assert file_random_content_unit.pulp_href in content_hrefs + + +@pytest.mark.parallel +def test_modify_without_publish( + file_bindings, + file_repo, + file_random_content_unit, + monitor_task, +): + """Test that modify without publish=True does not create a publication.""" + repo = file_bindings.RepositoriesFileApi.read(file_repo.pulp_href) + + # Modify the repository without publish + monitor_task( + file_bindings.RepositoriesFileApi.modify( + repo.pulp_href, + {"add_content_units": [file_random_content_unit.pulp_href]}, + ).task + ) + repo = file_bindings.RepositoriesFileApi.read(repo.pulp_href) + + # A new version should have been created but no publication + assert repo.latest_version_href.endswith("/versions/1/") + assert file_bindings.PublicationsFileApi.list(repository=repo.pulp_href).count == 0 diff --git a/pulpcore/app/models/repository.py b/pulpcore/app/models/repository.py index a2350072eab..8f84216b55f 100644 --- a/pulpcore/app/models/repository.py +++ b/pulpcore/app/models/repository.py @@ -2,6 +2,7 @@ Repository related Django models. """ +import inspect from contextlib import suppress from gettext import gettext as _ from os import path @@ -1414,7 +1415,18 @@ def __exit__(self, exc_type, exc_value, traceback): self.save() self._compute_counts() self.repository.cleanup_old_versions() - repository.on_new_version(self) + publish = getattr(self, "_publish", False) + sig = inspect.signature(repository.on_new_version) + if publish and "publish" in sig.parameters: + repository.on_new_version(self, publish=True) + else: + if publish: + _logger.warning( + "publish=True was requested but the repository type " + "'%s' does not support it in on_new_version.", + repository.TYPE, + ) + repository.on_new_version(self) except Exception: self.delete() raise diff --git a/pulpcore/app/serializers/repository.py b/pulpcore/app/serializers/repository.py index fbcf19b9283..667ca2e81c4 100644 --- a/pulpcore/app/serializers/repository.py +++ b/pulpcore/app/serializers/repository.py @@ -333,6 +333,14 @@ class RepositoryAddRemoveContentSerializer(ModelSerializer, NestedHyperlinkedMod "for the new repository version" ), ) + publish = serializers.BooleanField( + required=False, + default=False, + help_text=_( + "Whether to publish the repository version created by this modification. " + "The repository's modify task must support the ``publish`` parameter." + ), + ) def validate_add_content_units(self, value): add_content_units = {} @@ -366,4 +374,4 @@ def validate_remove_content_units(self, value): class Meta: model = models.RepositoryVersion - fields = ["add_content_units", "remove_content_units", "base_version"] + fields = ["add_content_units", "remove_content_units", "base_version", "publish"] diff --git a/pulpcore/app/tasks/repository.py b/pulpcore/app/tasks/repository.py index b20bf3c7dbd..6877a02590b 100644 --- a/pulpcore/app/tasks/repository.py +++ b/pulpcore/app/tasks/repository.py @@ -205,7 +205,13 @@ def repair_all_artifacts(verify_checksums): loop.run_until_complete(_repair_artifacts_for_content(verify_checksums=verify_checksums)) -def add_and_remove(repository_pk, add_content_units, remove_content_units, base_version_pk=None): +def add_and_remove( + repository_pk, + add_content_units, + remove_content_units, + base_version_pk=None, + publish=False, +): """ Create a new repository version by adding and then removing content units. @@ -218,6 +224,7 @@ def add_and_remove(repository_pk, add_content_units, remove_content_units, base_ should be removed from the previous Repository Version for this Repository. base_version_pk (uuid): the primary key for a RepositoryVersion whose content will be used as the initial set of content for our new RepositoryVersion + publish (bool): whether to publish the new repository version after creation """ repository = models.Repository.objects.get(pk=repository_pk).cast() @@ -234,6 +241,8 @@ def add_and_remove(repository_pk, add_content_units, remove_content_units, base_ remove_content_units = [] with repository.new_version(base_version=base_version) as new_version: + if publish: + new_version._publish = True new_version.remove_content(models.Content.objects.filter(pk__in=remove_content_units)) new_version.add_content(models.Content.objects.filter(pk__in=add_content_units)) diff --git a/pulpcore/plugin/actions.py b/pulpcore/plugin/actions.py index 7dc739d5e97..4a608752271 100644 --- a/pulpcore/plugin/actions.py +++ b/pulpcore/plugin/actions.py @@ -1,5 +1,8 @@ +import inspect + from drf_spectacular.utils import extend_schema from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError from pulpcore.app import tasks from pulpcore.app.models import RepositoryVersion @@ -35,14 +38,26 @@ def modify(self, request, pk): else: base_version_pk = None + publish = serializer.validated_data.get("publish", False) + + task_kwargs = { + "repository_pk": pk, + "base_version_pk": base_version_pk, + "add_content_units": serializer.validated_data.get("add_content_units", []), + "remove_content_units": serializer.validated_data.get("remove_content_units", []), + } + + if publish: + sig = inspect.signature(self.modify_task) + if "publish" not in sig.parameters: + raise ValidationError( + {"publish": "This repository type does not support the publish parameter."} + ) + task_kwargs["publish"] = True + task = dispatch( self.modify_task, exclusive_resources=[repository], - kwargs={ - "repository_pk": pk, - "base_version_pk": base_version_pk, - "add_content_units": serializer.validated_data.get("add_content_units", []), - "remove_content_units": serializer.validated_data.get("remove_content_units", []), - }, + kwargs=task_kwargs, ) return OperationPostponedResponse(task, request)