Skip to content
Merged
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
20 changes: 18 additions & 2 deletions bin/run_cfn_lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ if [ ! -d "${VENV}" ]; then
fi

"${VENV}/bin/python" -m pip install cfn-lint --upgrade --quiet
# update cfn schema
"${VENV}/bin/cfn-lint" -u
# update cfn schema with retry logic (can fail due to network issues)
MAX_RETRIES=3
RETRY_COUNT=0
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
if "${VENV}/bin/cfn-lint" -u; then
echo "Successfully updated cfn-lint schema"
break
else
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "cfn-lint schema update failed, retrying... (attempt $RETRY_COUNT of $MAX_RETRIES)"
sleep 2
else
echo "cfn-lint schema update failed after $MAX_RETRIES attempts"
exit 1
fi
fi
done
"${VENV}/bin/cfn-lint" --format parseable
35 changes: 31 additions & 4 deletions samtranslator/intrinsics/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@
from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException


def _get_parameter_value(parameters: Dict[str, Any], param_name: str, default: Any = None) -> Any:
"""
Get parameter value from parameters dict, but return default (None) if
- it's a CloudFormation internal placeholder.
- param_name is not in the parameters.

CloudFormation internal placeholders are passed during changeset creation with --include-nested-stacks
when there are cross-references between nested stacks that don't exist yet.
These placeholders should not be resolved by SAM.

:param parameters: Dictionary of parameter values
:param param_name: Name of the parameter to retrieve
:param default: Default value to return if parameter not found or is a placeholder
:return: Parameter value, or default if not found or is a CloudFormation placeholder
"""
value = parameters.get(param_name, default)

# Check if the value is a CloudFormation internal placeholder
# E.g. {{IntrinsicFunction:api-xx/MyStack.Outputs.API/Fn::GetAtt}}
if isinstance(value, str) and value.startswith("{{IntrinsicFunction:"):
return default

return value


class Action(ABC):
"""
Base class for intrinsic function actions. Each intrinsic function must subclass this,
Expand Down Expand Up @@ -103,9 +128,9 @@ def resolve_parameter_refs(self, input_dict: Optional[Any], parameters: Dict[str
if not isinstance(param_name, str):
return input_dict

if param_name in parameters:
return parameters[param_name]
return input_dict
# Use the wrapper function to get parameter value
# It returns the original input unchanged if the parameter is a CloudFormation internal placeholder
return _get_parameter_value(parameters, param_name, input_dict)

def resolve_resource_refs(
self, input_dict: Optional[Any], supported_resource_refs: Dict[str, Any]
Expand Down Expand Up @@ -193,7 +218,9 @@ def do_replacement(full_ref: str, prop_name: str) -> Any:
:param prop_name: => logicalId.property
:return: Either the value it resolves to. If not the original reference
"""
return parameters.get(prop_name, full_ref)
# Use the wrapper function to get parameter value
# It returns the original input unchanged if the parameter is a CloudFormation internal placeholder
return _get_parameter_value(parameters, prop_name, full_ref)

return self._handle_sub_action(input_dict, do_replacement)

Expand Down
127 changes: 127 additions & 0 deletions tests/intrinsics/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,133 @@ def test_short_circuit_on_empty_parameters(self):
self.assertEqual(resolver.resolve_parameter_refs(input), expected)
resolver._try_resolve_parameter_refs.assert_not_called()

def test_cloudformation_internal_placeholder_not_resolved(self):
"""Test that CloudFormation internal placeholders are not resolved"""
parameter_values = {
"UserPoolArn": "{{IntrinsicFunction:debugging-cloudformation-issues4/Cognito.Outputs.UserPoolArn/Fn::GetAtt}}",
"NormalParam": "normal-value",
}
resolver = IntrinsicsResolver(parameter_values)

# CloudFormation placeholder should not be resolved
input1 = {"Ref": "UserPoolArn"}
expected1 = {"Ref": "UserPoolArn"}
output1 = resolver.resolve_parameter_refs(input1)
self.assertEqual(output1, expected1)

# Normal parameter should still be resolved
input2 = {"Ref": "NormalParam"}
expected2 = "normal-value"
output2 = resolver.resolve_parameter_refs(input2)
self.assertEqual(output2, expected2)

