diff --git a/packages/@aws-cdk/aws-sns-subscriptions/README.md b/packages/@aws-cdk/aws-sns-subscriptions/README.md index 416177f518138..1956484a309ee 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/README.md +++ b/packages/@aws-cdk/aws-sns-subscriptions/README.md @@ -31,7 +31,7 @@ const myTopic = new sns.Topic(this, 'MyTopic'); ### HTTPS -Add an HTTPS Subscription to your topic: +Add an HTTP or HTTPS Subscription to your topic: ```ts import subscriptions = require('@aws-cdk/aws-sns-subscriptions'); @@ -39,6 +39,16 @@ import subscriptions = require('@aws-cdk/aws-sns-subscriptions'); myTopic.addSubscription(new subscriptions.UrlSubscription('https://foobar.com/')); ``` +The URL being subscribed can also be [tokens](https://docs.aws.amazon.com/cdk/latest/guide/tokens.html), that resolve +to a URL during deployment. A typical use case is when the URL is passed in as a [CloudFormation +parameter](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html). The +following code defines a CloudFormation parameter and uses it in a URL subscription. + +```ts +const url = new CfnParameter(this, 'url-param'); +myTopic.addSubscription(new subscriptions.UrlSubscription(url.valueAsString())); +``` + ### Amazon SQS Subscribe a queue to your topic: @@ -82,5 +92,15 @@ import subscriptions = require('@aws-cdk/aws-sns-subscriptions'); myTopic.addSubscription(new subscriptions.EmailSubscription('foo@bar.com')); ``` +The email being subscribed can also be [tokens](https://docs.aws.amazon.com/cdk/latest/guide/tokens.html), that resolve +to an email address during deployment. A typical use case is when the email address is passed in as a [CloudFormation +parameter](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html). The +following code defines a CloudFormation parameter and uses it in an email subscription. + +```ts +const emailAddress = new CfnParameter(this, 'email-param'); +myTopic.addSubscription(new subscriptions.EmailSubscription(emailAddress.valueAsString())); +``` + Note that email subscriptions require confirmation by visiting the link sent to the email address. diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts index da1709fb4e6d8..12cdabd40e7b0 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts @@ -53,7 +53,7 @@ export class UrlSubscription implements sns.ITopicSubscription { public bind(_topic: sns.ITopic): sns.TopicSubscriptionConfig { return { - subscriberId: this.unresolvedUrl ? 'UnresolvedUrl' : this.url, + subscriberId: this.url, endpoint: this.url, protocol: this.protocol, rawMessageDelivery: this.props.rawMessageDelivery, 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 d7bfe9c6d6c21..095ba53f29f01 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; -import { CfnParameter, SecretValue, Stack } from '@aws-cdk/core'; +import { CfnParameter, Stack, Token } from '@aws-cdk/core'; import * as subs from '../lib'; // tslint:disable:object-literal-key-quotes @@ -72,9 +72,8 @@ test('url subscription (with raw delivery)', () => { }); test('url subscription (unresolved url with protocol)', () => { - const secret = SecretValue.secretsManager('my-secret'); - const url = secret.toString(); - topic.addSubscription(new subs.UrlSubscription(url, {protocol: sns.SubscriptionProtocol.HTTPS})); + const urlToken = Token.asString({ Ref : "my-url-1" }); + topic.addSubscription(new subs.UrlSubscription(urlToken, {protocol: sns.SubscriptionProtocol.HTTPS})); expect(stack).toMatchTemplate({ "Resources": { @@ -85,10 +84,52 @@ test('url subscription (unresolved url with protocol)', () => { "TopicName": "topicName" } }, - "MyTopicUnresolvedUrlBA127FB3": { + "MyTopicTokenSubscription141DD1BE2": { "Type": "AWS::SNS::Subscription", "Properties": { - "Endpoint": "{{resolve:secretsmanager:my-secret:SecretString:::}}", + "Endpoint": { + "Ref": "my-url-1" + }, + "Protocol": "https", + "TopicArn": { "Ref": "MyTopic86869434" }, + } + } + } + }); +}); + +test('url subscription (double unresolved url with protocol)', () => { + const urlToken1 = Token.asString({ Ref : "my-url-1" }); + const urlToken2 = Token.asString({ Ref : "my-url-2" }); + + topic.addSubscription(new subs.UrlSubscription(urlToken1, {protocol: sns.SubscriptionProtocol.HTTPS})); + topic.addSubscription(new subs.UrlSubscription(urlToken2, {protocol: sns.SubscriptionProtocol.HTTPS})); + + expect(stack).toMatchTemplate({ + "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DisplayName": "displayName", + "TopicName": "topicName" + } + }, + "MyTopicTokenSubscription141DD1BE2": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref" : "my-url-1" + }, + "Protocol": "https", + "TopicArn": { "Ref": "MyTopic86869434" }, + } + }, + "MyTopicTokenSubscription293BFE3F9": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref" : "my-url-2" + }, "Protocol": "https", "TopicArn": { "Ref": "MyTopic86869434" }, } @@ -103,9 +144,9 @@ test('url subscription (unknown protocol)', () => { }); test('url subscription (unresolved url without protocol)', () => { - const secret = SecretValue.secretsManager('my-secret'); - const url = secret.toString(); - expect(() => topic.addSubscription(new subs.UrlSubscription(url))) + const urlToken = Token.asString({ Ref : "my-url-1" }); + + expect(() => topic.addSubscription(new subs.UrlSubscription(urlToken))) .toThrowError(/Must provide protocol if url is unresolved/); }); @@ -329,6 +370,150 @@ test('email subscription', () => { }); }); +test('email subscription with unresolved', () => { + const emailToken = Token.asString({ Ref : "my-email-1" }); + topic.addSubscription(new subs.EmailSubscription(emailToken)); + + expect(stack).toMatchTemplate({ + "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DisplayName": "displayName", + "TopicName": "topicName" + } + }, + "MyTopicTokenSubscription141DD1BE2": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref" : "my-email-1" + }, + "Protocol": "email", + "TopicArn": { + "Ref": "MyTopic86869434" + } + } + } + } + }); +}); + +test('email and url subscriptions with unresolved', () => { + const emailToken = Token.asString({ Ref : "my-email-1" }); + const urlToken = Token.asString({ Ref : "my-url-1" }); + topic.addSubscription(new subs.EmailSubscription(emailToken)); + topic.addSubscription(new subs.UrlSubscription(urlToken, {protocol: sns.SubscriptionProtocol.HTTPS})); + + expect(stack).toMatchTemplate({ + "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DisplayName": "displayName", + "TopicName": "topicName" + } + }, + "MyTopicTokenSubscription141DD1BE2": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref" : "my-email-1" + }, + "Protocol": "email", + "TopicArn": { + "Ref": "MyTopic86869434" + } + } + }, + "MyTopicTokenSubscription293BFE3F9": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref" : "my-url-1" + }, + "Protocol": "https", + "TopicArn": { + "Ref": "MyTopic86869434" + } + } + } + } + }); +}); + +test('email and url subscriptions with unresolved - four subscriptions', () => { + const emailToken1 = Token.asString({ Ref : "my-email-1" }); + const emailToken2 = Token.asString({ Ref : "my-email-2" }); + const emailToken3 = Token.asString({ Ref : "my-email-3" }); + const emailToken4 = Token.asString({ Ref : "my-email-4" }); + + topic.addSubscription(new subs.EmailSubscription(emailToken1)); + topic.addSubscription(new subs.EmailSubscription(emailToken2)); + topic.addSubscription(new subs.EmailSubscription(emailToken3)); + topic.addSubscription(new subs.EmailSubscription(emailToken4)); + + expect(stack).toMatchTemplate({ + "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DisplayName": "displayName", + "TopicName": "topicName" + } + }, + "MyTopicTokenSubscription141DD1BE2": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref" : "my-email-1" + }, + "Protocol": "email", + "TopicArn": { + "Ref": "MyTopic86869434" + } + } + }, + "MyTopicTokenSubscription293BFE3F9": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref" : "my-email-2" + }, + "Protocol": "email", + "TopicArn": { + "Ref": "MyTopic86869434" + } + } + }, + "MyTopicTokenSubscription335C2B4CA": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref" : "my-email-3" + }, + "Protocol": "email", + "TopicArn": { + "Ref": "MyTopic86869434" + } + } + }, + "MyTopicTokenSubscription4DBE52A3F": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { + "Ref" : "my-email-4" + }, + "Protocol": "email", + "TopicArn": { + "Ref": "MyTopic86869434" + } + } + } + } + }); +}); + test('multiple subscriptions', () => { const queue = new sqs.Queue(stack, 'MyQueue'); const func = new lambda.Function(stack, 'MyFunc', { diff --git a/packages/@aws-cdk/aws-sns/lib/topic-base.ts b/packages/@aws-cdk/aws-sns/lib/topic-base.ts index 44e2bc9dad9dc..ff025cb09072f 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic-base.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic-base.ts @@ -1,5 +1,5 @@ import * as iam from '@aws-cdk/aws-iam'; -import { IResource, Resource } from '@aws-cdk/core'; +import { Construct, IResource, Resource, Token } from '@aws-cdk/core'; import { TopicPolicy } from './policy'; import { ITopicSubscription } from './subscriber'; import { Subscription } from './subscription'; @@ -59,7 +59,10 @@ export abstract class TopicBase extends Resource implements ITopic { const subscriptionConfig = subscription.bind(this); const scope = subscriptionConfig.subscriberScope || this; - const id = subscriptionConfig.subscriberId; + let id = subscriptionConfig.subscriberId; + if (Token.isUnresolved(subscriptionConfig.subscriberId)) { + id = this.nextTokenId(scope); + } // We use the subscriber's id as the construct id. There's no meaning // to subscribing the same subscriber twice on the same topic. @@ -102,4 +105,21 @@ export abstract class TopicBase extends Resource implements ITopic { }); } + private nextTokenId(scope: Construct) { + let nextSuffix = 1; + const re = /TokenSubscription:([\d]*)/gm; + // Search through the construct and all of its children + // for previous subscriptions that match our regex pattern + for (const source of scope.node.findAll()) { + const m = re.exec(source.node.id); // Use regex to find a match + if (m !== null) { // if we found a match + const matchSuffix = parseInt(m[1], 10); // get the suffix for that match (as integer) + if (matchSuffix >= nextSuffix) { // check if the match suffix is larger or equal to currently proposed suffix + nextSuffix = matchSuffix + 1; // increment the suffix + } + } + } + return `TokenSubscription:${nextSuffix}`; + } + }