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
228 changes: 3 additions & 225 deletions src/sentry/integrations/aws_lambda/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from botocore.exceptions import ClientError
from django.http.request import HttpRequest
from django.http.response import HttpResponseBase
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.fields import CharField, ChoiceField, IntegerField, ListField
Expand All @@ -26,14 +25,10 @@
from sentry.integrations.models.integration import Integration
from sentry.integrations.models.organization_integration import OrganizationIntegration
from sentry.integrations.pipeline import IntegrationPipeline
from sentry.organizations.services.organization import organization_service
from sentry.organizations.services.organization.model import RpcOrganization
from sentry.pipeline.types import PipelineStepResult
from sentry.pipeline.views.base import ApiPipelineSteps, PipelineView, render_react_view
from sentry.projects.services.project import project_service
from sentry.pipeline.views.base import ApiPipelineSteps
from sentry.silo.base import control_silo_function
from sentry.users.models.user import User
from sentry.users.services.user.serial import serialize_rpc_user
from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor
from sentry.utils.sdk import capture_exception

Expand Down Expand Up @@ -412,13 +407,8 @@ class AwsLambdaIntegrationProvider(IntegrationProvider):
integration_cls = AwsLambdaIntegration
features = frozenset([IntegrationFeatures.SERVERLESS])

def get_pipeline_views(self) -> list[PipelineView[IntegrationPipeline]]:
return [
AwsLambdaProjectSelectPipelineView(),
AwsLambdaCloudFormationPipelineView(),
AwsLambdaListFunctionsPipelineView(),
AwsLambdaSetupLayerPipelineView(),
]
def get_pipeline_views(self) -> list:
return []

