diff --git a/moto/__init__.py b/moto/__init__.py index 08cd66e9b..e0c5e6c8a 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -6,6 +6,7 @@ __title__ = 'moto' __version__ = '0.4.21' from .autoscaling import mock_autoscaling # flake8: noqa +from .awslambda import mock_lambda # flake8: noqa from .cloudformation import mock_cloudformation # flake8: noqa from .cloudwatch import mock_cloudwatch # flake8: noqa from .datapipeline import mock_datapipeline # flake8: noqa diff --git a/moto/awslambda/__init__.py b/moto/awslambda/__init__.py new file mode 100644 index 000000000..0076f7f76 --- /dev/null +++ b/moto/awslambda/__init__.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from .models import lambda_backends +from ..core.models import MockAWS + + +lambda_backend = lambda_backends['us-east-1'] + + +def mock_lambda(func=None): + if func: + return MockAWS(lambda_backends)(func) + else: + return MockAWS(lambda_backends) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py new file mode 100644 index 000000000..0f22c4a6c --- /dev/null +++ b/moto/awslambda/models.py @@ -0,0 +1,82 @@ +from __future__ import unicode_literals + +import datetime +import boto.awslambda +from moto.core import BaseBackend + + +class LambdaFunction(object): + + def __init__(self, spec): + self.function_name = spec['FunctionName'] + self.run_time = spec['Runtime'] + self.role = spec['Role'] + self.handler = spec['Handler'] + self.description = spec['Description'] + self.timeout = spec['Timeout'] + self.memory_size = spec['MemorySize'] + self.vpc_config = spec.get('VpcConfig', {}) + self.code = spec['Code'] + + self.version = '$LATEST' + self.last_modified = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') + self.code_size = 210 # hello world function + self.code_sha_256 = 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' # hello world function + self.function_arn = 'arn:aws:lambda:123456789012:function:{}'.format(self.function_name) + + def __repr__(self): + return json.dumps(self.get_configuration()) + + def get_configuration(self): + return { + "CodeSha256": self.code_sha_256, + "CodeSize": self.code_size, + "Description": self.description, + "FunctionArn": self.function_arn, + "FunctionName": self.function_name, + "Handler": self.handler, + "LastModified": self.last_modified, + "MemorySize": self.memory_size, + "Role": self.role, + "Runtime": self.run_time, + "Timeout": self.timeout, + "Version": self.version, + "VpcConfig": self.vpc_config, + } + + def get_code(self): + return { + "Code": { + "Location": "s3://lambda-functions.aws.amazon.com/{}".format(self.code['S3Key']), + "RepositoryType": "S3" + }, + "Configuration": self.get_configuration(), + } + + +class LambdaBackend(BaseBackend): + + def __init__(self): + self._functions = {} + + def has_function(self, function_name): + return function_name in self._functions + + def create_function(self, spec): + fn = LambdaFunction(spec) + self._functions[fn.function_name] = fn + return fn + + def get_function(self, function_name): + return self._functions[function_name] + + def delete_function(self, function_name): + del self._functions[function_name] + + def list_functions(self): + return self._functions.values() + + +lambda_backends = {} +for region in boto.awslambda.regions(): + lambda_backends[region.name] = LambdaBackend() diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py new file mode 100644 index 000000000..ffbdee9dc --- /dev/null +++ b/moto/awslambda/responses.py @@ -0,0 +1,78 @@ +from __future__ import unicode_literals + +import json +import re + +from moto.core.responses import BaseResponse +from .models import lambda_backends + + +class LambdaResponse(BaseResponse): + + @classmethod + def root(cls, request, full_url, headers): + if request.method == 'GET': + return cls()._list_functions(request, full_url, headers) + elif request.method == 'POST': + return cls()._create_function(request, full_url, headers) + else: + raise ValueError("Cannot handle request") + + @classmethod + def function(cls, request, full_url, headers): + if request.method == 'GET': + return cls()._get_function(request, full_url, headers) + elif request.method == 'DELETE': + return cls()._delete_function(request, full_url, headers) + else: + raise ValueError("Cannot handle request") + + def _list_functions(self, request, full_url, headers): + lambda_backend = self.get_lambda_backend(full_url) + return 200, headers, json.dumps({ + "Functions": [fn.get_configuration() for fn in lambda_backend.list_functions()], + "NextMarker": "aws-lambda-next-marker", + }) + + def _create_function(self, request, full_url, headers): + lambda_backend = self.get_lambda_backend(full_url) + + spec = json.loads(request.body) + fn = lambda_backend.create_function(spec) + config = fn.get_configuration() + return 200, headers, json.dumps(config) + + def _delete_function(self, request, full_url, headers): + lambda_backend = self.get_lambda_backend(full_url) + + function_name = request.path.split('/')[-1] + + if lambda_backend.has_function(function_name): + lambda_backend.delete_function(function_name) + return 204, headers, "" + else: + return 404, headers, "{}" + + def _get_function(self, request, full_url, headers): + lambda_backend = self.get_lambda_backend(full_url) + + function_name = request.path.split('/')[-1] + + if lambda_backend.has_function(function_name): + fn = lambda_backend.get_function(function_name) + code = fn.get_code() + return 200, headers, json.dumps(code) + else: + return 404, headers, "{}" + + def get_lambda_backend(self, full_url): + from moto.awslambda.models import lambda_backends + region = self._get_aws_region(full_url) + return lambda_backends[region] + + def _get_aws_region(self, full_url): + region = re.search(self.region_regex, full_url) + if region: + return region.group(1) + else: + return self.default_region diff --git a/moto/awslambda/urls.py b/moto/awslambda/urls.py new file mode 100644 index 000000000..1a9197029 --- /dev/null +++ b/moto/awslambda/urls.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from .responses import LambdaResponse + +url_bases = [ + "https?://lambda.(.+).amazonaws.com", +] + +url_paths = { + # double curly braces because the `format()` method is called on the strings + '{0}/\d{{4}}-\d{{2}}-\d{{2}}/functions/?$': LambdaResponse.root, + '{0}/\d{{4}}-\d{{2}}-\d{{2}}/functions/(?P[\w_-]+)/?$': LambdaResponse.function, +} diff --git a/moto/backends.py b/moto/backends.py index 83dfbc00d..7d4da577f 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals from moto.autoscaling import autoscaling_backend +from moto.awslambda import lambda_backend from moto.cloudwatch import cloudwatch_backend from moto.cloudformation import cloudformation_backend from moto.datapipeline import datapipeline_backend @@ -43,7 +44,8 @@ BACKENDS = { 'sns': sns_backend, 'sqs': sqs_backend, 'sts': sts_backend, - 'route53': route53_backend + 'route53': route53_backend, + 'lambda': lambda_backend, } diff --git a/requirements-dev.txt b/requirements-dev.txt index 378c84f52..0e2d603d6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,5 +5,5 @@ sure>=1.2.24 coverage freezegun flask -boto3 +boto3>=1.2.3 six \ No newline at end of file diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py new file mode 100644 index 000000000..01fdc2ae8 --- /dev/null +++ b/tests/test_awslambda/__init__.py @@ -0,0 +1,211 @@ +from __future__ import unicode_literals + +import boto3 +import sure # noqa + +from freezegun import freeze_time +from moto import mock_lambda + + +@mock_lambda +def test_list_functions(): + conn = boto3.client('lambda', 'us-west-2') + + result = conn.list_functions() + + result['Functions'].should.have.length_of(0) + + +@mock_lambda +@freeze_time('2015-01-01 00:00:00') +def test_create_function(): + conn = boto3.client('lambda', 'us-west-2') + + result = conn.create_function( + FunctionName='testFunction', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_handler.handler', + Code={ + 'S3Bucket': 'test-bucket', + 'S3Key': 'test.zip', + }, + Description='test lambda function', + Timeout=3, + MemorySize=128, + Publish=True, + # boto3 doesnt support it + # VpcConfig={ + # "SecurityGroupIds": ["sg-123abc"], + # "SubnetIds": ["subnet-123abc"], + # "VpcId": "vpc-123abc" + # }, + ) + result.should.equal({ + 'FunctionName': 'testFunction', + 'FunctionArn': 'arn:aws:lambda:123456789012:function:testFunction', + 'Runtime': 'python2.7', + 'Role': 'test-iam-role', + 'Handler': 'lambda_handler.handler', + 'CodeSize': 210, + 'Description': 'test lambda function', + 'Timeout': 3, + 'MemorySize': 128, + 'LastModified': '2015-01-01 00:00:00', + 'CodeSha256': 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', + 'Version': '$LATEST', + # boto3 doesnt support it + # VpcConfig={ + # "SecurityGroupIds": ["sg-123abc"], + # "SubnetIds": ["subnet-123abc"], + # "VpcId": "vpc-123abc" + # }, + + 'ResponseMetadata': {'HTTPStatusCode': 200}, + }) + + +@mock_lambda +@freeze_time('2015-01-01 00:00:00') +def test_get_function(): + conn = boto3.client('lambda', 'us-west-2') + + conn.create_function( + FunctionName='testFunction', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_handler.handler', + Code={ + 'S3Bucket': 'test-bucket', + 'S3Key': 'test.zip', + }, + Description='test lambda function', + Timeout=3, + MemorySize=128, + Publish=True, + ) + + result = conn.get_function(FunctionName='testFunction') + + result.should.equal({ + "Code": { + "Location": "s3://lambda-functions.aws.amazon.com/test.zip", + "RepositoryType": "S3" + }, + "Configuration": { + "CodeSha256": 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', + "CodeSize": 210, + "Description": "test lambda function", + "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", + "FunctionName": "testFunction", + "Handler": "lambda_handler.handler", + "LastModified": "2015-01-01 00:00:00", + "MemorySize": 128, + "Role": "test-iam-role", + "Runtime": "python2.7", + "Timeout": 3, + "Version": '$LATEST', + # "VpcConfig": { + # "SecurityGroupIds": [ + # "string" + # ], + # "SubnetIds": [ + # "string" + # ], + # "VpcId": "string" + # } + }, + 'ResponseMetadata': {'HTTPStatusCode': 200}, + }) + + + +@mock_lambda +def test_delete_function(): + conn = boto3.client('lambda', 'us-west-2') + + conn.create_function( + FunctionName='testFunction', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_handler.handler', + Code={ + 'S3Bucket': 'test-bucket', + 'S3Key': 'test.zip', + }, + Description='test lambda function', + Timeout=3, + MemorySize=128, + Publish=True, + ) + + success_result = conn.delete_function(FunctionName='testFunction') + + success_result.should.equal({'ResponseMetadata': {'HTTPStatusCode': 204}}) + + # FIXME:!!!! + # not_found_result = conn.delete_function(FunctionName='testFunctionThatDoesntExist') + # not_found_result.should.equal({'ResponseMetadata': {'HTTPStatusCode': 404}}) + + +@mock_lambda +@freeze_time('2015-01-01 00:00:00') +def test_list_create_list_get_delete_list(): + """ + test `list -> create -> list -> get -> delete -> list` integration + + """ + conn = boto3.client('lambda', 'us-west-2') + + conn.list_functions()['Functions'].should.have.length_of(0) + + conn.create_function( + FunctionName='testFunction', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_handler.handler', + Code={ + 'S3Bucket': 'test-bucket', + 'S3Key': 'test.zip', + }, + Description='test lambda function', + Timeout=3, + MemorySize=128, + Publish=True, + ) + expected_function_result = { + "Code": { + "Location": "s3://lambda-functions.aws.amazon.com/test.zip", + "RepositoryType": "S3" + }, + "Configuration": { + "CodeSha256": 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', + "CodeSize": 210, + "Description": "test lambda function", + "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", + "FunctionName": "testFunction", + "Handler": "lambda_handler.handler", + "LastModified": "2015-01-01 00:00:00", + "MemorySize": 128, + "Role": "test-iam-role", + "Runtime": "python2.7", + "Timeout": 3, + "Version": '$LATEST', + # "VpcConfig": { + # "SecurityGroupIds": [ + # "string" + # ], + # "SubnetIds": [ + # "string" + # ], + # "VpcId": "string" + # } + }, + 'ResponseMetadata': {'HTTPStatusCode': 200}, + } + conn.list_functions()['Functions'].should.equal([expected_function_result['Configuration']]) + + conn.get_function(FunctionName='testFunction').should.equal(expected_function_result) + conn.delete_function(FunctionName='testFunction') + + conn.list_functions()['Functions'].should.have.length_of(0) diff --git a/tests/test_awslambda/test_awslambda.py b/tests/test_awslambda/test_awslambda.py new file mode 100644 index 000000000..e69de29bb