diff --git a/CHANGES/3332.feature b/CHANGES/3332.feature new file mode 100644 index 0000000000..be943de910 --- /dev/null +++ b/CHANGES/3332.feature @@ -0,0 +1 @@ +Added set_label and delets_label endpoints to allow manipulating individual labels synchronously. diff --git a/pulpcore/app/serializers/__init__.py b/pulpcore/app/serializers/__init__.py index 81884f2df6..ae74827ff9 100644 --- a/pulpcore/app/serializers/__init__.py +++ b/pulpcore/app/serializers/__init__.py @@ -4,20 +4,22 @@ from .base import ( AsyncOperationResponseSerializer, + DeleteLabelSerializer, DetailIdentityField, DetailRelatedField, DomainUniqueValidator, GetOrCreateSerializerMixin, + HiddenFieldsMixin, IdentityField, ModelSerializer, NestedIdentityField, NestedRelatedField, RelatedField, RelatedResourceField, + SetLabelSerializer, TaskGroupOperationResponseSerializer, ValidateFieldsMixin, validate_unknown_fields, - HiddenFieldsMixin, ) from .fields import ( BaseURLField, diff --git a/pulpcore/app/serializers/base.py b/pulpcore/app/serializers/base.py index f842328f12..8e90c5c7f2 100644 --- a/pulpcore/app/serializers/base.py +++ b/pulpcore/app/serializers/base.py @@ -504,3 +504,27 @@ class TaskGroupOperationResponseSerializer(serializers.Serializer): view_name="task-groups-detail", allow_null=False, ) + + +class DeleteLabelSerializer(serializers.Serializer): + """ + Serializer for synchronously setting a label. + """ + + label = serializers.SlugField(required=True) + + def validate_label(self, value): + if value not in self.context["content_object"].pulp_labels: + raise serializers.ValidationError( + _("Label '{label}' is not set on the object.").format(label=value) + ) + return value + + +class SetLabelSerializer(serializers.Serializer): + """ + Serializer for synchronously setting a label. + """ + + label = serializers.SlugField(required=True) + value = serializers.CharField(required=True, allow_null=True, allow_blank=True) diff --git a/pulpcore/app/viewsets/__init__.py b/pulpcore/app/viewsets/__init__.py index 6d17c93fc8..6f017e6a03 100644 --- a/pulpcore/app/viewsets/__init__.py +++ b/pulpcore/app/viewsets/__init__.py @@ -2,6 +2,7 @@ AsyncCreateMixin, AsyncRemoveMixin, AsyncUpdateMixin, + LabelsMixin, NamedModelViewSet, RolesMixin, NAME_FILTER_OPTIONS, diff --git a/pulpcore/app/viewsets/base.py b/pulpcore/app/viewsets/base.py index 017cdcd419..0af78b33fb 100644 --- a/pulpcore/app/viewsets/base.py +++ b/pulpcore/app/viewsets/base.py @@ -4,6 +4,7 @@ from django.conf import settings from django.db import transaction +from django.db.models.expressions import RawSQL from django.core.exceptions import FieldError, ValidationError from django.urls import Resolver404, resolve from django.contrib.contenttypes.models import ContentType @@ -20,7 +21,12 @@ from pulpcore.app.models.role import GroupRole, UserRole from pulpcore.app.response import OperationPostponedResponse from pulpcore.app.role_util import get_objects_for_user -from pulpcore.app.serializers import AsyncOperationResponseSerializer, NestedRoleSerializer +from pulpcore.app.serializers import ( + AsyncOperationResponseSerializer, + NestedRoleSerializer, + DeleteLabelSerializer, + SetLabelSerializer, +) from pulpcore.app.util import get_viewset_for_model from pulpcore.tasking.tasks import dispatch @@ -626,3 +632,46 @@ def my_permissions(self, request, pk=None): ".".join((app_label, codename)) for codename in request.user.get_all_permissions(obj) ] return Response({"permissions": permissions}) + + +class LabelsMixin: + @extend_schema( + summary="Set a label", + description="Set a single pulp_label on the object to a specific value or null.", + responses={ + 200: SetLabelSerializer, + }, + ) + @action(detail=True, methods=["put"], serializer_class=SetLabelSerializer) + def set_label(self, request, pk=None): + obj = self.get_object() + serializer = SetLabelSerializer( + data=request.data, context={"request": request, "content_object": obj} + ) + serializer.is_valid(raise_exception=True) + obj._meta.model.objects.filter(pk=obj.pk).update( + pulp_labels=RawSQL( + "pulp_labels || hstore(%s, %s)", + [serializer.validated_data["label"], serializer.validated_data["value"]], + ) + ) + return Response(serializer.validated_data) + + @extend_schema( + summary="Delete a label", + description="Delete a single pulp_label on the object.", + responses={ + 204: None, + }, + ) + @action(detail=True, methods=["post"], serializer_class=DeleteLabelSerializer) + def delete_label(self, request, pk=None): + obj = self.get_object() + serializer = DeleteLabelSerializer( + data=request.data, context={"request": request, "content_object": obj} + ) + serializer.is_valid(raise_exception=True) + obj._meta.model.objects.filter(pk=obj.pk).update( + pulp_labels=RawSQL("pulp_labels - %s::text", [serializer.validated_data["label"]]) + ) + return Response(status=204) diff --git a/pulpcore/app/viewsets/publication.py b/pulpcore/app/viewsets/publication.py index 2431e5396c..087f73df4d 100644 --- a/pulpcore/app/viewsets/publication.py +++ b/pulpcore/app/viewsets/publication.py @@ -27,6 +27,7 @@ AsyncCreateMixin, AsyncRemoveMixin, AsyncUpdateMixin, + LabelsMixin, NamedModelViewSet, RolesMixin, ) @@ -434,6 +435,7 @@ class DistributionViewSet( AsyncCreateMixin, AsyncRemoveMixin, AsyncUpdateMixin, + LabelsMixin, ): """ Provides read and list methods and also provides asynchronous CUD methods to dispatch tasks diff --git a/pulpcore/app/viewsets/repository.py b/pulpcore/app/viewsets/repository.py index 75af664274..c28a5492b7 100644 --- a/pulpcore/app/viewsets/repository.py +++ b/pulpcore/app/viewsets/repository.py @@ -30,6 +30,7 @@ from pulpcore.app.viewsets import ( AsyncRemoveMixin, AsyncUpdateMixin, + LabelsMixin, NamedModelViewSet, ) from pulpcore.app.viewsets.base import ( @@ -168,7 +169,7 @@ class ImmutableRepositoryViewSet( """ -class RepositoryViewSet(ImmutableRepositoryViewSet, AsyncUpdateMixin): +class RepositoryViewSet(ImmutableRepositoryViewSet, AsyncUpdateMixin, LabelsMixin): """ A ViewSet for an ordinary repository. """ @@ -369,6 +370,7 @@ class RemoteViewSet( mixins.ListModelMixin, AsyncUpdateMixin, AsyncRemoveMixin, + LabelsMixin, ): endpoint_name = "remotes" serializer_class = RemoteSerializer diff --git a/pulpcore/plugin/viewsets/__init__.py b/pulpcore/plugin/viewsets/__init__.py index 6f00b938d9..0a4c9af9e9 100644 --- a/pulpcore/plugin/viewsets/__init__.py +++ b/pulpcore/plugin/viewsets/__init__.py @@ -18,6 +18,7 @@ ImmutableRepositoryViewSet, ImporterViewSet, ImportViewSet, + LabelsMixin, NamedModelViewSet, NAME_FILTER_OPTIONS, NULLABLE_NUMERIC_FILTER_OPTIONS,