Skip to content

Commit be80563

Browse files
authored
Merge pull request #237 from horike37/feature/support_local_function_name
Feature/support local function name
2 parents a7ed72c + cf5345a commit be80563

File tree

6 files changed

+294
-26
lines changed

6 files changed

+294
-26
lines changed

README.md

+16-12
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ plugins:
6767
6868
## Setup
6969
70-
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`.
70+
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`. Since `Ref` returns different things (ARN, ID, resource name, etc.) depending on the type of CloudFormation resource, please refer to [this page](https://theburningmonk.com/cloudformation-ref-and-getatt-cheatsheet/) to see whether you need to use `Ref` or `Fn::GetAtt`.
7171

7272
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.
7373

@@ -99,7 +99,7 @@ stepFunctions:
9999
HelloWorld1:
100100
Type: Task
101101
Resource:
102-
Fn::GetAtt: [HelloLambdaFunction, Arn]
102+
Fn::GetAtt: [hello, Arn]
103103
End: true
104104
dependsOn: CustomIamRole
105105
tags:
@@ -123,7 +123,7 @@ stepFunctions:
123123
HelloWorld2:
124124
Type: Task
125125
Resource:
126-
Fn::GetAtt: [HelloLambdaFunction, Arn]
126+
Fn::GetAtt: [hello, Arn]
127127
End: true
128128
dependsOn:
129129
- DynamoDBTable
@@ -140,6 +140,10 @@ plugins:
140140
- serverless-pseudo-parameters
141141
```
142142

143+
In the example above, notice that we used `Fn::GetAtt: [hello, Arn]` to get the ARN for the `hello` function defined earlier. This means you don't have to know how the `Serverless` framework converts these local names to CloudFormation logical IDs (e.g. `hello-world` becomes `HelloDashworldLambdaFunction`).
144+
145+
However, if you prefer to work with logical IDs, you can. You can also express the above `Fn::GetAtt` function as `Fn::GetAtt: [HelloLambdaFunction, Arn]`. If you're unfamiliar with the convention the `Serverless` framework uses, then the easiest thing to do is to first run `sls package` then look in the `.serverless` folder for the generated CloudFormation template. Here you can find the logical resource names for the functions you want to reference.
146+
143147
### Adding a custom name for a stateMachine
144148

145149
In case you need to interpolate a specific stage or service layer variable as the
@@ -691,7 +695,7 @@ functions:
691695
HelloWorld1:
692696
Type: Task
693697
Resource:
694-
Fn::GetAtt: [HelloLambdaFunction, Arn]
698+
Fn::GetAtt: [hello, Arn]
695699
End: true
696700
697701
@@ -1008,7 +1012,7 @@ stepFunctions:
10081012
FirstState:
10091013
Type: Task
10101014
Resource:
1011-
Fn::GetAtt: [HelloLambdaFunction, Arn]
1015+
Fn::GetAtt: [hello, Arn]
10121016
Next: wait_using_seconds
10131017
wait_using_seconds:
10141018
Type: Wait
@@ -1029,7 +1033,7 @@ stepFunctions:
10291033
FinalState:
10301034
Type: Task
10311035
Resource:
1032-
Fn::GetAtt: [HelloLambdaFunction, Arn]
1036+
Fn::GetAtt: [hello, Arn]
10331037
End: true
10341038
plugins:
10351039
- serverless-step-functions
@@ -1053,7 +1057,7 @@ stepFunctions:
10531057
HelloWorld:
10541058
Type: Task
10551059
Resource:
1056-
Fn::GetAtt: [HelloLambdaFunction, Arn]
1060+
Fn::GetAtt: [hello, Arn]
10571061
Retry:
10581062
- ErrorEquals:
10591063
- HandledError
@@ -1134,7 +1138,7 @@ stepFunctions:
11341138
HelloWorld:
11351139
Type: Task
11361140
Resource:
1137-
Fn::GetAtt: [HelloLambdaFunction, Arn]
1141+
Fn::GetAtt: [hello, Arn]
11381142
Catch:
11391143
- ErrorEquals: ["HandledError"]
11401144
Next: CustomErrorFallback
@@ -1183,7 +1187,7 @@ stepFunctions:
11831187
FirstState:
11841188
Type: Task
11851189
Resource:
1186-
Fn::GetAtt: [Hello1LambdaFunction, Arn]
1190+
Fn::GetAtt: [hello, Arn]
11871191
Next: ChoiceState
11881192
ChoiceState:
11891193
Type: Choice
@@ -1198,20 +1202,20 @@ stepFunctions:
11981202
FirstMatchState:
11991203
Type: Task
12001204
Resource:
1201-
Fn::GetAtt: [Hello2LambdaFunction, Arn]
1205+
Fn::GetAtt: [hello2, Arn]
12021206
Next: NextState
12031207
SecondMatchState:
12041208
Type: Task
12051209
Resource:
1206-
Fn::GetAtt: [Hello3LambdaFunction, Arn]
1210+
Fn::GetAtt: [hello3, Arn]
12071211
Next: NextState
12081212
DefaultState:
12091213
Type: Fail
12101214
Cause: "No Matches!"
12111215
NextState:
12121216
Type: Task
12131217
Resource:
1214-
Fn::GetAtt: [Hello4LambdaFunction, Arn]
1218+
Fn::GetAtt: [hello4, Arn]
12151219
End: true
12161220
plugins:
12171221
- serverless-step-functions

lib/deploy/stepFunctions/compileIamRole.js

+10-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const _ = require('lodash');
44
const BbPromise = require('bluebird');
55
const path = require('path');
6-
const { isIntrinsic } = require('../../utils/aws');
6+
const { isIntrinsic, translateLocalFunctionNames } = require('../../utils/aws');
77

88
function getTaskStates(states) {
99
return _.flatMap(states, (state) => {
@@ -177,7 +177,7 @@ function getLambdaPermissions(state) {
177177
// so you should be able to use Fn::GetAtt here to get the ARN
178178
return [{
179179
action: 'lambda:InvokeFunction',
180-
resource: functionName,
180+
resource: translateLocalFunctionNames.bind(this)(functionName),
181181
}];
182182
} if (_.has(functionName, 'Ref')) {
183183
// because the FunctionName parameter can be either a name or ARN
@@ -188,7 +188,7 @@ function getLambdaPermissions(state) {
188188
'Fn::Sub': [
189189
'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${FunctionName}',
190190
{
191-
FunctionName: functionName,
191+
FunctionName: translateLocalFunctionNames.bind(this)(functionName),
192192
},
193193
],
194194
},
@@ -239,16 +239,16 @@ function consolidatePermissionsByResource(permissions) {
239239
.value(); // unchain
240240
}
241241

242-
function getIamPermissions(serverless, taskStates) {
242+
function getIamPermissions(taskStates) {
243243
return _.flatMap(taskStates, (state) => {
244244
switch (state.Resource) {
245245
case 'arn:aws:states:::sqs:sendMessage':
246246
case 'arn:aws:states:::sqs:sendMessage.waitForTaskToken':
247-
return getSqsPermissions(serverless, state);
247+
return getSqsPermissions(this.serverless, state);
248248

249249
case 'arn:aws:states:::sns:publish':
250250
case 'arn:aws:states:::sns:publish.waitForTaskToken':
251-
return getSnsPermissions(serverless, state);
251+
return getSnsPermissions(this.serverless, state);
252252

253253
case 'arn:aws:states:::dynamodb:updateItem':
254254
return getDynamoDBPermissions('dynamodb:UpdateItem', state);
@@ -274,16 +274,16 @@ function getIamPermissions(serverless, taskStates) {
274274

275275
case 'arn:aws:states:::lambda:invoke':
276276
case 'arn:aws:states:::lambda:invoke.waitForTaskToken':
277-
return getLambdaPermissions(state);
277+
return getLambdaPermissions.bind(this)(state);
278278

279279
default:
280280
if (isIntrinsic(state.Resource) || state.Resource.startsWith('arn:aws:lambda')) {
281281
return [{
282282
action: 'lambda:InvokeFunction',
283-
resource: state.Resource,
283+
resource: translateLocalFunctionNames.bind(this)(state.Resource),
284284
}];
285285
}
286-
serverless.cli.consoleLog('Cannot generate IAM policy statement for Task state', state);
286+
this.serverless.cli.consoleLog('Cannot generate IAM policy statement for Task state', state);
287287
return [];
288288
}
289289
});
@@ -317,7 +317,7 @@ module.exports = {
317317
customRolesProvided.push('role' in stateMachineObj);
318318

319319
const taskStates = getTaskStates(stateMachineObj.definition.States);
320-
iamPermissions = iamPermissions.concat(getIamPermissions(this.serverless, taskStates));
320+
iamPermissions = iamPermissions.concat(getIamPermissions.bind(this)(taskStates));
321321
});
322322
if (_.isEqual(_.uniq(customRolesProvided), [true])) {
323323
return BbPromise.resolve();

lib/deploy/stepFunctions/compileIamRole.test.js

+110
Original file line numberDiff line numberDiff line change
@@ -1259,4 +1259,114 @@ describe('#compileIamRole', () => {
12591259
];
12601260
expect(lambdaPermissions[0].Resource).to.deep.eq(lambdaArns);
12611261
});
1262+
1263+
it('should support local function names', () => {
1264+
const getStateMachine = name => ({
1265+
name,
1266+
definition: {
1267+
StartAt: 'A',
1268+
States: {
1269+
A: {
1270+
Type: 'Task',
1271+
Resource: {
1272+
'Fn::GetAtt': ['hello-world', 'Arn'],
1273+
},
1274+
End: true,
1275+
},
1276+
},
1277+
},
1278+
});
1279+
1280+
serverless.service.functions = {
1281+
'hello-world': {
1282+
handler: 'hello-world.handler',
1283+
},
1284+
};
1285+
1286+
serverless.service.stepFunctions = {
1287+
stateMachines: {
1288+
myStateMachine1: getStateMachine('sm1'),
1289+
},
1290+
};
1291+
1292+
serverlessStepFunctions.compileIamRole();
1293+
const statements = serverlessStepFunctions.serverless.service
1294+
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
1295+
.Properties.Policies[0].PolicyDocument.Statement;
1296+
1297+
const lambdaPermissions = statements.filter(s => _.isEqual(s.Action, ['lambda:InvokeFunction']));
1298+
expect(lambdaPermissions).to.have.lengthOf(1);
1299+
1300+
const lambdaArns = [
1301+
{
1302+
'Fn::GetAtt': [
1303+
'HelloDashworldLambdaFunction',
1304+
'Arn',
1305+
],
1306+
},
1307+
];
1308+
expect(lambdaPermissions[0].Resource).to.deep.eq(lambdaArns);
1309+
});
1310+
1311+
it('should support local function names for lambda::invoke resource type', () => {
1312+
const getStateMachine = (name, functionName) => ({
1313+
name,
1314+
definition: {
1315+
StartAt: 'A',
1316+
States: {
1317+
A: {
1318+
Type: 'Task',
1319+
Resource: 'arn:aws:states:::lambda:invoke',
1320+
Parameters: {
1321+
FunctionName: functionName,
1322+
Payload: {
1323+
'ExecutionName.$': '$$.Execution.Name',
1324+
},
1325+
},
1326+
End: true,
1327+
},
1328+
},
1329+
},
1330+
});
1331+
1332+
const lambda1 = { Ref: 'hello-world' };
1333+
const lambda2 = { 'Fn::GetAtt': ['hello-world', 'Arn'] };
1334+
1335+
serverless.service.functions = {
1336+
'hello-world': {
1337+
handler: 'hello-world.handler',
1338+
},
1339+
};
1340+
1341+
serverless.service.stepFunctions = {
1342+
stateMachines: {
1343+
myStateMachine1: getStateMachine('sm1', lambda1),
1344+
myStateMachine2: getStateMachine('sm2', lambda2),
1345+
},
1346+
};
1347+
1348+
serverlessStepFunctions.compileIamRole();
1349+
const statements = serverlessStepFunctions.serverless.service
1350+
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
1351+
.Properties.Policies[0].PolicyDocument.Statement;
1352+
1353+
const lambdaPermissions = statements.filter(s => _.isEqual(s.Action, ['lambda:InvokeFunction']));
1354+
expect(lambdaPermissions).to.have.lengthOf(1);
1355+
1356+
const lambdaArns = [
1357+
{
1358+
'Fn::Sub': [
1359+
'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${FunctionName}',
1360+
{ FunctionName: { Ref: 'HelloDashworldLambdaFunction' } },
1361+
],
1362+
},
1363+
{
1364+
'Fn::GetAtt': [
1365+
'HelloDashworldLambdaFunction',
1366+
'Arn',
1367+
],
1368+
},
1369+
];
1370+
expect(lambdaPermissions[0].Resource).to.deep.eq(lambdaArns);
1371+
});
12621372
});

lib/deploy/stepFunctions/compileStateMachines.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const _ = require('lodash');
44
const BbPromise = require('bluebird');
55
const Chance = require('chance');
6-
const { isIntrinsic } = require('../../utils/aws');
6+
const { isIntrinsic, translateLocalFunctionNames } = require('../../utils/aws');
77

88
const chance = new Chance();
99

@@ -83,13 +83,17 @@ module.exports = {
8383
.replace(/\\n|\\r|\\n\\r/g, '');
8484
} else {
8585
const functionMappings = Array.from(getIntrinsicFunctions(stateMachineObj.definition));
86+
const definitionString = JSON.stringify(stateMachineObj.definition, undefined, 2);
87+
8688
if (_.isEmpty(functionMappings)) {
87-
DefinitionString = JSON.stringify(stateMachineObj.definition, undefined, 2);
89+
DefinitionString = definitionString;
8890
} else {
91+
const f = translateLocalFunctionNames.bind(this);
92+
const params = _.fromPairs(functionMappings.map(([k, v]) => [k, f(v)]));
8993
DefinitionString = {
9094
'Fn::Sub': [
91-
JSON.stringify(stateMachineObj.definition, undefined, 2),
92-
_.fromPairs(functionMappings),
95+
definitionString,
96+
params,
9397
],
9498
};
9599
}

0 commit comments

Comments
 (0)