diff --git a/CHANGES/1010.feature b/CHANGES/1010.feature new file mode 100644 index 000000000..93a1fc0fa --- /dev/null +++ b/CHANGES/1010.feature @@ -0,0 +1 @@ +Added retrieve functionality for ReleaseArchitecture and ReleaseComponent content. diff --git a/CHANGES/1010.removal b/CHANGES/1010.removal new file mode 100644 index 000000000..a3d9c70fd --- /dev/null +++ b/CHANGES/1010.removal @@ -0,0 +1,2 @@ +The API endpoints for ReleaseArchitecture and ReleaseComponent creation will no longer return a 400 ``non_field_errors`` if the content to be created already exists. +Instead a task is triggered that will list the existing content in its ``created_resources`` field. diff --git a/pulp_deb/app/serializers/content_serializers.py b/pulp_deb/app/serializers/content_serializers.py index f119138da..2c4486e14 100644 --- a/pulp_deb/app/serializers/content_serializers.py +++ b/pulp_deb/app/serializers/content_serializers.py @@ -750,6 +750,21 @@ class ReleaseArchitectureSerializer(NoArtifactContentSerializer): architecture = CharField(help_text="Name of the architecture.") distribution = CharField(help_text="Name of the distribution.") + def get_unique_together_validators(self): + """ + We do not want UniqueTogetherValidator since we have retrieve logic! + """ + return [] + + def retrieve(self, validated_data): + """ + If the ReleaseArchitecture already exists, retrieve it! + """ + return ReleaseArchitecture.objects.filter( + architecture=validated_data["architecture"], + distribution=validated_data["distribution"], + ).first() + class Meta(NoArtifactContentSerializer.Meta): model = ReleaseArchitecture fields = NoArtifactContentSerializer.Meta.fields + ( @@ -763,6 +778,21 @@ class ReleaseComponentSerializer(NoArtifactContentSerializer): A Serializer for ReleaseComponent. """ + def get_unique_together_validators(self): + """ + We do not want UniqueTogetherValidator since we have retrieve logic! + """ + return [] + + def retrieve(self, validated_data): + """ + If the ReleaseComponent already exists, retrieve it! + """ + return ReleaseComponent.objects.filter( + distribution=validated_data["distribution"], + component=validated_data["component"], + ).first() + component = CharField(help_text="Name of the component.") distribution = CharField(help_text="Name of the distribution.") diff --git a/pulp_deb/app/viewsets/content.py b/pulp_deb/app/viewsets/content.py index 7534e5c46..e7337deea 100644 --- a/pulp_deb/app/viewsets/content.py +++ b/pulp_deb/app/viewsets/content.py @@ -1,19 +1,54 @@ from gettext import gettext as _ # noqa from django_filters import Filter +from drf_spectacular.utils import extend_schema +from pulpcore.plugin.tasking import general_create from pulpcore.plugin.models import Repository, RepositoryVersion +from pulpcore.plugin.serializers import AsyncOperationResponseSerializer from pulpcore.plugin.serializers.content import ValidationError +from pulpcore.plugin.tasking import dispatch from pulpcore.plugin.viewsets import ( NAME_FILTER_OPTIONS, ContentFilter, ContentViewSet, NamedModelViewSet, + OperationPostponedResponse, SingleArtifactContentUploadViewSet, ) +from pulpcore.plugin.viewsets.content import DefaultDeferredContextMixin from pulp_deb.app import models, serializers +class NoArtifactContentViewSet(DefaultDeferredContextMixin, ContentViewSet): + """A ViewSet for content creation that does not require a file to be uploaded.""" + + @extend_schema( + description="Trigger an asynchronous task to create content," + "optionally create new repository version.", + responses={202: AsyncOperationResponseSerializer}, + ) + def create(self, request): + """Create a content unit.""" + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + exclusive_resources = [ + item for item in (serializer.validated_data.get(key) for key in ("repository",)) if item + ] + + task = dispatch( + general_create, + exclusive_resources=exclusive_resources, + args=(self.queryset.model._meta.app_label, serializer.__class__.__name__), + kwargs={ + "data": {k: v for k, v in request.data.items()}, + "context": self.get_deferred_context(request), + }, + ) + return OperationPostponedResponse(task, request) + + class GenericContentFilter(ContentFilter): """ FilterSet for GenericContent. @@ -440,7 +475,7 @@ class Meta: fields = ["architecture", "distribution"] -class ReleaseArchitectureViewSet(ContentViewSet): +class ReleaseArchitectureViewSet(NoArtifactContentViewSet): # The doc string is a top level element of the user facing REST API documentation: """ A ReleaseArchitecture represents a single dpkg architecture string. @@ -479,7 +514,7 @@ class Meta: fields = ["component", "distribution"] -class ReleaseComponentViewSet(ContentViewSet): +class ReleaseComponentViewSet(NoArtifactContentViewSet): # The doc string is a top level element of the user facing REST API documentation: """ A ReleaseComponent represents a single APT repository component. diff --git a/pulp_deb/tests/functional/api/test_crud_packages.py b/pulp_deb/tests/functional/api/test_crud_packages.py index e526457a7..b6d7177a6 100644 --- a/pulp_deb/tests/functional/api/test_crud_packages.py +++ b/pulp_deb/tests/functional/api/test_crud_packages.py @@ -92,3 +92,71 @@ def test_structured_package_upload( results = package_list.results[0] assert results.relative_path == attrs["relative_path"] + + +def test_release_component_upload( + apt_release_component_api, + deb_get_repository_by_href, + deb_release_component_factory, + deb_repository_factory, +): + """Test creating a ReleaseComponent directly in a repository.""" + repository = deb_repository_factory() + assert repository.latest_version_href.endswith("/0/") + + attrs = { + "repository": repository.pulp_href, + "distribution": str(uuid4()), + "component": str(uuid4()), + } + + component = deb_release_component_factory(**attrs) + repository = deb_get_repository_by_href(repository.pulp_href) + assert repository.latest_version_href.endswith("/1/") + + repo_version_components = apt_release_component_api.list( + repository_version=repository.latest_version_href + ) + + assert len(repo_version_components.results) == 1 + assert repo_version_components.results[0].pulp_href == component.pulp_href + + component2 = deb_release_component_factory(**attrs) + repository = deb_get_repository_by_href(repository.pulp_href) + + assert repository.latest_version_href.endswith("/1/") + assert component.pulp_href == component2.pulp_href + + +def test_release_architecture_upload( + apt_release_architecture_api, + deb_get_repository_by_href, + deb_release_architecture_factory, + deb_repository_factory, +): + """Test creating a ReleaseArchitecture directly in a repository.""" + repository = deb_repository_factory() + assert repository.latest_version_href.endswith("/0/") + + attrs = { + "repository": repository.pulp_href, + "distribution": str(uuid4()), + "architecture": str(uuid4()), + } + + architecture = deb_release_architecture_factory(**attrs) + repository = deb_get_repository_by_href(repository.pulp_href) + assert repository.latest_version_href.endswith("/1/") + + repo_version_architectures = apt_release_architecture_api.list( + repository_version=repository.latest_version_href + ) + + assert len(repo_version_architectures.results) == 1 + assert repo_version_architectures.results[0].pulp_href == architecture.pulp_href + + architecture2 = deb_release_architecture_factory(**attrs) + repository = deb_get_repository_by_href(repository.pulp_href) + + assert repository.latest_version_href.endswith("/1/") + assert architecture.pulp_href == architecture2.pulp_href diff --git a/pulp_deb/tests/functional/conftest.py b/pulp_deb/tests/functional/conftest.py index d3e284322..650cd99d5 100644 --- a/pulp_deb/tests/functional/conftest.py +++ b/pulp_deb/tests/functional/conftest.py @@ -13,11 +13,14 @@ ContentPackageIndicesApi, ContentPackageReleaseComponentsApi, ContentReleasesApi, + ContentReleaseArchitecturesApi, ContentReleaseComponentsApi, ContentReleaseFilesApi, Copy, DebAptPublication, DebCopyApi, + DebReleaseArchitecture, + DebReleaseComponent, DebVerbatimPublication, PublicationsVerbatimApi, ) @@ -64,9 +67,15 @@ def apt_release_api(apt_client): return ContentReleasesApi(apt_client) +@pytest.fixture(scope="session") +def apt_release_architecture_api(apt_client): + """Fixture for APT release architecture API.""" + return ContentReleaseArchitecturesApi(apt_client) + + @pytest.fixture(scope="session") def apt_release_component_api(apt_client): - """Fixture for APT release API.""" + """Fixture for APT release component API.""" return ContentReleaseComponentsApi(apt_client) @@ -104,6 +113,40 @@ def _deb_package_factory(**kwargs): return _deb_package_factory +@pytest.fixture(scope="class") +def deb_release_component_factory(apt_release_component_api, gen_object_with_cleanup): + """Fixture that generates deb package with cleanup.""" + + def _deb_release_component_factory(component, distribution, **kwargs): + """Create an APT ReleaseComponent. + + :returns: The created ReleaseComponent. + """ + release_component_object = DebReleaseComponent( + component=component, distribution=distribution, **kwargs + ) + return gen_object_with_cleanup(apt_release_component_api, release_component_object) + + return _deb_release_component_factory + + +@pytest.fixture(scope="class") +def deb_release_architecture_factory(apt_release_architecture_api, gen_object_with_cleanup): + """Fixture that generates deb package with cleanup.""" + + def _deb_release_architecture_factory(architecture, distribution, **kwargs): + """Create an APT ReleaseArchitecture. + + :returns: The created ReleaseArchitecture. + """ + release_architecture_object = DebReleaseArchitecture( + architecture=architecture, distribution=distribution, **kwargs + ) + return gen_object_with_cleanup(apt_release_architecture_api, release_architecture_object) + + return _deb_release_architecture_factory + + @pytest.fixture def deb_publication_by_version_factory(apt_publication_api, gen_object_with_cleanup): """Fixture that generates a deb publication with cleanup from a given repository version."""