Skip to content

Commit e1ca195

Browse files
committed
feat: allow explicit listing of function ids for local start-* command
1 parent d6ec326 commit e1ca195

17 files changed

Lines changed: 1049 additions & 19 deletions

File tree

samcli/commands/local/cli_common/invoke_context.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def __init__(
9898
invoke_images: Optional[str] = None,
9999
mount_symlinks: Optional[bool] = False,
100100
no_mem_limit: Optional[bool] = False,
101+
function_logical_ids: Optional[Tuple[str, ...]] = None,
101102
) -> None:
102103
"""
103104
Initialize the context
@@ -107,7 +108,7 @@ def __init__(
107108
template_file str
108109
Name or path to template
109110
function_identifier str
110-
Identifier of the function to invoke
111+
Identifier of the single function to invoke (used by 'sam local invoke' command)
111112
env_vars_file str
112113
Path to a file containing values for environment variables
113114
docker_volume_basedir str
@@ -154,10 +155,27 @@ def __init__(
154155
Optional. A dictionary that defines the custom invoke image URI of each function
155156
mount_symlinks bool
156157
Optional. Indicates if symlinks should be mounted inside the container
158+
function_logical_ids tuple(str)
159+
Optional. Tuple of function logical IDs to filter and make available for local execution.
160+
Used by 'sam local start-api' and 'sam local start-lambda' commands to limit which
161+
functions from the template are exposed. If not provided, all functions are available.
157162
"""
158163

159164
self._template_file = template_file
160165
self._function_identifier = function_identifier
166+
self._function_logical_ids = function_logical_ids
167+
168+
# Validate that function_identifier and function_logical_ids aren't both provided
169+
# function_identifier is for 'sam local invoke' (single function)
170+
# function_logical_ids is for 'sam local start-*' commands (multiple function filter)
171+
if self._function_identifier and self._function_logical_ids:
172+
LOG.warning(
173+
"Both function_identifier and function_logical_ids were provided. "
174+
"function_identifier is used for 'sam local invoke' to specify a single function, "
175+
"while function_logical_ids is used for 'sam local start-api/start-lambda' to filter functions. "
176+
"function_identifier will take precedence for single function invocation."
177+
)
178+
161179
self._env_vars_file = env_vars_file
162180
self._docker_volume_basedir = docker_volume_basedir
163181
self._docker_network = docker_network
@@ -241,10 +259,18 @@ def __enter__(self) -> "InvokeContext":
241259
if self._docker_volume_basedir:
242260
_function_providers_args[self._containers_mode].append(True)
243261

262+
# Add function_logical_ids parameter to both provider types
263+
_function_providers_kwargs: Dict[str, Any] = {}
264+
if self._function_logical_ids is not None:
265+
_function_providers_kwargs["function_logical_ids"] = self._function_logical_ids
266+
244267
self._function_provider = _function_providers_class[self._containers_mode](
245-
*_function_providers_args[self._containers_mode]
268+
*_function_providers_args[self._containers_mode], **_function_providers_kwargs
246269
)
247270

271+
# Validate function logical IDs after provider is initialized
272+
self._validate_function_logical_ids()
273+
248274
self._env_vars_value = self._get_env_vars_value(self._env_vars_file)
249275
self._container_env_vars_value = self._get_env_vars_value(self._container_env_vars_file)
250276
self._log_file_handle = self._setup_log_file(self._log_file)
@@ -448,6 +474,34 @@ def _clean_running_containers_and_related_resources(self) -> None:
448474
cast(WarmLambdaRuntime, self.lambda_runtime).clean_running_containers_and_related_resources()
449475
cast(RefreshableSamFunctionProvider, self._function_provider).stop_observer()
450476

477+
def _validate_function_logical_ids(self) -> None:
478+
"""
479+
Validates that all provided function logical IDs exist in the template.
480+
Raises InvalidFunctionNamesException with helpful error message if validation fails.
481+
"""
482+
if not self._function_logical_ids:
483+
return # No filtering requested
484+
485+
# Get all available function names/IDs from the provider
486+
all_functions_set = set()
487+
for func in self._function_provider.get_all():
488+
all_functions_set.add(func.name)
489+
all_functions_set.add(func.function_id)
490+
if func.functionname:
491+
all_functions_set.add(func.functionname)
492+
493+
# Check for invalid IDs
494+
invalid_ids = set(self._function_logical_ids) - all_functions_set
495+
496+
if invalid_ids:
497+
available_ids = sorted(all_functions_set)
498+
from samcli.commands.local.cli_common.user_exceptions import InvalidFunctionNamesException
499+
500+
raise InvalidFunctionNamesException(
501+
f"Invalid function logical ID(s): {', '.join(sorted(invalid_ids))}\n\n"
502+
f"Available functions in template:\n " + "\n ".join(available_ids)
503+
)
504+
451505
def _add_account_id_to_global(self) -> None:
452506
"""
453507
Attempts to get the Account ID from the current session
@@ -556,6 +610,21 @@ def initialized_container_ids(self) -> List[str]:
556610
"""
557611
return self._initialized_container_ids
558612

613+
@property
614+
def function_logical_ids(self) -> Optional[Tuple[str, ...]]:
615+
"""
616+
Returns the tuple of function logical IDs to filter, if provided.
617+
618+
This is used by 'sam local start-api' and 'sam local start-lambda' commands
619+
to limit which functions from the template are made available for local execution.
620+
This is different from function_identifier which is used by 'sam local invoke'
621+
to specify a single function to invoke.
622+
623+
Returns:
624+
Optional[Tuple[str, ...]]: Tuple of function logical IDs or None if no filter
625+
"""
626+
return self._function_logical_ids
627+
559628
@property
560629
def stdout(self) -> StreamWriter:
561630
"""

samcli/commands/local/cli_common/user_exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ class InvokeContextException(UserException):
1111
"""
1212

1313

14+
class InvalidFunctionNamesException(InvokeContextException):
15+
"""
16+
User provided function names that don't exist in the template
17+
"""
18+
19+
1420
class InvalidSamTemplateException(UserException):
1521
"""
1622
The template provided was invalid and not able to transform into a Standard CloudFormation Template

samcli/commands/local/lib/local_api_service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ 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, cwd=self.cwd, disable_authorizer=disable_authorizer
43+
lambda_invoke_context.stacks,
44+
cwd=self.cwd,
45+
disable_authorizer=disable_authorizer,
46+
function_logical_ids=lambda_invoke_context.function_logical_ids,
4447
)
4548
self.lambda_runner = lambda_invoke_context.local_lambda_runner
4649
self.stderr_stream = lambda_invoke_context.stderr

samcli/commands/local/start_api/cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
requires_credentials=False,
6161
context_settings={"max_content_width": 120},
6262
)
63+
@click.argument("function_logical_ids", nargs=-1, required=False)
6364
@configuration_option(provider=ConfigProvider(section="parameters"))
6465
@terraform_plan_file_option
6566
@hook_name_click_option(
@@ -107,6 +108,7 @@
107108
@print_cmdline_args
108109
def cli(
109110
ctx,
111+
function_logical_ids,
110112
# start-api Specific Options
111113
host,
112114
port,
@@ -150,6 +152,7 @@ def cli(
150152

151153
do_cli(
152154
ctx,
155+
function_logical_ids,
153156
host,
154157
port,
155158
disable_authorizer,
@@ -183,6 +186,7 @@ def cli(
183186

184187
def do_cli( # pylint: disable=R0914
185188
ctx,
189+
function_logical_ids,
186190
host,
187191
port,
188192
disable_authorizer,
@@ -256,6 +260,7 @@ def do_cli( # pylint: disable=R0914
256260
container_host_interface=container_host_interface,
257261
invoke_images=processed_invoke_images,
258262
add_host=add_host,
263+
function_logical_ids=function_logical_ids,
259264
no_mem_limit=no_mem_limit,
260265
) as invoke_context:
261266
ssl_context = (ssl_cert_file, ssl_key_file) if ssl_cert_file else None

samcli/commands/local/start_lambda/cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
requires_credentials=False,
5353
context_settings={"max_content_width": 120},
5454
)
55+
@click.argument("function_logical_ids", nargs=-1, required=False)
5556
@configuration_option(provider=ConfigProvider(section="parameters"))
5657
@terraform_plan_file_option
5758
@hook_name_click_option(
@@ -71,6 +72,7 @@
7172
@print_cmdline_args
7273
def cli(
7374
ctx, # pylint: disable=R0914
75+
function_logical_ids,
7476
# start-lambda Specific Options
7577
host,
7678
port,
@@ -110,6 +112,7 @@ def cli(
110112

111113
do_cli(
112114
ctx,
115+
function_logical_ids,
113116
host,
114117
port,
115118
template_file,
@@ -139,6 +142,7 @@ def cli(
139142

140143
def do_cli( # pylint: disable=R0914
141144
ctx,
145+
function_logical_ids,
142146
host,
143147
port,
144148
template,
@@ -208,6 +212,7 @@ def do_cli( # pylint: disable=R0914
208212
container_host_interface=container_host_interface,
209213
add_host=add_host,
210214
invoke_images=processed_invoke_images,
215+
function_logical_ids=function_logical_ids,
211216
no_mem_limit=no_mem_limit,
212217
) as invoke_context:
213218
service = LocalLambdaService(lambda_invoke_context=invoke_context, port=port, host=host)

samcli/lib/providers/api_provider.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
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
4+
from typing import Iterator, List, Optional, Tuple
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
1112

1213
LOG = logging.getLogger(__name__)
1314

1415

1516
class ApiProvider(AbstractApiProvider):
16-
def __init__(self, stacks: List[Stack], cwd: Optional[str] = None, disable_authorizer: Optional[bool] = False):
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+
):
1724
"""
1825
Initialize the class with template data. The template_dict is assumed
1926
to be valid, normalized and a dictionary. template_dict should be normalized by running any and all
@@ -31,13 +38,21 @@ def __init__(self, stacks: List[Stack], cwd: Optional[str] = None, disable_autho
3138
Optional working directory with respect to which we will resolve relative path to Swagger file
3239
disable_authorizer : Optional[bool]
3340
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
3443
"""
3544
self.stacks = stacks
3645

3746
# Store a set of apis
3847
self.cwd = cwd
3948
self.disable_authorizer = disable_authorizer
49+
self._function_logical_ids = function_logical_ids
4050
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+
4156
self.routes = self.api.routes
4257
LOG.debug("%d APIs found in the template", len(self.routes))
4358

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

5166
yield self.api
5267

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+
53112
def _extract_api(self) -> Api:
54113
"""
55114
Extracts all the routes by running through the one providers. The provider that has the first type matched

0 commit comments

Comments
 (0)