Skip to content

Commit 1f593dd

Browse files
feat: implemented intrinsic functions
Closes #178
1 parent efe9f31 commit 1f593dd

File tree

6 files changed

+302
-20
lines changed

6 files changed

+302
-20
lines changed

README.md

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ plugins:
1515
```
1616
1717
## Setup
18-
Specifies your statemachine definition using Amazon States Language in a `definition` statement in serverless.yml.
19-
We recommend to use [serverless-pseudo-parameters](https://www.npmjs.com/package/serverless-pseudo-parameters) plugin together so that it makes it easy to set up `Resource` section under `definition`.
18+
Specifies your statemachine definition using Amazon States Language in a `definition` statement in serverless.yml. You can use CloudFormation intrinsic functions such as `Ref` and `Fn::GetAtt` to reference Lambda functions, SNS topics, SQS queues and DynamoDB tables declared in the same `serverless.yml`.
19+
20+
Alternatively, you can also provide the raw ARN, or SQS queue URL, or DynamoDB table name as a string. If you need to construct the ARN by hand, then we recommend to use the [serverless-pseudo-parameters](https://www.npmjs.com/package/serverless-pseudo-parameters) plugin together to make your life easier.
2021

2122
```yml
2223
functions:
23-
hellofunc:
24+
hello:
2425
handler: handler.hello
2526
2627
stepFunctions:
@@ -45,7 +46,8 @@ stepFunctions:
4546
States:
4647
HelloWorld1:
4748
Type: Task
48-
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello
49+
Resource:
50+
Ref: HelloLambdaFunction
4951
End: true
5052
dependsOn: CustomIamRole
5153
tags:
@@ -68,7 +70,8 @@ stepFunctions:
6870
States:
6971
HelloWorld2:
7072
Type: Task
71-
Resource: arn:aws:states:#{AWS::Region}:#{AWS::AccountId}:activity:myTask
73+
Resource:
74+
Ref: HelloLambdaFunction
7275
End: true
7376
dependsOn:
7477
- DynamoDBTable
@@ -515,7 +518,8 @@ functions:
515518
States:
516519
HelloWorld1:
517520
Type: Task
518-
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello
521+
Resource:
522+
Ref: HelloLambdaFunction
519523
End: true
520524
521525
@@ -824,7 +828,8 @@ stepFunctions:
824828
States:
825829
FirstState:
826830
Type: Task
827-
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello
831+
Resource:
832+
Ref: HelloLambdaFunction
828833
Next: wait_using_seconds
829834
wait_using_seconds:
830835
Type: Wait
@@ -844,7 +849,8 @@ stepFunctions:
844849
Next: FinalState
845850
FinalState:
846851
Type: Task
847-
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello
852+
Resource:
853+
Ref: HelloLambdaFunction
848854
End: true
849855
plugins:
850856
- serverless-step-functions
@@ -866,7 +872,8 @@ stepFunctions:
866872
States:
867873
HelloWorld:
868874
Type: Task
869-
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello
875+
Resource:
876+
Ref: HelloLambdaFunction
870877
Retry:
871878
- ErrorEquals:
872879
- HandledError
@@ -946,7 +953,8 @@ stepFunctions:
946953
States:
947954
HelloWorld:
948955
Type: Task
949-
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello
956+
Resource:
957+
Ref: HelloLambdaFunction
950958
Catch:
951959
- ErrorEquals: ["HandledError"]
952960
Next: CustomErrorFallback
@@ -994,7 +1002,8 @@ stepFunctions:
9941002
States:
9951003
FirstState:
9961004
Type: Task
997-
Resource: arn:aws:lambda:${opt:region}:${self:custom.accountId}:function:${self:service}-${opt:stage}-hello1
1005+
Resource:
1006+
Ref: Hello1LambdaFunction
9981007
Next: ChoiceState
9991008
ChoiceState:
10001009
Type: Choice
@@ -1008,18 +1017,21 @@ stepFunctions:
10081017
Default: DefaultState
10091018
FirstMatchState:
10101019
Type: Task
1011-
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello2
1020+
Resource:
1021+
Ref: Hello2LambdaFunction
10121022
Next: NextState
10131023
SecondMatchState:
10141024
Type: Task
1015-
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello3
1025+
Resource:
1026+
Ref: Hello3LambdaFunction
10161027
Next: NextState
10171028
DefaultState:
10181029
Type: Fail
10191030
Cause: "No Matches!"
10201031
NextState:
10211032
Type: Task
1022-
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello4
1033+
Resource:
1034+
Ref: Hello4LambdaFunction
10231035
End: true
10241036
plugins:
10251037
- serverless-step-functions

