diff --git a/samcli/local/lambdafn/runtime.py b/samcli/local/lambdafn/runtime.py index 495174fe75..163f9670b9 100644 --- a/samcli/local/lambdafn/runtime.py +++ b/samcli/local/lambdafn/runtime.py @@ -1,20 +1,21 @@ """ Classes representing a local Lambda runtime """ - +import copy import os import shutil import tempfile import signal import logging import threading -from typing import Optional +from typing import Optional, Union, Dict from samcli.local.docker.lambda_container import LambdaContainer from samcli.lib.utils.file_observer import LambdaFunctionObserver from samcli.lib.utils.packagetype import ZIP from samcli.lib.telemetry.metric import capture_parameter from .zip import unzip +from ...lib.providers.provider import LayerVersion from ...lib.utils.stream_writer import StreamWriter LOG = logging.getLogger(__name__) @@ -68,6 +69,7 @@ def create(self, function_config, debug_context=None, container_host=None, conta env_vars = function_config.env_vars.resolve() code_dir = self._get_code_dir(function_config.code_abs_path) + layers = [self._unarchived_layer(layer) for layer in function_config.layers] container = LambdaContainer( function_config.runtime, function_config.imageuri, @@ -75,7 +77,7 @@ def create(self, function_config, debug_context=None, container_host=None, conta function_config.packagetype, function_config.imageconfig, code_dir, - function_config.layers, + layers, self._image_builder, memory_mb=function_config.memory, env_vars=env_vars, @@ -250,9 +252,9 @@ def signal_handler(sig, frame): timer.start() return timer - def _get_code_dir(self, code_path): + def _get_code_dir(self, code_path: str) -> str: """ - Method to get a path to a directory where the Lambda function code is available. This directory will + Method to get a path to a directory where the function/layer code is available. This directory will be mounted directly inside the Docker container. This method handles a few different cases for ``code_path``: @@ -274,13 +276,34 @@ def _get_code_dir(self, code_path): """ if code_path and os.path.isfile(code_path) and code_path.endswith(self.SUPPORTED_ARCHIVE_EXTENSIONS): - decompressed_dir = _unzip_file(code_path) + decompressed_dir: str = _unzip_file(code_path) self._temp_uncompressed_paths_to_be_cleaned += [decompressed_dir] return decompressed_dir LOG.debug("Code %s is not a zip/jar file", code_path) return code_path + def _unarchived_layer(self, layer: Union[str, Dict, LayerVersion]) -> Union[str, Dict, LayerVersion]: + """ + If the layer's content uri points to a supported local archive file, use self._get_code_dir() to + un-archive it and so that it can be mounted directly inside the Docker container. + Parameters + ---------- + layer + a str, dict or a LayerVersion object representing a layer + + Returns + ------- + as it is (if no archived file is identified) + or a LayerVersion with ContentUri pointing to an unarchived directory + """ + if isinstance(layer, LayerVersion) and isinstance(layer.codeuri, str): + unarchived_layer = copy.deepcopy(layer) + unarchived_layer.codeuri = self._get_code_dir(layer.codeuri) + return unarchived_layer if unarchived_layer.codeuri != layer.codeuri else layer + + return layer + def _clean_decompressed_paths(self): """ Clean the temporary decompressed code dirs diff --git a/tests/integration/local/invoke/test_integrations_cli.py b/tests/integration/local/invoke/test_integrations_cli.py index a9bbf4d824..a3d6206c40 100644 --- a/tests/integration/local/invoke/test_integrations_cli.py +++ b/tests/integration/local/invoke/test_integrations_cli.py @@ -13,7 +13,7 @@ from tests.integration.local.invoke.layer_utils import LayerUtils from .invoke_integ_base import InvokeIntegBase -from tests.testing_utils import IS_WINDOWS, RUNNING_ON_CI, RUNNING_TEST_FOR_MASTER_ON_CI, RUN_BY_CANARY +from tests.testing_utils import IS_WINDOWS, RUNNING_ON_CI, RUNNING_TEST_FOR_MASTER_ON_CI, RUN_BY_CANARY, run_command # Layers tests require credentials and Appveyor will only add credentials to the env if the PR is from the same repo. # This is to restrict layers tests to run outside of Appveyor, when the branch is not master and tests are not run by Canary. @@ -884,6 +884,24 @@ def test_caching_two_layers_with_layer_cache_env_set(self): self.assertEqual(2, len(os.listdir(str(self.layer_cache)))) +@skipIf(SKIP_LAYERS_TESTS, "Skip layers tests in Appveyor only") +class TestLocalZipLayerVersion(InvokeIntegBase): + template = Path("layers", "local-zip-layer-template.yml") + + def test_local_zip_layers( + self, + ): + command_list = self.get_command_list( + "OneLayerVersionServerlessFunction", + template_path=self.template_path, + no_event=True, + ) + + execute = run_command(command_list) + self.assertEqual(0, execute.process.returncode) + self.assertEqual('"Layer1"', execute.stdout.decode()) + + @skipIf(SKIP_LAYERS_TESTS, "Skip layers tests in Appveyor only") class TestLayerVersionThatDoNotCreateCache(InvokeIntegBase): template = Path("layers", "layer-template.yml") diff --git a/tests/integration/testdata/invoke/layers/local-zip-layer-template.yml b/tests/integration/testdata/invoke/layers/local-zip-layer-template.yml new file mode 100644 index 0000000000..466ff9791a --- /dev/null +++ b/tests/integration/testdata/invoke/layers/local-zip-layer-template.yml @@ -0,0 +1,19 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A hello world application. + +Resources: + LayerOne: + Type: AWS::Lambda::LayerVersion + Properties: + Content: ../layer_zips/layer1.zip + + OneLayerVersionServerlessFunction: + Type: AWS::Serverless::Function + Properties: + Handler: layer-main.one_layer_hanlder + Runtime: python3.6 + CodeUri: . + Timeout: 20 + Layers: + - !Ref LayerOne diff --git a/tests/unit/local/lambdafn/test_runtime.py b/tests/unit/local/lambdafn/test_runtime.py index ed848478de..38ba59110f 100644 --- a/tests/unit/local/lambdafn/test_runtime.py +++ b/tests/unit/local/lambdafn/test_runtime.py @@ -514,6 +514,28 @@ def test_must_return_a_valid_file(self, unzip_file_mock, shutil_mock, os_mock): shutil_mock.rmtree.assert_not_called() +class TestLambdaRuntime_unarchived_layer(TestCase): + def setUp(self): + self.manager_mock = Mock() + self.layer_downloader = Mock() + self.runtime = LambdaRuntime(self.manager_mock, self.layer_downloader) + + @parameterized.expand([(LayerVersion("arn", "file.zip"),)]) + @patch("samcli.local.lambdafn.runtime.LambdaRuntime._get_code_dir") + def test_unarchived_layer(self, layer, get_code_dir_mock): + new_url = get_code_dir_mock.return_value = Mock() + result = self.runtime._unarchived_layer(layer) + self.assertNotEqual(layer, result) + self.assertEqual(new_url, result.codeuri) + + @parameterized.expand([("arn",), (LayerVersion("arn", "folder"),), ({"Name": "hi", "Version": "x.y.z"},)]) + @patch("samcli.local.lambdafn.runtime.LambdaRuntime._get_code_dir") + def test_unarchived_layer_not_local_archive_file(self, layer, get_code_dir_mock): + get_code_dir_mock.side_effect = lambda x: x # directly return the input + result = self.runtime._unarchived_layer(layer) + self.assertEqual(layer, result) + + class TestWarmLambdaRuntime_invoke(TestCase): DEFAULT_MEMORY = 128