Add CloudFormation Update support for AWS::StepFunctions::StateMachine (#3440)
				
					
				
			Closes #3402
This commit is contained in:
		
							parent
							
								
									725ad7571d
								
							
						
					
					
						commit
						2f70373c2e
					
				| @ -16,7 +16,7 @@ from .exceptions import ( | ||||
|     ResourceNotFound, | ||||
|     StateMachineDoesNotExist, | ||||
| ) | ||||
| from .utils import paginate | ||||
| from .utils import paginate, api_to_cfn_tags, cfn_to_api_tags | ||||
| 
 | ||||
| 
 | ||||
| class StateMachine(CloudFormationModel): | ||||
| @ -62,11 +62,39 @@ class StateMachine(CloudFormationModel): | ||||
|     def physical_resource_id(self): | ||||
|         return self.arn | ||||
| 
 | ||||
|     def get_cfn_properties(self, prop_overrides): | ||||
|         property_names = [ | ||||
|             "DefinitionString", | ||||
|             "RoleArn", | ||||
|             "StateMachineName", | ||||
|         ] | ||||
|         properties = {} | ||||
|         for prop in property_names: | ||||
|             properties[prop] = prop_overrides.get(prop, self.get_cfn_attribute(prop)) | ||||
|         # Special handling for Tags | ||||
|         overridden_keys = [tag["Key"] for tag in prop_overrides.get("Tags", [])] | ||||
|         original_tags_to_include = [ | ||||
|             tag | ||||
|             for tag in self.get_cfn_attribute("Tags") | ||||
|             if tag["Key"] not in overridden_keys | ||||
|         ] | ||||
|         properties["Tags"] = original_tags_to_include + prop_overrides.get("Tags", []) | ||||
|         return properties | ||||
| 
 | ||||
|     def get_cfn_attribute(self, attribute_name): | ||||
|         from moto.cloudformation.exceptions import UnformattedGetAttTemplateException | ||||
| 
 | ||||
|         if attribute_name == "Name": | ||||
|             return self.name | ||||
|         elif attribute_name == "DefinitionString": | ||||
|             return self.definition | ||||
|         elif attribute_name == "RoleArn": | ||||
|             return self.roleArn | ||||
|         elif attribute_name == "StateMachineName": | ||||
|             return self.name | ||||
|         elif attribute_name == "Tags": | ||||
|             return api_to_cfn_tags(self.tags) | ||||
| 
 | ||||
|         raise UnformattedGetAttTemplateException() | ||||
| 
 | ||||
|     @staticmethod | ||||
| @ -85,18 +113,46 @@ class StateMachine(CloudFormationModel): | ||||
|         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] | ||||
|         tags = cfn_to_api_tags(properties.get("Tags", [])) | ||||
|         sf_backend = stepfunction_backends[region_name] | ||||
|         return sf_backend.create_state_machine( | ||||
|             name, definition, role_arn, tags=tags_xform | ||||
|         ) | ||||
|         return sf_backend.create_state_machine(name, definition, role_arn, tags=tags) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def delete_from_cloudformation_json(cls, resource_name, _, region_name): | ||||
|         sf_backend = stepfunction_backends[region_name] | ||||
|         sf_backend.delete_state_machine(resource_name) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def update_from_cloudformation_json( | ||||
|         cls, original_resource, new_resource_name, cloudformation_json, region_name | ||||
|     ): | ||||
|         properties = cloudformation_json.get("Properties", {}) | ||||
|         name = properties.get("StateMachineName", original_resource.name) | ||||
| 
 | ||||
|         if name != original_resource.name: | ||||
|             # Replacement | ||||
|             new_properties = original_resource.get_cfn_properties(properties) | ||||
|             cloudformation_json["Properties"] = new_properties | ||||
|             new_resource = cls.create_from_cloudformation_json( | ||||
|                 name, cloudformation_json, region_name | ||||
|             ) | ||||
|             cls.delete_from_cloudformation_json( | ||||
|                 original_resource.arn, cloudformation_json, region_name | ||||
|             ) | ||||
|             return new_resource | ||||
| 
 | ||||
|         else: | ||||
|             # No Interruption | ||||
|             definition = properties.get("DefinitionString") | ||||
|             role_arn = properties.get("RoleArn") | ||||
|             tags = cfn_to_api_tags(properties.get("Tags", [])) | ||||
|             sf_backend = stepfunction_backends[region_name] | ||||
|             state_machine = sf_backend.update_state_machine( | ||||
|                 original_resource.arn, definition=definition, role_arn=role_arn, | ||||
|             ) | ||||
|             state_machine.add_tags(tags) | ||||
|             return state_machine | ||||
| 
 | ||||
| 
 | ||||
| class Execution: | ||||
|     def __init__( | ||||
|  | ||||
| @ -136,3 +136,13 @@ class Paginator(object): | ||||
|             page_ending_result = results[index_end] | ||||
|             next_token = self._build_next_token(page_ending_result) | ||||
|         return results_page, next_token | ||||
| 
 | ||||
| 
 | ||||
| def cfn_to_api_tags(cfn_tags_entry): | ||||
|     api_tags = [{k.lower(): v for k, v in d.items()} for d in cfn_tags_entry] | ||||
|     return api_tags | ||||
| 
 | ||||
| 
 | ||||
| def api_to_cfn_tags(api_tags): | ||||
|     cfn_tags_entry = [{k.capitalize(): v for k, v in d.items()} for d in api_tags] | ||||
|     return cfn_tags_entry | ||||
|  | ||||
| @ -867,6 +867,148 @@ def test_state_machine_cloudformation(): | ||||
|     ex.exception.response["Error"]["Message"].should.contain("Does Not Exist") | ||||
| 
 | ||||
| 
 | ||||
| @mock_stepfunctions | ||||
| @mock_cloudformation | ||||
| def test_state_machine_cloudformation_update_with_replacement(): | ||||
|     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" | ||||
|     ) | ||||
|     properties = { | ||||
|         "StateMachineName": "HelloWorld-StateMachine", | ||||
|         "DefinitionString": definition, | ||||
|         "RoleArn": role_arn, | ||||
|         "Tags": [ | ||||
|             {"Key": "key1", "Value": "value1"}, | ||||
|             {"Key": "key2", "Value": "value2"}, | ||||
|         ], | ||||
|     } | ||||
|     template = { | ||||
|         "AWSTemplateFormatVersion": "2010-09-09", | ||||
|         "Description": "An example template for a Step Functions state machine.", | ||||
|         "Resources": { | ||||
|             "MyStateMachine": { | ||||
|                 "Type": "AWS::StepFunctions::StateMachine", | ||||
|                 "Properties": {}, | ||||
|             } | ||||
|         }, | ||||
|         "Outputs": { | ||||
|             "StateMachineArn": {"Value": {"Ref": "MyStateMachine"}}, | ||||
|             "StateMachineName": {"Value": {"Fn::GetAtt": ["MyStateMachine", "Name"]}}, | ||||
|         }, | ||||
|     } | ||||
|     template["Resources"]["MyStateMachine"]["Properties"] = properties | ||||
|     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"]) | ||||
|     original_machine_arn = state_machine["stateMachineArn"] | ||||
|     original_creation_date = state_machine["creationDate"] | ||||
| 
 | ||||
