88from django .http .request import HttpRequest
99from django .http .response import HttpResponseBase
1010from django .utils .translation import gettext_lazy as _
11+ from rest_framework import serializers
12+ from rest_framework .fields import CharField , ChoiceField , IntegerField , ListField
1113
1214from sentry import analytics , options
1315from sentry .analytics .events .integration_serverless_setup import IntegrationServerlessSetup
16+ from sentry .api .serializers .rest_framework .base import CamelSnakeSerializer
1417from sentry .integrations .base import (
1518 FeatureDescription ,
1619 IntegrationData ,
2528from sentry .integrations .pipeline import IntegrationPipeline
2629from sentry .organizations .services .organization import organization_service
2730from sentry .organizations .services .organization .model import RpcOrganization
28- from sentry .pipeline .views .base import PipelineView , render_react_view
31+ from sentry .pipeline .types import PipelineStepResult
32+ from sentry .pipeline .views .base import ApiPipelineSteps , PipelineView , render_react_view
2933from sentry .projects .services .project import project_service
3034from sentry .silo .base import control_silo_function
3135from sentry .users .models .user import User
@@ -198,6 +202,209 @@ def update_function_to_latest_version(self, target):
198202 return self .get_serialized_lambda_function (target )
199203
200204
205+ class ProjectSelectSerializer (CamelSnakeSerializer ):
206+ project_id = IntegerField (required = True )
207+
208+
209+ class ProjectSelectApiStep :
210+ step_name = "project_select"
211+
212+ def get_step_data (self , pipeline : IntegrationPipeline , request : HttpRequest ) -> dict [str , Any ]:
213+ return {}
214+
215+ def get_serializer_cls (self ) -> type :
216+ return ProjectSelectSerializer
217+
218+ def handle_post (
219+ self ,
220+ validated_data : dict [str , Any ],
221+ pipeline : IntegrationPipeline ,
222+ request : HttpRequest ,
223+ ) -> PipelineStepResult :
224+ project_id = validated_data ["project_id" ]
225+
226+ assert pipeline .organization is not None
227+ valid_project_ids = {p .id for p in pipeline .organization .projects }
228+ if project_id not in valid_project_ids :
229+ return PipelineStepResult .error ("Invalid project" )
230+
231+ pipeline .bind_state ("project_id" , project_id )
232+ return PipelineStepResult .advance ()
233+
234+
235+ class CloudFormationSerializer (CamelSnakeSerializer ):
236+ account_number = CharField (required = True )
237+ region = ChoiceField (choices = [(r , r ) for r in ALL_AWS_REGIONS ], required = True )
238+ aws_external_id = CharField (required = True )
239+
240+ def validate_account_number (self , value : str ) -> str :
241+ if not value .isdigit () or len (value ) != 12 :
242+ raise serializers .ValidationError ("Must be a 12-digit AWS account number" )
243+ return value
244+
245+
246+ class CloudFormationApiStep :
247+ step_name = "cloudformation"
248+
249+ def get_step_data (self , pipeline : IntegrationPipeline , request : HttpRequest ) -> dict [str , Any ]:
250+ template_url = options .get ("aws-lambda.cloudformation-url" )
251+ return {
252+ "baseCloudformationUrl" : "https://console.aws.amazon.com/cloudformation/home#/stacks/create/review" ,
253+ "templateUrl" : template_url ,
254+ "stackName" : "Sentry-Monitoring-Stack" ,
255+ "regionList" : ALL_AWS_REGIONS ,
256+ }
257+
258+ def get_serializer_cls (self ) -> type :
259+ return CloudFormationSerializer
260+
261+ def handle_post (
262+ self ,
263+ validated_data : dict [str , Any ],
264+ pipeline : IntegrationPipeline ,
265+ request : HttpRequest ,
266+ ) -> PipelineStepResult :
267+ account_number = validated_data ["account_number" ]
268+ region = validated_data ["region" ]
269+ aws_external_id = validated_data ["aws_external_id" ]
270+
271+ pipeline .bind_state ("account_number" , account_number )
272+ pipeline .bind_state ("region" , region )
273+ pipeline .bind_state ("aws_external_id" , aws_external_id )
274+
275+ try :
276+ gen_aws_client (account_number , region , aws_external_id )
277+ except ClientError :
278+ return PipelineStepResult .error (
279+ "Please validate the Cloudformation stack was created successfully"
280+ )
281+ except ConfigurationError :
282+ raise
283+ except Exception as e :
284+ logger .warning (
285+ "CloudFormationApiStep.unexpected_error" ,
286+ extra = {"error" : str (e )},
287+ )
288+ return PipelineStepResult .error ("Unknown error" )
289+
290+ return PipelineStepResult .advance ()
291+
292+
293+ class FunctionSelectSerializer (CamelSnakeSerializer ):
294+ enabled_functions = ListField (child = CharField (), required = True )
295+
296+
297+ class InstrumentationApiStep :
298+ step_name = "instrumentation"
299+
300+ def get_step_data (self , pipeline : IntegrationPipeline , request : HttpRequest ) -> dict [str , Any ]:
301+ account_number = pipeline .fetch_state ("account_number" )
302+ region = pipeline .fetch_state ("region" )
303+ aws_external_id = pipeline .fetch_state ("aws_external_id" )
304+
305+ lambda_client = gen_aws_client (account_number , region , aws_external_id )
306+ lambda_functions = get_supported_functions (lambda_client )
307+ lambda_functions .sort (key = lambda x : x ["FunctionName" ].lower ())
308+
309+ return {
310+ "functions" : [
311+ {
312+ "name" : fn ["FunctionName" ],
313+ "runtime" : fn ["Runtime" ],
314+ "description" : fn .get ("Description" , "" ),
315+ }
316+ for fn in lambda_functions
317+ ]
318+ }
319+
320+ def get_serializer_cls (self ) -> type :
321+ return FunctionSelectSerializer
322+
323+ def handle_post (
324+ self ,
325+ validated_data : dict [str , Any ],
326+ pipeline : IntegrationPipeline ,
327+ request : HttpRequest ,
328+ ) -> PipelineStepResult :
329+ assert pipeline .organization is not None
330+ organization = pipeline .organization
331+
332+ account_number = pipeline .fetch_state ("account_number" )
333+ region = pipeline .fetch_state ("region" )
334+ project_id = pipeline .fetch_state ("project_id" )
335+ aws_external_id = pipeline .fetch_state ("aws_external_id" )
336+
337+ enabled_functions = validated_data ["enabled_functions" ]
338+ enabled_lambdas = {name : True for name in enabled_functions }
339+
340+ sentry_project_dsn = get_dsn_for_project (organization .id , project_id )
341+
342+ lambda_client = gen_aws_client (account_number , region , aws_external_id )
343+ lambda_functions = get_supported_functions (lambda_client )
344+ lambda_functions .sort (key = lambda x : x ["FunctionName" ].lower ())
345+
346+ lambda_functions = [
347+ fn for fn in lambda_functions if enabled_lambdas .get (fn ["FunctionName" ])
348+ ]
349+
350+ def _enable_lambda (function ):
351+ try :
352+ enable_single_lambda (lambda_client , function , sentry_project_dsn )
353+ return (True , function , None )
354+ except Exception as e :
355+ return (False , function , e )
356+
357+ failures : list [dict [str , Any ]] = []
358+ success_count = 0
359+
360+ with ContextPropagatingThreadPoolExecutor (
361+ max_workers = options .get ("aws-lambda.thread-count" )
362+ ) as _lambda_setup_thread_pool :
363+ for success , function , e in _lambda_setup_thread_pool .map (
364+ _enable_lambda , lambda_functions
365+ ):
366+ name = function ["FunctionName" ]
367+ if success :
368+ success_count += 1
369+ else :
370+ err_message : str | _StrPromise = str (e )
371+ is_custom_err , err_message = get_sentry_err_message (err_message )
372+ if not is_custom_err :
373+ capture_exception (e )
374+ err_message = _ ("Unknown Error" )
375+ failures .append ({"name" : name , "error" : str (err_message )})
376+ logger .info (
377+ "update_function_configuration.error" ,
378+ extra = {
379+ "organization_id" : organization .id ,
380+ "lambda_name" : name ,
381+ "account_number" : account_number ,
382+ "region" : region ,
383+ "error" : str (e ),
384+ },
385+ )
386+
387+ analytics .record (
388+ IntegrationServerlessSetup (
389+ user_id = request .user .id ,
390+ organization_id = organization .id ,
391+ integration = "aws_lambda" ,
392+ success_count = success_count ,
393+ failure_count = len (failures ),
394+ )
395+ )
396+
397+ if failures :
398+ return PipelineStepResult .stay (
399+ data = {
400+ "failures" : failures ,
401+ "successCount" : success_count ,
402+ }
403+ )
404+
405+ return PipelineStepResult .advance ()
406+
407+
201408class AwsLambdaIntegrationProvider (IntegrationProvider ):
202409 key = "aws_lambda"
203410 name = "AWS Lambda"
@@ -213,6 +420,13 @@ def get_pipeline_views(self) -> list[PipelineView[IntegrationPipeline]]:
213420 AwsLambdaSetupLayerPipelineView (),
214421 ]
215422
423+ def get_pipeline_api_steps (self ) -> ApiPipelineSteps [IntegrationPipeline ]:
424+ return [
425+ ProjectSelectApiStep (),
426+ CloudFormationApiStep (),
427+ InstrumentationApiStep (),
428+ ]
429+
216430 @control_silo_function
217431 def build_integration (self , state : Mapping [str , Any ]) -> IntegrationData :
218432 region = state ["region" ]
0 commit comments