Add CloudFormation methods delete_change_set, describe_change_set and list_change_sets.
This commit is contained in:
parent
850496f29a
commit
337601b5fb
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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():
|
||||
|
Loading…
Reference in New Issue
Block a user