diff --git a/integration/combination/test_function_with_sns.py b/integration/combination/test_function_with_sns.py index fa90c1ee84..9f7dd597e5 100644 --- a/integration/combination/test_function_with_sns.py +++ b/integration/combination/test_function_with_sns.py @@ -17,7 +17,7 @@ def test_function_with_sns_bucket_trigger(self): lambda_subscription = next((x for x in subscriptions if x["Protocol"] == "lambda"), None) self.assertIsNotNone(lambda_subscription) - self.assertTrue(lambda_function_endpoint in lambda_subscription["Endpoint"]) + self.assertIn(lambda_function_endpoint, lambda_subscription["Endpoint"]) self.assertEqual(lambda_subscription["Protocol"], "lambda") self.assertEqual(lambda_subscription["TopicArn"], sns_topic_arn) @@ -26,3 +26,23 @@ def test_function_with_sns_bucket_trigger(self): self.assertIsNotNone(sqs_subscription) self.assertEqual(sqs_subscription["Protocol"], "sqs") self.assertEqual(sqs_subscription["TopicArn"], sns_topic_arn) + + def test_function_with_sns_intrinsics(self): + self.create_and_verify_stack("combination/function_with_sns_intrinsics") + + sns_client = self.client_provider.sns_client + + sns_topic_arn = self.get_physical_id_by_type("AWS::SNS::Topic") + + subscriptions = sns_client.list_subscriptions_by_topic(TopicArn=sns_topic_arn)["Subscriptions"] + self.assertEqual(len(subscriptions), 1) + + subscription = subscriptions[0] + + self.assertIsNotNone(subscription) + self.assertEqual(subscription["Protocol"], "sqs") + self.assertEqual(subscription["TopicArn"], sns_topic_arn) + + subscription_arn = subscription["SubscriptionArn"] + subscription_attributes = sns_client.get_subscription_attributes(SubscriptionArn=subscription_arn) + self.assertEqual(subscription_attributes["Attributes"]["FilterPolicy"], '{"price_usd":[{"numeric":["<",100]}]}') diff --git a/integration/helpers/resource.py b/integration/helpers/resource.py index d1150fc159..6a31d1a771 100644 --- a/integration/helpers/resource.py +++ b/integration/helpers/resource.py @@ -41,7 +41,9 @@ def verify_stack_resources(expected_file_path, stack_resources): parsed_resources = _sort_resources(stack_resources["StackResourceSummaries"]) if len(expected_resources) != len(parsed_resources): - return "'{}' resources expected, '{}' found".format(len(expected_resources), len(parsed_resources)) + return "'{}' resources expected, '{}' found: \n{}".format( + len(expected_resources), len(parsed_resources), json.dumps(parsed_resources, default=str) + ) for i in range(len(expected_resources)): exp = expected_resources[i] @@ -55,7 +57,7 @@ def verify_stack_resources(expected_file_path, stack_resources): "ResourceType": parsed["ResourceType"], } - return "'{}' expected, '{}' found (Resources must appear in the same order, don't include the LogicalId random suffix)".format( + return "'{}' expected, '{}' found (Don't include the LogicalId random suffix)".format( exp, parsed_trimed_down ) if exp["ResourceType"] != parsed["ResourceType"]: @@ -79,7 +81,8 @@ def generate_suffix(): def _sort_resources(resources): """ - Sorts a stack's resources by LogicalResourceId + Filters and sorts a stack's resources by LogicalResourceId. + Keeps only the LogicalResourceId and ResourceType properties Parameters ---------- @@ -93,7 +96,12 @@ def _sort_resources(resources): """ if resources is None: return [] - return sorted(resources, key=lambda d: d["LogicalResourceId"]) + + filtered_resources = map( + lambda x: {"LogicalResourceId": x["LogicalResourceId"], "ResourceType": x["ResourceType"]}, resources + ) + + return sorted(filtered_resources, key=lambda d: d["LogicalResourceId"]) def create_bucket(bucket_name, region): diff --git a/integration/resources/expected/combination/function_with_sns_intrinsics.json b/integration/resources/expected/combination/function_with_sns_intrinsics.json new file mode 100644 index 0000000000..8d715ef5d1 --- /dev/null +++ b/integration/resources/expected/combination/function_with_sns_intrinsics.json @@ -0,0 +1,30 @@ +[ + { + "LogicalResourceId": "MyLambdaFunction", + "ResourceType": "AWS::Lambda::Function" + }, + { + "LogicalResourceId": "MyLambdaFunctionRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "MySnsTopic", + "ResourceType": "AWS::SNS::Topic" + }, + { + "LogicalResourceId": "MyLambdaFunctionSNSEvent", + "ResourceType": "AWS::SNS::Subscription" + }, + { + "LogicalResourceId": "MyLambdaFunctionSNSEventQueue", + "ResourceType": "AWS::SQS::Queue" + }, + { + "LogicalResourceId": "MyLambdaFunctionSNSEventEventSourceMapping", + "ResourceType": "AWS::Lambda::EventSourceMapping" + }, + { + "LogicalResourceId": "MyLambdaFunctionSNSEventQueuePolicy", + "ResourceType": "AWS::SQS::QueuePolicy" + } +] \ No newline at end of file diff --git a/integration/resources/templates/combination/function_with_sns_intrinsics.yaml b/integration/resources/templates/combination/function_with_sns_intrinsics.yaml new file mode 100644 index 0000000000..08c9f5872e --- /dev/null +++ b/integration/resources/templates/combination/function_with_sns_intrinsics.yaml @@ -0,0 +1,38 @@ +Conditions: + MyCondition: + Fn::Equals: + - true + - false + +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + + Events: + SNSEvent: + Type: SNS + Properties: + Topic: + Ref: MySnsTopic + FilterPolicy: + Fn::If: + - MyCondition + - price_usd: + - numeric: + - ">=" + - 100 + - price_usd: + - numeric: + - "<" + - 100 + Region: + Ref: AWS::Region + SqsSubscription: true + + MySnsTopic: + Type: AWS::SNS::Topic diff --git a/tests/translator/input/error_sns_intrinsics.yaml b/tests/translator/input/error_sns_intrinsics.yaml new file mode 100644 index 0000000000..668a0aae81 --- /dev/null +++ b/tests/translator/input/error_sns_intrinsics.yaml @@ -0,0 +1,23 @@ +Parameters: + SnsSqsSubscription: + Type: Boolean + Default: false + +Resources: + SaveNotificationFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/notifications.zip + Handler: index.save_notification + Runtime: nodejs12.x + Events: + NotificationTopic: + Type: SNS + Properties: + SqsSubscription: + Ref: SnsSqsSubscription + Topic: + Ref: Notifications + + Notifications: + Type: AWS::SNS::Topic diff --git a/tests/translator/input/sns_intrinsics.yaml b/tests/translator/input/sns_intrinsics.yaml new file mode 100644 index 0000000000..cdafe672c6 --- /dev/null +++ b/tests/translator/input/sns_intrinsics.yaml @@ -0,0 +1,41 @@ +Parameters: + SnsRegion: + Type: String + Default: us-east-1 + +Conditions: + MyCondition: + Fn::Equals: + - true + - false + +Resources: + SaveNotificationFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/notifications.zip + Handler: index.save_notification + Runtime: nodejs12.x + Events: + NotificationTopic: + Type: SNS + Properties: + FilterPolicy: + Fn::If: + - MyCondition + - price_usd: + - numeric: + - ">=" + - 100 + - price_usd: + - numeric: + - "<" + - 100 + Region: + Ref: SnsRegion + SqsSubscription: true + Topic: + Ref: Notifications + + Notifications: + Type: AWS::SNS::Topic diff --git a/tests/translator/output/aws-cn/sns_intrinsics.json b/tests/translator/output/aws-cn/sns_intrinsics.json new file mode 100644 index 0000000000..b65f7eddfa --- /dev/null +++ b/tests/translator/output/aws-cn/sns_intrinsics.json @@ -0,0 +1,171 @@ +{ + "Conditions": { + "MyCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Parameters": { + "SnsRegion": { + "Default": "us-east-1", + "Type": "String" + } + }, + "Resources": { + "Notifications": { + "Type": "AWS::SNS::Topic" + }, + "SaveNotificationFunctionNotificationTopicQueue": { + "Type": "AWS::SQS::Queue", + "Properties": {} + }, + "SaveNotificationFunctionNotificationTopicQueuePolicy": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "Queues": [ + { + "Ref": "SaveNotificationFunctionNotificationTopicQueue" + } + ], + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sqs:SendMessage", + "Resource": { + "Fn::GetAtt": [ + "SaveNotificationFunctionNotificationTopicQueue", + "Arn" + ] + }, + "Effect": "Allow", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "Notifications" + } + } + }, + "Principal": "*" + } + ] + } + } + }, + "SaveNotificationFunctionNotificationTopic": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "FilterPolicy": { + "Fn::If": [ + "MyCondition", + { + "price_usd": [ + { + "numeric": [ + ">=", + 100 + ] + } + ] + }, + { + "price_usd": [ + { + "numeric": [ + "<", + 100 + ] + } + ] + } + ] + }, + "Region": { + "Ref": "SnsRegion" + }, + "Endpoint": { + "Fn::GetAtt": [ + "SaveNotificationFunctionNotificationTopicQueue", + "Arn" + ] + }, + "Protocol": "sqs", + "TopicArn": { + "Ref": "Notifications" + } + } + }, + "SaveNotificationFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole" + ], + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "SaveNotificationFunctionNotificationTopicEventSourceMapping": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 10, + "Enabled": true, + "FunctionName": { + "Ref": "SaveNotificationFunction" + }, + "EventSourceArn": { + "Fn::GetAtt": [ + "SaveNotificationFunctionNotificationTopicQueue", + "Arn" + ] + } + } + }, + "SaveNotificationFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.save_notification", + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "notifications.zip" + }, + "Role": { + "Fn::GetAtt": [ + "SaveNotificationFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/sns_intrinsics.json b/tests/translator/output/aws-us-gov/sns_intrinsics.json new file mode 100644 index 0000000000..0df391edf5 --- /dev/null +++ b/tests/translator/output/aws-us-gov/sns_intrinsics.json @@ -0,0 +1,171 @@ +{ + "Conditions": { + "MyCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Parameters": { + "SnsRegion": { + "Default": "us-east-1", + "Type": "String" + } + }, + "Resources": { + "Notifications": { + "Type": "AWS::SNS::Topic" + }, + "SaveNotificationFunctionNotificationTopicQueue": { + "Type": "AWS::SQS::Queue", + "Properties": {} + }, + "SaveNotificationFunctionNotificationTopicQueuePolicy": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "Queues": [ + { + "Ref": "SaveNotificationFunctionNotificationTopicQueue" + } + ], + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sqs:SendMessage", + "Resource": { + "Fn::GetAtt": [ + "SaveNotificationFunctionNotificationTopicQueue", + "Arn" + ] + }, + "Effect": "Allow", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "Notifications" + } + } + }, + "Principal": "*" + } + ] + } + } + }, + "SaveNotificationFunctionNotificationTopic": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "FilterPolicy": { + "Fn::If": [ + "MyCondition", + { + "price_usd": [ + { + "numeric": [ + ">=", + 100 + ] + } + ] + }, + { + "price_usd": [ + { + "numeric": [ + "<", + 100 + ] + } + ] + } + ] + }, + "Region": { + "Ref": "SnsRegion" + }, + "Endpoint": { + "Fn::GetAtt": [ + "SaveNotificationFunctionNotificationTopicQueue", + "Arn" + ] + }, + "Protocol": "sqs", + "TopicArn": { + "Ref": "Notifications" + } + } + }, + "SaveNotificationFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole" + ], + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "SaveNotificationFunctionNotificationTopicEventSourceMapping": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 10, + "Enabled": true, + "FunctionName": { + "Ref": "SaveNotificationFunction" + }, + "EventSourceArn": { + "Fn::GetAtt": [ + "SaveNotificationFunctionNotificationTopicQueue", + "Arn" + ] + } + } + }, + "SaveNotificationFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.save_notification", + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "notifications.zip" + }, + "Role": { + "Fn::GetAtt": [ + "SaveNotificationFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/error_sns_intrinsics.json b/tests/translator/output/error_sns_intrinsics.json new file mode 100644 index 0000000000..ad95a1a668 --- /dev/null +++ b/tests/translator/output/error_sns_intrinsics.json @@ -0,0 +1,8 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [SaveNotificationFunction] is invalid. Event with id [NotificationTopic] is invalid. No QueueARN or QueueURL provided.", + "errors": [ + { + "errorMessage": "Resource with id [SaveNotificationFunction] is invalid. Event with id [NotificationTopic] is invalid. No QueueARN or QueueURL provided." + } + ] +} \ No newline at end of file diff --git a/tests/translator/output/sns_intrinsics.json b/tests/translator/output/sns_intrinsics.json new file mode 100644 index 0000000000..c591b02664 --- /dev/null +++ b/tests/translator/output/sns_intrinsics.json @@ -0,0 +1,171 @@ +{ + "Conditions": { + "MyCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Parameters": { + "SnsRegion": { + "Default": "us-east-1", + "Type": "String" + } + }, + "Resources": { + "Notifications": { + "Type": "AWS::SNS::Topic" + }, + "SaveNotificationFunctionNotificationTopicQueue": { + "Type": "AWS::SQS::Queue", + "Properties": {} + }, + "SaveNotificationFunctionNotificationTopicQueuePolicy": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "Queues": [ + { + "Ref": "SaveNotificationFunctionNotificationTopicQueue" + } + ], + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sqs:SendMessage", + "Resource": { + "Fn::GetAtt": [ + "SaveNotificationFunctionNotificationTopicQueue", + "Arn" + ] + }, + "Effect": "Allow", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "Notifications" + } + } + }, + "Principal": "*" + } + ] + } + } + }, + "SaveNotificationFunctionNotificationTopic": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "FilterPolicy": { + "Fn::If": [ + "MyCondition", + { + "price_usd": [ + { + "numeric": [ + ">=", + 100 + ] + } + ] + }, + { + "price_usd": [ + { + "numeric": [ + "<", + 100 + ] + } + ] + } + ] + }, + "Region": { + "Ref": "SnsRegion" + }, + "Endpoint": { + "Fn::GetAtt": [ + "SaveNotificationFunctionNotificationTopicQueue", + "Arn" + ] + }, + "Protocol": "sqs", + "TopicArn": { + "Ref": "Notifications" + } + } + }, + "SaveNotificationFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole" + ], + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "SaveNotificationFunctionNotificationTopicEventSourceMapping": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "BatchSize": 10, + "Enabled": true, + "FunctionName": { + "Ref": "SaveNotificationFunction" + }, + "EventSourceArn": { + "Fn::GetAtt": [ + "SaveNotificationFunctionNotificationTopicQueue", + "Arn" + ] + } + } + }, + "SaveNotificationFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.save_notification", + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "notifications.zip" + }, + "Role": { + "Fn::GetAtt": [ + "SaveNotificationFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 0db19f45a8..c0bbdca3c0 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -335,6 +335,7 @@ class TestTranslatorEndToEnd(AbstractTestTranslator): "sns", "sns_sqs", "sns_existing_sqs", + "sns_intrinsics", "sns_outside_sqs", "sns_existing_other_subscription", "sns_topic_outside_template", @@ -723,6 +724,7 @@ def test_transform_success_no_side_effect(self, testcase, partition_with_region) "error_multiple_resource_errors", "error_null_application_id", "error_s3_not_in_template", + "error_sns_intrinsics", "error_table_invalid_attributetype", "error_table_primary_key_missing_name", "error_table_primary_key_missing_type",