From 171130fe7bc975904bade3ca259fc89818bcfe5a Mon Sep 17 00:00:00 2001 From: Brian Pandola Date: Mon, 2 Nov 2020 03:53:03 -0800 Subject: [PATCH] Add support for CloudFormation resource `AWS::StepFunctions::StateMachine` (#3429) Closes #3402 Co-authored-by: Bert Blommers --- moto/cloudformation/parsing.py | 1 + moto/stepfunctions/models.py | 43 ++++++++++++++- .../test_stepfunctions/test_stepfunctions.py | 54 ++++++++++++++++++- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 760142033..168536f79 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -38,6 +38,7 @@ from moto.s3 import models as s3_models, s3_backend # noqa 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 index 3731539f8..9dfa33ba8 100644 --- a/moto/stepfunctions/models.py +++ b/moto/stepfunctions/models.py @@ -4,7 +4,7 @@ from datetime import datetime 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 .exceptions import ( 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 @@ class StateMachine: 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__( diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py index e6592c2ff..1c961b882 100644 --- a/tests/test_stepfunctions/test_stepfunctions.py +++ b/tests/test_stepfunctions/test_stepfunctions.py @@ -8,7 +8,7 @@ from datetime import datetime 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: