Skip to content

Commit

Permalink
Add labels api
Browse files Browse the repository at this point in the history
fixes pulp#3332
  • Loading branch information
mdellweg committed Aug 31, 2023
1 parent 2d02e9f commit 0269c7e
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGES/3332.feature
@@ -0,0 +1 @@
Added set_label and delets_label endpoints to allow manipulating individual labels synchronously.
4 changes: 3 additions & 1 deletion pulpcore/app/serializers/__init__.py
Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions pulpcore/app/serializers/base.py
Expand Up @@ -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)
1 change: 1 addition & 0 deletions pulpcore/app/viewsets/__init__.py
Expand Up @@ -2,6 +2,7 @@
AsyncCreateMixin,
AsyncRemoveMixin,
AsyncUpdateMixin,
LabelsMixin,
NamedModelViewSet,
RolesMixin,
NAME_FILTER_OPTIONS,
Expand Down
51 changes: 50 additions & 1 deletion pulpcore/app/viewsets/base.py
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
2 changes: 2 additions & 0 deletions pulpcore/app/viewsets/publication.py
Expand Up @@ -27,6 +27,7 @@
AsyncCreateMixin,
AsyncRemoveMixin,
AsyncUpdateMixin,
LabelsMixin,
NamedModelViewSet,
RolesMixin,
)
Expand Down Expand Up @@ -434,6 +435,7 @@ class DistributionViewSet(
AsyncCreateMixin,
AsyncRemoveMixin,
AsyncUpdateMixin,
LabelsMixin,
):
"""
Provides read and list methods and also provides asynchronous CUD methods to dispatch tasks
Expand Down
4 changes: 3 additions & 1 deletion pulpcore/app/viewsets/repository.py
Expand Up @@ -30,6 +30,7 @@
from pulpcore.app.viewsets import (
AsyncRemoveMixin,
AsyncUpdateMixin,
LabelsMixin,
NamedModelViewSet,
)
from pulpcore.app.viewsets.base import (
Expand Down Expand Up @@ -168,7 +169,7 @@ class ImmutableRepositoryViewSet(
"""


class RepositoryViewSet(ImmutableRepositoryViewSet, AsyncUpdateMixin):
class RepositoryViewSet(ImmutableRepositoryViewSet, AsyncUpdateMixin, LabelsMixin):
"""
A ViewSet for an ordinary repository.
"""
Expand Down Expand Up @@ -369,6 +370,7 @@ class RemoteViewSet(
mixins.ListModelMixin,
AsyncUpdateMixin,
AsyncRemoveMixin,
LabelsMixin,
):
endpoint_name = "remotes"
serializer_class = RemoteSerializer
Expand Down
1 change: 1 addition & 0 deletions pulpcore/plugin/viewsets/__init__.py
Expand Up @@ -18,6 +18,7 @@
ImmutableRepositoryViewSet,
ImporterViewSet,
ImportViewSet,
LabelsMixin,
NamedModelViewSet,
NAME_FILTER_OPTIONS,
NULLABLE_NUMERIC_FILTER_OPTIONS,
Expand Down
63 changes: 63 additions & 0 deletions pulpcore/tests/functional/api/using_plugin/test_labels.py
@@ -0,0 +1,63 @@
import pytest


@pytest.fixture
def label_access_policy(access_policies_api_client):
orig_access_policy = access_policies_api_client.list(
viewset_name="repositories/file/file"
).results[0]
new_statements = orig_access_policy.statements.copy()
new_statements.append(
{
"action": ["set_label", "delete_label"],
"effect": "allow",
"condition": [
"has_model_or_domain_or_obj_perms:file.modify_filerepository",
"has_model_or_domain_or_obj_perms:file.view_filerepository",
],
"principal": "authenticated",
}
)
access_policies_api_client.partial_update(
orig_access_policy.pulp_href, {"statements": new_statements}
)
yield
if orig_access_policy.customized:
access_policies_api_client.partial_update(
orig_access_policy.pulp_href, {"statements": orig_access_policy.statements}
)
else:
access_policies_api_client.reset(orig_access_policy.pulp_href)


@pytest.mark.parallel
def test_set_label(label_access_policy, file_repository_api_client, file_repository_factory):
repository = file_repository_factory()
assert repository.pulp_labels == {}

file_repository_api_client.set_label(repository.pulp_href, {"label": "a", "value": None})
file_repository_api_client.set_label(repository.pulp_href, {"label": "b", "value": ""})
file_repository_api_client.set_label(repository.pulp_href, {"label": "c", "value": "val1"})
file_repository_api_client.set_label(repository.pulp_href, {"label": "d", "value": "val2"})
file_repository_api_client.set_label(repository.pulp_href, {"label": "e", "value": "val3"})
file_repository_api_client.set_label(repository.pulp_href, {"label": "c", "value": "val4"})
file_repository_api_client.set_label(repository.pulp_href, {"label": "d", "value": None})

repository = file_repository_api_client.read(repository.pulp_href)
assert repository.pulp_labels == {
"a": None,
"b": "",
"c": "val4",
"d": None,
"e": "val3",
}

file_repository_api_client.delete_label(repository.pulp_href, {"label": "e"})

repository = file_repository_api_client.read(repository.pulp_href)
assert repository.pulp_labels == {
"a": None,
"b": "",
"c": "val4",
"d": None,
}

0 comments on commit 0269c7e

Please sign in to comment.