|     # Update State Machine, with replacement. | ||||
|     updated_role = role_arn + "-updated" | ||||
|     updated_definition = definition.replace("HelloWorld", "HelloWorld2") | ||||
|     updated_properties = { | ||||
|         "StateMachineName": "New-StateMachine-Name", | ||||
|         "DefinitionString": updated_definition, | ||||
|         "RoleArn": updated_role, | ||||
|         "Tags": [ | ||||
|             {"Key": "key3", "Value": "value3"}, | ||||
|             {"Key": "key1", "Value": "updated_value"}, | ||||
|         ], | ||||
|     } | ||||
|     template["Resources"]["MyStateMachine"]["Properties"] = updated_properties | ||||
|     cf.Stack("test_stack").update(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_not.equal(original_machine_arn) | ||||
|     state_machine["name"].should.equal("New-StateMachine-Name") | ||||
|     state_machine["creationDate"].should.be.greater_than(original_creation_date) | ||||
|     state_machine["roleArn"].should.equal(updated_role) | ||||
|     state_machine["definition"].should.equal(updated_definition) | ||||
|     tags = sf.list_tags_for_resource(resourceArn=output["StateMachineArn"]).get("tags") | ||||
|     tags.should.have.length_of(3) | ||||
|     for tag in tags: | ||||
|         if tag["key"] == "key1": | ||||
|             tag["value"].should.equal("updated_value") | ||||
| 
 | ||||
|     with assert_raises(ClientError) as ex: | ||||
|         sf.describe_state_machine(stateMachineArn=original_machine_arn) | ||||
|     ex.exception.response["Error"]["Code"].should.equal("StateMachineDoesNotExist") | ||||
|     ex.exception.response["Error"]["Message"].should.contain( | ||||
|         "State Machine Does Not Exist" | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @mock_stepfunctions | ||||
| @mock_cloudformation | ||||
| def test_state_machine_cloudformation_update_with_no_interruption(): | ||||
|     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" | ||||
|     ) | ||||
|     properties = { | ||||
|         "StateMachineName": "HelloWorld-StateMachine", | ||||
|         "DefinitionString": definition, | ||||
|         "RoleArn": role_arn, | ||||
|         "Tags": [ | ||||
|             {"Key": "key1", "Value": "value1"}, | ||||
|             {"Key": "key2", "Value": "value2"}, | ||||
|         ], | ||||
|     } | ||||
|     template = { | ||||
|         "AWSTemplateFormatVersion": "2010-09-09", | ||||
|         "Description": "An example template for a Step Functions state machine.", | ||||
|         "Resources": { | ||||
|             "MyStateMachine": { | ||||
|                 "Type": "AWS::StepFunctions::StateMachine", | ||||
|                 "Properties": {}, | ||||
|             } | ||||
|         }, | ||||
|         "Outputs": { | ||||
|             "StateMachineArn": {"Value": {"Ref": "MyStateMachine"}}, | ||||
|             "StateMachineName": {"Value": {"Fn::GetAtt": ["MyStateMachine", "Name"]}}, | ||||
|         }, | ||||
|     } | ||||
|     template["Resources"]["MyStateMachine"]["Properties"] = properties | ||||
|     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"]) | ||||
|     machine_arn = state_machine["stateMachineArn"] | ||||
|     creation_date = state_machine["creationDate"] | ||||
| 
 | ||||
|     # Update State Machine in-place, no replacement. | ||||
|     updated_role = role_arn + "-updated" | ||||
|     updated_definition = definition.replace("HelloWorld", "HelloWorldUpdated") | ||||
|     updated_properties = { | ||||
|         "DefinitionString": updated_definition, | ||||
|         "RoleArn": updated_role, | ||||
|         "Tags": [ | ||||
|             {"Key": "key3", "Value": "value3"}, | ||||
|             {"Key": "key1", "Value": "updated_value"}, | ||||
|         ], | ||||
|     } | ||||
|     template["Resources"]["MyStateMachine"]["Properties"] = updated_properties | ||||
|     cf.Stack("test_stack").update(TemplateBody=json.dumps(template)) | ||||
| 
 | ||||
|     state_machine = sf.describe_state_machine(stateMachineArn=machine_arn) | ||||
|     state_machine["name"].should.equal("HelloWorld-StateMachine") | ||||
|     state_machine["creationDate"].should.equal(creation_date) | ||||
|     state_machine["roleArn"].should.equal(updated_role) | ||||
|     state_machine["definition"].should.equal(updated_definition) | ||||
|     tags = sf.list_tags_for_resource(resourceArn=machine_arn).get("tags") | ||||
|     tags.should.have.length_of(3) | ||||
|     for tag in tags: | ||||
|         if tag["key"] == "key1": | ||||
|             tag["value"].should.equal("updated_value") | ||||
| 
 | ||||
| 
 | ||||
| def _get_account_id(): | ||||
|     global account_id | ||||
|     if account_id: | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user