-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
feat(preprod): Add preprod artifact upload endpoint #92528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b3a7d58
new preprod artifact upload endpoint
NicoHinderling 0c44c72
trevor feedback
NicoHinderling 648a44c
initial comments from colton
NicoHinderling 560e97a
move assemble test as well
NicoHinderling 4d51928
add transaction + update tests
NicoHinderling 13cf722
fix patch typo in tests
NicoHinderling 1f7c4d7
fix type issue in test file
NicoHinderling b113c18
fix the codeowner issue too
NicoHinderling File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
src/sentry/preprod/api/endpoints/organization_preprod_artifact_assemble.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import jsonschema | ||
import orjson | ||
import sentry_sdk | ||
from rest_framework.request import Request | ||
from rest_framework.response import Response | ||
|
||
from sentry.api.api_owners import ApiOwner | ||
from sentry.api.api_publish_status import ApiPublishStatus | ||
from sentry.api.base import region_silo_endpoint | ||
from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission | ||
from sentry.debug_files.upload import find_missing_chunks | ||
from sentry.models.orgauthtoken import is_org_auth_token_auth, update_org_auth_token_last_used | ||
from sentry.preprod.tasks import assemble_preprod_artifact | ||
from sentry.tasks.assemble import ( | ||
AssembleTask, | ||
ChunkFileState, | ||
get_assemble_status, | ||
set_assemble_status, | ||
) | ||
|
||
|
||
def validate_preprod_artifact_schema(request_body: bytes) -> tuple[dict, str | None]: | ||
""" | ||
Validate the JSON schema for preprod artifact assembly requests. | ||
|
||
Returns: | ||
tuple: (parsed_data, error_message) where error_message is None if validation succeeds | ||
""" | ||
schema = { | ||
"type": "object", | ||
"properties": { | ||
"checksum": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, | ||
"chunks": { | ||
"type": "array", | ||
"items": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, | ||
}, | ||
# Optional metadata | ||
"git_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, | ||
"build_configuration": {"type": "string"}, | ||
}, | ||
"required": ["checksum", "chunks"], | ||
"additionalProperties": False, | ||
} | ||
|
||
error_messages = { | ||
"checksum": "The checksum field is required and must be a 40-character hexadecimal string.", | ||
"chunks": "The chunks field is required and must be provided as an array of 40-character hexadecimal strings.", | ||
"git_sha": "The git_sha field must be a 40-character hexadecimal string.", | ||
"build_configuration": "The build_configuration field must be a string.", | ||
} | ||
|
||
try: | ||
data = orjson.loads(request_body) | ||
jsonschema.validate(data, schema) | ||
return data, None | ||
except jsonschema.ValidationError as e: | ||
error_message = e.message | ||
# Get the field from the path if available | ||
if e.path: | ||
if field := e.path[0]: | ||
error_message = error_messages.get(str(field), error_message) | ||
return {}, error_message | ||
except (orjson.JSONDecodeError, TypeError): | ||
return {}, "Invalid json body" | ||
|
||
|
||
@region_silo_endpoint | ||
class ProjectPreprodArtifactAssembleEndpoint(ProjectEndpoint): | ||
owner = ApiOwner.EMERGE_TOOLS | ||
publish_status = { | ||
"POST": ApiPublishStatus.EXPERIMENTAL, | ||
} | ||
permission_classes = (ProjectReleasePermission,) | ||
|
||
def post(self, request: Request, project) -> Response: | ||
""" | ||
Assembles a preprod artifact (mobile build, etc.) and stores it in the database. | ||
""" | ||
with sentry_sdk.start_span(op="preprod_artifact.assemble"): | ||
data, error_message = validate_preprod_artifact_schema(request.body) | ||
if error_message: | ||
return Response({"error": error_message}, status=400) | ||
|
||
checksum = data.get("checksum") | ||
chunks = data.get("chunks", []) | ||
|
||
# Check if all requested chunks have been uploaded | ||
missing_chunks = find_missing_chunks(project.organization_id, set(chunks)) | ||
if missing_chunks: | ||
return Response( | ||
{ | ||
"state": ChunkFileState.NOT_FOUND, | ||
"missingChunks": missing_chunks, | ||
} | ||
) | ||
|
||
# Check current assembly status | ||
state, detail = get_assemble_status(AssembleTask.PREPROD_ARTIFACT, project.id, checksum) | ||
if state is not None: | ||
return Response({"state": state, "detail": detail, "missingChunks": []}) | ||
|
||
# There is neither a known file nor a cached state, so we will | ||
# have to create a new file. Assure that there are checksums. | ||
# If not, we assume this is a poll and report NOT_FOUND | ||
if not chunks: | ||
return Response({"state": ChunkFileState.NOT_FOUND, "missingChunks": []}) | ||
|
||
set_assemble_status( | ||
AssembleTask.PREPROD_ARTIFACT, project.id, checksum, ChunkFileState.CREATED | ||
) | ||
|
||
assemble_preprod_artifact.apply_async( | ||
kwargs={ | ||
"org_id": project.organization_id, | ||
"project_id": project.id, | ||
"checksum": checksum, | ||
"chunks": chunks, | ||
"git_sha": data.get("git_sha"), | ||
"build_configuration": data.get("build_configuration"), | ||
} | ||
) | ||
|
||
if is_org_auth_token_auth(request.auth): | ||
update_org_auth_token_last_used(request.auth, [project.id]) | ||
|
||
return Response({"state": ChunkFileState.CREATED, "missingChunks": []}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
from __future__ import annotations | ||
|
||
import datetime | ||
import logging | ||
import uuid | ||
|
||
from django.db import router, transaction | ||
|
||
from sentry.models.organization import Organization | ||
from sentry.models.project import Project | ||
from sentry.silo.base import SiloMode | ||
from sentry.tasks.assemble import AssembleTask, ChunkFileState, assemble_file, set_assemble_status | ||
from sentry.tasks.base import instrumented_task | ||
from sentry.taskworker.config import TaskworkerConfig | ||
from sentry.taskworker.namespaces import attachments_tasks | ||
from sentry.utils.sdk import bind_organization_context | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@instrumented_task( | ||
name="sentry.preprod.tasks.assemble_preprod_artifact", | ||
queue="assemble", | ||
silo_mode=SiloMode.REGION, | ||
taskworker_config=TaskworkerConfig( | ||
namespace=attachments_tasks, | ||
processing_deadline_duration=30, | ||
), | ||
) | ||
def assemble_preprod_artifact( | ||
org_id, | ||
project_id, | ||
checksum, | ||
chunks, | ||
git_sha=None, | ||
build_configuration=None, | ||
**kwargs, | ||
) -> None: | ||
""" | ||
Creates a preprod artifact from uploaded chunks. | ||
""" | ||
from sentry.preprod.models import PreprodArtifact, PreprodBuildConfiguration | ||
|
||
logger.info( | ||
"Starting preprod artifact assembly", | ||
extra={ | ||
"timestamp": datetime.datetime.now().isoformat(), | ||
"project_id": project_id, | ||
"organization_id": org_id, | ||
}, | ||
) | ||
|
||
try: | ||
organization = Organization.objects.get_from_cache(pk=org_id) | ||
project = Project.objects.get(id=project_id, organization=organization) | ||
bind_organization_context(organization) | ||
|
||
set_assemble_status( | ||
AssembleTask.PREPROD_ARTIFACT, org_id, checksum, ChunkFileState.ASSEMBLING | ||
) | ||
|
||
assemble_result = assemble_file( | ||
task=AssembleTask.PREPROD_ARTIFACT, | ||
org_or_project=organization, | ||
name=f"preprod-artifact-{uuid.uuid4().hex}", | ||
checksum=checksum, | ||
chunks=chunks, | ||
file_type="preprod.artifact", | ||
) | ||
|
||
if assemble_result is None: | ||
return | ||
|
||
with transaction.atomic(router.db_for_write(PreprodArtifact)): | ||
build_config = None | ||
if build_configuration: | ||
build_config, _ = PreprodBuildConfiguration.objects.get_or_create( | ||
project=project, | ||
name=build_configuration, | ||
) | ||
|
||
# Create PreprodArtifact record | ||
preprod_artifact = PreprodArtifact.objects.create( | ||
project=project, | ||
file_id=assemble_result.bundle.id, | ||
build_configuration=build_config, | ||
state=PreprodArtifact.ArtifactState.UPLOADED, | ||
) | ||
|
||
logger.info( | ||
"Created preprod artifact", | ||
extra={ | ||
"preprod_artifact_id": preprod_artifact.id, | ||
"project_id": project_id, | ||
"organization_id": org_id, | ||
}, | ||
) | ||
|
||
logger.info( | ||
"Finished preprod artifact assembly", | ||
extra={ | ||
"timestamp": datetime.datetime.now().isoformat(), | ||
"project_id": project_id, | ||
"organization_id": org_id, | ||
}, | ||
) | ||
|
||
# where next set of changes will happen | ||
# TODO: Trigger artifact processing (size analysis, etc.) | ||
# This is where you'd add logic to: | ||
# 1. create_or_update a new row in the Commit table as well (once base_sha is added as a column to it) | ||
# 2. Detect artifact type (iOS/Android/etc.) | ||
# 3. Queue processing tasks | ||
# 4. Update state to PROCESSED when done (also update the date_built value to reflect when the artifact was built, among other fields) | ||
|
||
except Exception as e: | ||
logger.exception( | ||
"Failed to assemble preprod artifact", | ||
extra={ | ||
"project_id": project_id, | ||
"organization_id": org_id, | ||
}, | ||
) | ||
set_assemble_status( | ||
AssembleTask.PREPROD_ARTIFACT, | ||
org_id, | ||
checksum, | ||
ChunkFileState.ERROR, | ||
detail=str(e), | ||
) | ||
else: | ||
set_assemble_status(AssembleTask.PREPROD_ARTIFACT, org_id, checksum, ChunkFileState.OK) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.