Skip to content

Commit cd232b3

Browse files
feat: support callbacks
Closes #201
1 parent b533027 commit cd232b3

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed

lib/deploy/stepFunctions/compileIamRole.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,34 @@ function getDynamoDBPermissions(action, state) {
144144
}];
145145
}
146146

147+
function getLambdaPermissions(state) {
148+
// function name can be name-only, name-only with alias, full arn or partial arn
149+
// https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestParameters
150+
const functionName = state.Parameters.FunctionName;
151+
const segments = functionName.split(':');
152+
153+
let functionArn;
154+
if (functionName.startsWith('arn:aws:lambda')) {
155+
// full ARN
156+
functionArn = functionName;
157+
} else if (segments.length === 3 && segments[0].match(/^\d+$/)) {
158+
// partial ARN
159+
functionArn = {
160+
'Fn::Sub': `arn:aws:lambda:\${AWS::Region}:${functionName}`,
161+
};
162+
} else {
163+
// name-only (with or without alias)
164+
functionArn = {
165+
'Fn::Sub': `arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${functionName}`,
166+
};
167+
}
168+
169+
return [{
170+
action: 'lambda:InvokeFunction',
171+
resource: functionArn,
172+
}];
173+
}
174+
147175
// if there are multiple permissions with the same action, then collapsed them into one
148176
// permission instead, and collect the resources into an array
149177
function consolidatePermissionsByAction(permissions) {
@@ -185,9 +213,11 @@ function getIamPermissions(serverless, taskStates) {
185213
return _.flatMap(taskStates, state => {
186214
switch (state.Resource) {
187215
case 'arn:aws:states:::sqs:sendMessage':
216+
case 'arn:aws:states:::sqs:sendMessage.waitForTaskToken':
188217
return getSqsPermissions(serverless, state);
189218

190219
case 'arn:aws:states:::sns:publish':
220+
case 'arn:aws:states:::sns:publish.waitForTaskToken':
191221
return getSnsPermissions(serverless, state);
192222

193223
case 'arn:aws:states:::dynamodb:updateItem':
@@ -208,9 +238,13 @@ function getIamPermissions(serverless, taskStates) {
208238
return getGluePermissions();
209239

210240
case 'arn:aws:states:::ecs:runTask.sync':
241+
case 'arn:aws:states:::ecs:runTask.waitForTaskToken':
211242
case 'arn:aws:states:::ecs:runTask':
212243
return getEcsPermissions();
213244

245+
case 'arn:aws:states:::lambda:invoke.waitForTaskToken':
246+
return getLambdaPermissions(state);
247+
214248
default:
215249
if (isIntrinsic(state.Resource) || state.Resource.startsWith('arn:aws:lambda')) {
216250
return [{

lib/deploy/stepFunctions/compileIamRole.test.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,4 +999,156 @@ describe('#compileIamRole', () => {
999999
'Fn::GetAtt': ['MyTable', 'Arn'],
10001000
}]);
10011001
});
1002+
1003+
it('should support callbacks', () => {
1004+
const getStateMachine = (name, function1, function2, snsTopicArn, sqsQueueUrl) => ({
1005+
name,
1006+
definition: {
1007+
StartAt: 'A',
1008+
States: {
1009+
A: {
1010+
Type: 'Task',
1011+
Resource: 'arn:aws:states:::sns:publish.waitForTaskToken',
1012+
Parameters: {
1013+
Message: {
1014+
'Input.$': '$',
1015+
'TaskToken.$': '$$.Task.Token',
1016+
},
1017+
MessageStructure: 'json',
1018+
TopicArn: snsTopicArn,
1019+
},
1020+
Next: 'B1',
1021+
},
1022+
B1: {
1023+
Type: 'Task',
1024+
Resource: 'arn:aws:states:::lambda:invoke.waitForTaskToken',
1025+
Parameters: {
1026+
FunctionName: function1,
1027+
Payload: {
1028+
'model.$': '$',
1029+
'token.$': '$$.Task.Token',
1030+
},
1031+
},
1032+
Next: 'B2',
1033+
},
1034+
B2: {
1035+
Type: 'Task',
1036+
Resource: 'arn:aws:states:::lambda:invoke.waitForTaskToken',
1037+
Parameters: {
1038+
FunctionName: function2,
1039+
Payload: {
1040+
'model.$': '$',
1041+
'token.$': '$$.Task.Token',
1042+
},
1043+
},
1044+
Next: 'C',
1045+
},
1046+
C: {
1047+
Type: 'Task',
1048+
Resource: 'arn:aws:states:::sqs:sendMessage.waitForTaskToken',
1049+
Parameters: {
1050+
QueueUrl: sqsQueueUrl,
1051+
MessageBody: {
1052+
'Input.$': '$',
1053+
'TaskToken.$': '$$.Task.Token',
1054+
},
1055+
},
1056+
Next: 'D',
1057+
},
1058+
D: {
1059+
Type: 'Task',
1060+
Resource: 'arn:aws:states:::ecs:runTask.waitForTaskToken',
1061+
Parameters: {
1062+
LaunchType: 'FARGATE',
1063+
Cluster: 'cluster-arn',
1064+
TaskDefinition: 'job-id',
1065+
Overrides: {
1066+
ContainerOverrides: [
1067+
{
1068+
Name: 'cluster-name',
1069+
Environment: [
1070+
{
1071+
Name: 'TASK_TOKEN_ENV_VARIABLE',
1072+
'Value.$': '$$.Task.Token',
1073+
},
1074+
],
1075+
},
1076+
],
1077+
},
1078+
},
1079+
End: true,
1080+
},
1081+
},
1082+
},
1083+
});
1084+
1085+
// function name can be...
1086+
const lambda1 = 'a'; // name-only
1087+
const lambda2 = 'b:v1'; // name-only with alias
1088+
const lambda3 = 'arn:aws:lambda:us-west-2:1234567890:function:c'; // full arn
1089+
const lambda4 = '1234567890:function:d'; // partial arn
1090+
1091+
const sns1 = 'arn:aws:sns:us-east-1:1234567890:foo';
1092+
const sns2 = 'arn:aws:sns:us-east-2:#{AWS::AccountId}:bar';
1093+
1094+
const sqs1 = 'https://sqs.us-east-1.amazonaws.com/1234567890/foo';
1095+
const sqs2 = 'https://sqs.us-east-2.amazonaws.com/#{AWS::AccountId}/bar';
1096+
1097+
const sqsArn1 = 'arn:aws:sqs:us-east-1:1234567890:foo';
1098+
const sqsArn2 = 'arn:aws:sqs:us-east-2:#{AWS::AccountId}:bar';
1099+
1100+
serverless.service.stepFunctions = {
1101+
stateMachines: {
1102+
myStateMachine1: getStateMachine('sm1', lambda1, lambda2, sns1, sqs1),
1103+
myStateMachine2: getStateMachine('sm2', lambda3, lambda4, sns2, sqs2),
1104+
},
1105+
};
1106+
1107+
serverlessStepFunctions.compileIamRole();
1108+
const statements = serverlessStepFunctions.serverless.service
1109+
.provider.compiledCloudFormationTemplate.Resources.IamRoleStateMachineExecution
1110+
.Properties.Policies[0].PolicyDocument.Statement;
1111+
1112+
const ecsPermissions = statements.filter(s =>
1113+
_.isEqual(s.Action, ['ecs:RunTask', 'ecs:StopTask', 'ecs:DescribeTasks', 'iam:PassRole'])
1114+
);
1115+
expect(ecsPermissions).to.have.lengthOf(1);
1116+
expect(ecsPermissions[0].Resource).to.equal('*');
1117+
1118+
const eventPermissions = statements.filter(s =>
1119+
_.isEqual(s.Action, ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'])
1120+
);
1121+
expect(eventPermissions).to.has.lengthOf(1);
1122+
expect(eventPermissions[0].Resource).to.deep.eq([{
1123+
'Fn::Join': [
1124+
':',
1125+
[
1126+
'arn:aws:events',
1127+
{ Ref: 'AWS::Region' },
1128+
{ Ref: 'AWS::AccountId' },
1129+
'rule/StepFunctionsGetEventsForECSTaskRule',
1130+
],
1131+
],
1132+
}]);
1133+
1134+
const snsPermissions = statements.filter(s => _.isEqual(s.Action, ['sns:Publish']));
1135+
expect(snsPermissions).to.have.lengthOf(1);
1136+
expect(snsPermissions[0].Resource).to.deep.eq([sns1, sns2]);
1137+
1138+
const sqsPermissions = statements.filter(s => _.isEqual(s.Action, ['sqs:SendMessage']));
1139+
expect(sqsPermissions).to.have.lengthOf(1);
1140+
expect(sqsPermissions[0].Resource).to.deep.eq([sqsArn1, sqsArn2]);
1141+
1142+
const lambdaPermissions = statements.filter(s =>
1143+
_.isEqual(s.Action, ['lambda:InvokeFunction']));
1144+
expect(lambdaPermissions).to.have.lengthOf(1);
1145+
1146+
const lambdaArns = [
1147+
{ 'Fn::Sub': 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:a' },
1148+
{ 'Fn::Sub': 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:b:v1' },
1149+
'arn:aws:lambda:us-west-2:1234567890:function:c',
1150+
{ 'Fn::Sub': 'arn:aws:lambda:${AWS::Region}:1234567890:function:d' },
1151+
];
1152+
expect(lambdaPermissions[0].Resource).to.deep.eq(lambdaArns);
1153+
});
10021154
});

0 commit comments

Comments
 (0)