create CloudFormation outputs and enable 'Fn::GetAtt' to work.
This commit is contained in:
parent
832769b8a7
commit
1d9ffafaa5
10 changed files with 269 additions and 8 deletions
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue