2020from sentry .investigations .services .action_kinds import get_action_kind , shared_cannot_apply_reason
2121from sentry .investigations .services .action_kinds .base import (
2222 ActionInvalid ,
23+ ActionKind ,
2324 ActionRequestError ,
2425 ActionStale ,
2526 ApplyContext ,
2627 ApplyError ,
2728 ApplyErrorCode ,
28- ApplyResult ,
29+ ApplyFailed ,
2930 CannotApplyReason ,
3031)
3132
@@ -229,11 +230,11 @@ def supersede_block_action_requests(investigation: Investigation, block_keys: Se
229230 )
230231
231232
232- # The usual detail for a reason; anything a kind refuses gets the last one.
233233_REASON_DETAILS = {
234234 CannotApplyReason .INVESTIGATION_ARCHIVED : "Archived investigations are read-only." ,
235235 CannotApplyReason .UNSUPPORTED_ACTION : "This request has no action to apply." ,
236236}
237+ _DEFAULT_REASON_DETAIL = "This action can't be applied."
237238
238239
239240def _not_suggested_error (action_request : InvestigationActionRequest ) -> ApplyError :
@@ -254,22 +255,39 @@ def _not_suggested_error(action_request: InvestigationActionRequest) -> ApplyErr
254255 }
255256
256257
257- def _error_messages (errors : object ) -> list [str ]:
258- if isinstance (errors , dict ):
259- return [message for value in errors .values () for message in _error_messages (value )]
260- if isinstance (errors , list ):
261- return [message for value in errors for message in _error_messages (value )]
262- return [str (errors )]
258+ def _check_can_apply (
259+ investigation : Investigation , action_request : InvestigationActionRequest , request : Request
260+ ) -> ActionKind :
261+ """Return the request's kind, or raise ApplyFailed if the viewer can't apply it."""
262+
263+ reason = shared_cannot_apply_reason (investigation , action_request )
264+ if reason == CannotApplyReason .NOT_SUGGESTED :
265+ raise ApplyFailed (_not_suggested_error (action_request ))
266+ if reason is None :
267+ kind = get_action_kind (action_request .action_kind )
268+ assert kind is not None
269+ # The same check that decides canApply guards the endpoint, so a kind can't forget it.
270+ reason = kind .cannot_apply_reasons (
271+ [action_request ],
272+ investigations = {investigation .id : investigation },
273+ access = request .access ,
274+ user = request .user ,
275+ )[action_request .id ]
276+ if reason is None :
277+ return kind
278+ raise ApplyFailed (
279+ {"detail" : _REASON_DETAILS .get (reason , _DEFAULT_REASON_DETAIL ), "code" : reason }
280+ )
263281
264282
265283def _apply (
266284 * ,
267285 investigation : Investigation ,
268- action_request_id : str ,
286+ action_request : InvestigationActionRequest ,
269287 request : Request ,
270288 actor_id : int ,
271289 dry_run : bool ,
272- ) -> ApplyResult :
290+ ) -> InvestigationActionRequest :
273291 investigations = Investigation .objects .select_related ("organization" )
274292 runs = InvestigationOrchestrationRun .objects .all ()
275293 action_requests = InvestigationActionRequest .objects .all ()
@@ -279,31 +297,9 @@ def _apply(
279297 action_requests = action_requests .select_for_update ()
280298
281299 locked = investigations .get (id = investigation .id )
282- try :
283- run = runs .get (investigation = locked )
284- except InvestigationOrchestrationRun .DoesNotExist :
285- raise ResourceDoesNotExist
286- try :
287- action_request = action_requests .get (id = action_request_id , investigation = locked )
288- except (InvestigationActionRequest .DoesNotExist , ValueError ):
289- raise ResourceDoesNotExist
290-
291- reason = shared_cannot_apply_reason (locked , action_request )
292- if reason == CannotApplyReason .NOT_SUGGESTED :
293- return ApplyResult (action_request , _not_suggested_error (action_request ))
294- kind = get_action_kind (action_request .action_kind )
295- if reason is None and kind is not None :
296- # The same check that decides canApply guards the endpoint, so a kind can't forget it.
297- reason = kind .cannot_apply_reasons (
298- [action_request ],
299- investigations = {locked .id : locked },
300- access = request .access ,
301- user = request .user ,
302- ).get (action_request .id , CannotApplyReason .UNSUPPORTED_ACTION )
303- if reason is not None :
304- detail = _REASON_DETAILS .get (reason , "This action can't be applied." )
305- return ApplyResult (action_request , {"detail" : detail , "code" : reason })
306- assert kind is not None
300+ run = runs .get (investigation = locked )
301+ action_request = action_requests .get (id = action_request .id )
302+ kind = _check_can_apply (locked , action_request , request )
307303
308304 context = ApplyContext (
309305 investigation = locked ,
@@ -315,7 +311,7 @@ def _apply(
315311 try :
316312 if dry_run :
317313 kind .check (context )
318- return ApplyResult ( action_request )
314+ return action_request
319315 details = kind .apply (context )
320316 except ActionStale as stale :
321317 if not dry_run :
@@ -325,23 +321,21 @@ def _apply(
325321 state = InvestigationActionRequestState .SUPERSEDED ,
326322 error = ActionRequestError (code = ApplyErrorCode .STALE , current = stale .current ),
327323 )
328- return ApplyResult (
329- action_request ,
324+ raise ApplyFailed (
330325 {
331326 "detail" : "This changed since Seer suggested it." ,
332327 "code" : ApplyErrorCode .STALE ,
333328 "current" : stale .current ,
334- },
329+ }
335330 )
336331 except ActionInvalid as invalid :
337332 # The request stays suggested, so the user can try again.
338- return ApplyResult (
339- action_request ,
333+ raise ApplyFailed (
340334 {
341335 "detail" : "The change wasn't valid." ,
342336 "code" : ApplyErrorCode .VALIDATION_FAILED ,
343- "messages" : _error_messages ( invalid .errors ) ,
344- },
337+ "messages" : invalid .messages ,
338+ }
345339 )
346340 action_request .update (
347341 state = InvestigationActionRequestState .APPLIED ,
@@ -350,7 +344,7 @@ def _apply(
350344 applied_details = details ,
351345 error = None ,
352346 )
353- return ApplyResult ( action_request )
347+ return action_request
354348
355349
356350def apply_action_request (
@@ -360,21 +354,33 @@ def apply_action_request(
360354 request : Request ,
361355 actor_id : int ,
362356 dry_run : bool = False ,
363- ) -> ApplyResult :
364- """Apply a suggested action as the requesting user. A dry run only says whether it would
365- work, and doesn't take locks or change anything.
357+ ) -> InvestigationActionRequest :
358+ """Apply a suggested action as the requesting user, or raise ApplyFailed . A dry run only
359+ says whether it would work, and doesn't take locks or change anything.
366360 """
367361
362+ try :
363+ action_request = InvestigationActionRequest .objects .get (
364+ id = action_request_id , investigation = investigation
365+ )
366+ except (InvestigationActionRequest .DoesNotExist , ValueError ):
367+ raise ResourceDoesNotExist
368+
368369 using = router .db_for_write (InvestigationActionRequest )
369370 with transaction .atomic (using = using ):
370- result = _apply (
371- investigation = investigation ,
372- action_request_id = action_request_id ,
373- request = request ,
374- actor_id = actor_id ,
375- dry_run = dry_run ,
376- )
377- if dry_run :
378- # Even if a check writes by mistake, a dry run keeps nothing.
379- transaction .set_rollback (True , using = using )
380- return result
371+ try :
372+ return _apply (
373+ investigation = investigation ,
374+ action_request = action_request ,
375+ request = request ,
376+ actor_id = actor_id ,
377+ dry_run = dry_run ,
378+ )
379+ except ApplyFailed as failed :
380+ error = failed
381+ finally :
382+ if dry_run :
383+ # The detector validator can create environments while validating.
384+ transaction .set_rollback (True , using = using )
385+ # Raise once the transaction commits, so a stale request stays superseded.
386+ raise error
0 commit comments