Merge pull request #235 from joekiller/enhance/cloudformation_getatt_tags_and_resource_naming
Enhance/cloudformation getatt tags and resource naming
This commit is contained in:
commit
c02ed667d5
16 changed files with 486 additions and 36 deletions
6
moto/cloudformation/exceptions.py
Normal file
6
moto/cloudformation/exceptions.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class UnformattedGetAttTemplateException(Exception):
|
||||
description = 'Template error: resource {0} does not support attribute type {1} in Fn::GetAtt'
|
||||
status_code = 400
|
||||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ from moto.ec2 import models as ec2_models
|
|||
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 .exceptions import UnformattedGetAttTemplateException
|
||||
from boto.cloudformation.stack import Output
|
||||
from boto.exception import BotoServerError
|
||||
|
||||
MODEL_MAP = {
|
||||
"AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup,
|
||||
|
|
@ -29,6 +33,20 @@ MODEL_MAP = {
|
|||
"AWS::SQS::Queue": sqs_models.Queue,
|
||||
}
|
||||
|
||||
# http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html
|
||||
NAME_TYPE_MAP = {
|
||||
"AWS::CloudWatch::Alarm": "Alarm",
|
||||
"AWS::DynamoDB::Table": "TableName",
|
||||
"AWS::ElastiCache::CacheCluster": "ClusterName",
|
||||
"AWS::ElasticBeanstalk::Application": "ApplicationName",
|
||||
"AWS::ElasticBeanstalk::Environment": "EnvironmentName",
|
||||
"AWS::ElasticLoadBalancing::LoadBalancer": "LoadBalancerName",
|
||||
"AWS::RDS::DBInstance": "DBInstanceIdentifier",
|
||||
"AWS::S3::Bucket": "BucketName",
|
||||
"AWS::SNS::Topic": "TopicName",
|
||||
"AWS::SQS::Queue": "QueueName"
|
||||
}
|
||||
|
||||
# Just ignore these models types for now
|
||||
NULL_MODELS = [
|
||||
"AWS::CloudFormation::WaitCondition",
|
||||
|
|
@ -54,6 +72,28 @@ def clean_json(resource_json, resources_map):
|
|||
else:
|
||||
return resource
|
||||
|
||||
if 'Fn::GetAtt' in resource_json:
|
||||
resource = resources_map[resource_json['Fn::GetAtt'][0]]
|
||||
if resource is None:
|
||||
return resource_json
|
||||
try:
|
||||
return resource.get_cfn_attribute(resource_json['Fn::GetAtt'][1])
|
||||
except NotImplementedError as n:
|
||||
logger.warning(n.message.format(resource_json['Fn::GetAtt'][0]))
|
||||
except UnformattedGetAttTemplateException:
|
||||
raise BotoServerError(
|
||||
UnformattedGetAttTemplateException.status_code,
|
||||
'Bad Request',
|
||||
UnformattedGetAttTemplateException.description.format(
|
||||
resource_json['Fn::GetAtt'][0], resource_json['Fn::GetAtt'][1]))
|
||||
|
||||
if 'Fn::Join' in resource_json:
|
||||
join_list = []
|
||||
for val in resource_json['Fn::Join'][1]:
|
||||
cleaned_val = clean_json(val, resources_map)
|
||||
join_list.append(cleaned_val if cleaned_val else '{0}'.format(val))
|
||||
return resource_json['Fn::Join'][0].join(join_list)
|
||||
|
||||
cleaned_json = {}
|
||||
for key, value in resource_json.items():
|
||||
cleaned_json[key] = clean_json(value, resources_map)
|
||||
|
|
@ -73,19 +113,49 @@ def resource_class_from_type(resource_type):
|
|||
return MODEL_MAP.get(resource_type)
|
||||
|
||||
|
||||
def parse_resource(resource_name, resource_json, resources_map):
|
||||
def resource_name_property_from_type(resource_type):
|
||||
if resource_type not in NAME_TYPE_MAP:
|
||||
return None
|
||||
return NAME_TYPE_MAP.get(resource_type)
|
||||
|
||||
|
||||
def parse_resource(logical_id, resource_json, resources_map):
|
||||
resource_type = resource_json['Type']
|
||||
resource_class = resource_class_from_type(resource_type)
|
||||
if not resource_class:
|
||||
return None
|
||||
|
||||
resource_json = clean_json(resource_json, resources_map)
|
||||
resource_name_property = resource_name_property_from_type(resource_type)
|
||||
if resource_name_property:
|
||||
if not 'Properties' in resource_json:
|
||||
resource_json['Properties'] = dict()
|
||||
if not resource_name_property in resource_json['Properties']:
|
||||
resource_json['Properties'][resource_name_property] = '{0}-{1}-{2}'.format(
|
||||
resources_map.get('AWS::StackName'),
|
||||
logical_id,
|
||||
random_suffix())
|
||||
resource_name = resource_json['Properties'][resource_name_property]
|
||||
else:
|
||||
resource_name = '{0}-{1}-{2}'.format(resources_map.get('AWS::StackName'),
|
||||
logical_id,
|
||||
random_suffix())
|
||||
|
||||
resource = resource_class.create_from_cloudformation_json(resource_name, resource_json)
|
||||
resource.type = resource_type
|
||||
resource.logical_resource_id = resource_name
|
||||
resource.logical_resource_id = logical_id
|
||||
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
|
||||
|
|
@ -106,24 +176,24 @@ class ResourceMap(collections.Mapping):
|
|||
}
|
||||
|
||||
def __getitem__(self, key):
|
||||
resource_name = key
|
||||
resource_logical_id = key
|
||||
|
||||
if resource_name in self._parsed_resources:
|
||||
return self._parsed_resources[resource_name]
|
||||
if resource_logical_id in self._parsed_resources:
|
||||
return self._parsed_resources[resource_logical_id]
|
||||
else:
|
||||
resource_json = self._resource_json_map.get(resource_name)
|
||||
new_resource = parse_resource(resource_name, resource_json, self)
|
||||
self._parsed_resources[resource_name] = new_resource
|
||||
resource_json = self._resource_json_map.get(resource_logical_id)
|
||||
new_resource = parse_resource(resource_logical_id, resource_json, self)
|
||||
self._parsed_resources[resource_logical_id] = new_resource
|
||||
return new_resource
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.resource_names)
|
||||
return iter(self.resources)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._resource_json_map)
|
||||
|
||||
@property
|
||||
def resource_names(self):
|
||||
def resources(self):
|
||||
return self._resource_json_map.keys()
|
||||
|
||||
def load_parameters(self):
|
||||
|
|
@ -137,5 +207,45 @@ class ResourceMap(collections.Mapping):
|
|||
|
||||
# Since this is a lazy map, to create every object we just need to
|
||||
# iterate through self.
|
||||
for resource_name in self.resource_names:
|
||||
self[resource_name]
|
||||
tags = {'aws:cloudformation:stack-name': self.get('AWS::StackName'),
|
||||
'aws:cloudformation:stack-id': self.get('AWS::StackId')}
|
||||
for resource in self.resources:
|
||||
self[resource]
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
from __future__ import unicode_literals
|
||||
import uuid
|
||||
import six
|
||||
import random
|
||||
|
||||
|
||||
def generate_stack_id(stack_name):
|
||||
random_id = uuid.uuid4()
|
||||
return "arn:aws:cloudformation:us-east-1:123456789:stack/{0}/{1}".format(stack_name, random_id)
|
||||
|
||||
|
||||
def random_suffix():
|
||||
size = 12
|
||||
chars = list(range(10)) + ['A-Z']
|
||||
return ''.join(six.text_type(random.choice(chars)) for x in range(size))
|
||||
|
|
|
|||
|
|
@ -192,6 +192,14 @@ class NetworkInterface(object):
|
|||
else:
|
||||
return self._group_set
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'PrimaryPrivateIpAddress':
|
||||
return self.private_ip_address
|
||||
elif attribute_name == 'SecondaryPrivateIpAddresses':
|
||||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "SecondaryPrivateIpAddresses" ]"')
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class NetworkInterfaceBackend(object):
|
||||
def __init__(self):
|
||||
|
|
@ -435,6 +443,20 @@ class Instance(BotoInstance, TaggedEC2Resource):
|
|||
eni.attachment_id = None
|
||||
eni.device_index = None
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
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 UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class InstanceBackend(object):
|
||||
|
||||
|
|
@ -994,6 +1016,12 @@ class SecurityGroup(object):
|
|||
return False
|
||||
return True
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'GroupId':
|
||||
return self.id
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class SecurityGroupBackend(object):
|
||||
|
||||
|
|
@ -1517,6 +1545,12 @@ class Subnet(TaggedEC2Resource):
|
|||
|
||||
return filter_value
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'AvailabilityZone':
|
||||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "AvailabilityZone" ]"')
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class SubnetBackend(object):
|
||||
def __init__(self):
|
||||
|
|
@ -1981,13 +2015,16 @@ class ElasticAddress(object):
|
|||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
|
||||
properties = cloudformation_json['Properties']
|
||||
properties = cloudformation_json.get('Properties')
|
||||
instance_id = None
|
||||
if properties:
|
||||
domain=properties.get('Domain')
|
||||
eip = ec2_backend.allocate_address(
|
||||
domain=domain if domain else 'standard')
|
||||
instance_id = properties.get('InstanceId')
|
||||
else:
|
||||
eip = ec2_backend.allocate_address(domain='standard')
|
||||
|
||||
eip = ec2_backend.allocate_address(
|
||||
domain=properties['Domain']
|
||||
)
|
||||
|
||||
instance_id = properties.get('InstanceId')
|
||||
if instance_id:
|
||||
instance = ec2_backend.get_instance_by_id(instance_id)
|
||||
ec2_backend.associate_address(instance, address=eip.public_ip)
|
||||
|
|
@ -1996,7 +2033,13 @@ class ElasticAddress(object):
|
|||
|
||||
@property
|
||||
def physical_resource_id(self):
|
||||
return self.allocation_id
|
||||
return self.allocation_id if self.allocation_id else self.public_ip
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'AllocationId':
|
||||
return self.allocation_id
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class ElasticAddressBackend(object):
|
||||
|
|
|
|||
|
|
@ -56,6 +56,20 @@ class FakeLoadBalancer(object):
|
|||
def physical_resource_id(self):
|
||||
return self.name
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
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 UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class ELBBackend(BaseBackend):
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ class Role(object):
|
|||
def physical_resource_id(self):
|
||||
return self.id
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'Arn':
|
||||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class InstanceProfile(object):
|
||||
def __init__(self, instance_profile_id, name, path, roles):
|
||||
|
|
@ -53,6 +59,12 @@ class InstanceProfile(object):
|
|||
def physical_resource_id(self):
|
||||
return self.name
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'Arn':
|
||||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class Certificate(object):
|
||||
def __init__(self, cert_name, cert_body, private_key, cert_chain=None, path=None):
|
||||
|
|
@ -78,6 +90,12 @@ class AccessKey(object):
|
|||
"%Y-%m-%d-%H-%M-%S"
|
||||
)
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'SecretAccessKey':
|
||||
return self.secret_access_key
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class Group(object):
|
||||
def __init__(self, name, path='/'):
|
||||
|
|
@ -91,6 +109,12 @@ class Group(object):
|
|||
|
||||
self.users = []
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'Arn':
|
||||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class User(object):
|
||||
def __init__(self, name, path='/'):
|
||||
|
|
@ -143,6 +167,12 @@ class User(object):
|
|||
else:
|
||||
raise BotoServerError(404, 'Not Found')
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'Arn':
|
||||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"')
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class IAMBackend(BaseBackend):
|
||||
|
||||
|
|
|
|||
|
|
@ -170,6 +170,14 @@ class FakeBucket(object):
|
|||
def is_versioned(self):
|
||||
return self.versioning_status == 'Enabled'
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'DomainName':
|
||||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DomainName" ]"')
|
||||
elif attribute_name == 'WebsiteURL':
|
||||
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "WebsiteURL" ]"')
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class S3Backend(BaseBackend):
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ class Topic(object):
|
|||
subscription.publish(message, message_id)
|
||||
return message_id
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'TopicName':
|
||||
return self.name
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class Subscription(object):
|
||||
def __init__(self, topic, endpoint, protocol):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import hashlib
|
|||
import time
|
||||
import re
|
||||
|
||||
|
||||
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,14 @@ class Queue(object):
|
|||
def add_message(self, message):
|
||||
self._messages.append(message)
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'Arn':
|
||||
return self.queue_arn
|
||||
elif attribute_name == 'QueueName':
|
||||
return self.name
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
||||
class SQSBackend(BaseBackend):
|
||||
def __init__(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue