Repository: getmoto/moto

Issue:
StepFunctions StateMachine cannot yet  be updated by Cloudformation
In my project, we use CDK to generate a large cloudformation template
We feed this cloudformation template into moto in our pytest suite so a number of things 'just work' like they would in the cloud.

I'm trying to upgrade to 1.3.15, and this setup broke.
When referencing a Statemachine in CDK, CDK emits cloudformation like this
`"Fn::Select": [6, {"Fn::Split": [":", {"Ref": "MyCoolStateMachine"}]}]}, "*"]]}}`

This ends up giving us this error

```
resource_json = {'Fn::Split': [':', {'Ref': 'MyCoolStateMachine'}]}, resources_map = <moto.cloudformation.parsing.ResourceMap object at 0x7fd2728eb220>

    def clean_json(resource_json, resources_map):
        """
        Cleanup the a resource dict. For now, this just means replacing any Ref node
        with the corresponding physical_resource_id.

        Eventually, this is where we would add things like function parsing (fn::)
        """
        if isinstance(resource_json, dict):
            if "Ref" in resource_json:
                # Parse resource reference
                resource = resources_map[resource_json["Ref"]]
                if hasattr(resource, "physical_resource_id"):
                    return resource.physical_resource_id
                else:
                    return resource

            if "Fn::FindInMap" in resource_json:
                map_name = resource_json["Fn::FindInMap"][0]
                map_path = resource_json["Fn::FindInMap"][1:]
                result = resources_map[map_name]
                for path in map_path:
                    if "Fn::Transform" in result:
                        result = resources_map[clean_json(path, resources_map)]
                    else:
                        result = result[clean_json(path, resources_map)]
                return result

            if "Fn::GetAtt" in resource_json:
                resource = resources_map.get(resource_json["Fn::GetAtt"][0])
                if resource is None:
                    return resource_json
                try:
                    return resource.get_cfn_attribute(resource_json["Fn::GetAtt"][1])
                except NotImplementedError as n:
                    logger.warning(str(n).format(resource_json["Fn::GetAtt"][0]))
                except UnformattedGetAttTemplateException:
                    raise ValidationError(
                        "Bad Request",
                        UnformattedGetAttTemplateException.description.format(
                            resource_json["Fn::GetAtt"][0], resource_json["Fn::GetAtt"][1]
                        ),
                    )

            if "Fn::If" in resource_json:
                condition_name, true_value, false_value = resource_json["Fn::If"]
                if resources_map.lazy_condition_map[condition_name]:
                    return clean_json(true_value, resources_map)
                else:
                    return clean_json(false_value, resources_map)

            if "Fn::Join" in resource_json:
                join_list = clean_json(resource_json["Fn::Join"][1], resources_map)
                return resource_json["Fn::Join"][0].join([str(x) for x in join_list]) 

            if "Fn::Split" in resource_json:
                to_split = clean_json(resource_json["Fn::Split"][1], resources_map)
>               return to_split.split(resource_json["Fn::Split"][0])

```

Looks like the resource Map value is `None`, if I had it guess it's because StateMachine doesn't implement CloudformationModel.


We are working around this problem with a simple monkey patch 
```

    from moto.cloudformation import parsing 
    cj = parsing.clean_json
    
    def new_cj(json, map):
        cleaned = cj(json, map)
        if cleaned is None:
            if json["Ref"].endswith("StateMachine"):
                return f'arn:aws:states:us-east-1:111122223333:stateMachine:{json["Ref"]}'
        return cleaned
    parsing.clean_json = new_cj
```

This is clearly not ideal, but allows us to upgrade. 


I was not able to find another issue for SFN support in Cloudformation




Discussion:
Similarly, when deleting the stack above we see

```
    def delete(self):
        remaining_resources = set(self.resources)
        tries = 1
        while remaining_resources and tries < 5:
            for resource in remaining_resources.copy():                parsed_resource = self._parsed_resources.get(resource)
                try:
                    if parsed_resource and hasattr(parsed_resource, "delete"):
                        parsed_resource.delete(self._region_name)
                    else:
                        if hasattr(parsed_resource, "physical_resource_id"):
                            resource_name = parsed_resource.physical_resource_id
                        else:
                            resource_name = None

                        resource_json = self._resource_json_map[
>                           parsed_resource.logical_resource_id
                        ]

```
Which is likely the Statemachine that has a `None` resource.
Thanks for raising this, @prestomation. 
In 1.3.14, there was no support for SFN at all, so I guess it just ignored that setup. We did introduce basic support for it in 1.3.15, but indeed no CloudFromation support yet. Marking it as an enhancement.

FYI: 1.3.15 has a bug surrounding dependency management, so you might be better off upgrading to 1.3.16 - literraly the only change in that version is a bugfix for 1.3.15.

Resolving patch:
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -38,6 +38,7 @@
 from moto.s3.utils import bucket_and_name_from_url
 from moto.sns import models as sns_models  # noqa
 from moto.sqs import models as sqs_models  # noqa
+from moto.stepfunctions import models as stepfunctions_models  # noqa
 
 # End ugly list of imports
 
diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py
--- a/moto/stepfunctions/models.py
+++ b/moto/stepfunctions/models.py
@@ -4,7 +4,7 @@
 
 from boto3 import Session
 
