diff --git a/localstack/services/stepfunctions/asl/component/common/error_name/failure_event.py b/localstack/services/stepfunctions/asl/component/common/error_name/failure_event.py index d0de86e860949..79c75bd68ed1a 100644 --- a/localstack/services/stepfunctions/asl/component/common/error_name/failure_event.py +++ b/localstack/services/stepfunctions/asl/component/common/error_name/failure_event.py @@ -1,6 +1,6 @@ from typing import Final -from localstack.aws.api.stepfunctions import HistoryEventType +from localstack.aws.api.stepfunctions import ExecutionFailedEventDetails, HistoryEventType from localstack.services.stepfunctions.asl.component.common.error_name.error_name import ErrorName from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails @@ -16,3 +16,20 @@ def __init__( self.error_name = error_name self.event_type = event_type self.event_details = event_details + + +class FailureEventException(Exception): + failure_event: Final[FailureEvent] + + def __init__(self, failure_event: FailureEvent): + self.failure_event = failure_event + + def get_execution_failed_event_details(self) -> ExecutionFailedEventDetails: + failure_event_spec = list(self.failure_event.event_details.values())[0] + execution_failed_event_details = ExecutionFailedEventDetails( + error=failure_event_spec.get("error") + or f"NoErrorSpecification in {failure_event_spec}", + ) + if "cause" in failure_event_spec: + execution_failed_event_details["cause"] = failure_event_spec["cause"] + return execution_failed_event_details diff --git a/localstack/services/stepfunctions/asl/component/program/program.py b/localstack/services/stepfunctions/asl/component/program/program.py index c9406b9f2637e..22971960d4858 100644 --- a/localstack/services/stepfunctions/asl/component/program/program.py +++ b/localstack/services/stepfunctions/asl/component/program/program.py @@ -9,6 +9,9 @@ HistoryEventType, ) from localstack.services.stepfunctions.asl.component.common.comment import Comment +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEventException, +) from localstack.services.stepfunctions.asl.component.common.flow.start_at import StartAt from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent from localstack.services.stepfunctions.asl.component.state.state import CommonStateField @@ -46,8 +49,16 @@ def _eval_body(self, env: Environment) -> None: while env.is_running(): next_state: CommonStateField = self._get_state(env.next_state_name) next_state.eval(env) + except FailureEventException as ex: + env.set_error(error=ex.get_execution_failed_event_details()) except Exception as ex: - LOG.debug(f"Stepfunctions computation ended with exception '{ex}'.") + cause = f"{type(ex)}({str(ex)})" + LOG.error(f"Stepfunctions computation ended with exception '{cause}'.") + env.set_error( + ExecutionFailedEventDetails( + error="Internal Error", cause=f"Internal Error due to '{cause}'" + ) + ) program_state: ProgramState = env.program_state() if isinstance(program_state, ProgramError): diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py b/localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py index 7fdbf84bfa625..3d119d4c025b5 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py @@ -5,11 +5,7 @@ from threading import Thread from typing import Any, Optional -from localstack.aws.api.stepfunctions import ( - ExecutionFailedEventDetails, - HistoryEventType, - TaskFailedEventDetails, -) +from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails from localstack.services.stepfunctions.asl.component.common.catch.catch_decl import CatchDecl from localstack.services.stepfunctions.asl.component.common.catch.catch_outcome import ( CatchOutcome, @@ -17,6 +13,7 @@ ) from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( FailureEvent, + FailureEventException, ) from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( StatesErrorName, @@ -126,13 +123,17 @@ def from_state_props(self, state_props: StateProps) -> None: self.heartbeat = heartbeat def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: - LOG.warning("State Task executed generic failure event reporting logic.") + if isinstance(ex, FailureEventException): + return ex.failure_event + LOG.warning( + "State Task encountered an unhandled exception that lead to a State.Runtime error." + ) return FailureEvent( - error_name=StatesErrorName(typ=StatesErrorNameType.StatesTaskFailed), + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), event_type=HistoryEventType.TaskFailed, event_details=EventDetails( taskFailedEventDetails=TaskFailedEventDetails( - error="Unsupported Error Handling", + error=StatesErrorNameType.StatesRuntime.to_name(), cause=str(ex), ) ), @@ -184,10 +185,7 @@ def _handle_uncaught(self, ex: Exception, env: Environment): @staticmethod def _terminate_with_event(failure_event: FailureEvent, env: Environment) -> None: - # Halt execution with the given failure event. - env.set_error( - ExecutionFailedEventDetails(**(list(failure_event.event_details.values())[0])) - ) + raise FailureEventException(failure_event=failure_event) def _evaluate_with_timeout(self, env: Environment) -> None: self.timeout.eval(env=env) diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/resource.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/resource.py index 65bc1057f5d95..2b376412a18b4 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/resource.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/resource.py @@ -9,6 +9,7 @@ class ResourceCondition(str): WaitForTaskToken = "waitForTaskToken" + Sync = "sync" class ResourceARN(TypedDict): @@ -121,5 +122,7 @@ def __init__( match tail_parts[-1]: case "waitForTaskToken": self.condition = ResourceCondition.WaitForTaskToken + case "sync": + self.condition = ResourceCondition.Sync case unsupported: raise RuntimeError(f"Unsupported condition '{unsupported}'.") diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py index 05ca26a35bab3..1b28f5f8bdd3d 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py @@ -65,6 +65,12 @@ def for_service(cls, service_name: str) -> StateTaskService: ) return StateTaskServiceSqs() + case "states": + from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_sfn import ( + StateTaskServiceSfn, + ) + + return StateTaskServiceSfn() case unknown: raise NotImplementedError(f"Unsupported service: '{unknown}'.") # noqa diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_aws_sdk.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_aws_sdk.py index 9f14a25396740..cdcd97bb48444 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_aws_sdk.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_aws_sdk.py @@ -1,3 +1,4 @@ +from botocore.config import Config from botocore.exceptions import ClientError from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails @@ -15,7 +16,6 @@ StateTaskServiceCallback, ) from localstack.services.stepfunctions.asl.component.state.state_props import StateProps -from localstack.services.stepfunctions.asl.eval.callback.callback import CallbackOutcomeFailureError from localstack.services.stepfunctions.asl.eval.environment import Environment from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails from localstack.utils.aws import aws_stack @@ -75,14 +75,10 @@ def _get_task_failure_event(self, error: str, cause: str) -> FailureEvent: ) def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: - if isinstance(ex, CallbackOutcomeFailureError): - return self._get_callback_outcome_failure_event(ex=ex) - if isinstance(ex, TimeoutError): - return self._get_timed_out_failure_event() - - norm_service_name: str = self._normalise_service_name(self.resource.api_name) - error: str = self._normalise_exception_name(norm_service_name, ex) if isinstance(ex, ClientError): + norm_service_name: str = self._normalise_service_name(self.resource.api_name) + error: str = self._normalise_exception_name(norm_service_name, ex) + error_message: str = ex.response["Error"]["Message"] cause_details = [ f"Service: {norm_service_name}", @@ -97,14 +93,12 @@ def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: cause: str = f"{error_message} ({', '.join(cause_details)})" failure_event = self._get_task_failure_event(error=error, cause=cause) return failure_event - - failure_event = self._get_task_failure_event( - error=error, cause=str(ex) # TODO: update cause decoration. - ) - return failure_event + return super()._from_error(env=env, ex=ex) def _eval_service_task(self, env: Environment, parameters: dict) -> None: - api_client = aws_stack.create_external_boto_client(service_name=self._normalised_api_name) + api_client = aws_stack.create_external_boto_client( + service_name=self._normalised_api_name, config=Config(parameter_validation=False) + ) response = getattr(api_client, self._normalised_api_action)(**parameters) or dict() if response: response.pop("ResponseMetadata", None) diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py index 883d8af16e5df..5b61e95c5fb00 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py @@ -86,6 +86,11 @@ def _wait_for_task_token(self, env: Environment) -> None: # noqa else: raise NotImplementedError(f"Unsupported CallbackOutcome type '{type(outcome)}'.") + def _sync(self, env: Environment) -> None: + raise RuntimeError( + f"Unsupported .sync callback procedure in resource {self.resource.resource_arn}" + ) + def _is_condition(self): return self.resource.condition is not None @@ -105,6 +110,11 @@ def _get_callback_outcome_failure_event(self, ex: CallbackOutcomeFailureError) - ), ) + def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: + if isinstance(ex, CallbackOutcomeFailureError): + return self._get_callback_outcome_failure_event(ex=ex) + return super()._from_error(env=env, ex=ex) + def _eval_execution(self, env: Environment) -> None: parameters = self._eval_parameters(env=env) parameters_str = to_json_str(parameters) @@ -137,10 +147,11 @@ def _eval_execution(self, env: Environment) -> None: ), ) - self._eval_service_task(env=env, parameters=parameters) + normalised_parameters = self._normalised_parameters_bindings(parameters) + self._eval_service_task(env=env, parameters=normalised_parameters) if self._is_condition(): - output = env.stack.pop() + output = env.stack[-1] env.event_history.add_event( hist_type_event=HistoryEventType.TaskSubmitted, event_detail=EventDetails( @@ -155,6 +166,8 @@ def _eval_execution(self, env: Environment) -> None: match self.resource.condition: case ResourceCondition.WaitForTaskToken: self._wait_for_task_token(env=env) + case ResourceCondition.Sync: + self._sync(env=env) case unsupported: raise NotImplementedError(f"Unsupported callback type '{unsupported}'.") diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_lambda.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_lambda.py index 15caa990bb2b5..f75722d4ecdbe 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_lambda.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_lambda.py @@ -9,19 +9,12 @@ from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( FailureEvent, ) -from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( - StatesErrorName, -) -from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( - StatesErrorNameType, -) from localstack.services.stepfunctions.asl.component.state.state_execution.state_task import ( lambda_eval_utils, ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_callback import ( StateTaskServiceCallback, ) -from localstack.services.stepfunctions.asl.eval.callback.callback import CallbackOutcomeFailureError from localstack.services.stepfunctions.asl.eval.environment import Environment from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails @@ -60,11 +53,6 @@ def _error_cause_from_client_error(client_error: ClientError) -> tuple[str, str] return error, cause def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: - if isinstance(ex, CallbackOutcomeFailureError): - return self._get_callback_outcome_failure_event(ex=ex) - if isinstance(ex, TimeoutError): - return self._get_timed_out_failure_event() - if isinstance(ex, lambda_eval_utils.LambdaFunctionErrorException): error = "Exception" error_name = CustomErrorName(error) @@ -73,10 +61,7 @@ def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: error, cause = self._error_cause_from_client_error(ex) error_name = CustomErrorName(error) else: - error = "Exception" - error_name = StatesErrorName(typ=StatesErrorNameType.StatesTaskFailed) - cause = str(ex) - + return super()._from_error(env=env, ex=ex) return FailureEvent( error_name=error_name, event_type=HistoryEventType.TaskFailed, diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_sfn.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_sfn.py new file mode 100644 index 0000000000000..203a7ed8dfcfe --- /dev/null +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_sfn.py @@ -0,0 +1,209 @@ +from typing import Any, Final, Optional + +from botocore.config import Config +from botocore.exceptions import ClientError + +from localstack.aws.api.stepfunctions import ( + DescribeExecutionOutput, + ExecutionStatus, + HistoryEventType, + TaskFailedEventDetails, +) +from localstack.services.stepfunctions.asl.component.common.error_name.custom_error_name import ( + CustomErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) +from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_callback import ( + StateTaskServiceCallback, +) +from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails +from localstack.services.stepfunctions.asl.utils.encoding import to_json_str +from localstack.utils.aws import aws_stack +from localstack.utils.collections import select_from_typed_dict +from localstack.utils.strings import camel_to_snake_case + + +class StateTaskServiceSfn(StateTaskServiceCallback): + _SUPPORTED_API_PARAM_BINDINGS: Final[dict[str, set[str]]] = { + "startexecution": {"Input", "Name", "StateMachineArn"} + } + + _SFN_TO_BOTO_PARAM_NORMALISERS = { + "startexecution": {"Input": "input", "Name": "name", "StateMachineArn": "stateMachineArn"} + } + + _BOTO_TO_SFN_RESPONSE_BINDINGS = { + "startexecution": ["StartDate", "ExecutionArn"], + "describeexecution": [ + "ExecutionArn", + "Input", + ["InputDetails", ["Included"]], + "Name", + "Output", + ["OutputDetails", ["Included"]], + "StartDate", + "StateMachineArn", + "Status", + "StopDate", + "Error", + "Cause", + ], + } + + def _get_supported_parameters(self) -> Optional[set[str]]: + return self._SUPPORTED_API_PARAM_BINDINGS.get(self.resource.api_action.lower()) + + def _get_parameters_normalising_bindings(self) -> dict[str, str]: + return self._SFN_TO_BOTO_PARAM_NORMALISERS.get(self.resource.api_action.lower(), dict()) + + def _normalise_botocore_response(self, api_action: str, response: dict[str, Any]) -> None: + keys = self._BOTO_TO_SFN_RESPONSE_BINDINGS.get(api_action.lower()) + if keys is None: + return + + def _build_lower_to_key_dict(key_list: list[str]) -> dict[str, Any]: + lower_to_key_dict = dict() + for key in key_list: + if isinstance(key, str): + lower_to_key_dict[key.lower()] = key + elif isinstance(key, list): + lower_to_key_dict[key[0].lower()] = [key[0], _build_lower_to_key_dict(key[1])] + return lower_to_key_dict + + def _update_key(old_key, new_key, obj): + if new_key != old_key: + value_bind = obj[old_key] + del obj[old_key] + obj[new_key] = value_bind + + def _apply_normalisation(lookup_keys, dictionary): + input_keys = list(dictionary.keys()) + for input_key in input_keys: + normalised_key = lookup_keys.get(input_key.lower()) + if isinstance(normalised_key, str): + _update_key(input_key, normalised_key, dictionary) + elif isinstance(normalised_key, list): + _update_key(input_key, normalised_key[0], dictionary) + _apply_normalisation(normalised_key[1], dictionary[normalised_key[0]]) + + lower_to_normalise_key = _build_lower_to_key_dict(keys) + _apply_normalisation(lower_to_normalise_key, response) + + @staticmethod + def _get_sfn_client(): + return aws_stack.create_external_boto_client( + "stepfunctions", config=Config(parameter_validation=False) + ) + + def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: + if isinstance(ex, ClientError): + error_code = ex.response["Error"]["Code"] + error_name: str = f"StepFunctions.{error_code}Exception" + error_cause_details = [ + "Service: AWSStepFunctions", + f"Status Code: {ex.response['ResponseMetadata']['HTTPStatusCode']}", + f"Error Code: {error_code}", + f"Request ID: {ex.response['ResponseMetadata']['RequestId']}", + "Proxy: null", # TODO: investigate this proxy value. + ] + if "HostId" in ex.response["ResponseMetadata"]: + error_cause_details.append( + f'Extended Request ID: {ex.response["ResponseMetadata"]["HostId"]}' + ) + error_cause: str = ( + f"{ex.response['Error']['Message']} ({'; '.join(error_cause_details)})" + ) + return FailureEvent( + error_name=CustomErrorName(error_name), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + error=error_name, + cause=error_cause, + resource=self._get_sfn_resource(), + resourceType=self._get_sfn_resource_type(), + ) + ), + ) + return super()._from_error(env=env, ex=ex) + + def _normalised_parameters_bindings(self, parameters: dict[str, str]) -> dict[str, str]: + normalised_parameters = super()._normalised_parameters_bindings(parameters=parameters) + + if self.resource.api_action.lower() == "startexecution": + optional_input = normalised_parameters.get("input") + if not isinstance(optional_input, str): + + # AWS Sfn's documentation states: + # If you don't include any JSON input data, you still must include the two braces. + if optional_input is None: + optional_input = {} + + normalised_parameters["input"] = to_json_str(optional_input, separators=(",", ":")) + + return normalised_parameters + + def _eval_service_task(self, env: Environment, parameters: dict) -> None: + api_action = camel_to_snake_case(self.resource.api_action) + sfn_client = self._get_sfn_client() + response = getattr(sfn_client, api_action)(**parameters) + response.pop("ResponseMetadata", None) + self._normalise_botocore_response(self.resource.api_action, response) + env.stack.append(response) + + def _sync_to_start_machine(self, env: Environment) -> None: + sfn_client = self._get_sfn_client() + + submission_output: dict = env.stack.pop() + execution_arn: str = submission_output["ExecutionArn"] + + def _has_terminated() -> Optional[dict]: + describe_execution_output = sfn_client.describe_execution(executionArn=execution_arn) + describe_execution_output: DescribeExecutionOutput = select_from_typed_dict( + DescribeExecutionOutput, describe_execution_output + ) + execution_status: ExecutionStatus = describe_execution_output["status"] + + if execution_status != ExecutionStatus.RUNNING: + self._normalise_botocore_response("describeexecution", describe_execution_output) + if execution_status == ExecutionStatus.SUCCEEDED: + return describe_execution_output + else: + raise FailureEventException( + FailureEvent( + error_name=StatesErrorName(typ=StatesErrorNameType.StatesTaskFailed), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + resource=self._get_sfn_resource(), + resourceType=self._get_sfn_resource_type(), + error=StatesErrorNameType.StatesTaskFailed.to_name(), + cause=to_json_str(describe_execution_output), + ) + ), + ) + ) + return None + + termination_output: Optional[dict] = None + while env.is_running() and not termination_output: + termination_output: Optional[dict] = _has_terminated() + + env.stack.append(termination_output) + + def _sync(self, env: Environment) -> None: + match self.resource.api_action.lower(): + case "startexecution": + self._sync_to_start_machine(env=env) + case _: + super()._sync(env=env) diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_sqs.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_sqs.py index b765b2054b64a..7608ef02fda2b 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_sqs.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_sqs.py @@ -1,5 +1,6 @@ from typing import Final, Optional +from botocore.config import Config from botocore.exceptions import ClientError from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails @@ -12,7 +13,6 @@ from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_callback import ( StateTaskServiceCallback, ) -from localstack.services.stepfunctions.asl.eval.callback.callback import CallbackOutcomeFailureError from localstack.services.stepfunctions.asl.eval.environment import Environment from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails from localstack.services.stepfunctions.asl.utils.encoding import to_json_str @@ -39,11 +39,6 @@ def _get_supported_parameters(self) -> Optional[set[str]]: return self._SUPPORTED_API_PARAM_BINDINGS.get(self.resource.api_action.lower()) def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: - if isinstance(ex, CallbackOutcomeFailureError): - return self._get_callback_outcome_failure_event(ex=ex) - if isinstance(ex, TimeoutError): - return self._get_timed_out_failure_event() - if isinstance(ex, ClientError): return FailureEvent( error_name=CustomErrorName(self._ERROR_NAME_CLIENT), @@ -59,19 +54,7 @@ def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: ) ), ) - else: - return FailureEvent( - error_name=CustomErrorName(self._ERROR_NAME_AWS), - event_type=HistoryEventType.TaskFailed, - event_details=EventDetails( - taskFailedEventDetails=TaskFailedEventDetails( - error=self._ERROR_NAME_AWS, - cause=str(ex), # TODO: update to report expected cause. - resource=self._get_sfn_resource(), - resourceType=self._get_sfn_resource_type(), - ) - ), - ) + return super()._from_error(env=env, ex=ex) def _eval_service_task(self, env: Environment, parameters: dict) -> None: # TODO: Stepfunctions automatically dumps to json MessageBody's definitions. @@ -82,7 +65,7 @@ def _eval_service_task(self, env: Environment, parameters: dict) -> None: parameters["MessageBody"] = to_json_str(message_body) api_action = camel_to_snake_case(self.resource.api_action) - sqs_client = aws_stack.create_external_boto_client("sqs") + sqs_client = aws_stack.connect_to_service("sqs", config=Config(parameter_validation=False)) response = getattr(sqs_client, api_action)(**parameters) response.pop("ResponseMetadata", None) env.stack.append(response) diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py index 89a75d8f397c8..3aaeecbc81863 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py @@ -1,9 +1,19 @@ from __future__ import annotations import abc +import copy from typing import Optional -from localstack.aws.api.stepfunctions import HistoryEventType +from localstack.aws.api.stepfunctions import HistoryEventType, TaskTimedOutEventDetails +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) from localstack.services.stepfunctions.asl.component.common.parameters import Parameters from localstack.services.stepfunctions.asl.component.state.state_execution.execute_state import ( ExecutionState, @@ -13,6 +23,7 @@ ) from localstack.services.stepfunctions.asl.component.state.state_props import StateProps from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails class StateTask(ExecutionState, abc.ABC): @@ -55,6 +66,34 @@ def _get_supported_parameters(self) -> Optional[set[str]]: # noqa def _get_parameters_normalising_bindings(self) -> dict[str, str]: # noqa return dict() + def _normalised_parameters_bindings(self, parameters: dict[str, str]) -> dict[str, str]: + normalised_parameters = copy.deepcopy(parameters) + # Normalise bindings. + parameter_normalisers = self._get_parameters_normalising_bindings() + for parameter_key in list(normalised_parameters.keys()): + norm_parameter_key = parameter_normalisers.get(parameter_key, None) + if norm_parameter_key: + tmp = normalised_parameters[parameter_key] + del normalised_parameters[parameter_key] + normalised_parameters[norm_parameter_key] = tmp + return normalised_parameters + + def _get_timed_out_failure_event(self) -> FailureEvent: + return FailureEvent( + error_name=StatesErrorName(typ=StatesErrorNameType.StatesTimeout), + event_type=HistoryEventType.TaskTimedOut, + event_details=EventDetails( + taskTimedOutEventDetails=TaskTimedOutEventDetails( + error=StatesErrorNameType.StatesTimeout.to_name(), + ) + ), + ) + + def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: + if isinstance(ex, TimeoutError): + return self._get_timed_out_failure_event() + return super()._from_error(env=env, ex=ex) + def _eval_parameters(self, env: Environment) -> dict: # Eval raw parameters. parameters = dict() @@ -73,15 +112,6 @@ def _eval_parameters(self, env: Environment) -> dict: for unsupported_parameter in unsupported_parameters: parameters.pop(unsupported_parameter, None) - # Normalise bindings. - parameter_normalisers = self._get_parameters_normalising_bindings() - for parameter_key in list(parameters.keys()): - norm_parameter_key = parameter_normalisers.get(parameter_key, None) - if norm_parameter_key: - tmp = parameters[parameter_key] - del parameters[parameter_key] - parameters[norm_parameter_key] = tmp - return parameters def _eval_body(self, env: Environment) -> None: diff --git a/localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py b/localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py index 0248ee6f902a6..725a94da46a3a 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +++ b/localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py @@ -1,11 +1,19 @@ from typing import Optional -from localstack.aws.api.stepfunctions import HistoryEventType +from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails from localstack.services.stepfunctions.asl.component.common.cause_decl import CauseDecl from localstack.services.stepfunctions.asl.component.common.error_decl import ErrorDecl +from localstack.services.stepfunctions.asl.component.common.error_name.custom_error_name import ( + CustomErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) from localstack.services.stepfunctions.asl.component.state.state import CommonStateField from localstack.services.stepfunctions.asl.component.state.state_props import StateProps from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails class StateFail(CommonStateField): @@ -23,5 +31,13 @@ def from_state_props(self, state_props: StateProps) -> None: self.error = state_props.get(ErrorDecl) def _eval_state(self, env: Environment) -> None: - # TODO. - env.set_error(self) + failure_event = FailureEvent( + error_name=CustomErrorName(self.error.error), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + error=self.error.error, cause=self.cause.cause + ) + ), + ) + raise FailureEventException(failure_event=failure_event) diff --git a/localstack/services/stepfunctions/asl/utils/encoding.py b/localstack/services/stepfunctions/asl/utils/encoding.py index 4164e5aa7a5e9..893db6cc28f44 100644 --- a/localstack/services/stepfunctions/asl/utils/encoding.py +++ b/localstack/services/stepfunctions/asl/utils/encoding.py @@ -1,7 +1,7 @@ import datetime import json from json import JSONEncoder -from typing import Any +from typing import Any, Optional class _DateTimeEncoder(JSONEncoder): @@ -12,5 +12,5 @@ def default(self, obj): return str(obj) -def to_json_str(obj: Any) -> str: - return json.dumps(obj, cls=_DateTimeEncoder) +def to_json_str(obj: Any, separators: Optional[tuple[str, str]] = None) -> str: + return json.dumps(obj, cls=_DateTimeEncoder, separators=separators) diff --git a/localstack/services/stepfunctions/backend/execution.py b/localstack/services/stepfunctions/backend/execution.py index 482902988fcea..ff34e965b6dc0 100644 --- a/localstack/services/stepfunctions/backend/execution.py +++ b/localstack/services/stepfunctions/backend/execution.py @@ -1,7 +1,6 @@ from __future__ import annotations import datetime -import json from typing import Final, Optional from localstack.aws.api.stepfunctions import ( @@ -13,7 +12,9 @@ GetExecutionHistoryOutput, HistoryEventList, InvalidName, + SensitiveCause, SensitiveData, + SensitiveError, StartExecutionOutput, Timestamp, TraceHeader, @@ -45,13 +46,17 @@ def __init__(self, execution: Execution): def terminated(self) -> None: exit_program_state: ProgramState = self.execution.exec_worker.env.program_state() self.execution.stop_date = datetime.datetime.now() - self.execution.output = to_json_str(self.execution.exec_worker.env.inp) if isinstance(exit_program_state, ProgramEnded): self.execution.exec_status = ExecutionStatus.SUCCEEDED + self.execution.output = to_json_str( + self.execution.exec_worker.env.inp, separators=(",", ":") + ) elif isinstance(exit_program_state, ProgramStopped): self.execution.exec_status = ExecutionStatus.ABORTED elif isinstance(exit_program_state, ProgramError): self.execution.exec_status = ExecutionStatus.FAILED + self.execution.error = exit_program_state.error["error"] + self.execution.cause = exit_program_state.error["cause"] else: raise RuntimeWarning( f"Execution ended with unsupported ProgramState type '{type(exit_program_state)}'." @@ -72,6 +77,9 @@ def terminated(self) -> None: output: Optional[SensitiveData] output_details: Optional[CloudWatchEventsExecutionDataDetails] + error: Optional[SensitiveError] + cause: Optional[SensitiveCause] + exec_worker: Optional[ExecutionWorker] def __init__( @@ -82,7 +90,6 @@ def __init__( state_machine: StateMachine, start_date: Timestamp, input_data: Optional[dict] = None, - input_details: Optional[CloudWatchEventsExecutionDataDetails] = None, trace_header: Optional[TraceHeader] = None, ): self.name = name @@ -91,31 +98,39 @@ def __init__( self.state_machine = state_machine self.start_date = start_date self.input_data = input_data - self.input_details = input_details + self.input_details = CloudWatchEventsExecutionDataDetails(included=True) self.trace_header = trace_header self.exec_status = None self.stop_date = None self.output = None - self.output_details = None + self.output_details = CloudWatchEventsExecutionDataDetails(included=True) self.exec_worker = None + self.error = None + self.cause = None def to_start_output(self) -> StartExecutionOutput: return StartExecutionOutput(executionArn=self.exec_arn, startDate=self.start_date) def to_describe_output(self) -> DescribeExecutionOutput: - return DescribeExecutionOutput( + describe_output = DescribeExecutionOutput( executionArn=self.exec_arn, stateMachineArn=self.state_machine.arn, - name=self.state_machine.name, + name=self.name, status=self.exec_status, startDate=self.start_date, stopDate=self.stop_date, - input=json.dumps(self.input_data), + input=to_json_str(self.input_data, separators=(",", ":")), inputDetails=self.input_details, - output=self.output, - outputDetails=self.output_details, traceHeader=self.trace_header, ) + if describe_output["status"] == ExecutionStatus.SUCCEEDED: + describe_output["output"] = self.output + describe_output["outputDetails"] = self.output_details + if self.error is not None: + describe_output["error"] = self.error + if self.cause is not None: + describe_output["cause"] = self.cause + return describe_output def to_execution_list_item(self) -> ExecutionListItem: return ExecutionListItem( diff --git a/localstack/services/stepfunctions/provider_v2.py b/localstack/services/stepfunctions/provider_v2.py index 700c658ab3efe..ef0bbd13a7d19 100644 --- a/localstack/services/stepfunctions/provider_v2.py +++ b/localstack/services/stepfunctions/provider_v2.py @@ -250,7 +250,7 @@ def start_execution( except Exception as ex: raise InvalidExecutionInput(str(ex)) # TODO: report parsing error like AWS. - exec_name = long_uid() + exec_name = name or long_uid() # TODO: validate name format arn_data: ArnData = parse_arn(state_machine_arn) exec_arn = ":".join( [ diff --git a/tests/integration/stepfunctions/templates/base/base_templates.py b/tests/integration/stepfunctions/templates/base/base_templates.py index 1b8a5543f7283..ea2753feec9c3 100644 --- a/tests/integration/stepfunctions/templates/base/base_templates.py +++ b/tests/integration/stepfunctions/templates/base/base_templates.py @@ -11,3 +11,4 @@ class BaseTemplate(TemplateLoader): BASE_PASS_RESULT: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/pass_result.json5") BASE_TASK_SEQ_2: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/task_seq_2.json5") BASE_WAIT_1_MIN: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/wait_1_min.json5") + BASE_RAISE_FAILURE: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/raise_failure.json5") diff --git a/tests/integration/stepfunctions/templates/base/statemachines/raise_failure.json5 b/tests/integration/stepfunctions/templates/base/statemachines/raise_failure.json5 new file mode 100644 index 0000000000000..d36c438c3860e --- /dev/null +++ b/tests/integration/stepfunctions/templates/base/statemachines/raise_failure.json5 @@ -0,0 +1,11 @@ +{ + "Comment": "BASE_RAISE_FAILURE", + "StartAt": "FailState", + "States": { + "FailState": { + "Type": "Fail", + "Error": "SomeFailure", + "Cause": "This state machines raises a 'SomeFailure' failure." + }, + }, +} \ No newline at end of file diff --git a/tests/integration/stepfunctions/templates/callbacks/callback_templates.py b/tests/integration/stepfunctions/templates/callbacks/callback_templates.py index 24a5369e69989..33dfc84a10db2 100644 --- a/tests/integration/stepfunctions/templates/callbacks/callback_templates.py +++ b/tests/integration/stepfunctions/templates/callbacks/callback_templates.py @@ -7,6 +7,9 @@ class CallbackTemplates(TemplateLoader): + SFN_START_EXECUTION_SYNC: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/sfn_start_execution_sync.json5" + ) SQS_SUCCESS_ON_TASK_TOKEN: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/sqs_success_on_task_token.json5" ) @@ -19,7 +22,6 @@ class CallbackTemplates(TemplateLoader): SQS_WAIT_FOR_TASK_TOKEN_WITH_TIMEOUT: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/sqs_wait_for_task_token_with_timeout.json5" ) - SQS_HEARTBEAT_SUCCESS_ON_TASK_TOKEN: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/sqs_hearbeat_success_on_task_token.json5" ) diff --git a/tests/integration/stepfunctions/templates/callbacks/statemachines/sfn_start_execution_sync.json5 b/tests/integration/stepfunctions/templates/callbacks/statemachines/sfn_start_execution_sync.json5 new file mode 100644 index 0000000000000..fdc2e961098ba --- /dev/null +++ b/tests/integration/stepfunctions/templates/callbacks/statemachines/sfn_start_execution_sync.json5 @@ -0,0 +1,16 @@ +{ + "Comment": "SFN_START_EXECUTION_SYNC", + "StartAt": "StartExecution", + "States": { + "StartExecution": { + "Type": "Task", + "Resource": "arn:aws:states:::states:startExecution.sync", + "Parameters": { + "StateMachineArn.$": "$.StateMachineArn", + "Input.$": "$.Input", + "Name.$": "$.Name" + }, + "End": true, + } + } +} \ No newline at end of file diff --git a/tests/integration/stepfunctions/templates/services/services_templates.py b/tests/integration/stepfunctions/templates/services/services_templates.py index 08b1b70007ac0..6277a8e6b875f 100644 --- a/tests/integration/stepfunctions/templates/services/services_templates.py +++ b/tests/integration/stepfunctions/templates/services/services_templates.py @@ -22,6 +22,9 @@ class ServicesTemplates(TemplateLoader): LAMBDA_LIST_FUNCTIONS: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/lambda_list_functions.json5" ) + SFN_START_EXECUTION: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/sfn_start_execution.json5" + ) # Lambda Functions. LAMBDA_ID_FUNCTION: Final[str] = os.path.join(_THIS_FOLDER, "lambdafunctions/id_function.py") diff --git a/tests/integration/stepfunctions/templates/services/statemachines/sfn_start_execution.json5 b/tests/integration/stepfunctions/templates/services/statemachines/sfn_start_execution.json5 new file mode 100644 index 0000000000000..934864d45c5b1 --- /dev/null +++ b/tests/integration/stepfunctions/templates/services/statemachines/sfn_start_execution.json5 @@ -0,0 +1,16 @@ +{ + "Comment": "SFN_START_EXECUTION", + "StartAt": "StartExecution", + "States": { + "StartExecution": { + "Type": "Task", + "Resource": "arn:aws:states:::states:startExecution", + "Parameters": { + "StateMachineArn.$": "$.StateMachineArn", + "Input.$": "$.Input", + "Name.$": "$.Name" + }, + "End": true, + } + } +} \ No newline at end of file diff --git a/tests/integration/stepfunctions/utils.py b/tests/integration/stepfunctions/utils.py index 25babfc07c887..3c252ba5eae3a 100644 --- a/tests/integration/stepfunctions/utils.py +++ b/tests/integration/stepfunctions/utils.py @@ -143,13 +143,11 @@ def _run_check(): LOG.warning(f"Timed out whilst awaiting for execution '{execution_arn}' to abort.") -def create_and_record_execution( - stepfunctions_client, +def create( create_iam_role_for_sfn, create_state_machine, snapshot, definition, - execution_input, ): snf_role_arn = create_iam_role_for_sfn() snapshot.add_transformer(RegexTransformer(snf_role_arn, "snf_role_arn")) @@ -167,6 +165,18 @@ def create_and_record_execution( creation_resp = create_state_machine(name=sm_name, definition=definition, roleArn=snf_role_arn) snapshot.add_transformer(snapshot.transform.sfn_sm_create_arn(creation_resp, 0)) state_machine_arn = creation_resp["stateMachineArn"] + return state_machine_arn + + +def create_and_record_execution( + stepfunctions_client, + create_iam_role_for_sfn, + create_state_machine, + snapshot, + definition, + execution_input, +): + state_machine_arn = create(create_iam_role_for_sfn, create_state_machine, snapshot, definition) exec_resp = stepfunctions_client.start_execution( stateMachineArn=state_machine_arn, input=execution_input diff --git a/tests/integration/stepfunctions/v2/base/__init__.py b/tests/integration/stepfunctions/v2/base/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/integration/stepfunctions/v2/base/test_base.py b/tests/integration/stepfunctions/v2/base/test_base.py new file mode 100644 index 0000000000000..bc8db7a022a99 --- /dev/null +++ b/tests/integration/stepfunctions/v2/base/test_base.py @@ -0,0 +1,35 @@ +import json + +import pytest + +from tests.integration.stepfunctions.templates.base.base_templates import BaseTemplate +from tests.integration.stepfunctions.utils import create_and_record_execution, is_old_provider + +pytestmark = pytest.mark.skipif( + condition=is_old_provider(), reason="Test suite for v2 provider only." +) + + +@pytest.mark.skip_snapshot_verify( + paths=["$..loggingConfiguration", "$..tracingConfiguration", "$..previousEventId"] +) +class TestSnfApi: + def test_state_fail( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + template = BaseTemplate.load_sfn_template(BaseTemplate.BASE_RAISE_FAILURE) + definition = json.dumps(template) + + exec_input = json.dumps({}) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) diff --git a/tests/integration/stepfunctions/v2/base/test_base.snapshot.json b/tests/integration/stepfunctions/v2/base/test_base.snapshot.json new file mode 100644 index 0000000000000..91f6e65a0b8dd --- /dev/null +++ b/tests/integration/stepfunctions/v2/base/test_base.snapshot.json @@ -0,0 +1,51 @@ +{ + "tests/integration/stepfunctions/v2/base/test_base.py::TestSnfApi::test_state_fail": { + "recorded-date": "05-07-2023, 12:21:19", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "FailState" + }, + "timestamp": "timestamp", + "type": "FailStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "This state machines raises a 'SomeFailure' failure.", + "error": "SomeFailure" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + } +} diff --git a/tests/integration/stepfunctions/v2/callback/test_callback.py b/tests/integration/stepfunctions/v2/callback/test_callback.py index 96e717e50ade8..25a4fd2616f87 100644 --- a/tests/integration/stepfunctions/v2/callback/test_callback.py +++ b/tests/integration/stepfunctions/v2/callback/test_callback.py @@ -4,13 +4,18 @@ from localstack.testing.snapshots.transformer import JsonpathTransformer, RegexTransformer from localstack.utils.strings import short_uid +from tests.integration.stepfunctions.templates.base.base_templates import BaseTemplate as BT from tests.integration.stepfunctions.templates.callbacks.callback_templates import ( CallbackTemplates as CT, ) from tests.integration.stepfunctions.templates.timeouts.timeout_templates import ( TimeoutTemplates as TT, ) -from tests.integration.stepfunctions.utils import create_and_record_execution, is_old_provider +from tests.integration.stepfunctions.utils import ( + create, + create_and_record_execution, + is_old_provider, +) pytestmark = pytest.mark.skipif( condition=is_old_provider(), reason="Test suite for v2 provider only." @@ -185,3 +190,171 @@ def test_sqs_wait_for_task_tok_with_heartbeat( definition, exec_input, ) + + def test_start_execution_sync( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..output.StartDate", + replacement="start-date", + replace_reference=False, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..output.StopDate", + replacement="stop-date", + replace_reference=False, + ) + ) + + template_target = BT.load_sfn_template(BT.BASE_PASS_RESULT) + definition_target = json.dumps(template_target) + state_machine_arn_target = create( + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition_target, + ) + + template = CT.load_sfn_template(CT.SFN_START_EXECUTION_SYNC) + definition = json.dumps(template) + + exec_input = json.dumps( + {"StateMachineArn": state_machine_arn_target, "Input": None, "Name": "TestStartTarget"} + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + + def test_start_execution_sync_delegate_failure( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..output.StartDate", + replacement="start-date", + replace_reference=False, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..cause.StartDate", + replacement="start-date", + replace_reference=False, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..cause.StopDate", + replacement="stop-date", + replace_reference=False, + ) + ) + + template_target = BT.load_sfn_template(BT.BASE_RAISE_FAILURE) + definition_target = json.dumps(template_target) + state_machine_arn_target = create( + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition_target, + ) + + template = CT.load_sfn_template(CT.SFN_START_EXECUTION_SYNC) + definition = json.dumps(template) + + exec_input = json.dumps( + {"StateMachineArn": state_machine_arn_target, "Input": None, "Name": "TestStartTarget"} + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + + def test_start_execution_sync_delegate_timeout( + self, + aws_client, + create_lambda_function, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..output.StartDate", + replacement="start-date", + replace_reference=False, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..cause.StartDate", + replacement="start-date", + replace_reference=False, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..cause.StopDate", + replacement="stop-date", + replace_reference=False, + ) + ) + + function_name = f"lambda_1_func_{short_uid()}" + lambda_creation_response = create_lambda_function( + func_name=function_name, + handler_file=TT.LAMBDA_WAIT_60_SECONDS, + runtime="python3.9", + ) + sfn_snapshot.add_transformer(RegexTransformer(function_name, "")) + lambda_arn = lambda_creation_response["CreateFunctionResponse"]["FunctionArn"] + + template_target = TT.load_sfn_template(TT.LAMBDA_WAIT_WITH_TIMEOUT_SECONDS) + template_target["States"]["Start"]["Resource"] = lambda_arn + definition_target = json.dumps(template_target) + + state_machine_arn_target = create( + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition_target, + ) + + template = CT.load_sfn_template(CT.SFN_START_EXECUTION_SYNC) + definition = json.dumps(template) + + exec_input = json.dumps( + { + "StateMachineArn": state_machine_arn_target, + "Input": {"Payload": None}, + "Name": "TestStartTarget", + } + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) diff --git a/tests/integration/stepfunctions/v2/callback/test_callback.snapshot.json b/tests/integration/stepfunctions/v2/callback/test_callback.snapshot.json index fee0bde65c621..c004245dabd3c 100644 --- a/tests/integration/stepfunctions/v2/callback/test_callback.snapshot.json +++ b/tests/integration/stepfunctions/v2/callback/test_callback.snapshot.json @@ -577,5 +577,543 @@ } } } + }, + "tests/integration/stepfunctions/v2/callback/test_callback.py::TestCallback::test_start_execution_sync": { + "recorded-date": "30-06-2023, 14:42:19", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Input": null, + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Name": "TestStartTarget" + }, + "region": "", + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "161" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "161", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "Input": "{}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": "{\"Arg1\":\"argument1\"}", + "OutputDetails": { + "Included": true + }, + "StartDate": "start-date", + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Status": "SUCCEEDED", + "StopDate": "stop-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "StartExecution", + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "Input": "{}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": "{\"Arg1\":\"argument1\"}", + "OutputDetails": { + "Included": true + }, + "StartDate": "start-date", + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Status": "SUCCEEDED", + "StopDate": "stop-date" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "Input": "{}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "Output": "{\"Arg1\":\"argument1\"}", + "OutputDetails": { + "Included": true + }, + "StartDate": "start-date", + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Status": "SUCCEEDED", + "StopDate": "stop-date" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/integration/stepfunctions/v2/callback/test_callback.py::TestCallback::test_start_execution_sync_delegate_failure": { + "recorded-date": "05-07-2023, 15:55:56", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Input": null, + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Name": "TestStartTarget" + }, + "region": "", + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "161" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "161", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskFailedEventDetails": { + "cause": { + "Cause": "This state machines raises a 'SomeFailure' failure.", + "Error": "SomeFailure", + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "Input": "{}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "StartDate": "start-date", + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Status": "FAILED", + "StopDate": "stop-date" + }, + "error": "States.TaskFailed", + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskFailed" + }, + { + "executionFailedEventDetails": { + "cause": { + "Cause": "This state machines raises a 'SomeFailure' failure.", + "Error": "SomeFailure", + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "Input": "{}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "StartDate": "start-date", + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Status": "FAILED", + "StopDate": "stop-date" + }, + "error": "States.TaskFailed" + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/integration/stepfunctions/v2/callback/test_callback.py::TestCallback::test_start_execution_sync_delegate_timeout": { + "recorded-date": "05-07-2023, 16:16:56", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": { + "Payload": null + }, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": { + "Payload": null + }, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Input": { + "Payload": null + }, + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Name": "TestStartTarget" + }, + "region": "", + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "161" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "161", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskFailedEventDetails": { + "cause": { + "Error": "States.Timeout", + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "Input": "{\"Payload\":null}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "StartDate": "start-date", + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Status": "FAILED", + "StopDate": "stop-date" + }, + "error": "States.TaskFailed", + "resource": "startExecution.sync", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskFailed" + }, + { + "executionFailedEventDetails": { + "cause": { + "Error": "States.Timeout", + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "Input": "{\"Payload\":null}", + "InputDetails": { + "Included": true + }, + "Name": "TestStartTarget", + "StartDate": "start-date", + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Status": "FAILED", + "StopDate": "stop-date" + }, + "error": "States.TaskFailed" + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/integration/stepfunctions/v2/error_handling/test_task_service_sfn.py b/tests/integration/stepfunctions/v2/error_handling/test_task_service_sfn.py new file mode 100644 index 0000000000000..2756ec6dad456 --- /dev/null +++ b/tests/integration/stepfunctions/v2/error_handling/test_task_service_sfn.py @@ -0,0 +1,78 @@ +import json + +import pytest + +from localstack.testing.snapshots.transformer import JsonpathTransformer, RegexTransformer +from localstack.utils.strings import short_uid +from tests.integration.stepfunctions.templates.base.base_templates import BaseTemplate as BT +from tests.integration.stepfunctions.templates.services.services_templates import ( + ServicesTemplates as ST, +) +from tests.integration.stepfunctions.utils import ( + create, + create_and_record_execution, + is_old_provider, +) + +pytestmark = pytest.mark.skipif( + condition=is_old_provider(), reason="Test suite for v2 provider only." +) + + +@pytest.mark.skip_snapshot_verify( + paths=[ + "$..loggingConfiguration", + "$..tracingConfiguration", + "$..previousEventId", + # TODO: add support for Sdk Http metadata. + "$..SdkHttpMetadata", + "$..SdkResponseMetadata", + ] +) +class TestTaskServiceSfn: + def test_start_execution_no_such_arn( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..output.StartDate", + replacement="start-date", + replace_reference=False, + ) + ) + + template_target = BT.load_sfn_template(BT.BASE_PASS_RESULT) + definition_target = json.dumps(template_target) + state_machine_arn_target = create( + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition_target, + ) + aws_client.stepfunctions.delete_state_machine(stateMachineArn=state_machine_arn_target) + + template = ST.load_sfn_template(ST.SFN_START_EXECUTION) + definition = json.dumps(template) + + random_arn_part = f"NoSuchArn{short_uid()}" + sfn_snapshot.add_transformer(RegexTransformer(random_arn_part, "")) + + exec_input = json.dumps( + { + "StateMachineArn": f"{state_machine_arn_target}{random_arn_part}", + "Input": None, + "Name": "TestStartTarget", + } + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) diff --git a/tests/integration/stepfunctions/v2/error_handling/test_task_service_sfn.snapshot.json b/tests/integration/stepfunctions/v2/error_handling/test_task_service_sfn.snapshot.json new file mode 100644 index 0000000000000..b39d354580583 --- /dev/null +++ b/tests/integration/stepfunctions/v2/error_handling/test_task_service_sfn.snapshot.json @@ -0,0 +1,97 @@ +{ + "tests/integration/stepfunctions/v2/error_handling/test_task_service_sfn.py::TestTaskServiceSfn::test_start_execution_no_such_arn": { + "recorded-date": "30-06-2023, 09:50:17", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Input": null, + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Name": "TestStartTarget" + }, + "region": "", + "resource": "startExecution", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "startExecution", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskFailedEventDetails": { + "cause": "State Machine Does Not Exist: 'arn:aws:states::111111111111:stateMachine:' (Service: AWSStepFunctions; Status Code: 400; Error Code: StateMachineDoesNotExist; Request ID: ; Proxy: null)", + "error": "StepFunctions.StateMachineDoesNotExistException", + "resource": "startExecution", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskFailed" + }, + { + "executionFailedEventDetails": { + "cause": "State Machine Does Not Exist: 'arn:aws:states::111111111111:stateMachine:' (Service: AWSStepFunctions; Status Code: 400; Error Code: StateMachineDoesNotExist; Request ID: ; Proxy: null)", + "error": "StepFunctions.StateMachineDoesNotExistException" + }, + "id": 6, + "previousEventId": 5, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + } +} diff --git a/tests/integration/stepfunctions/v2/error_handling/test_task_service_sqs.py b/tests/integration/stepfunctions/v2/error_handling/test_task_service_sqs.py index c4697a50ff205..e3bd810a1e874 100644 --- a/tests/integration/stepfunctions/v2/error_handling/test_task_service_sqs.py +++ b/tests/integration/stepfunctions/v2/error_handling/test_task_service_sqs.py @@ -87,6 +87,7 @@ def test_send_message_no_such_queue_no_catch( exec_input, ) + @pytest.mark.skip("SQS does not raise error on empty body.") def test_send_message_empty_body( self, aws_client, diff --git a/tests/integration/stepfunctions/v2/services/test_sfn_task_service.py b/tests/integration/stepfunctions/v2/services/test_sfn_task_service.py new file mode 100644 index 0000000000000..57c27b1225c1b --- /dev/null +++ b/tests/integration/stepfunctions/v2/services/test_sfn_task_service.py @@ -0,0 +1,112 @@ +import json + +import pytest + +from localstack.testing.snapshots.transformer import JsonpathTransformer +from tests.integration.stepfunctions.templates.base.base_templates import BaseTemplate as BT +from tests.integration.stepfunctions.templates.services.services_templates import ( + ServicesTemplates as ST, +) +from tests.integration.stepfunctions.utils import ( + create, + create_and_record_execution, + is_old_provider, +) + +pytestmark = pytest.mark.skipif( + condition=is_old_provider(), reason="Test suite for v2 provider only." +) + + +@pytest.mark.skip_snapshot_verify( + paths=[ + "$..loggingConfiguration", + "$..tracingConfiguration", + "$..previousEventId", + # TODO: add support for Sdk Http metadata. + "$..SdkHttpMetadata", + "$..SdkResponseMetadata", + ] +) +class TestTaskServiceSfn: + def test_start_execution( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..output.StartDate", + replacement="start-date", + replace_reference=False, + ) + ) + + template_target = BT.load_sfn_template(BT.BASE_PASS_RESULT) + definition_target = json.dumps(template_target) + state_machine_arn_target = create( + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition_target, + ) + + template = ST.load_sfn_template(ST.SFN_START_EXECUTION) + definition = json.dumps(template) + + exec_input = json.dumps( + {"StateMachineArn": state_machine_arn_target, "Input": None, "Name": "TestStartTarget"} + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + + def test_start_execution_input_json( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..output.StartDate", + replacement="start-date", + replace_reference=False, + ) + ) + + template_target = BT.load_sfn_template(BT.BASE_PASS_RESULT) + definition_target = json.dumps(template_target) + state_machine_arn_target = create( + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition_target, + ) + + template = ST.load_sfn_template(ST.SFN_START_EXECUTION) + definition = json.dumps(template) + + exec_input = json.dumps( + { + "StateMachineArn": state_machine_arn_target, + "Input": {"Hello": "World"}, + "Name": "TestStartTarget", + } + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) diff --git a/tests/integration/stepfunctions/v2/services/test_sfn_task_service.snapshot.json b/tests/integration/stepfunctions/v2/services/test_sfn_task_service.snapshot.json new file mode 100644 index 0000000000000..176503a77d7b3 --- /dev/null +++ b/tests/integration/stepfunctions/v2/services/test_sfn_task_service.snapshot.json @@ -0,0 +1,418 @@ +{ + "tests/integration/stepfunctions/v2/services/test_sfn_task_service.py::TestTaskServiceSfn::test_start_execution": { + "recorded-date": "28-06-2023, 11:07:54", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": null, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Input": null, + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Name": "TestStartTarget" + }, + "region": "", + "resource": "startExecution", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "startExecution", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSucceededEventDetails": { + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "160" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "160", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "StartExecution", + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "160" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "160", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "160" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "160", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/integration/stepfunctions/v2/services/test_sfn_task_service.py::TestTaskServiceSfn::test_start_execution_input_json": { + "recorded-date": "28-06-2023, 11:12:13", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": { + "Hello": "World" + }, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Input": { + "Hello": "World" + }, + "Name": "TestStartTarget" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Input": { + "Hello": "World" + }, + "StateMachineArn": "arn:aws:states::111111111111:stateMachine:", + "Name": "TestStartTarget" + }, + "region": "", + "resource": "startExecution", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "startExecution", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSucceededEventDetails": { + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "161" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "161", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + }, + "resource": "startExecution", + "resourceType": "states" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "StartExecution", + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "161" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "161", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "ExecutionArn": "arn:aws:states::111111111111:execution::TestStartTarget", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "161" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "161", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StartDate": "start-date" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + } +}