Merge pull request #964 from whummer/feat/cloudformation-models
Add extended CloudFormation models for Lambda and DynamoDB
This commit is contained in:
commit
5684aa5922
@ -4,6 +4,7 @@ import base64
|
|||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import zipfile
|
import zipfile
|
||||||
@ -16,12 +17,12 @@ except:
|
|||||||
import boto.awslambda
|
import boto.awslambda
|
||||||
from moto.core import BaseBackend, BaseModel
|
from moto.core import BaseBackend, BaseModel
|
||||||
from moto.s3.models import s3_backend
|
from moto.s3.models import s3_backend
|
||||||
from moto.s3.exceptions import MissingBucket
|
from moto.s3.exceptions import MissingBucket, MissingKey
|
||||||
|
|
||||||
|
|
||||||
class LambdaFunction(BaseModel):
|
class LambdaFunction(BaseModel):
|
||||||
|
|
||||||
def __init__(self, spec):
|
def __init__(self, spec, validate_s3=True):
|
||||||
# required
|
# required
|
||||||
self.code = spec['Code']
|
self.code = spec['Code']
|
||||||
self.function_name = spec['FunctionName']
|
self.function_name = spec['FunctionName']
|
||||||
@ -58,24 +59,25 @@ class LambdaFunction(BaseModel):
|
|||||||
self.code_size = len(to_unzip_code)
|
self.code_size = len(to_unzip_code)
|
||||||
self.code_sha_256 = hashlib.sha256(to_unzip_code).hexdigest()
|
self.code_sha_256 = hashlib.sha256(to_unzip_code).hexdigest()
|
||||||
else:
|
else:
|
||||||
# validate s3 bucket
|
# validate s3 bucket and key
|
||||||
|
key = None
|
||||||
try:
|
try:
|
||||||
# FIXME: does not validate bucket region
|
# FIXME: does not validate bucket region
|
||||||
key = s3_backend.get_key(
|
key = s3_backend.get_key(
|
||||||
self.code['S3Bucket'], self.code['S3Key'])
|
self.code['S3Bucket'], self.code['S3Key'])
|
||||||
except MissingBucket:
|
except MissingBucket:
|
||||||
raise ValueError(
|
if do_validate_s3():
|
||||||
"InvalidParameterValueException",
|
raise ValueError(
|
||||||
"Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist")
|
"InvalidParameterValueException",
|
||||||
else:
|
"Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist")
|
||||||
# validate s3 key
|
except MissingKey:
|
||||||
if key is None:
|
if do_validate_s3():
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"InvalidParameterValueException",
|
"InvalidParameterValueException",
|
||||||
"Error occurred while GetObject. S3 Error Code: NoSuchKey. S3 Error Message: The specified key does not exist.")
|
"Error occurred while GetObject. S3 Error Code: NoSuchKey. S3 Error Message: The specified key does not exist.")
|
||||||
else:
|
if key:
|
||||||
self.code_size = key.size
|
self.code_size = key.size
|
||||||
self.code_sha_256 = hashlib.sha256(key.value).hexdigest()
|
self.code_sha_256 = hashlib.sha256(key.value).hexdigest()
|
||||||
self.function_arn = 'arn:aws:lambda:123456789012:function:{0}'.format(
|
self.function_arn = 'arn:aws:lambda:123456789012:function:{0}'.format(
|
||||||
self.function_name)
|
self.function_name)
|
||||||
|
|
||||||
@ -209,6 +211,13 @@ class LambdaFunction(BaseModel):
|
|||||||
fn = backend.create_function(spec)
|
fn = backend.create_function(spec)
|
||||||
return fn
|
return fn
|
||||||
|
|
||||||
|
def get_cfn_attribute(self, attribute_name):
|
||||||
|
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||||
|
if attribute_name == 'Arn':
|
||||||
|
region = 'us-east-1'
|
||||||
|
return 'arn:aws:lambda:{0}:123456789012:function:{1}'.format(region, self.function_name)
|
||||||
|
raise UnformattedGetAttTemplateException()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_zipfile_from_plaintext_code(code):
|
def _create_zipfile_from_plaintext_code(code):
|
||||||
zip_output = io.BytesIO()
|
zip_output = io.BytesIO()
|
||||||
@ -219,6 +228,48 @@ class LambdaFunction(BaseModel):
|
|||||||
return zip_output.read()
|
return zip_output.read()
|
||||||
|
|
||||||
|
|
||||||
|
class EventSourceMapping(BaseModel):
|
||||||
|
|
||||||
|
def __init__(self, spec):
|
||||||
|
# required
|
||||||
|
self.function_name = spec['FunctionName']
|
||||||
|
self.event_source_arn = spec['EventSourceArn']
|
||||||
|
self.starting_position = spec['StartingPosition']
|
||||||
|
|
||||||
|
# optional
|
||||||
|
self.batch_size = spec.get('BatchSize', 100)
|
||||||
|
self.enabled = spec.get('Enabled', True)
|
||||||
|
self.starting_position_timestamp = spec.get('StartingPositionTimestamp', None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||||
|
properties = cloudformation_json['Properties']
|
||||||
|
spec = {
|
||||||
|
'FunctionName': properties['FunctionName'],
|
||||||
|
'EventSourceArn': properties['EventSourceArn'],
|
||||||
|
'StartingPosition': properties['StartingPosition']
|
||||||
|
}
|
||||||
|
optional_properties = 'BatchSize Enabled StartingPositionTimestamp'.split()
|
||||||
|
for prop in optional_properties:
|
||||||
|
if prop in properties:
|
||||||
|
spec[prop] = properties[prop]
|
||||||
|
return EventSourceMapping(spec)
|
||||||
|
|
||||||
|
|
||||||
|
class LambdaVersion(BaseModel):
|
||||||
|
|
||||||
|
def __init__(self, spec):
|
||||||
|
self.version = spec['Version']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||||
|
properties = cloudformation_json['Properties']
|
||||||
|
spec = {
|
||||||
|
'Version': properties.get('Version')
|
||||||
|
}
|
||||||
|
return LambdaVersion(spec)
|
||||||
|
|
||||||
|
|
||||||
class LambdaBackend(BaseBackend):
|
class LambdaBackend(BaseBackend):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -242,6 +293,10 @@ class LambdaBackend(BaseBackend):
|
|||||||
return self._functions.values()
|
return self._functions.values()
|
||||||
|
|
||||||
|
|
||||||
|
def do_validate_s3():
|
||||||
|
return os.environ.get('VALIDATE_LAMBDA_S3', '') in ['', '1', 'true']
|
||||||
|
|
||||||
|
|
||||||
lambda_backends = {}
|
lambda_backends = {}
|
||||||
for region in boto.awslambda.regions():
|
for region in boto.awslambda.regions():
|
||||||
lambda_backends[region.name] = LambdaBackend()
|
lambda_backends[region.name] = LambdaBackend()
|
||||||
|
@ -7,7 +7,9 @@ import warnings
|
|||||||
|
|
||||||
from moto.autoscaling import models as autoscaling_models
|
from moto.autoscaling import models as autoscaling_models
|
||||||
from moto.awslambda import models as lambda_models
|
from moto.awslambda import models as lambda_models
|
||||||
|
from moto.cloudwatch import models as cloudwatch_models
|
||||||
from moto.datapipeline import models as datapipeline_models
|
from moto.datapipeline import models as datapipeline_models
|
||||||
|
from moto.dynamodb import models as dynamodb_models
|
||||||
from moto.ec2 import models as ec2_models
|
from moto.ec2 import models as ec2_models
|
||||||
from moto.ecs import models as ecs_models
|
from moto.ecs import models as ecs_models
|
||||||
from moto.elb import models as elb_models
|
from moto.elb import models as elb_models
|
||||||
@ -27,7 +29,10 @@ from boto.cloudformation.stack import Output
|
|||||||
MODEL_MAP = {
|
MODEL_MAP = {
|
||||||
"AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup,
|
"AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup,
|
||||||
"AWS::AutoScaling::LaunchConfiguration": autoscaling_models.FakeLaunchConfiguration,
|
"AWS::AutoScaling::LaunchConfiguration": autoscaling_models.FakeLaunchConfiguration,
|
||||||
|
"AWS::DynamoDB::Table": dynamodb_models.Table,
|
||||||
|
"AWS::Lambda::EventSourceMapping": lambda_models.EventSourceMapping,
|
||||||
"AWS::Lambda::Function": lambda_models.LambdaFunction,
|
"AWS::Lambda::Function": lambda_models.LambdaFunction,
|
||||||
|
"AWS::Lambda::Version": lambda_models.LambdaVersion,
|
||||||
"AWS::EC2::EIP": ec2_models.ElasticAddress,
|
"AWS::EC2::EIP": ec2_models.ElasticAddress,
|
||||||
"AWS::EC2::Instance": ec2_models.Instance,
|
"AWS::EC2::Instance": ec2_models.Instance,
|
||||||
"AWS::EC2::InternetGateway": ec2_models.InternetGateway,
|
"AWS::EC2::InternetGateway": ec2_models.InternetGateway,
|
||||||
@ -53,6 +58,7 @@ MODEL_MAP = {
|
|||||||
"AWS::IAM::InstanceProfile": iam_models.InstanceProfile,
|
"AWS::IAM::InstanceProfile": iam_models.InstanceProfile,
|
||||||
"AWS::IAM::Role": iam_models.Role,
|
"AWS::IAM::Role": iam_models.Role,
|
||||||
"AWS::KMS::Key": kms_models.Key,
|
"AWS::KMS::Key": kms_models.Key,
|
||||||
|
"AWS::Logs::LogGroup": cloudwatch_models.LogGroup,
|
||||||
"AWS::RDS::DBInstance": rds_models.Database,
|
"AWS::RDS::DBInstance": rds_models.Database,
|
||||||
"AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup,
|
"AWS::RDS::DBSecurityGroup": rds_models.SecurityGroup,
|
||||||
"AWS::RDS::DBSubnetGroup": rds_models.SubnetGroup,
|
"AWS::RDS::DBSubnetGroup": rds_models.SubnetGroup,
|
||||||
@ -133,7 +139,7 @@ def clean_json(resource_json, resources_map):
|
|||||||
try:
|
try:
|
||||||
return resource.get_cfn_attribute(resource_json['Fn::GetAtt'][1])
|
return resource.get_cfn_attribute(resource_json['Fn::GetAtt'][1])
|
||||||
except NotImplementedError as n:
|
except NotImplementedError as n:
|
||||||
logger.warning(n.message.format(
|
logger.warning(str(n).format(
|
||||||
resource_json['Fn::GetAtt'][0]))
|
resource_json['Fn::GetAtt'][0]))
|
||||||
except UnformattedGetAttTemplateException:
|
except UnformattedGetAttTemplateException:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
|
@ -111,6 +111,27 @@ class CloudWatchBackend(BaseBackend):
|
|||||||
return self.metric_data
|
return self.metric_data
|
||||||
|
|
||||||
|
|
||||||
|
class LogGroup(BaseModel):
|
||||||
|
|
||||||
|
def __init__(self, spec):
|
||||||
|
# required
|
||||||
|
self.name = spec['LogGroupName']
|
||||||
|
# optional
|
||||||
|
self.tags = spec.get('Tags', [])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||||
|
properties = cloudformation_json['Properties']
|
||||||
|
spec = {
|
||||||
|
'LogGroupName': properties['LogGroupName']
|
||||||
|
}
|
||||||
|
optional_properties = 'Tags'.split()
|
||||||
|
for prop in optional_properties:
|
||||||
|
if prop in properties:
|
||||||
|
spec[prop] = properties[prop]
|
||||||
|
return LogGroup(spec)
|
||||||
|
|
||||||
|
|
||||||
cloudwatch_backends = {}
|
cloudwatch_backends = {}
|
||||||
for region in boto.ec2.cloudwatch.regions():
|
for region in boto.ec2.cloudwatch.regions():
|
||||||
cloudwatch_backends[region.name] = CloudWatchBackend()
|
cloudwatch_backends[region.name] = CloudWatchBackend()
|
||||||
|
@ -137,6 +137,20 @@ class Table(BaseModel):
|
|||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||||
|
properties = cloudformation_json['Properties']
|
||||||
|
key_attr = [i['AttributeName'] for i in properties['KeySchema'] if i['KeyType'] == 'HASH'][0]
|
||||||
|
key_type = [i['AttributeType'] for i in properties['AttributeDefinitions'] if i['AttributeName'] == key_attr][0]
|
||||||
|
spec = {
|
||||||
|
'name': properties['TableName'],
|
||||||
|
'hash_key_attr': key_attr,
|
||||||
|
'hash_key_type': key_type
|
||||||
|
}
|
||||||
|
# TODO: optional properties still missing:
|
||||||
|
# range_key_attr, range_key_type, read_capacity, write_capacity
|
||||||
|
return Table(**spec)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
count = 0
|
count = 0
|
||||||
for key, value in self.items.items():
|
for key, value in self.items.items():
|
||||||
@ -245,6 +259,14 @@ class Table(BaseModel):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_cfn_attribute(self, attribute_name):
|
||||||
|
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||||
|
if attribute_name == 'StreamArn':
|
||||||
|
region = 'us-east-1'
|
||||||
|
time = '2000-01-01T00:00:00.000'
|
||||||
|
return 'arn:aws:dynamodb:{0}:123456789012:table/{1}/stream/{2}'.format(region, self.name, time)
|
||||||
|
raise UnformattedGetAttTemplateException()
|
||||||
|
|
||||||
|
|
||||||
class DynamoDBBackend(BaseBackend):
|
class DynamoDBBackend(BaseBackend):
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import boto
|
import boto
|
||||||
@ -565,3 +566,80 @@ def test_describe_stack_events_shows_create_update_and_delete():
|
|||||||
assert False, "Too many stack events"
|
assert False, "Too many stack events"
|
||||||
|
|
||||||
list(stack_events_to_look_for).should.be.empty
|
list(stack_events_to_look_for).should.be.empty
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation_deprecated
|
||||||
|
@mock_route53_deprecated
|
||||||
|
def test_create_stack_lambda_and_dynamodb():
|
||||||
|
conn = boto.connect_cloudformation()
|
||||||
|
dummy_template = {
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Description": "Stack Lambda Test 1",
|
||||||
|
"Parameters": {
|
||||||
|
},
|
||||||
|
"Resources": {
|
||||||
|
"func1": {
|
||||||
|
"Type" : "AWS::Lambda::Function",
|
||||||
|
"Properties" : {
|
||||||
|
"Code": {
|
||||||
|
"S3Bucket": "bucket_123",
|
||||||
|
"S3Key": "key_123"
|
||||||
|
},
|
||||||
|
"FunctionName": "func1",
|
||||||
|
"Handler": "handler.handler",
|
||||||
|
"Role": "role1",
|
||||||
|
"Runtime": "python2.7",
|
||||||
|
"Description": "descr",
|
||||||
|
"MemorySize": 12345,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"func1version": {
|
||||||
|
"Type": "AWS::Lambda::LambdaVersion",
|
||||||
|
"Properties" : {
|
||||||
|
"Version": "v1.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tab1": {
|
||||||
|
"Type" : "AWS::DynamoDB::Table",
|
||||||
|
"Properties" : {
|
||||||
|
"TableName": "tab1",
|
||||||
|
"KeySchema": [{
|
||||||
|
"AttributeName": "attr1",
|
||||||
|
"KeyType": "HASH"
|
||||||
|
}],
|
||||||
|
"AttributeDefinitions": [{
|
||||||
|
"AttributeName": "attr1",
|
||||||
|
"AttributeType": "string"
|
||||||
|
}],
|
||||||
|
"ProvisionedThroughput": {
|
||||||
|
"ReadCapacityUnits": 10,
|
||||||
|
"WriteCapacityUnits": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"func1mapping": {
|
||||||
|
"Type": "AWS::Lambda::EventSourceMapping",
|
||||||
|
"Properties" : {
|
||||||
|
"FunctionName": "v1.2.3",
|
||||||
|
"EventSourceArn": "arn:aws:dynamodb:region:XXXXXX:table/tab1/stream/2000T00:00:00.000",
|
||||||
|
"StartingPosition": "0",
|
||||||
|
"BatchSize": 100,
|
||||||
|
"Enabled": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
validate_s3_before = os.environ.get('VALIDATE_LAMBDA_S3', '')
|
||||||
|
try:
|
||||||
|
os.environ['VALIDATE_LAMBDA_S3'] = 'false'
|
||||||
|
conn.create_stack(
|
||||||
|
"test_stack_lambda_1",
|
||||||
|
template_body=json.dumps(dummy_template),
|
||||||
|
parameters={}.items()
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
os.environ['VALIDATE_LAMBDA_S3'] = validate_s3_before
|
||||||
|
|
||||||
|
stack = conn.describe_stacks()[0]
|
||||||
|
resources = stack.list_resources()
|
||||||
|
assert len(resources) == 4
|
||||||
|
Loading…
Reference in New Issue
Block a user