Repository: localstack/localstack
Pull request: Correctly format code if inline code is used on update
Currently, the method wrapping the code in a zip file is not called on update.
This PR fixes this behavior, and adds a test to test it.

Fixes #4786 

Problem statement:
bug: Redeploying lambda with CDK fails "localstack.services.awslambda.lambda_api.ClientError: Uploaded Lambda code for runtime (nodejs14.x) is not in Zip format"
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

When using CDK to deploy a simple Lambda function the stack deploys first time and is successful. 
Though when deployed a second time it fails. 

If I change the name of the lambda it will be successfully. 

### Expected Behavior

To redeploy and update the lambda successfully 

### How are you starting LocalStack?

With a docker-compose file

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    `docker-compose up` 

Compose file: 

```yaml
version: '3.8'

services:
  localstack:
    container_name: '${LOCALSTACK_DOCKER_NAME-localstack_main}'
    image: localstack/localstack
    network_mode: bridge
    ports:
      - '127.0.0.1:53:53' # only required for Pro
      - '127.0.0.1:53:53/udp' # only required for Pro
      - '127.0.0.1:443:443' # only required for Pro
      - '127.0.0.1:4510-4530:4510-4530' # only required for Pro
      - '127.0.0.1:4566:4566'
      - '127.0.0.1:4571:4571'
    environment:
      - SERVICES=${SERVICES- }
      - DEBUG=${DEBUG- }
      - DATA_DIR=${DATA_DIR- }
      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- }
      - LOCALSTACK_API_KEY=${LOCALSTACK_API_KEY- } # only required for Pro
      - HOST_TMP_FOLDER=${TMPDIR:-/tmp/}localstack
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - '${TMPDIR:-/tmp}/localstack:/tmp/localstack'
      - '/var/run/docker.sock:/var/run/docker.sock'
```

Stack file:

```ts
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import { Construct } from '@aws-cdk/core';
import { Environment } from '../environment/environment';

function getStackName(environment: Environment): string {
  if (environment === 'local') {
    return 'local';
  }

  return `project-${environment}`;
}

export class TestServiceStack extends cdk.Stack {
  constructor(scope: cdk.App, environment: Environment, props?: cdk.StackProps) {
    super(scope, getStackName(environment), props);

    const lambda = this.createS3PackageReceived(this);
  }

  private createS3PackageReceived(owner: Construct): lambda.Function {
    const handler = new lambda.Function(owner, 'fn', {
      code: lambda.Code.fromInline(`exports.handler = async (event, context) => {return "hello"}`),
      handler: 'index.handler',
      functionName: 'test-func3',
      runtime: lambda.Runtime.NODEJS_14_X,
    });

    return handler;
  }
}
```

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

Deployments are done with: 
```bash
docker-compose up
npx cdklocal bootstrap -v
npx cdklocal deploy --progress events --require-approval never # will be successful 
npx cdklocal deploy --progress events --require-approval never # will fail 
```

In the logs there are two interesting things: 
1.  `2021-10-25T22:35:51:WARNING:localstack.utils.cloudformation.template_deployer: Unable to deploy resource type "CDK::Metadata": {'Type': 'AWS::CDK::Metadata', 'Metadata': {'aws:cdk:path': 'local/CDKMetadata/Default'}, 'Condition': 'CDKMetadataAvailable', 'LogicalResourceId': 'CDKMetadata', 'Properties': {'Analytics': 'v2:deflate64:H4sIAAAAAAAAE02MSQoCMRBFz9L7dLXlgLgTGjxAPEFMIlQPVZBBFyF31yiCq/fgDwgFcQeb7myesbduHqwED+WajJ3VKBxTyDap8c7aR8nB+ubvwFEi4apwe/qfN5bFrDdnoFwy29Zqk59XRWaFomX5PDXWWhWL8zDF4YF7wCMcuikS9SFzotWD/vIFs9XuYq4AAAA='}}` 
  This warning is generated for both successful and unsuccessful deployments

