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
4 changes: 4 additions & 0 deletions samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,10 @@ def manifest_path_override(self) -> Optional[str]:
def mode(self) -> Optional[str]:
return self._mode

@property
def use_base_dir(self) -> bool:
return self._use_raw_codeuri

@property
def resources_to_build(self) -> ResourcesToBuildCollector:
"""
Expand Down
12 changes: 6 additions & 6 deletions samcli/lib/providers/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,22 +603,22 @@ def get_parent_stack(child_stack: "Stack", stacks: List["Stack"]) -> Optional["S
return None

@staticmethod
def get_stack_by_logical_id(logical_id: str, stacks: List["Stack"]) -> Optional["Stack"]:
def get_stack_by_full_path(full_path: str, stacks: List["Stack"]) -> Optional["Stack"]:
"""
Return the stack with given logical id
Return the stack with given full path
Parameters
----------
logical_id str
logical_id of the stack
full_path str
full path of the stack like ChildStack/ChildChildStack
stacks : List[Stack]
a list of stack for searching
Returns
-------
Stack
The stack with the given logical id
The stack with the given full path
"""
for stack in stacks:
if stack.name == logical_id:
if stack.stack_path == full_path:
return stack
return None

Expand Down
4 changes: 3 additions & 1 deletion samcli/lib/providers/sam_function_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,9 @@ def _locate_layer_from_nested( # pylint: disable=too-many-return-statements
LOG.debug("Search layer %s in child stack", layer_reference)

child_stacks = Stack.get_child_stacks(stack, stacks)
child_stack = Stack.get_stack_by_logical_id(layer_stack_reference, child_stacks)
stack_prefix = stack.stack_path + "/" if stack.stack_path else ""
stack_path = stack_prefix + layer_stack_reference
child_stack = Stack.get_stack_by_full_path(stack_path, child_stacks)
if not child_stack:
LOG.debug("Child stack not found, layer can not be located in templates")
return None
Expand Down
24 changes: 13 additions & 11 deletions samcli/lib/sync/flows/generic_api_sync_flow.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""SyncFlow interface for HttpApi and RestApi"""
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, TYPE_CHECKING, cast
from typing import Any, Dict, List, Optional, TYPE_CHECKING

from samcli.lib.sync.sync_flow import SyncFlow, ResourceAPICall
from samcli.lib.sync.sync_flow import SyncFlow, ResourceAPICall, get_definition_path
from samcli.lib.providers.provider import Stack, get_resource_by_id, ResourceIdentifier

# BuildContext and DeployContext will only be imported for type checking to improve performance
Expand All @@ -20,7 +20,7 @@ class GenericApiSyncFlow(SyncFlow):

_api_client: Any
_api_identifier: str
_definition_uri: Optional[str]
_definition_uri: Optional[Path]
_stacks: List[Stack]
_swagger_body: Optional[bytes]

Expand Down Expand Up @@ -65,19 +65,21 @@ def gather_resources(self) -> None:
def _process_definition_file(self) -> Optional[bytes]:
if self._definition_uri is None:
return None
with open(self._definition_uri, "rb") as swagger_file:
with open(str(self._definition_uri), "rb") as swagger_file:
swagger_body = swagger_file.read()
return swagger_body

def _get_definition_file(self, api_identifier: str) -> Optional[str]:
def _get_definition_file(self, api_identifier: str) -> Optional[Path]:
api_resource = get_resource_by_id(self._stacks, ResourceIdentifier(api_identifier))
if api_resource is None:
if not api_resource:
return None
properties = api_resource.get("Properties", {})
definition_file = properties.get("DefinitionUri")
if self._build_context.base_dir and definition_file:
definition_file = str(Path(self._build_context.base_dir).joinpath(definition_file))
return cast(Optional[str], definition_file)
return get_definition_path(
api_resource,
self._api_identifier,
self._build_context.use_base_dir,
self._build_context.base_dir,
self._stacks,
)

