diff --git a/CHANGES/+add-pulp-exceptions.feature b/CHANGES/+add-pulp-exceptions.feature new file mode 100644 index 00000000..b0fd6437 --- /dev/null +++ b/CHANGES/+add-pulp-exceptions.feature @@ -0,0 +1 @@ +Add more Pulp Exceptions. diff --git a/pulp_python/app/exceptions.py b/pulp_python/app/exceptions.py new file mode 100644 index 00000000..cc0aa5fa --- /dev/null +++ b/pulp_python/app/exceptions.py @@ -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 + ) diff --git a/pulp_python/app/models.py b/pulp_python/app/models.py index fa45835d..47b25c17 100644 --- a/pulp_python/app/models.py +++ b/pulp_python/app/models.py @@ -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): @@ -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(): diff --git a/pulp_python/app/serializers.py b/pulp_python/app/serializers.py index 14784db3..560d8e93 100644 --- a/pulp_python/app/serializers.py +++ b/pulp_python/app/serializers.py @@ -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 @@ -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 @@ -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) ) diff --git a/pulp_python/app/tasks/sync.py b/pulp_python/app/tasks/sync.py index ee957da9..78a6a4f1 100644 --- a/pulp_python/app/tasks/sync.py +++ b/pulp_python/app/tasks/sync.py @@ -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 @@ -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, @@ -23,6 +22,7 @@ Stage, ) +from pulp_python.app.exceptions import UnsupportedProtocolError from pulp_python.app.models import ( PackageProvenance, PythonPackageContent, @@ -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() @@ -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 diff --git a/pulp_python/app/tasks/upload.py b/pulp_python/app/tasks/upload.py index 48faebeb..98ddf354 100644 --- a/pulp_python/app/tasks/upload.py +++ b/pulp_python/app/tasks/upload.py @@ -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, @@ -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) diff --git a/pulp_python/app/utils.py b/pulp_python/app/utils.py index 203a1530..8b5bd6eb 100644 --- a/pulp_python/app/utils.py +++ b/pulp_python/app/utils.py @@ -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__) @@ -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): diff --git a/pulp_python/tests/functional/api/test_attestations.py b/pulp_python/tests/functional/api/test_attestations.py index 0737256b..489833e8 100644 --- a/pulp_python/tests/functional/api/test_attestations.py +++ b/pulp_python/tests/functional/api/test_attestations.py @@ -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( diff --git a/pulp_python/tests/functional/api/test_crud_content_unit.py b/pulp_python/tests/functional/api/test_crud_content_unit.py index 5bf79956..63c12d22 100644 --- a/pulp_python/tests/functional/api/test_crud_content_unit.py +++ b/pulp_python/tests/functional/api/test_crud_content_unit.py @@ -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(