Skip to content

Commit 2bce299

Browse files
committed
Adds synchronous RPM Upload API
closes: #4027
1 parent ee9f0e5 commit 2bce299

5 files changed

Lines changed: 132 additions & 2 deletions

File tree

CHANGES/4027.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a synchronous RPM upload API. It's available at /pulp/api/v3/content/rpm/packages/upload/.

pulp_rpm/app/serializers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
ModulemdDefaultsSerializer,
2727
ModulemdObsoleteSerializer,
2828
)
29-
from .package import PackageSerializer, MinimalPackageSerializer # noqa
29+
from .package import PackageSerializer, PackageUploadSerializer, MinimalPackageSerializer # noqa
3030
from .prune import PrunePackagesSerializer # noqa
3131
from .repository import ( # noqa
3232
CopySerializer,

pulp_rpm/app/serializers/package.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import createrepo_c as cr
12
import logging
23
import traceback
34
from gettext import gettext as _
45

6+
from django.conf import settings
7+
from django.db import DatabaseError
58
from drf_spectacular.utils import extend_schema_serializer
69
from rest_framework import serializers
710
from rest_framework.exceptions import NotAcceptable
811

12+
from pulpcore.plugin.models import Artifact, PulpTemporaryFile
913
from pulpcore.plugin.serializers import (
14+
ArtifactSerializer,
1015
ContentChecksumSerializer,
1116
SingleArtifactContentUploadSerializer,
1217
)
@@ -382,3 +387,67 @@ class Meta:
382387
"checksum_type",
383388
)
384389
model = Package
390+
391+
392+
class PackageUploadSerializer(PackageSerializer):
393+
"""
394+
Serializer for requests to synchronously upload RPM packages.
395+
"""
396+
397+
class Meta(PackageSerializer.Meta):
398+
# This API does not support uploading to a repository
399+
fields = tuple(f for f in PackageSerializer.Meta.fields if f != "repository")
400+
model = Package
401+
# Name used for the OpenAPI request object
402+
ref_name = "PackageUpload"
403+
404+
def validate(self, data):
405+
uploaded_file = data.get("file")
406+
# export META from rpm and prepare dict as saveable format
407+
try:
408+
cr_object = cr.package_from_rpm(
409+
uploaded_file.file.name, changelog_limit=settings.KEEP_CHANGELOG_LIMIT
410+
)
411+
new_pkg = Package.createrepo_to_dict(cr_object)
412+
except OSError as e:
413+
log.info(traceback.format_exc())
414+
raise NotAcceptable(detail="RPM file cannot be parsed for metadata") from e
415+
416+
# Get or create the Artifact
417+
if "file" in data:
418+
file = data.pop("file")
419+
# if artifact already exists, let's use it
420+
try:
421+
artifact = Artifact.objects.get(
422+
sha256=file.hashers["sha256"].hexdigest(), pulp_domain=get_domain_pk()
423+
)
424+
if not artifact.pulp_domain.get_storage().exists(artifact.file.name):
425+
artifact.file = file
426+
artifact.save()
427+
else:
428+
artifact.touch()
429+
except (Artifact.DoesNotExist, DatabaseError):
430+
artifact_data = {"file": file}
431+
serializer = ArtifactSerializer(data=artifact_data)
432+
serializer.is_valid(raise_exception=True)
433+
artifact = serializer.save()
434+
data["artifact"] = artifact
435+
436+
filename = (
437+
format_nvra(
438+
new_pkg["name"],
439+
new_pkg["version"],
440+
new_pkg["release"],
441+
new_pkg["arch"],
442+
)
443+
+ ".rpm"
444+
)
445+
446+
if not data.get("relative_path"):
447+
data["relative_path"] = filename
448+
new_pkg["location_href"] = filename
449+
else:
450+
new_pkg["location_href"] = data["relative_path"]
451+
452+
data.update(new_pkg)
453+
return data

pulp_rpm/app/viewsets/package.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.db import transaction
12
from django_filters import CharFilter
23
from drf_spectacular.utils import extend_schema
34
from pulpcore.plugin.models import PulpTemporaryFile
@@ -8,10 +9,17 @@
89
OperationPostponedResponse,
910
SingleArtifactContentUploadViewSet,
1011
)
12+
from rest_framework import status
13+
from rest_framework.decorators import action
14+
from rest_framework.response import Response
1115

1216
from pulp_rpm.app import tasks as rpm_tasks
1317
from pulp_rpm.app.models import Package
14-
from pulp_rpm.app.serializers import MinimalPackageSerializer, PackageSerializer
18+
from pulp_rpm.app.serializers import (
19+
MinimalPackageSerializer,
20+
PackageSerializer,
21+
PackageUploadSerializer,
22+
)
1523

1624

1725
class PackageFilter(ContentFilter):
@@ -127,3 +135,22 @@ def create(self, request):
127135
},
128136
)
129137
return OperationPostponedResponse(task, request)
138+
139+
@extend_schema(
140+
description="Synchronously upload an RPM package.",
141+
request=PackageUploadSerializer,
142+
responses={201: PackageSerializer},
143+
summary="Upload an RPM package synchronously.",
144+
)
145+
@action(detail=False, methods=["post"], serializer_class=PackageUploadSerializer)
146+
def upload(self, request):
147+
"""Create an RPM package."""
148+
serializer = self.get_serializer(data=request.data)
149+
with transaction.atomic():
150+
# Create the artifact
151+
serializer.is_valid(raise_exception=True)
152+
# Create the Package
153+
serializer.save()
154+
155+
headers = self.get_success_headers(serializer.data)
156+
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

pulp_rpm/tests/functional/api/test_upload.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,39 @@ def test_upload_comps_xml_into_repo_replace(
206206
eval_counts(vers_resp.content_summary.added, is_small=False)
207207

208208

209+
def test_synchronous_package_upload(
210+
delete_orphans_pre, rpm_package_api, monitor_task, pulpcore_bindings
211+
):
212+
"""Test synchronously uploading an RPM.
213+
214+
1. Upload a unit
215+
2. Attempt to upload same unit with different labels
216+
3. Assert that labels don't change.
217+
"""
218+
# Single unit upload
219+
file_to_use = os.path.join(RPM_UNSIGNED_FIXTURE_URL, RPM_PACKAGE_FILENAME)
220+
221+
labels = {"key_1": "value_1"}
222+
with NamedTemporaryFile() as file_to_upload:
223+
file_to_upload.write(requests.get(file_to_use).content)
224+
upload_attrs = {"file": file_to_upload.name, "pulp_labels": labels}
225+
package = rpm_package_api.upload(**upload_attrs)
226+
227+
assert package.location_href == RPM_PACKAGE_FILENAME
228+
assert package.pulp_labels == labels
229+
230+
# Duplicate unit
231+
with NamedTemporaryFile() as file_to_upload:
232+
new_labels = {"key_2": "value_2"}
233+
file_to_upload.write(requests.get(file_to_use).content)
234+
upload_attrs = {"file": file_to_upload.name, "pulp_labels": new_labels}
235+
duplicate_package = rpm_package_api.upload(**upload_attrs)
236+
237+
assert duplicate_package.pulp_href == package.pulp_href
238+
assert duplicate_package.pulp_labels == package.pulp_labels
239+
assert duplicate_package.pulp_labels != new_labels
240+
241+
209242
def eval_resources(resources, is_small=True):
210243
"""Eval created_resources counts."""
211244
groups = [g for g in resources if "packagegroups" in g]

0 commit comments

Comments
 (0)