2. 
```
2021-10-25T22:36:50:ERROR:lambda_api: Exception on /2015-03-31/functions/test-func/code [PUT]
Traceback (most recent call last):
  File "/opt/code/localstack/.venv/lib/python3.7/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
  File "/opt/code/localstack/.venv/lib/python3.7/site-packages/flask/app.py", line 1518, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/opt/code/localstack/.venv/lib/python3.7/site-packages/flask_cors/extension.py", line 165, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/opt/code/localstack/.venv/lib/python3.7/site-packages/flask/app.py", line 1516, in full_dispatch_request
    rv = self.dispatch_request()
  File "/opt/code/localstack/.venv/lib/python3.7/site-packages/flask/app.py", line 1502, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
  File "/opt/code/localstack/localstack/services/awslambda/lambda_api.py", line 1482, in update_function_code
    result = set_function_code(lambda_function)
  File "/opt/code/localstack/localstack/services/awslambda/lambda_api.py", line 1013, in set_function_code
    run_for_max_seconds(config.LAMBDA_CODE_EXTRACT_TIME, _set_and_configure)
  File "/opt/code/localstack/localstack/utils/common.py", line 1747, in run_for_max_seconds
    raise result
  File "/opt/code/localstack/localstack/utils/common.py", line 1728, in _worker
    fn_result = _function(*args, **kwargs)
  File "/opt/code/localstack/localstack/services/awslambda/lambda_api.py", line 1006, in _set_and_configure
    lambda_handler = do_set_function_code(lambda_function)
  File "/opt/code/localstack/localstack/services/awslambda/lambda_api.py", line 1081, in do_set_function_code
    "Uploaded Lambda code for runtime ({}) is not in Zip format".format(runtime)
localstack.services.awslambda.lambda_api.ClientError: Uploaded Lambda code for runtime (nodejs14.x) is not in Zip format
``` 
   this is generated on the second deployment. This stack trace is generated multiple times. 



### Environment

