Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sns): support multiple tokens as url and email subscriptions #6357

Merged
merged 11 commits into from
Feb 24, 2020
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('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.
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}`;
}

}