From aec1c9b44e67e0148c61cc613889b0eb5e37e1a2 Mon Sep 17 00:00:00 2001 From: dryrun Date: Tue, 13 Oct 2026 16:11:25 -0700 Subject: [PATCH 1/2] fix: reject invalid WebFunction and NetworkConnector inputs instead of crashing Crashes (now InvalidDocumentException): - is_intrinsic() with a non-string single key (e.g. EnvironmentVariables, RevisionWeights or ReplicaRegions {1: ...}). - ReplicaRegions IncludeRegions/ExcludeRegions that aren't lists, and unrecognized ReplicaRegions keys of mixed types. - Non-string EnvironmentVariables keys and RevisionWeights revision IDs. - Non-string property names under Properties (any SAM resource). - Non-string Globals section names. - A null or non-object template. WebFunction EgressNetworkConnectorArn: - Reject a Ref or Fn::GetAtt on a network connector in the same template unless it uses LatestVersionArn (Ref and Arn return the unversioned ARN). - Reject non-string literals and include the expected ARN format and the given value in the error. NetworkConnector: name the allowed values for invalid ComputeTypes entries and VpcConfig.NetworkProtocol instead of "value is not a valid dict". bin/sam-translate.py exits 1 on an invalid template and no longer logs a generator object. --- bin/sam-translate.py | 3 +- samtranslator/model/__init__.py | 2 + samtranslator/model/intrinsics.py | 4 +- samtranslator/model/sam_resources.py | 86 ++++++++++++++++--- samtranslator/parser/parser.py | 3 +- samtranslator/plugins/globals/globals.py | 6 ++ tests/model/test_sam_resources.py | 38 ++++++++ tests/parser/test_parser.py | 5 ++ tests/test_intrinsics.py | 3 + .../error_globals_non_string_section.yaml | 12 +++ ...or_network_connector_invalid_protocol.yaml | 6 +- ...ork_connector_non_string_compute_type.yaml | 16 ++++ ...on_egress_network_connector_reference.yaml | 33 +++++++ ...or_webfunction_invalid_property_types.yaml | 65 ++++++++++++++ .../error_globals_non_string_section.json | 15 ++++ ...or_network_connector_invalid_protocol.json | 6 +- ...ork_connector_non_string_compute_type.json | 14 +++ ...on_egress_network_connector_reference.json | 21 +++++ ...or_webfunction_invalid_property_types.json | 41 +++++++++ 19 files changed, 356 insertions(+), 23 deletions(-) create mode 100644 tests/translator/input/error_globals_non_string_section.yaml create mode 100644 tests/translator/input/error_network_connector_non_string_compute_type.yaml create mode 100644 tests/translator/input/error_webfunction_egress_network_connector_reference.yaml create mode 100644 tests/translator/input/error_webfunction_invalid_property_types.yaml create mode 100644 tests/translator/output/error_globals_non_string_section.json create mode 100644 tests/translator/output/error_network_connector_non_string_compute_type.json create mode 100644 tests/translator/output/error_webfunction_egress_network_connector_reference.json create mode 100644 tests/translator/output/error_webfunction_invalid_property_types.json diff --git a/bin/sam-translate.py b/bin/sam-translate.py index 3ae9c4a98d..5aee7b7fd6 100755 --- a/bin/sam-translate.py +++ b/bin/sam-translate.py @@ -123,8 +123,7 @@ def transform_template(input_file_path: Path, output_file_path: Path, stdout: bo except InvalidDocumentException as e: error_message = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) LOG.error(error_message) - errors = (cause.message for cause in e.causes) - LOG.error(errors) + sys.exit(1) def deploy(template_file: Path) -> None: diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index 44a2bdaafb..568a50542d 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -243,6 +243,8 @@ def from_dict(cls, logical_id: str, resource_dict: dict[str, Any], relative_id: sam_plugins.act(LifeCycleEvents.before_transform_resource, logical_id, cls.resource_type, properties) for name, value in properties.items(): + if not isinstance(name, str): + raise InvalidResourceException(logical_id, f"Property names must be strings, got {name!r}.") setattr(resource, name, value) if "DependsOn" in resource_dict: diff --git a/samtranslator/model/intrinsics.py b/samtranslator/model/intrinsics.py index 4512ec28c4..c47bde01f7 100644 --- a/samtranslator/model/intrinsics.py +++ b/samtranslator/model/intrinsics.py @@ -147,8 +147,8 @@ def is_intrinsic(_input: Any) -> bool: """ if _input is not None and isinstance(_input, dict) and len(_input) == 1: - key: str = next(iter(_input.keys())) - return key in {"Ref", "Condition"} or key.startswith("Fn::") + key = next(iter(_input.keys())) + return isinstance(key, str) and (key in {"Ref", "Condition"} or key.startswith("Fn::")) return False diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index d7e50c6983..154d72dde0 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -1654,12 +1654,14 @@ class SamNetworkConnector(SamResourceMacro): __validation_rules__: list[Any] = [] _VALID_COMPUTE_TYPES = ("MicroVm", "WebFunction") + _VALID_NETWORK_PROTOCOLS = ("IPv4", "DualStack") @cw_timer def to_cloudformation(self, **kwargs: Any) -> list[Resource]: # Run the ComputeTypes check first so its actionable message wins at transform time; the schema # (validate_before_transform / cfn-lint) enforces the same rules for `sam validate --lint`. self._validate_compute_types() + self._validate_network_protocol() self.validate_before_transform( schema_class=aws_serverless_networkconnector.Properties, collect_all_errors=True, @@ -1693,12 +1695,26 @@ def _validate_compute_types(self) -> None: f"'ComputeTypes' must contain exactly one value, got {len(self.ComputeTypes)}.", ) for compute_type in self.ComputeTypes: - if isinstance(compute_type, str) and compute_type not in self._VALID_COMPUTE_TYPES: + if is_intrinsic(compute_type): + continue + if not isinstance(compute_type, str) or compute_type not in self._VALID_COMPUTE_TYPES: raise InvalidResourceException( self.logical_id, - f"'ComputeTypes' values must be one of {list(self._VALID_COMPUTE_TYPES)}, got '{compute_type}'.", + f"'ComputeTypes' values must be one of {list(self._VALID_COMPUTE_TYPES)}, got {compute_type!r}.", ) + def _validate_network_protocol(self) -> None: + # Name the allowed values; the schema's intrinsic-or-literal union reports "not a valid dict" instead. + network_protocol = self.VpcConfig.get("NetworkProtocol") if isinstance(self.VpcConfig, dict) else None + if network_protocol is None or is_intrinsic(network_protocol): + return + if network_protocol not in self._VALID_NETWORK_PROTOCOLS: + raise InvalidResourceException( + self.logical_id, + f"'VpcConfig.NetworkProtocol' must be one of {list(self._VALID_NETWORK_PROTOCOLS)}, " + f"got {network_protocol!r}.", + ) + class SamMicroVMImage(SamResourceMacro): """SAM MicrovmImage resource transformer""" @@ -1776,6 +1792,24 @@ def to_cloudformation(self, **kwargs: Any) -> list[Resource]: return resources +_NETWORK_CONNECTOR_TYPES = ("AWS::Serverless::NetworkConnector", "AWS::Lambda::NetworkConnector") + + +def _parse_ref_or_get_att(value: Any) -> tuple[str, str | None] | None: + """Return (logical ID, attribute) for a Ref (attribute None) or Fn::GetAtt, otherwise None.""" + if not is_intrinsic(value): + return None + if "Ref" in value: + target = value["Ref"] + return (target, None) if isinstance(target, str) else None + get_att = value.get("Fn::GetAtt") + if isinstance(get_att, str): + get_att = get_att.split(".", 1) + if isinstance(get_att, list) and len(get_att) == 2 and all(isinstance(x, str) for x in get_att): # noqa: PLR2004 + return get_att[0], get_att[1] + return None + + class SamWebFunction(SamResourceMacro): """ SAM resource for AWS::Serverless::WebFunction. @@ -1839,6 +1873,7 @@ def to_cloudformation(self, **kwargs: Any) -> list[Any]: """Transforms this SAM WebFunction into CloudFormation resources.""" intrinsics_resolver: IntrinsicsResolver = kwargs["intrinsics_resolver"] self._validate(intrinsics_resolver) + self._validate_egress_network_connector_reference(kwargs.get("resource_resolver")) resources: list[Any] = [] @@ -1861,7 +1896,7 @@ def to_cloudformation(self, **kwargs: Any) -> list[Any]: return resources - def _validate(self, intrinsics_resolver: IntrinsicsResolver) -> None: # noqa: PLR0912 + def _validate(self, intrinsics_resolver: IntrinsicsResolver) -> None: # noqa: PLR0912, PLR0915 valid_auto_deploy_modes = ("LatestRevision", "Disabled") # AutoDeploymentMode is a SAM-only property enforced entirely at transform time and never passed @@ -1957,6 +1992,8 @@ def _validate(self, intrinsics_resolver: IntrinsicsResolver) -> None: # noqa: P 'revision IDs to weights (e.g., {"rev-id": 100}). Intrinsic functions may be used for ' "individual weight values.", ) + if not all(isinstance(revision_id, str) for revision_id in self.RevisionWeights): + raise InvalidResourceException(self.logical_id, "'RevisionWeights' keys must be revision ID strings.") _MIN_WEIGHT = 1 _MAX_WEIGHT = 100 weights = list(self.RevisionWeights.values()) @@ -1994,7 +2031,7 @@ def _validate(self, intrinsics_resolver: IntrinsicsResolver) -> None: # noqa: P for key, value in self.EnvironmentVariables.items(): # Match the service key pattern exactly (leading letter required, no leading underscore) so # invalid keys fail here with a clear message instead of at stack creation. - if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", key): + if not isinstance(key, str) or not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", key): raise InvalidResourceException( self.logical_id, f"Environment variable key '{key}' must match pattern '^[a-zA-Z][a-zA-Z0-9_]*$'.", @@ -2018,15 +2055,40 @@ def _validate_egress_network_connector_arn(self) -> None: # value -- a network-connector ARN with a non-empty trailing segment -- without pinning the connector # name or qualifier grammar (SAM shouldn't guess at the service's naming/versioning charset, or it # would hard-fail otherwise-valid ARNs). Intrinsics (e.g. a Fn::GetAtt on the connector's - # LatestVersionArn) are passed through and validated by the service. + # LatestVersionArn) are passed through; references to connectors in the same template are checked in + # _validate_egress_network_connector_reference. + value = self.EgressNetworkConnectorArn + if value is None or is_intrinsic(value): + return network_connector_arn_pattern = r"^arn:[^:]+:lambda:[^:]*:[^:]*:network-connector:[^:]+:[^:]+$" - if isinstance(self.EgressNetworkConnectorArn, str) and not re.match( - network_connector_arn_pattern, self.EgressNetworkConnectorArn - ): + if not isinstance(value, str) or not re.match(network_connector_arn_pattern, value): raise InvalidResourceException( self.logical_id, "'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN " - "(ending in ':', e.g. the connector's LatestVersionArn).", + "('arn::lambda:::network-connector::', e.g. the " + f"connector's LatestVersionArn), got {value!r}.", + ) + + def _validate_egress_network_connector_reference(self, resource_resolver: ResourceResolver | None) -> None: + """Check a Ref or Fn::GetAtt on a network connector defined in the same template. + + Ref and the Arn attribute return the unversioned connector ARN, which the service rejects, so only + LatestVersionArn is allowed. + """ + reference = _parse_ref_or_get_att(self.EgressNetworkConnectorArn) + if resource_resolver is None or reference is None: + return + target, attribute = reference + connector = resource_resolver.get_resource_by_logical_id(target) + if not isinstance(connector, dict) or connector.get("Type") not in _NETWORK_CONNECTOR_TYPES: + return + + if attribute != "LatestVersionArn": + used = f"!Ref {target}" if attribute is None else f"!GetAtt {target}.{attribute}" + raise InvalidResourceException( + self.logical_id, + f"'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN; {used} " + f"doesn't return one. Use !GetAtt {target}.LatestVersionArn instead.", ) def _construct_function_name(self) -> Any: @@ -2196,7 +2258,7 @@ def _construct_region_list(self) -> list[Intrinsicable[str]] | None: if unknown_keys: raise InvalidResourceException( self.logical_id, - f"'ReplicaRegions' contains unrecognized keys: {sorted(unknown_keys)}. Valid keys are: IncludeRegions, ExcludeRegions.", + f"'ReplicaRegions' contains unrecognized keys: {sorted(map(str, unknown_keys))}. Valid keys are: IncludeRegions, ExcludeRegions.", ) # ExcludeRegions is only meaningful as a filter over an explicit inclusion set. Without IncludeRegions @@ -2215,6 +2277,10 @@ def _construct_region_list(self) -> list[Intrinsicable[str]] | None: "'IncludeRegions' and 'ExcludeRegions' do not support top-level intrinsic functions.", ) + for key, value in (("IncludeRegions", include_regions), ("ExcludeRegions", exclude_regions)): + if not isinstance(value, list): + raise InvalidResourceException(self.logical_id, f"'ReplicaRegions.{key}' must be a list.") + # Separate string entries from intrinsics in both lists include_strings = [r for r in include_regions if isinstance(r, str)] include_intrinsics = [r for r in include_regions if not isinstance(r, str)] diff --git a/samtranslator/parser/parser.py b/samtranslator/parser/parser.py index a032dd45bb..747c1c112b 100644 --- a/samtranslator/parser/parser.py +++ b/samtranslator/parser/parser.py @@ -26,7 +26,8 @@ def parse(self, sam_template: dict[str, Any], parameter_values: dict[str, Any], def validate_datatypes(sam_template): # type: ignore[no-untyped-def] """Validates the datatype within the template""" if ( - "Resources" not in sam_template + not isinstance(sam_template, dict) + or "Resources" not in sam_template or not isinstance(sam_template["Resources"], dict) or not sam_template["Resources"] ): diff --git a/samtranslator/plugins/globals/globals.py b/samtranslator/plugins/globals/globals.py index 329096489a..47b5ba1f11 100644 --- a/samtranslator/plugins/globals/globals.py +++ b/samtranslator/plugins/globals/globals.py @@ -314,6 +314,12 @@ def _parse(self, globals_dict): # type: ignore[no-untyped-def] raise InvalidGlobalsSectionException(self._KEYWORD, "It must be a non-empty dictionary") for section_name, properties in globals_dict.items(): + if not isinstance(section_name, str): + raise InvalidGlobalsSectionException( + self._KEYWORD, + f"'{section_name}' is not supported. " + f"Must be one of the following values - {self.supported_resource_section_names}", + ) resource_type = self._make_resource_type(section_name) # type: ignore[no-untyped-call] if resource_type not in self.supported_properties: diff --git a/tests/model/test_sam_resources.py b/tests/model/test_sam_resources.py index 0dd99b676a..a6bfd55a02 100644 --- a/tests/model/test_sam_resources.py +++ b/tests/model/test_sam_resources.py @@ -772,6 +772,44 @@ def test_egress_network_connector_arn_intrinsic_passthrough(self): {"Fn::GetAtt": ["MyConnector", "LatestVersionArn"]}, ) + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_egress_network_connector_latest_version_arn_same_template(self): + connector = { + "Type": "AWS::Serverless::NetworkConnector", + "Properties": {"ComputeTypes": ["WebFunction"], "VpcConfig": {}}, + } + function = SamWebFunction("foo") + function.CodeUri = "s3://foobar/foo.zip" + function.Runtime = "bar" + function.AuthType = "IamAuth" + function.EgressNetworkConnectorArn = {"Fn::GetAtt": ["MyConnector", "LatestVersionArn"]} + kwargs = {**self.kwargs, "resource_resolver": ResourceResolver({"MyConnector": connector})} + cfnResources = function.to_cloudformation(**kwargs) + revisions = [x for x in cfnResources if isinstance(x, LambdaWebFunctionRevision)] + self.assertEqual( + revisions[0].ServiceConfig["EgressNetworkConnectorArn"], + {"Fn::GetAtt": ["MyConnector", "LatestVersionArn"]}, + ) + + @parameterized.expand( + [ + ({"Ref": "MyConnector"}, {"ComputeTypes": ["WebFunction"]}), + ({"Fn::GetAtt": "MyConnector.Arn"}, {"ComputeTypes": ["WebFunction"]}), + ({"Fn::GetAtt": ["MyConnector", "Bogus"]}, {"ComputeTypes": ["WebFunction"]}), + ] + ) + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_egress_network_connector_invalid_same_template_reference_raises(self, arn, connector_properties): + connector = {"Type": "AWS::Serverless::NetworkConnector", "Properties": connector_properties} + function = SamWebFunction("foo") + function.CodeUri = "s3://foobar/foo.zip" + function.Runtime = "bar" + function.AuthType = "IamAuth" + function.EgressNetworkConnectorArn = arn + kwargs = {**self.kwargs, "resource_resolver": ResourceResolver({"MyConnector": connector})} + with pytest.raises(InvalidResourceException): + function.to_cloudformation(**kwargs) + @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_endpoint_defaults(self): function = SamWebFunction("foo") diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index fde6180a02..48ad334a7f 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -38,3 +38,8 @@ def test_validate_template_with_empty_resources(self): parser = Parser() with self.assertRaises(InvalidDocumentException): parser._validate({"Resources": {}}, {}) + + def test_validate_template_that_is_not_a_dict(self): + parser = Parser() + with self.assertRaises(InvalidDocumentException): + parser._validate(None, {}) diff --git a/tests/test_intrinsics.py b/tests/test_intrinsics.py index a542309527..faae95d8b8 100644 --- a/tests/test_intrinsics.py +++ b/tests/test_intrinsics.py @@ -24,6 +24,9 @@ def test_is_intrinsic_on_empty_input(self): def test_is_intrinsic_on_non_dict_input(self): self.assertFalse(is_intrinsic([1, 2, 3])) + def test_is_intrinsic_on_non_string_key(self): + self.assertFalse(is_intrinsic({1: "a"})) + def test_is_intrinsic_on_intrinsic_like_dict_input(self): self.assertFalse(is_intrinsic({"Ref": "foo", "key": "bar"})) diff --git a/tests/translator/input/error_globals_non_string_section.yaml b/tests/translator/input/error_globals_non_string_section.yaml new file mode 100644 index 0000000000..e288429545 --- /dev/null +++ b/tests/translator/input/error_globals_non_string_section.yaml @@ -0,0 +1,12 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Globals: + 5: {} + +Resources: + Function: + Type: AWS::Serverless::Function + Properties: + Runtime: python3.13 + Handler: index.handler + InlineCode: def handler(event, context) -> None diff --git a/tests/translator/input/error_network_connector_invalid_protocol.yaml b/tests/translator/input/error_network_connector_invalid_protocol.yaml index 48a1269798..d5f19a73a1 100644 --- a/tests/translator/input/error_network_connector_invalid_protocol.yaml +++ b/tests/translator/input/error_network_connector_invalid_protocol.yaml @@ -1,10 +1,6 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 -Description: > - Test invalid NetworkProtocol enum value. - Note: error message says "not a valid dict" due to SamIntrinsicable union type - matching - Pydantic reports the dict branch failure rather than the Literal mismatch. - This is a known limitation of the SAM validation framework. +Description: Test invalid NetworkProtocol enum value Resources: NetworkConnectorInvalidProtocol: diff --git a/tests/translator/input/error_network_connector_non_string_compute_type.yaml b/tests/translator/input/error_network_connector_non_string_compute_type.yaml new file mode 100644 index 0000000000..36959d6f40 --- /dev/null +++ b/tests/translator/input/error_network_connector_non_string_compute_type.yaml @@ -0,0 +1,16 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: NetworkConnector with a non-string ComputeTypes value + +Resources: + NetworkConnectorNonStringComputeType: + Type: AWS::Serverless::NetworkConnector + Properties: + ComputeTypes: + - 5 + VpcConfig: + SubnetIds: + - subnet-abc + SecurityGroupIds: + - sg-123 + NetworkProtocol: IPv4 diff --git a/tests/translator/input/error_webfunction_egress_network_connector_reference.yaml b/tests/translator/input/error_webfunction_egress_network_connector_reference.yaml new file mode 100644 index 0000000000..69f4220b91 --- /dev/null +++ b/tests/translator/input/error_webfunction_egress_network_connector_reference.yaml @@ -0,0 +1,33 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: EgressNetworkConnectorArn references to connectors in the same template + that don't return a version-qualified ARN + +Resources: + WebFunctionConnector: + Type: AWS::Serverless::NetworkConnector + Properties: + ComputeTypes: + - WebFunction + VpcConfig: + SubnetIds: + - subnet-abc + SecurityGroupIds: + - sg-123 + NetworkProtocol: IPv4 + + WebFunctionRefConnector: + Type: AWS::Serverless::WebFunction + Properties: + Runtime: nodejs24.x + AuthType: IamAuth + CodeUri: s3://bucket/key.zip + EgressNetworkConnectorArn: !Ref WebFunctionConnector + + WebFunctionGetAttArn: + Type: AWS::Serverless::WebFunction + Properties: + Runtime: nodejs24.x + AuthType: IamAuth + CodeUri: s3://bucket/key.zip + EgressNetworkConnectorArn: !GetAtt WebFunctionConnector.Arn diff --git a/tests/translator/input/error_webfunction_invalid_property_types.yaml b/tests/translator/input/error_webfunction_invalid_property_types.yaml new file mode 100644 index 0000000000..ce7ad07220 --- /dev/null +++ b/tests/translator/input/error_webfunction_invalid_property_types.yaml @@ -0,0 +1,65 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: WebFunction properties with the wrong key or value types + +Resources: + WebFunctionIntegerEnvKey: + Type: AWS::Serverless::WebFunction + Properties: + Runtime: nodejs24.x + AuthType: IamAuth + CodeUri: s3://bucket/key.zip + EnvironmentVariables: + 1: a + B: c + + WebFunctionIntegerRevisionId: + Type: AWS::Serverless::WebFunction + Properties: + Runtime: nodejs24.x + AuthType: IamAuth + CodeUri: s3://bucket/key.zip + AutoDeploymentMode: Disabled + RevisionWeights: + 1: 100 + + WebFunctionScalarIncludeRegions: + Type: AWS::Serverless::WebFunction + Properties: + Runtime: nodejs24.x + AuthType: IamAuth + CodeUri: s3://bucket/key.zip + EndpointType: MultiRegion + AutoDeploymentMode: Disabled + ReplicaRegions: + IncludeRegions: us-east-1 + + WebFunctionMixedReplicaRegionsKeys: + Type: AWS::Serverless::WebFunction + Properties: + Runtime: nodejs24.x + AuthType: IamAuth + CodeUri: s3://bucket/key.zip + EndpointType: MultiRegion + AutoDeploymentMode: Disabled + ReplicaRegions: + IncludeRegions: + - US + 1: 2 + Foo: 3 + + WebFunctionIntegerEgressArn: + Type: AWS::Serverless::WebFunction + Properties: + Runtime: nodejs24.x + AuthType: IamAuth + CodeUri: s3://bucket/key.zip + EgressNetworkConnectorArn: 5 + + WebFunctionIntegerPropertyName: + Type: AWS::Serverless::WebFunction + Properties: + Runtime: nodejs24.x + AuthType: IamAuth + CodeUri: s3://bucket/key.zip + 5: value diff --git a/tests/translator/output/error_globals_non_string_section.json b/tests/translator/output/error_globals_non_string_section.json new file mode 100644 index 0000000000..a3a799a69c --- /dev/null +++ b/tests/translator/output/error_globals_non_string_section.json @@ -0,0 +1,15 @@ +{ + "_autoGeneratedBreakdownErrorMessage": [ + "Invalid Serverless Application Specification document. ", + "Number of errors found: 1. ", + "'Globals' section is invalid. ", + "'5' is not supported. ", + "Must be one of the following values - ['Api', 'CapacityProvider', 'Function', 'HttpApi', 'LayerVersion', 'MicrovmImage', 'NetworkConnector', 'SimpleTable', 'StateMachine', 'WebSocketApi']" + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. 'Globals' section is invalid. '5' is not supported. Must be one of the following values - ['Api', 'CapacityProvider', 'Function', 'HttpApi', 'LayerVersion', 'MicrovmImage', 'NetworkConnector', 'SimpleTable', 'StateMachine', 'WebSocketApi']", + "errors": [ + { + "errorMessage": "'Globals' section is invalid. '5' is not supported. Must be one of the following values - ['Api', 'CapacityProvider', 'Function', 'HttpApi', 'LayerVersion', 'MicrovmImage', 'NetworkConnector', 'SimpleTable', 'StateMachine', 'WebSocketApi']" + } + ] +} diff --git a/tests/translator/output/error_network_connector_invalid_protocol.json b/tests/translator/output/error_network_connector_invalid_protocol.json index 709fb392f4..b579fef572 100644 --- a/tests/translator/output/error_network_connector_invalid_protocol.json +++ b/tests/translator/output/error_network_connector_invalid_protocol.json @@ -3,12 +3,12 @@ "Invalid Serverless Application Specification document. ", "Number of errors found: 1. ", "Resource with id [NetworkConnectorInvalidProtocol] is invalid. ", - "Property 'VpcConfig.NetworkProtocol' value is not a valid dict." + "'VpcConfig.NetworkProtocol' must be one of ['IPv4', 'DualStack'], got 'IPv6'." ], - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [NetworkConnectorInvalidProtocol] is invalid. Property 'VpcConfig.NetworkProtocol' value is not a valid dict.", + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [NetworkConnectorInvalidProtocol] is invalid. 'VpcConfig.NetworkProtocol' must be one of ['IPv4', 'DualStack'], got 'IPv6'.", "errors": [ { - "errorMessage": "Resource with id [NetworkConnectorInvalidProtocol] is invalid. Property 'VpcConfig.NetworkProtocol' value is not a valid dict." + "errorMessage": "Resource with id [NetworkConnectorInvalidProtocol] is invalid. 'VpcConfig.NetworkProtocol' must be one of ['IPv4', 'DualStack'], got 'IPv6'." } ] } diff --git a/tests/translator/output/error_network_connector_non_string_compute_type.json b/tests/translator/output/error_network_connector_non_string_compute_type.json new file mode 100644 index 0000000000..594527b7da --- /dev/null +++ b/tests/translator/output/error_network_connector_non_string_compute_type.json @@ -0,0 +1,14 @@ +{ + "_autoGeneratedBreakdownErrorMessage": [ + "Invalid Serverless Application Specification document. ", + "Number of errors found: 1. ", + "Resource with id [NetworkConnectorNonStringComputeType] is invalid. ", + "'ComputeTypes' values must be one of ['MicroVm', 'WebFunction'], got 5." + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [NetworkConnectorNonStringComputeType] is invalid. 'ComputeTypes' values must be one of ['MicroVm', 'WebFunction'], got 5.", + "errors": [ + { + "errorMessage": "Resource with id [NetworkConnectorNonStringComputeType] is invalid. 'ComputeTypes' values must be one of ['MicroVm', 'WebFunction'], got 5." + } + ] +} diff --git a/tests/translator/output/error_webfunction_egress_network_connector_reference.json b/tests/translator/output/error_webfunction_egress_network_connector_reference.json new file mode 100644 index 0000000000..016df8bc13 --- /dev/null +++ b/tests/translator/output/error_webfunction_egress_network_connector_reference.json @@ -0,0 +1,21 @@ +{ + "_autoGeneratedBreakdownErrorMessage": [ + "Invalid Serverless Application Specification document. ", + "Number of errors found: 2. ", + "Resource with id [WebFunctionGetAttArn] is invalid. ", + "'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN; !GetAtt WebFunctionConnector.Arn doesn't return one. ", + "Use !GetAtt WebFunctionConnector.LatestVersionArn instead. ", + "Resource with id [WebFunctionRefConnector] is invalid. ", + "'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN; !Ref WebFunctionConnector doesn't return one. ", + "Use !GetAtt WebFunctionConnector.LatestVersionArn instead." + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 2. Resource with id [WebFunctionGetAttArn] is invalid. 'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN; !GetAtt WebFunctionConnector.Arn doesn't return one. Use !GetAtt WebFunctionConnector.LatestVersionArn instead. Resource with id [WebFunctionRefConnector] is invalid. 'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN; !Ref WebFunctionConnector doesn't return one. Use !GetAtt WebFunctionConnector.LatestVersionArn instead.", + "errors": [ + { + "errorMessage": "Resource with id [WebFunctionRefConnector] is invalid. 'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN; !Ref WebFunctionConnector doesn't return one. Use !GetAtt WebFunctionConnector.LatestVersionArn instead." + }, + { + "errorMessage": "Resource with id [WebFunctionGetAttArn] is invalid. 'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN; !GetAtt WebFunctionConnector.Arn doesn't return one. Use !GetAtt WebFunctionConnector.LatestVersionArn instead." + } + ] +} diff --git a/tests/translator/output/error_webfunction_invalid_property_types.json b/tests/translator/output/error_webfunction_invalid_property_types.json new file mode 100644 index 0000000000..8374a85afe --- /dev/null +++ b/tests/translator/output/error_webfunction_invalid_property_types.json @@ -0,0 +1,41 @@ +{ + "_autoGeneratedBreakdownErrorMessage": [ + "Invalid Serverless Application Specification document. ", + "Number of errors found: 6. ", + "Resource with id [WebFunctionIntegerEgressArn] is invalid. ", + "'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN ('arn::lambda:::network-connector::', e.g. ", + "the connector's LatestVersionArn), got 5. ", + "Resource with id [WebFunctionIntegerEnvKey] is invalid. ", + "Environment variable key '1' must match pattern '^[a-zA-Z][a-zA-Z0-9_]*$'. ", + "Resource with id [WebFunctionIntegerPropertyName] is invalid. ", + "Property names must be strings, got 5. ", + "Resource with id [WebFunctionIntegerRevisionId] is invalid. ", + "'RevisionWeights' keys must be revision ID strings. ", + "Resource with id [WebFunctionMixedReplicaRegionsKeys] is invalid. ", + "'ReplicaRegions' contains unrecognized keys: ['1', 'Foo']. ", + "Valid keys are: IncludeRegions, ExcludeRegions. ", + "Resource with id [WebFunctionScalarIncludeRegions] is invalid. ", + "'ReplicaRegions.IncludeRegions' must be a list." + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 6. Resource with id [WebFunctionIntegerEgressArn] is invalid. 'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN ('arn::lambda:::network-connector::', e.g. the connector's LatestVersionArn), got 5. Resource with id [WebFunctionIntegerEnvKey] is invalid. Environment variable key '1' must match pattern '^[a-zA-Z][a-zA-Z0-9_]*$'. Resource with id [WebFunctionIntegerPropertyName] is invalid. Property names must be strings, got 5. Resource with id [WebFunctionIntegerRevisionId] is invalid. 'RevisionWeights' keys must be revision ID strings. Resource with id [WebFunctionMixedReplicaRegionsKeys] is invalid. 'ReplicaRegions' contains unrecognized keys: ['1', 'Foo']. Valid keys are: IncludeRegions, ExcludeRegions. Resource with id [WebFunctionScalarIncludeRegions] is invalid. 'ReplicaRegions.IncludeRegions' must be a list.", + "errors": [ + { + "errorMessage": "Resource with id [WebFunctionIntegerEnvKey] is invalid. Environment variable key '1' must match pattern '^[a-zA-Z][a-zA-Z0-9_]*$'." + }, + { + "errorMessage": "Resource with id [WebFunctionIntegerRevisionId] is invalid. 'RevisionWeights' keys must be revision ID strings." + }, + { + "errorMessage": "Resource with id [WebFunctionScalarIncludeRegions] is invalid. 'ReplicaRegions.IncludeRegions' must be a list." + }, + { + "errorMessage": "Resource with id [WebFunctionMixedReplicaRegionsKeys] is invalid. 'ReplicaRegions' contains unrecognized keys: ['1', 'Foo']. Valid keys are: IncludeRegions, ExcludeRegions." + }, + { + "errorMessage": "Resource with id [WebFunctionIntegerEgressArn] is invalid. 'EgressNetworkConnectorArn' must be a version-qualified network-connector ARN ('arn::lambda:::network-connector::', e.g. the connector's LatestVersionArn), got 5." + }, + { + "errorMessage": "Resource with id [WebFunctionIntegerPropertyName] is invalid. Property names must be strings, got 5." + } + ] +} From 771b4a30dc9c61b1c15a4330065024b97e2dbcc4 Mon Sep 17 00:00:00 2001 From: dryrun Date: Tue, 13 Oct 2026 16:56:31 -0700 Subject: [PATCH 2/2] fix: Ref the WebFunction for the revision and endpoint FunctionName The revision and endpoint repeated the function name instead of Ref on the function, to work around a service bug where a FunctionName given as the function ARN could be created but not deleted. That bug is fixed. Always Ref the function (its ARN) for the revision and endpoint FunctionName, even when FunctionName is set, like AWS::Lambda::Version does. A FunctionName change then only has to be made on the AWS::Lambda::WebFunction. The AuthType casing fix and the "name" runtime attribute from the original workaround are unchanged. --- .../internal/schema_source/sam-docs.json | 2 +- samtranslator/model/sam_resources.py | 14 ++--- samtranslator/schema/schema.json | 2 +- schema_source/sam.schema.json | 2 +- tests/model/test_sam_resources.py | 19 +++--- .../output/aws-cn/webfunction_basic.json | 58 +------------------ .../aws-cn/webfunction_multiregion.json | 58 +------------------ .../webfunction_with_all_properties.json | 58 +------------------ .../output/aws-us-gov/webfunction_basic.json | 58 +------------------ .../aws-us-gov/webfunction_multiregion.json | 58 +------------------ .../webfunction_with_all_properties.json | 58 +------------------ .../translator/output/webfunction_basic.json | 58 +------------------ .../output/webfunction_multiregion.json | 58 +------------------ .../webfunction_with_all_properties.json | 58 +------------------ 14 files changed, 33 insertions(+), 528 deletions(-) diff --git a/samtranslator/internal/schema_source/sam-docs.json b/samtranslator/internal/schema_source/sam-docs.json index 5cc070e18c..3e8d28affa 100644 --- a/samtranslator/internal/schema_source/sam-docs.json +++ b/samtranslator/internal/schema_source/sam-docs.json @@ -978,7 +978,7 @@ "EntryPoint": "The file that the runtime runs, relative to the root of the deployment package. AWS SAM passes it to the runtime in the reserved `AWS_LAMBDA_NODEJS_ENTRYPOINT` environment variable. \nYou can't use this property when `EnvironmentVariables` is an intrinsic function. \n*Type*: String \n*Required*: No \n*Default*: `index.js` \n*CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an CloudFormation equivalent.", "EnvironmentVariables": "Key-value pairs to set as environment variables for the revision. \n+ Keys must start with a letter and contain only letters, numbers, and underscores.\n+ Values must be non-empty and at most 4,096 characters.\n+ AWS SAM converts number and Boolean values to strings.\n*Type*: Map \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`ServiceConfig.EnvironmentVariables`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-lambda-webfunctionrevision-serviceconfig.html#cfn-lambda-webfunctionrevision-serviceconfig-environmentvariables) property of an `AWS::Lambda::WebFunctionRevision` resource.", "ExecutionRoleArn": "The ARN of the IAM role that the function assumes when it runs. If you don't specify a role, AWS SAM creates one with the `AWSLambdaBasicExecutionRole` managed policy and a logical ID of `Role`. \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`ServiceConfig.ExecutionRoleArn`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-lambda-webfunctionrevision-serviceconfig.html#cfn-lambda-webfunctionrevision-serviceconfig-executionrolearn) property of an `AWS::Lambda::WebFunctionRevision` resource.", - "FunctionName": "The name of the function. Function names must be unique within an account across all Regions. If you don't specify a name, AWS SAM generates one from the logical ID and the stack ID, at most 64 characters. Changing the name replaces the function, its revision, and its endpoint. \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`FunctionName`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-lambda-webfunction.html#cfn-lambda-webfunction-functionname) property of the `AWS::Lambda::WebFunction`, `AWS::Lambda::WebFunctionRevision`, and `AWS::Lambda::WebFunctionEndpoint` resources.", + "FunctionName": "The name of the function. Function names must be unique within an account across all Regions. If you don't specify a name, AWS SAM generates one from the logical ID and the stack ID, at most 64 characters. Changing the name replaces the function, its revision, and its endpoint. \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`FunctionName`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-lambda-webfunction.html#cfn-lambda-webfunction-functionname) property of an `AWS::Lambda::WebFunction` resource. The generated `AWS::Lambda::WebFunctionRevision` and `AWS::Lambda::WebFunctionEndpoint` resources reference the function with `Ref`.", "InlineCode": "Not supported for `AWS::Serverless::WebFunction`. Use [CodeUri](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-webfunction.html#sam-webfunction-codeuri). \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an CloudFormation equivalent.", "KmsKeyArn": "The ARN of the AWS KMS key used to encrypt the revision's environment variables. \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`KmsKeyArn`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-lambda-webfunctionrevision.html#cfn-lambda-webfunctionrevision-kmskeyarn) property of an `AWS::Lambda::WebFunctionRevision` resource.", "LoggingConfig": "The logging configuration for the revision, such as the log group and log levels. \n*Type*: Map \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`ServiceConfig.TelemetryConfig.LoggingConfig`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-lambda-webfunctionrevision-telemetryconfig.html#cfn-lambda-webfunctionrevision-telemetryconfig-loggingconfig) property of an `AWS::Lambda::WebFunctionRevision` resource.", diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 154d72dde0..b710c1fd93 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -2092,13 +2092,7 @@ def _validate_egress_network_connector_reference(self, resource_resolver: Resour ) def _construct_function_name(self) -> Any: - """Return the function name shared by the WebFunction, its Revision, and its Endpoint. - - Ref on AWS::Lambda::WebFunction resolves to the function ARN, not the name, so the Revision and - Endpoint must reuse this exact value rather than Ref the function. Referencing it with Ref would set - their FunctionName to the ARN, which the service rejects (the endpoint/revision then cannot be - deleted: the stored ARN does not match the name parsed from it). - """ + """Return the FunctionName for the AWS::Lambda::WebFunction: the user value or a generated name.""" if self.FunctionName: return self.FunctionName # Generate a name bounded by construction to the 64-char AWS::Lambda::WebFunction FunctionName @@ -2143,7 +2137,9 @@ def _construct_revision(self, execution_role_arn: Any) -> LambdaWebFunctionRevis revision = LambdaWebFunctionRevision( f"{self.logical_id}Revision", attributes=self.get_passthrough_resource_attributes() ) - revision.FunctionName = self._construct_function_name() + # Ref the function (its ARN) instead of repeating the name, like AWS::Lambda::Version does, so a + # FunctionName change only has to be made on the AWS::Lambda::WebFunction. + revision.FunctionName = ref(self.logical_id) revision.Description = self.RevisionDescription revision.KmsKeyArn = self.KmsKeyArn revision.BuildConfig = self._construct_build_config() @@ -2154,7 +2150,7 @@ def _construct_endpoint(self, revision: LambdaWebFunctionRevision) -> LambdaWebF endpoint = LambdaWebFunctionEndpoint( f"{self.logical_id}Endpoint", attributes=self.get_passthrough_resource_attributes() ) - endpoint.FunctionName = self._construct_function_name() + endpoint.FunctionName = ref(self.logical_id) endpoint.EndpointName = self.EndpointName or "prod" endpoint.EndpointType = self.EndpointType or "HomeRegion" # AuthType is required and has no default (validated in _validate); pass the customer's value through. diff --git a/samtranslator/schema/schema.json b/samtranslator/schema/schema.json index aba09809fe..4ceb4bbcb1 100644 --- a/samtranslator/schema/schema.json +++ b/samtranslator/schema/schema.json @@ -437239,7 +437239,7 @@ "type": "string" } ], - "markdownDescription": "The name of the function. Function names must be unique within an account across all Regions. If you don't specify a name, AWS SAM generates one from the logical ID and the stack ID, at most 64 characters. Changing the name replaces the function, its revision, and its endpoint. \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`FunctionName`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-lambda-webfunction.html#cfn-lambda-webfunction-functionname) property of the `AWS::Lambda::WebFunction`, `AWS::Lambda::WebFunctionRevision`, and `AWS::Lambda::WebFunctionEndpoint` resources.", + "markdownDescription": "The name of the function. Function names must be unique within an account across all Regions. If you don't specify a name, AWS SAM generates one from the logical ID and the stack ID, at most 64 characters. Changing the name replaces the function, its revision, and its endpoint. \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`FunctionName`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-lambda-webfunction.html#cfn-lambda-webfunction-functionname) property of an `AWS::Lambda::WebFunction` resource. The generated `AWS::Lambda::WebFunctionRevision` and `AWS::Lambda::WebFunctionEndpoint` resources reference the function with `Ref`.", "title": "FunctionName" }, "InlineCode": { diff --git a/schema_source/sam.schema.json b/schema_source/sam.schema.json index 70435ed8f7..63ce759f43 100644 --- a/schema_source/sam.schema.json +++ b/schema_source/sam.schema.json @@ -11340,7 +11340,7 @@ "type": "string" } ], - "markdownDescription": "The name of the function. Function names must be unique within an account across all Regions. If you don't specify a name, AWS SAM generates one from the logical ID and the stack ID, at most 64 characters. Changing the name replaces the function, its revision, and its endpoint. \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`FunctionName`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-lambda-webfunction.html#cfn-lambda-webfunction-functionname) property of the `AWS::Lambda::WebFunction`, `AWS::Lambda::WebFunctionRevision`, and `AWS::Lambda::WebFunctionEndpoint` resources.", + "markdownDescription": "The name of the function. Function names must be unique within an account across all Regions. If you don't specify a name, AWS SAM generates one from the logical ID and the stack ID, at most 64 characters. Changing the name replaces the function, its revision, and its endpoint. \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is passed directly to the [`FunctionName`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-lambda-webfunction.html#cfn-lambda-webfunction-functionname) property of an `AWS::Lambda::WebFunction` resource. The generated `AWS::Lambda::WebFunctionRevision` and `AWS::Lambda::WebFunctionEndpoint` resources reference the function with `Ref`.", "title": "FunctionName" }, "InlineCode": { diff --git a/tests/model/test_sam_resources.py b/tests/model/test_sam_resources.py index a6bfd55a02..90e894dab2 100644 --- a/tests/model/test_sam_resources.py +++ b/tests/model/test_sam_resources.py @@ -609,22 +609,17 @@ def test_generated_function_name_bounds_long_logical_id(self): self.assertLessEqual(55 + 1 + 8, 64) @patch("boto3.session.Session.region_name", "ap-southeast-1") - def test_generated_function_name_shared_by_all_resources(self): - # Ref on AWS::Lambda::WebFunction returns the ARN, so the revision and endpoint must reuse the same - # generated name value as the function rather than Ref it. + def test_generated_function_name_revision_and_endpoint_ref_function(self): + # The revision and endpoint Ref the function (its ARN), which the service accepts. function = SamWebFunction("MyWebFunction") function.CodeUri = "s3://foobar/foo.zip" function.Runtime = "bar" function.AuthType = "IamAuth" cfnResources = function.to_cloudformation(**self.kwargs) - fn = next(x for x in cfnResources if isinstance(x, LambdaWebFunction)) revision = next(x for x in cfnResources if isinstance(x, LambdaWebFunctionRevision)) endpoint = next(x for x in cfnResources if isinstance(x, LambdaWebFunctionEndpoint)) - self.assertEqual(revision.FunctionName, fn.FunctionName) - self.assertEqual(endpoint.FunctionName, fn.FunctionName) - # And specifically NOT a bare Ref (which resolves to the ARN) - self.assertNotEqual(revision.FunctionName, {"Ref": "MyWebFunction"}) - self.assertNotEqual(endpoint.FunctionName, {"Ref": "MyWebFunction"}) + self.assertEqual(revision.FunctionName, {"Ref": "MyWebFunction"}) + self.assertEqual(endpoint.FunctionName, {"Ref": "MyWebFunction"}) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_webfunction_name_runtime_attr_is_not_ref(self): @@ -639,7 +634,7 @@ def test_webfunction_name_runtime_attr_is_not_ref(self): self.assertNotEqual(fn.get_runtime_attr("name"), {"Ref": "MyWebFunction"}) @patch("boto3.session.Session.region_name", "ap-southeast-1") - def test_explicit_function_name_shared_by_all_resources(self): + def test_explicit_function_name_revision_and_endpoint_ref_function(self): function = SamWebFunction("MyWebFunction") function.CodeUri = "s3://foobar/foo.zip" function.Runtime = "bar" @@ -650,8 +645,8 @@ def test_explicit_function_name_shared_by_all_resources(self): revision = next(x for x in cfnResources if isinstance(x, LambdaWebFunctionRevision)) endpoint = next(x for x in cfnResources if isinstance(x, LambdaWebFunctionEndpoint)) self.assertEqual(fn.FunctionName, "my-explicit-name") - self.assertEqual(revision.FunctionName, "my-explicit-name") - self.assertEqual(endpoint.FunctionName, "my-explicit-name") + self.assertEqual(revision.FunctionName, {"Ref": "MyWebFunction"}) + self.assertEqual(endpoint.FunctionName, {"Ref": "MyWebFunction"}) @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_default_service_config(self): diff --git a/tests/translator/output/aws-cn/webfunction_basic.json b/tests/translator/output/aws-cn/webfunction_basic.json index 29e9c7ebca..7ef41a1724 100644 --- a/tests/translator/output/aws-cn/webfunction_basic.json +++ b/tests/translator/output/aws-cn/webfunction_basic.json @@ -51,34 +51,7 @@ "EndpointName": "prod", "EndpointType": "HomeRegion", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "RevisionWeights": [ { @@ -111,34 +84,7 @@ } }, "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "ServiceConfig": { "ExecutionRoleArn": { diff --git a/tests/translator/output/aws-cn/webfunction_multiregion.json b/tests/translator/output/aws-cn/webfunction_multiregion.json index a5c780c87e..bc8aa73d0f 100644 --- a/tests/translator/output/aws-cn/webfunction_multiregion.json +++ b/tests/translator/output/aws-cn/webfunction_multiregion.json @@ -54,34 +54,7 @@ "EndpointName": "prod", "EndpointType": "MultiRegion", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "Regions": [ "us-east-1", @@ -118,34 +91,7 @@ } }, "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "ServiceConfig": { "ExecutionRoleArn": { diff --git a/tests/translator/output/aws-cn/webfunction_with_all_properties.json b/tests/translator/output/aws-cn/webfunction_with_all_properties.json index 5b85c92a3d..97c0448ea1 100644 --- a/tests/translator/output/aws-cn/webfunction_with_all_properties.json +++ b/tests/translator/output/aws-cn/webfunction_with_all_properties.json @@ -56,34 +56,7 @@ "EndpointName": "staging", "EndpointType": "MultiRegion", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "Regions": [ "us-east-1", @@ -128,34 +101,7 @@ }, "Description": "initial revision", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "KmsKeyArn": "arn:aws:kms:us-east-1:123456789012:key/abcd1234", "ServiceConfig": { diff --git a/tests/translator/output/aws-us-gov/webfunction_basic.json b/tests/translator/output/aws-us-gov/webfunction_basic.json index 4bf11194de..7f8e0bebc8 100644 --- a/tests/translator/output/aws-us-gov/webfunction_basic.json +++ b/tests/translator/output/aws-us-gov/webfunction_basic.json @@ -51,34 +51,7 @@ "EndpointName": "prod", "EndpointType": "HomeRegion", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "RevisionWeights": [ { @@ -111,34 +84,7 @@ } }, "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "ServiceConfig": { "ExecutionRoleArn": { diff --git a/tests/translator/output/aws-us-gov/webfunction_multiregion.json b/tests/translator/output/aws-us-gov/webfunction_multiregion.json index a9018e53c7..147866b932 100644 --- a/tests/translator/output/aws-us-gov/webfunction_multiregion.json +++ b/tests/translator/output/aws-us-gov/webfunction_multiregion.json @@ -54,34 +54,7 @@ "EndpointName": "prod", "EndpointType": "MultiRegion", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "Regions": [ "us-east-1", @@ -118,34 +91,7 @@ } }, "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "ServiceConfig": { "ExecutionRoleArn": { diff --git a/tests/translator/output/aws-us-gov/webfunction_with_all_properties.json b/tests/translator/output/aws-us-gov/webfunction_with_all_properties.json index 2ab7a9909d..29aa41061d 100644 --- a/tests/translator/output/aws-us-gov/webfunction_with_all_properties.json +++ b/tests/translator/output/aws-us-gov/webfunction_with_all_properties.json @@ -56,34 +56,7 @@ "EndpointName": "staging", "EndpointType": "MultiRegion", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "Regions": [ "us-east-1", @@ -128,34 +101,7 @@ }, "Description": "initial revision", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "KmsKeyArn": "arn:aws:kms:us-east-1:123456789012:key/abcd1234", "ServiceConfig": { diff --git a/tests/translator/output/webfunction_basic.json b/tests/translator/output/webfunction_basic.json index 80f5f3f897..d836688b94 100644 --- a/tests/translator/output/webfunction_basic.json +++ b/tests/translator/output/webfunction_basic.json @@ -51,34 +51,7 @@ "EndpointName": "prod", "EndpointType": "HomeRegion", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "RevisionWeights": [ { @@ -111,34 +84,7 @@ } }, "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "ServiceConfig": { "ExecutionRoleArn": { diff --git a/tests/translator/output/webfunction_multiregion.json b/tests/translator/output/webfunction_multiregion.json index 9b9a574643..ecb5349c8d 100644 --- a/tests/translator/output/webfunction_multiregion.json +++ b/tests/translator/output/webfunction_multiregion.json @@ -54,34 +54,7 @@ "EndpointName": "prod", "EndpointType": "MultiRegion", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "Regions": [ "us-east-1", @@ -118,34 +91,7 @@ } }, "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "ServiceConfig": { "ExecutionRoleArn": { diff --git a/tests/translator/output/webfunction_with_all_properties.json b/tests/translator/output/webfunction_with_all_properties.json index ced092730c..f81521a47d 100644 --- a/tests/translator/output/webfunction_with_all_properties.json +++ b/tests/translator/output/webfunction_with_all_properties.json @@ -56,34 +56,7 @@ "EndpointName": "staging", "EndpointType": "MultiRegion", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "Regions": [ "us-east-1", @@ -128,34 +101,7 @@ }, "Description": "initial revision", "FunctionName": { - "Fn::Sub": [ - "MyWebFunction-${StackIdSuffix}", - { - "StackIdSuffix": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "-", - { - "Fn::Select": [ - 2, - { - "Fn::Split": [ - "/", - { - "Ref": "AWS::StackId" - } - ] - } - ] - } - ] - } - ] - } - } - ] + "Ref": "MyWebFunction" }, "KmsKeyArn": "arn:aws:kms:us-east-1:123456789012:key/abcd1234", "ServiceConfig": {