Skip to content

Commit c100003

Browse files
committed
refactor: revert function filtering for sam local start-api
Remove function name filtering functionality from 'sam local start-api' command while keeping it for 'sam local start-lambda'. This change: - Removes function_logical_ids parameter from start-api CLI - Removes function filtering logic from ApiProvider - Removes function_logical_ids parameter from LocalApiService - Removes all related integration and unit tests for start-api - Updates samconfig tests to match new signature The function filtering feature remains fully functional for 'sam local start-lambda' command.
1 parent 5a58fd7 commit c100003

8 files changed

Lines changed: 5 additions & 388 deletions

File tree

samcli/commands/local/lib/local_api_service.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,7 @@ def __init__(self, lambda_invoke_context, port, host, static_dir, disable_author
4040
self.cwd = lambda_invoke_context.get_cwd()
4141
self.disable_authorizer = disable_authorizer
4242
self.api_provider = ApiProvider(
43-
lambda_invoke_context.stacks,
44-
cwd=self.cwd,
45-
disable_authorizer=disable_authorizer,
46-
function_logical_ids=lambda_invoke_context.function_logical_ids,
43+
lambda_invoke_context.stacks, cwd=self.cwd, disable_authorizer=disable_authorizer
4744
)
4845
self.lambda_runner = lambda_invoke_context.local_lambda_runner
4946
self.stderr_stream = lambda_invoke_context.stderr

samcli/commands/local/start_api/cli.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
requires_credentials=False,
6161
context_settings={"max_content_width": 120},
6262
)
63-
@click.argument("function_logical_ids", nargs=-1, required=False)
6463
@configuration_option(provider=ConfigProvider(section="parameters"))
6564
@terraform_plan_file_option
6665
@hook_name_click_option(
@@ -108,7 +107,6 @@
108107
@print_cmdline_args
109108
def cli(
110109
ctx,
111-
function_logical_ids,
112110
# start-api Specific Options
113111
host,
114112
port,
@@ -152,7 +150,6 @@ def cli(
152150

153151
do_cli(
154152
ctx,
155-
function_logical_ids,
156153
host,
157154
port,
158155
disable_authorizer,
@@ -186,7 +183,6 @@ def cli(
186183

187184
def do_cli( # pylint: disable=R0914
188185
ctx,
189-
function_logical_ids,
190186
host,
191187
port,
192188
disable_authorizer,
@@ -260,7 +256,6 @@ def do_cli( # pylint: disable=R0914
260256
container_host_interface=container_host_interface,
261257
invoke_images=processed_invoke_images,
262258
add_host=add_host,
263-
function_logical_ids=function_logical_ids,
264259
no_mem_limit=no_mem_limit,
265260
) as invoke_context:
266261
ssl_context = (ssl_cert_file, ssl_key_file) if ssl_cert_file else None

samcli/lib/providers/api_provider.py

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
"""Class that provides the Api with a list of routes from a Template"""
22

33
import logging
4-
from typing import Iterator, List, Optional, Tuple
4+
from typing import Iterator, List, Optional
55

66
from samcli.lib.providers.api_collector import ApiCollector
77
from samcli.lib.providers.cfn_api_provider import CfnApiProvider
88
from samcli.lib.providers.cfn_base_api_provider import CfnBaseApiProvider
99
from samcli.lib.providers.provider import AbstractApiProvider, Api, Stack
1010
from samcli.lib.providers.sam_api_provider import SamApiProvider
11-
from samcli.local.apigw.route import Route
1211

1312
LOG = logging.getLogger(__name__)
1413

1514

1615
class ApiProvider(AbstractApiProvider):
17-
def __init__(
18-
self,
19-
stacks: List[Stack],
20-
cwd: Optional[str] = None,
21-
disable_authorizer: Optional[bool] = False,
22-
function_logical_ids: Optional[Tuple[str, ...]] = None,
23-
):
16+
def __init__(self, stacks: List[Stack], cwd: Optional[str] = None, disable_authorizer: Optional[bool] = False):
2417
"""
2518
Initialize the class with template data. The template_dict is assumed
2619
to be valid, normalized and a dictionary. template_dict should be normalized by running any and all
@@ -38,21 +31,13 @@ def __init__(
3831
Optional working directory with respect to which we will resolve relative path to Swagger file
3932
disable_authorizer : Optional[bool]
4033
Optional flag to disable the collection of lambda authorizers
41-
function_logical_ids : Optional[Tuple[str, ...]]
42-
Optional tuple of function logical IDs to filter API routes by
4334
"""
4435
self.stacks = stacks
4536

4637
# Store a set of apis
4738
self.cwd = cwd
4839
self.disable_authorizer = disable_authorizer
49-
self._function_logical_ids = function_logical_ids
5040
self.api = self._extract_api()
51-
52-
# Filter routes if function names provided
53-
if function_logical_ids:
54-
self.api = self._filter_api_routes(self.api, function_logical_ids)
55-
5641
self.routes = self.api.routes
5742
LOG.debug("%d APIs found in the template", len(self.routes))
5843

@@ -65,50 +50,6 @@ def get_all(self) -> Iterator[Api]:
6550

6651
yield self.api
6752

68-
def _filter_api_routes(self, api: Api, function_logical_ids: Tuple[str, ...]) -> Api:
69-
"""
70-
Filters API routes to only include those backed by specified functions.
71-
Logs warnings for functions not referenced by any routes.
72-
73-
Parameters
74-
----------
75-
api : Api
76-
The Api object containing all routes
77-
function_logical_ids : Tuple[str, ...]
78-
Tuple of function logical IDs to filter by
79-
80-
Returns
81-
-------
82-
Api
83-
The same Api object with filtered routes
84-
"""
85-
function_logical_ids_set = set(function_logical_ids)
86-
87-
# Only filter if routes is a list (not a set of strings)
88-
if not isinstance(api.routes, list):
89-
return api
90-
91-
functions_with_routes = {
92-
route.function_name
93-
for route in api.routes
94-
if isinstance(route, Route) and route.function_name in function_logical_ids_set
95-
}
96-
api.routes = [
97-
route
98-
for route in api.routes
99-
if isinstance(route, Route) and route.function_name in function_logical_ids_set
100-
]
101-
102-
# Warn about functions without routes
103-
functions_without_routes = function_logical_ids_set - functions_with_routes
104-
if functions_without_routes:
105-
LOG.warning(
106-
"The following functions are not referenced by any API routes: %s",
107-
", ".join(sorted(functions_without_routes)),
108-
)
109-
110-
return api
111-
11253
def _extract_api(self) -> Api:
11354
"""
11455
Extracts all the routes by running through the one providers. The provider that has the first type matched

tests/integration/local/start_api/test_start_api.py

Lines changed: 1 addition & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from unittest import skipIf
66
import uuid
77
import random
8-
import logging
98
from pathlib import Path
109
from typing import Dict
1110

@@ -22,12 +21,10 @@
2221
from samcli.commands.local.cli_common.options import get_default_layer_cache_dir
2322
from samcli.local.apigw.route import Route
2423
from samcli.local.docker.utils import get_validated_container_client
25-
from tests.testing_utils import IS_WINDOWS, get_sam_command
24+
from tests.testing_utils import IS_WINDOWS
2625
from .start_api_integ_base import StartApiIntegBaseClass, WritableStartApiIntegBaseClass
2726
from ..invoke.layer_utils import LayerUtils
2827

29-
LOG = logging.getLogger(__name__)
30-
3128

3229
@parameterized_class(
3330
("template_path",),
@@ -3443,165 +3440,3 @@ def test_starts_process_successfully(self):
34433440

34443441
self.assertEqual(response.status_code, 200)
34453442
self.assertEqual(response.json(), {"hello": "world"})
3446-
3447-
3448-
class TestFunctionNameFilteringWithFilter(StartApiIntegBaseClass):
3449-
"""Test function name filtering with specific functions"""
3450-
3451-
template_path = "/testdata/start_api/template.yaml"
3452-
3453-
@classmethod
3454-
def setUpClass(cls):
3455-
cls.command_list = [
3456-
get_sam_command(),
3457-
"local",
3458-
"start-api",
3459-
"HelloWorldFunction",
3460-
"EchoEventFunction",
3461-
"-t",
3462-
cls.integration_dir + cls.template_path,
3463-
]
3464-
super().setUpClass()
3465-
3466-
def setUp(self):
3467-
self.url = f"http://127.0.0.1:{self.port}"
3468-
3469-
@pytest.mark.flaky(reruns=3)
3470-
@pytest.mark.timeout(timeout=600, method="thread")
3471-
def test_accessing_filtered_route(self):
3472-
"""Test accessing a route backed by a filtered function"""
3473-
response = requests.get(self.url + "/anyandall", timeout=300)
3474-
self.assertEqual(response.status_code, 200)
3475-
self.assertEqual(response.json(), {"hello": "world"})
3476-
3477-
@pytest.mark.flaky(reruns=3)
3478-
@pytest.mark.timeout(timeout=600, method="thread")
3479-
def test_accessing_filtered_route_post(self):
3480-
"""Test accessing another filtered route"""
3481-
response = requests.post(self.url + "/echoeventbody", json={}, timeout=300)
3482-
self.assertEqual(response.status_code, 200)
3483-
3484-
@pytest.mark.flaky(reruns=3)
3485-
@pytest.mark.timeout(timeout=600, method="thread")
3486-
def test_accessing_non_filtered_route(self):
3487-
"""Test accessing a route backed by non-filtered function returns 404"""
3488-
response = requests.get(self.url + "/onlysetbody", timeout=300)
3489-
self.assertIn(response.status_code, [403, 404])
3490-
3491-
3492-
class TestFunctionNameFilteringMultipleRoutes(StartApiIntegBaseClass):
3493-
"""Test that filtering works correctly when functions have multiple routes"""
3494-
3495-
template_path = "/testdata/start_api/template.yaml"
3496-
3497-
@classmethod
3498-
def setUpClass(cls):
3499-
cls.command_list = [
3500-
get_sam_command(),
3501-
"local",
3502-
"start-api",
3503-
"HelloWorldFunction",
3504-
"-t",
3505-
cls.integration_dir + cls.template_path,
3506-
]
3507-
super().setUpClass()
3508-
3509-
def setUp(self):
3510-
self.url = f"http://127.0.0.1:{self.port}"
3511-
3512-
@pytest.mark.flaky(reruns=3)
3513-
@pytest.mark.timeout(timeout=600, method="thread")
3514-
def test_all_routes_for_filtered_function_available(self):
3515-
"""Test that all routes for a filtered function are available"""
3516-
self.assertEqual(requests.get(self.url + "/anyandall", timeout=300).status_code, 200)
3517-
self.assertEqual(requests.post(self.url + "/id", json={}, timeout=300).status_code, 200)
3518-
self.assertEqual(requests.get(self.url + "/proxypath/some/path", timeout=300).status_code, 200)
3519-
3520-
3521-
class TestFunctionNameFilteringWarmContainersEager(StartApiIntegBaseClass):
3522-
"""Test function filtering with EAGER warm containers"""
3523-
3524-
template_path = "/testdata/start_api/template-warm-containers.yaml"
3525-
container_mode = ContainersInitializationMode.EAGER.value
3526-
mode_env_variable = str(uuid.uuid4())
3527-
parameter_overrides = {"ModeEnvVariable": mode_env_variable}
3528-
3529-
@classmethod
3530-
def setUpClass(cls):
3531-
cls.command_list = [
3532-
get_sam_command(),
3533-
"local",
3534-
"start-api",
3535-
"HelloWorldFunction",
3536-
"-t",
3537-
cls.integration_dir + cls.template_path,
3538-
"--warm-containers",
3539-
cls.container_mode,
3540-
"--parameter-overrides",
3541-
cls._make_parameter_override_arg(cls.parameter_overrides),
3542-
]
3543-
super().setUpClass()
3544-
3545-
def setUp(self):
3546-
self.url = f"http://127.0.0.1:{self.port}"
3547-
3548-
@pytest.mark.flaky(reruns=3)
3549-
@pytest.mark.timeout(timeout=600, method="thread")
3550-
def test_only_filtered_function_has_container(self):
3551-
"""Test that only the filtered function has a pre-warmed container in EAGER mode"""
3552-
self.assertEqual(requests.get(self.url + "/hello", timeout=300).status_code, 200)
3553-
sleep(2)
3554-
3555-
containers = self.docker_client.containers.list(all=True, filters={"label": "sam.cli.container.type=lambda"})
3556-
initiated_containers = sum(
3557-
1
3558-
for c in containers
3559-
if any(self.mode_env_variable in env for env in c.attrs.get("Config", {}).get("Env", []))
3560-
)
3561-
self.assertEqual(initiated_containers, 1)
3562-
3563-
3564-
class TestFunctionNameFilteringWarmContainersLazy(StartApiIntegBaseClass):
3565-
"""Test function filtering with LAZY warm containers"""
3566-
3567-
template_path = "/testdata/start_api/template-warm-containers.yaml"
3568-
container_mode = ContainersInitializationMode.LAZY.value
3569-
mode_env_variable = str(uuid.uuid4())
3570-
parameter_overrides = {"ModeEnvVariable": mode_env_variable}
3571-
3572-
@classmethod
3573-
def setUpClass(cls):
3574-
cls.command_list = [
3575-
get_sam_command(),
3576-
"local",
3577-
"start-api",
3578-
"HelloWorldFunction",
3579-
"-t",
3580-
cls.integration_dir + cls.template_path,
3581-
"--warm-containers",
3582-
cls.container_mode,
3583-
"--parameter-overrides",
3584-
cls._make_parameter_override_arg(cls.parameter_overrides),
3585-
]
3586-
super().setUpClass()
3587-
3588-
def setUp(self):
3589-
self.url = f"http://127.0.0.1:{self.port}"
3590-
3591-
def _count_containers(self):
3592-
"""Count containers with the test's unique environment variable"""
3593-
containers = self.docker_client.containers.list(all=True, filters={"label": "sam.cli.container.type=lambda"})
3594-
return sum(
3595-
1
3596-
for c in containers
3597-
if any(self.mode_env_variable in env for env in c.attrs.get("Config", {}).get("Env", []))
3598-
)
3599-
3600-
@pytest.mark.flaky(reruns=3)
3601-
@pytest.mark.timeout(timeout=600, method="thread")
3602-
def test_container_created_on_demand_for_filtered_function(self):
3603-
"""Test that containers are created on-demand only for filtered functions in LAZY mode"""
3604-
initial_count = self._count_containers()
3605-
self.assertEqual(requests.get(self.url + "/hello", timeout=300).status_code, 200)
3606-
sleep(2)
3607-
self.assertEqual(self._count_containers() - initial_count, 1)

0 commit comments

Comments
 (0)