Skip to content

Commit 536270c

Browse files
feat: support intrinsic funcs
1 parent 059e6cc commit 536270c

File tree

3 files changed

+181
-117
lines changed

3 files changed

+181
-117
lines changed

lib/deploy/stepFunctions/compileNotifications.js

Lines changed: 36 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ const executionStatuses = [
1111
'ABORTED', 'FAILED', 'RUNNING', 'SUCCEEDED', 'TIMED_OUT',
1212
];
1313

14+
const supportedTargets = [
15+
'sns', 'sqs', 'kinesis', 'firehose', 'lambda', 'stepFunctions',
16+
];
17+
18+
const targetPermissions = {
19+
sns: 'sns:Publish',
20+
sqs: 'sqs:SendMessage',
21+
kinesis: 'kinesis:PutRecord',
22+
firehose: 'firehose:PutRecord',
23+
lambda: 'lambda:InvokeFunction',
24+
stepFunctions: 'states:StartExecution',
25+
};
26+
1427
function randomTargetId(stateMachineName, status) {
1528
const suffix = chance.string({
1629
length: 5,
@@ -21,100 +34,53 @@ function randomTargetId(stateMachineName, status) {
2134
}
2235

2336
function compileTarget(stateMachineName, status, targetObj) {
24-
if (targetObj.sns) {
25-
return {
26-
Arn: targetObj.sns,
27-
Id: randomTargetId(stateMachineName, status),
28-
};
29-
} else if (targetObj.sqs && _.isString(targetObj.sqs)) {
30-
return {
31-
Arn: targetObj.sqs,
32-
Id: randomTargetId(stateMachineName, status),
33-
};
34-
} else if (targetObj.sqs) {
37+
// SQS and Kinesis are special cases as they can have additional props
38+
if (_.has(targetObj, 'sqs.arn')) {
3539
return {
3640
Arn: targetObj.sqs.arn,
3741
Id: randomTargetId(stateMachineName, status),
3842
SqsParameters: {
3943
MessageGroupId: targetObj.sqs.messageGroupId,
4044
},
4145
};
42-
} else if (targetObj.kinesis && _.isString(targetObj.kinesis)) {
43-
return {
44-
Arn: targetObj.kinesis,
45-
Id: randomTargetId(stateMachineName, status),
46-
};
47-
} else if (targetObj.kinesis) {
46+
} else if (_.has(targetObj, 'kinesis.arn')) {
4847
return {
4948
Arn: targetObj.kinesis.arn,
5049
Id: randomTargetId(stateMachineName, status),
5150
KinesisParameters: {
5251
PartitionKeyPath: targetObj.kinesis.partitionKeyPath,
5352
},
5453
};
55-
} else if (targetObj.firehose) {
56-
return {
57-
Arn: targetObj.firehose,
58-
Id: randomTargetId(stateMachineName, status),
59-
};
60-
} else if (targetObj.lambda) {
61-
return {
62-
Arn: targetObj.lambda,
63-
Id: randomTargetId(stateMachineName, status),
64-
};
65-
} else if (targetObj.stepFunctions) {
66-
return {
67-
Arn: targetObj.stepFunctions,
68-
Id: randomTargetId(stateMachineName, status),
69-
};
7054
}
71-
return undefined;
55+
const targetType = supportedTargets.find(t => _.has(targetObj, t));
56+
const arn = _.get(targetObj, targetType);
57+
return {
58+
Arn: arn,
59+
Id: randomTargetId(stateMachineName, status),
60+
};
7261
}
7362

7463
function compileIamPermission(targetObj) {
75-
if (targetObj.sns) {
76-
return {
77-
action: 'sns:Publish',
78-
resource: targetObj.sns,
79-
};
80-
} else if (targetObj.sqs && _.isString(targetObj.sqs)) {
81-
return {
82-
action: 'sqs:SendMessage',
83-
resource: targetObj.sqs,
84-
};
85-
} else if (targetObj.sqs) {
86-
return {
87-
action: 'sqs:SendMessage',
88-
resource: targetObj.sqs.arn,
89-
};
90-
} else if (targetObj.kinesis && _.isString(targetObj.kinesis)) {
91-
return {
92-
action: 'kinesis:PutRecord',
93-
resource: targetObj.kinesis,
94-
};
95-
} else if (targetObj.kinesis) {
96-
return {
97-
action: 'kinesis:PutRecord',
98-
resource: targetObj.kinesis.arn,
99-
};
100-
} else if (targetObj.firehose) {
101-
return {
102-
action: 'firehose:PutRecord',
103-
resource: targetObj.firehose,
104-
};
105-
} else if (targetObj.lambda) {
64+
const targetType = supportedTargets.find(t => _.has(targetObj, t));
65+
const action = targetPermissions[targetType];
66+
67+
// SQS and Kinesis are special cases as they can have additional props
68+
if (_.has(targetObj, 'sqs.arn')) {
10669
return {
107-
action: 'lambda:InvokeFunction',
108-
resource: targetObj.lambda,
70+
action,
71+
resource: _.get(targetObj, 'sqs.arn'),
10972
};
110-
} else if (targetObj.stepFunctions) {
73+
} else if (_.has(targetObj, 'kinesis.arn')) {
11174
return {
112-
action: 'states:StartExecution',
113-
resource: targetObj.stepFunctions,
75+
action,
76+
resource: _.get(targetObj, 'kinesis.arn'),
11477
};
11578
}
11679

117-
return undefined;
80+
return {
81+
action,
82+
resource: targetObj[targetType],
83+
};
11884
}
11985

12086
function bootstrapIamRole() {

lib/deploy/stepFunctions/compileNotifications.schema.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
const Joi = require('@hapi/joi');
22

3+
const arn = Joi.alternatives().try(
4+
Joi.string(),
5+
Joi.object().keys({
6+
Ref: Joi.string(),
7+
}),
8+
Joi.object().keys({
9+
'Fn::GetAtt': Joi.array().items(Joi.string()),
10+
})
11+
);
12+
313
const sqsWithParams = Joi.object().keys({
4-
arn: Joi.string().required(),
14+
arn: arn.required(),
515
messageGroupId: Joi.string().required(),
616
});
717

818
const kinesisWithParams = Joi.object().keys({
9-
arn: Joi.string().required(),
19+
arn: arn.required(),
1020
partitionKeyPath: Joi.string(),
1121
});
1222

1323
const target = Joi.object().keys({
14-
sns: Joi.string(),
15-
sqs: Joi.alternatives().try(sqsWithParams, Joi.string()),
16-
kinesis: Joi.alternatives().try(kinesisWithParams, Joi.string()),
17-
firehose: Joi.string(),
18-
lambda: Joi.string(),
19-
stepFunctions: Joi.string(),
24+
sns: arn,
25+
sqs: Joi.alternatives().try(sqsWithParams, arn),
26+
kinesis: Joi.alternatives().try(kinesisWithParams, arn),
27+
firehose: arn,
28+
lambda: arn,
29+
stepFunctions: arn,
2030
});
2131

2232
const targets = Joi.array().items(target);

lib/deploy/stepFunctions/compileNotifications.test.js

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,21 @@ describe('#compileNotifications', () => {
2727
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options);
2828
});
2929

30-
const validateCloudWatchEvent = (event, status) => {
30+
const validateCloudWatchEvent = (resources, logicalId, status) => {
31+
expect(resources).to.haveOwnProperty(logicalId);
32+
const event = resources[logicalId];
3133
expect(event.Type).to.equal('AWS::Events::Rule');
3234
expect(event.Properties.EventPattern.source).to.deep.equal(['aws.states']);
3335
expect(event.Properties.EventPattern.detail.status).to.deep.equal([status]);
3436
expect(event.Properties.Targets).to.have.lengthOf(8);
3537

3638
for (const target of event.Properties.Targets) {
37-
expect(_.isString(target.Arn)).to.equal(true);
38-
expect(typeof target.Arn).to.equal('string');
39-
expect(_.isString(target.Id)).to.equal(true);
39+
const isStringOrFn =
40+
_.isString(target.Arn) ||
41+
(target.Arn.Ref && _.isString(target.Arn.Ref)) ||
42+
(target.Arn['Fn::GetAtt'] && _.isArray(target.Arn['Fn::GetAtt']));
43+
44+
expect(isStringOrFn).to.equal(true);
4045
}
4146

4247
const sqsWithParam = event.Properties.Targets.find(t => t.SqsParameters);
@@ -47,33 +52,22 @@ describe('#compileNotifications', () => {
4752

4853
const validateHasPermission = (iamRole, action, resource) => {
4954
const statements = iamRole.Properties.Policies[0].PolicyDocument.Statement;
50-
expect(statements.find(x => x.Action === action && x.Resource === resource))
55+
expect(statements.find(x => x.Action === action && _.isEqual(x.Resource, resource)))
5156
.to.not.equal(undefined);
5257
};
5358

54-
const validateCloudWatchIamRole = (iamRole) => {
55-
// 8 targets, 5 event rules = 5 * 8 = 40 statements
56-
expect(iamRole.Properties.Policies[0].PolicyDocument.Statement).to.have.lengthOf(40);
57-
validateHasPermission(iamRole, 'sns:Publish', 'SNS_TOPIC_ARN');
58-
validateHasPermission(iamRole, 'sqs:SendMessage', 'SQS_QUEUE_ARN');
59-
validateHasPermission(iamRole, 'kinesis:PutRecord', 'KINESIS_STREAM_ARN');
60-
validateHasPermission(iamRole, 'firehose:PutRecord', 'FIREHOSE_STREAM_ARN');
61-
validateHasPermission(iamRole, 'lambda:InvokeFunction', 'LAMBDA_FUNCTION_ARN');
62-
validateHasPermission(iamRole, 'states:StartExecution', 'STATE_MACHINE_ARN');
63-
};
59+
it('should generate CloudWatch Event Rules with strig ARNs', () => {
60+
const targets = [
61+
{ sns: 'SNS_TOPIC_ARN' },
62+
{ sqs: 'SQS_QUEUE_ARN' },
63+
{ sqs: { arn: 'SQS_QUEUE_ARN', messageGroupId: '12345' } },
64+
{ lambda: 'LAMBDA_FUNCTION_ARN' },
65+
{ kinesis: 'KINESIS_STREAM_ARN' },
66+
{ kinesis: { arn: 'KINESIS_STREAM_ARN', partitionKeyPath: '$.id' } },
67+
{ firehose: 'FIREHOSE_STREAM_ARN' },
68+
{ stepFunctions: 'STATE_MACHINE_ARN' },
69+
];
6470

65-
const targets = [
66-
{ sns: 'SNS_TOPIC_ARN' },
67-
{ sqs: 'SQS_QUEUE_ARN' },
68-
{ sqs: { arn: 'SQS_QUEUE_ARN', messageGroupId: '12345' } },
69-
{ lambda: 'LAMBDA_FUNCTION_ARN' },
70-
{ kinesis: 'KINESIS_STREAM_ARN' },
71-
{ kinesis: { arn: 'KINESIS_STREAM_ARN', partitionKeyPath: '$.id' } },
72-
{ firehose: 'FIREHOSE_STREAM_ARN' },
73-
{ stepFunctions: 'STATE_MACHINE_ARN' },
74-
];
75-
76-
it('should generate CloudWatch Event Rules', () => {
7771
const genStateMachine = (name) => ({
7872
id: name,
7973
name,
@@ -105,18 +99,109 @@ describe('#compileNotifications', () => {
10599
serverlessStepFunctions.compileNotifications();
106100
const resources = serverlessStepFunctions.serverless.service
107101
.provider.compiledCloudFormationTemplate.Resources;
108-
validateCloudWatchEvent(resources.Beta1NotificationsABORTEDEventRule, 'ABORTED');
109-
validateCloudWatchEvent(resources.Beta1NotificationsFAILEDEventRule, 'FAILED');
110-
validateCloudWatchEvent(resources.Beta1NotificationsRUNNINGEventRule, 'RUNNING');
111-
validateCloudWatchEvent(resources.Beta1NotificationsSUCCEEDEDEventRule, 'SUCCEEDED');
112-
validateCloudWatchEvent(resources.Beta1NotificationsTIMEDOUTEventRule, 'TIMED_OUT');
113-
validateCloudWatchIamRole(resources.Beta1NotificationsIamRole);
114-
validateCloudWatchEvent(resources.Beta2NotificationsABORTEDEventRule, 'ABORTED');
115-
validateCloudWatchEvent(resources.Beta2NotificationsFAILEDEventRule, 'FAILED');
116-
validateCloudWatchEvent(resources.Beta2NotificationsRUNNINGEventRule, 'RUNNING');
117-
validateCloudWatchEvent(resources.Beta2NotificationsSUCCEEDEDEventRule, 'SUCCEEDED');
118-
validateCloudWatchEvent(resources.Beta2NotificationsTIMEDOUTEventRule, 'TIMED_OUT');
119-
validateCloudWatchIamRole(resources.Beta2NotificationsIamRole);
102+
103+
const validateCloudWatchEvents = (prefix) => {
104+
validateCloudWatchEvent(resources, `${prefix}NotificationsABORTEDEventRule`, 'ABORTED');
105+
validateCloudWatchEvent(resources, `${prefix}NotificationsFAILEDEventRule`, 'FAILED');
106+
validateCloudWatchEvent(resources, `${prefix}NotificationsRUNNINGEventRule`, 'RUNNING');
107+
validateCloudWatchEvent(resources, `${prefix}NotificationsSUCCEEDEDEventRule`, 'SUCCEEDED');
108+
validateCloudWatchEvent(resources, `${prefix}NotificationsTIMEDOUTEventRule`, 'TIMED_OUT');
109+
};
110+
111+
validateCloudWatchEvents('Beta1');
112+
validateCloudWatchEvents('Beta2');
113+
114+
const validateIamRole = (iamRole) => {
115+
// 8 targets, 5 event rules = 5 * 8 = 40 statements
116+
expect(iamRole.Properties.Policies[0].PolicyDocument.Statement).to.have.lengthOf(40);
117+
validateHasPermission(iamRole, 'sns:Publish', 'SNS_TOPIC_ARN');
118+
validateHasPermission(iamRole, 'sqs:SendMessage', 'SQS_QUEUE_ARN');
119+
validateHasPermission(iamRole, 'kinesis:PutRecord', 'KINESIS_STREAM_ARN');
120+
validateHasPermission(iamRole, 'firehose:PutRecord', 'FIREHOSE_STREAM_ARN');
121+
validateHasPermission(iamRole, 'lambda:InvokeFunction', 'LAMBDA_FUNCTION_ARN');
122+
validateHasPermission(iamRole, 'states:StartExecution', 'STATE_MACHINE_ARN');
123+
};
124+
125+
validateIamRole(resources.Beta1NotificationsIamRole);
126+
validateIamRole(resources.Beta2NotificationsIamRole);
127+
128+
expect(consoleLogSpy.callCount).equal(0);
129+
});
130+
131+
it('should generate CloudWatch Event Rules with Ref ang Fn::GetAtt', () => {
132+
const snsArn = { Ref: 'MyTopic' };
133+
const sqsArn = { 'Fn::GetAtt': ['MyQueue', 'Arn'] };
134+
const lambdaArn = { 'Fn::GetAtt': ['MyFunction', 'Arn'] };
135+
const kinesisArn = { 'Fn::GetAtt': ['MyStream', 'Arn'] };
136+
const firehoseArn = { 'Fn::GetAtt': ['MyDeliveryStream', 'Arn'] };
137+
const stepFunctionsArn = { Ref: 'MyStateMachine' };
138+
const targets = [
139+
{ sns: snsArn },
140+
{ sqs: sqsArn },
141+
{ sqs: { arn: sqsArn, messageGroupId: '12345' } },
142+
{ lambda: lambdaArn },
143+
{ kinesis: kinesisArn },
144+
{ kinesis: { arn: kinesisArn, partitionKeyPath: '$.id' } },
145+
{ firehose: firehoseArn },
146+
{ stepFunctions: stepFunctionsArn },
147+
];
148+
149+
const genStateMachine = (name) => ({
150+
id: name,
151+
name,
152+
definition: {
153+
StartAt: 'A',
154+
States: {
155+
A: {
156+
Type: 'Pass',
157+
End: true,
158+
},
159+
},
160+
},
161+
notifications: {
162+
ABORTED: targets,
163+
FAILED: targets,
164+
RUNNING: targets,
165+
SUCCEEDED: targets,
166+
TIMED_OUT: targets,
167+
},
168+
});
169+
170+
serverless.service.stepFunctions = {
171+
stateMachines: {
172+
beta1: genStateMachine('Beta1'),
173+
beta2: genStateMachine('Beta2'),
174+
},
175+
};
176+
177+
serverlessStepFunctions.compileNotifications();
178+
const resources = serverlessStepFunctions.serverless.service
179+
.provider.compiledCloudFormationTemplate.Resources;
180+
181+
const validateCloudWatchEvents = (prefix) => {
182+
validateCloudWatchEvent(resources, `${prefix}NotificationsABORTEDEventRule`, 'ABORTED');
183+
validateCloudWatchEvent(resources, `${prefix}NotificationsFAILEDEventRule`, 'FAILED');
184+
validateCloudWatchEvent(resources, `${prefix}NotificationsRUNNINGEventRule`, 'RUNNING');
185+
validateCloudWatchEvent(resources, `${prefix}NotificationsSUCCEEDEDEventRule`, 'SUCCEEDED');
186+
validateCloudWatchEvent(resources, `${prefix}NotificationsTIMEDOUTEventRule`, 'TIMED_OUT');
187+
};
188+
189+
validateCloudWatchEvents('Beta1');
190+
validateCloudWatchEvents('Beta2');
191+
192+
const validateIamRole = (iamRole) => {
193+
// 8 targets, 5 event rules = 5 * 8 = 40 statements
194+
expect(iamRole.Properties.Policies[0].PolicyDocument.Statement).to.have.lengthOf(40);
195+
validateHasPermission(iamRole, 'sns:Publish', snsArn);
196+
validateHasPermission(iamRole, 'sqs:SendMessage', sqsArn);
197+
validateHasPermission(iamRole, 'kinesis:PutRecord', kinesisArn);
198+
validateHasPermission(iamRole, 'firehose:PutRecord', firehoseArn);
199+
validateHasPermission(iamRole, 'lambda:InvokeFunction', lambdaArn);
200+
validateHasPermission(iamRole, 'states:StartExecution', stepFunctionsArn);
201+
};
202+
203+
validateIamRole(resources.Beta1NotificationsIamRole);
204+
validateIamRole(resources.Beta2NotificationsIamRole);
120205

121206
expect(consoleLogSpy.callCount).equal(0);
122207
});
@@ -217,6 +302,9 @@ describe('#compileNotifications', () => {
217302
});
218303

219304
it('should log the validation errors if notifications contains non-existent status', () => {
305+
const targets = [
306+
{ sns: 'SNS_TOPIC_ARN' },
307+
];
220308
const genStateMachine = (name) => ({
221309
id: name,
222310
name,

0 commit comments

Comments
 (0)