Repository: localstack/localstack

Issue:
Allow deployment of single JAR for Java Lambdas
AWS supports two forms of archives for deploying Java Lambda functions:
1. "Fat" JAR - single jar file with all dependencies
2. ZIP - zip archive with class files in `/` and jar files in `/lib`

http://docs.aws.amazon.com/lambda/latest/dg/lambda-java-how-to-create-deployment-package.html

LocalStack currently expects a single JAR file within a ZIP file, which is incompatible with what AWS expects.


Discussion:
Another thing which doesn't work is having a python handler in a subdirectory. It's the same piece of code which is to blame it seems to me: https://github.com/localstack/localstack/blob/debb24a792a7e2a1751ddf1f30d5c79f80b4885f/localstack/services/awslambda/lambda_api.py#L373-L391
Hi, localstack still does not allow the java lambda zip package with multiple jars in lib folder.
https://docs.aws.amazon.com/lambda/latest/dg/create-deployment-pkg-zip-java.html
And raises the error "the CreateFunction operation: Expected exactly one *.jar entry in zip file, found 78"


Resolving patch:
diff --git a/localstack/services/awslambda/lambda_api.py b/localstack/services/awslambda/lambda_api.py
--- a/localstack/services/awslambda/lambda_api.py
+++ b/localstack/services/awslambda/lambda_api.py
@@ -470,15 +470,15 @@ def get_java_handler(zip_file_content, handler, main_file):
     """
     if not is_jar_archive(zip_file_content):
         with zipfile.ZipFile(BytesIO(zip_file_content)) as zip_ref:
+            # TODO: check if this is still needed (probably not)
             jar_entries = [e for e in zip_ref.infolist() if e.filename.endswith('.jar')]
-            if len(jar_entries) != 1:
-                raise ClientError('Expected exactly one *.jar entry in zip file, found %s' % len(jar_entries))
-            zip_file_content = zip_ref.read(jar_entries[0].filename)
-            LOG.info('Found jar file %s with %s bytes in Lambda zip archive' %
-                     (jar_entries[0].filename, len(zip_file_content)))
-            main_file = new_tmp_file()
-            save_file(main_file, zip_file_content)
-    if is_jar_archive(zip_file_content):
+            if len(jar_entries) == 1:
+                zip_file_content = zip_ref.read(jar_entries[0].filename)
+                LOG.info('Found single jar file %s with %s bytes in Lambda zip archive' %
+                         (jar_entries[0].filename, len(zip_file_content)))
+                main_file = new_tmp_file()
+                save_file(main_file, zip_file_content)
+    if is_zip_file(zip_file_content):
         def execute(event, context):
             result, log_output = lambda_executors.EXECUTOR_LOCAL.execute_java_lambda(
                 event, context, handler=handler, main_file=main_file)
@@ -557,37 +557,25 @@ def generic_handler(event, context):
 
     # Set the appropriate lambda handler.
     lambda_handler = generic_handler
+
     if runtime == LAMBDA_RUNTIME_JAVA8:
-        # The Lambda executors for Docker subclass LambdaExecutorContainers,
-        # which runs Lambda in Docker by passing all *.jar files in the function
-        # working directory as part of the classpath. Because of this, we need to
-        # save the zip_file_content as a .jar here.
+        # The Lambda executors for Docker subclass LambdaExecutorContainers, which
+        # runs Lambda in Docker by passing all *.jar files in the function working
+        # directory as part of the classpath. Obtain a Java handler function below.
         lambda_handler, zip_file_content = get_java_handler(zip_file_content, handler_name, tmp_file)
-        if is_jar_archive(zip_file_content):
-            jar_tmp_file = '{working_dir}/{file_name}'.format(
-                working_dir=lambda_cwd, file_name=LAMBDA_JAR_FILE_NAME)
-            save_file(jar_tmp_file, zip_file_content)
-
-            # AWS Lambda allows dependencies in the "lib" folder of the jar
-            # ref: https://docs.aws.amazon.com/lambda/latest/dg/create-deployment-pkg-zip-java.html
-            #     "The deployment package must have the following structure.
-            #       * All compiled class files and resource files at the root level.
-            #       * All required jars to run the code in the /lib directory."
-            # We extract the lib folder here in order to add it to the classpath in
-            # lambda_executors.py. This will always return a 0 exit code, even if
-            # the "lib" folder does not exist.
-            run('cd "%s"; jar xvf %s lib' % (os.path.dirname(jar_tmp_file), os.path.basename(jar_tmp_file)))
-    else:
+
+    if not is_local_mount:
+        # Lambda code must be uploaded in Zip format
+        if not is_zip_file(zip_file_content):
+            raise ClientError(
+                'Uploaded Lambda code for runtime ({}) is not in Zip format'.format(runtime))
+        unzip(tmp_file, lambda_cwd)
+
+    # Obtain handler details for any non-Java Lambda function
+    if runtime != LAMBDA_RUNTIME_JAVA8:
         handler_file = get_handler_file_from_name(handler_name, runtime=runtime)
         handler_function = get_handler_function_from_name(handler_name, runtime=runtime)
 
-        if not is_local_mount:
-            # Lambda code must be uploaded in Zip format
-            if not is_zip_file(zip_file_content):
-                raise ClientError(
-                    'Uploaded Lambda code for runtime ({}) is not in Zip format'.format(runtime))
-            unzip(tmp_file, lambda_cwd)
-
         main_file = '%s/%s' % (lambda_cwd, handler_file)
         if not os.path.exists(main_file):
             # Raise an error if (1) this is not a local mount lambda, or (2) we're
diff --git a/localstack/utils/testutil.py b/localstack/utils/testutil.py
--- a/localstack/utils/testutil.py
+++ b/localstack/utils/testutil.py
@@ -83,7 +83,6 @@ def create_zip_file(file_path, get_content=False):
                 zip_file.write(full_name, dest)
     if not get_content:
         TMP_FILES.append(tmp_dir)
-        shutil.rmtree(tmp_dir)
         return full_zip_file
     zip_file_content = None
     with open(full_zip_file, 'rb') as file_obj:


Test patch:
diff --git a/tests/integration/test_lambda.py b/tests/integration/test_lambda.py
--- a/tests/integration/test_lambda.py
+++ b/tests/integration/test_lambda.py
@@ -2,6 +2,7 @@
 import os
 import json
 import time
+import shutil
 import unittest
 import six
 from io import BytesIO
@@ -10,7 +11,8 @@
 from localstack.utils import testutil
 from localstack.utils.aws import aws_stack
 from localstack.utils.common import (
-    short_uid, load_file, to_str, mkdir, download, run_safe, get_free_tcp_port, get_service_protocol)
+    unzip, new_tmp_dir, short_uid, load_file, to_str, mkdir, download,
+    run_safe, get_free_tcp_port, get_service_protocol)
 from localstack.services.infra import start_proxy
 from localstack.services.awslambda import lambda_api, lambda_executors
 from localstack.services.generic_proxy import ProxyListener
@@ -40,7 +42,6 @@
 TEST_LAMBDA_NAME_JAVA = 'test_lambda_java'
 TEST_LAMBDA_NAME_JAVA_STREAM = 'test_lambda_java_stream'
 TEST_LAMBDA_NAME_JAVA_SERIALIZABLE = 'test_lambda_java_serializable'
-TEST_LAMBDA_NAME_JAVA_WITH_LIB = 'test_lambda_java_with_lib'
 TEST_LAMBDA_NAME_ENV = 'test_lambda_env'
 
 MAVEN_BASE_URL = 'https://repo.maven.apache.org/maven2'
@@ -521,22 +522,12 @@ def setUpClass(cls):
             handler='cloud.localstack.sample.SerializedInputLambdaHandler'
         )
 
-        # upload the JAR directly
-        cls.test_java_jar_with_lib = load_file(TEST_LAMBDA_JAVA_WITH_LIB, mode='rb')
-        testutil.create_lambda_function(
-            func_name=TEST_LAMBDA_NAME_JAVA_WITH_LIB,
-            zip_file=cls.test_java_jar_with_lib,
-            runtime=LAMBDA_RUNTIME_JAVA8,
-            handler='cloud.localstack.sample.LambdaHandlerWithLib'
-        )
-
     @classmethod
     def tearDownClass(cls):
         # clean up
         testutil.delete_lambda_function(TEST_LAMBDA_NAME_JAVA)
         testutil.delete_lambda_function(TEST_LAMBDA_NAME_JAVA_STREAM)
         testutil.delete_lambda_function(TEST_LAMBDA_NAME_JAVA_SERIALIZABLE)
-        testutil.delete_lambda_function(TEST_LAMBDA_NAME_JAVA_WITH_LIB)
 
     def test_java_runtime(self):
         self.assertIsNotNone(self.test_java_jar)
@@ -549,14 +540,30 @@ def test_java_runtime(self):
         self.assertIn('LinkedHashMap', to_str(result_data))
 
     def test_java_runtime_with_lib(self):
-        self.assertIsNotNone(self.test_java_jar_with_lib)
-
-        result = self.lambda_client.invoke(
-            FunctionName=TEST_LAMBDA_NAME_JAVA_WITH_LIB, Payload=b'{"echo":"echo"}')
-        result_data = result['Payload'].read()
-
-        self.assertEqual(result['StatusCode'], 200)
-        self.assertIn('echo', to_str(result_data))
+        java_jar_with_lib = load_file(TEST_LAMBDA_JAVA_WITH_LIB, mode='rb')
+
+        # create ZIP file from JAR file
+        jar_dir = new_tmp_dir()
+        zip_dir = new_tmp_dir()
+        unzip(TEST_LAMBDA_JAVA_WITH_LIB, jar_dir)
+        shutil.move(os.path.join(jar_dir, 'lib'), os.path.join(zip_dir, 'lib'))
+        jar_without_libs_file = testutil.create_zip_file(jar_dir)
+        shutil.copy(jar_without_libs_file, os.path.join(zip_dir, 'lib', 'lambda.jar'))
+        java_zip_with_lib = testutil.create_zip_file(zip_dir, get_content=True)
+
+        for archive in [java_jar_with_lib, java_zip_with_lib]:
+            lambda_name = 'test-%s' % short_uid()
+            testutil.create_lambda_function(func_name=lambda_name,
+                zip_file=archive, runtime=LAMBDA_RUNTIME_JAVA8,
+                handler='cloud.localstack.sample.LambdaHandlerWithLib')
+
+            result = self.lambda_client.invoke(FunctionName=lambda_name, Payload=b'{"echo":"echo"}')
+            result_data = result['Payload'].read()
+
+            self.assertEqual(result['StatusCode'], 200)
+            self.assertIn('echo', to_str(result_data))
+            # clean up
+            testutil.delete_lambda_function(lambda_name)
 
     def test_sns_event(self):
         result = self.lambda_client.invoke(