Skip to content

Commit 6c0128e

Browse files
feat: intrinsic func in compile sm
1 parent 2b006d6 commit 6c0128e

File tree

2 files changed

+130
-3
lines changed

2 files changed

+130
-3
lines changed

lib/deploy/stepFunctions/compileStateMachines.js

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
const _ = require('lodash');
33
const BbPromise = require('bluebird');
44

5+
function randomName() {
6+
const chars = 'abcdefghijklmnopqrstufwxyzABCDEFGHIJKLMNOPQRSTUFWXYZ1234567890';
7+
const pwd = _.sampleSize(chars, 10);
8+
return pwd.join('');
9+
}
10+
511
function isIntrinsic(obj) {
6-
const isObject = typeof obj === 'object';
7-
return isObject && Object.keys(obj).some((k) => k.startsWith('Fn::') || k.startsWith('Ref'));
12+
return typeof obj === 'object' &&
13+
Object.keys(obj).some((k) => k.startsWith('Fn::') || k.startsWith('Ref'));
814
}
915

1016
function toTags(obj, serverless) {
@@ -26,6 +32,39 @@ function toTags(obj, serverless) {
2632
return tags;
2733
}
2834

35+
// return an iterable of
36+
// [ ParamName, IntrinsicFunction ]
37+
// e.g. [ 'mptFnX05Fb', { Ref: 'MyTopic' } ]
38+
// this makes it easy to use _.fromPairs to construct an object afterwards
39+
function* getIntrinsicFunctions(obj) {
40+
// eslint-disable-next-line no-restricted-syntax
41+
for (const key in obj) {
42+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
43+
const value = obj[key];
44+
45+
if (Array.isArray(value)) {
46+
// eslint-disable-next-line guard-for-in, no-restricted-syntax
47+
for (const idx in value) {
48+
const innerFuncs = Array.from(getIntrinsicFunctions(value[idx]));
49+
for (const x of innerFuncs) {
50+
yield x;
51+
}
52+
}
53+
} else if (isIntrinsic(value)) {
54+
const paramName = randomName();
55+
// eslint-disable-next-line no-param-reassign
56+
obj[key] = `\${${paramName}}`;
57+
yield [paramName, value];
58+
} else if (typeof value === 'object') {
59+
const innerFuncs = Array.from(getIntrinsicFunctions(value));
60+
for (const x of innerFuncs) {
61+
yield x;
62+
}
63+
}
64+
}
65+
}
66+
}
67+
2968
module.exports = {
3069
isIntrinsic,
3170
compileStateMachines() {
@@ -42,7 +81,17 @@ module.exports = {
4281
DefinitionString = JSON.stringify(stateMachineObj.definition)
4382
.replace(/\\n|\\r|\\n\\r/g, '');
4483
} else {
45-
DefinitionString = JSON.stringify(stateMachineObj.definition, undefined, 2);
84+
const functionMappings = Array.from(getIntrinsicFunctions(stateMachineObj.definition));
85+
if (_.isEmpty(functionMappings)) {
86+
DefinitionString = JSON.stringify(stateMachineObj.definition, undefined, 2);
87+
} else {
88+
DefinitionString = {
89+
'Fn::Sub': [
90+
JSON.stringify(stateMachineObj.definition, undefined, 2),
91+
_.fromPairs(functionMappings),
92+
],
93+
};
94+
}
4695
}
4796
} else {
4897
const errorMessage = [

lib/deploy/stepFunctions/compileStateMachines.test.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,4 +607,82 @@ describe('#compileStateMachines', () => {
607607

608608
expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error);
609609
});
610+
611+
it('should respect CloudFormation intrinsic functions for Resource', () => {
612+
serverless.service.stepFunctions = {
613+
stateMachines: {
614+
myStateMachine: {
615+
name: 'stateMachine',
616+
definition: {
617+
StartAt: 'Lambda',
618+
States: {
619+
Lambda: {
620+
Type: 'Task',
621+
Resource: {
622+
Ref: 'MyFunction',
623+
},
624+
Next: 'Sns',
625+
},
626+
Sns: {
627+
Type: 'Task',
628+
Resource: 'arn:aws:states:::sns:publish',
629+
Parameters: {
630+
Message: {
631+
'Fn::GetAtt': ['MyTopic', 'TopicName'],
632+
},
633+
TopicArn: {
634+
Ref: 'MyTopic',
635+
},
636+
},
637+
Next: 'Sqs',
638+
},
639+
Sqs: {
640+
Type: 'Task',
641+
Resource: 'arn:aws:states:::sqs:sendMessage',
642+
Parameters: {
643+
QueueUrl: {
644+
Ref: 'MyQueue',
645+
},
646+
MessageBody: 'This is a static message',
647+
},
648+
End: true,
649+
},
650+
},
651+
},
652+
},
653+
},
654+
};
655+
656+
serverlessStepFunctions.compileStateMachines();
657+
const stateMachine = serverlessStepFunctions.serverless.service
658+
.provider.compiledCloudFormationTemplate.Resources
659+
.StateMachine;
660+
661+
expect(stateMachine.Properties.DefinitionString).to.haveOwnProperty('Fn::Sub');
662+
expect(stateMachine.Properties.DefinitionString['Fn::Sub']).to.have.lengthOf(2);
663+
664+
const [json, params] = stateMachine.Properties.DefinitionString['Fn::Sub'];
665+
const modifiedDefinition = JSON.parse(json);
666+
667+
const lambda = modifiedDefinition.States.Lambda;
668+
expect(lambda.Resource.startsWith('${')).to.eq(true);
669+
const functionParam = lambda.Resource.replace(/[${}]/g, '');
670+
expect(params).to.haveOwnProperty(functionParam);
671+
expect(params[functionParam]).to.eql({ Ref: 'MyFunction' });
672+
673+
const sns = modifiedDefinition.States.Sns;
674+
expect(sns.Parameters.Message.startsWith('${')).to.eq(true);
675+
const topicNameParam = sns.Parameters.Message.replace(/[${}]/g, '');
676+
expect(params).to.haveOwnProperty(topicNameParam);
677+
expect(params[topicNameParam]).to.eql({ 'Fn::GetAtt': ['MyTopic', 'TopicName'] });
678+
expect(sns.Parameters.TopicArn.startsWith('${')).to.eq(true);
679+
const topicArnParam = sns.Parameters.TopicArn.replace(/[${}]/g, '');
680+
expect(params).to.haveOwnProperty(topicArnParam);
681+
expect(params[topicArnParam]).to.eql({ Ref: 'MyTopic' });
682+
683+
const sqs = modifiedDefinition.States.Sqs;
684+
expect(sqs.Parameters.QueueUrl.startsWith('${')).to.eq(true);
685+
const queueUrlParam = sqs.Parameters.QueueUrl.replace(/[${}]/g, '');
686+
expect(params[queueUrlParam]).to.eql({ Ref: 'MyQueue' });
687+
});
610688
});

0 commit comments

Comments
 (0)