Skip to content

Commit c961a6f

Browse files
feat: fixed and made notifications work
1 parent 536270c commit c961a6f

File tree

2 files changed

+303
-117
lines changed

2 files changed

+303
-117
lines changed

lib/deploy/stepFunctions/compileNotifications.js

Lines changed: 181 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,23 @@ function randomTargetId(stateMachineName, status) {
3333
return `${stateMachineName}-${status}-${suffix}`;
3434
}
3535

36-
function compileTarget(stateMachineName, status, targetObj) {
36+
function randomLogicalId(prefix) {
37+
const suffix = chance.string({
38+
length: 5,
39+
pool: 'ABCDEFGHIJKLMNOPQRSTUFWXYZ',
40+
});
41+
return `${prefix}${suffix}`;
42+
}
43+
44+
function randomPolicyName(status, targetType) {
45+
const suffix = chance.string({
46+
length: 5,
47+
pool: 'abcdefghijklmnopqrstufwxyzABCDEFGHIJKLMNOPQRSTUFWXYZ',
48+
});
49+
return `${status}-${targetType}-${suffix}`;
50+
}
51+
52+
function compileTarget(stateMachineName, status, targetObj, iamRoleLogicalId) {
3753
// SQS and Kinesis are special cases as they can have additional props
3854
if (_.has(targetObj, 'sqs.arn')) {
3955
return {
@@ -51,7 +67,16 @@ function compileTarget(stateMachineName, status, targetObj) {
5167
PartitionKeyPath: targetObj.kinesis.partitionKeyPath,
5268
},
5369
};
70+
} else if (_.has(targetObj, 'stepFunctions')) {
71+
return {
72+
Arn: targetObj.stepFunctions,
73+
Id: randomTargetId(stateMachineName, status),
74+
RoleArn: {
75+
'Fn::GetAtt': [iamRoleLogicalId, 'Arn'],
76+
},
77+
};
5478
}
79+
5580
const targetType = supportedTargets.find(t => _.has(targetObj, t));
5681
const arn = _.get(targetObj, targetType);
5782
return {
@@ -60,24 +85,110 @@ function compileTarget(stateMachineName, status, targetObj) {
6085
};
6186
}
6287

63-
function compileIamPermission(targetObj) {
64-
const targetType = supportedTargets.find(t => _.has(targetObj, t));
65-
const action = targetPermissions[targetType];
88+
function compileSnsPolicy(status, snsTarget) {
89+
return {
90+
Type: 'AWS::SNS::TopicPolicy',
91+
Properties: {
92+
PolicyDocument: {
93+
Version: '2012-10-17',
94+
Statement: {
95+
Sid: randomPolicyName(status, 'sns'),
96+
Principal: {
97+
Service: 'events.amazonaws.com',
98+
},
99+
Effect: 'Allow',
100+
Action: 'sns:Publish',
101+
Resource: snsTarget,
102+
},
103+
},
104+
Topics: [snsTarget],
105+
},
106+
};
107+
}
66108

67-
// SQS and Kinesis are special cases as they can have additional props
68-
if (_.has(targetObj, 'sqs.arn')) {
109+
function convertToQueueUrl(sqsArn) {
110+
if (_.isString(sqsArn)) {
111+
const segments = sqsArn.split(':');
112+
const queueName = _.last(segments);
69113
return {
70-
action,
71-
resource: _.get(targetObj, 'sqs.arn'),
114+
'Fn::Sub': [
115+
'https://sqs.${AWS::Region}.amazonaws.com/${AWS::AccountId}/${QueueName}',
116+
{ QueueName: queueName },
117+
],
72118
};
73-
} else if (_.has(targetObj, 'kinesis.arn')) {
119+
} else if (sqsArn['Fn::GetAtt']) {
120+
const logicalId = sqsArn['Fn::GetAtt'][0];
121+
return { Ref: logicalId };
122+
}
123+
throw new Error(
124+
`Unable to convert SQS ARN [${sqsArn}] to SQS Url. ` +
125+
'This is required for setting up Step Functions notifications to SQS. ' +
126+
'Try using Fn::GetAtt when setting the SQS arn.');
127+
}
128+
129+
function compileSqsPolicy(status, sqsTarget) {
130+
return {
131+
Type: 'AWS::SQS::QueuePolicy',
132+
Properties: {
133+
PolicyDocument: {
134+
Version: '2012-10-17',
135+
Statement: {
136+
Sid: randomPolicyName(status, 'sqs'),
137+
Principal: {
138+
Service: 'events.amazonaws.com',
139+
},
140+
Effect: 'Allow',
141+
Action: 'sqs:SendMessage',
142+
Resource: sqsTarget,
143+
},
144+
},
145+
Queues: [convertToQueueUrl(sqsTarget)],
146+
},
147+
};
148+
}
149+
150+
function compileLambdaPermission(lambdaTarget) {
151+
return {
152+
Type: 'AWS::Lambda::Permission',
153+
Properties: {
154+
Action: 'lambda:InvokeFunction',
155+
FunctionName: lambdaTarget,
156+
Principal: 'events.amazonaws.com',
157+
},
158+
};
159+
}
160+
161+
function compilePermissionForTarget(status, targetObj) {
162+
if (targetObj.sns) {
163+
return {
164+
type: 'policy',
165+
resource: compileSnsPolicy(status, targetObj.sns),
166+
};
167+
} else if (targetObj.sqs) {
168+
const arn = _.get(targetObj, 'sqs.arn', targetObj.sqs);
169+
return {
170+
type: 'policy',
171+
resource: compileSqsPolicy(status, arn),
172+
};
173+
} else if (targetObj.kinesis) {
174+
const arn = _.get(targetObj, 'kinesis.arn', targetObj.kinesis);
175+
return {
176+
type: 'iam',
177+
action: 'kinesis:PutRecord',
178+
resource: arn,
179+
};
180+
} else if (targetObj.lambda) {
74181
return {
75-
action,
76-
resource: _.get(targetObj, 'kinesis.arn'),
182+
type: 'policy',
183+
resource: compileLambdaPermission(targetObj.lambda),
77184
};
78185
}
79186

187+
const targetType = supportedTargets.find(t => _.has(targetObj, t));
188+
const action = targetPermissions[targetType];
189+
80190
return {
191+
type: 'iam',
81192
action,
82193
resource: targetObj[targetType],
83194
};
@@ -96,42 +207,74 @@ function bootstrapIamRole() {
96207
},
97208
},
98209
},
99-
Policies: [
100-
{
101-
PolicyName: 'root',
102-
PolicyDocument: {
103-
Version: '2012-10-17',
104-
Statement: [],
105-
},
106-
},
107-
],
210+
Policies: [],
108211
},
109212
};
110-
const addPermission = (action, resource) => {
111-
iamRole.Properties.Policies[0].PolicyDocument.Statement.push({
112-
Effect: 'Allow',
113-
Action: action,
114-
Resource: resource,
213+
const addPolicy = (name, action, resource) => {
214+
iamRole.Properties.Policies.push({
215+
PolicyName: name,
216+
PolicyDocument: {
217+
Version: '2012-10-17',
218+
Statement: [{
219+
Effect: 'Allow',
220+
Action: action,
221+
Resource: resource,
222+
}],
223+
},
115224
});
116225
};
117226

118-
return { iamRole, addPermission };
227+
return { iamRole, addPolicy };
228+
}
229+
230+
function* compilePermissionResources(stateMachineLogicalId, iamRoleLogicalId, targets) {
231+
const { iamRole, addPolicy } = bootstrapIamRole();
232+
233+
for (const { status, target } of targets) {
234+
const perm = compilePermissionForTarget(status, target);
235+
if (perm.type === 'iam') {
236+
const targetType = _.keys(target)[0];
237+
addPolicy(
238+
randomPolicyName(status, targetType),
239+
perm.action,
240+
perm.resource);
241+
} else if (perm.type === 'policy') {
242+
yield {
243+
logicalId: randomLogicalId(`${stateMachineLogicalId}ResourcePolicy`),
244+
resource: perm.resource,
245+
};
246+
}
247+
}
248+
249+
if (!_.isEmpty(iamRole.Properties.Policies)) {
250+
yield {
251+
logicalId: iamRoleLogicalId,
252+
resource: iamRole,
253+
};
254+
}
119255
}
120256

121257
function* compileResources(stateMachineLogicalId, stateMachineName, notificationsObj) {
122258
const iamRoleLogicalId = `${stateMachineLogicalId}NotificationsIamRole`;
123-
const { iamRole, addPermission } = bootstrapIamRole();
259+
const allTargets = _.flatMap(executionStatuses, status =>
260+
_.get(notificationsObj, status, []).map(target => ({ status, target })));
261+
const permissions = compilePermissionResources(
262+
stateMachineLogicalId, iamRoleLogicalId, allTargets);
263+
const permissionResources = Array.from(permissions);
264+
for (const { logicalId, resource } of permissionResources) {
265+
yield [logicalId, resource];
266+
}
267+
268+
const needRoleArn = permissionResources.some(({ logicalId }) => logicalId === iamRoleLogicalId);
269+
const roleArn = needRoleArn
270+
? { 'Fn::GetAtt': [iamRoleLogicalId, 'Arn'] }
271+
: undefined;
124272

125273
for (const status of executionStatuses) {
126274
const targets = notificationsObj[status];
127275
if (!_.isEmpty(targets)) {
128-
const cfnTargets = targets
129-
.map(t => compileTarget(stateMachineName, status, t))
130-
.filter(_.isObjectLike);
131-
targets
132-
.map(compileIamPermission)
133-
.filter(_.isObjectLike)
134-
.forEach(({ action, resource }) => addPermission(action, resource));
276+
const cfnTargets = targets.map(t =>
277+
compileTarget(stateMachineName, status, t, iamRoleLogicalId));
135278

136279
const eventRuleLogicalId =
137280
`${stateMachineLogicalId}Notifications${status.replace('_', '')}EventRule`;
@@ -144,22 +287,19 @@ function* compileResources(stateMachineLogicalId, stateMachineName, notification
144287
'detail-type': ['Step Functions Execution Status Change'],
145288
detail: {
146289
status: [status],
290+
stateMachineArn: [{
291+
Ref: stateMachineLogicalId,
292+
}],
147293
},
148294
},
149295
Name: `${stateMachineName}-${status}-notification`,
150-
RoleArn: {
151-
'Fn::GetAtt': [iamRoleLogicalId, 'Arn'],
152-
},
296+
RoleArn: roleArn,
153297
Targets: cfnTargets,
154298
},
155299
};
156300
yield [eventRuleLogicalId, eventRule];
157301
}
158302
}
159-
160-
if (!_.isEmpty(iamRole.Properties.Policies[0].PolicyDocument.Statement)) {
161-
yield [iamRoleLogicalId, iamRole];
162-
}
163303
}
164304

165305
function validateConfig(serverless, stateMachineName, notificationsObj) {

0 commit comments

Comments
 (0)