Skip to content

Commit

Permalink
Add labels api
Browse files Browse the repository at this point in the history
fixes #3332
  • Loading branch information
mdellweg committed Sep 1, 2023
1 parent 96ee562 commit 6c0389f
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGES/3332.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added ``set_label`` and ``unset_label`` endpoints to allow manipulating individual labels
synchronously.
3 changes: 3 additions & 0 deletions CHANGES/plugin_api/3332.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added a ``LabelsMixin`` for views to allow syncronous manipulation of labels on existing objects.
Repository, remote and distribution views inherit this from pulpcore, but default access policies
need to be adjusted.
4 changes: 2 additions & 2 deletions docs/plugin_dev/plugin-writer/concepts/rbac/access_policy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Below is an example policy used by ``FileRemote``, with an explanation of its ef
"condition": "has_model_or_domain_or_obj_perms:file.view_fileremote",
},
{
"action": ["update", "partial_update"],
"action": ["update", "partial_update", "set_label", "unset_label"],
"principal": "authenticated",
"effect": "allow",
"condition": "has_model_or_domain_or_obj_perms:file.change_fileremote",
Expand Down Expand Up @@ -167,7 +167,7 @@ Here's an example of code to define a default policy:
"condition": "has_model_or_domain_or_obj_perms:file.view_fileremote",
},
{
"action": ["update", "partial_update"],
"action": ["update", "partial_update", "set_label", "unset_label"],
"principal": "authenticated",
"effect": "allow",
"condition": "has_model_or_domain_or_obj_perms:file.change_fileremote",
Expand Down
4 changes: 3 additions & 1 deletion pulpcore/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@
DetailRelatedField,
DomainUniqueValidator,
GetOrCreateSerializerMixin,
HiddenFieldsMixin,
IdentityField,
ModelSerializer,
NestedIdentityField,
NestedRelatedField,
RelatedField,
RelatedResourceField,
SetLabelSerializer,
TaskGroupOperationResponseSerializer,
UnsetLabelSerializer,
ValidateFieldsMixin,
validate_unknown_fields,
HiddenFieldsMixin,
)
from .fields import (
BaseURLField,
Expand Down
30 changes: 30 additions & 0 deletions pulpcore/app/serializers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,33 @@ class TaskGroupOperationResponseSerializer(serializers.Serializer):
view_name="task-groups-detail",
allow_null=False,
)


class SetLabelSerializer(serializers.Serializer):
"""
Serializer for synchronously setting a label.
"""

key = serializers.SlugField(required=True)
value = serializers.CharField(required=True, allow_null=True, allow_blank=True)


class UnsetLabelSerializer(serializers.Serializer):
"""
Serializer for synchronously setting a label.
"""

key = serializers.SlugField(required=True)
value = serializers.CharField(read_only=True)

def validate_key(self, value):
if value not in self.context["content_object"].pulp_labels:
raise serializers.ValidationError(
_("Label '{key}' is not set on the object.").format(key=value)
)
return value

def validate(self, data):
data = super().validate(data)
data["value"] = self.context["content_object"].pulp_labels[data["key"]]
return data
1 change: 1 addition & 0 deletions pulpcore/app/viewsets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
AsyncCreateMixin,
AsyncRemoveMixin,
AsyncUpdateMixin,
LabelsMixin,
NamedModelViewSet,
RolesMixin,
NAME_FILTER_OPTIONS,
Expand Down
45 changes: 44 additions & 1 deletion pulpcore/app/viewsets/base.py
Original file line number Diff line number Diff line change
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,
SetLabelSerializer,
UnsetLabelSerializer,
)
from pulpcore.app.util import get_viewset_for_model
from pulpcore.tasking.tasks import dispatch

Expand Down Expand Up @@ -626,3 +632,40 @@ 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.",
)
@action(detail=True, methods=["post"], 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["key"], serializer.validated_data["value"]],
)
)
return Response(serializer.data, status=201)

@extend_schema(
summary="Unset a label",
description="Unset a single pulp_label on the object.",
)
@action(detail=True, methods=["post"], serializer_class=UnsetLabelSerializer)
def unset_label(self, request, pk=None):
obj = self.get_object()
serializer = UnsetLabelSerializer(
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["key"]])
)
return Response(serializer.data, status=201)
2 changes: 2 additions & 0 deletions pulpcore/app/viewsets/publication.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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", "unset_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, {"key": "a", "value": None})
file_repository_api_client.set_label(repository.pulp_href, {"key": "b", "value": ""})
file_repository_api_client.set_label(repository.pulp_href, {"key": "c", "value": "val1"})
file_repository_api_client.set_label(repository.pulp_href, {"key": "d", "value": "val2"})
file_repository_api_client.set_label(repository.pulp_href, {"key": "e", "value": "val3"})
file_repository_api_client.set_label(repository.pulp_href, {"key": "c", "value": "val4"})
file_repository_api_client.set_label(repository.pulp_href, {"key": "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.unset_label(repository.pulp_href, {"key": "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 6c0389f

Please sign in to comment.