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
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):
|
||||
|
9
tests/test_cloudformation/fixtures/ec2_classic_eip.py
Normal file
9
tests/test_cloudformation/fixtures/ec2_classic_eip.py
Normal file
@ -0,0 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
template = {
|
||||
"Resources": {
|
||||
"EC2EIP": {
|
||||
"Type": "AWS::EC2::EIP"
|
||||
}
|
||||
}
|
||||
}
|
23
tests/test_cloudformation/fixtures/fn_join.py
Normal file
23
tests/test_cloudformation/fixtures/fn_join.py
Normal file
@ -0,0 +1,23 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
template = {
|
||||
"Resources": {
|
||||
"EC2EIP": {
|
||||
"Type": "AWS::EC2::EIP"
|
||||
}
|
||||
},
|
||||
"Outputs": {
|
||||
"EIP": {
|
||||
"Description": "EIP for joining",
|
||||
"Value": {
|
||||
"Fn::Join": [
|
||||
":",
|
||||
[
|
||||
"test eip",
|
||||
{"Ref": "EC2EIP"}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
tests/test_cloudformation/fixtures/vpc_eip.py
Normal file
12
tests/test_cloudformation/fixtures/vpc_eip.py
Normal file
@ -0,0 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
template = {
|
||||
"Resources": {
|
||||
"VPCEIP": {
|
||||
"Type": "AWS::EC2::EIP",
|
||||
"Properties": {
|
||||
"Domain": "vpc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,13 @@ from moto import (
|
||||
mock_iam,
|
||||
)
|
||||
|
||||
from .fixtures import single_instance_with_ebs_volume, vpc_single_instance_in_subnet
|
||||
from .fixtures import (
|
||||
single_instance_with_ebs_volume,
|
||||
vpc_single_instance_in_subnet,
|
||||
ec2_classic_eip,
|
||||
vpc_eip,
|
||||
fn_join
|
||||
)
|
||||
|
||||
|
||||
@mock_cloudformation()
|
||||
@ -75,7 +81,7 @@ def test_stack_ec2_integration():
|
||||
stack = conn.describe_stacks()[0]
|
||||
instance = stack.describe_resources()[0]
|
||||
instance.resource_type.should.equal('AWS::EC2::Instance')
|
||||
instance.logical_resource_id.should.equal("WebServerGroup")
|
||||
instance.logical_resource_id.should.contain("WebServerGroup")
|
||||
instance.physical_resource_id.should.equal(ec2_instance.id)
|
||||
|
||||
|
||||
@ -186,7 +192,7 @@ def test_stack_security_groups():
|
||||
ec2_conn = boto.connect_ec2()
|
||||
security_groups = ec2_conn.get_all_security_groups()
|
||||
for group in security_groups:
|
||||
if group.name == "InstanceSecurityGroup":
|
||||
if "InstanceSecurityGroup" in group.name:
|
||||
instance_group = group
|
||||
else:
|
||||
other_group = group
|
||||
@ -245,6 +251,7 @@ def test_autoscaling_group_with_elb():
|
||||
"InstancePort": "80",
|
||||
"Protocol": "HTTP"
|
||||
}],
|
||||
"LoadBalancerName": "my-elb",
|
||||
"HealthCheck": {
|
||||
"Target": "80",
|
||||
"HealthyThreshold": "3",
|
||||
@ -267,7 +274,7 @@ def test_autoscaling_group_with_elb():
|
||||
|
||||
autoscale_conn = boto.connect_autoscale()
|
||||
autoscale_group = autoscale_conn.get_all_groups()[0]
|
||||
autoscale_group.launch_config_name.should.equal("my-launch-config")
|
||||
autoscale_group.launch_config_name.should.contain("my-launch-config")
|
||||
autoscale_group.load_balancers[0].should.equal('my-elb')
|
||||
|
||||
# Confirm the Launch config was actually created
|
||||
@ -280,13 +287,13 @@ def test_autoscaling_group_with_elb():
|
||||
stack = conn.describe_stacks()[0]
|
||||
resources = stack.describe_resources()
|
||||
as_group_resource = [resource for resource in resources if resource.resource_type == 'AWS::AutoScaling::AutoScalingGroup'][0]
|
||||
as_group_resource.physical_resource_id.should.equal("my-as-group")
|
||||
as_group_resource.physical_resource_id.should.contain("my-as-group")
|
||||
|
||||
launch_config_resource = [resource for resource in resources if resource.resource_type == 'AWS::AutoScaling::LaunchConfiguration'][0]
|
||||
launch_config_resource.physical_resource_id.should.equal("my-launch-config")
|
||||
launch_config_resource.physical_resource_id.should.contain("my-launch-config")
|
||||
|
||||
elb_resource = [resource for resource in resources if resource.resource_type == 'AWS::ElasticLoadBalancing::LoadBalancer'][0]
|
||||
elb_resource.physical_resource_id.should.equal("my-elb")
|
||||
elb_resource.physical_resource_id.should.contain("my-elb")
|
||||
|
||||
|
||||
@mock_ec2()
|
||||
@ -427,18 +434,21 @@ def test_iam_roles():
|
||||
|
||||
iam_conn = boto.connect_iam()
|
||||
|
||||
role = iam_conn.get_role("my-role")
|
||||
role.role_name.should.equal("my-role")
|
||||
role_result = iam_conn.list_roles()['list_roles_response']['list_roles_result']['roles'][0]
|
||||
role = iam_conn.get_role(role_result.role_name)
|
||||
role.role_name.should.contain("my-role")
|
||||
role.path.should.equal("my-path")
|
||||
|
||||
instance_profile = iam_conn.get_instance_profile("my-instance-profile")
|
||||
instance_profile.instance_profile_name.should.equal("my-instance-profile")
|
||||
instance_profile_response = iam_conn.list_instance_profiles()['list_instance_profiles_response']
|
||||
cfn_instance_profile = instance_profile_response['list_instance_profiles_result']['instance_profiles'][0]
|
||||
instance_profile = iam_conn.get_instance_profile(cfn_instance_profile.instance_profile_name)
|
||||
instance_profile.instance_profile_name.should.contain("my-instance-profile")
|
||||
instance_profile.path.should.equal("my-path")
|
||||
instance_profile.role_id.should.equal(role.role_id)
|
||||
|
||||
autoscale_conn = boto.connect_autoscale()
|
||||
launch_config = autoscale_conn.get_all_launch_configurations()[0]
|
||||
launch_config.instance_profile_name.should.equal("my-instance-profile")
|
||||
launch_config.instance_profile_name.should.contain("my-instance-profile")
|
||||
|
||||
stack = conn.describe_stacks()[0]
|
||||
resources = stack.describe_resources()
|
||||
@ -472,3 +482,59 @@ def test_single_instance_with_ebs_volume():
|
||||
resources = stack.describe_resources()
|
||||
ebs_volume = [resource for resource in resources if resource.resource_type == 'AWS::EC2::Volume'][0]
|
||||
ebs_volume.physical_resource_id.should.equal(volume.id)
|
||||
|
||||
|
||||
@mock_ec2()
|
||||
@mock_cloudformation()
|
||||
def test_classic_eip():
|
||||
|
||||
template_json = json.dumps(ec2_classic_eip.template)
|
||||
conn = boto.connect_cloudformation()
|
||||
conn.create_stack(
|
||||
"test_stack",
|
||||
template_body=template_json,
|
||||
)
|
||||
ec2_conn = boto.connect_ec2()
|
||||
eip = ec2_conn.get_all_addresses()[0]
|
||||
|
||||
stack = conn.describe_stacks()[0]
|
||||
resources = stack.describe_resources()
|
||||
cfn_eip = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0]
|
||||
cfn_eip.physical_resource_id.should.equal(eip.public_ip)
|
||||
|
||||
|
||||
@mock_ec2()
|
||||
@mock_cloudformation()
|
||||
def test_vpc_eip():
|
||||
|
||||
template_json = json.dumps(vpc_eip.template)
|
||||
conn = boto.connect_cloudformation()
|
||||
conn.create_stack(
|
||||
"test_stack",
|
||||
template_body=template_json,
|
||||
)
|
||||
ec2_conn = boto.connect_ec2()
|
||||
eip = ec2_conn.get_all_addresses()[0]
|
||||
|
||||
stack = conn.describe_stacks()[0]
|
||||
resources = stack.describe_resources()
|
||||
cfn_eip = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0]
|
||||
cfn_eip.physical_resource_id.should.equal(eip.allocation_id)
|
||||
|
||||
|
||||
@mock_ec2()
|
||||
@mock_cloudformation()
|
||||
def test_fn_join():
|
||||
|
||||
template_json = json.dumps(fn_join.template)
|
||||
conn = boto.connect_cloudformation()
|
||||
conn.create_stack(
|
||||
"test_stack",
|
||||
template_body=template_json,
|
||||
)
|
||||
ec2_conn = boto.connect_ec2()
|
||||
eip = ec2_conn.get_all_addresses()[0]
|
||||
|
||||
stack = conn.describe_stacks()[0]
|
||||
fn_join_output = stack.outputs[0]
|
||||
fn_join_output.value.should.equal('test eip:{0}'.format(eip.public_ip))
|
||||
|
@ -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",
|
||||
@ -25,7 +26,55 @@ dummy_template = {
|
||||
},
|
||||
}
|
||||
|
||||
name_type_template = {
|
||||
"AWSTemplateFormatVersion": "2010-09-09",
|
||||
|
||||
"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": {
|
||||
"Queue": {
|
||||
"Type": "AWS::SQS::Queue",
|
||||
"Properties": {
|
||||
"VisibilityTimeout": 60,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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(list(dummy_template.items()) + list(output_dict.items()))
|
||||
bad_outputs_template = dict(list(dummy_template.items()) + list(bad_output.items()))
|
||||
get_attribute_outputs_template = dict(list(dummy_template.items()) + list(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():
|
||||
@ -36,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")
|
||||
@ -46,3 +95,46 @@ def test_parse_stack_resources():
|
||||
def test_missing_resource_logs(logger):
|
||||
resource_class_from_type("foobar")
|
||||
logger.warning.assert_called_with('No Moto CloudFormation support for %s', 'foobar')
|
||||
|
||||
|
||||
def test_parse_stack_with_name_type_resource():
|
||||
stack = FakeStack(
|
||||
stack_id="test_id",
|
||||
name="test_stack",
|
||||
template=name_type_template_json)
|
||||
|
||||
stack.resource_map.should.have.length_of(1)
|
||||
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)
|
Loading…
Reference in New Issue
Block a user