Skip to content

Commit e557067

Browse files
committed
Resolve Layer symlinks before mounting container
1 parent 1d97d1d commit e557067

File tree

5 files changed

+138
-34
lines changed

5 files changed

+138
-34
lines changed

samcli/lib/build/workflow_config.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,30 @@
2424

2525
LOG = logging.getLogger(__name__)
2626

27+
LAYER_SUBFOLDERS = {
28+
"python3.7": "python",
29+
"python3.8": "python",
30+
"python3.9": "python",
31+
"python3.10": "python",
32+
"python3.11": "python",
33+
"nodejs4.3": "nodejs",
34+
"nodejs6.10": "nodejs",
35+
"nodejs8.10": "nodejs",
36+
"nodejs12.x": "nodejs",
37+
"nodejs14.x": "nodejs",
38+
"nodejs16.x": "nodejs",
39+
"nodejs18.x": "nodejs",
40+
"ruby2.7": "ruby/lib",
41+
"ruby3.2": "ruby/lib",
42+
"java8": "java",
43+
"java11": "java",
44+
"java8.al2": "java",
45+
"java17": "java",
46+
"dotnet6": "dotnet",
47+
# User is responsible for creating subfolder in these workflows
48+
"makefile": "",
49+
}
50+
2751

2852
class UnsupportedRuntimeException(Exception):
2953
pass
@@ -84,34 +108,10 @@ def get_selector(
84108

85109

86110
def get_layer_subfolder(build_workflow: str) -> str:
87-
subfolders_by_runtime = {
88-
"python3.7": "python",
89-
"python3.8": "python",
90-
"python3.9": "python",
91-
"python3.10": "python",
92-
"python3.11": "python",
93-
"nodejs4.3": "nodejs",
94-
"nodejs6.10": "nodejs",
95-
"nodejs8.10": "nodejs",
96-
"nodejs12.x": "nodejs",
97-
"nodejs14.x": "nodejs",
98-
"nodejs16.x": "nodejs",
99-
"nodejs18.x": "nodejs",
100-
"ruby2.7": "ruby/lib",
101-
"ruby3.2": "ruby/lib",
102-
"java8": "java",
103-
"java11": "java",
104-
"java8.al2": "java",
105-
"java17": "java",
106-
"dotnet6": "dotnet",
107-
# User is responsible for creating subfolder in these workflows
108-
"makefile": "",
109-
}
110-
111-
if build_workflow not in subfolders_by_runtime:
111+
if build_workflow not in LAYER_SUBFOLDERS:
112112
raise UnsupportedRuntimeException("'{}' runtime is not supported for layers".format(build_workflow))
113113

114-
return subfolders_by_runtime[build_workflow]
114+
return LAYER_SUBFOLDERS[build_workflow]
115115

116116

117117
def get_workflow_config(

samcli/local/docker/container.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def create(self):
159159
"bind": self._working_dir,
160160
"mode": mount_mode,
161161
},
162-
**self._create_mapped_symlink_files(),
162+
**Container._create_mapped_symlink_files(self._host_dir, self._working_dir),
163163
}
164164

165165
kwargs = {
@@ -226,12 +226,20 @@ def create(self):
226226

227227
return self.id
228228

229-
def _create_mapped_symlink_files(self) -> Dict[str, Dict[str, str]]:
229+
@staticmethod
230+
def _create_mapped_symlink_files(search_directory: str, bind_directory: str) -> Dict[str, Dict[str, str]]:
230231
"""
231232
Resolves any top level symlinked files and folders that are found on the
232233
host directory and creates additional bind mounts to correctly map them
233234
inside of the container.
234235
236+
Parameters
237+
----------
238+
search_directory: str
239+
The folder to walk the root level of to search for symlinks
240+
bind_directory: str
241+
The folder inside of the container to resolve the symlink for
242+
235243
Returns
236244
-------
237245
Dict[str, Dict[str, str]]
@@ -241,13 +249,13 @@ def _create_mapped_symlink_files(self) -> Dict[str, Dict[str, str]]:
241249
mount_mode = "ro,delegated"
242250
additional_volumes = {}
243251

244-
with os.scandir(self._host_dir) as directory_iterator:
252+
with os.scandir(search_directory) as directory_iterator:
245253
for file in directory_iterator:
246254
if not file.is_symlink():
247255
continue
248256

249257
host_resolved_path = os.path.realpath(file.path)
250-
container_full_path = pathlib.Path(self._working_dir, file.name).as_posix()
258+
container_full_path = pathlib.Path(bind_directory, file.name).as_posix()
251259

252260
additional_volumes[host_resolved_path] = {
253261
"bind": container_full_path,

samcli/local/docker/lambda_container.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
Represents Lambda runtime containers.
33
"""
44
import logging
5-
from typing import List
5+
from pathlib import Path
6+
from typing import Dict, List
67

8+
from samcli.lib.build.workflow_config import LAYER_SUBFOLDERS
9+
from samcli.lib.providers.provider import LayerVersion
710
from samcli.lib.utils.packagetype import IMAGE
811
from samcli.local.docker.lambda_debug_settings import LambdaDebugSettings
912

@@ -99,6 +102,7 @@ def __init__(
99102
entry, container_env_vars = LambdaContainer._get_debug_settings(runtime, debug_options)
100103
additional_options = LambdaContainer._get_additional_options(runtime, debug_options)
101104
additional_volumes = LambdaContainer._get_additional_volumes(runtime, debug_options)
105+
layer_volume_mounts = LambdaContainer._get_layer_folder_mounts(layers)
102106

103107
_work_dir = self._WORKING_DIR
104108
_entrypoint = None
@@ -133,11 +137,60 @@ def __init__(
133137
entrypoint=_entrypoint if _entrypoint else entry,
134138
env_vars=env_vars,
135139
container_opts=additional_options,
136-
additional_volumes=additional_volumes,
140+
additional_volumes={**additional_volumes, **layer_volume_mounts},
137141
container_host=container_host,
138142
container_host_interface=container_host_interface,
139143
)
140144

145+
@staticmethod
146+
def _get_layer_folder_mounts(layers: List[LayerVersion]) -> Dict[str, Dict[str, str]]:
147+
"""
148+
Searches the code uri of the Layer to resolve any root level symlinks before
149+
the container is created
150+
151+
Paramters
152+
---------
153+
layers: List[LayerVersion]
154+
A list of layers to check for any symlinks
155+
156+
Returns
157+
-------
158+
Dict[str, Dict[str, str]]
159+
A dictonary representing the resolved file or directory and the bound path
160+
on the container
161+
"""
162+
layer_mappings: Dict[str, Dict[str, str]] = {}
163+
164+
for layer in layers:
165+
# layer.compatible_runtimes can return None
166+
for runtime in layer.compatible_runtimes or []:
167+
layer_folder = LAYER_SUBFOLDERS[runtime] if runtime in LAYER_SUBFOLDERS else None
168+
169+
# unsupported runtime for layers
170+
if not layer_folder:
171+
LOG.debug("Skipping symlink check for layer %s, unsupported runtime (%s)", layer.layer_id, runtime)
172+
continue
173+
174+
# not locally built layer
175+
if not layer.codeuri:
176+
LOG.debug(
177+
"Skipping symlink check for layer %s, layer does not have locally defined resources",
178+
layer.layer_id,
179+
)
180+
continue
181+
182+
# eg. `.aws-sam/build/MyLayer` + `nodejs`
183+
artifact_layer_path = Path(layer.codeuri, layer_folder)
184+
# eg. `/opt` + `nodejs`
185+
container_bind_path = Path(LambdaImage._LAYERS_DIR, layer_folder)
186+
187+
mappings = LambdaContainer._create_mapped_symlink_files(
188+
str(artifact_layer_path), str(container_bind_path)
189+
)
190+
layer_mappings.update(mappings)
191+
192+
return layer_mappings
193+
141194
@staticmethod
142195
def _get_exposed_ports(debug_options):
143196
"""

tests/unit/local/docker/test_container.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,7 @@ def test_no_symlinks_returns_empty(self, mock_scandir):
997997
mock_context.__enter__ = Mock(return_value=[self.mock_regular_file])
998998
mock_scandir.return_value = mock_context
999999

1000-
volumes = self.container._create_mapped_symlink_files()
1000+
volumes = Container._create_mapped_symlink_files("mock_host_dir", "mock_container_dir")
10011001

10021002
self.assertEqual(volumes, {})
10031003

@@ -1017,6 +1017,6 @@ def test_resolves_symlink(self, mock_path, mock_realpath, mock_scandir):
10171017
mock_context.__enter__ = Mock(return_value=[self.mock_symlinked_file])
10181018
mock_scandir.return_value = mock_context
10191019

1020-
volumes = self.container._create_mapped_symlink_files()
1020+
volumes = Container._create_mapped_symlink_files("mock_host_dir", "mock_container_dir")
10211021

10221022
self.assertEqual(volumes, {host_path: {"bind": container_path, "mode": ANY}})

tests/unit/local/docker/test_lambda_container.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from parameterized import parameterized, param
88

99
from samcli.commands.local.lib.debug_context import DebugContext
10+
from samcli.lib.build.workflow_config import LAYER_SUBFOLDERS
11+
from samcli.lib.providers.provider import LayerVersion
1012
from samcli.lib.utils.packagetype import IMAGE, ZIP
1113
from samcli.local.docker.lambda_container import LambdaContainer, Runtime
1214
from samcli.local.docker.lambda_debug_settings import DebuggingNotSupported
@@ -614,3 +616,44 @@ def test_additional_volumes_returns_volume_with_debugger_path_is_set(self, runti
614616
result = LambdaContainer._get_additional_volumes(runtime, debug_options)
615617
print(result)
616618
self.assertEqual(result, expected)
619+
620+
621+
class TestLambdaContainer_resolve_layers(TestCase):
622+
@parameterized.expand(
623+
[
624+
([],), # no layers
625+
([LayerVersion("a:b:c", codeuri=None, compatible_runtimes=["nodejs18.x"])],), # no codeuri
626+
([LayerVersion("a:b:c", codeuri="codeuri")],), # no runtime
627+
(
628+
[LayerVersion("a:b:c", codeuri="codeuri", compatible_runtimes=["hello world"])],
629+
), # unsupported/invalid runtime
630+
]
631+
)
632+
def test_returns_no_mounts_invalid_layer(self, layer):
633+
result = LambdaContainer._get_layer_folder_mounts(layer)
634+
635+
self.assertEqual(result, {})
636+
637+
@patch.object(LambdaContainer, "_create_mapped_symlink_files")
638+
def test_returns_no_mounts_no_links(self, create_map_mock):
639+
create_map_mock.return_value = {}
640+
641+
layer = LayerVersion("a:b:c", codeuri="some/path", compatible_runtimes=["nodejs18.x"])
642+
result = LambdaContainer._get_layer_folder_mounts([layer])
643+
644+
create_map_mock.assert_called_once()
645+
self.assertEqual(result, {})
646+
647+
@patch.object(LambdaContainer, "_create_mapped_symlink_files")
648+
def test_returns_mounts(self, create_map_mock):
649+
code_uri = "some/path"
650+
runtime = "nodejs18.x"
651+
layer_folder = "nodejs"
652+
653+
expected_local_path = f"{code_uri}/{layer_folder}"
654+
expected_container_path = f"/opt/{layer_folder}"
655+
656+
layer = LayerVersion("a:b:c", codeuri=code_uri, compatible_runtimes=[runtime])
657+
LambdaContainer._get_layer_folder_mounts([layer])
658+
659+
create_map_mock.assert_called_once_with(expected_local_path, expected_container_path)

0 commit comments

Comments
 (0)