Repository: pulp/pulpcore

Issue:
Update CI files from 3.20 branch
[noissue]


Discussion:

Resolving patch:
diff --git a/pulpcore/app/tasks/__init__.py b/pulpcore/app/tasks/__init__.py
--- a/pulpcore/app/tasks/__init__.py
+++ b/pulpcore/app/tasks/__init__.py
@@ -1,3 +1,3 @@
-from pulpcore.app.tasks import base, repository  # noqa
+from pulpcore.app.tasks import base, distribution, repository  # noqa
 
 from .orphan import orphan_cleanup  # noqa
diff --git a/pulpcore/app/tasks/distribution.py b/pulpcore/app/tasks/distribution.py
new file mode 100644
--- /dev/null
+++ b/pulpcore/app/tasks/distribution.py
@@ -0,0 +1,56 @@
+from django.core.exceptions import ObjectDoesNotExist
+
+from pulpcore.app.models import Distribution, CreatedResource
+from pulpcore.app.serializers import DistributionSerializer
+
+
+def create(*args, **kwargs):
+    """
+    Creates a :class:`~pulpcore.app.models.Distribution`
+
+    Raises:
+        ValidationError: If the DistributionSerializer is not valid
+    """
+    data = kwargs.pop('data', None)
+    serializer = DistributionSerializer(data=data)
+    serializer.is_valid(raise_exception=True)
+    serializer.save()
+    resource = CreatedResource(content_object=serializer.instance)
+    resource.save()
+
+
+def update(instance_id, *args, **kwargs):
+    """
+    Updates a :class:`~pulpcore.app.models.Distribution`
+
+    Args:
+        instance_id (int): The id of the distribution to be updated
+
+    Raises:
+        ValidationError: If the DistributionSerializer is not valid
+    """
+    data = kwargs.pop('data', None)
+    partial = kwargs.pop('partial', False)
+    instance = Distribution.objects.get(pk=instance_id)
+    serializer = DistributionSerializer(instance, data=data, partial=partial)
+    serializer.is_valid(raise_exception=True)
+    serializer.save()
+
+
+def delete(instance_id, *args, **kwargs):
+    """
+    Delete a :class:`~pulpcore.app.models.Distribution`
+
+    Args:
+        instance_id (int): The id of the Distribution to be deleted
+
+    Raises:
+        ObjectDoesNotExist: If the Distribution was already deleted
+    """
+    try:
+        instance = Distribution.objects.get(pk=instance_id)
+    except ObjectDoesNotExist:
+        # The object was already deleted, and we don't want an error thrown trying to delete again.
+        return
+    else:
+        instance.delete()
diff --git a/pulpcore/app/viewsets/publication.py b/pulpcore/app/viewsets/publication.py
--- a/pulpcore/app/viewsets/publication.py
+++ b/pulpcore/app/viewsets/publication.py
@@ -1,7 +1,12 @@
 from django_filters.rest_framework import filters, DjangoFilterBackend
+from drf_yasg.utils import swagger_auto_schema
 from rest_framework import mixins
 from rest_framework.filters import OrderingFilter
 
+from pulpcore.app import tasks
+from pulpcore.app.response import OperationPostponedResponse
+from pulpcore.tasking.tasks import enqueue_with_reservation
+
 from pulpcore.app.models import (
     ContentGuard,
     Distribution,
@@ -70,12 +75,72 @@ class Meta:
 
 
 class DistributionViewSet(NamedModelViewSet,
-                          mixins.CreateModelMixin,
                           mixins.UpdateModelMixin,
                           mixins.RetrieveModelMixin,
                           mixins.ListModelMixin,
                           mixins.DestroyModelMixin):
+    """
+    Provides read and list methods and also provides asynchronous CUD methods to dispatch tasks
+    with reservation that lock all Distributions preventing race conditions during base_path
+    checking.
+    """
     endpoint_name = 'distributions'
     queryset = Distribution.objects.all()
     serializer_class = DistributionSerializer
     filterset_class = DistributionFilter
+
+    @swagger_auto_schema(operation_description="Trigger an asynchronous create task",
+                         responses={202: DistributionSerializer})
+    def create(self, request, *args, **kwargs):
+        """
+        Dispatches a task with reservation for creating a distribution.
+        """
+        serializer = self.get_serializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
+        async_result = enqueue_with_reservation(
+            tasks.distribution.create,
+            "/api/v3/distributions/",
+            kwargs={'data': request.data}
+        )
+        return OperationPostponedResponse(async_result, request)
+
+    @swagger_auto_schema(operation_description="Trigger an asynchronous update task",
+                         responses={202: DistributionSerializer})
+    def update(self, request, pk, *args, **kwargs):
+        """
+        Dispatches a task with reservation for updating a distribution.
+        """
+        partial = kwargs.pop('partial', False)
+        instance = self.get_object()
+        serializer = self.get_serializer(instance, data=request.data, partial=partial)
+        serializer.is_valid(raise_exception=True)
+        async_result = enqueue_with_reservation(
+            tasks.distribution.update,
+            "/api/v3/distributions/",
+            args=(pk,),
+            kwargs={'data': request.data, 'partial': partial}
+        )
+        return OperationPostponedResponse(async_result, request)
+
+    @swagger_auto_schema(operation_description="Trigger an asynchronous partial update task",
+                         responses={202: DistributionSerializer})
+    def partial_update(self, request, *args, **kwargs):
+        """
+        Dispatches a task with reservation for partially updating a distribution.
+        """
+        kwargs['partial'] = True
+        return self.update(request, *args, **kwargs)
+
+    @swagger_auto_schema(operation_description="Trigger an asynchronous delete task",
+                         responses={202: DistributionSerializer})
+    def delete(self, request, pk, *args, **kwargs):
+        """
+        Dispatches a task with reservation for deleting a distribution.
+        """
+        self.get_object()
+        async_result = enqueue_with_reservation(
+            tasks.distribution.delete,
+            "/api/v3/distributions/",
+            args=(pk,)
+        )
+        return OperationPostponedResponse(async_result, request)


Test patch:
diff --git a/pulpcore/tests/functional/api/test_crud_distributions.py b/pulpcore/tests/functional/api/test_crud_distributions.py
--- a/pulpcore/tests/functional/api/test_crud_distributions.py
+++ b/pulpcore/tests/functional/api/test_crud_distributions.py
@@ -26,9 +26,12 @@ def setUpClass(cls):
     def test_01_create_distribution(self):
         """Create a distribution."""
         body = gen_distribution()
-        type(self).distribution = self.client.post(
+        response_dict = self.client.post(
             DISTRIBUTION_PATH, body
         )
+        dist_task = self.client.get(response_dict['task'])
+        distribution_href = dist_task['created_resources'][0]
+        type(self).distribution = self.client.get(distribution_href)
         for key, val in body.items():
             with self.subTest(key=key):
                 self.assertEqual(self.distribution[key], val)
@@ -158,7 +161,10 @@ def setUpClass(cls):
         cls.client = api.Client(cls.cfg, api.json_handler)
         body = gen_distribution()
         body['base_path'] = body['base_path'].replace('-', '/')
-        cls.distribution = cls.client.post(DISTRIBUTION_PATH, body)
+        response_dict = cls.client.post(DISTRIBUTION_PATH, body)
+        dist_task = cls.client.get(response_dict['task'])
+        distribution_href = dist_task['created_resources'][0]
+        cls.distribution = cls.client.get(distribution_href)
 
     @classmethod
     def tearDownClass(cls):
diff --git a/pulpcore/tests/functional/api/using_plugin/test_auto_distribution.py b/pulpcore/tests/functional/api/using_plugin/test_auto_distribution.py
--- a/pulpcore/tests/functional/api/using_plugin/test_auto_distribution.py
+++ b/pulpcore/tests/functional/api/using_plugin/test_auto_distribution.py
@@ -94,7 +94,10 @@ def test_repo_auto_distribution(self):
         body['repository'] = repo['_href']
         body['publisher'] = publisher['_href']
 
-        distribution = self.client.post(DISTRIBUTION_PATH, body)
+        response_dict = self.client.post(DISTRIBUTION_PATH, body)
+        dist_task = self.client.get(response_dict['task'])
+        distribution_href = dist_task['created_resources'][0]
+        distribution = self.client.get(distribution_href)
         self.addCleanup(self.client.delete, distribution['_href'])
 
         last_version_href = get_versions(repo)[-1]['_href']
@@ -172,16 +175,20 @@ def test_all(self):
         body = gen_distribution()
         body['publisher'] = publisher['_href']
         body['repository'] = repo['_href']
-        distribution = self.client.post(DISTRIBUTION_PATH, body)
+        response_dict = self.client.post(DISTRIBUTION_PATH, body)
+        dist_task = self.client.get(response_dict['task'])
+        distribution_href = dist_task['created_resources'][0]
+        distribution = self.client.get(distribution_href)
         self.addCleanup(self.client.delete, distribution['_href'])
 
         # Update the distribution.
         self.try_update_distribution(distribution, publisher=None)
         self.try_update_distribution(distribution, repository=None)
-        distribution = self.client.patch(distribution['_href'], {
+        self.client.patch(distribution['_href'], {
             'publisher': None,
             'repository': None,
         })
+        distribution = self.client.get(distribution['_href'])
         self.assertIsNone(distribution['publisher'], distribution)
         self.assertIsNone(distribution['repository'], distribution)
 
diff --git a/pulpcore/tests/functional/api/using_plugin/test_content_app.py b/pulpcore/tests/functional/api/using_plugin/test_content_app.py
--- a/pulpcore/tests/functional/api/using_plugin/test_content_app.py
+++ b/pulpcore/tests/functional/api/using_plugin/test_content_app.py
@@ -77,7 +77,10 @@ def test_content_app_returns_404(self):
         body['repository'] = repo['_href']
         body['publisher'] = publisher['_href']
 
-        distribution = self.client.post(DISTRIBUTION_PATH, body)
+        response_dict = self.client.post(DISTRIBUTION_PATH, body)
+        dist_task = self.client.get(response_dict['task'])
+        distribution_href = dist_task['created_resources'][0]
+        distribution = self.client.get(distribution_href)
         self.addCleanup(self.client.delete, distribution['_href'])
 
         last_version_href = get_versions(repo)[-1]['_href']
diff --git a/pulpcore/tests/functional/api/using_plugin/test_content_promotion.py b/pulpcore/tests/functional/api/using_plugin/test_content_promotion.py
--- a/pulpcore/tests/functional/api/using_plugin/test_content_promotion.py
+++ b/pulpcore/tests/functional/api/using_plugin/test_content_promotion.py
@@ -74,7 +74,10 @@ def test_all(self):
         for _ in range(2):
             body = gen_distribution()
             body['publication'] = publication['_href']
-            distribution = client.post(DISTRIBUTION_PATH, body)
+            response_dict = client.post(DISTRIBUTION_PATH, body)
+            dist_task = client.get(response_dict['task'])
+            distribution_href = dist_task['created_resources'][0]
+            distribution = client.get(distribution_href)
             distributions.append(distribution)
             self.addCleanup(client.delete, distribution['_href'])
 
diff --git a/pulpcore/tests/functional/api/using_plugin/test_crd_publications.py b/pulpcore/tests/functional/api/using_plugin/test_crd_publications.py
--- a/pulpcore/tests/functional/api/using_plugin/test_crd_publications.py
+++ b/pulpcore/tests/functional/api/using_plugin/test_crd_publications.py
@@ -140,7 +140,10 @@ def test_05_read_publications(self):
         """Read a publication by its distribution."""
         body = gen_distribution()
         body['publication'] = self.publication['_href']
-        distribution = self.client.post(DISTRIBUTION_PATH, body)
+        response_dict = self.client.post(DISTRIBUTION_PATH, body)
+        dist_task = self.client.get(response_dict['task'])
+        distribution_href = dist_task['created_resources'][0]
+        distribution = self.client.get(distribution_href)
         self.addCleanup(self.client.delete, distribution['_href'])
 
         self.publication.update(self.client.get(self.publication['_href']))