def compare_remote(self) -> bool:
return False
Expand Down
24 changes: 13 additions & 11 deletions samcli/lib/sync/flows/stepfunctions_sync_flow.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Base SyncFlow for StepFunctions"""
import logging
from pathlib import Path
from typing import Any, Dict, List, TYPE_CHECKING, cast, Optional
from typing import Any, Dict, List, TYPE_CHECKING, Optional


from samcli.lib.providers.provider import Stack, get_resource_by_id, ResourceIdentifier
from samcli.lib.sync.sync_flow import SyncFlow, ResourceAPICall
from samcli.lib.sync.sync_flow import SyncFlow, ResourceAPICall, get_definition_path
from samcli.lib.sync.exceptions import InfraSyncRequiredError
from samcli.lib.providers.exceptions import MissingLocalDefinition

Expand All @@ -21,7 +21,7 @@ class StepFunctionsSyncFlow(SyncFlow):
_state_machine_identifier: str
_stepfunctions_client: Any
_stacks: List[Stack]
_definition_uri: Optional[str]
_definition_uri: Optional[Path]
_states_definition: Optional[str]

def __init__(
Expand Down Expand Up @@ -75,18 +75,20 @@ def gather_resources(self) -> None:
def _process_definition_file(self) -> Optional[str]:
if self._definition_uri is None:
return None
with open(self._definition_uri, "r", encoding="utf-8") as states_file:
with open(str(self._definition_uri), "r", encoding="utf-8") as states_file:
states_data = states_file.read()
return states_data

def _get_definition_file(self, state_machine_identifier: str) -> Optional[str]:
if self._resource is None:
def _get_definition_file(self, state_machine_identifier: str) -> Optional[Path]:
if not self._resource:
return None
properties = self._resource.get("Properties", {})
definition_file = properties.get("DefinitionUri")
if self._build_context.base_dir:
definition_file = str(Path(self._build_context.base_dir).joinpath(definition_file))
return cast(Optional[str], definition_file)
return get_definition_path(
self._resource,
state_machine_identifier,
self._build_context.use_base_dir,
self._build_context.base_dir,
self._stacks,
)

def compare_remote(self) -> bool:
# Not comparing with remote right now, instead only making update api calls
Expand Down
38 changes: 38 additions & 0 deletions samcli/lib/sync/sync_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from abc import ABC, abstractmethod
from enum import Enum
from pathlib import Path
from threading import Lock
from typing import Any, Dict, List, NamedTuple, Optional, TYPE_CHECKING, cast, Set
from boto3.session import Session
Expand Down Expand Up @@ -315,3 +316,40 @@ def execute(self) -> List["SyncFlow"]:
dependencies = self.gather_dependencies()
LOG.debug("%sFinished", self.log_prefix)
return dependencies


def get_definition_path(
resource: Dict, identifier: str, use_base_dir: bool, base_dir: str, stacks: List[Stack]
) -> Optional[Path]:
"""
A helper method used by non-function sync flows to resolve definition file path
that are relative to the child stack to absolute path for nested stacks

Parameters
-------
resource: Dict
The resource's template dict
identifier: str
The logical ID identifier of the resource
use_base_dir: bool
Whether or not the base_dir option was used
base_dir: str
Base directory if provided, otherwise the root template directory
stacks: List[Stack]
The list of stacks for the application

