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
2 changes: 1 addition & 1 deletion samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def run(self):
if self._create_auto_dependency_layer:
LOG.debug("Auto creating dependency layer for each function resource into a nested stack")
nested_stack_manager = NestedStackManager(
self._stack_name, self.build_dir, stack.location, modified_template, build_result
stack, self._stack_name, self.build_dir, modified_template, build_result
)
modified_template = nested_stack_manager.generate_auto_dependency_layer_stack()
move_template(stack.location, output_template_path, modified_template)
Expand Down
16 changes: 14 additions & 2 deletions samcli/lib/bootstrap/nested_stack/nested_stack_builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
StackBuilder implementation for nested stack
"""
import re
from typing import cast

from samcli.lib.bootstrap.stack_builder import AbstractStackBuilder
Expand All @@ -10,6 +11,7 @@

CREATED_BY_METADATA_KEY = "CreatedBy"
CREATED_BY_METADATA_VALUE = "AWS SAM CLI sync command"
NON_ALPHANUM_REGEX = re.compile(r"\W+")


class NestedStackBuilder(AbstractStackBuilder):
Expand Down Expand Up @@ -43,14 +45,16 @@ def add_function(
@staticmethod
def get_layer_logical_id(function_logical_id: str) -> str:
function_logical_id_hash = str_checksum(function_logical_id)
return f"{function_logical_id[:48]}{function_logical_id_hash[:8]}DepLayer"
sanitized_function_logical_id = NestedStackBuilder._get_logical_id_compliant_str(function_logical_id)
return f"{sanitized_function_logical_id[:48]}{function_logical_id_hash[:8]}DepLayer"

@staticmethod
def get_layer_name(stack_name: str, function_logical_id: str) -> str:
function_logical_id_hash = str_checksum(function_logical_id)
sanitized_function_logical_id = NestedStackBuilder._get_logical_id_compliant_str(function_logical_id)
stack_name_hash = str_checksum(stack_name)
return (
f"{stack_name[:16]}{stack_name_hash[:8]}-{function_logical_id[:22]}{function_logical_id_hash[:8]}"
f"{stack_name[:16]}{stack_name_hash[:8]}-{sanitized_function_logical_id[:22]}{function_logical_id_hash[:8]}"
f"-DepLayer"
)

Expand All @@ -76,3 +80,11 @@ def get_nested_stack_reference_resource(nested_template_location):
"Properties": {"TemplateURL": nested_template_location},
"Metadata": {CREATED_BY_METADATA_KEY: CREATED_BY_METADATA_VALUE},
}

@staticmethod
def _get_logical_id_compliant_str(function_logical_id: str):
"""
Removes all non-alphanumeric chars to make it usable for resource name definition
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html
"""
return NON_ALPHANUM_REGEX.sub("", function_logical_id)
29 changes: 19 additions & 10 deletions samcli/lib/bootstrap/nested_stack/nested_stack_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

class NestedStackManager:

_stack: Stack
_stack_name: str
_build_dir: str
_stack_location: str
Expand All @@ -43,32 +44,32 @@ class NestedStackManager:

def __init__(
self,
stack: Stack,
stack_name: str,
build_dir: str,
stack_location: str,
current_template: Dict,
app_build_result: ApplicationBuildResult,
stack_metadata: Optional[Dict] = None,
):
"""
Parameters
----------
stack: Stack
Stack that it is currently been processed
stack_name : str
Original stack name, which is used to generate layer name
build_dir : str
Build directory for storing the new nested stack template
stack_location : str
Used to move template and its resources' relative path information
current_template : Dict
Current template of the project
app_build_result: ApplicationBuildResult
Application build result, which contains build graph, and built artifacts information
stack_metadata: Optional[Dict]
The nested stack resource metadata values.
"""
self._stack = stack
self._stack_name = stack_name
self._build_dir = build_dir
self._stack_location = stack_location
self._current_template = current_template
self._app_build_result = app_build_result
self._nested_stack_builder = NestedStackBuilder()
Expand All @@ -83,8 +84,7 @@ def generate_auto_dependency_layer_stack(self) -> Dict:
template = deepcopy(self._current_template)
resources = template.get("Resources", {})

stack = Stack("", "", self._stack_location, {}, template_dict=template, metadata=self._stack_metadata)
function_provider = SamFunctionProvider([stack], ignore_code_extraction_warnings=True)
function_provider = SamFunctionProvider([self._stack], ignore_code_extraction_warnings=True)
zip_functions = [function for function in function_provider.get_all() if function.packagetype == ZIP]

for zip_function in zip_functions:
Expand All @@ -105,18 +105,21 @@ def generate_auto_dependency_layer_stack(self) -> Dict:
LOG.debug("No function has been added for auto dependency layer creation")
return template

nested_template_location = os.path.join(self._build_dir, "nested_template.yaml")
move_template(self._stack_location, nested_template_location, self._nested_stack_builder.build_as_dict())
nested_template_location = str(self._get_template_folder().joinpath("adl_nested_template.yaml"))
move_template(self._stack.location, nested_template_location, self._nested_stack_builder.build_as_dict())

resources[NESTED_STACK_NAME] = self._nested_stack_builder.get_nested_stack_reference_resource(
nested_template_location
)
return template

def _get_template_folder(self) -> Path:
return Path(self._stack.get_output_template_path(self._build_dir)).parent

def _add_layer(self, dependencies_dir: str, function: Function, resources: Dict):
layer_logical_id = NestedStackBuilder.get_layer_logical_id(function.full_path)
layer_location = self.update_layer_folder(
self._build_dir, dependencies_dir, layer_logical_id, function.full_path, function.runtime
str(self._get_template_folder()), dependencies_dir, layer_logical_id, function.full_path, function.runtime
)

layer_output_key = self._nested_stack_builder.add_function(self._stack_name, layer_location, function)
Expand Down Expand Up @@ -195,4 +198,10 @@ def _get_dependencies_dir(self, function_full_path: str) -> Optional[str]:
function_full_path
)

return function_build_definition.dependencies_dir if function_build_definition else None
if not function_build_definition or not function_build_definition.dependencies_dir:
return None

if not os.path.isdir(function_build_definition.dependencies_dir):
return None

return function_build_definition.dependencies_dir
34 changes: 31 additions & 3 deletions tests/unit/commands/buildcmd/test_build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,8 @@ def test_cached_dir_and_deps_dir_creation(


class TestBuildContext_run(TestCase):
@parameterized.expand([(True,), (False,)])
@patch("samcli.commands.build.build_context.NestedStackManager")
@patch("samcli.commands.build.build_context.SamLocalStackProvider.get_stacks")
@patch("samcli.commands.build.build_context.SamApiProvider")
@patch("samcli.commands.build.build_context.SamFunctionProvider")
Expand All @@ -797,6 +799,7 @@ class TestBuildContext_run(TestCase):
@patch("samcli.commands.build.build_context.os")
def test_run_build_context(
self,
auto_dependency_layer,
os_mock,
get_template_data_mock,
move_template_mock,
Expand All @@ -809,11 +812,10 @@ def test_run_build_context(
SamFunctionProviderMock,
SamApiProviderMock,
get_buildable_stacks_mock,
nested_stack_manager_mock,
):

root_stack = Mock()
root_stack.is_root_stack = True
auto_dependency_layer = False
root_stack.get_output_template_path = Mock(return_value="./build_dir/template.yaml")
child_stack = Mock()
child_stack.get_output_template_path = Mock(return_value="./build_dir/abcd/template.yaml")
Expand All @@ -825,7 +827,7 @@ def test_run_build_context(

builder_mock = ApplicationBuilderMock.return_value = Mock()
artifacts = "artifacts"
builder_mock.build.return_value = ApplicationBuildResult(Mock(), artifacts)
application_build_result = builder_mock.build.return_value = ApplicationBuildResult(Mock(), artifacts)
modified_template_root = "modified template 1"
modified_template_child = "modified template 2"
builder_mock.update_template.side_effect = [modified_template_root, modified_template_child]
Expand All @@ -843,6 +845,13 @@ def test_run_build_context(
container_mgr_mock = ContainerManagerMock.return_value = Mock()
build_dir_mock.return_value = "build_dir"

given_nested_stack_manager = Mock()
given_nested_stack_manager.generate_auto_dependency_layer_stack.side_effect = [
modified_template_root,
modified_template_child,
]
nested_stack_manager_mock.return_value = given_nested_stack_manager

with BuildContext(
resource_identifier="function_identifier",
template_file="template_file",
Expand Down Expand Up @@ -913,6 +922,25 @@ def test_run_build_context(
]
)

if auto_dependency_layer:
nested_stack_manager_mock.assert_has_calls(
[
call(
root_stack, None, build_context.build_dir, modified_template_root, application_build_result
),
call(
child_stack,
None,
build_context.build_dir,
modified_template_child,
application_build_result,
),
],
any_order=True,
)
# assert that nested stack manager is called by both root stack and child stack
given_nested_stack_manager.generate_auto_dependency_layer_stack.assert_has_calls([call(), call()])

@parameterized.expand(
[
(UnsupportedRuntimeException(), "UnsupportedRuntimeException"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,15 @@ def test_get_layer_name(self):
self.assertTrue(layer_name.endswith("DepLayer"))
self.assertIn(function_logical_id[:22], layer_name)
self.assertLessEqual(len(layer_name), 64)

def test_layer_logical_id_should_be_different(self):
self.assertNotEqual(
NestedStackBuilder.get_layer_logical_id("ChildApp/HelloWorldFunction"),
NestedStackBuilder.get_layer_logical_id("ChildAppHelloWorldFunction"),
)

def test_layer_name_should_be_different(self):
self.assertNotEqual(
NestedStackBuilder.get_layer_name("my-app", "ChildApp/HelloWorldFunction"),
NestedStackBuilder.get_layer_name("my-app", "ChildAppHelloWorldFunction"),
)
Loading