Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@

import abc
import copy
import datetime
import logging
from typing import Any, Final

from botocore.model import ListShape, Shape, StringShape, StructureShape
import botocore.session
from botocore.model import (
ListShape,
OperationModel,
Shape,
StringShape,
StructureShape,
)
from botocore.response import StreamingBody

from moto.stepfunctions.parser.api import (
Expand Down Expand Up @@ -99,6 +107,27 @@ def _get_timed_out_failure_event(self, env: Environment) -> FailureEvent:
),
)

@staticmethod
def _get_boto_operation_model(
boto_service_name: str, service_action_name: str
) -> OperationModel:
norm_service_action_name = camel_to_snake_case(service_action_name)

service = botocore.session.get_session().get_service_model(boto_service_name)

boto_operation_names = {
camel_to_snake_case(operation_name): operation_name
for operation_name in service.operation_names
} # noqa
boto_operation_name = boto_operation_names.get(norm_service_action_name)
if boto_operation_name is None:
raise RuntimeError(
f"No api action named '{service_action_name}' available for service '{boto_service_name}'."
)

operation_model = service.operation_model(boto_operation_name)
return operation_model

def _to_boto_request_value(self, request_value: Any, value_shape: Shape) -> Any:
boto_request_value = request_value
if isinstance(value_shape, StructureShape):
Expand Down Expand Up @@ -159,6 +188,10 @@ def _from_boto_response_value(response_value: Any) -> Any:
if isinstance(response_value, StreamingBody):
body_str = to_str(response_value.read())
return body_str
if isinstance(response_value, datetime.datetime):
# Match the AWS JSON protocol representation of timestamps:
# the number of seconds since the epoch.
return response_value.timestamp()
return response_value

def _from_boto_response(
Expand Down Expand Up @@ -211,15 +244,37 @@ def _normalise_parameters(
boto_service_name: str | None = None,
service_action_name: str | None = None,
) -> None:
pass
boto_service_name = self._get_boto_service_name(
boto_service_name=boto_service_name
)
service_action_name = self._get_boto_service_action(
service_action_name=service_action_name
)
input_shape = self._get_boto_operation_model(
boto_service_name=boto_service_name,
service_action_name=service_action_name,
).input_shape
if input_shape is not None:
self._to_boto_request(parameters, input_shape) # noqa

def _normalise_response(
self,
response: Any,
boto_service_name: str | None = None,
service_action_name: str | None = None,
) -> None:
pass
boto_service_name = self._get_boto_service_name(
boto_service_name=boto_service_name
)
service_action_name = self._get_boto_service_action(
service_action_name=service_action_name
)
output_shape = self._get_boto_operation_model(
boto_service_name=boto_service_name,
service_action_name=service_action_name,
).output_shape
if output_shape is not None:
self._from_boto_response(response, output_shape) # noqa

def _verify_size_quota(self, env: Environment, value: Any) -> None:
is_within: bool = is_within_size_quota(value)
Expand Down
111 changes: 111 additions & 0 deletions tests/test_stepfunctions/parser/test_stepfunctions_sfn_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import json
from time import sleep
from unittest import SkipTest
from uuid import uuid4

import boto3
import pytest

from moto import settings

from . import allow_aws_request, aws_verified, sfn_role_policy

sfn_allow_start_execution = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"states:StartExecution",
"states:DescribeExecution",
"states:StopExecution",
"events:PutTargets",
"events:PutRule",
"events:DescribeRule",
],
"Resource": "*",
}
],
}


@aws_verified
@pytest.mark.aws_verified
def test_state_machine_calling_child_state_machine():
if settings.TEST_SERVER_MODE:
raise SkipTest("Don't need to test this in ServerMode")

# https://github.com/getmoto/moto/issues/10076
# The states:startExecution.sync:2 integration used to fail with
# KeyError: 'Input', because the PascalCase ASL parameters were not
# converted to the casing of the boto operation members.
iam = boto3.client("iam", region_name="us-east-1")
role_name = f"sfn_role_{str(uuid4())[0:6]}"
sfn_role = iam.create_role(
RoleName=role_name,
AssumeRolePolicyDocument=json.dumps(sfn_role_policy),
Path="/",
)["Role"]["Arn"]
iam.put_role_policy(
PolicyDocument=json.dumps(sfn_allow_start_execution),
PolicyName="allowStartExecution",
RoleName=role_name,
)

client = boto3.client("stepfunctions", region_name="us-east-1")
child_name = f"sfn_child_{str(uuid4())[0:6]}"
child_arn = client.create_state_machine(
name=child_name,
definition=json.dumps(
{"StartAt": "P", "States": {"P": {"Type": "Pass", "End": True}}}
),
roleArn=sfn_role,
)["stateMachineArn"]

parent_name = f"sfn_parent_{str(uuid4())[0:6]}"
parent_arn = client.create_state_machine(
name=parent_name,
definition=json.dumps(
{
"StartAt": "CallChild",
"States": {
"CallChild": {
"Type": "Task",
"Resource": "arn:aws:states:::states:startExecution.sync:2",
"Parameters": {
"Input": {"foo": "bar"},
"StateMachineArn": child_arn,
},
"End": True,
}
},
}
),
roleArn=sfn_role,
)["stateMachineArn"]

try:
execution_arn = client.start_execution(
name="run1", stateMachineArn=parent_arn, input="{}"
)["executionArn"]

execution = None
for _ in range(30):
execution = client.describe_execution(executionArn=execution_arn)
if execution["status"] != "RUNNING":
break
sleep(10 if allow_aws_request() else 0.2)
assert execution["status"] == "SUCCEEDED"

# The task result of a .sync:2 integration contains the (parsed)
# output of the child execution
output = json.loads(execution["output"])
assert output["Output"] == {"foo": "bar"}
finally:
for arn in [parent_arn, child_arn]:
for exc in client.list_executions(stateMachineArn=arn)["executions"]:
if exc["status"] == "RUNNING":
client.stop_execution(executionArn=exc["executionArn"])
client.delete_state_machine(stateMachineArn=arn)
iam.delete_role_policy(RoleName=role_name, PolicyName="allowStartExecution")
iam.delete_role(RoleName=role_name)
Loading