Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/+add-pulp-exceptions.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add more Pulp Exceptions.
Comment thread
jobselko marked this conversation as resolved.
71 changes: 71 additions & 0 deletions pulp_python/app/exceptions.py
Comment thread
aKlimau marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from gettext import gettext as _

from pulpcore.plugin.exceptions import PulpException


class AttestationVerificationError(PulpException):
"""
Raised when attestation verification fails.
"""

error_code = "PYT0002"

def __init__(self, message):
super().__init__()
self.message = message

def __str__(self):
return f"[{self.error_code}] " + _("Attestation verification failed: {message}").format(
message=self.message
)


class UnsupportedProtocolError(PulpException):
"""
Raised when an unsupported protocol is used for syncing.
"""

error_code = "PYT0004"

def __init__(self, protocol):
super().__init__()
self.protocol = protocol

def __str__(self):
return f"[{self.error_code}] " + _(
"Only HTTP(S) is supported for python syncing, got: {protocol}"
).format(protocol=self.protocol)


class RemoteFetchError(PulpException):
"""
Raised when fetching metadata from all remotes fails.
"""

error_code = "PYT0008"

def __init__(self, url):
super().__init__()
self.url = url

def __str__(self):
return f"[{self.error_code}] " + _("Failed to fetch {url} from any remote.").format(
url=self.url
)


class InvalidAttestationsError(PulpException):
"""
Raised when attestation data cannot be validated.
"""

error_code = "PYT0009"

def __init__(self, message):
super().__init__()
self.message = message

def __str__(self):
return f"[{self.error_code}] " + _("Invalid attestations: {message}").format(
message=self.message
)
12 changes: 6 additions & 6 deletions pulp_python/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,16 +412,16 @@ def finalize_new_version(self, new_version):

def _check_for_package_substitution(self, new_version):
"""
Raise a ValidationError if newly added packages would replace existing packages that have
the same filename but a different sha256 checksum.
Raise a PackageSubstitutionError if newly added packages would replace existing packages
that have the same filename but a different sha256 checksum.
"""
qs = PythonPackageContent.objects.filter(pk__in=new_version.content)
duplicates = collect_duplicates(qs, ("filename",))
if duplicates:
raise ValidationError(
"Found duplicate packages being added with the same filename but different checksums. " # noqa: E501
"To allow this, set 'allow_package_substitution' to True on the repository. "
f"Conflicting packages: {duplicates}"
"Found duplicate packages being added with the same filename but different "
"checksums. To allow this, set 'allow_package_substitution' to True on the "
f"repository. Conflicting packages: {duplicates}"
)

def _check_blocklist(self, new_version):
Expand All @@ -436,7 +436,7 @@ def _check_blocklist(self, new_version):

def check_blocklist_for_packages(self, packages):
"""
Raise a ValidationError if any of the given packages match a blocklist entry.
Raise a BlocklistedPackageError if any of the given packages match a blocklist entry.
"""
entries = PythonBlocklistEntry.objects.filter(repository=self)
if not entries.exists():
Expand Down
7 changes: 4 additions & 3 deletions pulp_python/app/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from drf_spectacular.utils import extend_schema_serializer
from packaging.requirements import Requirement
from packaging.version import InvalidVersion, Version
from pydantic import TypeAdapter, ValidationError
from pydantic import TypeAdapter
from pydantic import ValidationError as PydanticValidationError
from pypi_attestations import AttestationError
from rest_framework import serializers

Expand Down Expand Up @@ -387,7 +388,7 @@ def validate_attestations(self, value):
attestations = TypeAdapter(list[Attestation]).validate_json(value)
else:
attestations = TypeAdapter(list[Attestation]).validate_python(value)
except ValidationError as e:
except PydanticValidationError as e:
raise serializers.ValidationError(_("Invalid attestations: {}").format(e))
return attestations

Expand Down Expand Up @@ -654,7 +655,7 @@ def deferred_validate(self, data):
try:
provenance = Provenance.model_validate_json(data["file"].read())
data["provenance"] = provenance.model_dump(mode="json")
except ValidationError as e:
except PydanticValidationError as e:
raise serializers.ValidationError(
_("The uploaded provenance is not valid: {}").format(e)
)
Expand Down
9 changes: 5 additions & 4 deletions pulp_python/app/tasks/sync.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio
import logging
from functools import partial
from gettext import gettext as _
from urllib.parse import urljoin

from aiohttp import ClientError, ClientResponseError
Expand All @@ -12,9 +11,9 @@
from packaging.requirements import Requirement
from pypi_attestations import Provenance
from pypi_simple import IndexPage
from rest_framework import serializers

