From 41af98c98b6c372c1c158f42611dccdf4dcf6c7e Mon Sep 17 00:00:00 2001 From: Seth Black Date: Tue, 8 Oct 2019 15:59:03 -0500 Subject: [PATCH 1/3] added UpdateFunctionCode and UpdateFunctionConfiguration and associated test cases --- moto/awslambda/models.py | 65 +++++++++++++++++ moto/awslambda/responses.py | 36 ++++++++++ moto/awslambda/urls.py | 4 +- tests/test_awslambda/test_lambda.py | 106 ++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 1 deletion(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index acc7a5257..f2400ec38 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -273,6 +273,71 @@ class LambdaFunction(BaseModel): "Configuration": self.get_configuration(), } + def update_configuration(self, config_updates): + for key, value in config_updates.items(): + if key == "Description": + self.description = value + elif key == "Handler": + self.handler = value + elif key == "MemorySize": + self.memory_size = value + elif key == "Role": + self.role = value + elif key == "Runtime": + self.run_time = value + elif key == "Timeout": + self.timeout = value + elif key == "VpcConfig": + self.vpc_config = value + + return self.get_configuration() + + def update_function_code(self, spec): + if 'DryRun' in spec and spec['DryRun']: + return self.get_configuration() + + if 'Publish' in spec and spec['Publish']: + self.set_version(self.version + 1) + + if 'ZipFile' in spec: + # using the "hackery" from __init__" because it seems to work + # TODOs and FIXMEs included, because they'll need to be fixed + # in both places now + try: + to_unzip_code = base64.b64decode( + bytes(spec['ZipFile'], 'utf-8')) + except Exception: + to_unzip_code = base64.b64decode(spec['ZipFile']) + + self.code_bytes = to_unzip_code + self.code_size = len(to_unzip_code) + self.code_sha_256 = hashlib.sha256(to_unzip_code).hexdigest() + + # TODO: we should be putting this in a lambda bucket + self.code['UUID'] = str(uuid.uuid4()) + self.code['S3Key'] = '{}-{}'.format(self.function_name, self.code['UUID']) + else: + key = None + try: + # FIXME: does not validate bucket region + key = s3_backend.get_key(spec['S3Bucket'], spec['S3Key']) + except MissingBucket: + if do_validate_s3(): + raise ValueError( + "InvalidParameterValueException", + "Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist") + except MissingKey: + if do_validate_s3(): + raise ValueError( + "InvalidParameterValueException", + "Error occurred while GetObject. S3 Error Code: NoSuchKey. S3 Error Message: The specified key does not exist.") + if key: + self.code_bytes = key.value + self.code_size = key.size + self.code_sha_256 = hashlib.sha256(key.value).hexdigest() + + return self.get_configuration() + @staticmethod def convert(s): try: diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 1e7feb0d0..e22fee152 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -122,6 +122,18 @@ class LambdaResponse(BaseResponse): if request.method == 'POST': return self._add_policy(request, full_url, headers) + def configuration(self, request, full_url, headers): + if request.method == 'PUT': + return self._put_configuration(request, full_url) + else: + raise ValueError("Cannot handle request") + + def code(self, request, full_url, headers): + if request.method == 'PUT': + return self._put_code(request, full_url, headers) + else: + raise ValueError("Cannot handle request") + def _add_policy(self, request, full_url, headers): path = request.path if hasattr(request, 'path') else path_url(request.url) function_name = path.split('/')[-2] @@ -308,3 +320,27 @@ class LambdaResponse(BaseResponse): return 204, {}, "{}" else: return 404, {}, "{}" + + def _put_configuration(self, request, full_url): + function_name = self._get_param('FunctionName', None) + qualifier = self._get_param('Qualifier', None) + + fn = self.lambda_backend.get_function(function_name, qualifier) + + if fn: + config = fn.update_configuration(json.loads(request.body)) + return 200, {}, json.dumps(config) + else: + return 404, {}, "{}" + + def _put_code(self, request, full_url, headers): + function_name = self._get_param('FunctionName', None) + qualifier = self._get_param('Qualifier', None) + + fn = self.lambda_backend.get_function(function_name, qualifier) + + if fn: + config = fn.update_function_code(json.loads(request.body)) + return 200, {}, json.dumps(config) + else: + return 404, {}, "{}" diff --git a/moto/awslambda/urls.py b/moto/awslambda/urls.py index fb2c6ee7e..a306feff5 100644 --- a/moto/awslambda/urls.py +++ b/moto/awslambda/urls.py @@ -16,5 +16,7 @@ url_paths = { r'{0}/(?P[^/]+)/functions/(?P[\w_-]+)/invocations/?$': response.invoke, r'{0}/(?P[^/]+)/functions/(?P[\w_-]+)/invoke-async/?$': response.invoke_async, r'{0}/(?P[^/]+)/tags/(?P.+)': response.tag, - r'{0}/(?P[^/]+)/functions/(?P[\w_-]+)/policy/?$': response.policy + r'{0}/(?P[^/]+)/functions/(?P[\w_-]+)/policy/?$': response.policy, + r'{0}/(?P[^/]+)/functions/(?P[\w_-]+)/configuration/?$': response.configuration, + r'{0}/(?P[^/]+)/functions/(?P[\w_-]+)/code/?$': response.code } diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 9467b0803..0a6df7cca 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -1245,3 +1245,109 @@ def test_delete_event_source_mapping(): assert response['State'] == 'Deleting' conn.get_event_source_mapping.when.called_with(UUID=response['UUID'])\ .should.throw(botocore.client.ClientError) + + +@mock_lambda +@mock_s3 +def test_update_configuration(): + s3_conn = boto3.client('s3', 'us-west-2') + s3_conn.create_bucket(Bucket='test-bucket') + + zip_content = get_test_zip_file2() + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=zip_content) + conn = boto3.client('lambda', 'us-west-2') + + fxn = conn.create_function( + FunctionName='testFunction', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_function.lambda_handler', + Code={ + 'S3Bucket': 'test-bucket', + 'S3Key': 'test.zip', + }, + Description='test lambda function', + Timeout=3, + MemorySize=128, + Publish=True, + ) + + assert fxn['Description'] == 'test lambda function' + assert fxn['Handler'] == 'lambda_function.lambda_handler' + assert fxn['MemorySize'] == 128 + assert fxn['Runtime'] == 'python2.7' + assert fxn['Timeout'] == 3 + + updated_config = conn.update_function_configuration( + FunctionName='testFunction', + Description='updated test lambda function', + Handler='lambda_function.new_lambda_handler', + Runtime='python3.6', + Timeout=7 + ) + + assert updated_config['ResponseMetadata']['HTTPStatusCode'] == 200 + assert updated_config['Description'] == 'updated test lambda function' + assert updated_config['Handler'] == 'lambda_function.new_lambda_handler' + assert updated_config['MemorySize'] == 128 + assert updated_config['Runtime'] == 'python3.6' + assert updated_config['Timeout'] == 7 + + +@mock_lambda +@freeze_time('2015-01-01 00:00:00') +def test_update_function(): + conn = boto3.client('lambda', 'us-west-2') + + zip_content_one = get_test_zip_file1() + + fxn = conn.create_function( + FunctionName='testFunction', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_function.lambda_handler', + Code={ + 'ZipFile': zip_content_one, + }, + Description='test lambda function', + Timeout=3, + MemorySize=128, + Publish=True, + ) + + zip_content_two = get_test_zip_file2() + + conn.update_function_code( + FunctionName='testFunction', + ZipFile=zip_content_two, + Publish=True + ) + + response = conn.get_function( + FunctionName='testFunction' + ) + response['Configuration'].pop('LastModified') + + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + assert len(response['Code']) == 2 + assert response['Code']['RepositoryType'] == 'S3' + assert response['Code']['Location'].startswith('s3://awslambda-{0}-tasks.s3-{0}.amazonaws.com'.format(_lambda_region)) + response['Configuration'].should.equal( + { + "CodeSha256": hashlib.sha256(zip_content_two).hexdigest(), + "CodeSize": len(zip_content_two), + "Description": "test lambda function", + "FunctionArn": 'arn:aws:lambda:{}:123456789012:function:testFunction:2'.format(_lambda_region), + "FunctionName": "testFunction", + "Handler": "lambda_function.lambda_handler", + "MemorySize": 128, + "Role": "test-iam-role", + "Runtime": "python2.7", + "Timeout": 3, + "Version": '$LATEST', + "VpcConfig": { + "SecurityGroupIds": [], + "SubnetIds": [], + } + }, + ) From 20dc8ae5c4b44439d302809bd8c4a74179bb35d1 Mon Sep 17 00:00:00 2001 From: Seth Black Date: Wed, 9 Oct 2019 15:15:10 -0500 Subject: [PATCH 2/3] getting tests working in server mode --- moto/awslambda/models.py | 20 +++++++++++--------- moto/awslambda/responses.py | 18 ++++++++++-------- tests/test_awslambda/test_lambda.py | 3 +-- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index f2400ec38..a1643cd03 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -292,22 +292,24 @@ class LambdaFunction(BaseModel): return self.get_configuration() - def update_function_code(self, spec): - if 'DryRun' in spec and spec['DryRun']: + def update_function_code(self, updated_spec): + if 'DryRun' in updated_spec and updated_spec['DryRun']: return self.get_configuration() - if 'Publish' in spec and spec['Publish']: + if 'Publish' in updated_spec and updated_spec['Publish']: self.set_version(self.version + 1) - if 'ZipFile' in spec: - # using the "hackery" from __init__" because it seems to work + if 'ZipFile' in updated_spec: + self.code['ZipFile'] = updated_spec['ZipFile'] + + # using the "hackery" from __init__ because it seems to work # TODOs and FIXMEs included, because they'll need to be fixed # in both places now try: to_unzip_code = base64.b64decode( - bytes(spec['ZipFile'], 'utf-8')) + bytes(updated_spec['ZipFile'], 'utf-8')) except Exception: - to_unzip_code = base64.b64decode(spec['ZipFile']) + to_unzip_code = base64.b64decode(updated_spec['ZipFile']) self.code_bytes = to_unzip_code self.code_size = len(to_unzip_code) @@ -316,11 +318,11 @@ class LambdaFunction(BaseModel): # TODO: we should be putting this in a lambda bucket self.code['UUID'] = str(uuid.uuid4()) self.code['S3Key'] = '{}-{}'.format(self.function_name, self.code['UUID']) - else: + elif 'S3Bucket' in updated_spec and 'S3Key' in updated_spec: key = None try: # FIXME: does not validate bucket region - key = s3_backend.get_key(spec['S3Bucket'], spec['S3Key']) + key = s3_backend.get_key(updated_spec['S3Bucket'], updated_spec['S3Key']) except MissingBucket: if do_validate_s3(): raise ValueError( diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index e22fee152..2041aa660 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -123,14 +123,16 @@ class LambdaResponse(BaseResponse): return self._add_policy(request, full_url, headers) def configuration(self, request, full_url, headers): + self.setup_class(request, full_url, headers) if request.method == 'PUT': - return self._put_configuration(request, full_url) + return self._put_configuration(request) else: raise ValueError("Cannot handle request") def code(self, request, full_url, headers): + self.setup_class(request, full_url, headers) if request.method == 'PUT': - return self._put_code(request, full_url, headers) + return self._put_code() else: raise ValueError("Cannot handle request") @@ -321,26 +323,26 @@ class LambdaResponse(BaseResponse): else: return 404, {}, "{}" - def _put_configuration(self, request, full_url): - function_name = self._get_param('FunctionName', None) + def _put_configuration(self, request): + function_name = self.path.rsplit('/', 2)[-2] qualifier = self._get_param('Qualifier', None) fn = self.lambda_backend.get_function(function_name, qualifier) if fn: - config = fn.update_configuration(json.loads(request.body)) + config = fn.update_configuration(self.json_body) return 200, {}, json.dumps(config) else: return 404, {}, "{}" - def _put_code(self, request, full_url, headers): - function_name = self._get_param('FunctionName', None) + def _put_code(self): + function_name = self.path.rsplit('/', 2)[-2] qualifier = self._get_param('Qualifier', None) fn = self.lambda_backend.get_function(function_name, qualifier) if fn: - config = fn.update_function_code(json.loads(request.body)) + config = fn.update_function_code(self.json_body) return 200, {}, json.dumps(config) else: return 404, {}, "{}" diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 0a6df7cca..20a806de2 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -1295,7 +1295,6 @@ def test_update_configuration(): @mock_lambda -@freeze_time('2015-01-01 00:00:00') def test_update_function(): conn = boto3.client('lambda', 'us-west-2') @@ -1317,7 +1316,7 @@ def test_update_function(): zip_content_two = get_test_zip_file2() - conn.update_function_code( + fxn_updated = conn.update_function_code( FunctionName='testFunction', ZipFile=zip_content_two, Publish=True From dff24cb032fc11d80b50034423613b5739b9b492 Mon Sep 17 00:00:00 2001 From: Seth Black Date: Wed, 9 Oct 2019 16:20:49 -0500 Subject: [PATCH 3/3] bringing up test percentage --- moto/awslambda/models.py | 3 -- moto/awslambda/responses.py | 3 ++ tests/test_awslambda/test_lambda.py | 81 ++++++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index a1643cd03..bd85ded9a 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -296,9 +296,6 @@ class LambdaFunction(BaseModel): if 'DryRun' in updated_spec and updated_spec['DryRun']: return self.get_configuration() - if 'Publish' in updated_spec and updated_spec['Publish']: - self.set_version(self.version + 1) - if 'ZipFile' in updated_spec: self.code['ZipFile'] = updated_spec['ZipFile'] diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 2041aa660..83a1cefca 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -342,6 +342,9 @@ class LambdaResponse(BaseResponse): fn = self.lambda_backend.get_function(function_name, qualifier) if fn: + if self.json_body.get('Publish', False): + fn = self.lambda_backend.publish_function(function_name) + config = fn.update_function_code(self.json_body) return 200, {}, json.dumps(config) else: diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 20a806de2..d6ee3f7f9 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -1295,13 +1295,13 @@ def test_update_configuration(): @mock_lambda -def test_update_function(): +def test_update_function_zip(): conn = boto3.client('lambda', 'us-west-2') zip_content_one = get_test_zip_file1() fxn = conn.create_function( - FunctionName='testFunction', + FunctionName='testFunctionZip', Runtime='python2.7', Role='test-iam-role', Handler='lambda_function.lambda_handler', @@ -1317,13 +1317,14 @@ def test_update_function(): zip_content_two = get_test_zip_file2() fxn_updated = conn.update_function_code( - FunctionName='testFunction', + FunctionName='testFunctionZip', ZipFile=zip_content_two, Publish=True ) response = conn.get_function( - FunctionName='testFunction' + FunctionName='testFunctionZip', + Qualifier='2' ) response['Configuration'].pop('LastModified') @@ -1336,14 +1337,80 @@ def test_update_function(): "CodeSha256": hashlib.sha256(zip_content_two).hexdigest(), "CodeSize": len(zip_content_two), "Description": "test lambda function", - "FunctionArn": 'arn:aws:lambda:{}:123456789012:function:testFunction:2'.format(_lambda_region), - "FunctionName": "testFunction", + "FunctionArn": 'arn:aws:lambda:{}:123456789012:function:testFunctionZip:2'.format(_lambda_region), + "FunctionName": "testFunctionZip", "Handler": "lambda_function.lambda_handler", "MemorySize": 128, "Role": "test-iam-role", "Runtime": "python2.7", "Timeout": 3, - "Version": '$LATEST', + "Version": '2', + "VpcConfig": { + "SecurityGroupIds": [], + "SubnetIds": [], + } + }, + ) + +@mock_lambda +@mock_s3 +def test_update_function_s3(): + s3_conn = boto3.client('s3', 'us-west-2') + s3_conn.create_bucket(Bucket='test-bucket') + + zip_content = get_test_zip_file1() + s3_conn.put_object(Bucket='test-bucket', Key='test.zip', Body=zip_content) + + conn = boto3.client('lambda', 'us-west-2') + + fxn = conn.create_function( + FunctionName='testFunctionS3', + Runtime='python2.7', + Role='test-iam-role', + Handler='lambda_function.lambda_handler', + Code={ + 'S3Bucket': 'test-bucket', + 'S3Key': 'test.zip', + }, + Description='test lambda function', + Timeout=3, + MemorySize=128, + Publish=True, + ) + + zip_content_two = get_test_zip_file2() + s3_conn.put_object(Bucket='test-bucket', Key='test2.zip', Body=zip_content_two) + + fxn_updated = conn.update_function_code( + FunctionName='testFunctionS3', + S3Bucket='test-bucket', + S3Key='test2.zip', + Publish=True + ) + + response = conn.get_function( + FunctionName='testFunctionS3', + Qualifier='2' + ) + response['Configuration'].pop('LastModified') + + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + assert len(response['Code']) == 2 + assert response['Code']['RepositoryType'] == 'S3' + assert response['Code']['Location'].startswith('s3://awslambda-{0}-tasks.s3-{0}.amazonaws.com'.format(_lambda_region)) + response['Configuration'].should.equal( + { + "CodeSha256": hashlib.sha256(zip_content_two).hexdigest(), + "CodeSize": len(zip_content_two), + "Description": "test lambda function", + "FunctionArn": 'arn:aws:lambda:{}:123456789012:function:testFunctionS3:2'.format(_lambda_region), + "FunctionName": "testFunctionS3", + "Handler": "lambda_function.lambda_handler", + "MemorySize": 128, + "Role": "test-iam-role", + "Runtime": "python2.7", + "Timeout": 3, + "Version": '2', "VpcConfig": { "SecurityGroupIds": [], "SubnetIds": [],