Merge pull request #376 from spulec/cloudformation-update-stack

Cloudformation update stack
This commit is contained in:
Steve Pulec 2015-07-13 15:28:50 -04:00
commit a854efbf56
6 changed files with 222 additions and 33 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,17 +156,12 @@ 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):
resource_type = resource_json['Type']
resource_class = resource_class_from_type(resource_type)
if not resource_class:
return None
condition = resource_json.get('Condition')
if condition and not resources_map[condition]:
# If this has a False condition, don't create the resource
return None
resource_json = clean_json(resource_json, resources_map)
resource_name_property = resource_name_property_from_type(resource_type)
if resource_name_property:
@ -182,13 +177,38 @@ def parse_resource(logical_id, resource_json, resources_map, region_name):
resource_name = '{0}-{1}-{2}'.format(resources_map.get('AWS::StackName'),
logical_id,
random_suffix())
return resource_class, resource_json, resource_name
def parse_and_create_resource(logical_id, resource_json, resources_map, region_name):
condition = resource_json.get('Condition')
if condition and not resources_map[condition]:
# If this has a False condition, don't create the resource
return None
resource_type = resource_json['Type']
resource_tuple = parse_resource(logical_id, resource_json, resources_map)
if not resource_tuple:
return None
resource_class, resource_json, resource_name = resource_tuple
resource = resource_class.create_from_cloudformation_json(resource_name, resource_json, region_name)
resource.type = resource_type
resource.logical_resource_id = logical_id
return resource
def parse_and_update_resource(logical_id, resource_json, resources_map, region_name):
resource_class, resource_json, resource_name = parse_resource(logical_id, resource_json, resources_map)
resource = resource_class.update_from_cloudformation_json(resource_name, resource_json, region_name)
return resource
def parse_and_delete_resource(logical_id, resource_json, resources_map, region_name):
resource_class, resource_json, resource_name = parse_resource(logical_id, resource_json, resources_map)
resource_class.delete_from_cloudformation_json(resource_name, resource_json, region_name)
return None
def parse_condition(condition, resources_map, condition_map):
if isinstance(condition, bool):
return condition
@ -258,7 +278,7 @@ class ResourceMap(collections.Mapping):
return self._parsed_resources[resource_logical_id]
else:
resource_json = self._resource_json_map.get(resource_logical_id)
new_resource = parse_resource(resource_logical_id, resource_json, self, self._region_name)
new_resource = parse_and_create_resource(resource_logical_id, resource_json, self, self._region_name)
self._parsed_resources[resource_logical_id] = new_resource
return new_resource
@ -318,6 +338,34 @@ 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_and_create_resource(resource_name, resource_json, self, self._region_name)
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_and_delete_resource(resource_name, resource_json, self, self._region_name)
self._parsed_resources.pop(resource_name)
for resource_name in new_template:
if resource_name in old_template and new_template[resource_name] != old_template[resource_name]:
resource_json = new_template[resource_name]
changed_resource = parse_and_update_resource(resource_name, resource_json, self, self._region_name)
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():