commit
3d886aeadc
@ -42,7 +42,7 @@ class FakeStack(BaseModel):
|
|||||||
return resource_map
|
return resource_map
|
||||||
|
|
||||||
def _create_output_map(self):
|
def _create_output_map(self):
|
||||||
output_map = OutputMap(self.resource_map, self.template_dict)
|
output_map = OutputMap(self.resource_map, self.template_dict, self.stack_id)
|
||||||
output_map.create()
|
output_map.create()
|
||||||
return output_map
|
return output_map
|
||||||
|
|
||||||
@ -90,6 +90,10 @@ class FakeStack(BaseModel):
|
|||||||
def stack_outputs(self):
|
def stack_outputs(self):
|
||||||
return self.output_map.values()
|
return self.output_map.values()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exports(self):
|
||||||
|
return self.output_map.exports
|
||||||
|
|
||||||
def update(self, template, role_arn=None, parameters=None, tags=None):
|
def update(self, template, role_arn=None, parameters=None, tags=None):
|
||||||
self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated")
|
self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated")
|
||||||
self.template = template
|
self.template = template
|
||||||
@ -131,6 +135,7 @@ class CloudFormationBackend(BaseBackend):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.stacks = OrderedDict()
|
self.stacks = OrderedDict()
|
||||||
self.deleted_stacks = {}
|
self.deleted_stacks = {}
|
||||||
|
self.exports = OrderedDict()
|
||||||
|
|
||||||
def create_stack(self, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None):
|
def create_stack(self, name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None):
|
||||||
stack_id = generate_stack_id(name)
|
stack_id = generate_stack_id(name)
|
||||||
@ -145,6 +150,9 @@ class CloudFormationBackend(BaseBackend):
|
|||||||
role_arn=role_arn,
|
role_arn=role_arn,
|
||||||
)
|
)
|
||||||
self.stacks[stack_id] = new_stack
|
self.stacks[stack_id] = new_stack
|
||||||
|
self._validate_export_uniqueness(new_stack)
|
||||||
|
for export in new_stack.exports:
|
||||||
|
self.exports[export.name] = export
|
||||||
return new_stack
|
return new_stack
|
||||||
|
|
||||||
def describe_stacks(self, name_or_stack_id):
|
def describe_stacks(self, name_or_stack_id):
|
||||||
@ -191,6 +199,7 @@ class CloudFormationBackend(BaseBackend):
|
|||||||
stack = self.stacks.pop(name_or_stack_id, None)
|
stack = self.stacks.pop(name_or_stack_id, None)
|
||||||
stack.delete()
|
stack.delete()
|
||||||
self.deleted_stacks[stack.stack_id] = stack
|
self.deleted_stacks[stack.stack_id] = stack
|
||||||
|
[self.exports.pop(export.name) for export in stack.exports]
|
||||||
return self.stacks.pop(name_or_stack_id, None)
|
return self.stacks.pop(name_or_stack_id, None)
|
||||||
else:
|
else:
|
||||||
# Delete by stack name
|
# Delete by stack name
|
||||||
@ -198,6 +207,23 @@ class CloudFormationBackend(BaseBackend):
|
|||||||
if stack.name == name_or_stack_id:
|
if stack.name == name_or_stack_id:
|
||||||
self.delete_stack(stack.stack_id)
|
self.delete_stack(stack.stack_id)
|
||||||
|
|
||||||
|
def list_exports(self, token):
|
||||||
|
all_exports = list(self.exports.values())
|
||||||
|
if token is None:
|
||||||
|
exports = all_exports[0:100]
|
||||||
|
next_token = '100' if len(all_exports) > 100 else None
|
||||||
|
else:
|
||||||
|
token = int(token)
|
||||||
|
exports = all_exports[token:token + 100]
|
||||||
|
next_token = str(token + 100) if len(all_exports) > token + 100 else None
|
||||||
|
return exports, next_token
|
||||||
|
|
||||||
|
def _validate_export_uniqueness(self, stack):
|
||||||
|
new_stack_export_names = [x.name for x in stack.exports]
|
||||||
|
export_names = self.exports.keys()
|
||||||
|
if not set(export_names).isdisjoint(new_stack_export_names):
|
||||||
|
raise ValidationError(stack.stack_id, message='Export names must be unique across a given region')
|
||||||
|
|
||||||
|
|
||||||
cloudformation_backends = {}
|
cloudformation_backends = {}
|
||||||
for region in boto.cloudformation.regions():
|
for region in boto.cloudformation.regions():
|
||||||
|
@ -460,8 +460,9 @@ class ResourceMap(collections.Mapping):
|
|||||||
|
|
||||||
class OutputMap(collections.Mapping):
|
class OutputMap(collections.Mapping):
|
||||||
|
|
||||||
def __init__(self, resources, template):
|
def __init__(self, resources, template, stack_id):
|
||||||
self._template = template
|
self._template = template
|
||||||
|
self._stack_id = stack_id
|
||||||
self._output_json_map = template.get('Outputs')
|
self._output_json_map = template.get('Outputs')
|
||||||
|
|
||||||
# Create the default resources
|
# Create the default resources
|
||||||
@ -490,6 +491,35 @@ class OutputMap(collections.Mapping):
|
|||||||
def outputs(self):
|
def outputs(self):
|
||||||
return self._output_json_map.keys() if self._output_json_map else []
|
return self._output_json_map.keys() if self._output_json_map else []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exports(self):
|
||||||
|
exports = []
|
||||||
|
if self.outputs:
|
||||||
|
for key, value in self._output_json_map.items():
|
||||||
|
if value.get('Export'):
|
||||||
|
exports.append(Export(self._stack_id, value['Export'].get('Name'), value.get('Value')))
|
||||||
|
return exports
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
for output in self.outputs:
|
for output in self.outputs:
|
||||||
self[output]
|
self[output]
|
||||||
|
|
||||||
|
|
||||||
|
class Export(object):
|
||||||
|
|
||||||
|
def __init__(self, exporting_stack_id, name, value):
|
||||||
|
self._exporting_stack_id = exporting_stack_id
|
||||||
|
self._name = name
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exporting_stack_id(self):
|
||||||
|
return self._exporting_stack_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
@ -210,6 +210,12 @@ class CloudFormationResponse(BaseResponse):
|
|||||||
template = self.response_template(DELETE_STACK_RESPONSE_TEMPLATE)
|
template = self.response_template(DELETE_STACK_RESPONSE_TEMPLATE)
|
||||||
return template.render()
|
return template.render()
|
||||||
|
|
||||||
|
def list_exports(self):
|
||||||
|
token = self._get_param('NextToken')
|
||||||
|
exports, next_token = self.cloudformation_backend.list_exports(token=token)
|
||||||
|
template = self.response_template(LIST_EXPORTS_RESPONSE)
|
||||||
|
return template.render(exports=exports, next_token=next_token)
|
||||||
|
|
||||||
|
|
||||||
CREATE_STACK_RESPONSE_TEMPLATE = """<CreateStackResponse>
|
CREATE_STACK_RESPONSE_TEMPLATE = """<CreateStackResponse>
|
||||||
<CreateStackResult>
|
<CreateStackResult>
|
||||||
@ -410,3 +416,23 @@ DELETE_STACK_RESPONSE_TEMPLATE = """<DeleteStackResponse>
|
|||||||
</ResponseMetadata>
|
</ResponseMetadata>
|
||||||
</DeleteStackResponse>
|
</DeleteStackResponse>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
LIST_EXPORTS_RESPONSE = """<ListExportsResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
|
||||||
|
<ListExportsResult>
|
||||||
|
<Exports>
|
||||||
|
{% for export in exports %}
|
||||||
|
<member>
|
||||||
|
<ExportingStackId>{{ export.exporting_stack_id }}</ExportingStackId>
|
||||||
|
<Name>{{ export.name }}</Name>
|
||||||
|
<Value>{{ export.value }}</Value>
|
||||||
|
</member>
|
||||||
|
{% endfor %}
|
||||||
|
</Exports>
|
||||||
|
{% if next_token %}
|
||||||
|
<NextToken>{{ next_token }}</NextToken>
|
||||||
|
{% endif %}
|
||||||
|
</ListExportsResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>5ccc7dcd-744c-11e5-be70-example</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</ListExportsResponse>"""
|
||||||
|
@ -12,6 +12,7 @@ import sure # noqa
|
|||||||
# Ensure 'assert_raises' context manager support for Python 2.6
|
# Ensure 'assert_raises' context manager support for Python 2.6
|
||||||
import tests.backport_assert_raises # noqa
|
import tests.backport_assert_raises # noqa
|
||||||
from nose.tools import assert_raises
|
from nose.tools import assert_raises
|
||||||
|
import random
|
||||||
|
|
||||||
dummy_template = {
|
dummy_template = {
|
||||||
"AWSTemplateFormatVersion": "2010-09-09",
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
@ -57,8 +58,31 @@ dummy_update_template = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dummy_output_template = {
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Description": "Stack 1",
|
||||||
|
"Resources": {
|
||||||
|
"Instance": {
|
||||||
|
"Type": "AWS::EC2::Instance",
|
||||||
|
"Properties": {
|
||||||
|
"ImageId": "ami-08111162"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Outputs" : {
|
||||||
|
"StackVPC" : {
|
||||||
|
"Description" : "The ID of the VPC",
|
||||||
|
"Value" : "VPCID",
|
||||||
|
"Export" : {
|
||||||
|
"Name" : "My VPC ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dummy_template_json = json.dumps(dummy_template)
|
dummy_template_json = json.dumps(dummy_template)
|
||||||
dummy_update_template_json = json.dumps(dummy_template)
|
dummy_update_template_json = json.dumps(dummy_template)
|
||||||
|
dummy_output_template_json = json.dumps(dummy_output_template)
|
||||||
|
|
||||||
|
|
||||||
@mock_cloudformation
|
@mock_cloudformation
|
||||||
@ -408,3 +432,72 @@ def test_stack_events():
|
|||||||
assert False, "Too many stack events"
|
assert False, "Too many stack events"
|
||||||
|
|
||||||
list(stack_events_to_look_for).should.be.empty
|
list(stack_events_to_look_for).should.be.empty
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
def test_list_exports():
|
||||||
|
cf_client = boto3.client('cloudformation', region_name='us-east-1')
|
||||||
|
cf_resource = boto3.resource('cloudformation', region_name='us-east-1')
|
||||||
|
stack = cf_resource.create_stack(
|
||||||
|
StackName="test_stack",
|
||||||
|
TemplateBody=dummy_output_template_json,
|
||||||
|
)
|
||||||
|
output_value = 'VPCID'
|
||||||
|
exports = cf_client.list_exports()['Exports']
|
||||||
|
|
||||||
|
stack.outputs.should.have.length_of(1)
|
||||||
|
stack.outputs[0]['OutputValue'].should.equal(output_value)
|
||||||
|
|
||||||
|
exports.should.have.length_of(1)
|
||||||
|
exports[0]['ExportingStackId'].should.equal(stack.stack_id)
|
||||||
|
exports[0]['Name'].should.equal('My VPC ID')
|
||||||
|
exports[0]['Value'].should.equal(output_value)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
def test_list_exports_with_token():
|
||||||
|
cf = boto3.client('cloudformation', region_name='us-east-1')
|
||||||
|
for i in range(101):
|
||||||
|
# Add index to ensure name is unique
|
||||||
|
dummy_output_template['Outputs']['StackVPC']['Export']['Name'] += str(i)
|
||||||
|
cf.create_stack(
|
||||||
|
StackName="test_stack",
|
||||||
|
TemplateBody=json.dumps(dummy_output_template),
|
||||||
|
)
|
||||||
|
exports = cf.list_exports()
|
||||||
|
exports['Exports'].should.have.length_of(100)
|
||||||
|
exports.get('NextToken').should_not.be.none
|
||||||
|
|
||||||
|
more_exports = cf.list_exports(NextToken=exports['NextToken'])
|
||||||
|
more_exports['Exports'].should.have.length_of(1)
|
||||||
|
more_exports.get('NextToken').should.be.none
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
def test_delete_stack_with_export():
|
||||||
|
cf = boto3.client('cloudformation', region_name='us-east-1')
|
||||||
|
stack = cf.create_stack(
|
||||||
|
StackName="test_stack",
|
||||||
|
TemplateBody=dummy_output_template_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
stack_id = stack['StackId']
|
||||||
|
exports = cf.list_exports()['Exports']
|
||||||
|
exports.should.have.length_of(1)
|
||||||
|
|
||||||
|
cf.delete_stack(StackName=stack_id)
|
||||||
|
cf.list_exports()['Exports'].should.have.length_of(0)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
def test_export_names_must_be_unique():
|
||||||
|
cf = boto3.resource('cloudformation', region_name='us-east-1')
|
||||||
|
first_stack = cf.create_stack(
|
||||||
|
StackName="test_stack",
|
||||||
|
TemplateBody=dummy_output_template_json,
|
||||||
|
)
|
||||||
|
with assert_raises(ClientError):
|
||||||
|
cf.create_stack(
|
||||||
|
StackName="test_stack",
|
||||||
|
TemplateBody=dummy_output_template_json,
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user