def get_pipeline_api_steps(self) -> ApiPipelineSteps[IntegrationPipeline]:
return [
Expand Down Expand Up @@ -475,215 +465,3 @@ def post_install(
organization_id=organization.id, integration=integration
):
oi.update(config={"default_project_id": default_project_id})


class AwsLambdaProjectSelectPipelineView:
def dispatch(self, request: HttpRequest, pipeline: IntegrationPipeline) -> HttpResponseBase:
# if we have the projectId, go to the next step
if "projectId" in request.GET:
pipeline.bind_state("project_id", request.GET["projectId"])
return pipeline.next_step()

assert pipeline.organization is not None
organization = pipeline.organization
projects = organization.projects

# if only one project, automatically use that
if len(projects) == 1:
pipeline.bind_state("skipped_project_select", True)
pipeline.bind_state("project_id", projects[0].id)
return pipeline.next_step()

projects = sorted(projects, key=lambda p: p.slug)
serialized_projects = project_service.serialize_many(
organization_id=organization.id,
filter=dict(project_ids=[p.id for p in projects]),
)
return render_react_view(
request, "awsLambdaProjectSelect", {"projects": serialized_projects}
)


class AwsLambdaCloudFormationPipelineView:
def dispatch(self, request: HttpRequest, pipeline: IntegrationPipeline) -> HttpResponseBase:
curr_step = 0 if pipeline.fetch_state("skipped_project_select") else 1

def render_response(error=None):
assert pipeline.organization is not None
serialized_organization = organization_service.serialize_organization(
id=pipeline.organization.id,
as_user=(
serialize_rpc_user(request.user) if isinstance(request.user, User) else None
),
)
template_url = options.get("aws-lambda.cloudformation-url")
context = {
"baseCloudformationUrl": "https://console.aws.amazon.com/cloudformation/home#/stacks/create/review",
"templateUrl": template_url,
"stackName": "Sentry-Monitoring-Stack",
"regionList": ALL_AWS_REGIONS,
"accountNumber": pipeline.fetch_state("account_number"),
"region": pipeline.fetch_state("region"),
"error": error,
"initialStepNumber": curr_step,
"organization": serialized_organization,
"awsExternalId": pipeline.fetch_state("aws_external_id"),
}
return render_react_view(request, "awsLambdaCloudformation", context)

# form submit adds accountNumber to GET parameters
if "accountNumber" in request.GET:
data = request.GET

# load parameters post request
account_number = data["accountNumber"]
region = data["region"]
aws_external_id = data["awsExternalId"]

pipeline.bind_state("account_number", account_number)
pipeline.bind_state("region", region)
pipeline.bind_state("aws_external_id", aws_external_id)

# now validate the arn works
try:
gen_aws_client(account_number, region, aws_external_id)
except ClientError:
return render_response(
_("Please validate the Cloudformation stack was created successfully")
)
except ConfigurationError:
# if we have a configuration error, we should blow up the pipeline
raise
except Exception as e:
logger.warning(
"AwsLambdaCloudFormationPipelineView.unexpected_error",
extra={"error": str(e)},
)
return render_response(_("Unknown error"))

# if no error, continue
return pipeline.next_step()

return render_response()


class AwsLambdaListFunctionsPipelineView:
def dispatch(self, request: HttpRequest, pipeline: IntegrationPipeline) -> HttpResponseBase:
if request.method == "POST":
raw_data = request.POST
data = {}
for key, val in raw_data.items():
# form posts have string values for booleans and this form only sends booleans
data[key] = val == "true"
pipeline.bind_state("enabled_lambdas", data)
return pipeline.next_step()

account_number = pipeline.fetch_state("account_number")
region = pipeline.fetch_state("region")
aws_external_id = pipeline.fetch_state("aws_external_id")

lambda_client = gen_aws_client(account_number, region, aws_external_id)

lambda_functions = get_supported_functions(lambda_client)

curr_step = 2 if pipeline.fetch_state("skipped_project_select") else 3

return render_react_view(
request,
"awsLambdaFunctionSelect",
{"lambdaFunctions": lambda_functions, "initialStepNumber": curr_step},
)


class AwsLambdaSetupLayerPipelineView:
def dispatch(self, request: HttpRequest, pipeline: IntegrationPipeline) -> HttpResponseBase:
if "finish_pipeline" in request.GET:
return pipeline.finish_pipeline()

assert pipeline.organization is not None
organization = pipeline.organization

account_number = pipeline.fetch_state("account_number")
region = pipeline.fetch_state("region")

project_id = pipeline.fetch_state("project_id")
aws_external_id = pipeline.fetch_state("aws_external_id")
enabled_lambdas = pipeline.fetch_state("enabled_lambdas")
assert enabled_lambdas is not None

sentry_project_dsn = get_dsn_for_project(organization.id, project_id)

lambda_client = gen_aws_client(account_number, region, aws_external_id)

lambda_functions = get_supported_functions(lambda_client)
lambda_functions.sort(key=lambda x: x["FunctionName"].lower())

def is_lambda_enabled(function):
name = function["FunctionName"]
# check to see if the user wants to enable this function
return enabled_lambdas.get(name)

lambda_functions = filter(is_lambda_enabled, lambda_functions)

def _enable_lambda(function):
try:
enable_single_lambda(lambda_client, function, sentry_project_dsn)
return (True, function, None)
except Exception as e:
return (False, function, e)

failures = []
success_count = 0

with ContextPropagatingThreadPoolExecutor(
max_workers=options.get("aws-lambda.thread-count")
) as _lambda_setup_thread_pool:
# use threading here to parallelize requests
# no timeout on the thread since the underlying request will time out
# if it takes too long
for success, function, e in _lambda_setup_thread_pool.map(
_enable_lambda, lambda_functions
):
name = function["FunctionName"]
if success:
success_count += 1
else:
# need to make sure we catch any error to continue to the next function
err_message: str | _StrPromise = str(e)
is_custom_err, err_message = get_sentry_err_message(err_message)
if not is_custom_err:
capture_exception(e)
err_message = _("Unknown Error")
failures.append({"name": function["FunctionName"], "error": err_message})
logger.info(
"update_function_configuration.error",
extra={
"organization_id": organization.id,
"lambda_name": name,
"account_number": account_number,
"region": region,
"error": str(e),
},
)

analytics.record(
IntegrationServerlessSetup(
user_id=request.user.id,
organization_id=organization.id,
integration="aws_lambda",
success_count=success_count,
failure_count=len(failures),
)
)

# if we have failures, show them to the user
# otherwise, finish

if failures:
return render_react_view(
request,
"awsLambdaFailureDetails",
{"lambdaFunctionFailures": failures, "successCount": success_count},
)
else:
return pipeline.finish_pipeline()
Loading
Loading