diff --git a/CHANGES/+add-pulp-exceptions.bugfix b/CHANGES/+add-pulp-exceptions.bugfix new file mode 100644 index 00000000..b0fd6437 --- /dev/null +++ b/CHANGES/+add-pulp-exceptions.bugfix @@ -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..e0f520a9 --- /dev/null +++ b/pulp_python/app/exceptions.py @@ -0,0 +1,88 @@ +from gettext import gettext as _ + +from pulpcore.plugin.exceptions import PulpException + + +class ProvenanceVerificationError(PulpException): + """ + Raised when provenance verification fails. + """ + + error_code = "PLPY0001" + + def __init__(self, message): + """ + :param message: Description of the provenance verification error + :type message: str + """ + self.message = message + + def __str__(self): + return f"[{self.error_code}] " + _("Provenance verification failed: {message}").format( + message=self.message + ) + + +class AttestationVerificationError(PulpException): + """ + Raised when attestation verification fails. + """ + + error_code = "PLPY0002" + + def __init__(self, message): + """ + :param message: Description of the attestation verification error + :type message: str + """ + self.message = message + + def __str__(self): + return f"[{self.error_code}] " + _("Attestation verification failed: {message}").format( + message=self.message + ) + + +class PackageSubstitutionError(PulpException): + """ + Raised when packages with the same filename but different checksums are being added. + """ + + error_code = "PLPY0003" + + def __init__(self, duplicates): + """ + :param duplicates: Description of duplicate packages + :type duplicates: str + """ + self.duplicates = duplicates + + def __str__(self): + return ( + f"[{self.error_code}] " + + _( + "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. ") + + _("Conflicting packages: {duplicates}").format(duplicates=self.duplicates) + ) + + +class UnsupportedProtocolError(PulpException): + """ + Raised when an unsupported protocol is used for syncing. + """ + + error_code = "PLPY0004" + + def __init__(self, protocol): + """ + :param protocol: The unsupported protocol + :type protocol: str + """ + 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) diff --git a/pulp_python/app/models.py b/pulp_python/app/models.py index eda9a7d1..b9aa8499 100644 --- a/pulp_python/app/models.py +++ b/pulp_python/app/models.py @@ -11,7 +11,6 @@ BEFORE_SAVE, hook, ) -from rest_framework.serializers import ValidationError from pulpcore.plugin.models import ( AutoAddObjPermsMixin, Content, @@ -23,6 +22,7 @@ from pulpcore.plugin.responses import ArtifactResponse from pathlib import PurePath +from .exceptions import PackageSubstitutionError from .provenance import Provenance from .utils import ( artifact_to_python_content_data, @@ -407,14 +407,10 @@ 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}" - ) + raise PackageSubstitutionError(duplicates) diff --git a/pulp_python/app/serializers.py b/pulp_python/app/serializers.py index dc4355f9..790e528c 100644 --- a/pulp_python/app/serializers.py +++ b/pulp_python/app/serializers.py @@ -8,14 +8,16 @@ from packaging.requirements import Requirement from rest_framework import serializers from pypi_attestations import AttestationError -from pydantic import TypeAdapter, ValidationError +from pydantic import TypeAdapter, ValidationError as PydanticValidationError from urllib.parse import urljoin +from pulpcore.plugin.exceptions import DigestValidationError, ValidationError from pulpcore.plugin import models as core_models from pulpcore.plugin import serializers as core_serializers from pulpcore.plugin.util import get_domain, get_prn, get_current_authenticated_user from pulp_python.app import models as python_models +from pulp_python.app.exceptions import AttestationVerificationError, ProvenanceVerificationError from pulp_python.app.provenance import ( Attestation, Provenance, @@ -374,7 +376,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 @@ -387,9 +389,7 @@ def handle_attestations(self, filename, sha256, attestations, offline=True): try: verify_provenance(filename, sha256, provenance, offline=offline) except AttestationError as e: - raise serializers.ValidationError( - {"attestations": _("Attestations failed verification: {}".format(e))} - ) + raise AttestationVerificationError(str(e)) return provenance.model_dump(mode="json") def deferred_validate(self, data): @@ -408,13 +408,13 @@ def deferred_validate(self, data): try: filename = data["relative_path"] except KeyError: - raise serializers.ValidationError(detail={"relative_path": _("This field is required")}) + raise ValidationError(_("This field is required: relative_path")) artifact = data["artifact"] try: _data = artifact_to_python_content_data(filename, artifact, domain=get_domain()) except ValueError: - raise serializers.ValidationError( + raise ValidationError( _( "Extension on {} is not a valid python extension " "(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)" @@ -422,12 +422,9 @@ def deferred_validate(self, data): ) if data.get("sha256") and data["sha256"] != artifact.sha256: - raise serializers.ValidationError( - detail={ - "sha256": _( - "The uploaded artifact's sha256 checksum does not match the one provided" - ) - } + raise DigestValidationError( + actual=artifact.sha256, + expected=data["sha256"], ) data.update(_data) @@ -641,15 +638,13 @@ 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: - raise serializers.ValidationError( - _("The uploaded provenance is not valid: {}".format(e)) - ) + except PydanticValidationError as e: + raise ValidationError(_("The uploaded provenance is not valid: {}".format(e))) if data.pop("verify"): try: verify_provenance(data["package"].filename, data["package"].sha256, provenance) except AttestationError as e: - raise serializers.ValidationError(_("Provenance verification failed: {}".format(e))) + raise ProvenanceVerificationError(str(e)) return data def retrieve(self, validated_data): diff --git a/pulp_python/app/tasks/sync.py b/pulp_python/app/tasks/sync.py index 1ceaf207..83797219 100644 --- a/pulp_python/app/tasks/sync.py +++ b/pulp_python/app/tasks/sync.py @@ -17,6 +17,7 @@ Stage, ) +from pulp_python.app.exceptions import UnsupportedProtocolError from pulp_python.app.models import ( PythonPackageContent, PythonRemote, @@ -117,7 +118,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/tests/functional/api/test_attestations.py b/pulp_python/tests/functional/api/test_attestations.py index 2fb652f2..abfde174 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 "[PLPY0001]" in e.value.task.error["description"] # Test creating a provenance without verifying provenance = python_bindings.ContentProvenanceApi.create( @@ -239,4 +239,4 @@ def test_bad_attestation_upload(python_bindings, twine_package, monitor_task): with pytest.raises(PulpTaskError) as e: monitor_task(task) assert e.value.task.state == "failed" - assert "Attestations failed verification" in e.value.task.error["description"] + assert "[PLPY0002]" in e.value.task.error["description"] 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 fe049b3b..9dfc502b 100644 --- a/pulp_python/tests/functional/api/test_crud_content_unit.py +++ b/pulp_python/tests/functional/api/test_crud_content_unit.py @@ -112,7 +112,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" + msg = "[PLP0003]" assert msg in e.value.task.error["description"] @@ -241,6 +241,7 @@ def test_disallow_package_substitution( repository=repo.pulp_href, **content_body2 ) monitor_task(response.task) + assert "[PLPY0003]" in exc.value.task.error["description"] assert msg1 in exc.value.task.error["description"] assert msg2 in exc.value.task.error["description"] @@ -257,6 +258,7 @@ def test_disallow_package_substitution( body = {"add_content_units": [content2.pulp_href], "base_version": repo.latest_version_href} with pytest.raises(PulpTaskError) as exc: monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task) + assert "[PLPY0003]" in exc.value.task.error["description"] assert msg1 in exc.value.task.error["description"] assert msg2 in exc.value.task.error["description"]