Repository: localstack/localstack

Issue:
Lambdas do not run due to failure to create a log group
Sometimes, attempting to run lambdas will return this type of error:
```
{"Type": "Server", "message": "Error executing Lambda function arn:aws:lambda:ap-northeast-1:000000000000:function:service-stage-functionName: An error occurred (ResourceAlreadyExistsException) when calling the CreateLogGroup operation: The specified log group already exists Traceback (most recent call last):\n 
File \"/opt/code/localstack/localstack/services/awslambda/lambda_api.py\", line 338, in run_lambda\n 
event, context=context, version=version, asynchronous=asynchronous)\n 
File \"/opt/code/localstack/localstack/services/awslambda/lambda_executors.py\", line 81, in execute\n 
return do_execute()\n File \"/opt/code/localstack/localstack/services/awslambda/lambda_executors.py\", line 70, in do_execute\n self._store_logs(func_details, log_output, invocation_time)\n 
File \"/opt/code/localstack/localstack/services/awslambda/lambda_executors.py\", line 105, in _store_logs\n logs_client.create_log_group(logGroupName=log_group_name)\n 
File \"/opt/code/localstack/.venv/lib/python3.6/site-packages/botocore/client.py\", line 357, in _api_call\n 
return self._make_api_call(operation_name, kwargs)\n 
File \"/opt/code/localstack/.venv/lib/python3.6/site-packages/botocore/client.py\", line 661, in _make_api_call\n 
raise error_class(parsed_response, operation_name)\n
botocore.errorfactory.ResourceAlreadyExistsException: An error occurred (ResourceAlreadyExistsException) when calling the CreateLogGroup operation: The specified log group already exists\n", "__type": "InternalFailure"}
```
I am not yet sure if there are any particular settings that cause it, since it does not occur for certain deployments.

Will update if I can figure out the exact configuration.


Discussion:

Resolving patch:
diff --git a/localstack/services/awslambda/lambda_executors.py b/localstack/services/awslambda/lambda_executors.py
--- a/localstack/services/awslambda/lambda_executors.py
+++ b/localstack/services/awslambda/lambda_executors.py
@@ -5,12 +5,12 @@
 import logging
 import threading
 import subprocess
+import botocore.errorfactory
 from multiprocessing import Process, Queue
 try:
     from shlex import quote as cmd_quote
 except ImportError:
-    # for Python 2.7
-    from pipes import quote as cmd_quote
+    from pipes import quote as cmd_quote  # for Python 2.7
 from localstack import config
 from localstack.utils.aws import aws_stack
 from localstack.utils.common import (
@@ -102,7 +102,11 @@ def _store_logs(self, func_details, log_output, invocation_time):
         log_groups = logs_client.describe_log_groups()['logGroups']
         log_groups = [lg['logGroupName'] for lg in log_groups]
         if log_group_name not in log_groups:
-            logs_client.create_log_group(logGroupName=log_group_name)
+            try:
+                logs_client.create_log_group(logGroupName=log_group_name)
+            except botocore.errorfactory.ResourceAlreadyExistsException:
+                # this can happen in certain cases, possibly due to a race condition
+                pass
 
         # create a new log stream for this lambda invocation
         logs_client.create_log_stream(logGroupName=log_group_name, logStreamName=log_stream_name)
diff --git a/localstack/services/cloudformation/cloudformation_starter.py b/localstack/services/cloudformation/cloudformation_starter.py
--- a/localstack/services/cloudformation/cloudformation_starter.py
+++ b/localstack/services/cloudformation/cloudformation_starter.py
@@ -152,7 +152,16 @@ def parse_and_create_resource(logical_id, resource_json, resources_map, region_n
                       (logical_id, e, traceback.format_exc()))
             raise
 
-    def _parse_and_create_resource(logical_id, resource_json, resources_map, region_name):
+    def parse_and_update_resource(logical_id, resource_json, resources_map, region_name):
+        try:
+            return _parse_and_create_resource(logical_id,
+                resource_json, resources_map, region_name, update=True)
+        except Exception as e:
+            LOG.error('Unable to parse and update resource "%s": %s %s' %
+                      (logical_id, e, traceback.format_exc()))
+            raise
+
+    def _parse_and_create_resource(logical_id, resource_json, resources_map, region_name, update=False):
         stack_name = resources_map.get('AWS::StackName')
         resource_hash_key = (stack_name, logical_id)
 
@@ -175,6 +184,8 @@ def _parse_and_create_resource(logical_id, resource_json, resources_map, region_
 
         # check if this resource already exists in the resource map
         resource = resources_map._parsed_resources.get(logical_id)
+        if resource and not update:
+            return resource
 
         # check whether this resource needs to be deployed
         resource_wrapped = {logical_id: resource_json}
@@ -186,17 +197,20 @@ def _parse_and_create_resource(logical_id, resource_json, resources_map, region_
                 if resource:
                     return resource
 
-        if not resource:
-            # fix resource ARNs, make sure to convert account IDs 000000000000 to 123456789012
-            resource_json_arns_fixed = clone(json_safe(convert_objs_to_ids(resource_json)))
-            set_moto_account_ids(resource_json_arns_fixed)
-            # create resource definition and store CloudFormation metadata in moto
+        # fix resource ARNs, make sure to convert account IDs 000000000000 to 123456789012
+        resource_json_arns_fixed = clone(json_safe(convert_objs_to_ids(resource_json)))
+        set_moto_account_ids(resource_json_arns_fixed)
+        # create resource definition and store CloudFormation metadata in moto
+        if resource or update:
+            parse_and_update_resource_orig(logical_id,
+                resource_json_arns_fixed, resources_map, region_name)
+        elif not resource:
             resource = parse_and_create_resource_orig(logical_id,
                 resource_json_arns_fixed, resources_map, region_name)
-            # Fix for moto which sometimes hard-codes region name as 'us-east-1'
-            if hasattr(resource, 'region_name') and resource.region_name != region_name:
-                LOG.debug('Updating incorrect region from %s to %s' % (resource.region_name, region_name))
-                resource.region_name = region_name
+        # Fix for moto which sometimes hard-codes region name as 'us-east-1'
+        if hasattr(resource, 'region_name') and resource.region_name != region_name:
+            LOG.debug('Updating incorrect region from %s to %s' % (resource.region_name, region_name))
+            resource.region_name = region_name
 
         # Apply some fixes/patches to the resource names, then deploy resource in LocalStack
         update_resource_name(resource, resource_json)
@@ -204,7 +218,7 @@ def _parse_and_create_resource(logical_id, resource_json, resources_map, region_
 
         try:
             CURRENTLY_UPDATING_RESOURCES[resource_hash_key] = True
-            deploy_func = template_deployer.deploy_resource if should_be_created else template_deployer.update_resource
+            deploy_func = template_deployer.update_resource if update else template_deployer.deploy_resource
             result = deploy_func(logical_id, resource_wrapped, stack_name=stack_name)
         finally:
             CURRENTLY_UPDATING_RESOURCES[resource_hash_key] = False
@@ -305,6 +319,8 @@ def update_physical_resource_id(resource):
 
     parse_and_create_resource_orig = parsing.parse_and_create_resource
     parsing.parse_and_create_resource = parse_and_create_resource
+    parse_and_update_resource_orig = parsing.parse_and_update_resource
+    parsing.parse_and_update_resource = parse_and_update_resource
 
     # Patch CloudFormation parse_output(..) method to fix a bug in moto
 
@@ -423,6 +439,37 @@ def Lambda_create_from_cloudformation_json(cls, resource_name, cloudformation_js
     Lambda_create_from_cloudformation_json_orig = lambda_models.LambdaFunction.create_from_cloudformation_json
     lambda_models.LambdaFunction.create_from_cloudformation_json = Lambda_create_from_cloudformation_json
 
+    # Patch LambdaFunction update_from_cloudformation_json(..) method in moto
+
+    @classmethod
+    def Lambda_update_from_cloudformation_json(cls,
+            original_resource, new_resource_name, cloudformation_json, region_name):
+        resource_name = cloudformation_json.get('Properties', {}).get('FunctionName') or new_resource_name
+        return Lambda_create_from_cloudformation_json_orig(resource_name, cloudformation_json, region_name)
+
+    if not hasattr(lambda_models.LambdaFunction, 'update_from_cloudformation_json'):
+        lambda_models.LambdaFunction.update_from_cloudformation_json = Lambda_update_from_cloudformation_json
+
+    # patch ApiGateway Deployment
+
+    def depl_delete_from_cloudformation_json(
+            resource_name, resource_json, region_name):
+        properties = resource_json['Properties']
+        LOG.info('TODO: apigateway.Deployment.delete_from_cloudformation_json %s' % properties)
+
+    if not hasattr(apigw_models.Deployment, 'delete_from_cloudformation_json'):
+        apigw_models.Deployment.delete_from_cloudformation_json = depl_delete_from_cloudformation_json
+
+    # patch Lambda Version
+
+    def vers_delete_from_cloudformation_json(
+            resource_name, resource_json, region_name):
+        properties = resource_json['Properties']
+        LOG.info('TODO: apigateway.Deployment.delete_from_cloudformation_json %s' % properties)
+
+    if not hasattr(lambda_models.LambdaVersion, 'delete_from_cloudformation_json'):
+        lambda_models.LambdaVersion.delete_from_cloudformation_json = vers_delete_from_cloudformation_json
+
     # add CloudWatch types
 
     parsing.MODEL_MAP['AWS::ApiGateway::Deployment'] = apigw_models.Deployment
diff --git a/localstack/utils/cloudformation/template_deployer.py b/localstack/utils/cloudformation/template_deployer.py
--- a/localstack/utils/cloudformation/template_deployer.py
+++ b/localstack/utils/cloudformation/template_deployer.py
@@ -94,7 +94,9 @@ def get_bucket_location_config(**kwargs):
         'create': {
             'function': 'publish_version',
             'parameters': {
-                # TODO
+                'FunctionName': 'FunctionName',
+                'CodeSha256': 'CodeSha256',
+                'Description': 'Description'
             }
         }
     },


Test patch: