Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/342.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ``twine`` (and other similar Python tools) package upload support
1 change: 1 addition & 0 deletions doc_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
build
coreapi
django
djangorestframework
Expand Down
1 change: 1 addition & 0 deletions docs/_scripts/index.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pulp python distribution create --name my-pypi --base-path my-pypi --repository foo
4 changes: 4 additions & 0 deletions docs/_scripts/quickstart.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

# From the _scripts directory, run with `source quickstart.sh` (source to preserve the environment
# variables)
export PLUGIN_SOURCE="../../"
set -e

source base.sh
Expand All @@ -23,3 +24,6 @@ source pip.sh

source upload.sh
source add_content_repo.sh

source index.sh
source twine.sh
4 changes: 4 additions & 0 deletions docs/_scripts/twine.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Build custom package
python -m build $PLUGIN_SOURCE
# Upload built package distributions to my-pypi
twine upload --repository-url $BASE_ADDR/pypi/my-pypi/simple/ -u admin -p password "$PLUGIN_SOURCE"dist/*
3 changes: 2 additions & 1 deletion docs/tech-preview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ The following features are currently being released as part of a tech preview
* New endpoint “pulp/api/v3/remotes/python/python/from_bandersnatch/” that allows for Python remote creation from a
Bandersnatch config file.
* PyPI’s json API at content endpoint ‘/pypi/{package-name}/json’. Allows for basic Pulp-to-Pulp syncing.
* Fully mirror Python repositories like PyPI
* Fully mirror Python repositories like PyPI.
* ``Twine`` upload packages to indexes at endpoints '/simple` or '/legacy'.
1 change: 1 addition & 0 deletions docs/workflows/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Pulp CLI documentation on how to do that.
.. toctree::
:maxdepth: 2

pypi
sync
upload
publish
70 changes: 70 additions & 0 deletions docs/workflows/pypi.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
.. _pypi-workflow:

Setup your own PyPI:
====================

This section guides you through the quickest way to to setup ``pulp_python`` to act as your very own
private ``PyPI``.

Create a Repository:
--------------------

Repositories are the base objects ``Pulp`` uses to store and organize its content. They are automatically
versioned when content is added or deleted and allow for easy rollbacks to previous versions.

.. literalinclude:: ../_scripts/repo.sh
:language: bash

Repository Create Response::

{
"pulp_href": "/pulp/api/v3/repositories/python/python/3fe0d204-217f-4250-8177-c83b30751fca/",
"pulp_created": "2021-06-02T14:54:53.387054Z",
"versions_href": "/pulp/api/v3/repositories/python/python/3fe0d204-217f-4250-8177-c83b30751fca/versions/",
"pulp_labels": {},
"latest_version_href": "/pulp/api/v3/repositories/python/python/3fe0d204-217f-4250-8177-c83b30751fca/versions/1/",
"name": "foo",
"description": null,
"retained_versions": null,
"remote": null,
"autopublish": false
}

Create a Distribution:
----------------------

Distributions serve the content stored in repositories so that it can be used by tools like ``pip``.

.. literalinclude:: ../_scripts/index.sh
:language: bash

Distribution Create Response::

{
"pulp_href": "/pulp/api/v3/distributions/python/pypi/e8438593-fd40-4654-8577-65398833c331/",
"pulp_created": "2021-06-03T20:04:18.233230Z",
"base_path": "my-pypi",
"base_url": "https://pulp3-source-fedora33.localhost.example.com/pypi/foo/",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍

"content_guard": null,
"pulp_labels": {},
"name": "my-pypi",
"repository": "/pulp/api/v3/repositories/python/python/3fe0d204-217f-4250-8177-c83b30751fca/",
"publication": null,
"allow_uploads": true
}

Upload and Install Packages:
----------------------------

Packages can now be uploaded to the index using your favorite Python tool. The index url will be available
at ``/pypi/<distribution.base_path>/simple/``.

.. literalinclude:: ../_scripts/twine.sh
:language: bash

Packages can then be installed using your favorite Python tool::

$ pip install --trusted-host localhost -i $BASE_ADDR/pypi/my-pypi/simple/ shelf-reader

Now you have a fully operational Python package index. Check out the other workflows to see more features of
``pulp_python``.
1 change: 1 addition & 0 deletions functest_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
git+https://github.com/pulp/pulp-smash.git#egg=pulp-smash
pytest
lxml
twine
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-06-10 14:50

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('python', '0008_pythonpackagecontent_unique_sha256'),
]

operations = [
migrations.AddField(
model_name='pythondistribution',
name='allow_uploads',
field=models.BooleanField(default=True),
),
]
2 changes: 2 additions & 0 deletions pulp_python/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class PythonDistribution(Distribution):

TYPE = 'python'

allow_uploads = models.BooleanField(default=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like the container registry plugin uses an entirely separate distribution type for this. I'm not sure what the reasons are, or if they would apply to the Python plugin as well. @ipanova could you weigh in?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Container registry uses same type of distribution, it however has 2 repo_types. Push repo type is auto-created during 'podman push', this is basically a read-only repo which you subsequently cannot use for mirroring.
I see that a python repo is being created in advance before python packages twine upload and you have just one repo type.
Is it possible to use same repo for mirroring and uploading? if there are no constraints in that area then i probably don't see the reason to apply this limitation, and if applying I would probably move this flag to the repo model instead.

@dralley dralley Jun 11, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's my understanding of the way this PR works currently. Are there security reasons for the way the container plugin does it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

podman push operation creates a lot of intermediary repo-versions and only last repo-version is content compete meaning that in push-repo-type can be served only last version and it also means that rollback does not work.
Also if using one repo-type sync with 'mirror' mode would just remove everything what has been pushed previously by push api and this behavior can be undesirable. tldr; because of several subtleties it was easier and more explicit to have 2 repo types where one would be used for sync and another for push workflows. I think in registry world one would not use same repo for push and sync workflows and so that's why we have decided to bring the safeguards in a form of different repo types.

This however brings certain amount of complexity which we are figuring out how to minimize https://hackmd.io/z7i694VeTR-p4hgTk2Oocw#Food-for-thoughts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So it sounds like the container plugin has to deal with inter-content dependencies, and therefore intermediate repository versions are broken -- but because Python repositories only have one type of content, and one upload per content, that we therefore don't need to follow what the container plugin does with regards to separate repository / distribution types.

Thanks @ipanova, that is super helpful feedback.


def content_handler(self, path):
"""
Handler to serve extra, non-Artifact content for this Distribution
Expand Down
70 changes: 69 additions & 1 deletion pulp_python/app/pypi/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import logging
from gettext import gettext as _

from rest_framework import serializers
from pulp_python.app.tasks.upload import DIST_EXTENSIONS
from pulpcore.plugin.models import Artifact
from django.db.utils import IntegrityError

log = logging.getLogger(__name__)


class SummarySerializer(serializers.Serializer):
Expand All @@ -9,7 +15,9 @@ class SummarySerializer(serializers.Serializer):
"""

projects = serializers.IntegerField(help_text=_("Number of Python projects in index"))
releases = serializers.IntegerField(help_text=_("Number of Python distributions in index"))
releases = serializers.IntegerField(
help_text=_("Number of Python distribution releases in index")
)
files = serializers.IntegerField(help_text=_("Number of files for all distributions in index"))


Expand All @@ -22,3 +30,63 @@ class PackageMetadataSerializer(serializers.Serializer):
info = serializers.JSONField(help_text=_("Core metadata of the package"))
releases = serializers.JSONField(help_text=_("List of all the releases of the package"))
urls = serializers.JSONField()


class PackageUploadSerializer(serializers.Serializer):
"""
A Serializer for Python packages being uploaded to the index.
"""

content = serializers.FileField(
help_text=_("A Python package release file to upload to the index."),
required=True,
write_only=True,
)
action = serializers.CharField(
help_text=_("Defaults to `file_upload`, don't change it or request will fail!"),
default="file_upload",
source=":action"
)
sha256_digest = serializers.CharField(
help_text=_("SHA256 of package to validate upload integrity."),
required=True,
min_length=64,
max_length=64,
)

def validate(self, data):
"""Validates the request."""
action = data.get(":action")
if action != "file_upload":
raise serializers.ValidationError(
_("We do not support the :action {}").format(action)
)
file = data.get("content")
for ext, packagetype in DIST_EXTENSIONS.items():
if file.name.endswith(ext):
break
else:
raise serializers.ValidationError(_(
"Extension on {} is not a valid python extension "
"(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)").format(file.name)
)
sha256 = data.get("sha256_digest")
digests = {"sha256": sha256} if sha256 else None
artifact = Artifact.init_and_validate(file, expected_digests=digests)
try:
artifact.save()
except IntegrityError:
artifact = Artifact.objects.get(sha256=artifact.sha256)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is inconsistent with Warehouse. The standard PyPI returns 403 on duplicate artifacts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, inconsistent with Warehouse, but not so for Pulp. One of Pulp's key features is artifact de-duplication. If an artifact is shared among multiple content or a piece of content is contained in multiple repositories, Pulp only stores the one artifact or content unit once. So for pulp python the content unit is the Python package and its artifact is the tarball or wheel itself. Since Pulp can have multiple repositories if the package is already present in Pulp, but not in the repository, then it should be treated as adding that package to that repository.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What if the artifact exists but the hash is different? It mustn't be possible to mutate the same dist. At least this case should error out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The Artifact.init_and_validate will throw an error if the digest is wrong, so the same dist can't be mutated. However, if the package and its artifact already exist and you try to upload the same package with a different artifact, Pulp will just use the present artifact instead. This could be considered incorrect behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

you try to upload the same package with a different artifact

With the same hash or a different one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Either or. Currently artifacts in Pulp are unique by their hash, but python packages in pulp_python are only unique by their filename. Since a goal of Pulp is to allow hosting multiple indexes, I think I'm going to need to change the global uniqueness constraint for python packages to be their hash, but still have the local uniqueness constraint for repositories be their filename.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've changed the uniqueness constraint for packages to be their sha256. If you upload a package with the same hash as the one already in Pulp, but not in the repository you will simply add that package to that repository. If you try to upload a package with the same name as another in Pulp but not in the repository and with a different hash then a new package will be created and added to the repository. This means there can be two packages with the same name but different digests in Pulp, however they both can't be in the same repository. The sha256 of the package's metadata and its artifact are now linked and can't be mismatched.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This means there can be two packages with the same name but different digests in Pulp, however they both can't be in the same repository.

Awesome, this makes perfect sense 👍🏻 thx!

log.info(f"Artifact for {file.name} already existed in database")
data["content"] = (artifact, file.name)
return data


class PackageUploadTaskSerializer(serializers.Serializer):
"""
A Serializer for responding to a package upload task.
"""

session = serializers.CharField()
task = serializers.CharField()
task_start_time = serializers.DateTimeField()
Loading