Skip to content

Commit 1fd39ad

Browse files
committed
fix(export): use attribute substitution for reserved keywords
in DynamoDB feat(terraform): Add new method for subscribing SQS to multiple SNS Need to be able to update the SQS access policy to allow two ARNs; creating subscriptions with existing method results with the policy being chosen randomly since only one can be applied to a queue. Since we are already using the wrapped helper stacks, just make a new one. Alternatively just write it all manually.
1 parent 28fd6c9 commit 1fd39ad

File tree

6 files changed

+705
-25
lines changed

6 files changed

+705
-25
lines changed

infrastructure/account-data-deleter/src/main.ts

+23-24
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { provider as localProvider } from '@cdktf/provider-local';
2121
import { provider as nullProvider } from '@cdktf/provider-null';
2222
import {
2323
ApplicationSQSQueue,
24-
ApplicationSqsSnsTopicSubscription,
24+
ApplicationSqsSnsTopicsSubscription,
2525
PocketVPC,
2626
ApplicationDynamoDBTable,
2727
ApplicationDynamoDBTableCapacityMode,
@@ -135,33 +135,32 @@ class AccountDataDeleter extends TerraformStack {
135135
},
136136
);
137137

138-
new ApplicationSqsSnsTopicSubscription(
138+
new ApplicationSqsSnsTopicsSubscription(
139139
this,
140140
'list-events-sns-subscription',
141141
{
142-
name: `${config.envVars.exportRequestQueueName}-SNS`,
143-
snsTopicArn: `arn:aws:sns:${pocketVpc.region}:${pocketVpc.accountId}:${config.lambda.snsTopicName.listEvents}`,
142+
name: `${config.envVars.exportRequestQueueName}-subs`,
144143
sqsQueue: exportRequestQueue.sqsQueue,
145-
filterPolicyScope: 'MessageBody',
146-
filterPolicy: JSON.stringify({
147-
'detail-type': ['list-export-requested'],
148-
}),
149-
tags: config.tags,
150-
},
151-
);
152-
153-
// Forward status updates on export components (shareable list, list, annotations)
154-
new ApplicationSqsSnsTopicSubscription(
155-
this,
156-
'export-status-events-sns-subscription',
157-
{
158-
name: `${config.envVars.exportRequestQueueName}-Status-SNS`,
159-
snsTopicArn: `arn:aws:sns:${pocketVpc.region}:${pocketVpc.accountId}:${config.lambda.snsTopicName.exportUpdateEvents}`,
160-
sqsQueue: exportRequestQueue.sqsQueue,
161-
filterPolicyScope: 'MessageBody',
162-
filterPolicy: JSON.stringify({
163-
'detail-type': ['export-part-complete'],
164-
}),
144+
subscriptions: [
145+
// Forward initial export request from list topic
146+
{
147+
name: `${config.envVars.exportRequestQueueName}-SNS`,
148+
snsTopicArn: `arn:aws:sns:${pocketVpc.region}:${pocketVpc.accountId}:${config.lambda.snsTopicName.listEvents}`,
149+
filterPolicyScope: 'MessageBody',
150+
filterPolicy: JSON.stringify({
151+
'detail-type': ['list-export-requested'],
152+
}),
153+
},
154+
// Forward status updates on export components (shareable list, list, annotations)
155+
{
156+
name: `${config.envVars.exportRequestQueueName}-Status-SNS`,
157+
snsTopicArn: `arn:aws:sns:${pocketVpc.region}:${pocketVpc.accountId}:${config.lambda.snsTopicName.exportUpdateEvents}`,
158+
filterPolicyScope: 'MessageBody',
159+
filterPolicy: JSON.stringify({
160+
'detail-type': ['export-part-complete'],
161+
}),
162+
},
163+
],
165164
tags: config.tags,
166165
},
167166
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { sqsQueue } from '@cdktf/provider-aws';
2+
import { Testing } from 'cdktf';
3+
import { ApplicationSqsSnsTopicsSubscription } from './ApplicationSqsSnsTopicsSubscription.ts';
4+
5+
describe('ApplicationSqsSnsTopicSubscription', () => {
6+
const getConfig = (stack) => ({
7+
name: 'test-sns-subscription',
8+
subscriptions: [
9+
{ name: 'TopicName', snsTopicArn: 'arn:aws:sns:TopicName' },
10+
{ name: 'TopicSub2', snsTopicArn: 'arn:aws:sns:AnotherTopic' },
11+
],
12+
sqsQueue: new sqsQueue.SqsQueue(stack, 'sqs', {
13+
name: 'test-sqs',
14+
}),
15+
});
16+
17+
const getConfigWithDlq = (stack) => ({
18+
name: 'test-sns-subscription',
19+
subscriptions: [
20+
{
21+
snsTopicArn: 'arn:aws:sns:TopicName',
22+
name: 'TopicSub',
23+
snsDlq: new sqsQueue.SqsQueue(stack, 'dlq', {
24+
name: 'test-sqs-dlq',
25+
}),
26+
},
27+
],
28+
sqsQueue: new sqsQueue.SqsQueue(stack, 'sqs', {
29+
name: 'test-sqs',
30+
}),
31+
});
32+
33+
it('renders an SQS SNS subscription without tags', () => {
34+
const synthed = Testing.synthScope((stack) => {
35+
new ApplicationSqsSnsTopicsSubscription(
36+
stack,
37+
'sqs-sns-subscription',
38+
getConfig(stack),
39+
);
40+
});
41+
expect(synthed).toMatchSnapshot();
42+
});
43+
44+
it('renders an SQS SNS subscription with tags', () => {
45+
const synthed = Testing.synthScope((stack) => {
46+
new ApplicationSqsSnsTopicsSubscription(stack, 'sqs-sns-subscription', {
47+
...getConfig(stack),
48+
tags: { hello: 'there' },
49+
});
50+
});
51+
expect(synthed).toMatchSnapshot();
52+
});
53+
54+
it('renders an SQS SNS subscription with dlq passed', () => {
55+
const synthed = Testing.synthScope((stack) => {
56+
new ApplicationSqsSnsTopicsSubscription(stack, 'sqs-sns-subscription', {
57+
...getConfigWithDlq(stack),
58+
tags: { hello: 'there' },
59+
});
60+
});
61+
expect(synthed).toMatchSnapshot();
62+
});
63+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import {
2+
dataAwsIamPolicyDocument,
3+
snsTopicSubscription,
4+
sqsQueue,
5+
sqsQueuePolicy,
6+
dataAwsSqsQueue,
7+
} from '@cdktf/provider-aws';
8+
import { type SnsTopicSubscriptionConfig } from '@cdktf/provider-aws/lib/sns-topic-subscription';
9+
import { TerraformMetaArguments, TerraformResource } from 'cdktf';
10+
import { Construct } from 'constructs';
11+
12+
export interface SnsSqsSubscriptionProps {
13+
name: string;
14+
snsTopicArn: string;
15+
snsDlq?: sqsQueue.SqsQueue;
16+
filterPolicy?: SnsTopicSubscriptionConfig['filterPolicy'];
17+
filterPolicyScope?: SnsTopicSubscriptionConfig['filterPolicyScope'];
18+
}
19+
20+
export interface ApplicationSqsSnsTopicsSubscriptionProps
21+
extends TerraformMetaArguments {
22+
subscriptions: SnsSqsSubscriptionProps[];
23+
name: string;
24+
sqsQueue: sqsQueue.SqsQueue | dataAwsSqsQueue.DataAwsSqsQueue;
25+
tags?: { [key: string]: string };
26+
dependsOn?: TerraformResource[];
27+
}
28+
29+
/**
30+
* Creates an SNS to SQS subscription, allowing an SQS queue to
31+
* subscribe to multiple topics (in the rare case where this pattern
32+
* is useful)
33+
*/
34+
export class ApplicationSqsSnsTopicsSubscription extends Construct {
35+
public readonly snsTopicSubscriptions: snsTopicSubscription.SnsTopicSubscription[];
36+
37+
constructor(
38+
scope: Construct,
39+
name: string,
40+
private config: ApplicationSqsSnsTopicsSubscriptionProps,
41+
) {
42+
super(scope, name);
43+
const subscriptions = config.subscriptions.map((sub) => ({
44+
...sub,
45+
snsDlq: sub.snsDlq ?? this.createSqsSubscriptionDlq(sub.name),
46+
}));
47+
this.snsTopicSubscriptions = subscriptions.map((sub) => {
48+
return this.createSnsTopicSubscription(sub);
49+
});
50+
this.createPoliciesForSnsToSQS(subscriptions);
51+
}
52+
53+
/**
54+
* Create a dead-letter queue for failed SNS messages
55+
* @private
56+
*/
57+
private createSqsSubscriptionDlq(name: string): sqsQueue.SqsQueue {
58+
return new sqsQueue.SqsQueue(this, `${name}-sns-topic-dql`, {
59+
name: `${name}-SNS-Topic-DLQ`,
60+
tags: this.config.tags,
61+
provider: this.config.provider,
62+
});
63+
}
64+
65+
/**
66+
* Create an SNS subscription for SQS
67+
* @param snsTopicDlq
68+
* @private
69+
*/
70+
private createSnsTopicSubscription(
71+
properties: Omit<SnsSqsSubscriptionProps, 'snsDlq'> & {
72+
snsDlq: sqsQueue.SqsQueue;
73+
},
74+
): snsTopicSubscription.SnsTopicSubscription {
75+
return new snsTopicSubscription.SnsTopicSubscription(
76+
this,
77+
`${properties.name}-sns-subscription`,
78+
{
79+
topicArn: properties.snsTopicArn,
80+
protocol: 'sqs',
81+
endpoint: this.config.sqsQueue.arn,
82+
redrivePolicy: JSON.stringify({
83+
deadLetterTargetArn: properties.snsDlq.arn,
84+
}),
85+
filterPolicy: properties.filterPolicy,
86+
filterPolicyScope: properties.filterPolicyScope,
87+
dependsOn: [
88+
properties.snsDlq,
89+
...(this.config.dependsOn ? this.config.dependsOn : []),
90+
],
91+
provider: this.config.provider,
92+
} as snsTopicSubscription.SnsTopicSubscriptionConfig,
93+
);
94+
}
95+
96+
/**
97+
* Create IAM policies to allow SNS to write to the target SQS queue and a
98+
* dead-letter queue
99+
* @param snsTopicDlq
100+
* @private
101+
*/
102+
private createPoliciesForSnsToSQS(
103+
subscriptions: Array<
104+
Omit<SnsSqsSubscriptionProps, 'snsDlq'> & {
105+
snsDlq: sqsQueue.SqsQueue;
106+
}
107+
>,
108+
): void {
109+
// Make DLQ policies first since they are separate
110+
subscriptions.forEach((sub) => {
111+
const policy = new dataAwsIamPolicyDocument.DataAwsIamPolicyDocument(
112+
this,
113+
`${sub.name}-sns-dlq-policy-document`,
114+
{
115+
statement: [
116+
{
117+
effect: 'Allow',
118+
actions: ['sqs:SendMessage'],
119+
resources: [sub.snsDlq.arn],
120+
principals: [
121+
{
122+
identifiers: ['sns.amazonaws.com'],
123+
type: 'Service',
124+
},
125+
],
126+
condition: [
127+
{
128+
test: 'ArnEquals',
129+
variable: 'aws:SourceArn',
130+
values: [sub.snsTopicArn],
131+
},
132+
],
133+
},
134+
],
135+
dependsOn: [sub.snsDlq] as TerraformResource[],
136+
provider: this.config.provider,
137+
},
138+
).json;
139+
140+
return new sqsQueuePolicy.SqsQueuePolicy(
141+
this,
142+
`${sub.name}-sns-dlq-policy`,
143+
{
144+
queueUrl: sub.snsDlq.url,
145+
policy: policy,
146+
provider: this.config.provider,
147+
},
148+
);
149+
});
150+
151+
const queuePolicyDoc =
152+
new dataAwsIamPolicyDocument.DataAwsIamPolicyDocument(
153+
this,
154+
`${this.config.name}-sns-sqs-policy-document`,
155+
{
156+
statement: [
157+
{
158+
effect: 'Allow',
159+
actions: ['sqs:SendMessage'],
160+
resources: [this.config.sqsQueue.arn],
161+
principals: [
162+
{
163+
identifiers: ['sns.amazonaws.com'],
164+
type: 'Service',
165+
},
166+
],
167+
condition: [
168+
{
169+
test: 'ArnEquals',
170+
variable: 'aws:SourceArn',
171+
values: subscriptions.map((sub) => sub.snsTopicArn),
172+
},
173+
],
174+
},
175+
],
176+
dependsOn: [this.config.sqsQueue] as TerraformResource[],
177+
provider: this.config.provider,
178+
},
179+
).json;
180+
181+
new sqsQueuePolicy.SqsQueuePolicy(
182+
this,
183+
`${this.config.name}-sns-sqs-policy`,
184+
{
185+
queueUrl: this.config.sqsQueue.url,
186+
policy: queuePolicyDoc,
187+
provider: this.config.provider,
188+
},
189+
);
190+
}
191+
}

0 commit comments

Comments
 (0)