Add UpdateStack functionality for Cloudformation.

This commit is contained in:
Steve Pulec 2015-07-13 13:56:46 -04:00
parent 7f73d7e26d
commit 53fff2eb84
6 changed files with 209 additions and 28 deletions

View File

@ -14,19 +14,25 @@ class FakeStack(object):
self.stack_id = stack_id
self.name = name
self.template = template
self.template_dict = json.loads(self.template)
self.parameters = parameters
self.region_name = region_name
self.notification_arns = notification_arns if notification_arns else []
self.status = 'CREATE_COMPLETE'
template_dict = json.loads(self.template)
self.description = template_dict.get('Description')
self.description = self.template_dict.get('Description')
self.resource_map = self._create_resource_map()
self.output_map = self._create_output_map()
self.resource_map = ResourceMap(stack_id, name, parameters, region_name, template_dict)
self.resource_map.create()
def _create_resource_map(self):
resource_map = ResourceMap(self.stack_id, self.name, self.parameters, self.region_name, self.template_dict)
resource_map.create()
return resource_map
self.output_map = OutputMap(self.resource_map, template_dict)
self.output_map.create()
def _create_output_map(self):
output_map = OutputMap(self.resource_map, self.template_dict)
output_map.create()
return output_map
@property
def stack_parameters(self):
@ -40,6 +46,11 @@ class FakeStack(object):
def stack_outputs(self):
return self.output_map.values()
def update(self, template):
self.template = template
self.resource_map.update(json.loads(template))
self.output_map = self._create_output_map()
class CloudFormationBackend(BaseBackend):
@ -86,10 +97,10 @@ class CloudFormationBackend(BaseBackend):
# Lookup by stack name
return [stack for stack in self.stacks.values() if stack.name == name_or_stack_id][0]
# def update_stack(self, name, template):
# stack = self.get_stack(name)
# stack.template = template
# return stack
def update_stack(self, name, template):
stack = self.get_stack(name)
stack.update(template)
return stack
def list_stack_resources(self, stack_name_or_id):
stack = self.get_stack(stack_name_or_id)

View File

@ -156,7 +156,7 @@ def resource_name_property_from_type(resource_type):
return NAME_TYPE_MAP.get(resource_type)
def parse_resource(logical_id, resource_json, resources_map, region_name):
def parse_resource(logical_id, resource_json, resources_map, region_name, action='create'):
resource_type = resource_json['Type']
resource_class = resource_class_from_type(resource_type)
if not resource_class:
@ -183,7 +183,14 @@ def parse_resource(logical_id, resource_json, resources_map, region_name):
logical_id,
random_suffix())
resource = resource_class.create_from_cloudformation_json(resource_name, resource_json, region_name)
if action == 'create':
resource = resource_class.create_from_cloudformation_json(resource_name, resource_json, region_name)
elif action == 'update':
resource = resource_class.update_from_cloudformation_json(resource_name, resource_json, region_name)
elif action == 'delete':
resource_class.delete_from_cloudformation_json(resource_name, resource_json, region_name)
return None
resource.type = resource_type
resource.logical_resource_id = logical_id
return resource
@ -318,6 +325,39 @@ class ResourceMap(collections.Mapping):
tags['aws:cloudformation:logical-id'] = resource
ec2_models.ec2_backends[self._region_name].create_tags([self[resource].physical_resource_id], tags)
def update(self, template):
self.load_mapping()
self.load_parameters()
self.load_conditions()
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:
resource_json = new_template[resource_name]
new_resource = parse_resource(resource_name, resource_json, self, self._region_name, action='create')
self._parsed_resources[resource_name] = new_resource
removed_resource_nams = set(old_template) - set(new_template)
for resource_name in removed_resource_nams:
resource_json = old_template[resource_name]
parse_resource(resource_name, resource_json, self, self._region_name, action='delete')
self._parsed_resources.pop(resource_name)
changed_resource_names = []
for resource_name in new_template:
if resource_name in old_template:
if new_template[resource_name] != old_template[resource_name]:
changed_resource_names.append(resource_name)
for resource_name in changed_resource_names:
resource_json = new_template[resource_name]
changed_resource = parse_resource(resource_name, resource_json, self, self._region_name, action='update')
self._parsed_resources[resource_name] = changed_resource
class OutputMap(collections.Mapping):
def __init__(self, resources, template):

View File

