diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py
index d6298906f..d9d09410d 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 e407be9d2..9cab62a63 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.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }}
+ {{ 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 = """
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py
index bbf65fae6..e45dafbfa 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_crud.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py
@@ -354,3 +354,39 @@ def test_update_stack_when_rolled_back():
ex.error_code.should.equal('ValidationError')
ex.reason.should.equal('Bad Request')
ex.status.should.equal(400)
+
+@mock_cloudformation
+def test_describe_stack_events_shows_create_update_and_delete():
+ conn = boto.connect_cloudformation()
+ stack_id = conn.create_stack("test_stack", template_body=dummy_template_json)
+ conn.update_stack(stack_id, template_body=dummy_template_json2)
+ conn.delete_stack(stack_id)
+
+ # assert begins and ends with stack events
+ events = conn.describe_stack_events(stack_id)
+ events[0].resource_type.should.equal("AWS::CloudFormation::Stack")
+ events[-1].resource_type.should.equal("AWS::CloudFormation::Stack")
+
+ # testing ordering of stack events without assuming resource events will not exist
+ stack_events_to_look_for = iter([
+ ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None),
+ ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None),
+ ("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)])
+ try:
+ for event in events:
+ event.stack_id.should.equal(stack_id)
+ event.stack_name.should.equal("test_stack")
+
+ if event.resource_type == "AWS::CloudFormation::Stack":
+ event.logical_resource_id.should.equal("test_stack")
+ event.physical_resource_id.should.equal(stack_id)
+
+ status_to_look_for, reason_to_look_for = next(stack_events_to_look_for)
+ event.resource_status.should.equal(status_to_look_for)
+ if reason_to_look_for is not None:
+ event.resource_status_reason.should.equal(reason_to_look_for)
+ except StopIteration:
+ assert False, "Too many stack events"
+
+ list(stack_events_to_look_for).should.be.empty
+
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
index 0e5d93986..4197d2628 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
@@ -312,3 +312,42 @@ def test_stack_tags():
expected_tag_items = set(
item for items in [tag.items() for tag in tags] for item in items)
observed_tag_items.should.equal(expected_tag_items)
+
+@mock_cloudformation
+def test_stack_events():
+ cf = boto3.resource('cloudformation', region_name='us-east-1')
+ stack = cf.create_stack(
+ StackName="test_stack",
+ TemplateBody=dummy_template_json,
+ )
+ stack.update(TemplateBody=dummy_update_template_json)
+ stack = cf.Stack(stack.stack_id)
+ stack.delete()
+
+ # assert begins and ends with stack events
+ events = list(stack.events.all())
+ events[0].resource_type.should.equal("AWS::CloudFormation::Stack")
+ events[-1].resource_type.should.equal("AWS::CloudFormation::Stack")
+
+ # testing ordering of stack events without assuming resource events will not exist
+ stack_events_to_look_for = iter([
+ ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None),
+ ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None),
+ ("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)])
+ try:
+ for event in events:
+ event.stack_id.should.equal(stack.stack_id)
+ event.stack_name.should.equal("test_stack")
+
+ if event.resource_type == "AWS::CloudFormation::Stack":
+ event.logical_resource_id.should.equal("test_stack")
+ event.physical_resource_id.should.equal(stack.stack_id)
+
+ status_to_look_for, reason_to_look_for = next(stack_events_to_look_for)
+ event.resource_status.should.equal(status_to_look_for)
+ if reason_to_look_for is not None:
+ event.resource_status_reason.should.equal(reason_to_look_for)
+ except StopIteration:
+ assert False, "Too many stack events"
+
+ list(stack_events_to_look_for).should.be.empty