diff --git a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py index aecc2a85aad..0ab0352722f 100644 --- a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py +++ b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py @@ -716,24 +716,14 @@ def handle_fn_if(self, intrinsic_value, ignore_errors): ------- This will return value_if_true and value_if_false depending on how the condition is evaluated """ - arguments = self.intrinsic_property_resolver( - intrinsic_value, ignore_errors, parent_function=IntrinsicResolver.FN_IF - ) - verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_IF) - verify_number_arguments(arguments, IntrinsicResolver.FN_IF, num=3) + verify_intrinsic_type_list(intrinsic_value, IntrinsicResolver.FN_IF) + verify_number_arguments(intrinsic_value, IntrinsicResolver.FN_IF, num=3) condition_name = self.intrinsic_property_resolver( - arguments[0], ignore_errors, parent_function=IntrinsicResolver.FN_IF + intrinsic_value[0], ignore_errors, parent_function=IntrinsicResolver.FN_IF ) verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_IF) - value_if_true = self.intrinsic_property_resolver( - arguments[1], ignore_errors, parent_function=IntrinsicResolver.FN_IF - ) - value_if_false = self.intrinsic_property_resolver( - arguments[2], ignore_errors, parent_function=IntrinsicResolver.FN_IF - ) - condition = self._conditions.get(condition_name) verify_intrinsic_type_dict( condition, @@ -750,7 +740,8 @@ def handle_fn_if(self, intrinsic_value, ignore_errors): message="The result of {} must evaluate to bool".format(IntrinsicResolver.FN_IF), ) - return value_if_true if condition_evaluated else value_if_false + selected_value = intrinsic_value[1] if condition_evaluated else intrinsic_value[2] + return self.intrinsic_property_resolver(selected_value, ignore_errors, parent_function=IntrinsicResolver.FN_IF) def handle_fn_equals(self, intrinsic_value, ignore_errors): """ diff --git a/tests/integration/local/invoke/test_integrations_cli.py b/tests/integration/local/invoke/test_integrations_cli.py index 95a0a95cea4..dfc011e8f7d 100644 --- a/tests/integration/local/invoke/test_integrations_cli.py +++ b/tests/integration/local/invoke/test_integrations_cli.py @@ -393,6 +393,27 @@ def test_invoke_with_env_using_parameters(self): self.assertEqual(environ["MyRuntimeVersion"], "v0") self.assertEqual(environ["EmptyDefaultParameter"], "") + @pytest.mark.flaky(reruns=3) + def test_invoke_with_env_using_fn_if_ignores_unresolvable_branch(self): + command_list = InvokeIntegBase.get_command_list( + "EchoEnvWithFnIf", + template_path=self.template_path, + event_path=self.event_path, + ) + + process = Popen(command_list, stdout=PIPE) + try: + stdout, _ = process.communicate(timeout=TIMEOUT) + except TimeoutExpired: + process.kill() + raise + + self.assertEqual(process.returncode, 0) + process_stdout = stdout.strip() + environ = json.loads(process_stdout.decode("utf-8")) + + self.assertEqual(environ["FunctionUrl"], "https://custom.example.com/") + @pytest.mark.flaky(reruns=3) def test_invoke_multi_tenant_function(self): command_list = InvokeIntegBase.get_command_list( diff --git a/tests/integration/testdata/invoke/template.yml b/tests/integration/testdata/invoke/template.yml index 61efc176eb5..2695fcf3587 100644 --- a/tests/integration/testdata/invoke/template.yml +++ b/tests/integration/testdata/invoke/template.yml @@ -26,6 +26,17 @@ Parameters: Type: String Default: "2" + UseCustomFunctionUrl: + Type: String + Default: "true" + + CustomFunctionUrl: + Type: String + Default: "https://custom.example.com/" + +Conditions: + ShouldUseCustomFunctionUrl: !Equals [!Ref UseCustomFunctionUrl, "true"] + Mappings: common: LambdaFunction: @@ -202,6 +213,30 @@ Resources: MyRuntimeVersion: !Ref MyRuntimeVersion EmptyDefaultParameter: !Ref EmptyDefaultParameter + FunctionWithUrlConfig: + Type: AWS::Serverless::Function + Properties: + Handler: main.handler + Runtime: python3.9 + CodeUri: . + Timeout: 600 + FunctionUrlConfig: + AuthType: NONE + + EchoEnvWithFnIf: + Type: AWS::Serverless::Function + Properties: + Handler: main.env_var_echo_hanler + Runtime: python3.9 + CodeUri: . + Timeout: 600 + Environment: + Variables: + FunctionUrl: !If + - ShouldUseCustomFunctionUrl + - !Ref CustomFunctionUrl + - !GetAtt FunctionWithUrlConfigUrl.FunctionUrl + TimeoutFunctionWithStringParameter: Type: AWS::Serverless::Function Properties: diff --git a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py index 39d342764de..0759c7a0bdc 100644 --- a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py +++ b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py @@ -3,7 +3,7 @@ from copy import deepcopy from pathlib import Path from unittest import TestCase -from unittest.mock import patch +from unittest.mock import MagicMock, patch from parameterized import parameterized @@ -906,6 +906,38 @@ def test_fn_if_condition_not_bool_fail(self): with self.assertRaises(InvalidIntrinsicException, msg="Invalid Condition"): self.resolver.intrinsic_property_resolver({"Fn::If": ["InvalidCondition", "test", "test"]}, True) + def test_fn_if_selects_resolvable_true_branch_ignoring_unresolvable_false_branch(self): + intrinsic = {"Fn::If": ["TestCondition", "resolved-value", {"Fn::GetAtt": ["Function2Url", "FunctionUrl"]}]} + + result = self.resolver.intrinsic_property_resolver(intrinsic, False) + self.assertEqual(result, "resolved-value") + + def test_fn_if_selects_resolvable_false_branch_ignoring_unresolvable_true_branch(self): + intrinsic = {"Fn::If": ["NotTestCondition", {"Fn::GetAtt": ["Function2Url", "FunctionUrl"]}, "resolved-value"]} + + result = self.resolver.intrinsic_property_resolver(intrinsic, False) + self.assertEqual(result, "resolved-value") + + def test_fn_if_does_not_evaluate_unselected_false_branch(self): + mock_handle_fn_getatt = MagicMock() + self.resolver.intrinsic_key_function_map[IntrinsicResolver.FN_GET_ATT] = mock_handle_fn_getatt + intrinsic = {"Fn::If": ["TestCondition", "resolved-value", {"Fn::GetAtt": ["Function2Url", "FunctionUrl"]}]} + + result = self.resolver.intrinsic_property_resolver(intrinsic, False) + + self.assertEqual(result, "resolved-value") + mock_handle_fn_getatt.assert_not_called() + + def test_fn_if_does_not_evaluate_unselected_true_branch(self): + mock_handle_fn_getatt = MagicMock() + self.resolver.intrinsic_key_function_map[IntrinsicResolver.FN_GET_ATT] = mock_handle_fn_getatt + intrinsic = {"Fn::If": ["NotTestCondition", {"Fn::GetAtt": ["Function2Url", "FunctionUrl"]}, "resolved-value"]} + + result = self.resolver.intrinsic_property_resolver(intrinsic, False) + + self.assertEqual(result, "resolved-value") + mock_handle_fn_getatt.assert_not_called() + class TestIntrinsicAttribteResolution(TestCase): def setUp(self): @@ -1013,6 +1045,22 @@ def test_template_ignore_errors(self): } self.assertEqual(expected_template, dict(result)) + def test_template_ignore_errors_leaves_unresolvable_layer_getatt_as_dict(self): + resources = deepcopy(self.resources) + resources["ReferenceLambdaLayerVersionLambdaFunction"]["Properties"]["Layers"] = [ + {"Fn::GetAtt": ["NestedStack", "Outputs.MyDepLayer"]} + ] + template = {"Mappings": self.mappings, "Conditions": self.conditions, "Resources": resources} + symbol_resolver = IntrinsicsSymbolTable(template=template, logical_id_translator=self.logical_id_translator) + resolver = IntrinsicResolver(template=template, symbol_resolver=symbol_resolver) + + result = resolver.resolve_attribute(resources, ignore_errors=True) + + self.assertEqual( + result["ReferenceLambdaLayerVersionLambdaFunction"]["Properties"]["Layers"], + [{"Fn::GetAtt": ["NestedStack", "Outputs.MyDepLayer"]}], + ) + class TestResolveTemplate(TestCase): def test_parameter_not_resolved(self):