From bfad9b9ed6182e48cec7a6cb1f044c0b759bab9e Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Fri, 12 Feb 2016 14:39:20 -0500 Subject: [PATCH 01/17] [lambda] Implement ListFunctions, GetFunction, and DeleteFunction --- moto/__init__.py | 1 + moto/awslambda/__init__.py | 13 ++ moto/awslambda/models.py | 82 ++++++++++ moto/awslambda/responses.py | 78 +++++++++ moto/awslambda/urls.py | 12 ++ moto/backends.py | 4 +- requirements-dev.txt | 2 +- tests/test_awslambda/__init__.py | 211 +++++++++++++++++++++++++ tests/test_awslambda/test_awslambda.py | 0 9 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 moto/awslambda/__init__.py create mode 100644 moto/awslambda/models.py create mode 100644 moto/awslambda/responses.py create mode 100644 moto/awslambda/urls.py create mode 100644 tests/test_awslambda/__init__.py create mode 100644 tests/test_awslambda/test_awslambda.py 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 From 576b9f683c3a9d0a771b8427ace4fcaf9c303878 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Fri, 12 Feb 2016 15:38:39 -0500 Subject: [PATCH 02/17] [lambda] Add CloudFormation support for AWS::Lambda::Function --- moto/awslambda/models.py | 43 +++++++++++++---- moto/cloudformation/parsing.py | 2 + .../test_cloudformation_stack_integration.py | 48 +++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 0f22c4a6c..fd2ce11c1 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -8,16 +8,21 @@ 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', {}) + # required self.code = spec['Code'] + self.function_name = spec['FunctionName'] + self.handler = spec['Handler'] + self.role = spec['Role'] + self.run_time = spec['Runtime'] + # optional + self.description = spec.get('Description', '') + self.memory_size = spec.get('MemorySize', 128) + self.publish = spec.get('Publish', False) + self.timeout = spec.get('Timeout', 3) + self.vpc_config = spec.get('VpcConfig', {}) + + # auto-generated self.version = '$LATEST' self.last_modified = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') self.code_size = 210 # hello world function @@ -53,6 +58,28 @@ class LambdaFunction(object): "Configuration": self.get_configuration(), } + @classmethod + def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): + properties = cloudformation_json['Properties'] + + backend = lambda_backends[region_name] + fn = backend.create_function({ + # required + 'Code': properties['Code'], + 'FunctionName': resource_name, + 'Handler': properties['Handler'], + 'Role': properties['Role'], + 'Runtime': properties['Runtime'], + + # optional + 'Description': properties.get('Description', ''), + 'MemorySize': properties.get('MemorySize', 128), + 'Publish': properties.get('Publish', False), + 'Timeout': properties.get('Timeout', 3), + 'VpcConfig': properties.get('VpcConfig', {}), + }) + return fn + class LambdaBackend(BaseBackend): diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 418d736d5..ebdd83634 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -4,6 +4,7 @@ import functools import logging from moto.autoscaling import models as autoscaling_models +from moto.awslambda import models as lambda_models from moto.datapipeline import models as datapipeline_models from moto.ec2 import models as ec2_models from moto.elb import models as elb_models @@ -21,6 +22,7 @@ from boto.exception import BotoServerError MODEL_MAP = { "AWS::AutoScaling::AutoScalingGroup": autoscaling_models.FakeAutoScalingGroup, "AWS::AutoScaling::LaunchConfiguration": autoscaling_models.FakeLaunchConfiguration, + "AWS::Lambda::Function": lambda_models.LambdaFunction, "AWS::EC2::EIP": ec2_models.ElasticAddress, "AWS::EC2::Instance": ec2_models.Instance, "AWS::EC2::InternetGateway": ec2_models.InternetGateway, diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 29b5396e1..4a7ff7f77 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -13,6 +13,7 @@ import boto.redshift import boto.sns import boto.sqs import boto.vpc +import boto3 import sure # noqa from moto import ( @@ -22,6 +23,7 @@ from moto import ( mock_ec2, mock_elb, mock_iam, + mock_lambda, mock_rds, mock_redshift, mock_route53, @@ -1478,3 +1480,49 @@ def test_datapipeline(): stack_resources = cf_conn.list_stack_resources(stack_id) stack_resources.should.have.length_of(1) stack_resources[0].physical_resource_id.should.equal(data_pipelines['pipelineIdList'][0]['id']) + + +@mock_cloudformation +@mock_lambda +def test_lambda_function(): + conn = boto3.client('lambda', 'us-east-1') + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "lambdaTest": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": {"Fn::Join": [ + "\n", + """ + exports.handler = function(event, context) { + context.succeed(); + } + """.splitlines() + ]} + }, + "Handler": "index.handler", + "Description": "Test function", + "MemorySize": 128, + "Role": "test-role", + "Runtime": "nodejs", + } + }, + } + } + + template_json = json.dumps(template) + cf_conn = boto.cloudformation.connect_to_region("us-east-1") + cf_conn.create_stack( + "test_stack", + template_body=template_json, + ) + + result = conn.list_functions() + result['Functions'].should.have.length_of(1) + result['Functions'][0]['Description'].should.equal('Test function') + result['Functions'][0]['Handler'].should.equal('index.handler') + result['Functions'][0]['MemorySize'].should.equal(128) + result['Functions'][0]['Role'].should.equal('test-role') + result['Functions'][0]['Runtime'].should.equal('nodejs') From ca3babc60678419daaef6d49ff39b59f4e321e77 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Fri, 12 Feb 2016 15:43:48 -0500 Subject: [PATCH 03/17] [lambda] Add comment about Publish parameter --- moto/awslambda/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index fd2ce11c1..65ead3e6b 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -18,7 +18,7 @@ class LambdaFunction(object): # optional self.description = spec.get('Description', '') self.memory_size = spec.get('MemorySize', 128) - self.publish = spec.get('Publish', False) + self.publish = spec.get('Publish', False) # this is ignored currently self.timeout = spec.get('Timeout', 3) self.vpc_config = spec.get('VpcConfig', {}) From 75c6f6f660ef1be9d964e097b7d07fe3072a5bf6 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 12:41:28 -0500 Subject: [PATCH 04/17] [lambda] Make NextMarker a real UUID --- moto/awslambda/responses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index ffbdee9dc..ec60af8d3 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import json import re +import uuid from moto.core.responses import BaseResponse from .models import lambda_backends @@ -31,7 +32,7 @@ class LambdaResponse(BaseResponse): 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", + "NextMarker": str(uuid.uuid4()), }) def _create_function(self, request, full_url, headers): From 9c4d07fb6aab5f18b63e2ee65f8044b4104c7f92 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 14:17:10 -0500 Subject: [PATCH 05/17] [lambda] Add more meaningful names to tests --- tests/test_awslambda/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py index 01fdc2ae8..99755e34d 100644 --- a/tests/test_awslambda/__init__.py +++ b/tests/test_awslambda/__init__.py @@ -18,14 +18,14 @@ def test_list_functions(): @mock_lambda @freeze_time('2015-01-01 00:00:00') -def test_create_function(): +def test_create_function_from_aws_bucket(): conn = boto3.client('lambda', 'us-west-2') result = conn.create_function( FunctionName='testFunction', Runtime='python2.7', Role='test-iam-role', - Handler='lambda_handler.handler', + Handler='lambda_function.handler', Code={ 'S3Bucket': 'test-bucket', 'S3Key': 'test.zip', @@ -74,7 +74,7 @@ def test_get_function(): FunctionName='testFunction', Runtime='python2.7', Role='test-iam-role', - Handler='lambda_handler.handler', + Handler='lambda_function.handler', Code={ 'S3Bucket': 'test-bucket', 'S3Key': 'test.zip', @@ -98,7 +98,7 @@ def test_get_function(): "Description": "test lambda function", "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", "FunctionName": "testFunction", - "Handler": "lambda_handler.handler", + "Handler": "lambda_function.handler", "LastModified": "2015-01-01 00:00:00", "MemorySize": 128, "Role": "test-iam-role", @@ -128,7 +128,7 @@ def test_delete_function(): FunctionName='testFunction', Runtime='python2.7', Role='test-iam-role', - Handler='lambda_handler.handler', + Handler='lambda_function.handler', Code={ 'S3Bucket': 'test-bucket', 'S3Key': 'test.zip', @@ -163,7 +163,7 @@ def test_list_create_list_get_delete_list(): FunctionName='testFunction', Runtime='python2.7', Role='test-iam-role', - Handler='lambda_handler.handler', + Handler='lambda_function.handler', Code={ 'S3Bucket': 'test-bucket', 'S3Key': 'test.zip', @@ -184,7 +184,7 @@ def test_list_create_list_get_delete_list(): "Description": "test lambda function", "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", "FunctionName": "testFunction", - "Handler": "lambda_handler.handler", + "Handler": "lambda_function.handler", "LastModified": "2015-01-01 00:00:00", "MemorySize": 128, "Role": "test-iam-role", From 5a29ec4986a7445d8bbe1de3024450d15428aea4 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 15:15:34 -0500 Subject: [PATCH 06/17] [lambda] Add support to upload functions with ZipFile --- moto/awslambda/models.py | 12 +++++- tests/test_awslambda/__init__.py | 73 +++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 65ead3e6b..f591ebb9a 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -1,6 +1,9 @@ from __future__ import unicode_literals +import base64 import datetime +import hashlib + import boto.awslambda from moto.core import BaseBackend @@ -25,8 +28,13 @@ class LambdaFunction(object): # auto-generated 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 + if 'ZipFile' in self.code: + code = base64.b64decode(self.code['ZipFile']) + self.code_size = len(code) + self.code_sha_256 = hashlib.sha256(code).hexdigest() + else: + self.code_size = 123 + self.code_sha_256 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' self.function_arn = 'arn:aws:lambda:123456789012:function:{}'.format(self.function_name) def __repr__(self): diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py index 99755e34d..9306e8a69 100644 --- a/tests/test_awslambda/__init__.py +++ b/tests/test_awslambda/__init__.py @@ -1,6 +1,9 @@ from __future__ import unicode_literals import boto3 +import hashlib +import io +import zipfile import sure # noqa from freezegun import freeze_time @@ -46,13 +49,69 @@ def test_create_function_from_aws_bucket(): 'FunctionArn': 'arn:aws:lambda:123456789012:function:testFunction', 'Runtime': 'python2.7', 'Role': 'test-iam-role', - 'Handler': 'lambda_handler.handler', - 'CodeSize': 210, + 'Handler': 'lambda_function.handler', + 'CodeSize': 123, 'Description': 'test lambda function', 'Timeout': 3, 'MemorySize': 128, 'LastModified': '2015-01-01 00:00:00', - 'CodeSha256': 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', + 'CodeSha256': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + '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_create_function_from_zipfile(): + conn = boto3.client('lambda', 'us-west-2') + + zip_output = io.BytesIO() + with zipfile.ZipFile(zip_output, 'w') as f: + f.writestr('lambda_function.py', b'''\ +def handler(event, context): + return "hello world" +''') + zip_output.seek(0) + zip_content = zip_output.read() + result = conn.create_function( + FunctionName='testFunction', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_function.handler', + Code={ + 'ZipFile': zip_content, + }, + 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_function.handler', + 'CodeSize': len(zip_content), + 'Description': 'test lambda function', + 'Timeout': 3, + 'MemorySize': 128, + 'LastModified': '2015-01-01 00:00:00', + 'CodeSha256': hashlib.sha256(zip_content).hexdigest(), 'Version': '$LATEST', # boto3 doesnt support it # VpcConfig={ @@ -93,8 +152,8 @@ def test_get_function(): "RepositoryType": "S3" }, "Configuration": { - "CodeSha256": 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', - "CodeSize": 210, + "CodeSha256": 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + "CodeSize": 123, "Description": "test lambda function", "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", "FunctionName": "testFunction", @@ -179,8 +238,8 @@ def test_list_create_list_get_delete_list(): "RepositoryType": "S3" }, "Configuration": { - "CodeSha256": 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', - "CodeSize": 210, + "CodeSha256": 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + "CodeSize": 123, "Description": "test lambda function", "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", "FunctionName": "testFunction", From 5f7ea2b77f0c028d1b67089c252a7efd7dbf0527 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 15:24:41 -0500 Subject: [PATCH 07/17] [lambda] Fix HTTP code of CreateFunction --- moto/awslambda/responses.py | 2 +- tests/test_awslambda/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index ec60af8d3..13457820a 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -41,7 +41,7 @@ class LambdaResponse(BaseResponse): spec = json.loads(request.body) fn = lambda_backend.create_function(spec) config = fn.get_configuration() - return 200, headers, json.dumps(config) + return 201, headers, json.dumps(config) def _delete_function(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py index 9306e8a69..e4d254ddf 100644 --- a/tests/test_awslambda/__init__.py +++ b/tests/test_awslambda/__init__.py @@ -64,7 +64,7 @@ def test_create_function_from_aws_bucket(): # "VpcId": "vpc-123abc" # }, - 'ResponseMetadata': {'HTTPStatusCode': 200}, + 'ResponseMetadata': {'HTTPStatusCode': 201}, }) @@ -120,7 +120,7 @@ def handler(event, context): # "VpcId": "vpc-123abc" # }, - 'ResponseMetadata': {'HTTPStatusCode': 200}, + 'ResponseMetadata': {'HTTPStatusCode': 201}, }) From 7c36fca1dd4672ca73ce62d8df78495a5c82a928 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 15:35:29 -0500 Subject: [PATCH 08/17] [lambda] Add VpcConfig to the responses --- moto/awslambda/models.py | 11 +++++- requirements-dev.txt | 1 + tests/test_awslambda/__init__.py | 63 +++++++++++--------------------- 3 files changed, 32 insertions(+), 43 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index f591ebb9a..0a8760964 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -23,7 +23,9 @@ class LambdaFunction(object): self.memory_size = spec.get('MemorySize', 128) self.publish = spec.get('Publish', False) # this is ignored currently self.timeout = spec.get('Timeout', 3) - self.vpc_config = spec.get('VpcConfig', {}) + + # this isn't finished yet. it needs to find out the VpcId value + self._vpc_config = spec.get('VpcConfig', {'SubnetIds': [], 'SecurityGroupIds': []}) # auto-generated self.version = '$LATEST' @@ -37,6 +39,13 @@ class LambdaFunction(object): self.code_sha_256 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' self.function_arn = 'arn:aws:lambda:123456789012:function:{}'.format(self.function_name) + @property + def vpc_config(self): + config = self._vpc_config.copy() + if config['SecurityGroupIds']: + config.update({"VpcId": "vpc-123abc"}) + return config + def __repr__(self): return json.dumps(self.get_configuration()) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0e2d603d6..d2e70ba89 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,4 +6,5 @@ coverage freezegun flask boto3>=1.2.3 +botocore>=1.3.26 six \ No newline at end of file diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py index e4d254ddf..445530ab9 100644 --- a/tests/test_awslambda/__init__.py +++ b/tests/test_awslambda/__init__.py @@ -37,12 +37,10 @@ def test_create_function_from_aws_bucket(): Timeout=3, MemorySize=128, Publish=True, - # boto3 doesnt support it - # VpcConfig={ - # "SecurityGroupIds": ["sg-123abc"], - # "SubnetIds": ["subnet-123abc"], - # "VpcId": "vpc-123abc" - # }, + VpcConfig={ + "SecurityGroupIds": ["sg-123abc"], + "SubnetIds": ["subnet-123abc"], + }, ) result.should.equal({ 'FunctionName': 'testFunction', @@ -57,12 +55,11 @@ def test_create_function_from_aws_bucket(): 'LastModified': '2015-01-01 00:00:00', 'CodeSha256': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'Version': '$LATEST', - # boto3 doesnt support it - # VpcConfig={ - # "SecurityGroupIds": ["sg-123abc"], - # "SubnetIds": ["subnet-123abc"], - # "VpcId": "vpc-123abc" - # }, + 'VpcConfig': { + "SecurityGroupIds": ["sg-123abc"], + "SubnetIds": ["subnet-123abc"], + "VpcId": "vpc-123abc" + }, 'ResponseMetadata': {'HTTPStatusCode': 201}, }) @@ -93,12 +90,6 @@ def handler(event, context): 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', @@ -113,12 +104,10 @@ def handler(event, context): 'LastModified': '2015-01-01 00:00:00', 'CodeSha256': hashlib.sha256(zip_content).hexdigest(), 'Version': '$LATEST', - # boto3 doesnt support it - # VpcConfig={ - # "SecurityGroupIds": ["sg-123abc"], - # "SubnetIds": ["subnet-123abc"], - # "VpcId": "vpc-123abc" - # }, + 'VpcConfig': { + "SecurityGroupIds": [], + "SubnetIds": [], + }, 'ResponseMetadata': {'HTTPStatusCode': 201}, }) @@ -164,15 +153,10 @@ def test_get_function(): "Runtime": "python2.7", "Timeout": 3, "Version": '$LATEST', - # "VpcConfig": { - # "SecurityGroupIds": [ - # "string" - # ], - # "SubnetIds": [ - # "string" - # ], - # "VpcId": "string" - # } + "VpcConfig": { + "SecurityGroupIds": [], + "SubnetIds": [], + } }, 'ResponseMetadata': {'HTTPStatusCode': 200}, }) @@ -250,15 +234,10 @@ def test_list_create_list_get_delete_list(): "Runtime": "python2.7", "Timeout": 3, "Version": '$LATEST', - # "VpcConfig": { - # "SecurityGroupIds": [ - # "string" - # ], - # "SubnetIds": [ - # "string" - # ], - # "VpcId": "string" - # } + "VpcConfig": { + "SecurityGroupIds": [], + "SubnetIds": [], + } }, 'ResponseMetadata': {'HTTPStatusCode': 200}, } From 4c10e54a36d5885cac7905970c140a46ce32d81b Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 15:38:59 -0500 Subject: [PATCH 09/17] [lambda] Add 404 test for DeleteFunction --- tests/test_awslambda/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py index 445530ab9..a6f53aa3b 100644 --- a/tests/test_awslambda/__init__.py +++ b/tests/test_awslambda/__init__.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import botocore.client import boto3 import hashlib import io @@ -183,12 +184,9 @@ def test_delete_function(): ) 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}}) + conn.delete_function.when.called_with(FunctionName='testFunctionThatDoesntExist').should.throw(botocore.client.ClientError) @mock_lambda From 482751f731516bcd60892d6104b554d8646203ac Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 16:43:33 -0500 Subject: [PATCH 10/17] [lambda] Add S3 validation for Code["S3Bucket"] and Code["S3Key"]. TODO: validate region. --- moto/awslambda/models.py | 21 ++++++++++- moto/awslambda/responses.py | 10 +++-- tests/test_awslambda/__init__.py | 63 +++++++++++++++++++++++++++----- 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 0a8760964..bf928deb2 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -6,6 +6,8 @@ import hashlib import boto.awslambda from moto.core import BaseBackend +from moto.s3.models import s3_backend +from moto.s3.exceptions import MissingBucket class LambdaFunction(object): @@ -35,8 +37,23 @@ class LambdaFunction(object): self.code_size = len(code) self.code_sha_256 = hashlib.sha256(code).hexdigest() else: - self.code_size = 123 - self.code_sha_256 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + # validate s3 bucket + try: + # FIXME: does not validate bucket region + key = s3_backend.get_key(self.code['S3Bucket'], self.code['S3Key']) + except MissingBucket: + raise ValueError( + "InvalidParameterValueException", + "Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist") + else: + # validate s3 key + if key is None: + raise ValueError( + "InvalidParameterValueException", + "Error occurred while GetObject. S3 Error Code: NoSuchKey. S3 Error Message: The specified key does not exist.") + else: + self.code_size = 123 + self.code_sha_256 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' self.function_arn = 'arn:aws:lambda:123456789012:function:{}'.format(self.function_name) @property diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 13457820a..73f969890 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -39,9 +39,13 @@ class LambdaResponse(BaseResponse): lambda_backend = self.get_lambda_backend(full_url) spec = json.loads(request.body) - fn = lambda_backend.create_function(spec) - config = fn.get_configuration() - return 201, headers, json.dumps(config) + try: + fn = lambda_backend.create_function(spec) + except ValueError as e: + return 400, headers, json.dumps({"Error": {"Code": e.args[0], "Message": e.args[1]}}) + else: + config = fn.get_configuration() + return 201, headers, json.dumps(config) def _delete_function(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py index a6f53aa3b..cdd61db35 100644 --- a/tests/test_awslambda/__init__.py +++ b/tests/test_awslambda/__init__.py @@ -8,7 +8,18 @@ import zipfile import sure # noqa from freezegun import freeze_time -from moto import mock_lambda +from moto import mock_lambda, mock_s3 + + +def get_test_zip_file(): + zip_output = io.BytesIO() + with zipfile.ZipFile(zip_output, 'w') as f: + f.writestr('lambda_function.py', b'''\ +def handler(event, context): + return "hello world" +''') + zip_output.seek(0) + return zip_output.read() @mock_lambda @@ -22,7 +33,36 @@ def test_list_functions(): @mock_lambda @freeze_time('2015-01-01 00:00:00') +def test_create_based_on_s3_with_missing_bucket(): + conn = boto3.client('lambda', 'us-west-2') + + conn.create_function.when.called_with( + FunctionName='testFunction', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_function.handler', + Code={ + 'S3Bucket': 'this-bucket-does-not-exist', + 'S3Key': 'test.zip', + }, + Description='test lambda function', + Timeout=3, + MemorySize=128, + Publish=True, + VpcConfig={ + "SecurityGroupIds": ["sg-123abc"], + "SubnetIds": ["subnet-123abc"], + }, + ).should.throw(botocore.client.ClientError) + + +@mock_lambda +@mock_s3 +@freeze_time('2015-01-01 00:00:00') def test_create_function_from_aws_bucket(): + s3_conn = boto3.client('s3', 'us-west-2') + s3_conn.create_bucket(Bucket='test-bucket') + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=get_test_zip_file()) conn = boto3.client('lambda', 'us-west-2') result = conn.create_function( @@ -71,14 +111,7 @@ def test_create_function_from_aws_bucket(): def test_create_function_from_zipfile(): conn = boto3.client('lambda', 'us-west-2') - zip_output = io.BytesIO() - with zipfile.ZipFile(zip_output, 'w') as f: - f.writestr('lambda_function.py', b'''\ -def handler(event, context): - return "hello world" -''') - zip_output.seek(0) - zip_content = zip_output.read() + zip_content = get_test_zip_file() result = conn.create_function( FunctionName='testFunction', Runtime='python2.7', @@ -115,8 +148,12 @@ def handler(event, context): @mock_lambda +@mock_s3 @freeze_time('2015-01-01 00:00:00') def test_get_function(): + s3_conn = boto3.client('s3', 'us-west-2') + s3_conn.create_bucket(Bucket='test-bucket') + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=get_test_zip_file()) conn = boto3.client('lambda', 'us-west-2') conn.create_function( @@ -165,7 +202,11 @@ def test_get_function(): @mock_lambda +@mock_s3 def test_delete_function(): + s3_conn = boto3.client('s3', 'us-west-2') + s3_conn.create_bucket(Bucket='test-bucket') + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=get_test_zip_file()) conn = boto3.client('lambda', 'us-west-2') conn.create_function( @@ -190,12 +231,16 @@ def test_delete_function(): @mock_lambda +@mock_s3 @freeze_time('2015-01-01 00:00:00') def test_list_create_list_get_delete_list(): """ test `list -> create -> list -> get -> delete -> list` integration """ + s3_conn = boto3.client('s3', 'us-west-2') + s3_conn.create_bucket(Bucket='test-bucket') + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=get_test_zip_file()) conn = boto3.client('lambda', 'us-west-2') conn.list_functions()['Functions'].should.have.length_of(0) From ac66a8eb8757fb162248710c18cebca831aa6bed Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 16:44:44 -0500 Subject: [PATCH 11/17] [lambda] Change string format to use {0} instead of {} --- moto/awslambda/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index bf928deb2..0a9d00fb3 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -54,7 +54,7 @@ class LambdaFunction(object): else: self.code_size = 123 self.code_sha_256 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - self.function_arn = 'arn:aws:lambda:123456789012:function:{}'.format(self.function_name) + self.function_arn = 'arn:aws:lambda:123456789012:function:{0}'.format(self.function_name) @property def vpc_config(self): @@ -86,7 +86,7 @@ class LambdaFunction(object): def get_code(self): return { "Code": { - "Location": "s3://lambda-functions.aws.amazon.com/{}".format(self.code['S3Key']), + "Location": "s3://lambda-functions.aws.amazon.com/{0}".format(self.code['S3Key']), "RepositoryType": "S3" }, "Configuration": self.get_configuration(), From 45d91bf963f6358909ab1e1cc29cbbab73fe7133 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 16:56:20 -0500 Subject: [PATCH 12/17] [lambda] Do not use ZipFile as a context manager (it is not supported in Python 2.6) --- tests/test_awslambda/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py index cdd61db35..9fe544867 100644 --- a/tests/test_awslambda/__init__.py +++ b/tests/test_awslambda/__init__.py @@ -13,11 +13,12 @@ from moto import mock_lambda, mock_s3 def get_test_zip_file(): zip_output = io.BytesIO() - with zipfile.ZipFile(zip_output, 'w') as f: - f.writestr('lambda_function.py', b'''\ + zip_file = zipfile.ZipFile(zip_output, 'w') + zip_file.writestr('lambda_function.py', b'''\ def handler(event, context): return "hello world" ''') + zip_file.close() zip_output.seek(0) return zip_output.read() From 2d744c6626a2e31616b87dc37595a547dd4d172b Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 17:07:35 -0500 Subject: [PATCH 13/17] [lambda] Do not duplicate defaults for LambdaFunction objects. The duplication caused a bug before and this commit is a bugfix and will prevent similar bugs in the future. --- moto/awslambda/models.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 0a9d00fb3..2fb396178 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -96,22 +96,22 @@ class LambdaFunction(object): def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] - backend = lambda_backends[region_name] - fn = backend.create_function({ - # required + # required + spec = { 'Code': properties['Code'], 'FunctionName': resource_name, 'Handler': properties['Handler'], 'Role': properties['Role'], 'Runtime': properties['Runtime'], + } + optional_properties = 'Description MemorySize Publish Timeout VpcConfig'.split() + # NOTE: Not doing `properties.get(k, DEFAULT)` to avoid duplicating the default logic + for prop in optional_properties: + if prop in properties: + spec[prop] = properties[prop] - # optional - 'Description': properties.get('Description', ''), - 'MemorySize': properties.get('MemorySize', 128), - 'Publish': properties.get('Publish', False), - 'Timeout': properties.get('Timeout', 3), - 'VpcConfig': properties.get('VpcConfig', {}), - }) + backend = lambda_backends[region_name] + fn = backend.create_function(spec) return fn From 39a57195a279e0566a8ef68da53464578299c5d4 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Tue, 16 Feb 2016 17:38:39 -0500 Subject: [PATCH 14/17] [lambda] Make JSON load python 3-compatible --- moto/awslambda/responses.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 73f969890..6f4f76c0f 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -37,8 +37,7 @@ class LambdaResponse(BaseResponse): def _create_function(self, request, full_url, headers): lambda_backend = self.get_lambda_backend(full_url) - - spec = json.loads(request.body) + spec = json.loads(request.body.decode('utf-8')) try: fn = lambda_backend.create_function(spec) except ValueError as e: From b8d8844286afd45dd0eb2dd42b8534caa7ada414 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Wed, 17 Feb 2016 16:16:23 -0500 Subject: [PATCH 15/17] [lambda] Renamed test file to follow the convention --- tests/test_awslambda/test_awslambda.py | 0 tests/test_awslambda/{__init__.py => test_lambda.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/test_awslambda/test_awslambda.py rename tests/test_awslambda/{__init__.py => test_lambda.py} (100%) diff --git a/tests/test_awslambda/test_awslambda.py b/tests/test_awslambda/test_awslambda.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/test_lambda.py similarity index 100% rename from tests/test_awslambda/__init__.py rename to tests/test_awslambda/test_lambda.py From 8f586d86370fd3af105291b06fb4e53724ecf1e8 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Wed, 17 Feb 2016 16:24:17 -0500 Subject: [PATCH 16/17] [lambda] Use S3Key to figure out code size and SHA256 --- moto/awslambda/models.py | 4 ++-- tests/test_awslambda/test_lambda.py | 28 ++++++++++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 2fb396178..5879b9d90 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -52,8 +52,8 @@ class LambdaFunction(object): "InvalidParameterValueException", "Error occurred while GetObject. S3 Error Code: NoSuchKey. S3 Error Message: The specified key does not exist.") else: - self.code_size = 123 - self.code_sha_256 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + self.code_size = key.size + self.code_sha_256 = hashlib.sha256(key.value).hexdigest() self.function_arn = 'arn:aws:lambda:123456789012:function:{0}'.format(self.function_name) @property diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 9fe544867..ea4eea310 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -63,7 +63,9 @@ def test_create_based_on_s3_with_missing_bucket(): def test_create_function_from_aws_bucket(): s3_conn = boto3.client('s3', 'us-west-2') s3_conn.create_bucket(Bucket='test-bucket') - s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=get_test_zip_file()) + + zip_content = get_test_zip_file() + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=zip_content) conn = boto3.client('lambda', 'us-west-2') result = conn.create_function( @@ -90,12 +92,12 @@ def test_create_function_from_aws_bucket(): 'Runtime': 'python2.7', 'Role': 'test-iam-role', 'Handler': 'lambda_function.handler', - 'CodeSize': 123, + "CodeSha256": hashlib.sha256(zip_content).hexdigest(), + "CodeSize": len(zip_content), 'Description': 'test lambda function', 'Timeout': 3, 'MemorySize': 128, 'LastModified': '2015-01-01 00:00:00', - 'CodeSha256': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'Version': '$LATEST', 'VpcConfig': { "SecurityGroupIds": ["sg-123abc"], @@ -154,7 +156,9 @@ def test_create_function_from_zipfile(): def test_get_function(): s3_conn = boto3.client('s3', 'us-west-2') s3_conn.create_bucket(Bucket='test-bucket') - s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=get_test_zip_file()) + + zip_content = get_test_zip_file() + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=zip_content) conn = boto3.client('lambda', 'us-west-2') conn.create_function( @@ -180,8 +184,8 @@ def test_get_function(): "RepositoryType": "S3" }, "Configuration": { - "CodeSha256": 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - "CodeSize": 123, + "CodeSha256": hashlib.sha256(zip_content).hexdigest(), + "CodeSize": len(zip_content), "Description": "test lambda function", "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", "FunctionName": "testFunction", @@ -207,7 +211,9 @@ def test_get_function(): def test_delete_function(): s3_conn = boto3.client('s3', 'us-west-2') s3_conn.create_bucket(Bucket='test-bucket') - s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=get_test_zip_file()) + + zip_content = get_test_zip_file() + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=zip_content) conn = boto3.client('lambda', 'us-west-2') conn.create_function( @@ -241,7 +247,9 @@ def test_list_create_list_get_delete_list(): """ s3_conn = boto3.client('s3', 'us-west-2') s3_conn.create_bucket(Bucket='test-bucket') - s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=get_test_zip_file()) + + zip_content = get_test_zip_file() + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=zip_content) conn = boto3.client('lambda', 'us-west-2') conn.list_functions()['Functions'].should.have.length_of(0) @@ -266,8 +274,8 @@ def test_list_create_list_get_delete_list(): "RepositoryType": "S3" }, "Configuration": { - "CodeSha256": 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - "CodeSize": 123, + "CodeSha256": hashlib.sha256(zip_content).hexdigest(), + "CodeSize": len(zip_content), "Description": "test lambda function", "FunctionArn": "arn:aws:lambda:123456789012:function:testFunction", "FunctionName": "testFunction", From 7d0dae1fb7f1b769a0fc5f6fc739f9e989ab13e7 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Wed, 17 Feb 2016 16:33:32 -0500 Subject: [PATCH 17/17] [lambda] Add @mock_lambda to the README file --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6761e3e0b..764fd5694 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ It gets even better! Moto isn't just S3. Here's the status of the other AWS serv |------------------------------------------------------------------------------| | IAM | @mock_iam | core endpoints done | |------------------------------------------------------------------------------| +| Lambda | @mock_lambda | basic endpoints done | +|------------------------------------------------------------------------------| | Kinesis | @mock_kinesis | core endpoints done | |------------------------------------------------------------------------------| | RDS | @mock_rds | core endpoints done |