Repository: getmoto/moto

Issue:
AWS Lambdas not updating from cloudformation properly
## Reporting Bugs

# Issue
I'm deploying lambdas with serverless, and localstack, and the issue we are running into is that the old lambdas are not being deleted which I believe is leading to the newly created lambdas not being able to be created.

Example: I have a serverless application called my-app, I deploy to localstack and the first time it works perfectly. I make changes, and deploy again, and the function is still only the function from the first deploy.

There is some traceback, but from what I've read it may not be an issue. 

# Traceback
```
2019-07-16T11:51:46:ERROR:werkzeug: Error on request:
Traceback (most recent call last):
File "/usr/local/lib/python2.7/site-packages/werkzeug/serving.py", line 303, in run_wsgi
execute(self.server.app)
File "/usr/local/lib/python2.7/site-packages/werkzeug/serving.py", line 291, in execute
application_iter = app(environ, start_response)
File "/usr/local/lib/python2.7/site-packages/moto/server.py", line 104, in __call__
return backend_app(environ, start_response)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2309, in __call__
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2295, in wsgi_app
response = self.handle_exception(e)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1741, in handle_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 2292, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1815, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1718, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1813, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1799, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/usr/local/lib/python2.7/site-packages/moto/core/utils.py", line 140, in __call__
result = self.callback(request, request.url, {})
File "/usr/local/lib/python2.7/site-packages/moto/core/responses.py", line 117, in dispatch
return cls()._dispatch(*args, **kwargs)
File "/usr/local/lib/python2.7/site-packages/moto/core/responses.py", line 200, in _dispatch
return self.call_action()
File "/usr/local/lib/python2.7/site-packages/moto/core/responses.py", line 274, in call_action
response = method()
File "/usr/local/lib/python2.7/site-packages/moto/cloudformation/responses.py", line 294, in update_stack
tags=tags,
File "/usr/local/lib/python2.7/site-packages/moto/cloudformation/models.py", line 330, in update_stack
stack.update(template, role_arn, parameters=parameters, tags=tags)
File "/usr/local/lib/python2.7/site-packages/moto/cloudformation/models.py", line 112, in update
self.resource_map.update(self.template_dict, parameters)
File "/usr/local/lib/python2.7/site-packages/moto/cloudformation/parsing.py", line 526, in update
resource_name, resource_json, self, self._region_name)
File "/usr/local/lib/python2.7/site-packages/moto/cloudformation/parsing.py", line 323, in parse_and_delete_resource
resource_class.delete_from_cloudformation_json(
AttributeError: type object 'LambdaVersion' has no attribute 'delete_from_cloudformation_json'
```

I added in a temporary dummy method, to fix that, and then I got almost the same error with a similar traceback, with the error 
```
...
File "/usr/local/lib/python2.7/site-packages/moto/cloudformation/models.py", line 330, in update_stack
stack.update(template, role_arn, parameters=parameters, tags=tags)
File "/usr/local/lib/python2.7/site-packages/moto/cloudformation/models.py", line 112, in update
self.resource_map.update(self.template_dict, parameters)
File "/usr/local/lib/python2.7/site-packages/moto/cloudformation/parsing.py", line 545, in update
raise last_exception
AttributeError: type object 'LambdaFunction' has no attribute 'update_from_cloudformation_json'
```


Discussion:
Thanks for opening; setting this as a feature request.
https://github.com/spulec/moto/issues/793

Resolving patch:
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -357,6 +357,8 @@ def update_function_code(self, updated_spec):
                 self.code_bytes = key.value
                 self.code_size = key.size
                 self.code_sha_256 = hashlib.sha256(key.value).hexdigest()
+                self.code["S3Bucket"] = updated_spec["S3Bucket"]
+                self.code["S3Key"] = updated_spec["S3Key"]
 
         return self.get_configuration()
 
@@ -520,6 +522,15 @@ def get_cfn_attribute(self, attribute_name):
             return make_function_arn(self.region, ACCOUNT_ID, self.function_name)
         raise UnformattedGetAttTemplateException()
 
+    @classmethod
+    def update_from_cloudformation_json(
+        cls, new_resource_name, cloudformation_json, original_resource, region_name
+    ):
+        updated_props = cloudformation_json["Properties"]
+        original_resource.update_configuration(updated_props)
+        original_resource.update_function_code(updated_props["Code"])
+        return original_resource
+
     @staticmethod
     def _create_zipfile_from_plaintext_code(code):
         zip_output = io.BytesIO()
@@ -529,6 +540,9 @@ def _create_zipfile_from_plaintext_code(code):
         zip_output.seek(0)
         return zip_output.read()
 
+    def delete(self, region):
+        lambda_backends[region].delete_function(self.function_name)
+
 
 class EventSourceMapping(BaseModel):
     def __init__(self, spec):


