Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ language: node_js

matrix:
include:
- node_js: '4.4'
- node_js: '5.11'
- node_js: '6.2'
- node_js: '6.2'
- node_js: '8.9'
- node_js: '10.6'
- node_js: '10.6'
env:
- DISABLE_TESTS=true
- LINTING=true
Expand All @@ -20,4 +20,4 @@ script:
- if [[ ! -z "$DISABLE_TESTS" && ! -z "$LINTING" ]]; then npm run lint; fi

after_success:
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ stepFunctions:
Type: Task
Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-hello
End: true
dependsOn: CustomIamRole
hellostepfunc2:
definition:
StartAt: HelloWorld2
Expand All @@ -55,6 +56,10 @@ stepFunctions:
Type: Task
Resource: arn:aws:states:#{AWS::Region}:#{AWS::AccountId}:activity:myTask
End: true
dependsOn:
- DynamoDBTable
- KinesisStream
- CUstomIamRole
activities:
- myTask
- yourTask
Expand Down Expand Up @@ -110,6 +115,23 @@ plugins:

You can then `Ref: SendMessageStateMachine` in various parts of CloudFormation or serverless.yml

#### Depending on another logical id
If your state machine depends on another resource defined in your `serverless.yml` then you can add a `dependsOn` field to the state machine `definition`. This would add the `DependsOn`clause to the generated CloudFormation template.

This `dependsOn` field can be either a string, or an array of strings.

```yaml
stepFunctions:
stateMachines:
myStateMachine:
dependsOn: myDB

myOtherStateMachine:
dependsOn:
- myOtherDB
- myStream
```

#### Current Gotcha
Please keep this gotcha in mind if you want to reference the `name` from the `resources` section. To generate Logical ID for CloudFormation, the plugin transforms the specified name in serverless.yml based on the following scheme.

Expand Down Expand Up @@ -329,11 +351,11 @@ stepFunctions:
events:
- http:
path: /users
...
...
authorizer:
# Provide both type and authorizerId
type: COGNITO_USER_POOLS # TOKEN, CUSTOM or COGNITO_USER_POOLS, same as AWS Cloudformation documentation
authorizerId:
authorizerId:
Ref: ApiGatewayAuthorizer # or hard-code Authorizer ID
```

Expand Down Expand Up @@ -581,7 +603,7 @@ stepFunctions:
state:
- pending
definition:
...
...
```

