Add CloudFormation methods delete_change_set, describe_change_set and list_change_sets.

This commit is contained in:
Mark Challoner 2018-10-30 11:09:35 +00:00
parent 850496f29a
commit 337601b5fb
No known key found for this signature in database
GPG Key ID: 58EABDB1293D248B
5 changed files with 308 additions and 23 deletions

View File

@ -477,12 +477,12 @@
- [X] create_stack
- [ ] create_stack_instances
- [ ] create_stack_set
- [ ] delete_change_set
- [X] delete_change_set
- [X] delete_stack
- [ ] delete_stack_instances
- [ ] delete_stack_set
- [ ] describe_account_limits
- [ ] describe_change_set
- [X] describe_change_set
- [ ] describe_stack_events
- [ ] describe_stack_instance
- [ ] describe_stack_resource
@ -495,7 +495,7 @@
- [ ] get_stack_policy
- [ ] get_template
- [ ] get_template_summary
- [ ] list_change_sets
- [X] list_change_sets
- [X] list_exports
- [ ] list_imports
- [ ] list_stack_instances

View File

@ -127,6 +127,49 @@ class FakeStack(BaseModel):
self.status = "DELETE_COMPLETE"
class FakeChange(BaseModel):
def __init__(self, action, logical_resource_id, resource_type):
self.action = action
self.logical_resource_id = logical_resource_id
self.resource_type = resource_type
class FakeChangeSet(FakeStack):
def __init__(self, stack_id, stack_name, stack_template, change_set_id, change_set_name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, cross_stack_resources=None):
super(FakeChangeSet, self).__init__(
stack_id,
stack_name,
stack_template,
parameters,
region_name,
notification_arns=notification_arns,
tags=tags,
role_arn=role_arn,
cross_stack_resources=cross_stack_resources,
create_change_set=True,
)
self.stack_name = stack_name
self.change_set_id = change_set_id
self.change_set_name = change_set_name
self.changes = self.diff(template=template, parameters=parameters)
def diff(self, template, parameters=None):
self.template = template
self._parse_template()
changes = []
resources_by_action = self.resource_map.diff(self.template_dict, parameters)
for action, resources in resources_by_action.items():
for resource_name, resource in resources.items():
changes.append(FakeChange(
action=action,
logical_resource_id=resource_name,
resource_type=resource['ResourceType'],
))
return changes
class FakeEvent(BaseModel):
def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None):
@ -171,24 +214,62 @@ class CloudFormationBackend(BaseBackend):
return new_stack
def create_change_set(self, stack_name, change_set_name, template, parameters, region_name, change_set_type, notification_arns=None, tags=None, role_arn=None):
stack_id = None
stack_template = None
if change_set_type == 'UPDATE':
stacks = self.stacks.values()
stack = None
for s in stacks:
if s.name == stack_name:
stack = s
stack_id = stack.stack_id
stack_template = stack.template
if stack is None:
raise ValidationError(stack_name)
else:
stack = self.create_stack(stack_name, template, parameters,
region_name, notification_arns, tags,
role_arn, create_change_set=True)
stack_id = generate_stack_id(stack_name)
stack_template = template
change_set_id = generate_changeset_id(change_set_name, region_name)
self.stacks[change_set_name] = {'Id': change_set_id,
'StackId': stack.stack_id}
self.change_sets[change_set_id] = stack
return change_set_id, stack.stack_id
new_change_set = FakeChangeSet(
stack_id=stack_id,
stack_name=stack_name,
stack_template=stack_template,
change_set_id=change_set_id,
change_set_name=change_set_name,
template=template,
parameters=parameters,
region_name=region_name,
notification_arns=notification_arns,
tags=tags,
role_arn=role_arn,
cross_stack_resources=self.exports
)
self.change_sets[change_set_id] = new_change_set
self.stacks[stack_id] = new_change_set
return change_set_id, stack_id
def delete_change_set(self, change_set_name, stack_name=None):
if change_set_name in self.change_sets:
# This means arn was passed in
del self.change_sets[change_set_name]
else:
for cs in self.change_sets:
if self.change_sets[cs].change_set_name == change_set_name:
del self.change_sets[cs]
def describe_change_set(self, change_set_name, stack_name=None):
change_set = None
if change_set_name in self.change_sets:
# This means arn was passed in
change_set = self.change_sets[change_set_name]
else:
for cs in self.change_sets:
if self.change_sets[cs].change_set_name == change_set_name:
change_set = self.change_sets[cs]
if change_set is None:
raise ValidationError(change_set_name)
return change_set
def execute_change_set(self, change_set_name, stack_name=None):
stack = None
@ -197,7 +278,7 @@ class CloudFormationBackend(BaseBackend):
stack = self.change_sets[change_set_name]
else:
for cs in self.change_sets:
if self.change_sets[cs].name == change_set_name:
if self.change_sets[cs].change_set_name == change_set_name:
stack = self.change_sets[cs]
if stack is None:
raise ValidationError(stack_name)
@ -223,6 +304,9 @@ class CloudFormationBackend(BaseBackend):
else:
return list(stacks)
def list_change_sets(self):
return self.change_sets.values()
def list_stacks(self):
return [
v for v in self.stacks.values()

View File

@ -465,36 +465,70 @@ class ResourceMap(collections.Mapping):
ec2_models.ec2_backends[self._region_name].create_tags(
[self[resource].physical_resource_id], self.tags)
def update(self, template, parameters=None):
def diff(self, template, parameters=None):
if parameters:
self.input_parameters = parameters
self.load_mapping()
self.load_parameters()
self.load_conditions()
old_template = self._resource_json_map
new_template = template['Resources']
resource_names_by_action = {
'Add': set(new_template) - set(old_template),
'Modify': set(name for name in new_template if name in old_template and new_template[
name] != old_template[name]),
'Remove': set(old_template) - set(new_template)
}
resources_by_action = {
'Add': {},
'Modify': {},
'Remove': {},
}
for resource_name in resource_names_by_action['Add']:
resources_by_action['Add'][resource_name] = {
'LogicalResourceId': resource_name,
'ResourceType': new_template[resource_name]['Type']
}
for resource_name in resource_names_by_action['Modify']:
resources_by_action['Modify'][resource_name] = {
'LogicalResourceId': resource_name,
'ResourceType': new_template[resource_name]['Type']
}
for resource_name in resource_names_by_action['Remove']:
resources_by_action['Remove'][resource_name] = {
'LogicalResourceId': resource_name,
'ResourceType': old_template[resource_name]['Type']
}
return resources_by_action
def update(self, template, parameters=None):
resources_by_action = self.diff(template, parameters)
old_template = self._resource_json_map
new_template = template['Resources']
self._resource_json_map = new_template
new_resource_names = set(new_template) - set(old_template)
for resource_name in new_resource_names:
for resource_name, resource in resources_by_action['Add'].items():
resource_json = new_template[resource_name]
new_resource = parse_and_create_resource(
resource_name, resource_json, self, self._region_name)
self._parsed_resources[resource_name] = new_resource
removed_resource_names = set(old_template) - set(new_template)
for resource_name in removed_resource_names:
for resource_name, resource in resources_by_action['Remove'].items():
resource_json = old_template[resource_name]
parse_and_delete_resource(
resource_name, resource_json, self, self._region_name)
self._parsed_resources.pop(resource_name)
resources_to_update = set(name for name in new_template if name in old_template and new_template[
name] != old_template[name])
tries = 1
while resources_to_update and tries < 5:
for resource_name in resources_to_update.copy():
while resources_by_action['Modify'] and tries < 5:
for resource_name, resource in resources_by_action['Modify'].copy().items():
resource_json = new_template[resource_name]
try:
changed_resource = parse_and_update_resource(
@ -505,7 +539,7 @@ class ResourceMap(collections.Mapping):
last_exception = e
else:
self._parsed_resources[resource_name] = changed_resource
resources_to_update.remove(resource_name)
del resources_by_action['Modify'][resource_name]
tries += 1
if tries == 5:
raise last_exception

View File

@ -120,6 +120,31 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(CREATE_CHANGE_SET_RESPONSE_TEMPLATE)
return template.render(stack_id=stack_id, change_set_id=change_set_id)
def delete_change_set(self):
stack_name = self._get_param('StackName')
change_set_name = self._get_param('ChangeSetName')
self.cloudformation_backend.delete_change_set(change_set_name=change_set_name, stack_name=stack_name)
if self.request_json:
return json.dumps({
'DeleteChangeSetResponse': {
'DeleteChangeSetResult': {},
}
})
else:
template = self.response_template(DELETE_CHANGE_SET_RESPONSE_TEMPLATE)
return template.render()
def describe_change_set(self):
stack_name = self._get_param('StackName')
change_set_name = self._get_param('ChangeSetName')
change_set = self.cloudformation_backend.describe_change_set(
change_set_name=change_set_name,
stack_name=stack_name,
)
template = self.response_template(DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE)
return template.render(change_set=change_set)
@amzn_request_id
def execute_change_set(self):
stack_name = self._get_param('StackName')
@ -187,6 +212,11 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(DESCRIBE_STACK_EVENTS_RESPONSE)
return template.render(stack=stack)
def list_change_sets(self):
change_sets = self.cloudformation_backend.list_change_sets()
template = self.response_template(LIST_CHANGE_SETS_RESPONSE)
return template.render(change_sets=change_sets)
def list_stacks(self):
stacks = self.cloudformation_backend.list_stacks()
template = self.response_template(LIST_STACKS_RESPONSE)
@ -354,6 +384,66 @@ CREATE_CHANGE_SET_RESPONSE_TEMPLATE = """<CreateStackResponse>
</CreateStackResponse>
"""
DELETE_CHANGE_SET_RESPONSE_TEMPLATE = """<DeleteChangeSetResponse>
<DeleteChangeSetResult>
</DeleteChangeSetResult>
<ResponseMetadata>
<RequestId>3d3200a1-810e-3023-6cc3-example</RequestId>
</ResponseMetadata>
</DeleteChangeSetResponse>
"""
DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE = """<DescribeChangeSetResponse>
<DescribeChangeSetResult>
<ChangeSetId>{{ change_set.change_set_id }}</ChangeSetId>
<ChangeSetName>{{ change_set.change_set_name }}</ChangeSetName>
<StackId>{{ change_set.stack_id }}</StackId>
<StackName>{{ change_set.stack_name }}</StackName>
<Description>{{ change_set.description }}</Description>
<Parameters>
{% for param_name, param_value in change_set.stack_parameters.items() %}
<member>
<ParameterKey>{{ param_name }}</ParameterKey>
<ParameterValue>{{ param_value }}</ParameterValue>
</member>
{% endfor %}
</Parameters>
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
<ExecutionStatus>{{ change_set.execution_status }}</ExecutionStatus>
<Status>{{ change_set.status }}</Status>
<StatusReason>{{ change_set.status_reason }}</StatusReason>
{% if change_set.notification_arns %}
<NotificationARNs>
{% for notification_arn in change_set.notification_arns %}
<member>{{ notification_arn }}</member>
{% endfor %}
</NotificationARNs>
{% else %}
<NotificationARNs/>
{% endif %}
{% if change_set.role_arn %}
<RoleARN>{{ change_set.role_arn }}</RoleARN>
{% endif %}
{% if change_set.changes %}
<Changes>
{% for change in change_set.changes %}
<member>
<Type>Resource</Type>
<ResourceChange>
<Action>{{ change.action }}</Action>
<LogicalResourceId>{{ change.logical_resource_id }}</LogicalResourceId>
<ResourceType>{{ change.resource_type }}</ResourceType>
</ResourceChange>
</member>
{% endfor %}
</Changes>
{% endif %}
{% if next_token %}
<NextToken>{{ next_token }}</NextToken>
{% endif %}
</DescribeChangeSetResult>
</DescribeChangeSetResponse>"""
EXECUTE_CHANGE_SET_RESPONSE_TEMPLATE = """<ExecuteChangeSetResponse>
<ExecuteChangeSetResult>
<ExecuteChangeSetResult/>
@ -479,6 +569,27 @@ DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://c
</DescribeStackEventsResponse>"""
LIST_CHANGE_SETS_RESPONSE = """<ListChangeSetsResponse>
<ListChangeSetsResult>
<Summaries>
{% for change_set in change_sets %}
<member>
<StackId>{{ change_set.stack_id }}</StackId>
<StackName>{{ change_set.stack_name }}</StackName>
<ChangeSetId>{{ change_set.change_set_id }}</ChangeSetId>
<ChangeSetName>{{ change_set.change_set_name }}</ChangeSetName>
<ExecutionStatus>{{ change_set.execution_status }}</ExecutionStatus>
<Status>{{ change_set.status }}</Status>
<StatusReason>{{ change_set.status_reason }}</StatusReason>
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
<Description>{{ change_set.description }}</Description>
</member>
{% endfor %}
</Summaries>
</ListChangeSetsResult>
</ListChangeSetsResponse>"""
LIST_STACKS_RESPONSE = """<ListStacksResponse>
<ListStacksResult>
<StackSummaries>

View File

@ -399,6 +399,32 @@ def test_create_change_set_from_s3_url():
assert 'arn:aws:cloudformation:us-east-1:123456789:stack/NewStack' in response['StackId']
@mock_cloudformation
def test_describe_change_set():
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
cf_conn.create_change_set(
StackName='NewStack',
TemplateBody=dummy_template_json,
ChangeSetName='NewChangeSet',
ChangeSetType='CREATE',
)
stack = cf_conn.describe_change_set(ChangeSetName="NewChangeSet")
stack['ChangeSetName'].should.equal('NewChangeSet')
stack['StackName'].should.equal('NewStack')
cf_conn.create_change_set(
StackName='NewStack',
TemplateBody=dummy_update_template_json,
ChangeSetName='NewChangeSet2',
ChangeSetType='UPDATE',
)
stack = cf_conn.describe_change_set(ChangeSetName="NewChangeSet2")
stack['ChangeSetName'].should.equal('NewChangeSet2')
stack['StackName'].should.equal('NewStack')
stack['Changes'].should.have.length_of(2)
@mock_cloudformation
def test_execute_change_set_w_arn():
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
@ -420,7 +446,7 @@ def test_execute_change_set_w_name():
ChangeSetName='NewChangeSet',
ChangeSetType='CREATE',
)
cf_conn.execute_change_set(ChangeSetName='NewStack', StackName='NewStack')
cf_conn.execute_change_set(ChangeSetName='NewChangeSet', StackName='NewStack')
@mock_cloudformation
@ -489,6 +515,20 @@ def test_describe_stack_by_stack_id():
stack_by_id['StackName'].should.equal("test_stack")
@mock_cloudformation
def test_list_change_sets():
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
cf_conn.create_change_set(
StackName='NewStack2',
TemplateBody=dummy_template_json,
ChangeSetName='NewChangeSet2',
ChangeSetType='CREATE',
)
change_set = cf_conn.list_change_sets(StackName='NewStack2')['Summaries'][0]
change_set['StackName'].should.equal('NewStack2')
change_set['ChangeSetName'].should.equal('NewChangeSet2')
@mock_cloudformation
def test_list_stacks():
cf = boto3.resource('cloudformation', region_name='us-east-1')
@ -521,6 +561,22 @@ def test_delete_stack_from_resource():
list(cf.stacks.all()).should.have.length_of(0)
@mock_cloudformation
@mock_ec2
def test_delete_change_set():
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
cf_conn.create_change_set(
StackName='NewStack',
TemplateBody=dummy_template_json,
ChangeSetName='NewChangeSet',
ChangeSetType='CREATE',
)
cf_conn.list_change_sets(StackName='NewStack')['Summaries'].should.have.length_of(1)
cf_conn.delete_change_set(ChangeSetName='NewChangeSet', StackName='NewStack')
cf_conn.list_change_sets(StackName='NewStack')['Summaries'].should.have.length_of(0)
@mock_cloudformation
@mock_ec2
def test_delete_stack_by_name():