diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 618e736e0..9fd913ca4 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -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) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 6528c41d2..abba3defd 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -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): diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index d0ee90138..fe0af6640 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -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] diff --git a/moto/sqs/models.py b/moto/sqs/models.py index dd288f997..33abe3684 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -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]) diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index b5947c639..69b3f63df 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -240,7 +240,6 @@ LIST_QUEUES_RESPONSE = """ {% for queue in queues %} http://sqs.us-east-1.amazonaws.com/123456789012/{{ queue.name }} - {{ queue.visibility_timeout }} {% endfor %} diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index f06acf30b..5b9387b13 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -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():