diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/email.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/email.ts index f75fa067bbf2c..81c71c4d13154 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/email.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/email.ts @@ -32,6 +32,7 @@ export class EmailSubscription implements sns.ITopicSubscription { endpoint: this.emailAddress, protocol: this.props.json ? sns.SubscriptionProtocol.EMAIL_JSON : sns.SubscriptionProtocol.EMAIL, filterPolicy: this.props.filterPolicy, + filterPolicyWithMessageBody: this.props.filterPolicyWithMessageBody, deadLetterQueue: this.props.deadLetterQueue, }; } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts index 7b5446abbdf23..a4b0cde12704c 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts @@ -45,6 +45,7 @@ export class LambdaSubscription implements sns.ITopicSubscription { endpoint: this.fn.functionArn, protocol: sns.SubscriptionProtocol.LAMBDA, filterPolicy: this.props.filterPolicy, + filterPolicyWithMessageBody: this.props.filterPolicyWithMessageBody, region: this.regionFromArn(topic), deadLetterQueue: this.props.deadLetterQueue, }; diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/sms.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/sms.ts index 151fc7b494a25..0f2446278def3 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/sms.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/sms.ts @@ -20,6 +20,7 @@ export class SmsSubscription implements sns.ITopicSubscription { endpoint: this.phoneNumber, protocol: sns.SubscriptionProtocol.SMS, filterPolicy: this.props.filterPolicy, + filterPolicyWithMessageBody: this.props.filterPolicyWithMessageBody, }; } } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts index ce55f1778265a..340643632876c 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts @@ -75,6 +75,7 @@ export class SqsSubscription implements sns.ITopicSubscription { protocol: sns.SubscriptionProtocol.SQS, rawMessageDelivery: this.props.rawMessageDelivery, filterPolicy: this.props.filterPolicy, + filterPolicyWithMessageBody: this.props.filterPolicyWithMessageBody, region: this.regionFromArn(topic), deadLetterQueue: this.props.deadLetterQueue, subscriptionDependency: queuePolicyDependable, diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/subscription.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/subscription.ts index b95d2b54bcc2e..291699ca304f1 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/subscription.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/subscription.ts @@ -11,7 +11,13 @@ export interface SubscriptionProps { * @default - all messages are delivered */ readonly filterPolicy?: { [attribute: string]: sns.SubscriptionFilter }; - + /** + * The filter policy that is applied on the message body. + * To apply a filter policy to the message attributes, use `filterPolicy`. A maximum of one of `filterPolicyWithMessageBody` and `filterPolicy` may be used. + * + * @default - all messages are delivered + */ + readonly filterPolicyWithMessageBody?: { [attribute: string]: sns.FilterOrPolicy }; /** * Queue to be used as dead letter queue. * If not passed no dead letter queue is enabled. diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts index 1c3ddccf51520..bab121ba61a22 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts @@ -61,6 +61,7 @@ export class UrlSubscription implements sns.ITopicSubscription { protocol: this.protocol, rawMessageDelivery: this.props.rawMessageDelivery, filterPolicy: this.props.filterPolicy, + filterPolicyWithMessageBody: this.props.filterPolicyWithMessageBody, deadLetterQueue: this.props.deadLetterQueue, }; } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/package.json b/packages/@aws-cdk/aws-sns-subscriptions/package.json index 5c8d4c76df917..9bc3d86788313 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/package.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/package.json @@ -75,6 +75,7 @@ "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^27.5.2", diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/aws-cdk-sns-lambda.assets.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/aws-cdk-sns-lambda.assets.json index 555987868e39f..1e8da21ab7952 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/aws-cdk-sns-lambda.assets.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/aws-cdk-sns-lambda.assets.json @@ -1,7 +1,7 @@ { - "version": "20.0.0", + "version": "22.0.0", "files": { - "451c86b70ff9eaa09b0558745f5aa97d2af847bd82e8960d44d5c91c88fa855a": { + "b4d5d97a3217325cd1f06b3e725bdfbb9c4fd3c5c4a6adba15b286efa9146e23": { "source": { "path": "aws-cdk-sns-lambda.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "451c86b70ff9eaa09b0558745f5aa97d2af847bd82e8960d44d5c91c88fa855a.json", + "objectKey": "b4d5d97a3217325cd1f06b3e725bdfbb9c4fd3c5c4a6adba15b286efa9146e23.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/aws-cdk-sns-lambda.template.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/aws-cdk-sns-lambda.template.json index 2bddd7e2aa658..9f3e5c86b34db 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/aws-cdk-sns-lambda.template.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/aws-cdk-sns-lambda.template.json @@ -241,6 +241,101 @@ ] } } + }, + "FilteredMessageBodyServiceRoleB2EB82B3": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "FilteredMessageBody222AE8F1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function handler(event, _context, callback) {\n /* eslint-disable no-console */\n console.log('====================================================');\n console.log(JSON.stringify(event, undefined, 2));\n console.log('====================================================');\n return callback(undefined, event);\n}" + }, + "Role": { + "Fn::GetAtt": [ + "FilteredMessageBodyServiceRoleB2EB82B3", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "FilteredMessageBodyServiceRoleB2EB82B3" + ] + }, + "FilteredMessageBodyAllowInvokeawscdksnslambdaMyTopic6C62AB90FB54CEA4": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "FilteredMessageBody222AE8F1", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "MyTopic86869434" + } + } + }, + "FilteredMessageBodyMyTopicAD1F55C4": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "MyTopic86869434" + }, + "Endpoint": { + "Fn::GetAtt": [ + "FilteredMessageBody222AE8F1", + "Arn" + ] + }, + "FilterPolicy": { + "background": { + "color": [ + "red", + { + "prefix": "bl" + }, + { + "prefix": "ye" + } + ] + } + }, + "FilterPolicyScope": "MessageBody" + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/cdk.out b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/cdk.out index 588d7b269d34f..145739f539580 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"20.0.0"} \ No newline at end of file +{"version":"22.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/integ.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/integ.json index 252910a4df52c..3f4ce5ecc9748 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "22.0.0", "testCases": { "integ.sns-lambda": { "stacks": [ diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/manifest.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/manifest.json index f4d3c8f7b4c69..a1dc0e7aeeecc 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/manifest.json @@ -1,12 +1,6 @@ { - "version": "20.0.0", + "version": "22.0.0", "artifacts": { - "Tree": { - "type": "cdk:tree", - "properties": { - "file": "tree.json" - } - }, "aws-cdk-sns-lambda.assets": { "type": "cdk:asset-manifest", "properties": { @@ -23,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/451c86b70ff9eaa09b0558745f5aa97d2af847bd82e8960d44d5c91c88fa855a.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/b4d5d97a3217325cd1f06b3e725bdfbb9c4fd3c5c4a6adba15b286efa9146e23.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -105,6 +99,30 @@ "data": "FilteredMyTopicC8395C27" } ], + "/aws-cdk-sns-lambda/FilteredMessageBody/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FilteredMessageBodyServiceRoleB2EB82B3" + } + ], + "/aws-cdk-sns-lambda/FilteredMessageBody/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FilteredMessageBody222AE8F1" + } + ], + "/aws-cdk-sns-lambda/FilteredMessageBody/AllowInvoke:awscdksnslambdaMyTopic6C62AB90": [ + { + "type": "aws:cdk:logicalId", + "data": "FilteredMessageBodyAllowInvokeawscdksnslambdaMyTopic6C62AB90FB54CEA4" + } + ], + "/aws-cdk-sns-lambda/FilteredMessageBody/MyTopic/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FilteredMessageBodyMyTopicAD1F55C4" + } + ], "/aws-cdk-sns-lambda/BootstrapVersion": [ { "type": "aws:cdk:logicalId", @@ -119,6 +137,12 @@ ] }, "displayName": "aws-cdk-sns-lambda" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/tree.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/tree.json index ca55020a7b40f..58272873148d2 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.js.snapshot/tree.json @@ -4,14 +4,6 @@ "id": "App", "path": "", "children": { - "Tree": { - "id": "Tree", - "path": "Tree", - "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" - } - }, "aws-cdk-sns-lambda": { "id": "aws-cdk-sns-lambda", "path": "aws-cdk-sns-lambda", @@ -46,6 +38,14 @@ "id": "ServiceRole", "path": "aws-cdk-sns-lambda/Echo/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "aws-cdk-sns-lambda/Echo/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "aws-cdk-sns-lambda/Echo/ServiceRole/Resource", @@ -269,6 +269,14 @@ "id": "ServiceRole", "path": "aws-cdk-sns-lambda/Filtered/ServiceRole", "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "aws-cdk-sns-lambda/Filtered/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "aws-cdk-sns-lambda/Filtered/ServiceRole/Resource", @@ -429,17 +437,203 @@ "fqn": "@aws-cdk/aws-lambda.Function", "version": "0.0.0" } + }, + "FilteredMessageBody": { + "id": "FilteredMessageBody", + "path": "aws-cdk-sns-lambda/FilteredMessageBody", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "aws-cdk-sns-lambda/FilteredMessageBody/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "aws-cdk-sns-lambda/FilteredMessageBody/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-sns-lambda/FilteredMessageBody/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-sns-lambda/FilteredMessageBody/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "zipFile": "exports.handler = function handler(event, _context, callback) {\n /* eslint-disable no-console */\n console.log('====================================================');\n console.log(JSON.stringify(event, undefined, 2));\n console.log('====================================================');\n return callback(undefined, event);\n}" + }, + "role": { + "Fn::GetAtt": [ + "FilteredMessageBodyServiceRoleB2EB82B3", + "Arn" + ] + }, + "handler": "index.handler", + "runtime": "nodejs14.x" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnFunction", + "version": "0.0.0" + } + }, + "AllowInvoke:awscdksnslambdaMyTopic6C62AB90": { + "id": "AllowInvoke:awscdksnslambdaMyTopic6C62AB90", + "path": "aws-cdk-sns-lambda/FilteredMessageBody/AllowInvoke:awscdksnslambdaMyTopic6C62AB90", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Permission", + "aws:cdk:cloudformation:props": { + "action": "lambda:InvokeFunction", + "functionName": { + "Fn::GetAtt": [ + "FilteredMessageBody222AE8F1", + "Arn" + ] + }, + "principal": "sns.amazonaws.com", + "sourceArn": { + "Ref": "MyTopic86869434" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnPermission", + "version": "0.0.0" + } + }, + "MyTopic": { + "id": "MyTopic", + "path": "aws-cdk-sns-lambda/FilteredMessageBody/MyTopic", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sns-lambda/FilteredMessageBody/MyTopic/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SNS::Subscription", + "aws:cdk:cloudformation:props": { + "protocol": "lambda", + "topicArn": { + "Ref": "MyTopic86869434" + }, + "endpoint": { + "Fn::GetAtt": [ + "FilteredMessageBody222AE8F1", + "Arn" + ] + }, + "filterPolicy": { + "background": { + "color": [ + "red", + { + "prefix": "bl" + }, + { + "prefix": "ye" + } + ] + } + }, + "filterPolicyScope": "MessageBody" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sns.CfnSubscription", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sns.Subscription", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Function", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-sns-lambda/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-sns-lambda/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } } }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.189" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.ts index bd847b73d18f0..da150e9929861 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.ts @@ -1,3 +1,5 @@ + + import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -10,23 +12,23 @@ class SnsToLambda extends cdk.Stack { const topic = new sns.Topic(this, 'MyTopic'); - const fction = new lambda.Function(this, 'Echo', { + const func = new lambda.Function(this, 'Echo', { handler: 'index.handler', runtime: lambda.Runtime.NODEJS_14_X, code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`), }); - topic.addSubscription(new subs.LambdaSubscription(fction, { + topic.addSubscription(new subs.LambdaSubscription(func, { deadLetterQueue: new sqs.Queue(this, 'DeadLetterQueue'), })); - const fctionFiltered = new lambda.Function(this, 'Filtered', { + const funcFiltered = new lambda.Function(this, 'Filtered', { handler: 'index.handler', runtime: lambda.Runtime.NODEJS_14_X, code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`), }); - topic.addSubscription(new subs.LambdaSubscription(fctionFiltered, { + topic.addSubscription(new subs.LambdaSubscription(funcFiltered, { filterPolicy: { color: sns.SubscriptionFilter.stringFilter({ allowlist: ['red'], @@ -40,6 +42,23 @@ class SnsToLambda extends cdk.Stack { }), }, })); + + const funcFilteredWithMessageBody = new lambda.Function(this, 'FilteredMessageBody', { + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_14_X, + code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`), + }); + + topic.addSubscription(new subs.LambdaSubscription(funcFilteredWithMessageBody, { + filterPolicyWithMessageBody: { + background: sns.FilterOrPolicy.policy({ + color: sns.FilterOrPolicy.filter(sns.SubscriptionFilter.stringFilter({ + allowlist: ['red'], + matchPrefixes: ['bl', 'ye'], + })), + }), + }, + })); } } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/QueueStack.assets.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/QueueStack.assets.json new file mode 100644 index 0000000000000..5c5893e1b8478 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/QueueStack.assets.json @@ -0,0 +1,19 @@ +{ + "version": "22.0.0", + "files": { + "706116cb7bbce00b1ab402aeeaa66b35fc2fef8e57e4192f8ee4747dc5864691": { + "source": { + "path": "QueueStack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "706116cb7bbce00b1ab402aeeaa66b35fc2fef8e57e4192f8ee4747dc5864691.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/QueueStack.template.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/QueueStack.template.json new file mode 100644 index 0000000000000..1b75dd29570ef --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/QueueStack.template.json @@ -0,0 +1,163 @@ +{ + "Resources": { + "MyQueueE6CA6235": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MyQueuePolicy6BBEDDAC": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::ImportValue": "SnsToSqsStack:ExportsOutputRefMyTopic868694349D03D60F" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "MyQueueE6CA6235", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "MyQueueE6CA6235" + } + ] + } + }, + "MyQueueSnsToSqsStackMyTopic3F1182C25E300A0F": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "sqs", + "TopicArn": { + "Fn::ImportValue": "SnsToSqsStack:ExportsOutputRefMyTopic868694349D03D60F" + }, + "Endpoint": { + "Fn::GetAtt": [ + "MyQueueE6CA6235", + "Arn" + ] + }, + "FilterPolicy": { + "background": { + "color": [ + "red", + "green", + { + "anything-but": [ + "white", + "orange" + ] + } + ] + }, + "price": [ + { + "numeric": [ + "=", + 100 + ] + }, + { + "numeric": [ + "=", + 200 + ] + }, + { + "numeric": [ + ">", + 500 + ] + }, + { + "numeric": [ + "<", + 1000 + ] + }, + { + "numeric": [ + ">=", + 300, + "<=", + 350 + ] + }, + { + "numeric": [ + ">", + 2000, + "<", + 3000 + ] + } + ] + }, + "FilterPolicyScope": "MessageBody" + }, + "DependsOn": [ + "MyQueuePolicy6BBEDDAC" + ] + } + }, + "Outputs": { + "ExportsOutputRefMyQueueE6CA623512A57419": { + "Value": { + "Ref": "MyQueueE6CA6235" + }, + "Export": { + "Name": "QueueStack:ExportsOutputRefMyQueueE6CA623512A57419" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SNSSubscriptionsDefaultTestDeployAssertD77125EB.assets.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SNSSubscriptionsDefaultTestDeployAssertD77125EB.assets.json new file mode 100644 index 0000000000000..f35a1ebd0b58e --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SNSSubscriptionsDefaultTestDeployAssertD77125EB.assets.json @@ -0,0 +1,32 @@ +{ + "version": "22.0.0", + "files": { + "278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4": { + "source": { + "path": "asset.278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.bundle", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "9c226b6b87c5f7d528d0a2cdc0627cfb3165de9fb9df32956d25a79356409185": { + "source": { + "path": "SNSSubscriptionsDefaultTestDeployAssertD77125EB.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "9c226b6b87c5f7d528d0a2cdc0627cfb3165de9fb9df32956d25a79356409185.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SNSSubscriptionsDefaultTestDeployAssertD77125EB.template.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SNSSubscriptionsDefaultTestDeployAssertD77125EB.template.json new file mode 100644 index 0000000000000..857e8d4435869 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SNSSubscriptionsDefaultTestDeployAssertD77125EB.template.json @@ -0,0 +1,154 @@ +{ + "Resources": { + "AwsApiCallSNSpublish": { + "Type": "Custom::DeployAssert@SdkCallSNSpublish", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "SNS", + "api": "publish", + "parameters": { + "Message": "{ background: { color: 'green' }, price: 200 }", + "TopicArn": { + "Fn::ImportValue": "SnsToSqsStack:ExportsOutputRefMyTopic868694349D03D60F" + } + }, + "flattenResponse": "false", + "salt": "1677122791193" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sns:Publish" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, + { + "Action": [ + "sqs:ReceiveMessage" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + } + ] + } + } + ] + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.zip" + }, + "Timeout": 120, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + }, + "AwsApiCallSQSreceiveMessage": { + "Type": "Custom::DeployAssert@SdkCallSQSreceiveMessage", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "SQS", + "api": "receiveMessage", + "parameters": { + "QueueUrl": { + "Fn::ImportValue": "QueueStack:ExportsOutputRefMyQueueE6CA623512A57419" + }, + "WaitTimeSeconds": 20 + }, + "flattenResponse": "false", + "salt": "1677122791193" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SnsToSqsStack.assets.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SnsToSqsStack.assets.json new file mode 100644 index 0000000000000..e01f9dadedc45 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SnsToSqsStack.assets.json @@ -0,0 +1,19 @@ +{ + "version": "22.0.0", + "files": { + "1b2f5417b0f02417ad80f41e818ff0917ea4b65ef69016cec3a20d314fe454bc": { + "source": { + "path": "SnsToSqsStack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "1b2f5417b0f02417ad80f41e818ff0917ea4b65ef69016cec3a20d314fe454bc.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SnsToSqsStack.template.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SnsToSqsStack.template.json new file mode 100644 index 0000000000000..5406da3773d92 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/SnsToSqsStack.template.json @@ -0,0 +1,51 @@ +{ + "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + } + }, + "Outputs": { + "ExportsOutputRefMyTopic868694349D03D60F": { + "Value": { + "Ref": "MyTopic86869434" + }, + "Export": { + "Name": "SnsToSqsStack:ExportsOutputRefMyTopic868694349D03D60F" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/asset.278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.bundle/index.js b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/asset.278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.bundle/index.js new file mode 100644 index 0000000000000..2bf09d6726a42 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/asset.278d42fa865f60954d898636503d0ee86a6689d73dc50eb912fac62def0ef6a4.bundle/index.js @@ -0,0 +1,1052 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// lib/assertions/providers/lambda-handler/index.ts +var lambda_handler_exports = {}; +__export(lambda_handler_exports, { + handler: () => handler, + isComplete: () => isComplete, + onTimeout: () => onTimeout +}); +module.exports = __toCommonJS(lambda_handler_exports); + +// ../assertions/lib/matcher.ts +var Matcher = class { + static isMatcher(x) { + return x && x instanceof Matcher; + } +}; +var MatchResult = class { + constructor(target) { + this.failuresHere = /* @__PURE__ */ new Map(); + this.captures = /* @__PURE__ */ new Map(); + this.finalized = false; + this.innerMatchFailures = /* @__PURE__ */ new Map(); + this._hasFailed = false; + this._failCount = 0; + this._cost = 0; + this.target = target; + } + push(matcher, path, message) { + return this.recordFailure({ matcher, path, message }); + } + recordFailure(failure) { + const failKey = failure.path.join("."); + let list = this.failuresHere.get(failKey); + if (!list) { + list = []; + this.failuresHere.set(failKey, list); + } + this._failCount += 1; + this._cost += failure.cost ?? 1; + list.push(failure); + this._hasFailed = true; + return this; + } + get isSuccess() { + return !this._hasFailed; + } + hasFailed() { + return this._hasFailed; + } + get failCount() { + return this._failCount; + } + get failCost() { + return this._cost; + } + compose(id, inner) { + if (inner.hasFailed()) { + this._hasFailed = true; + this._failCount += inner.failCount; + this._cost += inner._cost; + this.innerMatchFailures.set(id, inner); + } + inner.captures.forEach((vals, capture) => { + vals.forEach((value) => this.recordCapture({ capture, value })); + }); + return this; + } + finished() { + if (this.finalized) { + return this; + } + if (this.failCount === 0) { + this.captures.forEach((vals, cap) => cap._captured.push(...vals)); + } + this.finalized = true; + return this; + } + toHumanStrings() { + const failures = new Array(); + debugger; + recurse(this, []); + return failures.map((r) => { + const loc = r.path.length === 0 ? "" : ` at /${r.path.join("/")}`; + return "" + r.message + loc + ` (using ${r.matcher.name} matcher)`; + }); + function recurse(x, prefix) { + for (const fail of Array.from(x.failuresHere.values()).flat()) { + failures.push({ + matcher: fail.matcher, + message: fail.message, + path: [...prefix, ...fail.path] + }); + } + for (const [key, inner] of x.innerMatchFailures.entries()) { + recurse(inner, [...prefix, key]); + } + } + } + renderMismatch() { + if (!this.hasFailed()) { + return ""; + } + const parts = new Array(); + const indents = new Array(); + emitFailures(this, ""); + recurse(this); + return moveMarkersToFront(parts.join("").trimEnd()); + function emit(x) { + if (x === void 0) { + debugger; + } + parts.push(x.replace(/\n/g, ` +${indents.join("")}`)); + } + function emitFailures(r, path, scrapSet) { + for (const fail of r.failuresHere.get(path) ?? []) { + emit(`!! ${fail.message} +`); + } + scrapSet == null ? void 0 : scrapSet.delete(path); + } + function recurse(r) { + const remainingFailures = new Set(Array.from(r.failuresHere.keys()).filter((x) => x !== "")); + if (Array.isArray(r.target)) { + indents.push(" "); + emit("[\n"); + for (const [first, i] of enumFirst(range(r.target.length))) { + if (!first) { + emit(",\n"); + } + emitFailures(r, `${i}`, remainingFailures); + const innerMatcher = r.innerMatchFailures.get(`${i}`); + if (innerMatcher) { + emitFailures(innerMatcher, ""); + recurseComparingValues(innerMatcher, r.target[i]); + } else { + emit(renderAbridged(r.target[i])); + } + } + emitRemaining(); + indents.pop(); + emit("\n]"); + return; + } + if (r.target && typeof r.target === "object") { + indents.push(" "); + emit("{\n"); + const keys = Array.from(/* @__PURE__ */ new Set([ + ...Object.keys(r.target), + ...Array.from(remainingFailures) + ])).sort(); + for (const [first, key] of enumFirst(keys)) { + if (!first) { + emit(",\n"); + } + emitFailures(r, key, remainingFailures); + const innerMatcher = r.innerMatchFailures.get(key); + if (innerMatcher) { + emitFailures(innerMatcher, ""); + emit(`${jsonify(key)}: `); + recurseComparingValues(innerMatcher, r.target[key]); + } else { + emit(`${jsonify(key)}: `); + emit(renderAbridged(r.target[key])); + } + } + emitRemaining(); + indents.pop(); + emit("\n}"); + return; + } + emitRemaining(); + emit(jsonify(r.target)); + function emitRemaining() { + if (remainingFailures.size > 0) { + emit("\n"); + } + for (const key of remainingFailures) { + emitFailures(r, key); + } + } + } + function recurseComparingValues(inner, actualValue) { + if (inner.target === actualValue) { + return recurse(inner); + } + emit(renderAbridged(actualValue)); + emit(" <*> "); + recurse(inner); + } + function renderAbridged(x) { + if (Array.isArray(x)) { + switch (x.length) { + case 0: + return "[]"; + case 1: + return `[ ${renderAbridged(x[0])} ]`; + case 2: + if (x.every((e) => ["number", "boolean", "string"].includes(typeof e))) { + return `[ ${x.map(renderAbridged).join(", ")} ]`; + } + return "[ ... ]"; + default: + return "[ ... ]"; + } + } + if (x && typeof x === "object") { + const keys = Object.keys(x); + switch (keys.length) { + case 0: + return "{}"; + case 1: + return `{ ${JSON.stringify(keys[0])}: ${renderAbridged(x[keys[0]])} }`; + default: + return "{ ... }"; + } + } + return jsonify(x); + } + function jsonify(x) { + return JSON.stringify(x) ?? "undefined"; + } + function moveMarkersToFront(x) { + const re = /^(\s+)!!/gm; + return x.replace(re, (_, spaces) => `!!${spaces.substring(0, spaces.length - 2)}`); + } + } + recordCapture(options) { + let values = this.captures.get(options.capture); + if (values === void 0) { + values = []; + } + values.push(options.value); + this.captures.set(options.capture, values); + } +}; +function* range(n) { + for (let i = 0; i < n; i++) { + yield i; + } +} +function* enumFirst(xs) { + let first = true; + for (const x of xs) { + yield [first, x]; + first = false; + } +} + +// ../assertions/lib/private/matchers/absent.ts +var AbsentMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual !== void 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Received ${actual}, but key should be absent` + }); + } + return result; + } +}; + +// ../assertions/lib/private/sorting.ts +function sortKeyComparator(keyFn) { + return (a, b) => { + const ak = keyFn(a); + const bk = keyFn(b); + for (let i = 0; i < ak.length && i < bk.length; i++) { + const av = ak[i]; + const bv = bk[i]; + let diff = 0; + if (typeof av === "number" && typeof bv === "number") { + diff = av - bv; + } else if (typeof av === "string" && typeof bv === "string") { + diff = av.localeCompare(bv); + } + if (diff !== 0) { + return diff; + } + } + return bk.length - ak.length; + }; +} + +// ../assertions/lib/private/sparse-matrix.ts +var SparseMatrix = class { + constructor() { + this.matrix = /* @__PURE__ */ new Map(); + } + get(row, col) { + var _a; + return (_a = this.matrix.get(row)) == null ? void 0 : _a.get(col); + } + row(row) { + var _a; + return Array.from(((_a = this.matrix.get(row)) == null ? void 0 : _a.entries()) ?? []); + } + set(row, col, value) { + let r = this.matrix.get(row); + if (!r) { + r = /* @__PURE__ */ new Map(); + this.matrix.set(row, r); + } + r.set(col, value); + } +}; + +// ../assertions/lib/private/type.ts +function getType(obj) { + return Array.isArray(obj) ? "array" : typeof obj; +} + +// ../assertions/lib/match.ts +var Match = class { + static absent() { + return new AbsentMatch("absent"); + } + static arrayWith(pattern) { + return new ArrayMatch("arrayWith", pattern); + } + static arrayEquals(pattern) { + return new ArrayMatch("arrayEquals", pattern, { subsequence: false }); + } + static exact(pattern) { + return new LiteralMatch("exact", pattern, { partialObjects: false }); + } + static objectLike(pattern) { + return new ObjectMatch("objectLike", pattern); + } + static objectEquals(pattern) { + return new ObjectMatch("objectEquals", pattern, { partial: false }); + } + static not(pattern) { + return new NotMatch("not", pattern); + } + static serializedJson(pattern) { + return new SerializedJson("serializedJson", pattern); + } + static anyValue() { + return new AnyMatch("anyValue"); + } + static stringLikeRegexp(pattern) { + return new StringLikeRegexpMatch("stringLikeRegexp", pattern); + } +}; +var LiteralMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partialObjects = options.partialObjects ?? false; + if (Matcher.isMatcher(this.pattern)) { + throw new Error("LiteralMatch cannot directly contain another matcher. Remove the top-level matcher or nest it more deeply."); + } + } + test(actual) { + if (Array.isArray(this.pattern)) { + return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual); + } + if (typeof this.pattern === "object") { + return new ObjectMatch(this.name, this.pattern, { partial: this.partialObjects }).test(actual); + } + const result = new MatchResult(actual); + if (typeof this.pattern !== typeof actual) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected type ${typeof this.pattern} but received ${getType(actual)}` + }); + return result; + } + if (actual !== this.pattern) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected ${this.pattern} but received ${actual}` + }); + } + return result; + } +}; +var ArrayMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.subsequence = options.subsequence ?? true; + this.partialObjects = options.partialObjects ?? false; + } + test(actual) { + if (!Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type array but received ${getType(actual)}` + }); + } + return this.subsequence ? this.testSubsequence(actual) : this.testFullArray(actual); + } + testFullArray(actual) { + const result = new MatchResult(actual); + let i = 0; + for (; i < this.pattern.length && i < actual.length; i++) { + const patternElement = this.pattern[i]; + const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const innerResult = matcher.test(actual[i]); + result.compose(`${i}`, innerResult); + } + if (i < this.pattern.length) { + result.recordFailure({ + matcher: this, + message: `Not enough elements in array (expecting ${this.pattern.length}, got ${actual.length})`, + path: [`${i}`] + }); + } + if (i < actual.length) { + result.recordFailure({ + matcher: this, + message: `Too many elements in array (expecting ${this.pattern.length}, got ${actual.length})`, + path: [`${i}`] + }); + } + return result; + } + testSubsequence(actual) { + const result = new MatchResult(actual); + let patternIdx = 0; + let actualIdx = 0; + const matches = new SparseMatrix(); + while (patternIdx < this.pattern.length && actualIdx < actual.length) { + const patternElement = this.pattern[patternIdx]; + const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const matcherName = matcher.name; + if (matcherName == "absent" || matcherName == "anyValue") { + throw new Error(`The Matcher ${matcherName}() cannot be nested within arrayWith()`); + } + const innerResult = matcher.test(actual[actualIdx]); + matches.set(patternIdx, actualIdx, innerResult); + actualIdx++; + if (innerResult.isSuccess) { + result.compose(`${actualIdx}`, innerResult); + patternIdx++; + } + } + if (patternIdx < this.pattern.length) { + for (let spi = 0; spi < patternIdx; spi++) { + const foundMatch = matches.row(spi).find(([, r]) => r.isSuccess); + if (!foundMatch) { + continue; + } + const [index] = foundMatch; + result.compose(`${index}`, new MatchResult(actual[index]).recordFailure({ + matcher: this, + message: `arrayWith pattern ${spi} matched here`, + path: [], + cost: 0 + })); + } + const failedMatches = matches.row(patternIdx); + failedMatches.sort(sortKeyComparator(([i, r]) => [r.failCost, i])); + if (failedMatches.length > 0) { + const [index, innerResult] = failedMatches[0]; + result.recordFailure({ + matcher: this, + message: `Could not match arrayWith pattern ${patternIdx}. This is the closest match`, + path: [`${index}`], + cost: 0 + }); + result.compose(`${index}`, innerResult); + } else { + result.recordFailure({ + matcher: this, + message: `Could not match arrayWith pattern ${patternIdx}. No more elements to try`, + path: [`${actual.length}`] + }); + } + } + return result; + } +}; +var ObjectMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partial = options.partial ?? true; + } + test(actual) { + if (typeof actual !== "object" || Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type object but received ${getType(actual)}` + }); + } + const result = new MatchResult(actual); + if (!this.partial) { + for (const a of Object.keys(actual)) { + if (!(a in this.pattern)) { + result.recordFailure({ + matcher: this, + path: [a], + message: `Unexpected key ${a}` + }); + } + } + } + for (const [patternKey, patternVal] of Object.entries(this.pattern)) { + if (!(patternKey in actual) && !(patternVal instanceof AbsentMatch)) { + result.recordFailure({ + matcher: this, + path: [patternKey], + message: `Missing key '${patternKey}'` + }); + continue; + } + const matcher = Matcher.isMatcher(patternVal) ? patternVal : new LiteralMatch(this.name, patternVal, { partialObjects: this.partial }); + const inner = matcher.test(actual[patternKey]); + result.compose(patternKey, inner); + } + return result; + } +}; +var SerializedJson = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + if (getType(actual) !== "string") { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected JSON as a string but found ${getType(actual)}` + }); + } + let parsed; + try { + parsed = JSON.parse(actual); + } catch (err) { + if (err instanceof SyntaxError) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Invalid JSON string: ${actual}` + }); + } else { + throw err; + } + } + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(parsed); + if (innerResult.hasFailed()) { + innerResult.recordFailure({ + matcher: this, + path: [], + message: "Encoded JSON value does not match" + }); + } + return innerResult; + } +}; +var NotMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Found unexpected match: ${JSON.stringify(actual, void 0, 2)}` + }); + } + return result; + } +}; +var AnyMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual == null) { + result.recordFailure({ + matcher: this, + path: [], + message: "Expected a value but found none" + }); + } + return result; + } +}; +var StringLikeRegexpMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + const regex = new RegExp(this.pattern, "gm"); + if (typeof actual !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected a string, but got '${typeof actual}'` + }); + } + if (!regex.test(actual)) { + result.recordFailure({ + matcher: this, + path: [], + message: `String '${actual}' did not match pattern '${this.pattern}'` + }); + } + return result; + } +}; + +// lib/assertions/providers/lambda-handler/base.ts +var https = __toESM(require("https")); +var url = __toESM(require("url")); +var AWS = __toESM(require("aws-sdk")); +var CustomResourceHandler = class { + constructor(event, context) { + this.event = event; + this.context = context; + this.timedOut = false; + this.timeout = setTimeout(async () => { + await this.respond({ + status: "FAILED", + reason: "Lambda Function Timeout", + data: this.context.logStreamName + }); + this.timedOut = true; + }, context.getRemainingTimeInMillis() - 1200); + this.event = event; + this.physicalResourceId = extractPhysicalResourceId(event); + } + async handle() { + try { + if ("stateMachineArn" in this.event.ResourceProperties) { + const req = { + stateMachineArn: this.event.ResourceProperties.stateMachineArn, + name: this.event.RequestId, + input: JSON.stringify(this.event) + }; + await this.startExecution(req); + return; + } else { + const response = await this.processEvent(this.event.ResourceProperties); + return response; + } + } catch (e) { + console.log(e); + throw e; + } finally { + clearTimeout(this.timeout); + } + } + async handleIsComplete() { + try { + const result = await this.processEvent(this.event.ResourceProperties); + return result; + } catch (e) { + console.log(e); + return; + } finally { + clearTimeout(this.timeout); + } + } + async startExecution(req) { + try { + const sfn = new AWS.StepFunctions(); + await sfn.startExecution(req).promise(); + } finally { + clearTimeout(this.timeout); + } + } + respond(response) { + if (this.timedOut) { + return; + } + const cfResponse = { + Status: response.status, + Reason: response.reason, + PhysicalResourceId: this.physicalResourceId, + StackId: this.event.StackId, + RequestId: this.event.RequestId, + LogicalResourceId: this.event.LogicalResourceId, + NoEcho: false, + Data: response.data + }; + const responseBody = JSON.stringify(cfResponse); + console.log("Responding to CloudFormation", responseBody); + const parsedUrl = url.parse(this.event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: "PUT", + headers: { "content-type": "", "content-length": responseBody.length } + }; + return new Promise((resolve, reject) => { + try { + const request2 = https.request(requestOptions, resolve); + request2.on("error", reject); + request2.write(responseBody); + request2.end(); + } catch (e) { + reject(e); + } finally { + clearTimeout(this.timeout); + } + }); + } +}; +function extractPhysicalResourceId(event) { + switch (event.RequestType) { + case "Create": + return event.LogicalResourceId; + case "Update": + case "Delete": + return event.PhysicalResourceId; + } +} + +// lib/assertions/providers/lambda-handler/assertion.ts +var AssertionHandler = class extends CustomResourceHandler { + async processEvent(request2) { + let actual = decodeCall(request2.actual); + const expected = decodeCall(request2.expected); + let result; + const matcher = new MatchCreator(expected).getMatcher(); + console.log(`Testing equality between ${JSON.stringify(request2.actual)} and ${JSON.stringify(request2.expected)}`); + const matchResult = matcher.test(actual); + matchResult.finished(); + if (matchResult.hasFailed()) { + result = { + failed: true, + assertion: JSON.stringify({ + status: "fail", + message: matchResult.renderMismatch() + }) + }; + if (request2.failDeployment) { + throw new Error(result.assertion); + } + } else { + result = { + assertion: JSON.stringify({ + status: "success" + }) + }; + } + return result; + } +}; +var MatchCreator = class { + constructor(obj) { + this.parsedObj = { + matcher: obj + }; + } + getMatcher() { + try { + const final = JSON.parse(JSON.stringify(this.parsedObj), function(_k, v) { + const nested = Object.keys(v)[0]; + switch (nested) { + case "$ArrayWith": + return Match.arrayWith(v[nested]); + case "$ObjectLike": + return Match.objectLike(v[nested]); + case "$StringLike": + return Match.stringLikeRegexp(v[nested]); + case "$SerializedJson": + return Match.serializedJson(v[nested]); + default: + return v; + } + }); + if (Matcher.isMatcher(final.matcher)) { + return final.matcher; + } + return Match.exact(final.matcher); + } catch { + return Match.exact(this.parsedObj.matcher); + } + } +}; +function decodeCall(call) { + if (!call) { + return void 0; + } + try { + const parsed = JSON.parse(call); + return parsed; + } catch (e) { + return call; + } +} + +// lib/assertions/providers/lambda-handler/utils.ts +function decode(object) { + return JSON.parse(JSON.stringify(object), (_k, v) => { + switch (v) { + case "TRUE:BOOLEAN": + return true; + case "FALSE:BOOLEAN": + return false; + default: + return v; + } + }); +} + +// lib/assertions/providers/lambda-handler/sdk.ts +function flatten(object) { + return Object.assign( + {}, + ...function _flatten(child, path = []) { + return [].concat(...Object.keys(child).map((key) => { + let childKey = Buffer.isBuffer(child[key]) ? child[key].toString("utf8") : child[key]; + if (typeof childKey === "string") { + childKey = isJsonString(childKey); + } + return typeof childKey === "object" && childKey !== null ? _flatten(childKey, path.concat([key])) : { [path.concat([key]).join(".")]: childKey }; + })); + }(object) + ); +} +var AwsApiCallHandler = class extends CustomResourceHandler { + async processEvent(request2) { + const AWS2 = require("aws-sdk"); + console.log(`AWS SDK VERSION: ${AWS2.VERSION}`); + if (!Object.prototype.hasOwnProperty.call(AWS2, request2.service)) { + throw Error(`Service ${request2.service} does not exist in AWS SDK version ${AWS2.VERSION}.`); + } + const service = new AWS2[request2.service](); + const response = await service[request2.api](request2.parameters && decode(request2.parameters)).promise(); + console.log(`SDK response received ${JSON.stringify(response)}`); + delete response.ResponseMetadata; + const respond = { + apiCallResponse: response + }; + const flatData = { + ...flatten(respond) + }; + let resp = respond; + if (request2.outputPaths) { + resp = filterKeys(flatData, request2.outputPaths); + } else if (request2.flattenResponse === "true") { + resp = flatData; + } + console.log(`Returning result ${JSON.stringify(resp)}`); + return resp; + } +}; +function filterKeys(object, searchStrings) { + return Object.entries(object).reduce((filteredObject, [key, value]) => { + for (const searchString of searchStrings) { + if (key.startsWith(`apiCallResponse.${searchString}`)) { + filteredObject[key] = value; + } + } + return filteredObject; + }, {}); +} +function isJsonString(value) { + try { + return JSON.parse(value); + } catch { + return value; + } +} + +// lib/assertions/providers/lambda-handler/types.ts +var ASSERT_RESOURCE_TYPE = "Custom::DeployAssert@AssertEquals"; +var SDK_RESOURCE_TYPE_PREFIX = "Custom::DeployAssert@SdkCall"; + +// lib/assertions/providers/lambda-handler/index.ts +async function handler(event, context) { + console.log(`Event: ${JSON.stringify({ ...event, ResponseURL: "..." })}`); + const provider = createResourceHandler(event, context); + try { + if (event.RequestType === "Delete") { + await provider.respond({ + status: "SUCCESS", + reason: "OK" + }); + return; + } + const result = await provider.handle(); + if ("stateMachineArn" in event.ResourceProperties) { + console.info('Found "stateMachineArn", waiter statemachine started'); + return; + } else if ("expected" in event.ResourceProperties) { + console.info('Found "expected", testing assertions'); + const actualPath = event.ResourceProperties.actualPath; + const actual = actualPath ? result[`apiCallResponse.${actualPath}`] : result.apiCallResponse; + const assertion = new AssertionHandler({ + ...event, + ResourceProperties: { + ServiceToken: event.ServiceToken, + actual, + expected: event.ResourceProperties.expected + } + }, context); + try { + const assertionResult = await assertion.handle(); + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: { + ...assertionResult, + ...result + } + }); + return; + } catch (e) { + await provider.respond({ + status: "FAILED", + reason: e.message ?? "Internal Error" + }); + return; + } + } + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: result + }); + } catch (e) { + await provider.respond({ + status: "FAILED", + reason: e.message ?? "Internal Error" + }); + return; + } + return; +} +async function onTimeout(timeoutEvent) { + const isCompleteRequest = JSON.parse(JSON.parse(timeoutEvent.Cause).errorMessage); + const provider = createResourceHandler(isCompleteRequest, standardContext); + await provider.respond({ + status: "FAILED", + reason: "Operation timed out: " + JSON.stringify(isCompleteRequest) + }); +} +async function isComplete(event, context) { + console.log(`Event: ${JSON.stringify({ ...event, ResponseURL: "..." })}`); + const provider = createResourceHandler(event, context); + try { + const result = await provider.handleIsComplete(); + const actualPath = event.ResourceProperties.actualPath; + if (result) { + const actual = actualPath ? result[`apiCallResponse.${actualPath}`] : result.apiCallResponse; + if ("expected" in event.ResourceProperties) { + const assertion = new AssertionHandler({ + ...event, + ResourceProperties: { + ServiceToken: event.ServiceToken, + actual, + expected: event.ResourceProperties.expected + } + }, context); + const assertionResult = await assertion.handleIsComplete(); + if (!(assertionResult == null ? void 0 : assertionResult.failed)) { + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: { + ...assertionResult, + ...result + } + }); + return; + } else { + console.log(`Assertion Failed: ${JSON.stringify(assertionResult)}`); + throw new Error(JSON.stringify(event)); + } + } + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: result + }); + } else { + console.log("No result"); + throw new Error(JSON.stringify(event)); + } + return; + } catch (e) { + console.log(e); + throw new Error(JSON.stringify(event)); + } +} +function createResourceHandler(event, context) { + if (event.ResourceType.startsWith(SDK_RESOURCE_TYPE_PREFIX)) { + return new AwsApiCallHandler(event, context); + } else if (event.ResourceType.startsWith(ASSERT_RESOURCE_TYPE)) { + return new AssertionHandler(event, context); + } else { + throw new Error(`Unsupported resource type "${event.ResourceType}`); + } +} +var standardContext = { + getRemainingTimeInMillis: () => 9e4 +}; +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + handler, + isComplete, + onTimeout +}); diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/cdk.out b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/cdk.out new file mode 100644 index 0000000000000..145739f539580 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"22.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/integ.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/integ.json new file mode 100644 index 0000000000000..9ef53e2c0530b --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "22.0.0", + "testCases": { + "SNS Subscriptions/DefaultTest": { + "stacks": [ + "SnsToSqsStack" + ], + "assertionStack": "SNS Subscriptions/DefaultTest/DeployAssert", + "assertionStackName": "SNSSubscriptionsDefaultTestDeployAssertD77125EB" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/manifest.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/manifest.json new file mode 100644 index 0000000000000..23c2f223c4c8b --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/manifest.json @@ -0,0 +1,215 @@ +{ + "version": "22.0.0", + "artifacts": { + "SnsToSqsStack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "SnsToSqsStack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "SnsToSqsStack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "SnsToSqsStack.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/1b2f5417b0f02417ad80f41e818ff0917ea4b65ef69016cec3a20d314fe454bc.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "SnsToSqsStack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "SnsToSqsStack.assets" + ], + "metadata": { + "/SnsToSqsStack/MyTopic/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyTopic86869434" + } + ], + "/SnsToSqsStack/Exports/Output{\"Ref\":\"MyTopic86869434\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefMyTopic868694349D03D60F" + } + ], + "/SnsToSqsStack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/SnsToSqsStack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "SnsToSqsStack" + }, + "QueueStack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "QueueStack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "QueueStack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "QueueStack.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/706116cb7bbce00b1ab402aeeaa66b35fc2fef8e57e4192f8ee4747dc5864691.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "QueueStack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "SnsToSqsStack", + "QueueStack.assets" + ], + "metadata": { + "/QueueStack/MyQueue/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyQueueE6CA6235" + } + ], + "/QueueStack/MyQueue/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyQueuePolicy6BBEDDAC" + } + ], + "/QueueStack/MyQueue/SnsToSqsStackMyTopic3F1182C2/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyQueueSnsToSqsStackMyTopic3F1182C25E300A0F" + } + ], + "/QueueStack/Exports/Output{\"Ref\":\"MyQueueE6CA6235\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefMyQueueE6CA623512A57419" + } + ], + "/QueueStack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/QueueStack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "QueueStack" + }, + "SNSSubscriptionsDefaultTestDeployAssertD77125EB.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "SNSSubscriptionsDefaultTestDeployAssertD77125EB.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "SNSSubscriptionsDefaultTestDeployAssertD77125EB": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "SNSSubscriptionsDefaultTestDeployAssertD77125EB.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/9c226b6b87c5f7d528d0a2cdc0627cfb3165de9fb9df32956d25a79356409185.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "SNSSubscriptionsDefaultTestDeployAssertD77125EB.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "SnsToSqsStack", + "QueueStack", + "SNSSubscriptionsDefaultTestDeployAssertD77125EB.assets" + ], + "metadata": { + "/SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSNSpublish/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallSNSpublish" + } + ], + "/SNS Subscriptions/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73" + } + ], + "/SNS Subscriptions/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F" + } + ], + "/SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSQSreceiveMessage/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallSQSreceiveMessage" + } + ], + "/SNS Subscriptions/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/SNS Subscriptions/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "SNS Subscriptions/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/tree.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/tree.json new file mode 100644 index 0000000000000..4d3e124a72458 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.js.snapshot/tree.json @@ -0,0 +1,476 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "SnsToSqsStack": { + "id": "SnsToSqsStack", + "path": "SnsToSqsStack", + "children": { + "MyTopic": { + "id": "MyTopic", + "path": "SnsToSqsStack/MyTopic", + "children": { + "Resource": { + "id": "Resource", + "path": "SnsToSqsStack/MyTopic/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SNS::Topic", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sns.CfnTopic", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sns.Topic", + "version": "0.0.0" + } + }, + "Exports": { + "id": "Exports", + "path": "SnsToSqsStack/Exports", + "children": { + "Output{\"Ref\":\"MyTopic86869434\"}": { + "id": "Output{\"Ref\":\"MyTopic86869434\"}", + "path": "SnsToSqsStack/Exports/Output{\"Ref\":\"MyTopic86869434\"}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "SnsToSqsStack/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "SnsToSqsStack/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "QueueStack": { + "id": "QueueStack", + "path": "QueueStack", + "children": { + "MyQueue": { + "id": "MyQueue", + "path": "QueueStack/MyQueue", + "children": { + "Resource": { + "id": "Resource", + "path": "QueueStack/MyQueue/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SQS::Queue", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sqs.CfnQueue", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "QueueStack/MyQueue/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "QueueStack/MyQueue/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SQS::QueuePolicy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::ImportValue": "SnsToSqsStack:ExportsOutputRefMyTopic868694349D03D60F" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "MyQueueE6CA6235", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "queues": [ + { + "Ref": "MyQueueE6CA6235" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sqs.CfnQueuePolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sqs.QueuePolicy", + "version": "0.0.0" + } + }, + "SnsToSqsStackMyTopic3F1182C2": { + "id": "SnsToSqsStackMyTopic3F1182C2", + "path": "QueueStack/MyQueue/SnsToSqsStackMyTopic3F1182C2", + "children": { + "Resource": { + "id": "Resource", + "path": "QueueStack/MyQueue/SnsToSqsStackMyTopic3F1182C2/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SNS::Subscription", + "aws:cdk:cloudformation:props": { + "protocol": "sqs", + "topicArn": { + "Fn::ImportValue": "SnsToSqsStack:ExportsOutputRefMyTopic868694349D03D60F" + }, + "endpoint": { + "Fn::GetAtt": [ + "MyQueueE6CA6235", + "Arn" + ] + }, + "filterPolicy": { + "background": { + "color": [ + "red", + "green", + { + "anything-but": [ + "white", + "orange" + ] + } + ] + }, + "price": [ + { + "numeric": [ + "=", + 100 + ] + }, + { + "numeric": [ + "=", + 200 + ] + }, + { + "numeric": [ + ">", + 500 + ] + }, + { + "numeric": [ + "<", + 1000 + ] + }, + { + "numeric": [ + ">=", + 300, + "<=", + 350 + ] + }, + { + "numeric": [ + ">", + 2000, + "<", + 3000 + ] + } + ] + }, + "filterPolicyScope": "MessageBody" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sns.CfnSubscription", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sns.Subscription", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sqs.Queue", + "version": "0.0.0" + } + }, + "Exports": { + "id": "Exports", + "path": "QueueStack/Exports", + "children": { + "Output{\"Ref\":\"MyQueueE6CA6235\"}": { + "id": "Output{\"Ref\":\"MyQueueE6CA6235\"}", + "path": "QueueStack/Exports/Output{\"Ref\":\"MyQueueE6CA6235\"}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "QueueStack/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "QueueStack/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "SNS Subscriptions": { + "id": "SNS Subscriptions", + "path": "SNS Subscriptions", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "SNS Subscriptions/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "SNS Subscriptions/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "SNS Subscriptions/DefaultTest/DeployAssert", + "children": { + "AwsApiCallSNSpublish": { + "id": "AwsApiCallSNSpublish", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSNSpublish", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSNSpublish/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSNSpublish/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSNSpublish/Default", + "children": { + "Default": { + "id": "Default", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSNSpublish/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AwsApiCall", + "version": "0.0.0" + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81": { + "id": "SingletonFunction1488541a7b23466481b69b4408076b81", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81", + "children": { + "Staging": { + "id": "Staging", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + }, + "AwsApiCallSQSreceiveMessage": { + "id": "AwsApiCallSQSreceiveMessage", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSQSreceiveMessage", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSQSreceiveMessage/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSQSreceiveMessage/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSQSreceiveMessage/Default", + "children": { + "Default": { + "id": "Default", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/AwsApiCallSQSreceiveMessage/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AwsApiCall", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "SNS Subscriptions/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.189" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.ts new file mode 100644 index 0000000000000..0a6c2ec1b8037 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.ts @@ -0,0 +1,52 @@ +import * as sns from '@aws-cdk/aws-sns'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as cdk from '@aws-cdk/core'; +import { IntegTest, ExpectedResult } from '@aws-cdk/integ-tests'; +import * as subs from '../lib'; +class SnsToSqsStack extends cdk.Stack { + topic: sns.Topic; + queue: sqs.Queue; + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + this.topic = new sns.Topic(this, 'MyTopic'); + const queueStack = new cdk.Stack(app, 'QueueStack'); + this.queue = new sqs.Queue(queueStack, 'MyQueue'); + this.topic.addSubscription(new subs.SqsSubscription(this.queue, { + filterPolicyWithMessageBody: { + background: sns.Policy.policy({ + color: sns.Filter.filter(sns.SubscriptionFilter.stringFilter({ + allowlist: ['red', 'green'], + denylist: ['white', 'orange'], + })), + }), + price: sns.Filter.filter(sns.SubscriptionFilter.numericFilter({ + allowlist: [100, 200], + between: { start: 300, stop: 350 }, + greaterThan: 500, + lessThan: 1000, + betweenStrict: { start: 2000, stop: 3000 }, + })), + }, + })); + } +} +// Beginning of the test suite +const app = new cdk.App(); +const stack = new SnsToSqsStack(app, 'SnsToSqsStack'); +const integTest = new IntegTest(app, 'SNS Subscriptions', { + testCases: [ + stack, + ], +}); +integTest.assertions.awsApiCall('SNS', 'publish', { + Message: '{ background: { color: \'green\' }, price: 200 }', + TopicArn: stack.topic.topicArn, +}); +const message = integTest.assertions.awsApiCall('SQS', 'receiveMessage', { + QueueUrl: stack.queue.queueUrl, + WaitTimeSeconds: 20, +}); +message.expect(ExpectedResult.objectLike({ + Messages: [{ Body: '{color: "green", price: 200}' }], +})); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts index 4eed6baf482e1..0b768d668e698 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts @@ -1194,13 +1194,13 @@ describe('Restrict sqs decryption feature flag', () => { }); test('lambda subscription', () => { - const fction = new lambda.Function(stack, 'MyFunc', { + const func = new lambda.Function(stack, 'MyFunc', { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = function(e, c, cb) { return cb() }'), }); - topic.addSubscription(new subs.LambdaSubscription(fction)); + topic.addSubscription(new subs.LambdaSubscription(func)); Template.fromStack(stack).templateMatches({ 'Resources': { @@ -1305,13 +1305,13 @@ test('lambda subscription, cross region env agnostic', () => { topicName: 'topicName', displayName: 'displayName', }); - const fction = new lambda.Function(lambdaStack, 'MyFunc', { + const func = new lambda.Function(lambdaStack, 'MyFunc', { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = function(e, c, cb) { return cb() }'), }); - topic1.addSubscription(new subs.LambdaSubscription(fction)); + topic1.addSubscription(new subs.LambdaSubscription(func)); Template.fromStack(lambdaStack).templateMatches({ 'Resources': { @@ -1419,13 +1419,13 @@ test('lambda subscription, cross region', () => { topicName: 'topicName', displayName: 'displayName', }); - const fction = new lambda.Function(lambdaStack, 'MyFunc', { + const func = new lambda.Function(lambdaStack, 'MyFunc', { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = function(e, c, cb) { return cb() }'), }); - topic1.addSubscription(new subs.LambdaSubscription(fction)); + topic1.addSubscription(new subs.LambdaSubscription(func)); Template.fromStack(lambdaStack).templateMatches({ 'Resources': { @@ -1873,13 +1873,13 @@ test('throws with mutliple subscriptions of the same subscriber', () => { }); test('with filter policy', () => { - const fction = new lambda.Function(stack, 'MyFunc', { + const func = new lambda.Function(stack, 'MyFunc', { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = function(e, c, cb) { return cb() }'), }); - topic.addSubscription(new subs.LambdaSubscription(fction, { + topic.addSubscription(new subs.LambdaSubscription(func, { filterPolicy: { color: sns.SubscriptionFilter.stringFilter({ allowlist: ['red'], @@ -1927,6 +1927,53 @@ test('with filter policy', () => { }); }); +test('with filter policy scope MessageBody', () => { + const func = new lambda.Function(stack, 'MyFunc', { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = function(e, c, cb) { return cb() }'), + }); + + topic.addSubscription(new subs.LambdaSubscription(func, { + filterPolicyWithMessageBody: { + color: sns.FilterOrPolicy.policy({ + background: sns.FilterOrPolicy.filter(sns.SubscriptionFilter.stringFilter({ + allowlist: ['red'], + matchPrefixes: ['bl', 'ye'], + })), + }), + size: sns.FilterOrPolicy.filter(sns.SubscriptionFilter.stringFilter({ + denylist: ['small', 'medium'], + })), + }, + })); + + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { + 'FilterPolicy': { + 'color': { + 'background': [ + 'red', + { + 'prefix': 'bl', + }, + { + 'prefix': 'ye', + }, + ], + }, + 'size': [ + { + 'anything-but': [ + 'small', + 'medium', + ], + }, + ], + }, + FilterPolicyScope: 'MessageBody', + }); +}); + test('region property is present on an imported topic - sqs', () => { const imported = sns.Topic.fromTopicArn(stack, 'mytopic', 'arn:aws:sns:us-east-1:1234567890:mytopic'); const queue = new sqs.Queue(stack, 'myqueue'); diff --git a/packages/@aws-cdk/aws-sns/README.md b/packages/@aws-cdk/aws-sns/README.md index 5bec12a2a7fcf..0b03c87d5821a 100644 --- a/packages/@aws-cdk/aws-sns/README.md +++ b/packages/@aws-cdk/aws-sns/README.md @@ -93,6 +93,31 @@ myTopic.addSubscription(new subscriptions.LambdaSubscription(fn, { })); ``` +#### Payload-based filtering + +To filter messages based on the payload or body of the message, use the `filterPolicyWithMessageBody` property. This type of filter policy supports creating filters on nested objects. + +Example with a Lambda subscription: + +```ts +import * as lambda from '@aws-cdk/aws-lambda'; + +const myTopic = new sns.Topic(this, 'MyTopic'); +declare const fn: lambda.Function; + +// Lambda should receive only message matching the following conditions on message body: +// color: 'red' or 'orange' +myTopic.addSubscription(new subscriptions.LambdaSubscription(fn, { + filterPolicyWithMessageBody: { + background: sns.FilterOrPolicy.policy({ + color: sns.FilterOrPolicy.filter(sns.SubscriptionFilter.stringFilter({ + allowlist: ['red', 'orange'], + })), + }), + }, +})); +``` + ### Example of Firehose Subscription ```ts diff --git a/packages/@aws-cdk/aws-sns/lib/subscription.ts b/packages/@aws-cdk/aws-sns/lib/subscription.ts index 8d405fc289b76..e8e7d0cde84d9 100644 --- a/packages/@aws-cdk/aws-sns/lib/subscription.ts +++ b/packages/@aws-cdk/aws-sns/lib/subscription.ts @@ -36,7 +36,15 @@ export interface SubscriptionOptions { * * @default - all messages are delivered */ - readonly filterPolicy?: { [attribute: string]: SubscriptionFilter }; + readonly filterPolicy? : { [attribute: string]: SubscriptionFilter }; + + /** + * The filter policy that is applied on the message body. + * To apply a filter policy to the message attributes, use `filterPolicy`. A maximum of one of `filterPolicyWithMessageBody` and `filterPolicy` may be used. + * + * @default - all messages are delivered + */ + readonly filterPolicyWithMessageBody?: { [attribute: string]: FilterOrPolicy }; /** * The region where the topic resides, in the case of cross-region subscriptions @@ -85,6 +93,8 @@ export class Subscription extends Resource { private readonly filterPolicy?: { [attribute: string]: any[] }; + private readonly filterPolicyWithMessageBody? : {[attribute: string]: FilterOrPolicy }; + constructor(scope: Construct, id: string, props: SubscriptionProps) { super(scope, id); @@ -115,20 +125,30 @@ export class Subscription extends Resource { if (total > 150) { throw new Error(`The total combination of values (${total}) must not exceed 150.`); } + } else if (props.filterPolicyWithMessageBody) { + if (Object.keys(props.filterPolicyWithMessageBody).length > 5) { + throw new Error('A filter policy can have a maximum of 5 attribute names.'); + } + this.filterPolicyWithMessageBody = props.filterPolicyWithMessageBody; } if (props.protocol === SubscriptionProtocol.FIREHOSE && !props.subscriptionRoleArn) { throw new Error('Subscription role arn is required field for subscriptions with a firehose protocol.'); } - this.deadLetterQueue = this.buildDeadLetterQueue(props); + // Format filter policy + const filterPolicy = this.filterPolicyWithMessageBody + ? buildFilterPolicyWithMessageBody(this.filterPolicyWithMessageBody) + : this.filterPolicy; + this.deadLetterQueue = this.buildDeadLetterQueue(props); new CfnSubscription(this, 'Resource', { endpoint: props.endpoint, protocol: props.protocol, topicArn: props.topic.topicArn, rawMessageDelivery: props.rawMessageDelivery, - filterPolicy: this.filterPolicy, + filterPolicy, + filterPolicyScope: this.filterPolicyWithMessageBody ? 'MessageBody' : undefined, region: props.region, redrivePolicy: this.buildDeadLetterConfig(this.deadLetterQueue), subscriptionRoleArn: props.subscriptionRoleArn, @@ -215,3 +235,118 @@ export enum SubscriptionProtocol { */ FIREHOSE = 'firehose' } + +function buildFilterPolicyWithMessageBody( + inputObject: { [key: string]: FilterOrPolicy }, + depth = 1, + totalCombinationValues = [1], +): { [key: string]: any } { + const result: { [key: string]: any } = {}; + + for (const [key, filterOrPolicy] of Object.entries(inputObject)) { + if (filterOrPolicy.isPolicy()) { + result[key] = buildFilterPolicyWithMessageBody(filterOrPolicy.policyDoc, depth + 1, totalCombinationValues); + } else if (filterOrPolicy.isFilter()) { + const filter = filterOrPolicy.filterDoc.conditions; + result[key] = filter; + totalCombinationValues[0] *= filter.length * depth; + } + } + + // https://docs.aws.amazon.com/sns/latest/dg/subscription-filter-policy-constraints.html + if (totalCombinationValues[0] > 150) { + throw new Error(`The total combination of values (${totalCombinationValues}) must not exceed 150.`); + } + + return result; +}; + +/** + * The type of the MessageBody at a given key value pair + */ +export enum FilterOrPolicyType { + /** + * The filter of the MessageBody + */ + FILTER, + /** + * A nested key of the MessageBody + */ + POLICY, +} + +/** + * Class for building the FilterPolicy by avoiding union types + */ +export abstract class FilterOrPolicy { + /** + * Filter of MessageBody + * @param filter + * @returns + */ + public static filter(filter: SubscriptionFilter) { + return new Filter(filter); + } + + /** + * Policy of MessageBody + * @param policy + * @returns + */ + public static policy(policy: { [attribute: string]: FilterOrPolicy }) { + return new Policy(policy); + } + + /** + * Type switch for disambiguating between subclasses + */ + abstract readonly type: FilterOrPolicyType; + + /** + * Check if instance is `Policy` type + */ + public isPolicy(): this is Policy { + return this.type === FilterOrPolicyType.POLICY; + } + + /** + * Check if instance is `Filter` type + */ + public isFilter(): this is Filter { + return this.type === FilterOrPolicyType.FILTER; + } +} + +/** + * Filter implementation of FilterOrPolicy + */ +export class Filter extends FilterOrPolicy { + /** + * Type used in DFS buildFilterPolicyWithMessageBody to determine json value type + */ + public readonly type = FilterOrPolicyType.FILTER; + /** + * Policy constructor + * @param filterDoc filter argument to construct + */ + public constructor(public readonly filterDoc: SubscriptionFilter) { + super(); + } +} + +/** + * Policy Implementation of FilterOrPolicy + */ +export class Policy extends FilterOrPolicy { + /** + * Type used in DFS buildFilterPolicyWithMessageBody to determine json value type + */ + public readonly type = FilterOrPolicyType.POLICY; + /** + * Policy constructor + * @param policyDoc policy argument to construct + */ + public constructor(public readonly policyDoc: { [attribute: string]: FilterOrPolicy }) { + super(); + } +} diff --git a/packages/@aws-cdk/aws-sns/test/subscription.test.ts b/packages/@aws-cdk/aws-sns/test/subscription.test.ts index 37359f2769b96..5b5797c186cc0 100644 --- a/packages/@aws-cdk/aws-sns/test/subscription.test.ts +++ b/packages/@aws-cdk/aws-sns/test/subscription.test.ts @@ -153,6 +153,56 @@ describe('Subscription', () => { }); + test('with filter policy and filter policy scope MessageBody', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + new sns.Subscription(stack, 'Subscription', { + endpoint: 'endpoint', + filterPolicyWithMessageBody: { + background: sns.Policy.policy({ + color: sns.Filter.filter(sns.SubscriptionFilter.stringFilter({ + allowlist: ['red', 'green'], + denylist: ['white', 'orange'], + })), + }), + price: sns.Filter.filter(sns.SubscriptionFilter.numericFilter({ + allowlist: [100, 200], + between: { start: 300, stop: 350 }, + greaterThan: 500, + lessThan: 1000, + betweenStrict: { start: 2000, stop: 3000 }, + })), + }, + protocol: sns.SubscriptionProtocol.LAMBDA, + topic, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Subscription', { + FilterPolicy: { + background: { + color: [ + 'red', + 'green', + { 'anything-but': ['white', 'orange'] }, + ], + }, + price: [ + { numeric: ['=', 100] }, + { numeric: ['=', 200] }, + { numeric: ['>', 500] }, + { numeric: ['<', 1000] }, + { numeric: ['>=', 300, '<=', 350] }, + { numeric: ['>', 2000, '<', 3000] }, + ], + }, + FilterPolicyScope: 'MessageBody', + }); + }); + test('with numeric filter and 0 values', () => { // GIVEN const stack = new cdk.Stack(); @@ -275,6 +325,24 @@ describe('Subscription', () => { }); + test('throws with more than 150 conditions in a filter policy with filter policy scope set to MessageBody', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + // THEN + expect(() => new sns.Subscription(stack, 'Subscription', { + endpoint: 'endpoint', + protocol: sns.SubscriptionProtocol.LAMBDA, + topic, + filterPolicyWithMessageBody: { + a: sns.Policy.policy({ b: sns.Filter.filter(new sns.SubscriptionFilter([...Array.from(Array(10).keys())])) }), + c: sns.Policy.policy({ d: sns.Filter.filter(new sns.SubscriptionFilter([...Array.from(Array(5).keys())])) }), + }, + })).toThrow(/\(200\) must not exceed 150/); + + }); + test('throws an error when subscription role arn is not entered with firehose subscription protocol', () => { // GIVEN const stack = new cdk.Stack();