Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
22 changes: 19 additions & 3 deletions samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
hoangsetup marked this conversation as resolved.
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}"

Expand All @@ -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
Expand All @@ -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
-------
Expand All @@ -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):
Expand Down
170 changes: 170 additions & 0 deletions tests/unit/lib/intrinsic_resolver/test_fn_join_with_getatt.py
Original file line number Diff line number Diff line change
@@ -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)