lib/deploy/stepFunctions/compileIamRole.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const _ = require('lodash');
33
const BbPromise = require('bluebird');
44
const path = require('path');
5+
const { isIntrinsic } = require('../../utils/aws');
56

67
function getTaskStates(states) {
78
return _.flatMap(states, state => {
@@ -28,6 +29,12 @@ function sqsQueueUrlToArn(serverless, queueUrl) {
2829
const accountId = match[2];
2930
const queueName = match[3];
3031
return `arn:aws:sqs:${region}:${accountId}:${queueName}`;
32+
} else if (isIntrinsic(queueUrl) && queueUrl.Ref) {
33+
// most likely we'll see a { Ref: LogicalId }, which we need to map to
34+
// { Fn::GetAtt: [ LogicalId, Arn ] } to get the ARN
35+
return {
36+
'Fn::GetAtt': [queueUrl.Ref, 'Arn'],
37+
};
3138
}
3239
serverless.cli.consoleLog(`Unable to parse SQS queue url [${queueUrl}]`);
3340
return [];
@@ -58,6 +65,14 @@ function getSnsPermissions(serverless, state) {
5865
}
5966

6067
function getDynamoDBArn(tableName) {
68+
if (isIntrinsic(tableName) && tableName.Ref) {
69+
// most likely we'll see a { Ref: LogicalId }, which we need to map to
70+
// { Fn::GetAtt: [ LogicalId, Arn ] } to get the ARN
71+
return {
72+
'Fn::GetAtt': [tableName.Ref, 'Arn'],
73+
};
74+
}
75+
6176
return {
6277
'Fn::Join': [
6378
':',
@@ -197,7 +212,7 @@ function getIamPermissions(serverless, taskStates) {
197212
return getEcsPermissions();
198213

199214
default:
200-
if (state.Resource.startsWith('arn:aws:lambda')) {
215+
if (isIntrinsic(state.Resource) || state.Resource.startsWith('arn:aws:lambda')) {
201216
return [{
202217
action: 'lambda:InvokeFunction',
203218
resource: state.Resource,

lib/deploy/stepFunctions/compileIamRole.test.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,4 +900,103 @@ describe('#compileIamRole', () => {
900900
.Properties.Policies[0];
901901
expectDenyAllPolicy(policy);
902902
});
903+
904+
it('should respect CloudFormation intrinsic functions for Resource', () => {
905+
serverless.service.stepFunctions = {
906+
stateMachines: {
907+
myStateMachine: {
908+
name: 'stateMachine',
909+
definition: {
910+
StartAt: 'Lambda',
911+
States: {
912+
Lambda: {
913+
Type: 'Task',
914+
Resource: {
915+
Ref: 'MyFunction',
916+
},
917+
Next: 'Sns',
918+
},
919+
Sns: {
920+
Type: 'Task',
921+
Resource: 'arn:aws:states:::sns:publish',
922+
Parameters: {
923+
Message: {
924+
'Fn::GetAtt': ['MyTopic', 'TopicName'],
925+
},
926+
TopicArn: {
927+
Ref: 'MyTopic',
928+
},
929+
},
930+
Next: 'Sqs',
931+
},
932+
Sqs: {
933+
Type: 'Task',
934+
Resource: 'arn:aws:states:::sqs:sendMessage',
935+
Parameters: {
936+
QueueUrl: {
937+
Ref: 'MyQueue',
938+
},
939+
MessageBody: 'This is a static message',
940+
},
941+
Next: 'DynamoDB',
942+
},
943+
DynamoDB: {
944+
Type: 'Task',
945+
Resource: 'arn:aws:states:::dynamodb:putItem',
946+
Parameters: {
947+
TableName: {
948+
Ref: 'MyTable',
949+
},
950+
},
951+
Next: 'Parallel',
952+
},
953+
Parallel: {
954+
Type: 'Parallel',
955+
End: true,
956+
Branches: [
957+
{
958+
StartAt: 'Lambda2',
959+
States: {
960+
Lambda2: {
961+
Type: 'Task',
962+
Resource: {
963+
Ref: 'MyFunction2',
964+
},
965+
End: true,
966+
},
967+
},
968+
},
969+
],
970+
},
971+
},
972+
},
973+
},
974+
},
975+
};
976+
977+
serverlessStepFunctions.compileIamRole();
978+
serverlessStepFunctions.compileStateMachines();
979+
const policy = serverlessStepFunctions.serverless.service
980+
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
981+
.Properties.Policies[0];
982+
983+
const statements = policy.PolicyDocument.Statement;
984+
985+
const lambdaPermissions = statements.find(x => x.Action[0] === 'lambda:InvokeFunction');
986+
expect(lambdaPermissions.Resource).to.be.deep.equal([
987+
{ Ref: 'MyFunction' }, { Ref: 'MyFunction2' }]);
988+
989+
const snsPermissions = statements.find(x => x.Action[0] === 'sns:Publish');
990+
expect(snsPermissions.Resource).to.be.deep.equal([{ Ref: 'MyTopic' }]);
991+
992+
const sqsPermissions = statements.find(x => x.Action[0] === 'sqs:SendMessage');
993+
expect(sqsPermissions.Resource).to.be.deep.equal([{
994+
'Fn::GetAtt': ['MyQueue', 'Arn'],
995+
}]);
996+
997+
const dynamodbPermissions = statements.find(x => x.Action[0] === 'dynamodb:PutItem');
998+
expect(dynamodbPermissions.Resource).to.be.deep.equal([{
999+
'Fn::GetAtt': ['MyTable', 'Arn'],
1000+
}]);
1001+
});
9031002
});

lib/deploy/stepFunctions/compileStateMachines.js

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
'use strict';
22
const _ = require('lodash');
33
const BbPromise = require('bluebird');
4+
const { isIntrinsic } = require('../../utils/aws');
45

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

1012
function toTags(obj, serverless) {
@@ -26,8 +28,40 @@ function toTags(obj, serverless) {
2628
return tags;
2729
}
2830

31+
// return an iterable of
32+
// [ ParamName, IntrinsicFunction ]
33+
// e.g. [ 'mptFnX05Fb', { Ref: 'MyTopic' } ]
34+
// this makes it easy to use _.fromPairs to construct an object afterwards
35+
function* getIntrinsicFunctions(obj) {
36+
// eslint-disable-next-line no-restricted-syntax
37+
for (const key in obj) {
38+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
39+
const value = obj[key];
40+
41+
if (Array.isArray(value)) {
42+
// eslint-disable-next-line guard-for-in, no-restricted-syntax
43+
for (const idx in value) {
44+
const innerFuncs = Array.from(getIntrinsicFunctions(value[idx]));
45+
for (const x of innerFuncs) {
46+
yield x;
47+
}
48+
}
49+
} else if (isIntrinsic(value)) {
50+
const paramName = randomName();
51+
// eslint-disable-next-line no-param-reassign
52+
obj[key] = `\${${paramName}}`;
53+
yield [paramName, value];
54+
} else if (typeof value === 'object') {
55+
const innerFuncs = Array.from(getIntrinsicFunctions(value));
56+
for (const x of innerFuncs) {
57+
yield x;
58+
}
59+
}
60+
}
61+
}
62+
}
63+
2964
module.exports = {
30-
isIntrinsic,
3165
compileStateMachines() {
3266
if (this.isStateMachines()) {
3367
this.getAllStateMachines().forEach((stateMachineName) => {
@@ -42,7 +76,17 @@ module.exports = {
4276
DefinitionString = JSON.stringify(stateMachineObj.definition)
4377
.replace(/\\n|\\r|\\n\\r/g, '');
4478
} else {
45-
DefinitionString = JSON.stringify(stateMachineObj.definition, undefined, 2);
79+
const functionMappings = Array.from(getIntrinsicFunctions(stateMachineObj.definition));
80+
if (_.isEmpty(functionMappings)) {
81+
DefinitionString = JSON.stringify(stateMachineObj.definition, undefined, 2);
82+
} else {
83+
DefinitionString = {
84+
'Fn::Sub': [
85+
JSON.stringify(stateMachineObj.definition, undefined, 2),
86+
_.fromPairs(functionMappings),
87+
],
88+
};
89+
}
4690
}
4791
} else {
4892
const errorMessage = [

0 commit comments

Comments
 (0)