def test_cloudformation_placeholders_in_nested_structure(self):
"""Test CloudFormation placeholders in nested structures"""
parameter_values = {
"Placeholder1": "{{IntrinsicFunction:stack/Output1/Fn::GetAtt}}",
"Placeholder2": "{{IntrinsicFunction:stack/Output2/Fn::GetAtt}}",
"NormalParam": "value",
}
resolver = IntrinsicsResolver(parameter_values)

input = {
"Resources": {
"Resource1": {
"Properties": {
"Prop1": {"Ref": "Placeholder1"},
"Prop2": {"Ref": "NormalParam"},
"Prop3": {"Ref": "Placeholder2"},
}
}
}
}

expected = {
"Resources": {
"Resource1": {
"Properties": {
"Prop1": {"Ref": "Placeholder1"}, # Not resolved
"Prop2": "value", # Resolved
"Prop3": {"Ref": "Placeholder2"}, # Not resolved
}
}
}
}

output = resolver.resolve_parameter_refs(input)
self.assertEqual(output, expected)

def test_cloudformation_placeholders_in_lists(self):
"""Test CloudFormation placeholders in list structures"""
parameter_values = {
"VpceId": "{{IntrinsicFunction:stack/VpcEndpoint.Outputs.Id/Fn::GetAtt}}",
"Region": "us-east-1",
}
resolver = IntrinsicsResolver(parameter_values)

input = [{"Ref": "VpceId"}, {"Ref": "Region"}, "static-value", {"Ref": "VpceId"}]

expected = [
{"Ref": "VpceId"}, # Not resolved
"us-east-1", # Resolved
"static-value",
{"Ref": "VpceId"}, # Not resolved
]

output = resolver.resolve_parameter_refs(input)
self.assertEqual(output, expected)

def test_cloudformation_placeholders_with_sub(self):
"""Test that CloudFormation placeholders inside Fn::Sub are not substituted

Similar to Ref, Fn::Sub should not substitute CloudFormation internal placeholders.
This prevents the placeholders from being embedded in strings where they can't be
properly handled by CloudFormation.
"""
parameter_values = {
"Placeholder": "{{IntrinsicFunction:stack/Output/Fn::GetAtt}}",
"NormalParam": "normal-value",
}
resolver = IntrinsicsResolver(parameter_values)

# Sub should not substitute CloudFormation placeholders, but should substitute normal params
input = {"Fn::Sub": "Value is ${Placeholder} and ${NormalParam}"}
expected = {"Fn::Sub": "Value is ${Placeholder} and normal-value"}

output = resolver.resolve_parameter_refs(input)
self.assertEqual(output, expected)

def test_various_cloudformation_placeholder_formats(self):
"""Test various CloudFormation placeholder formats"""
parameter_values = {
"Valid1": "{{IntrinsicFunction:stack/Resource.Outputs.Value/Fn::GetAtt}}",
"Valid2": "{{IntrinsicFunction:name-with-dashes/Out/Fn::GetAtt}}",
"Valid3": "{{IntrinsicFunction:stack123/Resource.Out/Fn::GetAtt}}",
"NotPlaceholder1": "{{SomethingElse}}",
"NotPlaceholder2": "{{intrinsicfunction:lowercase}}",
"NotPlaceholder3": "normal-string",
}
resolver = IntrinsicsResolver(parameter_values)

# Valid placeholders should not be resolved
for param in ["Valid1", "Valid2", "Valid3"]:
input = {"Ref": param}
expected = {"Ref": param}
output = resolver.resolve_parameter_refs(input)
self.assertEqual(output, expected, f"Failed for {param}")

# Non-placeholders should be resolved
test_cases = [
("NotPlaceholder1", "{{SomethingElse}}"),
("NotPlaceholder2", "{{intrinsicfunction:lowercase}}"),
("NotPlaceholder3", "normal-string"),
]

for param, expected_value in test_cases:
input = {"Ref": param}
output = resolver.resolve_parameter_refs(input)
self.assertEqual(output, expected_value, f"Failed for {param}")


class TestResourceReferenceResolution(TestCase):
def setUp(self):
Expand Down