Skip to content

Commit f225296

Browse files
runningcodeclaude
andcommitted
feat(preprod): Add distribution error endpoint for launchpad
Add a dedicated endpoint for launchpad to report distribution processing errors back to the monolith, mirroring the existing size analysis endpoint pattern. This sets installable_app_error_code and installable_app_error_message on the PreprodArtifact. Refs EME-842, EME-422 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a1d2401 commit f225296

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
import orjson
6+
import pydantic
7+
from pydantic import BaseModel
8+
from rest_framework import serializers
9+
from rest_framework.request import Request
10+
from rest_framework.response import Response
11+
12+
from sentry.api.api_owners import ApiOwner
13+
from sentry.api.api_publish_status import ApiPublishStatus
14+
from sentry.api.base import internal_region_silo_endpoint
15+
from sentry.models.project import Project
16+
from sentry.preprod.api.bases.preprod_artifact_endpoint import PreprodArtifactEndpoint
17+
from sentry.preprod.authentication import (
18+
LaunchpadRpcPermission,
19+
LaunchpadRpcSignatureAuthentication,
20+
)
21+
from sentry.preprod.models import PreprodArtifact
22+
23+
logger = logging.getLogger(__name__)
24+
25+
26+
class PutDistribution(BaseModel):
27+
error_code: int
28+
error_message: str
29+
30+
31+
@internal_region_silo_endpoint
32+
class ProjectPreprodDistributionEndpoint(PreprodArtifactEndpoint):
33+
owner = ApiOwner.EMERGE_TOOLS
34+
publish_status = {
35+
"PUT": ApiPublishStatus.PRIVATE,
36+
}
37+
authentication_classes = (LaunchpadRpcSignatureAuthentication,)
38+
permission_classes = (LaunchpadRpcPermission,)
39+
40+
def put(
41+
self,
42+
request: Request,
43+
project: Project,
44+
head_artifact_id: int,
45+
head_artifact: PreprodArtifact,
46+
) -> Response:
47+
try:
48+
j = orjson.loads(request.body)
49+
except orjson.JSONDecodeError:
50+
raise serializers.ValidationError("Invalid json")
51+
try:
52+
put = PutDistribution(**j)
53+
except pydantic.ValidationError:
54+
logger.exception("Could not parse PutDistribution")
55+
raise serializers.ValidationError("Could not parse PutDistribution")
56+
57+
head_artifact.installable_app_error_code = put.error_code
58+
head_artifact.installable_app_error_message = put.error_message
59+
head_artifact.save(
60+
update_fields=[
61+
"installable_app_error_code",
62+
"installable_app_error_message",
63+
"date_updated",
64+
]
65+
)
66+
67+
return Response({"artifactId": str(head_artifact.id)})

src/sentry/preprod/api/endpoints/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from .project_preprod_artifact_update import ProjectPreprodArtifactUpdateEndpoint
3939
from .project_preprod_build_details import ProjectPreprodBuildDetailsEndpoint
4040
from .project_preprod_check_for_updates import ProjectPreprodArtifactCheckForUpdatesEndpoint
41+
from .project_preprod_distribution import ProjectPreprodDistributionEndpoint
4142
from .project_preprod_size import (
4243
ProjectPreprodSizeEndpoint,
4344
ProjectPreprodSizeWithIdentifierEndpoint,
@@ -204,6 +205,11 @@
204205
ProjectPreprodArtifactAssembleGenericEndpoint.as_view(),
205206
name="sentry-api-0-project-preprod-artifact-assemble-generic",
206207
),
208+
re_path(
209+
r"^(?P<organization_id_or_slug>[^/]+)/(?P<project_id_or_slug>[^/]+)/files/preprodartifacts/(?P<head_artifact_id>[^/]+)/distribution/$",
210+
ProjectPreprodDistributionEndpoint.as_view(),
211+
name="sentry-api-0-project-preprod-artifact-distribution",
212+
),
207213
re_path(
208214
r"^(?P<organization_id_or_slug>[^/]+)/(?P<project_id_or_slug>[^/]+)/files/preprodartifacts/(?P<head_artifact_id>[^/]+)/size/$",
209215
ProjectPreprodSizeEndpoint.as_view(),
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import orjson
2+
from django.test import override_settings
3+
4+
from sentry.preprod.models import PreprodArtifact
5+
from sentry.testutils.auth import generate_service_request_signature
6+
from sentry.testutils.cases import TestCase
7+
8+
SHARED_SECRET_FOR_TESTS = "test-secret-key"
9+
10+
11+
class ProjectPreprodDistributionEndpointTest(TestCase):
12+
def setUp(self) -> None:
13+
super().setUp()
14+
self.file = self.create_file(name="test_artifact.apk", type="application/octet-stream")
15+
self.artifact = self.create_preprod_artifact(
16+
project=self.project,
17+
file_id=self.file.id,
18+
state=PreprodArtifact.ArtifactState.PROCESSED,
19+
)
20+
21+
def _put(self, data, secret=SHARED_SECRET_FOR_TESTS):
22+
url = f"/api/0/internal/{self.organization.slug}/{self.project.slug}/files/preprodartifacts/{self.artifact.id}/distribution/"
23+
signature = generate_service_request_signature(url, data, [secret], "Launchpad")
24+
return self.client.put(
25+
url,
26+
data=data,
27+
content_type="application/json",
28+
HTTP_AUTHORIZATION=f"rpcsignature {signature}",
29+
)
30+
31+
@override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS])
32+
def test_bad_auth(self) -> None:
33+
response = self._put(b"{}", secret="wrong secret")
34+
assert response.status_code == 401
35+
36+
@override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS])
37+
def test_missing_fields(self) -> None:
38+
response = self._put(b"{}")
39+
assert response.status_code == 400
40+
41+
@override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS])
42+
def test_bad_json(self) -> None:
43+
response = self._put(b"{")
44+
assert response.status_code == 400
45+
46+
@override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS])
47+
def test_set_error(self) -> None:
48+
response = self._put(
49+
orjson.dumps({"error_code": 3, "error_message": "Unsupported artifact type"})
50+
)
51+
52+
assert response.status_code == 200
53+
self.artifact.refresh_from_db()
54+
assert (
55+
self.artifact.installable_app_error_code
56+
== PreprodArtifact.InstallableAppErrorCode.PROCESSING_ERROR
57+
)
58+
assert self.artifact.installable_app_error_message == "Unsupported artifact type"
59+
60+
@override_settings(LAUNCHPAD_RPC_SHARED_SECRET=[SHARED_SECRET_FOR_TESTS])
61+
def test_requires_launchpad_rpc_authentication(self) -> None:
62+
self.login_as(self.user)
63+
64+
url = f"/api/0/internal/{self.organization.slug}/{self.project.slug}/files/preprodartifacts/{self.artifact.id}/distribution/"
65+
response = self.client.put(
66+
url,
67+
data=orjson.dumps({"error_code": 3, "error_message": "some error"}),
68+
content_type="application/json",
69+
)
70+
71+
assert response.status_code == 401

0 commit comments

Comments
 (0)