Skip to content

Commit 3b5fbbb

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

10 files changed

Lines changed: 659 additions & 14 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-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/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/sam_function_provider.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import logging
66
from pathlib import Path
7-
from typing import Any, Dict, Iterator, List, Optional, cast
7+
from typing import Any, Dict, Iterator, List, Optional, Tuple, cast
88

99
from samtranslator.policy_template_processor.exceptions import TemplateNotFoundException
1010

@@ -45,6 +45,7 @@ def __init__(
4545
use_raw_codeuri: bool = False,
4646
ignore_code_extraction_warnings: bool = False,
4747
locate_layer_nested: bool = False,
48+
function_logical_ids: Optional[Tuple[str, ...]] = None,
4849
) -> None:
4950
"""
5051
Initialize the class with SAM template data. The SAM template passed to this provider is assumed
@@ -61,16 +62,18 @@ def __init__(
6162
Note(xinhol): use_raw_codeuri is temporary to fix a bug, and will be removed for a permanent solution.
6263
:param bool ignore_code_extraction_warnings: Ignores Log warnings
6364
:param bool locate_layer_nested: resolved nested layer reference to their actual location in the nested stack
65+
:param tuple function_logical_ids: Optional tuple of function logical IDs to filter by
6466
"""
6567

6668
self._stacks = stacks
69+
self._function_logical_ids = function_logical_ids
6770

6871
for stack in stacks:
6972
LOG.debug("%d resources found in the stack %s", len(stack.resources), stack.stack_path)
7073

7174
# Store a map of function full_path to function information for quick reference
7275
self.functions = SamFunctionProvider._extract_functions(
73-
self._stacks, use_raw_codeuri, ignore_code_extraction_warnings, locate_layer_nested
76+
self._stacks, use_raw_codeuri, ignore_code_extraction_warnings, locate_layer_nested, function_logical_ids
7477
)
7578

7679
self._colored = Colored()
@@ -90,6 +93,7 @@ def update(
9093
use_raw_codeuri: bool = False,
9194
ignore_code_extraction_warnings: bool = False,
9295
locate_layer_nested: bool = False,
96+
function_logical_ids: Optional[Tuple[str, ...]] = None,
9397
) -> None:
9498
"""
9599
Hydrate the function provider with updated stacks
@@ -98,10 +102,11 @@ def update(
98102
Note(xinhol): use_raw_codeuri is temporary to fix a bug, and will be removed for a permanent solution.
99103
:param bool ignore_code_extraction_warnings: Ignores Log warnings
100104
:param bool locate_layer_nested: resolved nested layer reference to their actual location in the nested stack
105+
:param tuple function_logical_ids: Optional tuple of function logical IDs to filter by
101106
"""
102107
self._stacks = stacks
103108
self.functions = SamFunctionProvider._extract_functions(
104-
self._stacks, use_raw_codeuri, ignore_code_extraction_warnings, locate_layer_nested
109+
self._stacks, use_raw_codeuri, ignore_code_extraction_warnings, locate_layer_nested, function_logical_ids
105110
)
106111

107112
def get(self, name: str) -> Optional[Function]:
@@ -180,12 +185,29 @@ def get_all(self) -> Iterator[Function]:
180185
for _, function in self.functions.items():
181186
yield function
182187

