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,
|
ResourceNotFound,
|
||||||
StateMachineDoesNotExist,
|
StateMachineDoesNotExist,
|
||||||
)
|
)
|
||||||
from .utils import paginate
|
from .utils import paginate, api_to_cfn_tags, cfn_to_api_tags
|
||||||
|
|
||||||
|
|
||||||
class StateMachine(CloudFormationModel):
|
class StateMachine(CloudFormationModel):
|
||||||
@ -62,11 +62,39 @@ class StateMachine(CloudFormationModel):
|
|||||||
def physical_resource_id(self):
|
def physical_resource_id(self):
|
||||||
return self.arn
|
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):
|
def get_cfn_attribute(self, attribute_name):
|
||||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||||
|
|
||||||
if attribute_name == "Name":
|
if attribute_name == "Name":
|
||||||
return self.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()
|
raise UnformattedGetAttTemplateException()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -85,18 +113,46 @@ class StateMachine(CloudFormationModel):
|
|||||||
name = properties.get("StateMachineName", resource_name)
|
name = properties.get("StateMachineName", resource_name)
|
||||||
definition = properties.get("DefinitionString", "")
|
definition = properties.get("DefinitionString", "")
|
||||||
role_arn = properties.get("RoleArn", "")
|
role_arn = properties.get("RoleArn", "")
|
||||||
tags = properties.get("Tags", [])
|
tags = cfn_to_api_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]
|
sf_backend = stepfunction_backends[region_name]
|
||||||
return sf_backend.create_state_machine(
|
return sf_backend.create_state_machine(name, definition, role_arn, tags=tags)
|
||||||
name, definition, role_arn, tags=tags_xform
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete_from_cloudformation_json(cls, resource_name, _, region_name):
|
def delete_from_cloudformation_json(cls, resource_name, _, region_name):
|
||||||
sf_backend = stepfunction_backends[region_name]
|
sf_backend = stepfunction_backends[region_name]
|
||||||
sf_backend.delete_state_machine(resource_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:
|
class Execution:
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -136,3 +136,13 @@ class Paginator(object):
|
|||||||
page_ending_result = results[index_end]
|
page_ending_result = results[index_end]
|
||||||
next_token = self._build_next_token(page_ending_result)
|
next_token = self._build_next_token(page_ending_result)
|
||||||
return results_page, next_token
|
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")
|
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():
|
def _get_account_id():
|
||||||
global account_id
|
global account_id
|
||||||
if account_id:
|
if account_id:
|
||||||
|
Loading…
Reference in New Issue
Block a user