```markdown
- OS: OSX 11.6
- LocalStack: 
  - localstack_main | LocalStack version: 0.12.19.1
  - localstack_main | LocalStack Docker container id: deb3b6cc94e5
  - localstack_main | LocalStack build date: 2021-10-25
  - localstack_main | LocalStack build git hash: 109e0173
- package.json dependencies 

{
  "devDependencies": {
    "@aws-cdk/assert": "1.113.0",
    "@types/express": "^4.17.13",
    "@types/jest": "^26.0.23",
    "@types/supertest": "^2.0.11",
    "aws-cdk": "^1.129.0",
    "aws-cdk-local": "^1.65.8",
    "jest": "^26.6.3",
    "nodemon": "^2.0.10",
    "supertest": "^6.1.3",
    "ts-jest": "^26.5.5",
    "ts-node": "^10.0.0",
    "typescript": "^4.3.3"
  },
  "dependencies": {
    "@aws-cdk/aws-lambda": "^1.113.0",
    "@aws-cdk/aws-lambda-event-sources": "^1.129.0",
    "@aws-cdk/aws-s3": "^1.113.0",
    "@aws-cdk/core": "1.113.0",
    "@squiz-dx/asset-api": "^1.0.0",
    "express": "^4.17.1"
  }
}
```
```


### Anything else?

_No response_

Hints from issue discussion:

Commit message: fix double-zipping of files, activate test

Patch to review:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5838285f15149..ef98a45323e6e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -77,7 +77,7 @@ jobs:
           name: Test docker client
           environment:
             DEBUG: 1
-            TEST_PATH: "tests/integration/docker"
+            TEST_PATH: "tests/integration/docker_utils"
             TEST_SKIP_LOCALSTACK_START: 1
             PYTEST_ARGS: "--reruns 2 --junitxml=target/reports/docker-client.xml -o junit_suite_name='docker-client'"
             COVERAGE_ARGS: "-p"
diff --git a/localstack/services/cloudformation/models/awslambda.py b/localstack/services/cloudformation/models/awslambda.py
index 6083363110105..a2a54c4e9d3b0 100644
--- a/localstack/services/cloudformation/models/awslambda.py
+++ b/localstack/services/cloudformation/models/awslambda.py
@@ -9,7 +9,17 @@
 )
 from localstack.services.cloudformation.service_models import LOG, REF_ID_ATTRS, GenericBaseModel
 from localstack.utils.aws import aws_stack
-from localstack.utils.common import cp_r, is_base64, mkdir, new_tmp_dir, rm_rf, save_file, short_uid
+from localstack.utils.common import (
+    cp_r,
+    is_base64,
+    is_zip_file,
+    mkdir,
+    new_tmp_dir,
+    rm_rf,
+    save_file,
+    short_uid,
+    to_bytes,
+)
 from localstack.utils.testutil import create_zip_file
 
 
@@ -52,6 +62,7 @@ def update_resource(self, new_resource, stack_name, resources):
                     'Updating code for Lambda "%s" from location: %s'
                     % (props["FunctionName"], code)
                 )
+            code = self.get_deploy_templates()["create"]["parameters"].get("Code")(props)
             client.update_function_code(FunctionName=props["FunctionName"], **code)
         if "Environment" in update_props:
             environment_variables = update_props["Environment"].get("Variables", {})
@@ -73,7 +84,7 @@ def get_deploy_templates():
         def get_lambda_code_param(params, **kwargs):
             code = params.get("Code", {})
             zip_file = code.get("ZipFile")
-            if zip_file and not is_base64(zip_file):
+            if zip_file and not is_base64(zip_file) and not is_zip_file(to_bytes(zip_file)):
                 tmp_dir = new_tmp_dir()
                 handler_file = get_handler_file_from_name(
                     params["Handler"], runtime=params["Runtime"]
diff --git a/tests/integration/cloudformation/test_cloudformation_lambda.py b/tests/integration/cloudformation/test_cloudformation_lambda.py
index 6ec4b1797b862..228c973d14b27 100644
--- a/tests/integration/cloudformation/test_cloudformation_lambda.py
+++ b/tests/integration/cloudformation/test_cloudformation_lambda.py
@@ -1,6 +1,6 @@
 import jinja2
 
-from localstack.utils.common import short_uid
+from localstack.utils.common import short_uid, to_str
 from localstack.utils.generic.wait_utils import wait_until
 from tests.integration.cloudformation.test_cloudformation_changesets import load_template_raw
 
@@ -41,3 +41,44 @@ def test_lambda_autogenerated_name(
     finally:
         cleanup_changesets([change_set_id])
         cleanup_stacks([stack_id])
+
+
+def test_update_lambda_inline_code(
+    cfn_client, lambda_client, is_stack_created, is_stack_updated, cleanup_stacks
+):
+    stack_name = "stack-%s" % short_uid()
+    function_name = "test-func3"
+
+    try:
+        template_1 = jinja2.Template(load_template_raw("lambda_inline_code.yaml")).render(
+            lambda_return_value="hello world"
+        )
+        response = cfn_client.create_stack(
+            StackName=stack_name, TemplateBody=template_1, Capabilities=["CAPABILITY_IAM"]
+        )
+        stack_id = response["StackId"]
+        assert stack_id
+        wait_until(is_stack_created(stack_id))
+
+        rs = lambda_client.get_function(FunctionName=function_name)
+        assert function_name == rs["Configuration"]["FunctionName"]
+        result = lambda_client.invoke(FunctionName=function_name)
+        result = to_str(result["Payload"].read())
+        assert result.strip('" \n') == "hello world"
+
+        template_2 = jinja2.Template(load_template_raw("lambda_inline_code.yaml")).render(
+            lambda_return_value="hello globe"
+        )
+        cfn_client.update_stack(
+            StackName=stack_name, TemplateBody=template_2, Capabilities=["CAPABILITY_IAM"]
+        )
+        wait_until(is_stack_updated(stack_id))
+
+        rs = lambda_client.get_function(FunctionName=function_name)
+        assert function_name == rs["Configuration"]["FunctionName"]
+        result = lambda_client.invoke(FunctionName=function_name)
+        result = to_str(result["Payload"].read())
+        assert result.strip('" \n') == "hello globe"
+    finally:
+        # cleanup
+        cleanup_stacks([stack_name])
diff --git a/tests/integration/docker/__init__.py b/tests/integration/docker_utils/__init__.py
similarity index 100%
rename from tests/integration/docker/__init__.py
rename to tests/integration/docker_utils/__init__.py
diff --git a/tests/integration/docker/conftest.py b/tests/integration/docker_utils/conftest.py
similarity index 100%
rename from tests/integration/docker/conftest.py
rename to tests/integration/docker_utils/conftest.py
diff --git a/tests/integration/docker/test_docker.py b/tests/integration/docker_utils/test_docker.py
similarity index 100%
rename from tests/integration/docker/test_docker.py
rename to tests/integration/docker_utils/test_docker.py
diff --git a/tests/integration/templates/lambda_inline_code.yaml b/tests/integration/templates/lambda_inline_code.yaml
new file mode 100644
index 0000000000000..86430720de69f
--- /dev/null
+++ b/tests/integration/templates/lambda_inline_code.yaml
@@ -0,0 +1,114 @@
+Resources:
+  fnServiceRole5D180AFD:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action: sts:AssumeRole
+            Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+        Version: "2012-10-17"
+      ManagedPolicyArns:
+        - Fn::Join:
+            - ""
+            - - "arn:"
+              - Ref: AWS::Partition
+              - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+    Metadata:
+      aws:cdk:path: TestStack/fn/ServiceRole/Resource
+  fn5FF616E3:
+    Type: AWS::Lambda::Function
+    Properties:
+      Code:
+        ZipFile: exports.handler = async (event, context) => {return "{{ lambda_return_value }}"}
+      Role:
+        Fn::GetAtt:
+          - fnServiceRole5D180AFD
+          - Arn
+      FunctionName: test-func3
+      Handler: index.handler
+      Runtime: nodejs14.x
+    DependsOn:
+      - fnServiceRole5D180AFD
+    Metadata:
+      aws:cdk:path: TestStack/fn/Resource
+  CDKMetadata:
+    Type: AWS::CDK::Metadata
+    Properties:
+      Analytics: v2:deflate64:H4sIAAAAAAAA/zWLSwoCMRBEzzL7TI8RUdwJAx4gniB2IvTMpBvy0UXI3TWKq3pU1dOg92fYDRf7SiO6daoo0UO9ZYurmoVTjgWzmh9sfJIS0Xf+DI4yCTfVxbrZcHcW6rUw9rp//twU2QDVyPZVe7bWFIvzsKTpqQ+gT3AclkQ0xsKZggfzyzfx9cLbnwAAAA==
+    Metadata:
+      aws:cdk:path: TestStack/CDKMetadata/Default
+    Condition: CDKMetadataAvailable
+Conditions:
+  CDKMetadataAvailable:
+    Fn::Or:
+      - Fn::Or:
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - af-south-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - ap-east-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - ap-northeast-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - ap-northeast-2
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - ap-south-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - ap-southeast-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - ap-southeast-2
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - ca-central-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - cn-north-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - cn-northwest-1
+      - Fn::Or:
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - eu-central-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - eu-north-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - eu-south-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - eu-west-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - eu-west-2
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - eu-west-3
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - me-south-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - sa-east-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - us-east-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - us-east-2
+      - Fn::Or:
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - us-west-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - us-west-2


Code review comments:
diff_hunk: @@ -0,0 +1,114 @@
+Resources:
+  fnServiceRole5D180AFD:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action: sts:AssumeRole
+            Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+        Version: "2012-10-17"
+      ManagedPolicyArns:
+        - Fn::Join:
+            - ""
+            - - "arn:"
+              - Ref: AWS::Partition
+              - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+    Metadata:
+      aws:cdk:path: TestStack/fn/ServiceRole/Resource
+  fn5FF616E3:
+    Type: AWS::Lambda::Function
+    Properties:
+      Code:
+        ZipFile: exports.handler = async (event, context) => {return "{{ lambda_return_value }}"}
+      Role:
+        Fn::GetAtt:
+          - fnServiceRole5D180AFD
+          - Arn
+      FunctionName: test-func3
+      Handler: index.handler
+      Runtime: nodejs14.x
+    DependsOn:
+      - fnServiceRole5D180AFD
+    Metadata:
+      aws:cdk:path: TestStack/fn/Resource
+  CDKMetadata:
path: tests/integration/templates/lambda_inline_code.yaml
text: @user1:
In general I'd try to avoid including the cdk metadata in these templates to increase readability.

You can set the following settings in `cdk.json` to avoid generating these:

```
{
...
  "versionReporting": false
}
```

diff_hunk: @@ -52,6 +62,7 @@ def update_resource(self, new_resource, stack_name, resources):
                     'Updating code for Lambda "%s" from location: %s'
                     % (props["FunctionName"], code)
                 )
+            code = self.get_deploy_templates()["create"]["parameters"].get("Code")(props)
path: localstack/services/cloudformation/models/awslambda.py
text: @user1:
nit: might be better to "extract" this code param from props generation in get_deploy_templates and use it here as well