@ -88,22 +88,22 @@ class CloudFormationResponse(BaseResponse):
stack = self.cloudformation_backend.get_stack(name_or_stack_id)
return stack.template
# def update_stack(self):
# stack_name = self._get_param('StackName')
# stack_body = self._get_param('TemplateBody')
def update_stack(self):
stack_name = self._get_param('StackName')
stack_body = self._get_param('TemplateBody')
# stack = self.cloudformation_backend.update_stack(
# name=stack_name,
# template=stack_body,
# )
# stack_body = {
# 'UpdateStackResponse': {
# 'UpdateStackResult': {
# 'StackId': stack.name,
# }
# }
# }
# return json.dumps(stack_body)
stack = self.cloudformation_backend.update_stack(
name=stack_name,
template=stack_body,
)
stack_body = {
'UpdateStackResponse': {
'UpdateStackResult': {
'StackId': stack.name,
}
}
}
return json.dumps(stack_body)
def delete_stack(self):
name_or_stack_id = self.querystring.get('StackName')[0]

View File

@ -130,6 +130,24 @@ class Queue(object):
visibility_timeout=properties.get('VisibilityTimeout'),
)
@classmethod
def update_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
properties = cloudformation_json['Properties']
queue_name = properties['QueueName']
sqs_backend = sqs_backends[region_name]
queue = sqs_backend.get_queue(queue_name)
if 'VisibilityTimeout' in properties:
queue.visibility_timeout = int(properties['VisibilityTimeout'])
return queue
@classmethod
def delete_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
properties = cloudformation_json['Properties']
queue_name = properties['QueueName']
sqs_backend = sqs_backends[region_name]
sqs_backend.delete_queue(queue_name)
@property
def approximate_number_of_messages_delayed(self):
return len([m for m in self._messages if m.delayed])

View File

@ -240,7 +240,6 @@ LIST_QUEUES_RESPONSE = """<ListQueuesResponse>
<ListQueuesResult>
{% for queue in queues %}
<QueueUrl>http://sqs.us-east-1.amazonaws.com/123456789012/{{ queue.name }}</QueueUrl>
<VisibilityTimeout>{{ queue.visibility_timeout }}</VisibilityTimeout>
{% endfor %}
</ListQueuesResult>
<ResponseMetadata>

View File

@ -99,6 +99,119 @@ def test_stack_list_resources():
queue.physical_resource_id.should.equal("my-queue")
@mock_cloudformation()
@mock_sqs()
def test_update_stack():
sqs_template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"QueueGroup": {
"Type": "AWS::SQS::Queue",
"Properties": {
"QueueName": "my-queue",
"VisibilityTimeout": 60,
}
},
},
}
sqs_template_json = json.dumps(sqs_template)
conn = boto.cloudformation.connect_to_region("us-west-1")
conn.create_stack(
"test_stack",
template_body=sqs_template_json,
)
sqs_conn = boto.sqs.connect_to_region("us-west-1")
queues = sqs_conn.get_all_queues()
queues.should.have.length_of(1)
queues[0].get_attributes('VisibilityTimeout')['VisibilityTimeout'].should.equal('60')
sqs_template['Resources']['QueueGroup']['Properties']['VisibilityTimeout'] = 100
sqs_template_json = json.dumps(sqs_template)
conn.update_stack("test_stack", sqs_template_json)
queues = sqs_conn.get_all_queues()
queues.should.have.length_of(1)
queues[0].get_attributes('VisibilityTimeout')['VisibilityTimeout'].should.equal('100')
@mock_cloudformation()
@mock_sqs()
def test_update_stack_and_remove_resource():
sqs_template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"QueueGroup": {
"Type": "AWS::SQS::Queue",
"Properties": {
"QueueName": "my-queue",
"VisibilityTimeout": 60,
}
},
},
}
sqs_template_json = json.dumps(sqs_template)
conn = boto.cloudformation.connect_to_region("us-west-1")
conn.create_stack(
"test_stack",
template_body=sqs_template_json,
)
sqs_conn = boto.sqs.connect_to_region("us-west-1")
queues = sqs_conn.get_all_queues()
queues.should.have.length_of(1)
sqs_template['Resources'].pop('QueueGroup')
sqs_template_json = json.dumps(sqs_template)
conn.update_stack("test_stack", sqs_template_json)
queues = sqs_conn.get_all_queues()
queues.should.have.length_of(0)
@mock_cloudformation()
@mock_sqs()
def test_update_stack_and_add_resource():
sqs_template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {},
}
sqs_template_json = json.dumps(sqs_template)
conn = boto.cloudformation.connect_to_region("us-west-1")
conn.create_stack(
"test_stack",
template_body=sqs_template_json,
)
sqs_conn = boto.sqs.connect_to_region("us-west-1")
queues = sqs_conn.get_all_queues()
queues.should.have.length_of(0)
sqs_template = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"QueueGroup": {
"Type": "AWS::SQS::Queue",
"Properties": {
"QueueName": "my-queue",
"VisibilityTimeout": 60,
}
},
},
}
sqs_template_json = json.dumps(sqs_template)
conn.update_stack("test_stack", sqs_template_json)
queues = sqs_conn.get_all_queues()
queues.should.have.length_of(1)
@mock_ec2()
@mock_cloudformation()
def test_stack_ec2_integration():