Fix merge conflicts. Add basic cloudformation support. Closes #111.
This commit is contained in:
parent
069c48b43a
commit
ef876dd27e
28 changed files with 2473 additions and 11 deletions
2
moto/cloudformation/__init__.py
Normal file
2
moto/cloudformation/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .models import cloudformation_backend
|
||||
mock_cloudformation = cloudformation_backend.decorator
|
||||
70
moto/cloudformation/models.py
Normal file
70
moto/cloudformation/models.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import json
|
||||
|
||||
from moto.core import BaseBackend
|
||||
|
||||
from .parsing import ResourceMap
|
||||
from .utils import generate_stack_id
|
||||
|
||||
|
||||
class FakeStack(object):
|
||||
def __init__(self, stack_id, name, template):
|
||||
self.stack_id = stack_id
|
||||
self.name = name
|
||||
self.template = template
|
||||
|
||||
template_dict = json.loads(self.template)
|
||||
self.description = template_dict.get('Description')
|
||||
|
||||
self.resource_map = ResourceMap(stack_id, name, template_dict)
|
||||
self.resource_map.create()
|
||||
|
||||
@property
|
||||
def stack_resources(self):
|
||||
return self.resource_map.values()
|
||||
|
||||
|
||||
class CloudFormationBackend(BaseBackend):
|
||||
|
||||
def __init__(self):
|
||||
self.stacks = {}
|
||||
|
||||
def create_stack(self, name, template):
|
||||
stack_id = generate_stack_id(name)
|
||||
new_stack = FakeStack(stack_id=stack_id, name=name, template=template)
|
||||
self.stacks[stack_id] = new_stack
|
||||
return new_stack
|
||||
|
||||
def describe_stacks(self, names):
|
||||
stacks = self.stacks.values()
|
||||
if names:
|
||||
return [stack for stack in stacks if stack.name in names]
|
||||
else:
|
||||
return stacks
|
||||
|
||||
def list_stacks(self):
|
||||
return self.stacks.values()
|
||||
|
||||
def get_stack(self, name_or_stack_id):
|
||||
if name_or_stack_id in self.stacks:
|
||||
# Lookup by stack id
|
||||
return self.stacks.get(name_or_stack_id)
|
||||
else:
|
||||
# Lookup by stack name
|
||||
return [stack for stack in self.stacks.values() if stack.name == name_or_stack_id][0]
|
||||
|
||||
# def update_stack(self, name, template):
|
||||
# stack = self.get_stack(name)
|
||||
# stack.template = template
|
||||
# return stack
|
||||
|
||||
def delete_stack(self, name_or_stack_id):
|
||||
if name_or_stack_id in self.stacks:
|
||||
# Delete by stack id
|
||||
return self.stacks.pop(name_or_stack_id, None)
|
||||
else:
|
||||
# Delete by stack name
|
||||
stack_to_delete = [stack for stack in self.stacks.values() if stack.name == name_or_stack_id][0]
|
||||
self.delete_stack(stack_to_delete.stack_id)
|
||||
|
||||
|
||||
cloudformation_backend = CloudFormationBackend()
|
||||
140
moto/cloudformation/parsing.py
Normal file
140
moto/cloudformation/parsing.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import collections
|
||||
import logging
|
||||
|
||||
from moto.autoscaling import models as autoscaling_models
|
||||
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
|
||||
|
||||
MODEL_MAP = {
|
||||
"AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup,
|
||||
"AWS::AutoScaling::LaunchConfiguration": autoscaling_models.FakeLaunchConfiguration,
|
||||
"AWS::EC2::EIP": ec2_models.ElasticAddress,
|
||||
"AWS::EC2::Instance": ec2_models.Instance,
|
||||
"AWS::EC2::InternetGateway": ec2_models.InternetGateway,
|
||||
"AWS::EC2::Route": ec2_models.Route,
|
||||
"AWS::EC2::RouteTable": ec2_models.RouteTable,
|
||||
"AWS::EC2::SecurityGroup": ec2_models.SecurityGroup,
|
||||
"AWS::EC2::Subnet": ec2_models.Subnet,
|
||||
"AWS::EC2::SubnetRouteTableAssociation": ec2_models.SubnetRouteTableAssociation,
|
||||
"AWS::EC2::Volume": ec2_models.Volume,
|
||||
"AWS::EC2::VolumeAttachment": ec2_models.VolumeAttachment,
|
||||
"AWS::EC2::VPC": ec2_models.VPC,
|
||||
"AWS::EC2::VPCGatewayAttachment": ec2_models.VPCGatewayAttachment,
|
||||
"AWS::ElasticLoadBalancing::LoadBalancer": elb_models.FakeLoadBalancer,
|
||||
"AWS::IAM::InstanceProfile": iam_models.InstanceProfile,
|
||||
"AWS::IAM::Role": iam_models.Role,
|
||||
"AWS::SQS::Queue": sqs_models.Queue,
|
||||
}
|
||||
|
||||
# Just ignore these models types for now
|
||||
NULL_MODELS = [
|
||||
"AWS::CloudFormation::WaitCondition",
|
||||
"AWS::CloudFormation::WaitConditionHandle",
|
||||
]
|
||||
|
||||
logger = logging.getLogger("moto")
|
||||
|
||||
|
||||
def clean_json(resource_json, resources_map):
|
||||
"""
|
||||
Cleanup the a resource dict. For now, this just means replacing any Ref node
|
||||
with the corresponding physical_resource_id.
|
||||
|
||||
Eventually, this is where we would add things like function parsing (fn::)
|
||||
"""
|
||||
if isinstance(resource_json, dict):
|
||||
if 'Ref' in resource_json:
|
||||
# Parse resource reference
|
||||
resource = resources_map[resource_json['Ref']]
|
||||
if hasattr(resource, 'physical_resource_id'):
|
||||
return resource.physical_resource_id
|
||||
else:
|
||||
return resource
|
||||
|
||||
cleaned_json = {}
|
||||
for key, value in resource_json.iteritems():
|
||||
cleaned_json[key] = clean_json(value, resources_map)
|
||||
return cleaned_json
|
||||
elif isinstance(resource_json, list):
|
||||
return [clean_json(val, resources_map) for val in resource_json]
|
||||
else:
|
||||
return resource_json
|
||||
|
||||
|
||||
def resource_class_from_type(resource_type):
|
||||
if resource_type in NULL_MODELS:
|
||||
return None
|
||||
if resource_type not in MODEL_MAP:
|
||||
logger.warning("No Moto CloudFormation support for %s", resource_type)
|
||||
return None
|
||||
return MODEL_MAP.get(resource_type)
|
||||
|
||||
|
||||
def parse_resource(resource_name, 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 = resource_class.create_from_cloudformation_json(resource_name, resource_json)
|
||||
resource.type = resource_type
|
||||
resource.logical_resource_id = resource_name
|
||||
return resource
|
||||
|
||||
|
||||
class ResourceMap(collections.Mapping):
|
||||
"""
|
||||
This is a lazy loading map for resources. This allows us to create resources
|
||||
without needing to create a full dependency tree. Upon creation, each
|
||||
each resources is passed this lazy map that it can grab dependencies from.
|
||||
"""
|
||||
|
||||
def __init__(self, stack_id, stack_name, template):
|
||||
self._template = template
|
||||
self._resource_json_map = template['Resources']
|
||||
|
||||
# Create the default resources
|
||||
self._parsed_resources = {
|
||||
"AWS::AccountId": "123456789012",
|
||||
"AWS::Region": "us-east-1",
|
||||
"AWS::StackId": stack_id,
|
||||
"AWS::StackName": stack_name,
|
||||
}
|
||||
|
||||
def __getitem__(self, key):
|
||||
resource_name = key
|
||||
|
||||
if resource_name in self._parsed_resources:
|
||||
return self._parsed_resources[resource_name]
|
||||
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
|
||||
return new_resource
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.resource_names)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._resource_json_map)
|
||||
|
||||
@property
|
||||
def resource_names(self):
|
||||
return self._resource_json_map.keys()
|
||||
|
||||
def load_parameters(self):
|
||||
parameters = self._template.get('Parameters', {})
|
||||
for parameter_name, parameter in parameters.items():
|
||||
# Just initialize parameters to empty string for now.
|
||||
self._parsed_resources[parameter_name] = ""
|
||||
|
||||
def create(self):
|
||||
self.load_parameters()
|
||||
|
||||
# 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]
|
||||
128
moto/cloudformation/responses.py
Normal file
128
moto/cloudformation/responses.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import json
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import cloudformation_backend
|
||||
|
||||
|
||||
class CloudFormationResponse(BaseResponse):
|
||||
|
||||
def create_stack(self):
|
||||
stack_name = self._get_param('StackName')
|
||||
stack_body = self._get_param('TemplateBody')
|
||||
|
||||
stack = cloudformation_backend.create_stack(
|
||||
name=stack_name,
|
||||
template=stack_body,
|
||||
)
|
||||
stack_body = {
|
||||
'CreateStackResponse': {
|
||||
'CreateStackResult': {
|
||||
'StackId': stack.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
return json.dumps(stack_body)
|
||||
|
||||
def describe_stacks(self):
|
||||
names = [value[0] for key, value in self.querystring.items() if "StackName" in key]
|
||||
stacks = cloudformation_backend.describe_stacks(names)
|
||||
|
||||
template = Template(DESCRIBE_STACKS_TEMPLATE)
|
||||
return template.render(stacks=stacks)
|
||||
|
||||
def describe_stack_resources(self):
|
||||
stack_name = self._get_param('StackName')
|
||||
stack = cloudformation_backend.get_stack(stack_name)
|
||||
|
||||
template = Template(LIST_STACKS_RESOURCES_RESPONSE)
|
||||
return template.render(stack=stack)
|
||||
|
||||
def list_stacks(self):
|
||||
stacks = cloudformation_backend.list_stacks()
|
||||
template = Template(LIST_STACKS_RESPONSE)
|
||||
return template.render(stacks=stacks)
|
||||
|
||||
def get_template(self):
|
||||
name_or_stack_id = self.querystring.get('StackName')[0]
|
||||
|
||||
stack = cloudformation_backend.get_stack(name_or_stack_id)
|
||||
return stack.template
|
||||
|
||||
# def update_stack(self):
|
||||
# stack_name = self._get_param('StackName')
|
||||
# stack_body = self._get_param('TemplateBody')
|
||||
|
||||
# stack = cloudformation_backend.update_stack(
|
||||
# name=stack_name,
|
||||
# template=stack_body,
|
||||
# )
|
||||
# stack_body = {
|
||||
# 'UpdateStackResponse': {
|
||||
# 'UpdateStackResult': {
|
||||
# 'StackId': stack.name,
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# return json.dumps(stack_body)
|
||||
|
||||
def delete_stack(self):
|
||||
name_or_stack_id = self.querystring.get('StackName')[0]
|
||||
|
||||
cloudformation_backend.delete_stack(name_or_stack_id)
|
||||
return json.dumps({
|
||||
'DeleteStackResponse': {
|
||||
'DeleteStackResult': {},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResult>
|
||||
<Stacks>
|
||||
{% for stack in stacks %}
|
||||
<member>
|
||||
<StackName>{{ stack.name }}</StackName>
|
||||
<StackId>{{ stack.stack_id }}</StackId>
|
||||
<CreationTime>2010-07-27T22:28:28Z</CreationTime>
|
||||
<StackStatus>CREATE_COMPLETE</StackStatus>
|
||||
<DisableRollback>false</DisableRollback>
|
||||
<Outputs></Outputs>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Stacks>
|
||||
</DescribeStacksResult>"""
|
||||
|
||||
|
||||
LIST_STACKS_RESPONSE = """<ListStacksResponse>
|
||||
<ListStacksResult>
|
||||
<StackSummaries>
|
||||
{% for stack in stacks %}
|
||||
<member>
|
||||
<StackId>{{ stack.id }}</StackId>
|
||||
<StackStatus>CREATE_IN_PROGRESS</StackStatus>
|
||||
<StackName>{{ stack.name }}</StackName>
|
||||
<CreationTime>2011-05-23T15:47:44Z</CreationTime>
|
||||
<TemplateDescription>{{ stack.description }}</TemplateDescription>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</StackSummaries>
|
||||
</ListStacksResult>
|
||||
</ListStacksResponse>"""
|
||||
|
||||
|
||||
LIST_STACKS_RESOURCES_RESPONSE = """<DescribeStackResourcesResult>
|
||||
<StackResources>
|
||||
{% for resource in stack.stack_resources %}
|
||||
<member>
|
||||
<StackId>{{ stack.stack_id }}</StackId>
|
||||
<StackName>{{ stack.name }}</StackName>
|
||||
<LogicalResourceId>{{ resource.logical_resource_id }}</LogicalResourceId>
|
||||
<PhysicalResourceId>{{ resource.physical_resource_id }}</PhysicalResourceId>
|
||||
<ResourceType>{{ resource.type }}</ResourceType>
|
||||
<Timestamp>2010-07-27T22:27:28Z</Timestamp>
|
||||
<ResourceStatus>CREATE_COMPLETE</ResourceStatus>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</StackResources>
|
||||
</DescribeStackResourcesResult>"""
|
||||
9
moto/cloudformation/urls.py
Normal file
9
moto/cloudformation/urls.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from .responses import CloudFormationResponse
|
||||
|
||||
url_bases = [
|
||||
"https?://cloudformation.(.+).amazonaws.com",
|
||||
]
|
||||
|
||||
url_paths = {
|
||||
'{0}/$': CloudFormationResponse().dispatch,
|
||||
}
|
||||
6
moto/cloudformation/utils.py
Normal file
6
moto/cloudformation/utils.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import uuid
|
||||
|
||||
|
||||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue