From 8c6a6d1c00d138562b47f98294264c2ed768739a Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Wed, 11 Mar 2026 16:01:30 +0900 Subject: [PATCH 1/2] fix: handle intrinsic functions in Lambda FunctionName for GetAtt resolution --- .../intrinsics_symbol_table.py | 22 ++- .../test_fn_join_with_getatt.py | 170 ++++++++++++++++++ 2 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 tests/unit/lib/intrinsic_resolver/test_fn_join_with_getatt.py diff --git a/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py b/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py index ff013653f87..d3722e5429b 100644 --- a/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py +++ b/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py @@ -270,12 +270,19 @@ def arn_resolver(self, logical_id, service_name="lambda"): partition_name = self.handle_pseudo_partition() if service_name == "lambda": resource_name = self._get_function_name(logical_id) - resource_name = self.logical_id_translator.get(resource_name) or resource_name + # Only use logical_id_translator if resource_name is a string (not an unresolved intrinsic) + if isinstance(resource_name, str): + resource_name = self.logical_id_translator.get(resource_name) or resource_name + else: + # If resource_name is still an intrinsic (dict), fall back to logical_id + resource_name = logical_id str_format = "arn:{partition_name}:{service_name}:{aws_region}:{account_id}:function:{resource_name}" else: resource_name = logical_id - resource_name = self.logical_id_translator.get(resource_name) or resource_name + # Only use logical_id_translator if resource_name is a string + if isinstance(resource_name, str): + resource_name = self.logical_id_translator.get(resource_name) or resource_name str_format = "arn:{partition_name}:{service_name}:{aws_region}:{account_id}:{resource_name}" @@ -287,7 +294,7 @@ def arn_resolver(self, logical_id, service_name="lambda"): resource_name=resource_name, ) - def _get_function_name(self, logical_id): + def _get_function_name(self, logical_id, intrinsic_resolver=None): """ This function returns the function name associated with the logical ID. If the template doesn't define a FunctionName, it will just return the @@ -297,6 +304,8 @@ def _get_function_name(self, logical_id): ----------- logical_id: str This the reference to the function name used + intrinsic_resolver: IntrinsicResolver + Optional resolver to resolve intrinsic functions in FunctionName Return ------- @@ -313,6 +322,13 @@ def _get_function_name(self, logical_id): return logical_id resource_name = resource_properties.get(IntrinsicsSymbolTable.CFN_LAMBDA_FUNCTION_NAME) + + # If resource_name is an intrinsic function (dict), resolve it + if resource_name and isinstance(resource_name, dict) and intrinsic_resolver: + resource_name = intrinsic_resolver.intrinsic_property_resolver( + resource_name, ignore_errors=True, parent_function="FunctionName" + ) + return resource_name or logical_id def get_translation(self, logical_id, resource_attributes=IntrinsicResolver.REF): diff --git a/tests/unit/lib/intrinsic_resolver/test_fn_join_with_getatt.py b/tests/unit/lib/intrinsic_resolver/test_fn_join_with_getatt.py new file mode 100644 index 00000000000..df99481c277 --- /dev/null +++ b/tests/unit/lib/intrinsic_resolver/test_fn_join_with_getatt.py @@ -0,0 +1,170 @@ +""" +Unit tests for Fn::Join with Fn::GetAtt bug fix +""" + +from unittest import TestCase +from samcli.lib.intrinsic_resolver.intrinsic_property_resolver import IntrinsicResolver +from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable + + +class TestFnJoinWithGetAtt(TestCase): + """ + Test that Fn::Join works correctly with Fn::GetAtt for Lambda function ARNs, + especially when the FunctionName property contains intrinsic functions. + """ + + def test_fn_join_with_getatt_simple_function_name(self): + """ + Test Fn::Join with Fn::GetAtt when FunctionName is a simple string + """ + template = { + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "FunctionName": "my-function-name", + }, + } + } + } + + resolver = IntrinsicResolver( + template=template, symbol_resolver=IntrinsicsSymbolTable(logical_id_translator={}, template=template) + ) + + uri_intrinsic = { + "Fn::Join": [ + "", + [ + "arn:", + {"Ref": "AWS::Partition"}, + ":apigateway:", + {"Ref": "AWS::Region"}, + ":lambda:path/2015-03-31/functions/", + {"Fn::GetAtt": ["MyFunction", "Arn"]}, + "/invocations", + ], + ] + } + + result = resolver.intrinsic_property_resolver(uri_intrinsic, ignore_errors=False) + + self.assertIsInstance(result, str) + self.assertIn("my-function-name", result) + self.assertIn("arn:aws:apigateway:", result) + self.assertIn("lambda:path/2015-03-31/functions/", result) + self.assertIn("/invocations", result) + + def test_fn_join_with_getatt_intrinsic_function_name(self): + """ + Test Fn::Join with Fn::GetAtt when FunctionName contains Fn::Sub + This was the original bug - it would throw TypeError: unhashable type: 'dict' + """ + template = { + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "FunctionName": {"Fn::Sub": "my-${AWS::StackName}-function"}, + }, + } + } + } + + resolver = IntrinsicResolver( + template=template, symbol_resolver=IntrinsicsSymbolTable(logical_id_translator={}, template=template) + ) + + uri_intrinsic = { + "Fn::Join": [ + "", + [ + "arn:", + {"Ref": "AWS::Partition"}, + ":apigateway:", + {"Ref": "AWS::Region"}, + ":lambda:path/2015-03-31/functions/", + {"Fn::GetAtt": ["MyFunction", "Arn"]}, + "/invocations", + ], + ] + } + + result = resolver.intrinsic_property_resolver(uri_intrinsic, ignore_errors=False) + + self.assertIsInstance(result, str) + self.assertIn("MyFunction", result) + self.assertIn("arn:aws:apigateway:", result) + self.assertIn("lambda:path/2015-03-31/functions/", result) + self.assertIn("/invocations", result) + + def test_fn_join_with_getatt_no_function_name(self): + """ + Test Fn::Join with Fn::GetAtt when FunctionName property is not defined + Should use the logical ID as the function name + """ + template = { + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + } + } + } + + resolver = IntrinsicResolver( + template=template, symbol_resolver=IntrinsicsSymbolTable(logical_id_translator={}, template=template) + ) + + uri_intrinsic = { + "Fn::Join": [ + "", + [ + "arn:", + {"Ref": "AWS::Partition"}, + ":apigateway:", + {"Ref": "AWS::Region"}, + ":lambda:path/2015-03-31/functions/", + {"Fn::GetAtt": ["MyFunction", "Arn"]}, + "/invocations", + ], + ] + } + + result = resolver.intrinsic_property_resolver(uri_intrinsic, ignore_errors=False) + + self.assertIsInstance(result, str) + self.assertIn("MyFunction", result) + self.assertIn("arn:aws:apigateway:", result) + self.assertIn("lambda:path/2015-03-31/functions/", result) + self.assertIn("/invocations", result) + + def test_fn_sub_still_works_with_getatt(self): + """ + Ensure that the fix doesn't break Fn::Sub with Fn::GetAtt (which was already working) + """ + template = { + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "FunctionName": {"Fn::Sub": "my-${AWS::StackName}-function"}, + }, + } + } + } + + resolver = IntrinsicResolver( + template=template, symbol_resolver=IntrinsicsSymbolTable(logical_id_translator={}, template=template) + ) + + uri_intrinsic = { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + + result = resolver.intrinsic_property_resolver(uri_intrinsic, ignore_errors=False) + + self.assertIsInstance(result, str) + self.assertIn("MyFunction", result) + self.assertIn("arn:aws:apigateway:", result) + self.assertIn("lambda:path/2015-03-31/functions/", result) + self.assertIn("/invocations", result) From 5047f7e1a727852ce681c62b74c4add41c29bc63 Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Fri, 27 Mar 2026 11:03:26 +0900 Subject: [PATCH 2/2] fix: remove dead code from _get_function_name --- .../lib/intrinsic_resolver/intrinsics_symbol_table.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py b/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py index d3722e5429b..cf5b77e0475 100644 --- a/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py +++ b/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py @@ -294,7 +294,7 @@ def arn_resolver(self, logical_id, service_name="lambda"): resource_name=resource_name, ) - def _get_function_name(self, logical_id, intrinsic_resolver=None): + def _get_function_name(self, logical_id): """ This function returns the function name associated with the logical ID. If the template doesn't define a FunctionName, it will just return the @@ -304,8 +304,6 @@ def _get_function_name(self, logical_id, intrinsic_resolver=None): ----------- logical_id: str This the reference to the function name used - intrinsic_resolver: IntrinsicResolver - Optional resolver to resolve intrinsic functions in FunctionName Return ------- @@ -322,13 +320,6 @@ def _get_function_name(self, logical_id, intrinsic_resolver=None): return logical_id resource_name = resource_properties.get(IntrinsicsSymbolTable.CFN_LAMBDA_FUNCTION_NAME) - - # If resource_name is an intrinsic function (dict), resolve it - if resource_name and isinstance(resource_name, dict) and intrinsic_resolver: - resource_name = intrinsic_resolver.intrinsic_property_resolver( - resource_name, ignore_errors=True, parent_function="FunctionName" - ) - return resource_name or logical_id def get_translation(self, logical_id, resource_attributes=IntrinsicResolver.REF):