Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added upload support #113

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/52.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ability to upload Maven Artifacts to repositories.
1 change: 1 addition & 0 deletions docs/workflows/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ set is the hostname and port::
:maxdepth: 2

cache
upload
49 changes: 49 additions & 0 deletions docs/workflows/upload.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Upload a Jar to a Maven Repository
==================================

Create a maven Repository for the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftovers


Create a Maven Repository
-------------------------

``$ http POST http://localhost:24817/pulp/api/v3/repositories/maven/maven/ name='my-snapshot-repository'``

Create a Maven Distribution for the Maven Repository
----------------------------------------------------

``$ http POST http://localhost:24817/pulp/api/v3/distributions/maven/maven/ name='my-snapshot-repository' base_path='my/local/snapshots' repository=$REPO_HREF``


.. code:: json

{
"pulp_href": "/pulp/api/v3/distributions/67baa17e-0a9f-4302-b04a-dbf324d139de/"
}

Upload a Jar to the Repository
------------------------------

``$ http --form POST http://localhost:24817/pulp/api/v3/content/maven/artifact/ group_id='org.openapitools' artifact_id='openapi-generator-cli' version='6.4.0-SNAPSHOT' filename='openapi-generator-cli-6.4.0.jar' file@./openapi-generator-cli.jar repository=$REPO_HREF``


.. code:: json

{
"task": "/pulp/api/v3/tasks/03d5a40b-4bda-4ee7-96cb-f0639b6c5d6a/"
}

Add Pulp as mirror for Maven
----------------------------

.. code:: xml

<settings>
<mirrors>
<mirror>
<id>pulp-maven-central</id>
<name>Local Maven Central mirror </name>
<url>http://localhost:24816/pulp/content/my/local/my/local/snapshots</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth adding one last step which shows that the uploaded jar is consumable by the maven client?

70 changes: 46 additions & 24 deletions pulp_maven/app/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,70 @@

from rest_framework import serializers

from pulpcore.plugin import serializers as platform
from pulpcore.plugin.serializers import (
ContentChecksumSerializer,
DetailRelatedField,
DistributionSerializer,
RemoteSerializer,
RepositorySerializer,
SingleArtifactContentUploadSerializer,
)

from . import models


class MavenRepositorySerializer(platform.RepositorySerializer):
class MavenRepositorySerializer(RepositorySerializer):
"""
Serializer for Maven Repositories.
"""

class Meta:
fields = platform.RepositorySerializer.Meta.fields
fields = RepositorySerializer.Meta.fields
model = models.MavenRepository


class MavenArtifactSerializer(platform.SingleArtifactContentSerializer):
class MavenArtifactSerializer(SingleArtifactContentUploadSerializer, ContentChecksumSerializer):
"""
A Serializer for MavenArtifact.
"""

group_id = serializers.CharField(
help_text=_("Group Id of the artifact's package."), read_only=True
)
artifact_id = serializers.CharField(
help_text=_("Artifact Id of the artifact's package."), read_only=True
)
version = serializers.CharField(
help_text=_("Version of the artifact's package."), read_only=True
)
filename = serializers.CharField(help_text=_("Filename of the artifact."), read_only=True)
group_id = serializers.CharField(help_text=_("Group Id of the artifact's package."))
artifact_id = serializers.CharField(help_text=_("Artifact Id of the artifact's package."))
version = serializers.CharField(help_text=_("Version of the artifact's package."))
filename = serializers.CharField(help_text=_("Filename of the artifact."))

def deferred_validate(self, data):
"""Validate the FileContent data."""
data = super().deferred_validate(data)
data["relative_path"] = (
f"{data['group_id'].replace('.', '/')}/{data['artifact_id']}/{data['version']}/"
f"{data['filename']}"
)
return data

def retrieve(self, validated_data):
content = models.MavenArtifact.objects.filter(
group_id=validated_data["group_id"],
artifact_id=validated_data["artifact_id"],
version=validated_data["version"],
filename=validated_data["filename"],
)
return content.first()

