Merge pull request #653 from 2rs2ts/describe-stack-events
Add CloudFormation:DescribeStackEvents
This commit is contained in:
commit
84d0c44bd3
@ -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)
|
||||
|
@ -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 = """<DescribeStackResourcesResponse>
|
||||
</DescribeStackResourcesResponse>"""
|
||||
|
||||
|
||||
DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
|
||||
<DescribeStackEventsResult>
|
||||
<StackEvents>
|
||||
{% for event in stack.events %}
|
||||
<member>
|
||||
<Timestamp>{{ event.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }}</Timestamp>
|
||||
<ResourceStatus>{{ event.resource_status }}</ResourceStatus>
|
||||
<StackId>{{ event.stack_id }}</StackId>
|
||||
<EventId>{{ event.event_id }}</EventId>
|
||||
<LogicalResourceId>{{ event.logical_resource_id }}</LogicalResourceId>
|
||||
{% if event.resource_status_reason %}<ResourceStatusReason>{{ event.resource_status_reason }}</ResourceStatusReason>{% endif %}
|
||||
<StackName>{{ event.stack_name }}</StackName>
|
||||
<PhysicalResourceId>{{ event.physical_resource_id }}</PhysicalResourceId>
|
||||
{% if event.resource_properties %}<ResourceProperties>{{ event.resource_properties }}</ResourceProperties>{% endif %}
|
||||
<ResourceType>{{ event.resource_type }}</ResourceType>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</StackEvents>
|
||||
</DescribeStackEventsResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>b9b4b068-3a41-11e5-94eb-example</RequestId>
|
||||
</ResponseMetadata>
|
||||
</DescribeStackEventsResponse>"""
|
||||
|
||||
|
||||
LIST_STACKS_RESPONSE = """<ListStacksResponse>
|
||||
<ListStacksResult>
|
||||
<StackSummaries>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user