from pulpcore.plugin.download import HttpDownloader
from pulpcore.plugin.exceptions import SyncError
from pulpcore.plugin.models import Artifact, ProgressReport, Remote, Repository
from pulpcore.plugin.stages import (
DeclarativeArtifact,
Expand All @@ -23,6 +22,7 @@
Stage,
)

from pulp_python.app.exceptions import UnsupportedProtocolError
from pulp_python.app.models import (
PackageProvenance,
PythonPackageContent,
Expand Down Expand Up @@ -52,7 +52,7 @@ def sync(remote_pk, repository_pk, mirror):
repository = Repository.objects.get(pk=repository_pk)

if not remote.url:
raise serializers.ValidationError(detail=_("A remote must have a url attribute to sync."))
raise SyncError("A remote must have a url attribute to sync.")

first_stage = PythonBanderStage(remote)
DeclarativeVersion(first_stage, repository, mirror).create()
Expand Down Expand Up @@ -115,7 +115,8 @@ async def run(self):
url = self.remote.url.rstrip("/")
downloader = self.remote.get_downloader(url=url)
if not isinstance(downloader, HttpDownloader):
raise ValueError("Only HTTP(S) is supported for python syncing")
protocol = type(downloader).__name__
raise UnsupportedProtocolError(protocol)

async with Master(url, allow_non_https=True) as master:
# Replace the session with the remote's downloader session
Expand Down
13 changes: 11 additions & 2 deletions pulp_python/app/tasks/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
from django.contrib.sessions.models import Session
from django.db import transaction
from pydantic import TypeAdapter
from pydantic import ValidationError as PydanticValidationError
from pypi_attestations import AttestationError

from pulpcore.plugin.models import Artifact, Content, ContentArtifact, CreatedResource
from pulpcore.plugin.util import get_current_authenticated_user, get_domain, get_prn

from pulp_python.app.exceptions import AttestationVerificationError, InvalidAttestationsError
from pulp_python.app.models import PackageProvenance, PythonPackageContent, PythonRepository
from pulp_python.app.provenance import (
AnyPublisher,
Expand Down Expand Up @@ -123,13 +126,19 @@ def create_provenance(package, attestations, domain):
Returns:
the newly created PackageProvenance
"""
attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
try:
attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
except PydanticValidationError as e:
raise InvalidAttestationsError(str(e))

user = get_current_authenticated_user()
publisher = AnyPublisher(kind="Pulp User", prn=get_prn(user))
att_bundle = AttestationBundle(publisher=publisher, attestations=attestations)
provenance = Provenance(attestation_bundles=[att_bundle])
verify_provenance(package.filename, package.sha256, provenance)
try:
verify_provenance(package.filename, package.sha256, provenance)
except AttestationError as e:
raise AttestationVerificationError(str(e))
provenance_json = provenance.model_dump(mode="json")

prov_sha256 = PackageProvenance.calculate_sha256(provenance_json)
Expand Down
4 changes: 3 additions & 1 deletion pulp_python/app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from pulpcore.plugin.models import Artifact, Remote
from pulpcore.plugin.util import get_domain

from pulp_python.app.exceptions import RemoteFetchError

log = logging.getLogger(__name__)


Expand Down Expand Up @@ -356,7 +358,7 @@ def fetch_json_release_metadata(name: str, version: str, remotes: set[Remote]) -
json_data = json.load(file)
return json_data
else:
raise Exception(f"Failed to fetch {url} from any remote.")
raise RemoteFetchError(url=url)


def python_content_to_json(base_path, content_query, version=None, domain=None):
Expand Down
2 changes: 1 addition & 1 deletion pulp_python/tests/functional/api/test_attestations.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_verify_provenance(python_bindings, twine_package, python_content_factor
with pytest.raises(PulpTaskError) as e:
monitor_task(provenance.task)
assert e.value.task.state == "failed"
assert "twine-6.2.0-py3-none-any.whl != twine-6.2.0.tar.gz" in e.value.task.error["description"]
assert "Provenance verification failed" in e.value.task.error["description"]

# Test creating a provenance without verifying
provenance = python_bindings.ContentProvenanceApi.create(
Expand Down
3 changes: 1 addition & 2 deletions pulp_python/tests/functional/api/test_crud_content_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ def test_content_crud(
with pytest.raises(PulpTaskError) as e:
response = python_bindings.ContentPackagesApi.create(**content_body)
monitor_task(response.task)
msg = "The uploaded artifact's sha256 checksum does not match the one provided"
assert msg in e.value.task.error["description"]
assert "sha256 checksum does not match" in e.value.task.error["description"]


def test_content_create_new_metadata(
Expand Down
Loading