Skip to content

Commit

Permalink
Merge cf5345a into a7ed72c
Browse files Browse the repository at this point in the history
  • Loading branch information
theburningmonk committed Jul 30, 2019
2 parents a7ed72c + cf5345a commit f838faa
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 26 deletions.
28 changes: 16 additions & 12 deletions README.md
Expand Up @@ -67,7 +67,7 @@ plugins:

## Setup

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`.
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`.

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.

Expand Down Expand Up @@ -99,7 +99,7 @@ stepFunctions:
HelloWorld1:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
Fn::GetAtt: [hello, Arn]
End: true
dependsOn: CustomIamRole
tags:
Expand All @@ -123,7 +123,7 @@ stepFunctions:
HelloWorld2:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
Fn::GetAtt: [hello, Arn]
End: true
dependsOn:
- DynamoDBTable
Expand All @@ -140,6 +140,10 @@ plugins:
- serverless-pseudo-parameters
```

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`).

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.

### Adding a custom name for a stateMachine

In case you need to interpolate a specific stage or service layer variable as the
Expand Down Expand Up @@ -691,7 +695,7 @@ functions:
HelloWorld1:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
Fn::GetAtt: [hello, Arn]
End: true


Expand Down Expand Up @@ -1008,7 +1012,7 @@ stepFunctions:
FirstState:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
Fn::GetAtt: [hello, Arn]
Next: wait_using_seconds
wait_using_seconds:
Type: Wait
Expand All @@ -1029,7 +1033,7 @@ stepFunctions:
FinalState:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
Fn::GetAtt: [hello, Arn]
End: true
plugins:
- serverless-step-functions
Expand All @@ -1053,7 +1057,7 @@ stepFunctions:
HelloWorld:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
Fn::GetAtt: [hello, Arn]
Retry:
- ErrorEquals:
- HandledError
Expand Down Expand Up @@ -1134,7 +1138,7 @@ stepFunctions:
HelloWorld:
Type: Task
Resource:
Fn::GetAtt: [HelloLambdaFunction, Arn]
Fn::GetAtt: [hello, Arn]
Catch:
- ErrorEquals: ["HandledError"]
Next: CustomErrorFallback
Expand Down Expand Up @@ -1183,7 +1187,7 @@ stepFunctions:
FirstState:
Type: Task
Resource:
Fn::GetAtt: [Hello1LambdaFunction, Arn]
Fn::GetAtt: [hello, Arn]
Next: ChoiceState
ChoiceState:
Type: Choice
Expand All @@ -1198,20 +1202,20 @@ stepFunctions:
FirstMatchState:
Type: Task
Resource:
Fn::GetAtt: [Hello2LambdaFunction, Arn]
Fn::GetAtt: [hello2, Arn]
Next: NextState
SecondMatchState:
Type: Task
Resource:
Fn::GetAtt: [Hello3LambdaFunction, Arn]
Fn::GetAtt: [hello3, Arn]
Next: NextState
DefaultState:
Type: Fail
Cause: "No Matches!"
NextState:
Type: Task
Resource:
Fn::GetAtt: [Hello4LambdaFunction, Arn]
Fn::GetAtt: [hello4, Arn]
End: true
plugins:
- serverless-step-functions
Expand Down
20 changes: 10 additions & 10 deletions lib/deploy/stepFunctions/compileIamRole.js
Expand Up @@ -3,7 +3,7 @@
const _ = require('lodash');
const BbPromise = require('bluebird');
const path = require('path');
const { isIntrinsic } = require('../../utils/aws');
const { isIntrinsic, translateLocalFunctionNames } = require('../../utils/aws');

function getTaskStates(states) {
return _.flatMap(states, (state) => {
Expand Down Expand Up @@ -177,7 +177,7 @@ function getLambdaPermissions(state) {
// so you should be able to use Fn::GetAtt here to get the ARN
return [{
action: 'lambda:InvokeFunction',
resource: functionName,
resource: translateLocalFunctionNames.bind(this)(functionName),
}];
} if (_.has(functionName, 'Ref')) {
// because the FunctionName parameter can be either a name or ARN
Expand All @@ -188,7 +188,7 @@ function getLambdaPermissions(state) {
'Fn::Sub': [
'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${FunctionName}',
{
FunctionName: functionName,
FunctionName: translateLocalFunctionNames.bind(this)(functionName),
},
],
},
Expand Down Expand Up @@ -239,16 +239,16 @@ function consolidatePermissionsByResource(permissions) {
.value(); // unchain
}

function getIamPermissions(serverless, taskStates) {
function getIamPermissions(taskStates) {
return _.flatMap(taskStates, (state) => {
switch (state.Resource) {
case 'arn:aws:states:::sqs:sendMessage':
case 'arn:aws:states:::sqs:sendMessage.waitForTaskToken':
return getSqsPermissions(serverless, state);
return getSqsPermissions(this.serverless, state);

case 'arn:aws:states:::sns:publish':
case 'arn:aws:states:::sns:publish.waitForTaskToken':
return getSnsPermissions(serverless, state);
return getSnsPermissions(this.serverless, state);

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

case 'arn:aws:states:::lambda:invoke':
case 'arn:aws:states:::lambda:invoke.waitForTaskToken':
return getLambdaPermissions(state);
return getLambdaPermissions.bind(this)(state);

default:
if (isIntrinsic(state.Resource) || state.Resource.startsWith('arn:aws:lambda')) {
return [{
action: 'lambda:InvokeFunction',
resource: state.Resource,
resource: translateLocalFunctionNames.bind(this)(state.Resource),
}];
}
serverless.cli.consoleLog('Cannot generate IAM policy statement for Task state', state);
this.serverless.cli.consoleLog('Cannot generate IAM policy statement for Task state', state);
return [];
}
});
Expand Down Expand Up @@ -317,7 +317,7 @@ module.exports = {
customRolesProvided.push('role' in stateMachineObj);

const taskStates = getTaskStates(stateMachineObj.definition.States);
iamPermissions = iamPermissions.concat(getIamPermissions(this.serverless, taskStates));
iamPermissions = iamPermissions.concat(getIamPermissions.bind(this)(taskStates));
});
if (_.isEqual(_.uniq(customRolesProvided), [true])) {
return BbPromise.resolve();
Expand Down
110 changes: 110 additions & 0 deletions lib/deploy/stepFunctions/compileIamRole.test.js
Expand Up @@ -1259,4 +1259,114 @@ describe('#compileIamRole', () => {
];
expect(lambdaPermissions[0].Resource).to.deep.eq(lambdaArns);
});

it('should support local function names', () => {
const getStateMachine = name => ({
name,
definition: {
StartAt: 'A',
States: {
A: {
Type: 'Task',
Resource: {
'Fn::GetAtt': ['hello-world', 'Arn'],
},
End: true,
},
},
},
});

serverless.service.functions = {
'hello-world': {
handler: 'hello-world.handler',
},
};

serverless.service.stepFunctions = {
stateMachines: {
myStateMachine1: getStateMachine('sm1'),
},
};

serverlessStepFunctions.compileIamRole();
const statements = serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
.Properties.Policies[0].PolicyDocument.Statement;

const lambdaPermissions = statements.filter(s => _.isEqual(s.Action, ['lambda:InvokeFunction']));
expect(lambdaPermissions).to.have.lengthOf(1);

const lambdaArns = [
{
'Fn::GetAtt': [
'HelloDashworldLambdaFunction',
'Arn',
],
},
];
expect(lambdaPermissions[0].Resource).to.deep.eq(lambdaArns);
});

it('should support local function names for lambda::invoke resource type', () => {
const getStateMachine = (name, functionName) => ({
name,
definition: {
StartAt: 'A',
States: {
A: {
Type: 'Task',
Resource: 'arn:aws:states:::lambda:invoke',
Parameters: {
FunctionName: functionName,
Payload: {
'ExecutionName.$': '$$.Execution.Name',
},
},
End: true,
},
},
},
});

const lambda1 = { Ref: 'hello-world' };
const lambda2 = { 'Fn::GetAtt': ['hello-world', 'Arn'] };

serverless.service.functions = {
'hello-world': {
handler: 'hello-world.handler',
},
};

serverless.service.stepFunctions = {
stateMachines: {
myStateMachine1: getStateMachine('sm1', lambda1),
myStateMachine2: getStateMachine('sm2', lambda2),
},
};

serverlessStepFunctions.compileIamRole();
const statements = serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
.Properties.Policies[0].PolicyDocument.Statement;

const lambdaPermissions = statements.filter(s => _.isEqual(s.Action, ['lambda:InvokeFunction']));
expect(lambdaPermissions).to.have.lengthOf(1);

const lambdaArns = [
{
'Fn::Sub': [
'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${FunctionName}',
{ FunctionName: { Ref: 'HelloDashworldLambdaFunction' } },
],
},
{
'Fn::GetAtt': [
'HelloDashworldLambdaFunction',
'Arn',
],
},
];
expect(lambdaPermissions[0].Resource).to.deep.eq(lambdaArns);
});
});
12 changes: 8 additions & 4 deletions lib/deploy/stepFunctions/compileStateMachines.js
Expand Up @@ -3,7 +3,7 @@
const _ = require('lodash');
const BbPromise = require('bluebird');
const Chance = require('chance');
const { isIntrinsic } = require('../../utils/aws');
const { isIntrinsic, translateLocalFunctionNames } = require('../../utils/aws');

const chance = new Chance();

Expand Down Expand Up @@ -83,13 +83,17 @@ module.exports = {
.replace(/\\n|\\r|\\n\\r/g, '');
} else {
const functionMappings = Array.from(getIntrinsicFunctions(stateMachineObj.definition));
const definitionString = JSON.stringify(stateMachineObj.definition, undefined, 2);

if (_.isEmpty(functionMappings)) {
DefinitionString = JSON.stringify(stateMachineObj.definition, undefined, 2);
DefinitionString = definitionString;
} else {
const f = translateLocalFunctionNames.bind(this);
const params = _.fromPairs(functionMappings.map(([k, v]) => [k, f(v)]));
DefinitionString = {
'Fn::Sub': [
JSON.stringify(stateMachineObj.definition, undefined, 2),
_.fromPairs(functionMappings),
definitionString,
params,
],
};
}
Expand Down

0 comments on commit f838faa

Please sign in to comment.