create CloudFormation outputs and enable 'Fn::GetAtt' to work.

This commit is contained in:
Joseph Lawson 2014-10-21 12:45:03 -04:00
parent 832769b8a7
commit 1d9ffafaa5
10 changed files with 269 additions and 8 deletions

View File

@ -3,7 +3,7 @@ import json
from moto.core import BaseBackend
from .parsing import ResourceMap
from .parsing import ResourceMap, OutputMap
from .utils import generate_stack_id
@ -19,10 +19,17 @@ class FakeStack(object):
self.resource_map = ResourceMap(stack_id, name, template_dict)
self.resource_map.create()
self.output_map = OutputMap(self.resource_map, template_dict)
self.output_map.create()
@property
def stack_resources(self):
return self.resource_map.values()
@property
def stack_outputs(self):
return self.output_map.values()
class CloudFormationBackend(BaseBackend):

View File

@ -8,6 +8,8 @@ from moto.elb import models as elb_models
from moto.iam import models as iam_models
from moto.sqs import models as sqs_models
from .utils import random_suffix
from boto.cloudformation.stack import Output
from boto.exception import BotoServerError
MODEL_MAP = {
"AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup,
@ -69,6 +71,19 @@ def clean_json(resource_json, resources_map):
else:
return resource
if 'Fn::GetAtt' in resource_json:
resource = resources_map[resource_json['Fn::GetAtt'][0]]
try:
return resource.get_cfn_attribute(resource_json['Fn::GetAtt'][1])
except NotImplementedError as n:
raise NotImplementedError(n.message.format(resource_json['Fn::GetAtt'][0]))
except AttributeError:
raise BotoServerError(
400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt'.format(
resource_json['Fn::GetAtt'][0], resource_json['Fn::GetAtt'][1]))
cleaned_json = {}
for key, value in resource_json.items():
cleaned_json[key] = clean_json(value, resources_map)
@ -122,6 +137,15 @@ def parse_resource(logical_id, resource_json, resources_map):
return resource
def parse_output(output_logical_id, output_json, resources_map):
output_json = clean_json(output_json, resources_map)
output = Output()
output.key = output_logical_id
output.value = output_json['Value']
output.description = output_json.get('Description')
return output
class ResourceMap(collections.Mapping):
"""
This is a lazy loading map for resources. This allows us to create resources
@ -180,3 +204,38 @@ class ResourceMap(collections.Mapping):
if isinstance(self[resource], ec2_models.TaggedEC2Resource):
tags['aws:cloudformation:logical-id'] = resource
ec2_models.ec2_backend.create_tags([self[resource].physical_resource_id],tags)
class OutputMap(collections.Mapping):
def __init__(self, resources, template):
self._template = template
self._output_json_map = template.get('Outputs')
# Create the default resources
self._resource_map = resources
self._parsed_outputs = dict()
def __getitem__(self, key):
output_logical_id = key
if output_logical_id in self._parsed_outputs:
return self._parsed_outputs[output_logical_id]
else:
output_json = self._output_json_map.get(output_logical_id)
new_output = parse_output(output_logical_id, output_json, self._resource_map)
self._parsed_outputs[output_logical_id] = new_output
return new_output
def __iter__(self):
return iter(self.outputs)
def __len__(self):
return len(self._output_json_map)
@property
def outputs(self):
return self._output_json_map.keys() if self._output_json_map else []
def create(self):
for output in self.outputs:
self[output]

View File

@ -88,7 +88,14 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResult>
<CreationTime>2010-07-27T22:28:28Z</CreationTime>
<StackStatus>CREATE_COMPLETE</StackStatus>
<DisableRollback>false</DisableRollback>
<Outputs></Outputs>
<Outputs>
{% for output in stack.stack_outputs %}
<member>
<OutputKey>{{ output.key }}</OutputKey>
<OutputValue>{{ output.value }}</OutputValue>
</member>
{% endfor %}
</Outputs>
</member>
{% endfor %}
</Stacks>

View File

@ -9,6 +9,7 @@ from boto.ec2.instance import Instance as BotoInstance, Reservation
from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType
from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest
from boto.ec2.launchspecification import LaunchSpecification
from boto.exception import BotoServerError
from moto.core import BaseBackend
from moto.core.models import Model
@ -185,6 +186,15 @@ class NetworkInterface(object):
else:
return self._group_set
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'PrimaryPrivateIpAddress':
return self.private_ip_address
elif attribute_name == 'SecondaryPrivateIpAddresses':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SecondaryPrivateIpAddresses" ]"')
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class NetworkInterfaceBackend(object):
def __init__(self):
@ -412,6 +422,21 @@ class Instance(BotoInstance, TaggedEC2Resource):
eni.attachment_id = None
eni.device_index = None
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'AvailabilityZone':
return self.placement
elif attribute_name == 'PrivateDnsName':
return self.private_dns_name
elif attribute_name == 'PublicDnsName':
return self.public_dns_name
elif attribute_name == 'PrivateIp':
return self.private_ip_address
elif attribute_name == 'PublicIp':
return self.ip_address
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class InstanceBackend(object):
@ -971,6 +996,13 @@ class SecurityGroup(object):
return False
return True
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'GroupId':
return self.id
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class SecurityGroupBackend(object):
@ -1494,6 +1526,13 @@ class Subnet(TaggedEC2Resource):
return filter_value
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'AvailabilityZone':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "AvailabilityZone" ]"')
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class SubnetBackend(object):
def __init__(self):
@ -1929,6 +1968,13 @@ class ElasticAddress(object):
def physical_resource_id(self):
return self.allocation_id
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'AllocationId':
return self.allocation_id
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class ElasticAddressBackend(object):

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
from moto.core import BaseBackend
from boto.exception import BotoServerError
class FakeHealthCheck(object):
@ -56,6 +57,21 @@ class FakeLoadBalancer(object):
def physical_resource_id(self):
return self.name
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'CanonicalHostedZoneName':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneName" ]"')
elif attribute_name == 'CanonicalHostedZoneNameID':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "CanonicalHostedZoneNameID" ]"')
elif attribute_name == 'DNSName':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DNSName" ]"')
elif attribute_name == 'SourceSecurityGroup.GroupName':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.GroupName" ]"')
elif attribute_name == 'SourceSecurityGroup.OwnerAlias':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SourceSecurityGroup.OwnerAlias" ]"')
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class ELBBackend(BaseBackend):

View File

@ -30,6 +30,13 @@ class Role(object):
def physical_resource_id(self):
return self.id
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'Arn':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class InstanceProfile(object):
def __init__(self, instance_profile_id, name, path, roles):
@ -53,6 +60,13 @@ class InstanceProfile(object):
def physical_resource_id(self):
return self.name
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'Arn':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class Certificate(object):
def __init__(self, cert_name, cert_body, private_key, cert_chain=None, path=None):
@ -78,6 +92,13 @@ class AccessKey(object):
"%Y-%m-%d-%H-%M-%S"
)
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'SecretAccessKey':
return self.secret_access_key
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class Group(object):
def __init__(self, name, path='/'):
@ -91,6 +112,13 @@ class Group(object):
self.users = []
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'Arn':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class User(object):
def __init__(self, name, path='/'):
@ -143,6 +171,13 @@ class User(object):
else:
raise BotoServerError(404, 'Not Found')
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'Arn':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class IAMBackend(BaseBackend):

View File

@ -12,6 +12,7 @@ from moto.core import BaseBackend
from moto.core.utils import iso_8601_datetime, rfc_1123_datetime
from .exceptions import BucketAlreadyExists, MissingBucket
from .utils import clean_key_name, _VersionedKeyStore
from boto.exception import BotoServerError
UPLOAD_ID_BYTES = 43
UPLOAD_PART_MIN_SIZE = 5242880
@ -170,6 +171,15 @@ class FakeBucket(object):
def is_versioned(self):
return self.versioning_status == 'Enabled'
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'DomainName':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DomainName" ]"')
elif attribute_name == 'WebsiteURL':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "WebsiteURL" ]"')
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class S3Backend(BaseBackend):

View File

@ -8,6 +8,7 @@ from moto.core import BaseBackend
from moto.core.utils import iso_8601_datetime
from moto.sqs.models import sqs_backend
from .utils import make_arn_for_topic, make_arn_for_subscription
from boto.exception import BotoServerError
DEFAULT_ACCOUNT_ID = 123456789012
@ -33,6 +34,13 @@ class Topic(object):
subscription.publish(message, message_id)
return message_id
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'TopicName':
return self.name
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class Subscription(object):
def __init__(self, topic, endpoint, protocol):

View File

@ -4,6 +4,7 @@ import hashlib
import time
import re
from boto.exception import BotoServerError
from moto.core import BaseBackend
from moto.core.utils import camelcase_to_underscores, get_random_message_id
from .utils import generate_receipt_handle, unix_time_millis
@ -152,6 +153,15 @@ class Queue(object):
def add_message(self, message):
self._messages.append(message)
def get_cfn_attribute(self, attribute_name):
if attribute_name == 'Arn':
return self.queue_arn
elif attribute_name == 'QueueName':
return self.name
raise BotoServerError(400,
'Bad Request',
'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt')
class SQSBackend(BaseBackend):
def __init__(self):

View File

@ -7,6 +7,8 @@ import sure # noqa
from moto.cloudformation.models import FakeStack
from moto.cloudformation.parsing import resource_class_from_type
from moto.sqs.models import Queue
from boto.cloudformation.stack import Output
from boto.exception import BotoServerError
dummy_template = {
"AWSTemplateFormatVersion": "2010-09-09",
@ -14,8 +16,7 @@ dummy_template = {
"Description": "Create a multi-az, load balanced, Auto Scaled sample web site. The Auto Scaling trigger is based on the CPU utilization of the web servers. The AMI is chosen based on the region in which the stack is run. This example creates a web service running across all availability zones in a region. The instances are load balanced with a simple health check. The web site is available on port 80, however, the instances can be configured to listen on any port (8888 by default). **WARNING** This template creates one or more Amazon EC2 instances. You will be billed for the AWS resources used if you create a stack from this template.",
"Resources": {
"WebServerGroup": {
"Queue": {
"Type": "AWS::SQS::Queue",
"Properties": {
"QueueName": "my-queue",
@ -31,8 +32,7 @@ name_type_template = {
"Description": "Create a multi-az, load balanced, Auto Scaled sample web site. The Auto Scaling trigger is based on the CPU utilization of the web servers. The AMI is chosen based on the region in which the stack is run. This example creates a web service running across all availability zones in a region. The instances are load balanced with a simple health check. The web site is available on port 80, however, the instances can be configured to listen on any port (8888 by default). **WARNING** This template creates one or more Amazon EC2 instances. You will be billed for the AWS resources used if you create a stack from this template.",
"Resources": {
"WebServerGroup": {
"Queue": {
"Type": "AWS::SQS::Queue",
"Properties": {
"VisibilityTimeout": 60,
@ -41,8 +41,40 @@ name_type_template = {
},
}
output_dict = {
"Outputs": {
"Output1": {
"Value": {"Ref": "Queue"},
"Description": "This is a description."
}
}
}
bad_output = {
"Outputs": {
"Output1": {
"Value": {"Fn::GetAtt": ["Queue", "InvalidAttribute"]}
}
}
}
get_attribute_output = {
"Outputs": {
"Output1": {
"Value": {"Fn::GetAtt": ["Queue", "QueueName"]}
}
}
}
outputs_template = dict(dummy_template.items() + output_dict.items())
bad_outputs_template = dict(dummy_template.items() + bad_output.items())
get_attribute_outputs_template = dict(dummy_template.items() + get_attribute_output.items())
dummy_template_json = json.dumps(dummy_template)
name_type_template_json = json.dumps(name_type_template)
output_type_template_json = json.dumps(outputs_template)
bad_output_template_json = json.dumps(bad_outputs_template)
get_attribute_outputs_template_json = json.dumps(get_attribute_outputs_template)
def test_parse_stack_resources():
@ -53,7 +85,7 @@ def test_parse_stack_resources():
)
stack.resource_map.should.have.length_of(1)
list(stack.resource_map.keys())[0].should.equal('WebServerGroup')
list(stack.resource_map.keys())[0].should.equal('Queue')
queue = list(stack.resource_map.values())[0]
queue.should.be.a(Queue)
queue.name.should.equal("my-queue")
@ -72,6 +104,37 @@ def test_parse_stack_with_name_type_resource():
template=name_type_template_json)
stack.resource_map.should.have.length_of(1)
list(stack.resource_map.keys())[0].should.equal('WebServerGroup')
list(stack.resource_map.keys())[0].should.equal('Queue')
queue = list(stack.resource_map.values())[0]
queue.should.be.a(Queue)
def test_parse_stack_with_outputs():
stack = FakeStack(
stack_id="test_id",
name="test_stack",
template=output_type_template_json)
stack.output_map.should.have.length_of(1)
list(stack.output_map.keys())[0].should.equal('Output1')
output = list(stack.output_map.values())[0]
output.should.be.a(Output)
output.description.should.equal("This is a description.")
def test_parse_stack_with_get_attribute_outputs():
stack = FakeStack(
stack_id="test_id",
name="test_stack",
template=get_attribute_outputs_template_json)
stack.output_map.should.have.length_of(1)
list(stack.output_map.keys())[0].should.equal('Output1')
output = list(stack.output_map.values())[0]
output.should.be.a(Output)
output.value.should.equal("my-queue")
def test_parse_stack_with_bad_get_attribute_outputs():
FakeStack.when.called_with(
"test_id", "test_stack", bad_output_template_json).should.throw(BotoServerError)