From 2a6f607ae57a3589a2685e593c8aed9872cb0750 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Wed, 29 Jun 2016 18:41:16 +0000 Subject: [PATCH 1/2] Add DescribeStackEvents endpoint It returns nothing right now because there's no backend implementation for events (yet.) --- moto/cloudformation/responses.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index e407be9d..2b71cb8b 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -91,6 +91,13 @@ class CloudFormationResponse(BaseResponse): template = self.response_template(DESCRIBE_STACK_RESOURCES_RESPONSE) return template.render(stack=stack) + def describe_stack_events(self): + stack_name = self._get_param('StackName') + stack = self.cloudformation_backend.get_stack(stack_name) + + template = self.response_template(DESCRIBE_STACK_EVENTS_RESPONSE) + return template.render(stack=stack) + def list_stacks(self): stacks = self.cloudformation_backend.list_stacks() template = self.response_template(LIST_STACKS_RESPONSE) @@ -269,6 +276,31 @@ DESCRIBE_STACK_RESOURCES_RESPONSE = """ """ +DESCRIBE_STACK_EVENTS_RESPONSE = """ + + + {% for event in stack.events %} + + {{ event.timestamp }} + {{ event.resource_status }} + {{ event.stack_id }} + {{ event.event_id }} + {{ event.logical_resource_id }} + {% if event.resource_status_reason %}{{ event.resource_status_reason }}{% endif %} + {{ event.stack_name }} + {{ event.physical_resource_id }} + {% if event.resource_properties %}{{ event.resource_properties }}{% endif %} + {{ event.resource_type }} + + {% endfor %} + + + + b9b4b068-3a41-11e5-94eb-example + +""" + + LIST_STACKS_RESPONSE = """ From 542248158f2f5df09de82d0a4c24c4d2ce7661aa Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Wed, 29 Jun 2016 21:56:39 +0000 Subject: [PATCH 2/2] Implement the meat for DescribeStackEvents Right now this just adds events for the stack itself via the lifecycle methods of the FakeStack object, but it is possible to add other kinds of events (I left a method for that should someone need inspiration later.) --- moto/cloudformation/models.py | 62 +++++++++++++++++-- moto/cloudformation/responses.py | 2 +- .../test_cloudformation_stack_crud.py | 36 +++++++++++ .../test_cloudformation_stack_crud_boto3.py | 39 ++++++++++++ 4 files changed, 132 insertions(+), 7 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index d6298906..d9d09410 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from datetime import datetime import json import boto.cloudformation @@ -19,11 +20,14 @@ class FakeStack(object): self.region_name = region_name self.notification_arns = notification_arns if notification_arns else [] self.tags = tags if tags else {} - self.status = 'CREATE_COMPLETE' + self.events = [] + self._add_stack_event("CREATE_IN_PROGRESS", resource_status_reason="User Initiated") self.description = self.template_dict.get('Description') self.resource_map = self._create_resource_map() self.output_map = self._create_output_map() + self._add_stack_event("CREATE_COMPLETE") + self.status = 'CREATE_COMPLETE' def _create_resource_map(self): resource_map = ResourceMap(self.stack_id, self.name, self.parameters, self.tags, self.region_name, self.template_dict) @@ -35,6 +39,32 @@ class FakeStack(object): output_map.create() return output_map + def _add_stack_event(self, resource_status, resource_status_reason=None, resource_properties=None): + self.events.append(FakeEvent( + stack_id=self.stack_id, + stack_name=self.name, + logical_resource_id=self.name, + physical_resource_id=self.stack_id, + resource_type="AWS::CloudFormation::Stack", + resource_status=resource_status, + resource_status_reason=resource_status_reason, + resource_properties=resource_properties, + )) + + def _add_resource_event(self, logical_resource_id, resource_status, resource_status_reason=None, resource_properties=None): + # not used yet... feel free to help yourself + resource = self.resource_map[logical_resource_id] + self.events.append(FakeEvent( + stack_id=self.stack_id, + stack_name=self.name, + logical_resource_id=logical_resource_id, + physical_resource_id=resource.physical_resource_id, + resource_type=resource.type, + resource_status=resource_status, + resource_status_reason=resource_status_reason, + resource_properties=resource_properties, + )) + @property def stack_parameters(self): return self.resource_map.resolved_parameters @@ -48,16 +78,33 @@ class FakeStack(object): return self.output_map.values() def update(self, template): + self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated") self.template = template self.resource_map.update(json.loads(template)) self.output_map = self._create_output_map() + self._add_stack_event("UPDATE_COMPLETE") self.status = "UPDATE_COMPLETE" def delete(self): + self._add_stack_event("DELETE_IN_PROGRESS", resource_status_reason="User Initiated") self.resource_map.delete() + self._add_stack_event("DELETE_COMPLETE") self.status = "DELETE_COMPLETE" +class FakeEvent(object): + def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None): + self.stack_id = stack_id + self.stack_name = stack_name + self.logical_resource_id = logical_resource_id + self.physical_resource_id = physical_resource_id + self.resource_type = resource_type + self.resource_status = resource_status + self.resource_status_reason = resource_status_reason + self.resource_properties = resource_properties + self.timestamp = datetime.utcnow() + + class CloudFormationBackend(BaseBackend): def __init__(self): @@ -97,12 +144,15 @@ class CloudFormationBackend(BaseBackend): return self.stacks.values() def get_stack(self, name_or_stack_id): - if name_or_stack_id in self.stacks: - # Lookup by stack id - return self.stacks.get(name_or_stack_id) + all_stacks = dict(self.deleted_stacks, **self.stacks) + if name_or_stack_id in all_stacks: + # Lookup by stack id - deleted stacks incldued + return all_stacks[name_or_stack_id] else: - # Lookup by stack name - return [stack for stack in self.stacks.values() if stack.name == name_or_stack_id][0] + # Lookup by stack name - undeleted stacks only + for stack in self.stacks.values(): + if stack.name == name_or_stack_id: + return stack def update_stack(self, name, template): stack = self.get_stack(name) diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 2b71cb8b..9cab62a6 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -281,7 +281,7 @@ DESCRIBE_STACK_EVENTS_RESPONSE = """