188+
@staticmethod
189+
def _should_include_function(function: Function, function_logical_ids: set) -> bool:
190+
"""
191+
Determines if a function should be included based on the filter.
192+
Matches against function_id, name, and functionname.
193+
194+
:param function: Function object to check
195+
:param function_logical_ids: Set of function logical IDs to match against
196+
:return bool: True if function should be included, False otherwise
197+
"""
198+
return bool(
199+
function.function_id in function_logical_ids
200+
or function.name in function_logical_ids
201+
or (function.functionname and function.functionname in function_logical_ids)
202+
)
203+
183204
@staticmethod
184205
def _extract_functions(
185206
stacks: List[Stack],
186207
use_raw_codeuri: bool = False,
187208
ignore_code_extraction_warnings: bool = False,
188209
locate_layer_nested: bool = False,
210+
function_logical_ids: Optional[Tuple[str, ...]] = None,
189211
) -> Dict[str, Function]:
190212
"""
191213
Extracts and returns function information from the given dictionary of SAM/CloudFormation resources. This
@@ -195,6 +217,7 @@ def _extract_functions(
195217
:param bool use_raw_codeuri: Do not resolve adjust core_uri based on the template path, use the raw uri.
196218
:param bool ignore_code_extraction_warnings: suppress log statements on code extraction from resources.
197219
:param bool locate_layer_nested: resolved nested layer reference to their actual location in the nested stack
220+
:param tuple function_logical_ids: Optional tuple of function logical IDs to filter by
198221
:return dict(string : samcli.commands.local.lib.provider.Function): Dictionary of function full_path to the
199222
Function configuration object
200223
"""
@@ -271,6 +294,17 @@ def _extract_functions(
271294

272295
# We don't care about other resource types. Just ignore them
273296

297+
# Apply filtering if function_logical_ids provided
298+
if function_logical_ids:
299+
function_logical_ids_set = set(function_logical_ids)
300+
filtered_functions = {}
301+
302+
for full_path, function in result.items():
303+
if SamFunctionProvider._should_include_function(function, function_logical_ids_set):
304+
filtered_functions[full_path] = function
305+
306+
return filtered_functions
307+
274308
return result
275309

276310
@staticmethod
@@ -800,6 +834,7 @@ def __init__(
800834
global_parameter_overrides: Optional[Dict] = None,
801835
use_raw_codeuri: bool = False,
802836
ignore_code_extraction_warnings: bool = False,
837+
function_logical_ids: Optional[Tuple[str, ...]] = None,
803838
) -> None:
804839
"""
805840
Initialize the class with SAM template data. The SAM template passed to this provider is assumed
@@ -815,9 +850,15 @@ def __init__(
815850
:param bool use_raw_codeuri: Do not resolve adjust core_uri based on the template path, use the raw uri.
816851
Note(xinhol): use_raw_codeuri is temporary to fix a bug, and will be removed for a permanent solution.
817852
:param bool ignore_code_extraction_warnings: Ignores Log warnings
853+
:param tuple function_logical_ids: Optional tuple of function logical IDs to filter by
818854
"""
819855

820-
super().__init__(stacks, use_raw_codeuri, ignore_code_extraction_warnings)
856+
# Store function_logical_ids before calling super().__init__
857+
self._function_logical_ids = function_logical_ids
858+
859+
super().__init__(
860+
stacks, use_raw_codeuri, ignore_code_extraction_warnings, function_logical_ids=function_logical_ids
861+
)
821862

822863
# initialize root templates. Usually it will be one template, as sam commands only support processing
823864
# one template. These templates will be fixed.
@@ -909,6 +950,7 @@ def _watch_stack_templates(self, stacks: List[Stack]) -> None:
909950
def _refresh_loaded_functions(self) -> None:
910951
"""
911952
Reload the stacks, and lambda functions from template files.
953+
Applies the same function filter during refresh.
912954
"""
913955
LOG.debug("A change got detected in one of the stack templates. Reload the lambda function resources")
914956
self._stacks = []
@@ -925,8 +967,12 @@ def _refresh_loaded_functions(self) -> None:
925967
raise ex
926968

927969
self.is_changed = False
970+
# Pass function_logical_ids to maintain filter during refresh
928971
self.functions = self._extract_functions(
929-
self._stacks, self._use_raw_codeuri, self._ignore_code_extraction_warnings
972+
self._stacks,
973+
self._use_raw_codeuri,
974+
self._ignore_code_extraction_warnings,
975+
function_logical_ids=self._function_logical_ids,
930976
)
931977
self._watch_stack_templates(self._stacks)
932978

tests/integration/local/start_lambda/start_lambda_api_integ_base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class StartLambdaIntegBaseClass(TestCase):
3737
terraform_plan_file: Optional[str] = None
3838
beta_features: Optional[bool] = None
3939
collect_start_lambda_process_output: bool = False
40+
function_logical_ids: Optional[List[str]] = None
4041

4142
build_before_invoke = False
4243
build_overrides: Optional[Dict[str, str]] = None
@@ -114,9 +115,14 @@ def get_start_lambda_command(
114115
hook_name=None,
115116
beta_features=None,
116117
terraform_plan_file=None,
118+
function_logical_ids=None,
117119
):
118120
command_list = [get_sam_command(), "local", "start-lambda"]
119121

122+
# Add function names as positional arguments first
123+
if function_logical_ids:
124+
command_list.extend(function_logical_ids)
125+
120126
if port:
121127
command_list += ["-p", port]
122128

@@ -159,6 +165,7 @@ def start_lambda(cls, wait_time=5, input=None, env=None):
159165
hook_name=cls.hook_name,
160166
beta_features=cls.beta_features,
161167
terraform_plan_file=cls.terraform_plan_file,
168+
function_logical_ids=cls.function_logical_ids,
162169
)
163170

164171
# Container labels are no longer needed - container IDs are parsed from output

0 commit comments

Comments
 (0)