Test patch:
diff --git a/tests/test_awslambda/test_lambda_cloudformation.py b/tests/test_awslambda/test_lambda_cloudformation.py
new file mode 100644
--- /dev/null
+++ b/tests/test_awslambda/test_lambda_cloudformation.py
@@ -0,0 +1,138 @@
+import boto3
+import io
+import sure  # noqa
+import zipfile
+from botocore.exceptions import ClientError
+from moto import mock_cloudformation, mock_iam, mock_lambda, mock_s3
+from nose.tools import assert_raises
+from string import Template
+from uuid import uuid4
+
+
+def _process_lambda(func_str):
+    zip_output = io.BytesIO()
+    zip_file = zipfile.ZipFile(zip_output, "w", zipfile.ZIP_DEFLATED)
+    zip_file.writestr("lambda_function.py", func_str)
+    zip_file.close()
+    zip_output.seek(0)
+    return zip_output.read()
+
+
+def get_zip_file():
+    pfunc = """
+def lambda_handler1(event, context):
+    return event
+def lambda_handler2(event, context):
+    return event
+"""
+    return _process_lambda(pfunc)
+
+
+template = Template(
+    """{
+    "AWSTemplateFormatVersion": "2010-09-09",
+    "Resources": {
+        "LF3ABOV": {
+            "Type": "AWS::Lambda::Function",
+            "Properties": {
+                "Handler": "$handler",
+                "Role": "$role_arn",
+                "Runtime": "$runtime",
+                "Code": {
+                    "S3Bucket": "$bucket_name",
+                    "S3Key": "$key"
+                },
+            }
+        }
+    }
+}"""
+)
+
+
+@mock_cloudformation
+@mock_lambda
+@mock_s3
+def test_lambda_can_be_updated_by_cloudformation():
+    s3 = boto3.client("s3", "us-east-1")
+    cf = boto3.client("cloudformation", region_name="us-east-1")
+    lmbda = boto3.client("lambda", region_name="us-east-1")
+    body2, stack = create_stack(cf, s3)
+    created_fn_name = get_created_function_name(cf, stack)
+    # Verify function has been created
+    created_fn = lmbda.get_function(FunctionName=created_fn_name)
+    created_fn["Configuration"]["Handler"].should.equal(
+        "lambda_function.lambda_handler1"
+    )
+    created_fn["Configuration"]["Runtime"].should.equal("python3.7")
+    created_fn["Code"]["Location"].should.match("/test1.zip")
+    # Update CF stack
+    cf.update_stack(StackName="teststack", TemplateBody=body2)
+    updated_fn_name = get_created_function_name(cf, stack)
+    # Verify function has been updated
+    updated_fn = lmbda.get_function(FunctionName=updated_fn_name)
+    updated_fn["Configuration"]["FunctionArn"].should.equal(
+        created_fn["Configuration"]["FunctionArn"]
+    )
+    updated_fn["Configuration"]["Handler"].should.equal(
+        "lambda_function.lambda_handler2"
+    )
+    updated_fn["Configuration"]["Runtime"].should.equal("python3.8")
+    updated_fn["Code"]["Location"].should.match("/test2.zip")
+
+
+@mock_cloudformation
+@mock_lambda
+@mock_s3
+def test_lambda_can_be_deleted_by_cloudformation():
+    s3 = boto3.client("s3", "us-east-1")
+    cf = boto3.client("cloudformation", region_name="us-east-1")
+    lmbda = boto3.client("lambda", region_name="us-east-1")
+    _, stack = create_stack(cf, s3)
+    created_fn_name = get_created_function_name(cf, stack)
+    # Delete Stack
+    cf.delete_stack(StackName=stack["StackId"])
+    # Verify function was deleted
+    with assert_raises(ClientError) as e:
+        lmbda.get_function(FunctionName=created_fn_name)
+    e.exception.response["Error"]["Code"].should.equal("404")
+
+
+def create_stack(cf, s3):
+    bucket_name = str(uuid4())
+    s3.create_bucket(Bucket=bucket_name)
+    s3.put_object(Bucket=bucket_name, Key="test1.zip", Body=get_zip_file())
+    s3.put_object(Bucket=bucket_name, Key="test2.zip", Body=get_zip_file())
+    body1 = get_template(bucket_name, "1", "python3.7")
+    body2 = get_template(bucket_name, "2", "python3.8")
+    stack = cf.create_stack(StackName="teststack", TemplateBody=body1)
+    return body2, stack
+
+
+def get_created_function_name(cf, stack):
+    res = cf.list_stack_resources(StackName=stack["StackId"])
+    return res["StackResourceSummaries"][0]["PhysicalResourceId"]
+
+
+def get_template(bucket_name, version, runtime):
+    key = "test" + version + ".zip"
+    handler = "lambda_function.lambda_handler" + version
+    return template.substitute(
+        bucket_name=bucket_name,
+        key=key,
+        handler=handler,
+        role_arn=get_role_arn(),
+        runtime=runtime,
+    )
+
+
+def get_role_arn():
+    with mock_iam():
+        iam = boto3.client("iam", region_name="us-west-2")
+        try:
+            return iam.get_role(RoleName="my-role")["Role"]["Arn"]
+        except ClientError:
+            return iam.create_role(
+                RoleName="my-role",
+                AssumeRolePolicyDocument="some policy",
+                Path="/my-path/",
+            )["Role"]["Arn"]