Skip to content
Open
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,11 @@ coverage.xml
tests/integration/buildcmd/scratch
tests/integration/testdata/buildcmd/Dotnet*/bin
tests/integration/testdata/buildcmd/Dotnet*/obj
tests/integration/testdata/buildcmd/Java/**/build/
tests/integration/testdata/invoke/credential_tests/inprocess/dotnet/STS/obj
tests/integration/testdata/sync/code/after/dotnet_function/src/HelloWorld/obj/
tests/integration/testdata/sync/code/before/dotnet_function/src/HelloWorld/obj/
samcli/lib/init/templates/cookiecutter-aws-sam-hello-java-gradle/**/.gradle/

# End of https://www.gitignore.io/api/osx,node,macos,linux,python,windows,pycharm,intellij,sublimetext,visualstudiocode

Expand Down
2 changes: 2 additions & 0 deletions samcli/commands/local/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def do_cli( # pylint: disable=R0914
from samcli.commands.local.lib.exceptions import NoPrivilegeException, OverridesNotWellDefinedError
from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException
from samcli.lib.providers.exceptions import InvalidLayerReference
from samcli.lib.utils.name_utils import InvalidFunctionNameException
from samcli.local.docker.lambda_debug_settings import DebuggingNotSupported
from samcli.local.docker.manager import DockerImagePullFailedException
from samcli.local.lambdafn.exceptions import FunctionNotFound
Expand Down Expand Up @@ -247,6 +248,7 @@ def do_cli( # pylint: disable=R0914
except (
InvalidSamDocumentException,
OverridesNotWellDefinedError,
InvalidFunctionNameException,
InvalidLayerReference,
InvalidIntermediateImageError,
DebuggingNotSupported,
Expand Down
6 changes: 5 additions & 1 deletion samcli/commands/local/lib/local_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from samcli.lib.utils.architecture import validate_architecture_runtime
from samcli.lib.utils.codeuri import resolve_code_path
from samcli.lib.utils.colors import Colored
from samcli.lib.utils.name_utils import normalize_lambda_function_name
from samcli.lib.utils.packagetype import IMAGE, ZIP
from samcli.lib.utils.stream_writer import StreamWriter
from samcli.local.docker.container import ContainerConnectionTimeoutException, ContainerResponseException
Expand Down Expand Up @@ -127,8 +128,11 @@ def invoke(
When we cannot find a function with the given name
"""

# Normalize function identifier from ARN if provided
normalized_function_identifier = normalize_lambda_function_name(function_identifier)

# Generate the correct configuration based on given inputs
function = self.provider.get(function_identifier)
function = self.provider.get(normalized_function_identifier)

if not function:
all_function_full_paths = [f.full_path for f in self.provider.get_all()]
Expand Down
133 changes: 133 additions & 0 deletions samcli/lib/utils/name_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""
Utilities for normalizing function names and identifiers
"""

import re

from samcli.lib.utils.arn_utils import ARNParts, InvalidArnValue

# Constants for ARN parsing
PARTIAL_ARN_MIN_PARTS = 3
FUNCTION_ALIAS_PARTS = 2
MIN_ARN_PARTS = 5 # Minimum parts for a valid ARN structure

LAMBDA_FUNCTION_NAME_PATTERN = (
r"^(arn:[^:]+:lambda:[^:]*:\d{12}:function:|\d{12}:function:)?[a-zA-Z0-9-_\.]+(:[\w$-]+)?$"
)


class InvalidFunctionNameException(Exception):
"""Exception raised when function name doesn't match AWS Lambda validation pattern"""

pass


def normalize_lambda_function_name(function_identifier: str) -> str:
"""
Normalize a Lambda function identifier by extracting the function name from various formats.

AWS Lambda supports multiple function identifier formats as documented in the AWS CLI/SDK:

Name formats:
- Function name: my-function (name-only), my-function:v1 (with alias)
- Function ARN: arn:aws:lambda:us-west-2:123456789012:function:my-function
- Partial ARN: 123456789012:function:my-function

This function normalizes all these formats to extract just the function name portion,
which is what SAM CLI uses internally for function lookup.

Parameters
----------
function_identifier : str
The function identifier in any of the supported formats

Returns
-------
str
The normalized function name

Raises
------
InvalidFunctionNameException
If the function identifier doesn't match AWS Lambda's validation pattern

Examples
--------
>>> normalize_lambda_function_name("my-function")
"my-function"
>>> normalize_lambda_function_name("my-function:v1")
"my-function"
>>> normalize_lambda_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-function")
"my-function"
>>> normalize_lambda_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-function:$LATEST")
"my-function"
>>> normalize_lambda_function_name("123456789012:function:my-function")
"my-function"
"""
# Handle full ARN format: arn:aws:lambda:region:account-id:function:function-name[:version]
if function_identifier.startswith("arn:"):
try:
arn_parts = ARNParts(function_identifier)
# Check if it's a Lambda ARN with function resource type
if arn_parts.service == "lambda" and arn_parts.resource_type == "function":
# For Lambda ARNs, the function name is in resource_id
# Handle versioned functions by splitting on ':'
function_name = arn_parts.resource_id.split(":")[0] if arn_parts.resource_id else ""
return function_name if function_name else function_identifier
elif arn_parts.service != "lambda":
# Non-Lambda ARNs should raise an exception
raise InvalidFunctionNameException(
f"1 validation error detected: Value '{function_identifier}' at 'functionName' "
f"failed to satisfy constraint: Member must satisfy regular expression pattern: "
f"{LAMBDA_FUNCTION_NAME_PATTERN}"
)
except InvalidArnValue:
# Very malformed ARNs (like "arn:aws:lambda") should raise an exception
if len(function_identifier.split(":")) < MIN_ARN_PARTS: # ARNs with less than 5 parts are too malformed
raise InvalidFunctionNameException(
f"1 validation error detected: Value '{function_identifier}' at 'functionName' "
f"failed to satisfy constraint: Member must satisfy regular expression pattern: "
f"{LAMBDA_FUNCTION_NAME_PATTERN}"
)
# Other malformed ARNs are returned unchanged
return function_identifier

# Handle partial ARN format: account-id:function:function-name[:version]
# This format has at least 3 parts separated by colons
elif ":" in function_identifier and len(function_identifier.split(":")) >= PARTIAL_ARN_MIN_PARTS:
parts = function_identifier.split(":")
# Check if it matches the partial ARN pattern: account-id:function:function-name[:version]
if len(parts) >= PARTIAL_ARN_MIN_PARTS and parts[1] == "function" and parts[2]:
# Extract function name (3rd part) and remove any version suffix
function_name = parts[2]
return function_name if function_name else function_identifier
# Invalid partial ARNs (like empty function name) should raise exception if they look like partial ARNs
elif len(parts) >= PARTIAL_ARN_MIN_PARTS and parts[1] == "function":
# This looks like a partial ARN but has invalid structure
raise InvalidFunctionNameException(
f"1 validation error detected: Value '{function_identifier}' at 'functionName' "
f"failed to satisfy constraint: Member must satisfy regular expression pattern: "
f"{LAMBDA_FUNCTION_NAME_PATTERN}"
)
# Other invalid partial ARNs are returned unchanged
return function_identifier

# Handle function name with alias: my-function:alias
# This is a simple function name with a single colon for alias/version
# But exclude partial ARN patterns like "account:function" (missing function name)
elif ":" in function_identifier:
parts = function_identifier.split(":")
if len(parts) == FUNCTION_ALIAS_PARTS and parts[1] != "function" and parts[0] and parts[1]:
# Return just the function name part (before the colon) if both parts are not empty
return parts[0]

# Validate plain function names against the pattern
if not re.match(LAMBDA_FUNCTION_NAME_PATTERN, function_identifier):
raise InvalidFunctionNameException(
f"1 validation error detected: Value '{function_identifier}' at 'functionName' "
f"failed to satisfy constraint: Member must satisfy regular expression pattern: "
f"{LAMBDA_FUNCTION_NAME_PATTERN}"
)

# Handle plain function name: my-function
return function_identifier
26 changes: 26 additions & 0 deletions samcli/local/lambda_service/lambda_error_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class LambdaErrorResponses:
# The request body could not be parsed as JSON.
InvalidRequestContentException = ("InvalidRequestContent", 400)

# One or more parameters values were invalid.
ValidationException = ("ValidationException", 400)

NotImplementedException = ("NotImplemented", 501)

ContainerCreationFailed = ("ContainerCreationFailed", 501)
Expand Down Expand Up @@ -85,6 +88,29 @@ def invalid_request_content(message):
exception_tuple[1],
)

@staticmethod
def validation_exception(message):
"""
Creates a Lambda Service ValidationException Response

Parameters
----------
message str
Message to be added to the body of the response

Returns
-------
Flask.Response
A response object representing the ValidationException Error
"""
exception_tuple = LambdaErrorResponses.ValidationException

return BaseLocalService.service_response(
LambdaErrorResponses._construct_error_response_body(LambdaErrorResponses.USER_ERROR, message),
LambdaErrorResponses._construct_headers(exception_tuple[0]),
exception_tuple[1],
)

@staticmethod
def unsupported_media_type(content_type):
"""
Expand Down
18 changes: 14 additions & 4 deletions samcli/local/lambda_service/local_lambda_invoke_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from werkzeug.routing import BaseConverter

from samcli.commands.local.lib.exceptions import UnsupportedInlineCodeError
from samcli.lib.utils.name_utils import InvalidFunctionNameException, normalize_lambda_function_name
from samcli.lib.utils.stream_writer import StreamWriter
from samcli.local.docker.exceptions import DockerContainerCreationFailedException
from samcli.local.lambdafn.exceptions import FunctionNotFound
Expand Down Expand Up @@ -151,7 +152,7 @@ def _invoke_request_handler(self, function_name):
Parameters
----------
function_name str
Name of the function to invoke
Name or ARN of the function to invoke

Returns
-------
Expand All @@ -166,15 +167,24 @@ def _invoke_request_handler(self, function_name):

request_data = request_data.decode("utf-8")

# Normalize function name from ARN if provided
try:
normalized_function_name = normalize_lambda_function_name(function_name)
except InvalidFunctionNameException as e:
LOG.error("Invalid function name: %s", str(e))
return LambdaErrorResponses.validation_exception(str(e))

stdout_stream_string = io.StringIO()
stdout_stream_bytes = io.BytesIO()
stdout_stream_writer = StreamWriter(stdout_stream_string, stdout_stream_bytes, auto_flush=True)

try:
self.lambda_runner.invoke(function_name, request_data, stdout=stdout_stream_writer, stderr=self.stderr)
self.lambda_runner.invoke(
normalized_function_name, request_data, stdout=stdout_stream_writer, stderr=self.stderr
)
except FunctionNotFound:
LOG.debug("%s was not found to invoke.", function_name)
return LambdaErrorResponses.resource_not_found(function_name)
LOG.debug("%s was not found to invoke.", normalized_function_name)
return LambdaErrorResponses.resource_not_found(normalized_function_name)
except UnsupportedInlineCodeError:
return LambdaErrorResponses.not_implemented_locally(
"Inline code is not supported for sam local commands. Please write your code in a separate file."
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/lib/utils/test_name_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Tests for samcli.lib.utils.name_utils module
"""

from unittest import TestCase

from parameterized import parameterized

from samcli.lib.utils.name_utils import normalize_lambda_function_name, InvalidFunctionNameException


class TestNormalizeLambdaFunctionName(TestCase):
"""Test cases for normalize_lambda_function_name function"""

@parameterized.expand(
[
# Simple function names (pass-through)
("my-function", "my-function"),
("function-with_mixed-chars123", "function-with_mixed-chars123"),
# Full ARNs with versions/aliases
("arn:aws:lambda:us-east-1:123456789012:function:my-function", "my-function"),
("arn:aws:lambda:us-east-1:123456789012:function:my-function:$LATEST", "my-function"),
("arn:aws:lambda:us-east-1:123456789012:function:my-function:1", "my-function"),
("arn:aws-cn:lambda:cn-north-1:123456789012:function:my-function", "my-function"),
# Partial ARNs
("123456789012:function:my-function", "my-function"),
("123456789012:function:my-function:$LATEST", "my-function"),
# Function names with versions/aliases
("my-function:$LATEST", "my-function"),
("my-function:v1", "my-function"),
# Invalid partial ARN format (should pass through)
("123456789012:invalid:my-function", "123456789012:invalid:my-function"),
]
)
def test_normalize_lambda_function_name(self, input_name, expected_output):
"""Test normalize_lambda_function_name with various inputs"""
result = normalize_lambda_function_name(input_name)
self.assertEqual(result, expected_output)

@parameterized.expand(
[
("arn:aws:s3:::my-bucket/my-key",),
("arn:aws:lambda",),
(":HelloWorld",),
("HelloWorld:",),
("",),
(":",),
("function-with-invalid-chars!",),
("123456789012:function:",),
]
)
def test_raises_exception_for_invalid_function_names(self, invalid_name):
"""Test that invalid function names raise InvalidFunctionNameException"""
with self.assertRaises(InvalidFunctionNameException) as exc_info:
normalize_lambda_function_name(invalid_name)

# Check that the error message matches AWS Lambda's format
self.assertIn("1 validation error detected", str(exc_info.exception))
self.assertIn(f"Value '{invalid_name}' at 'functionName'", str(exc_info.exception))
self.assertIn("failed to satisfy constraint", str(exc_info.exception))
13 changes: 11 additions & 2 deletions tests/unit/local/lambda_service/test_lambda_error_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,18 @@ def test_invalid_request_content(self, service_response_mock):
response = LambdaErrorResponses.invalid_request_content("InvalidRequestContent")

self.assertEqual(response, "InvalidRequestContent")

@patch("samcli.local.services.base_local_service.BaseLocalService.service_response")
def test_validation_exception(self, service_response_mock):
service_response_mock.return_value = "ValidationException"

response = LambdaErrorResponses.validation_exception("ValidationException")

self.assertEqual(response, "ValidationException")

service_response_mock.assert_called_once_with(
'{"Type": "User", "Message": "InvalidRequestContent"}',
{"x-amzn-errortype": "InvalidRequestContent", "Content-Type": "application/json"},
'{"Type": "User", "Message": "ValidationException"}',
{"x-amzn-errortype": "ValidationException", "Content-Type": "application/json"},
400,
)

Expand Down
Loading
Loading