diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 9238bb75d..2cdbc3c8b 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -1,6 +1,7 @@ """ SAM macro definitions """ import copy +import re from contextlib import suppress from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union, cast @@ -238,7 +239,6 @@ class SamFunction(SamResourceMacro): # DeadLetterQueue dead_letter_queue_policy_actions = {"SQS": "sqs:SendMessage", "SNS": "sns:Publish"} - # # Conditions conditions: Dict[str, Any] = {} # TODO: Replace `Any` with something more specific @@ -325,7 +325,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def] # noqa: P resources.append(url_permission) self._validate_deployment_preference_and_add_update_policy( - kwargs.get("deployment_preference_collection", None), + kwargs.get("deployment_preference_collection"), lambda_alias, intrinsics_resolver, cast(IntrinsicsResolver, mappings_resolver), # TODO: better handle mappings_resolver's Optional @@ -1002,7 +1002,22 @@ def _construct_alias(self, name: str, function: LambdaFunction, version: LambdaV if not name: raise InvalidResourceException(self.logical_id, "Alias name is required to create an alias") - logical_id = f"{function.logical_id}Alias{name}" + # Validate alias name against the required pattern: (?!^[0-9]+$)([a-zA-Z0-9-_]+) + # This ensures the alias name: + # 1. Contains only alphanumeric characters, hyphens, and underscores + # 2. Is not purely numeric + ALIAS_REGEX = r"(?!^[0-9]+$)([a-zA-Z0-9\-_]+)$" + if not re.match(ALIAS_REGEX, name): + raise InvalidResourceException( + self.logical_id, + f"AutoPublishAlias name ('{name}') must contain only alphanumeric characters, hyphens, or underscores matching (?!^[0-9]+$)([a-zA-Z0-9-_]+) pattern.", + ) + + # Strip hyphens and underscores from the alias name for the logical ID + # This ensures the logical ID contains only alphanumeric characters + alias_alphanumeric_name = name.replace("-", "D").replace("_", "U") + + logical_id = f"{function.logical_id}Alias{alias_alphanumeric_name}" alias = LambdaAlias(logical_id=logical_id, attributes=self.get_passthrough_resource_attributes()) alias.Name = name alias.FunctionName = function.get_runtime_attr("name") @@ -1014,7 +1029,7 @@ def _construct_alias(self, name: str, function: LambdaFunction, version: LambdaV def _validate_deployment_preference_and_add_update_policy( # noqa: PLR0913 self, - deployment_preference_collection: DeploymentPreferenceCollection, + deployment_preference_collection: Optional[DeploymentPreferenceCollection], lambda_alias: Optional[LambdaAlias], intrinsics_resolver: IntrinsicsResolver, mappings_resolver: IntrinsicsResolver, diff --git a/tests/translator/input/error_function_invalid_autopublishalias_name.yaml b/tests/translator/input/error_function_invalid_autopublishalias_name.yaml new file mode 100644 index 000000000..34eb31ff1 --- /dev/null +++ b/tests/translator/input/error_function_invalid_autopublishalias_name.yaml @@ -0,0 +1,9 @@ +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + # Using an invalid alias name with special characters that can't be properly transformed + AutoPublishAlias: invalid*alias@name diff --git a/tests/translator/input/function_with_alias_valid_names.yaml b/tests/translator/input/function_with_alias_valid_names.yaml new file mode 100644 index 000000000..e053f0543 --- /dev/null +++ b/tests/translator/input/function_with_alias_valid_names.yaml @@ -0,0 +1,54 @@ +Resources: + FunctionAliasNameCamelCase: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + AutoPublishAlias: camelCaseName + VersionDescription: sam-testing + + FunctionAliasNameUpperCase: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + AutoPublishAlias: UPPERCASE + VersionDescription: sam-testing + + FunctionAliasNameLowerCase: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + AutoPublishAlias: lowercase + VersionDescription: sam-testing + + FunctionAliasNameUnderscore: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + AutoPublishAlias: _underscore_name_ + VersionDescription: sam-testing + + FunctionAliasNameDash: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + AutoPublishAlias: underscore-name + VersionDescription: sam-testing + + FunctionAliasNameMix: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.9 + AutoPublishAlias: underScoreNAME_with-dash-01234 + VersionDescription: sam-testing diff --git a/tests/translator/output/aws-cn/function_with_alias_valid_names.json b/tests/translator/output/aws-cn/function_with_alias_valid_names.json new file mode 100644 index 000000000..ebcb45936 --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_alias_valid_names.json @@ -0,0 +1,472 @@ +{ + "Resources": { + "FunctionAliasNameCamelCase": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameCamelCaseRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameCamelCaseAliascamelCaseName": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameCamelCase" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameCamelCaseVersion640128d35d", + "Version" + ] + }, + "Name": "camelCaseName" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameCamelCaseRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameCamelCaseVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameCamelCase" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameDash": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameDashRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameDashAliasunderscoreDname": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameDash" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameDashVersion640128d35d", + "Version" + ] + }, + "Name": "underscore-name" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameDashRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameDashVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameDash" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameLowerCase": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameLowerCaseRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameLowerCaseAliaslowercase": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameLowerCase" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameLowerCaseVersion640128d35d", + "Version" + ] + }, + "Name": "lowercase" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameLowerCaseRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameLowerCaseVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameLowerCase" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameMix": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameMixRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameMixAliasunderScoreNAMEUwithDdashD01234": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameMix" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameMixVersion640128d35d", + "Version" + ] + }, + "Name": "underScoreNAME_with-dash-01234" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameMixRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameMixVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameMix" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameUnderscore": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameUnderscoreRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameUnderscoreAliasUunderscoreUnameU": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameUnderscore" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameUnderscoreVersion640128d35d", + "Version" + ] + }, + "Name": "_underscore_name_" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameUnderscoreRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameUnderscoreVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameUnderscore" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameUpperCase": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameUpperCaseRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameUpperCaseAliasUPPERCASE": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameUpperCase" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameUpperCaseVersion640128d35d", + "Version" + ] + }, + "Name": "UPPERCASE" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameUpperCaseRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameUpperCaseVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameUpperCase" + } + }, + "Type": "AWS::Lambda::Version" + } + } +} diff --git a/tests/translator/output/aws-us-gov/function_with_alias_valid_names.json b/tests/translator/output/aws-us-gov/function_with_alias_valid_names.json new file mode 100644 index 000000000..f4e163674 --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_alias_valid_names.json @@ -0,0 +1,472 @@ +{ + "Resources": { + "FunctionAliasNameCamelCase": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameCamelCaseRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameCamelCaseAliascamelCaseName": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameCamelCase" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameCamelCaseVersion640128d35d", + "Version" + ] + }, + "Name": "camelCaseName" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameCamelCaseRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameCamelCaseVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameCamelCase" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameDash": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameDashRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameDashAliasunderscoreDname": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameDash" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameDashVersion640128d35d", + "Version" + ] + }, + "Name": "underscore-name" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameDashRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameDashVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameDash" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameLowerCase": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameLowerCaseRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameLowerCaseAliaslowercase": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameLowerCase" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameLowerCaseVersion640128d35d", + "Version" + ] + }, + "Name": "lowercase" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameLowerCaseRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameLowerCaseVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameLowerCase" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameMix": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameMixRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameMixAliasunderScoreNAMEUwithDdashD01234": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameMix" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameMixVersion640128d35d", + "Version" + ] + }, + "Name": "underScoreNAME_with-dash-01234" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameMixRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameMixVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameMix" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameUnderscore": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameUnderscoreRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameUnderscoreAliasUunderscoreUnameU": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameUnderscore" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameUnderscoreVersion640128d35d", + "Version" + ] + }, + "Name": "_underscore_name_" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameUnderscoreRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameUnderscoreVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameUnderscore" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameUpperCase": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameUpperCaseRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameUpperCaseAliasUPPERCASE": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameUpperCase" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameUpperCaseVersion640128d35d", + "Version" + ] + }, + "Name": "UPPERCASE" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameUpperCaseRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameUpperCaseVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameUpperCase" + } + }, + "Type": "AWS::Lambda::Version" + } + } +} diff --git a/tests/translator/output/error_function_invalid_autopublishalias_name.json b/tests/translator/output/error_function_invalid_autopublishalias_name.json new file mode 100644 index 000000000..dd8cc09ec --- /dev/null +++ b/tests/translator/output/error_function_invalid_autopublishalias_name.json @@ -0,0 +1,9 @@ +{ + "_autoGeneratedBreakdownErrorMessage": [ + "Invalid Serverless Application Specification document. ", + "Number of errors found: 1. ", + "Resource with id [MyFunction] is invalid. ", + "AutoPublishAlias name ('invalid*alias@name') must contain only alphanumeric characters, hyphens, or underscores matching (?!^[0-9]+$)([a-zA-Z0-9-_]+) pattern." + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyFunction] is invalid. AutoPublishAlias name ('invalid*alias@name') must contain only alphanumeric characters, hyphens, or underscores matching (?!^[0-9]+$)([a-zA-Z0-9-_]+) pattern." +} diff --git a/tests/translator/output/function_with_alias_valid_names.json b/tests/translator/output/function_with_alias_valid_names.json new file mode 100644 index 000000000..7538f1e17 --- /dev/null +++ b/tests/translator/output/function_with_alias_valid_names.json @@ -0,0 +1,472 @@ +{ + "Resources": { + "FunctionAliasNameCamelCase": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameCamelCaseRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameCamelCaseAliascamelCaseName": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameCamelCase" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameCamelCaseVersion640128d35d", + "Version" + ] + }, + "Name": "camelCaseName" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameCamelCaseRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameCamelCaseVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameCamelCase" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameDash": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameDashRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameDashAliasunderscoreDname": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameDash" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameDashVersion640128d35d", + "Version" + ] + }, + "Name": "underscore-name" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameDashRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameDashVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameDash" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameLowerCase": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameLowerCaseRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameLowerCaseAliaslowercase": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameLowerCase" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameLowerCaseVersion640128d35d", + "Version" + ] + }, + "Name": "lowercase" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameLowerCaseRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameLowerCaseVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameLowerCase" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameMix": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameMixRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameMixAliasunderScoreNAMEUwithDdashD01234": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameMix" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameMixVersion640128d35d", + "Version" + ] + }, + "Name": "underScoreNAME_with-dash-01234" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameMixRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameMixVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameMix" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameUnderscore": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameUnderscoreRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameUnderscoreAliasUunderscoreUnameU": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameUnderscore" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameUnderscoreVersion640128d35d", + "Version" + ] + }, + "Name": "_underscore_name_" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameUnderscoreRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameUnderscoreVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameUnderscore" + } + }, + "Type": "AWS::Lambda::Version" + }, + "FunctionAliasNameUpperCase": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionAliasNameUpperCaseRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "FunctionAliasNameUpperCaseAliasUPPERCASE": { + "Properties": { + "FunctionName": { + "Ref": "FunctionAliasNameUpperCase" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "FunctionAliasNameUpperCaseVersion640128d35d", + "Version" + ] + }, + "Name": "UPPERCASE" + }, + "Type": "AWS::Lambda::Alias" + }, + "FunctionAliasNameUpperCaseRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionAliasNameUpperCaseVersion640128d35d": { + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "FunctionAliasNameUpperCase" + } + }, + "Type": "AWS::Lambda::Version" + } + } +} diff --git a/tests/translator/test_function_resources.py b/tests/translator/test_function_resources.py index 5fa2d0aa4..ad0c0185f 100644 --- a/tests/translator/test_function_resources.py +++ b/tests/translator/test_function_resources.py @@ -765,20 +765,68 @@ def test_version_logical_id_changes_with_snapstart(self): self.assertNotEqual(version1.logical_id, version_snapstart.logical_id) self.assertEqual(version1.logical_id, version_snapstart_none.logical_id) - def test_alias_creation(self): - name = "aliasname" - - alias = self.sam_func._construct_alias(name, self.lambda_func, self.lambda_version) + @parameterized.expand( + [ + # Valid cases + # Expect logical id should be {fn name}{'Alias'}{alphanumerica alias name without `-` or `_`} + ("aliasname", "fooAliasaliasname"), + ("alias-name", "fooAliasaliasDname"), + ("alias_name", "fooAliasaliasUname"), + ("alias123", "fooAliasalias123"), + ("123alias", "fooAlias123alias"), + ("UPPERCASE", "fooAliasUPPERCASE"), + ("mixed-Case_123", "fooAliasmixedDCaseU123"), + ("a", "fooAliasa"), # Single character + ("1a", "fooAlias1a"), # Starts with number + # Check the placement of dash and underscore + ("1-1_1", "fooAlias1D1U1"), + ("1_1-1", "fooAlias1U1D1"), + ("11-1", "fooAlias11D1"), + ("1-11", "fooAlias1D11"), + ("11_1", "fooAlias11U1"), + ("1_11", "fooAlias1U11"), + ("1-1-1", "fooAlias1D1D1"), + ("-1-1-1-", "fooAliasD1D1D1D"), + ("_1_1-1-", "fooAliasU1U1D1D"), + ] + ) + def test_alias_creation(self, alias_name, expected_logical_id): + alias = self.sam_func._construct_alias(alias_name, self.lambda_func, self.lambda_version) - expected_logical_id = f"{self.lambda_func.logical_id}Alias{name}" self.assertEqual(alias.logical_id, expected_logical_id) - self.assertEqual(alias.Name, name) + self.assertEqual(alias.Name, alias_name) self.assertEqual(alias.FunctionName, {"Ref": self.lambda_func.logical_id}) self.assertEqual(alias.FunctionVersion, {"Fn::GetAtt": [self.lambda_version.logical_id, "Version"]}) - def test_alias_creation_error(self): - with self.assertRaises(InvalidResourceException): - self.sam_func._construct_alias(None, self.lambda_func, self.lambda_version) + @parameterized.expand( + [ + # Invalid cases + ("", "Resource with id [foo] is invalid. Alias name is required to create an alias"), + (None, "Resource with id [foo] is invalid. Alias name is required to create an alias"), + ( + "123", + "Resource with id [foo] is invalid. AutoPublishAlias name ('123') must contain only alphanumeric characters, hyphens, or underscores matching (?!^[0-9]+$)([a-zA-Z0-9-_]+) pattern.", + ), + ( + "name with space", + "Resource with id [foo] is invalid. AutoPublishAlias name ('name with space') must contain only alphanumeric characters, hyphens, or underscores matching (?!^[0-9]+$)([a-zA-Z0-9-_]+) pattern.", + ), + ( + "alias@name", + "Resource with id [foo] is invalid. AutoPublishAlias name ('alias@name') must contain only alphanumeric characters, hyphens, or underscores matching (?!^[0-9]+$)([a-zA-Z0-9-_]+) pattern.", + ), + ( + "alias/name", + "Resource with id [foo] is invalid. AutoPublishAlias name ('alias/name') must contain only alphanumeric characters, hyphens, or underscores matching (?!^[0-9]+$)([a-zA-Z0-9-_]+) pattern.", + ), + ] + ) + def test_alias_creation_error(self, alias_name, expected_error_massage): + with self.assertRaises(InvalidResourceException) as context: + self.sam_func._construct_alias(alias_name, self.lambda_func, self.lambda_version) + + error = context.exception + self.assertEqual(str(error.message), expected_error_massage) def test_get_resolved_alias_name_must_work(self): property_name = "something"