create CloudFormation outputs and enable 'Fn::GetAtt' to work.
This commit is contained in:
parent
832769b8a7
commit
1d9ffafaa5
@ -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):
|
||||
|
@ -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)
|
Loading…
x
Reference in New Issue
Block a user