Returns
-------
Optional[Path]
A resolved absolute path for the definition file
"""
properties = resource.get("Properties", {})
definition_file = properties.get("DefinitionUri")
definition_path = None
if definition_file:
definition_path = Path(base_dir).joinpath(definition_file)
if not use_base_dir:
child_stack = Stack.get_stack_by_full_path(ResourceIdentifier(identifier).stack_path, stacks)
if child_stack:
definition_path = Path(child_stack.location).parent.joinpath(definition_file)
return definition_path
2 changes: 0 additions & 2 deletions tests/integration/sync/test_sync_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,6 @@ def test_sync_nested_function_layer_race_condition(self):
self.assertIn("extra_message", lambda_response)
self.assertEqual(lambda_response.get("message"), "10")

@pytest.mark.skip(reason="Currently not properly supported")
def test_sync_code_nested_rest_api(self):
shutil.rmtree(
TestSyncCodeBase.temp_dir.joinpath("child_stack").joinpath("child_child_stack").joinpath("apigateway"),
Expand Down Expand Up @@ -447,7 +446,6 @@ def test_sync_code_nested_rest_api(self):
rest_api = self.stack_resources.get(AWS_APIGATEWAY_RESTAPI)[0]
self.assertEqual(self._get_api_message(rest_api), '{"message": "hello 2"}')

@pytest.mark.skip(reason="Currently not properly supported")
def test_sync_code_nested_state_machine(self):
shutil.rmtree(
TestSyncCodeBase.temp_dir.joinpath("child_stack").joinpath("child_child_stack").joinpath("statemachine"),
Expand Down
1 change: 1 addition & 0 deletions tests/unit/commands/buildcmd/test_build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ def test_must_return_many_functions_to_build(
self.assertEqual(context.stacks, [stack])
self.assertEqual(context.manifest_path_override, os.path.abspath("manifest_path"))
self.assertEqual(context.mode, "buildmode")
self.assertFalse(context.use_base_dir)
self.assertFalse(context.is_building_specific_resource)
resources_to_build = context.resources_to_build
self.assertEqual(resources_to_build.functions, [func1, func2, func6])
Expand Down
31 changes: 22 additions & 9 deletions tests/unit/commands/local/lib/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,26 +802,39 @@ def test_get_resource_full_path_by_id(self, resource_id, expected_full_path):


class TestGetStack(TestCase):
root_stack = Stack("", "", "template.yaml", None, {})
child_stack = Stack("", "child", "template.yaml", None, {})
root_stack = Stack("", "Root", "template.yaml", None, {})
child_stack = Stack("Root", "Child", "root_stack/template.yaml", None, {})
child_child_stack = Stack("Root/Child", "ChildChild", "root_stack/child_stack/template.yaml", None, {})

def test_get_parent_stack(self):
stack = Stack.get_parent_stack(self.child_stack, [self.root_stack, self.child_stack])
stack = Stack.get_parent_stack(self.child_stack, [self.root_stack, self.child_stack, self.child_child_stack])
self.assertEqual(stack, self.root_stack)

stack = Stack.get_parent_stack(self.root_stack, [self.root_stack, self.child_stack])
stack = Stack.get_parent_stack(self.root_stack, [self.root_stack, self.child_stack, self.child_child_stack])
self.assertIsNone(stack)

def test_get_stack_by_logical_id(self):
stack = Stack.get_stack_by_logical_id("child", [self.root_stack, self.child_stack])
def test_get_stack_by_full_path(self):
stack = Stack.get_stack_by_full_path("Root/Child", [self.root_stack, self.child_stack, self.child_child_stack])
self.assertEqual(stack, self.child_stack)

stack = Stack.get_stack_by_logical_id("not_exist", [self.root_stack, self.child_stack])
stack = Stack.get_stack_by_full_path("Root", [self.root_stack, self.child_stack, self.child_child_stack])
self.assertEqual(stack, self.root_stack)

stack = Stack.get_stack_by_full_path("Child/Child", [self.root_stack, self.child_stack, self.child_child_stack])
self.assertIsNone(stack)

def test_get_child_stacks(self):
stack_list = Stack.get_child_stacks(self.root_stack, [self.root_stack, self.child_stack])
stack_list = Stack.get_child_stacks(
self.root_stack, [self.root_stack, self.child_stack, self.child_child_stack]
)
self.assertEqual(stack_list, [self.child_stack])

stack_list = Stack.get_child_stacks(self.child_stack, [self.root_stack, self.child_stack])
stack_list = Stack.get_child_stacks(
self.child_stack, [self.root_stack, self.child_stack, self.child_child_stack]
)
self.assertEqual(stack_list, [self.child_child_stack])

stack_list = Stack.get_child_stacks(
self.child_child_stack, [self.root_stack, self.child_stack, self.child_child_stack]
)
self.assertEqual(stack_list, [])
43 changes: 32 additions & 11 deletions tests/unit/lib/sync/flows/test_http_api_sync_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest import TestCase
from unittest.mock import MagicMock, mock_open, patch
from pathlib import Path
from samcli.lib.providers.provider import Stack

from samcli.lib.sync.flows.http_api_sync_flow import HttpApiSyncFlow
from samcli.lib.providers.exceptions import MissingLocalDefinition
Expand Down Expand Up @@ -45,31 +46,51 @@ def test_sync_direct(self, session_mock):
sync_flow._api_client.reimport_api.assert_called_once_with(ApiId="PhysicalApi1", Body='{"key": "value"}')

@patch("samcli.lib.sync.flows.generic_api_sync_flow.get_resource_by_id")
@patch("samcli.lib.sync.flows.generic_api_sync_flow.Path.joinpath")
def test_get_definition_file(self, join_path_mock, get_resource_mock):
@patch("samcli.lib.sync.flows.generic_api_sync_flow.get_definition_path")
def test_get_definition_file(self, get_path_mock, get_resource_mock):
sync_flow = self.create_sync_flow()

sync_flow._build_context.base_dir = None
join_path_mock.return_value = "test_uri"
sync_flow._build_context.use_base_dir = False
sync_flow._build_context.base_dir = "base_dir"
get_resource_mock.return_value = {"Properties": {"DefinitionUri": "test_uri"}}
result_uri = sync_flow._get_definition_file("test")
get_path_mock.return_value = Path("base_dir").joinpath("test_uri")

result_uri = sync_flow._get_definition_file(sync_flow._api_identifier)

self.assertEqual(result_uri, "test_uri")
get_path_mock.assert_called_with(
{"Properties": {"DefinitionUri": "test_uri"}},
sync_flow._api_identifier,
False,
"base_dir",
sync_flow._stacks,
)
self.assertEqual(result_uri, Path("base_dir").joinpath("test_uri"))

get_resource_mock.return_value = {"Properties": {}}
result_uri = sync_flow._get_definition_file("test")
get_resource_mock.return_value = {}
result_uri = sync_flow._get_definition_file(sync_flow._api_identifier)

self.assertEqual(result_uri, None)

@patch("samcli.lib.sync.flows.generic_api_sync_flow.get_resource_by_id")
def test_get_definition_file_with_base_dir(self, get_resource_mock):
@patch("samcli.lib.sync.flows.generic_api_sync_flow.get_definition_path")
def test_get_definition_file_with_base_dir(self, get_path_mock, get_resource_mock):
sync_flow = self.create_sync_flow()

sync_flow._build_context.use_base_dir = True
sync_flow._build_context.base_dir = "base_dir"
get_resource_mock.return_value = {"Properties": {"DefinitionUri": "test_uri"}}
result_uri = sync_flow._get_definition_file("test")
get_path_mock.return_value = Path("base_dir").joinpath("test_uri")

self.assertEqual(result_uri, str(Path("base_dir").joinpath("test_uri")))
result_uri = sync_flow._get_definition_file(sync_flow._api_identifier)

get_path_mock.assert_called_with(
{"Properties": {"DefinitionUri": "test_uri"}},
sync_flow._api_identifier,
True,
"base_dir",
sync_flow._stacks,
)
self.assertEqual(result_uri, Path("base_dir").joinpath("test_uri"))

def test_process_definition_file(self):
sync_flow = self.create_sync_flow()
Expand Down
Loading