## Specifying a Name
Expand Down Expand Up @@ -654,7 +676,7 @@ resources:
Resources:
StateMachineRole:
Type: AWS::IAM::Role
Properties:
Properties:
...
```

Expand Down
27 changes: 21 additions & 6 deletions lib/deploy/stepFunctions/compileIamRole.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function consolidatePermissionsByAction(permissions) {
.mapValues(perms => {
// find the unique resources
let resources = _.uniqWith(_.flatMap(perms, p => p.resource), _.isEqual);
if (resources.includes('*')) {
if (_.includes(resources, '*')) {
resources = '*';
}

Expand Down Expand Up @@ -198,6 +198,25 @@ function getIamPermissions(serverless, taskStates) {
});
}

function getIamStatements(iamPermissions) {
// when the state machine doesn't define any Task states, and therefore doesn't need ANY
// permission, then we should follow the behaviour of the AWS console and return a policy
// that denies access to EVERYTHING
if (_.isEmpty(iamPermissions)) {
return [{
Effect: 'Deny',
Action: '*',
Resource: '*',
}];
}

return iamPermissions.map(p => ({
Effect: 'Allow',
Action: p.action.split(','),
Resource: p.resource,
}));
}

module.exports = {
compileIamRole() {
const customRolesProvided = [];
Expand All @@ -223,11 +242,7 @@ module.exports = {
iamPermissions = consolidatePermissionsByAction(iamPermissions);
iamPermissions = consolidatePermissionsByResource(iamPermissions);

const iamStatements = iamPermissions.map(p => ({
Effect: 'Allow',
Action: p.action.split(','),
Resource: p.resource,
}));
const iamStatements = getIamStatements(iamPermissions);

const iamRoleJson =
iamRoleStateMachineExecutionTemplate
Expand Down
52 changes: 44 additions & 8 deletions lib/deploy/stepFunctions/compileIamRole.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ describe('#compileIamRole', () => {
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options);
});

const expectDenyAllPolicy = (policy) => {
const statements = policy.PolicyDocument.Statement;
expect(statements).to.have.lengthOf(1);
expect(statements[0].Effect).to.equal('Deny');
expect(statements[0].Action).to.equal('*');
expect(statements[0].Resource).to.equal('*');
};

it('should do nothing when role property exists in all statmachine properties', () => {
serverless.service.stepFunctions = {
stateMachines: {
Expand Down Expand Up @@ -243,7 +251,7 @@ describe('#compileIamRole', () => {
const policy = serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
.Properties.Policies[0];
expect(policy.PolicyDocument.Statement).to.have.lengthOf(0);
expectDenyAllPolicy(policy);
});

it('should give sqs:SendMessage permission for only SQS referenced by state machine', () => {
Expand Down Expand Up @@ -362,7 +370,7 @@ describe('#compileIamRole', () => {
const policy = serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
.Properties.Policies[0];
expect(policy.PolicyDocument.Statement).to.have.lengthOf(0);
expectDenyAllPolicy(policy);
});

it('should not give sqs:SendMessage permission if QueueUrl is invalid', () => {
Expand Down Expand Up @@ -789,10 +797,10 @@ describe('#compileIamRole', () => {
};

serverlessStepFunctions.compileIamRole();
const statements = serverlessStepFunctions.serverless.service
const policy = serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
.Properties.Policies[0].PolicyDocument.Statement;
expect(statements).to.have.lengthOf(0);
.Properties.Policies[0];
expectDenyAllPolicy(policy);
});

it('should not generate any permissions for Task states not yet supported', () => {
Expand All @@ -818,9 +826,37 @@ describe('#compileIamRole', () => {
};

serverlessStepFunctions.compileIamRole();
const statements = serverlessStepFunctions.serverless.service
const policy = serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
.Properties.Policies[0].PolicyDocument.Statement;
expect(statements).to.have.lengthOf(0);
.Properties.Policies[0];
expectDenyAllPolicy(policy);
});

it('should generate a Deny all statement if state machine has no tasks', () => {
const genStateMachine = (name) => ({
name,
definition: {
StartAt: 'A',
States: {
A: {
Type: 'Pass',
End: true,
},
},
},
});

serverless.service.stepFunctions = {
stateMachines: {
myStateMachine1: genStateMachine('stateMachineBeta1'),
myStateMachine2: genStateMachine('stateMachineBeta2'),
},
};

serverlessStepFunctions.compileIamRole();
const policy = serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
.Properties.Policies[0];
expectDenyAllPolicy(policy);
});
});
21 changes: 19 additions & 2 deletions lib/deploy/stepFunctions/compileStateMachines.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = {
const stateMachineObj = this.getStateMachine(stateMachineName);
let DefinitionString;
let RoleArn;
let DependsOn;
let DependsOn = [];

if (stateMachineObj.definition) {
if (typeof stateMachineObj.definition === 'string') {
Expand Down Expand Up @@ -63,7 +63,24 @@ module.exports = {
'Arn',
],
};
DependsOn = 'IamRoleStateMachineExecution';
DependsOn.push('IamRoleStateMachineExecution');
}

if (stateMachineObj.dependsOn) {
const dependsOn = stateMachineObj.dependsOn;

if (_.isArray(dependsOn) && _.every(dependsOn, _.isString)) {
DependsOn = _.concat(DependsOn, dependsOn);
} else if (_.isString(dependsOn)) {
DependsOn.push(dependsOn);
} else {
const errorMessage = [
`dependsOn property in stateMachine "${stateMachineName}" is neither a string`,
' nor an array of strings',
].join('');
throw new this.serverless.classes
.Error(errorMessage);
}
}

const stateMachineLogicalId = this.getStateMachineLogicalId(stateMachineName,
Expand Down
90 changes: 84 additions & 6 deletions lib/deploy/stepFunctions/compileStateMachines.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ describe('#compileStateMachines', () => {
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta1.DependsOn
).to.equal('IamRoleStateMachineExecution');
).to.deep.eq(['IamRoleStateMachineExecution']);
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta2.DependsOn
).to.equal('IamRoleStateMachineExecution');
).to.deep.eq(['IamRoleStateMachineExecution']);
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Outputs
.StateMachineBeta1Arn.Value.Ref
Expand Down Expand Up @@ -119,11 +119,11 @@ describe('#compileStateMachines', () => {
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.MyStateMachine1StepFunctionsStateMachine.DependsOn
).to.equal('IamRoleStateMachineExecution');
).to.deep.eq(['IamRoleStateMachineExecution']);
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.MyStateMachine2StepFunctionsStateMachine.DependsOn
).to.equal('IamRoleStateMachineExecution');
).to.deep.eq(['IamRoleStateMachineExecution']);
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Outputs
.MyStateMachine1StepFunctionsStateMachineArn.Value.Ref
Expand Down Expand Up @@ -176,11 +176,11 @@ describe('#compileStateMachines', () => {
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta1.DependsOn
).to.equal('IamRoleStateMachineExecution');
).to.deep.eq(['IamRoleStateMachineExecution']);
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta2.DependsOn
).to.equal('IamRoleStateMachineExecution');
).to.deep.eq(['IamRoleStateMachineExecution']);
});

it('should create corresponding resources when definition and role property are given', () => {
Expand Down Expand Up @@ -405,4 +405,82 @@ describe('#compileStateMachines', () => {

expect(actual).to.equal(JSON.stringify(definition, undefined, 2));
});

it('should add dependsOn resources', () => {
serverless.service.stepFunctions = {
stateMachines: {
myStateMachine1: {
definition: 'definition1',
name: 'stateMachineBeta1',
dependsOn: 'DynamoDBTable',
},
myStateMachine2: {
definition: 'definition2',
name: 'stateMachineBeta2',
dependsOn: [
'DynamoDBTable',
'KinesisStream',
],
},
},
};

serverlessStepFunctions.compileStateMachines();
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta1.Type
).to.equal('AWS::StepFunctions::StateMachine');
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta2.Type
).to.equal('AWS::StepFunctions::StateMachine');
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta1.Properties.DefinitionString
).to.equal('"definition1"');
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta2.Properties.DefinitionString
).to.equal('"definition2"');
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta1.Properties.RoleArn['Fn::GetAtt'][0]
).to.equal('IamRoleStateMachineExecution');
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta2.Properties.RoleArn['Fn::GetAtt'][0]
).to.equal('IamRoleStateMachineExecution');
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta1.DependsOn
).to.deep.eq(['IamRoleStateMachineExecution', 'DynamoDBTable']);
expect(serverlessStepFunctions.serverless.service
.provider.compiledCloudFormationTemplate.Resources
.StateMachineBeta2.DependsOn
).to.deep.eq(['IamRoleStateMachineExecution', 'DynamoDBTable', 'KinesisStream']);
});

it('should throw error when dependsOn property is neither string nor [string]', () => {
serverless.service.stepFunctions = {
stateMachines: {
myStateMachine1: {
definition: 'definition1',
name: 'stateMachineBeta1',
dependsOn: { Ref: 'ss' },
},
},
};
expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error);

serverless.service.stepFunctions = {
stateMachines: {
myStateMachine1: {
definition: 'definition1',
name: 'stateMachineBeta1',
dependsOn: [{ Ref: 'ss' }],
},
},
};
expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error);
});
});
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading