Skip to content

Commit b3d64f9

Browse files
Merge pull request #304 from purple-technology/master
Generating random variable names for intrinsic functions breaks Serverless caching
2 parents 938d011 + b9f7854 commit b3d64f9

File tree

6 files changed

+158
-50
lines changed

6 files changed

+158
-50
lines changed

lib/deploy/stepFunctions/compileNotifications.js

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
const _ = require('lodash');
44
const Joi = require('@hapi/joi');
5-
const Chance = require('chance');
5+
const crypto = require('crypto');
66
const BbPromise = require('bluebird');
77
const schema = require('./compileNotifications.schema');
88

9-
const chance = new Chance();
10-
119
const executionStatuses = [
1210
'ABORTED', 'FAILED', 'RUNNING', 'SUCCEEDED', 'TIMED_OUT',
1311
];
@@ -25,65 +23,72 @@ const targetPermissions = {
2523
stepFunctions: 'states:StartExecution',
2624
};
2725

28-
function randomTargetId(stateMachineName, status) {
29-
const suffix = chance.string({
30-
length: 5,
31-
pool: 'abcdefghijklmnopqrstufwxyzABCDEFGHIJKLMNOPQRSTUFWXYZ1234567890',
32-
});
26+
function generateTargetId(target, index, stateMachineName, status) {
27+
const suffix = crypto
28+
.createHash('md5')
29+
.update(JSON.stringify({ target, index }))
30+
.digest('hex')
31+
.substr(0, 5);
3332

3433
return `${stateMachineName}-${status}-${suffix}`;
3534
}
3635

37-
function randomLogicalId(prefix) {
38-
const suffix = chance.string({
39-
length: 5,
40-
pool: 'ABCDEFGHIJKLMNOPQRSTUFWXYZ',
41-
});
36+
function generateLogicalId(prefix, index, resource) {
37+
const suffix = crypto
38+
.createHash('md5')
39+
.update(JSON.stringify({ index, resource }))
40+
.digest('hex')
41+
.substr(0, 5);
4242
return `${prefix}${suffix}`;
4343
}
4444

45-
function randomPolicyName(status, targetType) {
46-
const suffix = chance.string({
47-
length: 5,
48-
pool: 'abcdefghijklmnopqrstufwxyzABCDEFGHIJKLMNOPQRSTUFWXYZ',
49-
});
45+
function generatePolicyName(status, targetType, action, resource) {
46+
const suffix = crypto
47+
.createHash('md5')
48+
.update(JSON.stringify({ action, resource }))
49+
.digest('hex')
50+
.substr(0, 5);
5051
return `${status}-${targetType}-${suffix}`;
5152
}
5253

53-
function compileTarget(stateMachineName, status, targetObj, iamRoleLogicalId) {
54+
function compileTarget(stateMachineName, status, targetObj, targetIndex, iamRoleLogicalId) {
5455
// SQS and Kinesis are special cases as they can have additional props
5556
if (_.has(targetObj, 'sqs.arn')) {
56-
return {
57+
const target = {
5758
Arn: targetObj.sqs.arn,
58-
Id: randomTargetId(stateMachineName, status),
5959
SqsParameters: {
6060
MessageGroupId: targetObj.sqs.messageGroupId,
6161
},
6262
};
63+
target.Id = generateTargetId(target, targetIndex, stateMachineName, status);
64+
return target;
6365
} if (_.has(targetObj, 'kinesis.arn')) {
64-
return {
66+
const target = {
6567
Arn: targetObj.kinesis.arn,
66-
Id: randomTargetId(stateMachineName, status),
6768
KinesisParameters: {
6869
PartitionKeyPath: targetObj.kinesis.partitionKeyPath,
6970
},
7071
};
72+
target.Id = generateTargetId(target, targetIndex, stateMachineName, status);
73+
return target;
7174
} if (_.has(targetObj, 'stepFunctions')) {
72-
return {
75+
const target = {
7376
Arn: targetObj.stepFunctions,
74-
Id: randomTargetId(stateMachineName, status),
7577
RoleArn: {
7678
'Fn::GetAtt': [iamRoleLogicalId, 'Arn'],
7779
},
7880
};
81+
target.Id = generateTargetId(target, targetIndex, stateMachineName, status);
82+
return target;
7983
}
8084

8185
const targetType = supportedTargets.find(t => _.has(targetObj, t));
8286
const arn = _.get(targetObj, targetType);
83-
return {
87+
const target = {
8488
Arn: arn,
85-
Id: randomTargetId(stateMachineName, status),
8689
};
90+
target.Id = generateTargetId(target, targetIndex, stateMachineName, status);
91+
return target;
8792
}
8893

8994
function compileSnsPolicy(status, snsTarget) {
@@ -93,7 +98,7 @@ function compileSnsPolicy(status, snsTarget) {
9398
PolicyDocument: {
9499
Version: '2012-10-17',
95100
Statement: {
96-
Sid: randomPolicyName(status, 'sns'),
101+
Sid: generatePolicyName(status, 'sns', 'sns:Publish', snsTarget),
97102
Principal: {
98103
Service: 'events.amazonaws.com',
99104
},
@@ -135,7 +140,7 @@ function compileSqsPolicy(status, sqsTarget) {
135140
PolicyDocument: {
136141
Version: '2012-10-17',
137142
Statement: {
138-
Sid: randomPolicyName(status, 'sqs'),
143+
Sid: generatePolicyName(status, 'sqs', 'sqs:SendMessage', sqsTarget),
139144
Principal: {
140145
Service: 'events.amazonaws.com',
141146
},
@@ -232,18 +237,19 @@ function bootstrapIamRole() {
232237
function* compilePermissionResources(stateMachineLogicalId, iamRoleLogicalId, targets) {
233238
const { iamRole, addPolicy } = bootstrapIamRole();
234239

235-
for (const { status, target } of targets) {
240+
for (let index = 0; index < targets.length; index++) {
241+
const { status, target } = targets[index];
236242
const perm = compilePermissionForTarget(status, target);
237243
if (perm.type === 'iam') {
238244
const targetType = _.keys(target)[0];
239245
addPolicy(
240-
randomPolicyName(status, targetType),
246+
generatePolicyName(status, targetType, perm.action, perm.resource),
241247
perm.action,
242248
perm.resource,
243249
);
244250
} else if (perm.type === 'policy') {
245251
yield {
246-
logicalId: randomLogicalId(`${stateMachineLogicalId}ResourcePolicy`),
252+
logicalId: generateLogicalId(`${stateMachineLogicalId}ResourcePolicy`, index, perm.resource),
247253
resource: perm.resource,
248254
};
249255
}
@@ -277,8 +283,8 @@ function* compileResources(stateMachineLogicalId, stateMachineName, notification
277283
for (const status of executionStatuses) {
278284
const targets = notificationsObj[status];
279285
if (!_.isEmpty(targets)) {
280-
const cfnTargets = targets.map(t => compileTarget(stateMachineName,
281-
status, t, iamRoleLogicalId));
286+
const cfnTargets = targets.map((t, index) => compileTarget(stateMachineName,
287+
status, t, index, iamRoleLogicalId));
282288

283289
const eventRuleLogicalId = `${stateMachineLogicalId}Notifications${status.replace('_', '')}EventRule`;
284290
const eventRule = {

lib/deploy/stepFunctions/compileNotifications.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,55 @@ describe('#compileNotifications', () => {
259259
expect(consoleLogSpy.callCount).equal(0);
260260
});
261261

262+
it('should do deterministic compilation of CloudWatch Event Rules', () => {
263+
const snsArn = { Ref: 'MyTopic' };
264+
const sqsArn = { 'Fn::GetAtt': ['MyQueue', 'Arn'] };
265+
const sqsNestedArn = { 'Fn::GetAtt': ['MyNestedQueue', 'Arn'] };
266+
const lambdaArn = { 'Fn::GetAtt': ['MyFunction', 'Arn'] };
267+
const kinesisArn = { 'Fn::GetAtt': ['MyStream', 'Arn'] };
268+
const kinesisNestedArn = { 'Fn::GetAtt': ['MyNestedStream', 'Arn'] };
269+
const firehoseArn = { 'Fn::GetAtt': ['MyDeliveryStream', 'Arn'] };
270+
const stepFunctionsArn = { Ref: 'MyStateMachine' };
271+
const targets = [
272+
{ sns: snsArn },
273+
{ sqs: sqsArn },
274+
{ sqs: { arn: sqsNestedArn, messageGroupId: '12345' } },
275+
{ lambda: lambdaArn },
276+
{ kinesis: kinesisArn },
277+
{ kinesis: { arn: kinesisNestedArn, partitionKeyPath: '$.id' } },
278+
{ firehose: firehoseArn },
279+
{ stepFunctions: stepFunctionsArn },
280+
{ sns: 'SNS_TOPIC_ARN' },
281+
{ sqs: 'SQS_QUEUE_ARN' },
282+
{ sqs: 'arn:aws:sqs:#{AWS::Region}:#{AWS::AccountId}:MyQueue' },
283+
{ sqs: { arn: 'SQS_QUEUE_NESTED_ARN', messageGroupId: '12345' } },
284+
{ lambda: 'LAMBDA_FUNCTION_ARN' },
285+
{ kinesis: 'KINESIS_STREAM_ARN' },
286+
{ kinesis: { arn: 'KINESIS_STREAM_NESTED_ARN', partitionKeyPath: '$.id' } },
287+
{ firehose: 'FIREHOSE_STREAM_ARN' },
288+
{ stepFunctions: 'STATE_MACHINE_ARN' },
289+
];
290+
291+
const definition = {
292+
stateMachines: {
293+
beta1: genStateMachineWithTargets('Beta1', targets),
294+
beta2: genStateMachineWithTargets('Beta2', targets),
295+
},
296+
};
297+
298+
serverless.service.stepFunctions = _.cloneDeep(definition);
299+
serverlessStepFunctions.compileNotifications();
300+
const resources1 = _.cloneDeep(serverlessStepFunctions.serverless.service
301+
.provider.compiledCloudFormationTemplate.Resources);
302+
303+
serverless.service.stepFunctions = _.cloneDeep(definition);
304+
serverlessStepFunctions.compileNotifications();
305+
const resources2 = _.cloneDeep(serverlessStepFunctions.serverless.service
306+
.provider.compiledCloudFormationTemplate.Resources);
307+
308+
expect(resources1).to.deep.equal(resources2);
309+
});
310+
262311
it('should not generate resources when no notifications are defined', () => {
263312
const genStateMachine = name => ({
264313
name,

lib/deploy/stepFunctions/compileStateMachines.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@
33
const _ = require('lodash');
44
const Joi = require('@hapi/joi');
55
const aslValidator = require('asl-validator');
6-
const Chance = require('chance');
76
const BbPromise = require('bluebird');
7+
const crypto = require('crypto');
88
const schema = require('./compileStateMachines.schema');
99
const { isIntrinsic, translateLocalFunctionNames, convertToFunctionVersion } = require('../../utils/aws');
1010

11-
const chance = new Chance();
12-
13-
function randomName() {
14-
return chance.string({
15-
length: 10,
16-
pool: 'abcdefghijklmnopqrstufwxyzABCDEFGHIJKLMNOPQRSTUFWXYZ1234567890',
17-
});
11+
function generateSubVariableName(element) {
12+
return crypto
13+
.createHash('md5')
14+
.update(JSON.stringify(element))
15+
.digest('hex');
1816
}
1917

2018
function toTags(obj) {
@@ -44,7 +42,7 @@ function* getIntrinsicFunctions(obj) {
4442
for (const idx in value) {
4543
const element = value[idx];
4644
if (isIntrinsic(element)) {
47-
const paramName = randomName();
45+
const paramName = generateSubVariableName(element);
4846
value[idx] = `\${${paramName}}`;
4947
yield [paramName, element];
5048
} else {
@@ -55,7 +53,7 @@ function* getIntrinsicFunctions(obj) {
5553
}
5654
}
5755
} else if (isIntrinsic(value)) {
58-
const paramName = randomName();
56+
const paramName = generateSubVariableName(value);
5957
// eslint-disable-next-line no-param-reassign
6058
obj[key] = `\${${paramName}}`;
6159
yield [paramName, value];

lib/deploy/stepFunctions/compileStateMachines.test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,67 @@ describe('#compileStateMachines', () => {
760760
});
761761
});
762762

763+
it('should do deterministic compilcation', () => {
764+
const definition = {
765+
stateMachines: {
766+
myStateMachine: {
767+
name: 'stateMachine',
768+
definition: {
769+
StartAt: 'LambdaA',
770+
States: {
771+
LambdaA: {
772+
Type: 'Task',
773+
Resource: {
774+
Ref: 'MyFunction',
775+
},
776+
Next: 'LambdaB',
777+
},
778+
LambdaB: {
779+
Type: 'Task',
780+
Resource: {
781+
Ref: 'MyFunction2',
782+
},
783+
Next: 'Parallel',
784+
},
785+
Parallel: {
786+
Type: 'Parallel',
787+
End: true,
788+
Branches: [
789+
{
790+
StartAt: 'Lambda2',
791+
States: {
792+
Lambda2: {
793+
Type: 'Task',
794+
Resource: {
795+
Ref: 'MyFunction',
796+
},
797+
End: true,
798+
},
799+
},
800+
},
801+
],
802+
},
803+
},
804+
},
805+
},
806+
},
807+
};
808+
809+
serverless.service.stepFunctions = _.cloneDeep(definition);
810+
serverlessStepFunctions.compileStateMachines();
811+
const stateMachine1 = _.cloneDeep(serverlessStepFunctions.serverless.service
812+
.provider.compiledCloudFormationTemplate.Resources
813+
.StateMachine);
814+
815+
serverless.service.stepFunctions = _.cloneDeep(definition);
816+
serverlessStepFunctions.compileStateMachines();
817+
const stateMachine2 = _.cloneDeep(serverlessStepFunctions.serverless.service
818+
.provider.compiledCloudFormationTemplate.Resources
819+
.StateMachine);
820+
821+
expect(stateMachine1).to.deep.equal(stateMachine2);
822+
});
823+
763824
it('should allow null values #193', () => {
764825
serverless.service.stepFunctions = {
765826
stateMachines: {

package-lock.json

Lines changed: 0 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
"aws-sdk": "^2.282.1",
4848
"bluebird": "^3.4.0",
4949
"chalk": "^1.1.1",
50-
"chance": "^1.0.18",
5150
"lodash": "^4.17.11",
5251
"serverless": "^1.46.1"
5352
},

0 commit comments

Comments
 (0)