Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions samcli/local/lambdafn/runtime.py
Original file line number Diff line number Diff line change
@@ -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__)
Expand Down Expand Up @@ -68,14 +69,15 @@ 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,
function_config.handler,
function_config.packagetype,
function_config.imageconfig,
code_dir,
function_config.layers,
layers,
self._image_builder,
memory_mb=function_config.memory,
env_vars=env_vars,
Expand Down Expand Up @@ -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``:
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to confirm, this only works if layer zip is present on local?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, same as how we handle Function today

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if customers provide a S3 zip url, it is not gonna work

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
Expand Down
20 changes: 19 additions & 1 deletion tests/integration/local/invoke/test_integrations_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions tests/unit/local/lambdafn/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down