Skip to content

Commit

Permalink
feat(sns): support multiple tokens as url and email subscriptions (#6357
Browse files Browse the repository at this point in the history
)

fixes #3996 to allow using tokens in email subscriptions, additionally
fixes a bug with URL subscriptions when using more than one token
subscription.

**The Issue**
Email Subscriptions currently use the value passed in as the construct
ID, when the value passed in is a token (For example a parameter) it
causes an error as tokens aren't supported as construct IDs. A previous
fix was done for URL Subscriptions but it also errors when more than
one URL subscription with a token is used.

**The fix**
In the topic base, identify if the subscription ID is a token and
override it to a valid construct ID. The method of ensuring a valid ID
is to convert it to a special prefix suffixed by a number and doing an
increment of the number for each new topic created with a token.
Subscriptions not utilizing a token are not effected.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*

<!-- 
Please read the contribution guidelines and follow the pull-request checklist:
https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md
 -->
  • Loading branch information
strazeadin authored Feb 24, 2020
1 parent b6d4d28 commit e5493bd
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 13 deletions.
22 changes: 21 additions & 1 deletion packages/@aws-cdk/aws-sns-subscriptions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,24 @@ 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');

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:
Expand Down Expand Up @@ -82,5 +92,15 @@ import subscriptions = require('@aws-cdk/aws-sns-subscriptions');
myTopic.addSubscription(new subscriptions.EmailSubscription('[email protected]'));
```

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.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
203 changes: 194 additions & 9 deletions packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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": {
Expand All @@ -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" },
}
Expand All @@ -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/);
});

Expand Down Expand Up @@ -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', {
Expand Down
24 changes: 22 additions & 2 deletions packages/@aws-cdk/aws-sns/lib/topic-base.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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}`;
}

}

0 comments on commit e5493bd

Please sign in to comment.