-from moto.core import ACCOUNT_ID, BaseBackend
+from moto.core import ACCOUNT_ID, BaseBackend, CloudFormationModel
 from moto.core.utils import iso_8601_datetime_with_milliseconds
 from uuid import uuid4
 from .exceptions import (
@@ -18,7 +18,7 @@
 from .utils import paginate
 
 
-class StateMachine:
+class StateMachine(CloudFormationModel):
     def __init__(self, arn, name, definition, roleArn, tags=None):
         self.creation_date = iso_8601_datetime_with_milliseconds(datetime.now())
         self.arn = arn
@@ -27,6 +27,45 @@ def __init__(self, arn, name, definition, roleArn, tags=None):
         self.roleArn = roleArn
         self.tags = tags
 
+    @property
+    def physical_resource_id(self):
+        return self.arn
+
+    def get_cfn_attribute(self, attribute_name):
+        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
+
+        if attribute_name == "Name":
+            return self.name
+        raise UnformattedGetAttTemplateException()
+
+    @staticmethod
+    def cloudformation_name_type():
+        return "StateMachine"
+
+    @staticmethod
+    def cloudformation_type():
+        return "AWS::StepFunctions::StateMachine"
+
+    @classmethod
+    def create_from_cloudformation_json(
+        cls, resource_name, cloudformation_json, region_name
+    ):
+        properties = cloudformation_json["Properties"]
+        name = properties.get("StateMachineName", resource_name)
+        definition = properties.get("DefinitionString", "")
+        role_arn = properties.get("RoleArn", "")
+        tags = properties.get("Tags", [])
+        tags_xform = [{k.lower(): v for k, v in d.items()} for d in tags]
+        sf_backend = stepfunction_backends[region_name]
+        return sf_backend.create_state_machine(
+            name, definition, role_arn, tags=tags_xform
+        )
+
+    @classmethod
+    def delete_from_cloudformation_json(cls, resource_name, _, region_name):
+        sf_backend = stepfunction_backends[region_name]
+        sf_backend.delete_state_machine(resource_name)
+
 
 class Execution:
     def __init__(


Test patch:
diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py
--- a/tests/test_stepfunctions/test_stepfunctions.py
+++ b/tests/test_stepfunctions/test_stepfunctions.py
@@ -8,7 +8,7 @@
 from botocore.exceptions import ClientError
 from nose.tools import assert_raises
 
-from moto import mock_sts, mock_stepfunctions
+from moto import mock_cloudformation, mock_sts, mock_stepfunctions
 from moto.core import ACCOUNT_ID
 
 region = "us-east-1"
@@ -709,6 +709,58 @@ def test_state_machine_describe_execution_after_stoppage():
     description["stopDate"].should.be.a(datetime)
 
 
+@mock_stepfunctions
+@mock_cloudformation
+def test_state_machine_cloudformation():
+    sf = boto3.client("stepfunctions", region_name="us-east-1")
+    cf = boto3.resource("cloudformation", region_name="us-east-1")
+    definition = '{"StartAt": "HelloWorld", "States": {"HelloWorld": {"Type": "Task", "Resource": "arn:aws:lambda:us-east-1:111122223333;:function:HelloFunction", "End": true}}}'
+    role_arn = (
+        "arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1;"
+    )
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "An example template for a Step Functions state machine.",
+        "Resources": {
+            "MyStateMachine": {
+                "Type": "AWS::StepFunctions::StateMachine",
+                "Properties": {
+                    "StateMachineName": "HelloWorld-StateMachine",
+                    "StateMachineType": "STANDARD",
+                    "DefinitionString": definition,
+                    "RoleArn": role_arn,
+                    "Tags": [
+                        {"Key": "key1", "Value": "value1"},
+                        {"Key": "key2", "Value": "value2"},
+                    ],
+                },
+            }
+        },
+        "Outputs": {
+            "StateMachineArn": {"Value": {"Ref": "MyStateMachine"}},
+            "StateMachineName": {"Value": {"Fn::GetAtt": ["MyStateMachine", "Name"]}},
+        },
+    }
+    cf.create_stack(StackName="test_stack", TemplateBody=json.dumps(template))
+    outputs_list = cf.Stack("test_stack").outputs
+    output = {item["OutputKey"]: item["OutputValue"] for item in outputs_list}
+    state_machine = sf.describe_state_machine(stateMachineArn=output["StateMachineArn"])
+    state_machine["stateMachineArn"].should.equal(output["StateMachineArn"])
+    state_machine["name"].should.equal(output["StateMachineName"])
+    state_machine["roleArn"].should.equal(role_arn)
+    state_machine["definition"].should.equal(definition)
+    tags = sf.list_tags_for_resource(resourceArn=output["StateMachineArn"]).get("tags")
+    for i, tag in enumerate(tags, 1):
+        tag["key"].should.equal("key{}".format(i))
+        tag["value"].should.equal("value{}".format(i))
+
+    cf.Stack("test_stack").delete()
+    with assert_raises(ClientError) as ex:
+        sf.describe_state_machine(stateMachineArn=output["StateMachineArn"])
+    ex.exception.response["Error"]["Code"].should.equal("StateMachineDoesNotExist")
+    ex.exception.response["Error"]["Message"].should.contain("Does Not Exist")
+
+
 def _get_account_id():
     global account_id
     if account_id: