Skip to content

Commit 8981580

Browse files
authored
Merge pull request #443 from danrivett/442-support-external-ddb-table-names
feat: support importing DynamoDB tables exported from external stacks
2 parents 5929794 + 0f611dd commit 8981580

File tree

3 files changed

+137
-4
lines changed

3 files changed

+137
-4
lines changed

README.md

+23
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ Specify your state machine definition using Amazon States Language in a `definit
8383

8484
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.
8585

86+
In addition, if you want to reference a DynamoDB table managed by an external CloudFormation Stack, as long as that table name is exported as an output from that stack, it can be referenced by importing it using `Fn::ImportValue`. See the `ddbtablestepfunc` Step Function definition below for an example.
87+
8688
```yml
8789
functions:
8890
hello:
@@ -138,6 +140,27 @@ stepFunctions:
138140
Resource:
139141
Fn::GetAtt: [hello, Arn]
140142
End: true
143+
ddbtablestepfunc:
144+
definition:
145+
Comment: Demonstrates how to reference a DynamoDB Table Name exported from an external CloudFormation Stack
146+
StartAt: ImportDDBTableName
147+
States:
148+
ImportDDBTableName:
149+
Type: Task
150+
Resource: "arn:aws:states:::dynamodb:updateItem"
151+
Parameters:
152+
TableName:
153+
Fn::ImportValue: MyExternalStack:ToDoTable:Name # imports a table name from an external stack
154+
Key:
155+
id:
156+
S.$: "$.todoId"
157+
UpdateExpression: "SET #status = :updatedStatus"
158+
ExpressionAttributeNames:
159+
"#status": status
160+
ExpressionAttributeValues:
161+
":updatedStatus":
162+
S: DONE
163+
End: true
141164
dependsOn:
142165
- DynamoDBTable
143166
- KinesisStream

lib/deploy/stepFunctions/compileIamRole.js

+29-4
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,37 @@ function getSnsPermissions(serverless, state) {
7676
}
7777

7878
function getDynamoDBArn(tableName) {
79-
if (isIntrinsic(tableName) && tableName.Ref) {
79+
if (isIntrinsic(tableName)) {
8080
// most likely we'll see a { Ref: LogicalId }, which we need to map to
8181
// { Fn::GetAtt: [ LogicalId, Arn ] } to get the ARN
82-
return {
83-
'Fn::GetAtt': [tableName.Ref, 'Arn'],
84-
};
82+
if (tableName.Ref) {
83+
return {
84+
'Fn::GetAtt': [tableName.Ref, 'Arn'],
85+
};
86+
}
87+
// but also support importing the table name from an external stack that exports it
88+
// as we still want to support direct state machine actions interacting with those tables
89+
if (tableName['Fn::ImportValue']) {
90+
return {
91+
'Fn::Join': [
92+
':',
93+
[
94+
'arn:aws:dynamodb',
95+
{ Ref: 'AWS::Region' },
96+
{ Ref: 'AWS::AccountId' },
97+
{
98+
'Fn::Join': [
99+
'/',
100+
[
101+
'table',
102+
tableName,
103+
],
104+
],
105+
},
106+
],
107+
],
108+
};
109+
}
85110
}
86111

87112
return {

lib/deploy/stepFunctions/compileIamRole.test.js

+85
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,91 @@ describe('#compileIamRole', () => {
576576
.to.be.deep.equal([worldTableArn]);
577577
});
578578

579+
it('should give dynamodb permission for table name imported from external stack', () => {
580+
const externalHelloTable = { 'Fn::ImportValue': 'HelloStack:Table:Name' };
581+
const helloTableArn = {
582+
'Fn::Join': [
583+
':', ['arn:aws:dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, { 'Fn::Join': ['/', ['table', externalHelloTable]] }],
584+
],
585+
};
586+
587+
const externalWorldTable = { 'Fn::ImportValue': 'WorldStack:Table:Name' };
588+
const worldTableArn = {
589+
'Fn::Join': [
590+
':', ['arn:aws:dynamodb', { Ref: 'AWS::Region' }, { Ref: 'AWS::AccountId' }, { 'Fn::Join': ['/', ['table', externalWorldTable]] }],
591+
],
592+
};
593+
594+
const genStateMachine = (id, tableName) => ({
595+
id,
596+
definition: {
597+
StartAt: 'A',
598+
States: {
599+
A: {
600+
Type: 'Task',
601+
Resource: 'arn:aws:states:::dynamodb:updateItem',
602+
Parameters: {
603+
TableName: tableName,
604+
},
605+
Next: 'B',
606+
},
607+
B: {
608+
Type: 'Task',
609+
Resource: 'arn:aws:states:::dynamodb:putItem',
610+
Parameters: {
611+
TableName: tableName,
612+
},
613+
Next: 'C',
614+
},
615+
C: {
616+
Type: 'Task',
617+
Resource: 'arn:aws:states:::dynamodb:getItem',
618+
Parameters: {
619+
TableName: tableName,
620+
},
621+
Next: 'D',
622+
},
623+
D: {
624+
Type: 'Task',
625+
Resource: 'arn:aws:states:::dynamodb:deleteItem',
626+
Parameters: {
627+
TableName: tableName,
628+
},
629+
End: true,
630+
},
631+
},
632+
},
633+
});
634+
635+
serverless.service.stepFunctions = {
636+
stateMachines: {
637+
myStateMachine1: genStateMachine('StateMachine1', externalHelloTable),
638+
myStateMachine2: genStateMachine('StateMachine2', externalWorldTable),
639+
},
640+
};
641+
642+
serverlessStepFunctions.compileIamRole();
643+
const resources = serverlessStepFunctions.serverless.service
644+
.provider.compiledCloudFormationTemplate.Resources;
645+
const policy1 = resources.StateMachine1Role.Properties.Policies[0];
646+
const policy2 = resources.StateMachine2Role.Properties.Policies[0];
647+
648+
[policy1, policy2].forEach((policy) => {
649+
expect(policy.PolicyDocument.Statement[0].Action)
650+
.to.be.deep.equal([
651+
'dynamodb:UpdateItem',
652+
'dynamodb:PutItem',
653+
'dynamodb:GetItem',
654+
'dynamodb:DeleteItem',
655+
]);
656+
});
657+
658+
expect(policy1.PolicyDocument.Statement[0].Resource)
659+
.to.be.deep.equal([helloTableArn]);
660+
expect(policy2.PolicyDocument.Statement[0].Resource)
661+
.to.be.deep.equal([worldTableArn]);
662+
});
663+
579664
it('should give dynamodb permission to * whenever TableName.$ is seen', () => {
580665
const helloTable = 'hello';
581666

0 commit comments

Comments
 (0)