class Meta:
fields = platform.SingleArtifactContentSerializer.Meta.fields + (
"group_id",
"artifact_id",
"version",
"filename",
fields = (
SingleArtifactContentUploadSerializer.Meta.fields
+ ContentChecksumSerializer.Meta.fields
+ ("group_id", "artifact_id", "version", "filename")
)
# Remove relative_path
fields = tuple(field for field in fields if field != "relative_path")
Comment on lines +56 to +62
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a single expression?
like fields = (Base.Meta.fields + ("group_id", ...) - ("relative_path"))?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeError: unsupported operand type(s) for -: 'tuple' and 'tuple'
Or maybe there's another way to remove it from the tuple.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I knew i've seen it before: https://github.com/pulp/pulp_container/blob/main/pulp_container/app/serializers.py#L214

Using a set and then casting it into a tuple was clever. 🚀

Comment on lines +56 to +62
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fields = (
SingleArtifactContentUploadSerializer.Meta.fields
+ ContentChecksumSerializer.Meta.fields
+ ("group_id", "artifact_id", "version", "filename")
)
# Remove relative_path
fields = tuple(field for field in fields if field != "relative_path")
fields = tuple(
set(SingleArtifactContentUploadSerializer.Meta.fields
+ ContentChecksumSerializer.Meta.fields
+ ("group_id", "artifact_id", "version", "filename")) - set("relative_path")
)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like this @mdellweg?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, exactly.

model = models.MavenArtifact
# Validation occurs in the task.
validators = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the validation about that we kill here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unique Togeth Validation .. I am still not sure how it is getting injected here. I followed the example from pulp_file for FileContentSerializer which doesn't have any validators, however, I kept having the UniqueTogetherValidator getting injected. This was the only way I found to fix that problem. However, I am open to other solutions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know of a better way.

git grep UniqueTogetherValidator is silent on both pulpcore and pulp_maven.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@quba42 see, i knew i saw it before somewhere...
How did you solve it again?



class MavenRemoteSerializer(platform.RemoteSerializer):
class MavenRemoteSerializer(RemoteSerializer):
"""
A Serializer for MavenRemote.

Expand All @@ -58,16 +80,16 @@ class Meta:
"""

class Meta:
fields = platform.RemoteSerializer.Meta.fields
fields = RemoteSerializer.Meta.fields
model = models.MavenRemote


class MavenDistributionSerializer(platform.DistributionSerializer):
class MavenDistributionSerializer(DistributionSerializer):
"""
Serializer for Maven Distributions.
"""

remote = platform.DetailRelatedField(
remote = DetailRelatedField(
required=False,
help_text=_("Remote that can be used to fetch content when using pull-through caching."),
queryset=models.MavenRemote.objects.all(),
Expand All @@ -76,5 +98,5 @@ class MavenDistributionSerializer(platform.DistributionSerializer):
)

class Meta:
fields = platform.DistributionSerializer.Meta.fields + ("remote",)
fields = DistributionSerializer.Meta.fields + ("remote",)
model = models.MavenDistribution
50 changes: 33 additions & 17 deletions pulp_maven/app/viewsets.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,78 @@
from pulpcore.plugin import viewsets as core
from pulpcore.plugin.viewsets import (
ContentFilter,
DistributionViewSet,
RemoteViewSet,
RepositoryVersionViewSet,
RepositoryViewSet,
SingleArtifactContentUploadViewSet,
)
from pulpcore.plugin.actions import ModifyRepositoryActionMixin

from . import models, serializers

from pulp_maven.app.models import MavenArtifact, MavenRemote, MavenRepository, MavenDistribution

class MavenArtifactFilter(core.ContentFilter):
from pulp_maven.app.serializers import (
MavenArtifactSerializer,
MavenRemoteSerializer,
MavenRepositorySerializer,
MavenDistributionSerializer,
)


class MavenArtifactFilter(ContentFilter):
"""
FilterSet for MavenArtifact.
"""

class Meta:
model = models.MavenArtifact
model = MavenArtifact
fields = ["group_id", "artifact_id", "version", "filename"]


class MavenArtifactViewSet(core.ContentViewSet):
class MavenArtifactViewSet(SingleArtifactContentUploadViewSet):
"""
A ViewSet for MavenArtifact.
"""

endpoint_name = "artifact"
queryset = models.MavenArtifact.objects.all()
serializer_class = serializers.MavenArtifactSerializer
queryset = MavenArtifact.objects.prefetch_related("_artifacts").all()
serializer_class = MavenArtifactSerializer
filterset_class = MavenArtifactFilter


class MavenRemoteViewSet(core.RemoteViewSet):
class MavenRemoteViewSet(RemoteViewSet):
"""
A ViewSet for MavenRemote.
"""

endpoint_name = "maven"
queryset = models.MavenRemote.objects.all()
serializer_class = serializers.MavenRemoteSerializer
queryset = MavenRemote.objects.all()
serializer_class = MavenRemoteSerializer


class MavenRepositoryViewSet(core.RepositoryViewSet):
class MavenRepositoryViewSet(RepositoryViewSet, ModifyRepositoryActionMixin):
"""
A ViewSet for MavenRemote.
"""

endpoint_name = "maven"
queryset = models.MavenRepository.objects.all()
serializer_class = serializers.MavenRepositorySerializer
queryset = MavenRepository.objects.all()
serializer_class = MavenRepositorySerializer


class MavenRepositoryVersionViewSet(core.RepositoryVersionViewSet):
class MavenRepositoryVersionViewSet(RepositoryVersionViewSet):
"""
MavenRepositoryVersion represents a single Maven repository version.
"""

parent_viewset = MavenRepositoryViewSet


class MavenDistributionViewSet(core.DistributionViewSet):
class MavenDistributionViewSet(DistributionViewSet):
"""
ViewSet for Maven Distributions.
"""

endpoint_name = "maven"
queryset = models.MavenDistribution.objects.all()
serializer_class = serializers.MavenDistributionSerializer
queryset = MavenDistribution.objects.all()
serializer_class = MavenDistributionSerializer
83 changes: 83 additions & 0 deletions pulp_maven/tests/functional/api/test_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import hashlib
from urllib.parse import urljoin

from pulp_maven.tests.functional.utils import download_file


def test_upload_workflow(
maven_repo_api_client,
maven_repo_factory,
random_maven_artifact_factory,
maven_artifact_api_client,
maven_distribution_factory,
gen_object_with_cleanup,
):
# Create a repository and assert that the latest version is 0
repo = maven_repo_factory()
assert repo.latest_version_href.endswith("/versions/0/")

# Create a random jar
jar_file = random_maven_artifact_factory()

# Upload the jar into the repository
artifact_kwargs = dict(
group_id=jar_file["group_id"],
artifact_id=jar_file["artifact_id"],
version=jar_file["version"],
filename=jar_file["filename"],
file=jar_file["full_path"],
repository=repo.pulp_href,
)
maven_artifact = gen_object_with_cleanup(maven_artifact_api_client, **artifact_kwargs)

# Assert that a Maven Artifact was created
assert maven_artifact.group_id == jar_file["group_id"]
assert maven_artifact.artifact_id == jar_file["artifact_id"]
assert maven_artifact.version == jar_file["version"]
assert maven_artifact.filename == jar_file["filename"]

# Assert that a repository version was created
repo = maven_repo_api_client.read(repo.pulp_href)
assert repo.latest_version_href.endswith("/versions/1/")

# Assert that this Maven Artifact is in the repository version
content_in_repo_version = maven_artifact_api_client.list(
repository_version=repo.latest_version_href
)
assert content_in_repo_version.results[0].pulp_href == maven_artifact.pulp_href

# Create a second repository and assert that latest version is 0
repo2 = maven_repo_factory()
assert repo2.latest_version_href.endswith("/versions/0/")

# Assert that the same content unit can be uploaded again
artifact_kwargs["repository"] = repo2.pulp_href
maven_artifact2 = gen_object_with_cleanup(maven_artifact_api_client, **artifact_kwargs)

# Assert that the existing artifact was identified by the upload API.
assert maven_artifact.pulp_href == maven_artifact2.pulp_href

# Assert that a new repository version was created.
repo2 = maven_repo_api_client.read(repo2.pulp_href)
assert repo2.latest_version_href.endswith("/versions/1/")

# Assert that this Maven Artifact is in the repository version
content_in_repo2_version = maven_artifact_api_client.list(
repository_version=repo2.latest_version_href
)
assert content_in_repo2_version.results[0].pulp_href == maven_artifact2.pulp_href

# Create a distribution and serve repo
distribution = maven_distribution_factory(repository=repo.pulp_href)

# Download the jar from the distribution
unit_path = (
f"{jar_file['group_id'].replace('.', '/')}/{jar_file['artifact_id']}/"
f"{jar_file['version']}/{jar_file['filename']}"
)
pulp_unit_url = urljoin(distribution.base_url, unit_path)
downloaded_file = download_file(pulp_unit_url)
downloaded_file_checksum = hashlib.sha256(downloaded_file.body).hexdigest()

# Assert that the downloaded file's checksum matches the original
assert jar_file["sha256"] == downloaded_file_checksum
20 changes: 20 additions & 0 deletions pulp_maven/tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
RepositoriesMavenApi,
)

from pulp_maven.tests.functional.utils import generate_jar


@pytest.fixture(scope="session")
def maven_client(_api_client_set, bindings_cfg):
Expand Down Expand Up @@ -69,3 +71,21 @@ def _maven_remote_factory(**kwargs):
return gen_object_with_cleanup(maven_remote_api_client, kwargs)

yield _maven_remote_factory


@pytest.fixture
def random_maven_artifact_factory(tmp_path):
"""A factory to generate a random maven artifact."""

def _random_maven_artifact_factory(**kwargs):
kwargs.setdefault("group_id", f"org.{str(uuid.uuid4())}")
kwargs.setdefault("artifact_id", str(uuid.uuid4()))
kwargs.setdefault("version", str(uuid.uuid4()))
kwargs.setdefault("filename", f"{str(uuid.uuid4())}.jar")
full_path = tmp_path / kwargs["filename"]
_, checksum = generate_jar(full_path)
kwargs["full_path"] = full_path
kwargs["sha256"] = checksum
return kwargs

return _random_maven_artifact_factory