diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts index 1d682e7db1cdf..8ff3e751c4fc9 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts @@ -1,5 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; +import { IAlias, IFunction, IVersion } from '@aws-cdk/aws-lambda'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -120,7 +121,7 @@ export class LambdaInvoke extends sfn.TaskStateBase { this.taskPolicies = [ new iam.PolicyStatement({ - resources: this.props.lambdaFunction.resourceArnsForGrantInvoke, + resources: this.determineResourceArnsForGrantInvoke(props.lambdaFunction), actions: ['lambda:InvokeFunction'], }), ]; @@ -161,6 +162,35 @@ export class LambdaInvoke extends sfn.TaskStateBase { }; } } + + /** + * Determine the ARN(s) to put into the resource field of the generated + * IAM policy based on the type of the provided lambda function. + * + * When invoking Lambda Versions, we need to grant permissions to all + * qualifiers. Otherwise in-flight StepFunction executions will fail with + * due to missing permissions. This is because a change of the referenced + * Version will cause the Policy to remove permissions for the previous + * Version - which is currently in-flight. + * + * @see https://github.com/aws/aws-cdk/issues/17515 + */ + private determineResourceArnsForGrantInvoke(lambdaFunction: IFunction): string[] { + if (isVersion(lambdaFunction)) { + return lambdaFunction.lambda.resourceArnsForGrantInvoke; + } + + return lambdaFunction.resourceArnsForGrantInvoke; + } +} + +/** + * Type guard to determine if a given `IFunction` implements IVersion + */ +function isVersion(lambdaFunction: IFunction | IAlias | IVersion): lambdaFunction is IVersion { + return !(lambdaFunction as IAlias).aliasName + && (lambdaFunction as IVersion).lambda + && Boolean((lambdaFunction as IVersion).version); } /** diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.qualifiers.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.qualifiers.ts new file mode 100644 index 0000000000000..cbc3be611bd4e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.qualifiers.ts @@ -0,0 +1,80 @@ +import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as integ from '@aws-cdk/integ-tests'; +import { LambdaInvoke } from '../../lib'; + + +/* + * Creates a state machine with a task state to invoke a Lambda function + * via Alias and Version qualifiers. + * + * The state machine creates a couple of Lambdas that pass results forward + * and into a Choice state that validates the output. + */ +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-lambda-invoke-integ'); + +const submitJobLambda = new Function(stack, 'submitJobLambda', { + code: Code.fromInline(`exports.handler = async () => { + return { + statusCode: '200', + body: 'hello, world!' + }; + };`), + runtime: Runtime.NODEJS_14_X, + handler: 'index.handler', +}); + +const submitJob = new LambdaInvoke(stack, 'Invoke Handler', { + lambdaFunction: submitJobLambda.currentVersion, + outputPath: '$.Payload', +}); + +const checkJobStateLambda = new Function(stack, 'checkJobStateLambda', { + code: Code.fromInline(`exports.handler = async function(event, context) { + return { + status: event.statusCode === '200' ? 'SUCCEEDED' : 'FAILED' + }; + };`), + runtime: Runtime.NODEJS_14_X, + handler: 'index.handler', +}); +const checkJobStateLambdaAlias = checkJobStateLambda.addAlias('IntegTest'); + +const checkJobState = new LambdaInvoke(stack, 'Check the job state', { + lambdaFunction: checkJobStateLambdaAlias, + resultSelector: { + status: sfn.JsonPath.stringAt('$.Payload.status'), + }, +}); + +const isComplete = new sfn.Choice(stack, 'Job Complete?'); +const jobFailed = new sfn.Fail(stack, 'Job Failed', { + cause: 'Job Failed', + error: 'Received a status that was not 200', +}); +const finalStatus = new sfn.Pass(stack, 'Final step'); + +const chain = sfn.Chain.start(submitJob) + .next(checkJobState) + .next( + isComplete + .when(sfn.Condition.stringEquals('$.status', 'FAILED'), jobFailed) + .when(sfn.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus), + ); + +const sm = new sfn.StateMachine(stack, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); + +new integ.IntegTest(app, 'StepFunctionsInvokeLambdaQualifiersTest', { + testCases: [stack], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/StepFunctionsInvokeLambdaQualifiersTestDefaultTestDeployAssert57F7A2BF.template.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/StepFunctionsInvokeLambdaQualifiersTestDefaultTestDeployAssert57F7A2BF.template.json new file mode 100644 index 0000000000000..9e26dfeeb6e64 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/StepFunctionsInvokeLambdaQualifiersTestDefaultTestDeployAssert57F7A2BF.template.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/aws-stepfunctions-tasks-lambda-invoke-integ.template.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/aws-stepfunctions-tasks-lambda-invoke-integ.template.json new file mode 100644 index 0000000000000..e4bd8804c3490 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/aws-stepfunctions-tasks-lambda-invoke-integ.template.json @@ -0,0 +1,345 @@ +{ + "Resources": { + "submitJobLambdaServiceRole4D897ABD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "submitJobLambdaEFB00F3C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async () => {\n return {\n statusCode: '200',\n body: 'hello, world!'\n };\n };" + }, + "Role": { + "Fn::GetAtt": [ + "submitJobLambdaServiceRole4D897ABD", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "submitJobLambdaServiceRole4D897ABD" + ] + }, + "submitJobLambdaCurrentVersionC751A4B121e776736cfe6b8b23a046fa837cb34d": { + "Type": "AWS::Lambda::Version", + "Properties": { + "FunctionName": { + "Ref": "submitJobLambdaEFB00F3C" + } + } + }, + "checkJobStateLambdaServiceRoleB8B57B65": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "checkJobStateLambda4618B7B7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) {\n return {\n status: event.statusCode === '200' ? 'SUCCEEDED' : 'FAILED'\n };\n };" + }, + "Role": { + "Fn::GetAtt": [ + "checkJobStateLambdaServiceRoleB8B57B65", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "checkJobStateLambdaServiceRoleB8B57B65" + ] + }, + "checkJobStateLambdaCurrentVersion7E54C1E7faaca3d64de2b71130611d5d83972ae1": { + "Type": "AWS::Lambda::Version", + "Properties": { + "FunctionName": { + "Ref": "checkJobStateLambda4618B7B7" + } + } + }, + "checkJobStateLambdaAliasIntegTestF2DEAA78": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": { + "Ref": "checkJobStateLambda4618B7B7" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "checkJobStateLambdaCurrentVersion7E54C1E7faaca3d64de2b71130611d5d83972ae1", + "Version" + ] + }, + "Name": "IntegTest" + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::FindInMap": [ + "ServiceprincipalMap", + { + "Ref": "AWS::Region" + }, + "states" + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "submitJobLambdaEFB00F3C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "submitJobLambdaEFB00F3C", + "Arn" + ] + }, + ":*" + ] + ] + }, + { + "Ref": "checkJobStateLambdaAliasIntegTestF2DEAA78" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Invoke Handler\",\"States\":{\"Invoke Handler\":{\"Next\":\"Check the job state\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Ref": "submitJobLambdaCurrentVersionC751A4B121e776736cfe6b8b23a046fa837cb34d" + }, + "\",\"Payload.$\":\"$\"}},\"Check the job state\":{\"Next\":\"Job Complete?\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"ResultSelector\":{\"status.$\":\"$.Payload.status\"},\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Ref": "checkJobStateLambdaAliasIntegTestF2DEAA78" + }, + "\",\"Payload.$\":\"$\"}},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Final step\"}]},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"Received a status that was not 200\",\"Cause\":\"Job Failed\"},\"Final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + }, + "Mappings": { + "ServiceprincipalMap": { + "af-south-1": { + "states": "states.af-south-1.amazonaws.com" + }, + "ap-east-1": { + "states": "states.ap-east-1.amazonaws.com" + }, + "ap-northeast-1": { + "states": "states.ap-northeast-1.amazonaws.com" + }, + "ap-northeast-2": { + "states": "states.ap-northeast-2.amazonaws.com" + }, + "ap-northeast-3": { + "states": "states.ap-northeast-3.amazonaws.com" + }, + "ap-south-1": { + "states": "states.ap-south-1.amazonaws.com" + }, + "ap-southeast-1": { + "states": "states.ap-southeast-1.amazonaws.com" + }, + "ap-southeast-2": { + "states": "states.ap-southeast-2.amazonaws.com" + }, + "ap-southeast-3": { + "states": "states.ap-southeast-3.amazonaws.com" + }, + "ca-central-1": { + "states": "states.ca-central-1.amazonaws.com" + }, + "cn-north-1": { + "states": "states.cn-north-1.amazonaws.com" + }, + "cn-northwest-1": { + "states": "states.cn-northwest-1.amazonaws.com" + }, + "eu-central-1": { + "states": "states.eu-central-1.amazonaws.com" + }, + "eu-north-1": { + "states": "states.eu-north-1.amazonaws.com" + }, + "eu-south-1": { + "states": "states.eu-south-1.amazonaws.com" + }, + "eu-south-2": { + "states": "states.eu-south-2.amazonaws.com" + }, + "eu-west-1": { + "states": "states.eu-west-1.amazonaws.com" + }, + "eu-west-2": { + "states": "states.eu-west-2.amazonaws.com" + }, + "eu-west-3": { + "states": "states.eu-west-3.amazonaws.com" + }, + "me-south-1": { + "states": "states.me-south-1.amazonaws.com" + }, + "sa-east-1": { + "states": "states.sa-east-1.amazonaws.com" + }, + "us-east-1": { + "states": "states.us-east-1.amazonaws.com" + }, + "us-east-2": { + "states": "states.us-east-2.amazonaws.com" + }, + "us-gov-east-1": { + "states": "states.us-gov-east-1.amazonaws.com" + }, + "us-gov-west-1": { + "states": "states.us-gov-west-1.amazonaws.com" + }, + "us-iso-east-1": { + "states": "states.amazonaws.com" + }, + "us-iso-west-1": { + "states": "states.amazonaws.com" + }, + "us-isob-east-1": { + "states": "states.amazonaws.com" + }, + "us-west-1": { + "states": "states.us-west-1.amazonaws.com" + }, + "us-west-2": { + "states": "states.us-west-2.amazonaws.com" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..588d7b269d34f --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"20.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/integ.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/integ.json new file mode 100644 index 0000000000000..3f24f16b7c4ea --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/integ.json @@ -0,0 +1,11 @@ +{ + "version": "20.0.0", + "testCases": { + "StepFunctionsInvokeLambdaQualifiersTest/DefaultTest": { + "stacks": [ + "aws-stepfunctions-tasks-lambda-invoke-integ" + ], + "assertionStack": "StepFunctionsInvokeLambdaQualifiersTestDefaultTestDeployAssert57F7A2BF" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..563ff414a905d --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/manifest.json @@ -0,0 +1,103 @@ +{ + "version": "20.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-stepfunctions-tasks-lambda-invoke-integ": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-stepfunctions-tasks-lambda-invoke-integ.template.json", + "validateOnSynth": false + }, + "metadata": { + "/aws-stepfunctions-tasks-lambda-invoke-integ/submitJobLambda/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "submitJobLambdaServiceRole4D897ABD" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/submitJobLambda/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "submitJobLambdaEFB00F3C" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/submitJobLambda/CurrentVersion/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "submitJobLambdaCurrentVersionC751A4B121e776736cfe6b8b23a046fa837cb34d" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "checkJobStateLambdaServiceRoleB8B57B65" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "checkJobStateLambda4618B7B7" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/CurrentVersion/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "checkJobStateLambdaCurrentVersion7E54C1E7faaca3d64de2b71130611d5d83972ae1" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/AliasIntegTest/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "checkJobStateLambdaAliasIntegTestF2DEAA78" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/StateMachine/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineRoleB840431D" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/StateMachine/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachineRoleDefaultPolicyDF1E6607" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/StateMachine/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StateMachine2E01A3A5" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/stateMachineArn": [ + { + "type": "aws:cdk:logicalId", + "data": "stateMachineArn" + } + ], + "/aws-stepfunctions-tasks-lambda-invoke-integ/Service-principalMap": [ + { + "type": "aws:cdk:logicalId", + "data": "ServiceprincipalMap" + } + ] + }, + "displayName": "aws-stepfunctions-tasks-lambda-invoke-integ" + }, + "StepFunctionsInvokeLambdaQualifiersTestDefaultTestDeployAssert57F7A2BF": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "StepFunctionsInvokeLambdaQualifiersTestDefaultTestDeployAssert57F7A2BF.template.json", + "validateOnSynth": false + }, + "displayName": "StepFunctionsInvokeLambdaQualifiersTest/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/tree.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/tree.json new file mode 100644 index 0000000000000..b7bfe60341be9 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.qualifiers.integ.snapshot/tree.json @@ -0,0 +1,541 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.33" + } + }, + "aws-stepfunctions-tasks-lambda-invoke-integ": { + "id": "aws-stepfunctions-tasks-lambda-invoke-integ", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ", + "children": { + "submitJobLambda": { + "id": "submitJobLambda", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/submitJobLambda", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/submitJobLambda/ServiceRole", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/submitJobLambda/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/submitJobLambda/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "zipFile": "exports.handler = async () => {\n return {\n statusCode: '200',\n body: 'hello, world!'\n };\n };" + }, + "role": { + "Fn::GetAtt": [ + "submitJobLambdaServiceRole4D897ABD", + "Arn" + ] + }, + "handler": "index.handler", + "runtime": "nodejs14.x" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnFunction", + "version": "0.0.0" + } + }, + "CurrentVersion": { + "id": "CurrentVersion", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/submitJobLambda/CurrentVersion", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/submitJobLambda/CurrentVersion/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Version", + "aws:cdk:cloudformation:props": { + "functionName": { + "Ref": "submitJobLambdaEFB00F3C" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnVersion", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Version", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Function", + "version": "0.0.0" + } + }, + "Invoke Handler": { + "id": "Invoke Handler", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/Invoke Handler", + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.TaskStateBase", + "version": "0.0.0" + } + }, + "checkJobStateLambda": { + "id": "checkJobStateLambda", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/ServiceRole", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "zipFile": "exports.handler = async function(event, context) {\n return {\n status: event.statusCode === '200' ? 'SUCCEEDED' : 'FAILED'\n };\n };" + }, + "role": { + "Fn::GetAtt": [ + "checkJobStateLambdaServiceRoleB8B57B65", + "Arn" + ] + }, + "handler": "index.handler", + "runtime": "nodejs14.x" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnFunction", + "version": "0.0.0" + } + }, + "CurrentVersion": { + "id": "CurrentVersion", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/CurrentVersion", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/CurrentVersion/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Version", + "aws:cdk:cloudformation:props": { + "functionName": { + "Ref": "checkJobStateLambda4618B7B7" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnVersion", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Version", + "version": "0.0.0" + } + }, + "AliasIntegTest": { + "id": "AliasIntegTest", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/AliasIntegTest", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/AliasIntegTest/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Alias", + "aws:cdk:cloudformation:props": { + "functionName": { + "Ref": "checkJobStateLambda4618B7B7" + }, + "functionVersion": { + "Fn::GetAtt": [ + "checkJobStateLambdaCurrentVersion7E54C1E7faaca3d64de2b71130611d5d83972ae1", + "Version" + ] + }, + "name": "IntegTest" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnAlias", + "version": "0.0.0" + } + }, + "ScalingRole": { + "id": "ScalingRole", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/checkJobStateLambda/AliasIntegTest/ScalingRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Alias", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Function", + "version": "0.0.0" + } + }, + "Check the job state": { + "id": "Check the job state", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/Check the job state", + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.TaskStateBase", + "version": "0.0.0" + } + }, + "Job Complete?": { + "id": "Job Complete?", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/Job Complete?", + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.Choice", + "version": "0.0.0" + } + }, + "Job Failed": { + "id": "Job Failed", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/Job Failed", + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.Fail", + "version": "0.0.0" + } + }, + "Final step": { + "id": "Final step", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/Final step", + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.Pass", + "version": "0.0.0" + } + }, + "StateMachine": { + "id": "StateMachine", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/StateMachine", + "children": { + "Role": { + "id": "Role", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/StateMachine/Role", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/StateMachine/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::FindInMap": [ + "ServiceprincipalMap", + { + "Ref": "AWS::Region" + }, + "states" + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/StateMachine/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/StateMachine/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "submitJobLambdaEFB00F3C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "submitJobLambdaEFB00F3C", + "Arn" + ] + }, + ":*" + ] + ] + }, + { + "Ref": "checkJobStateLambdaAliasIntegTestF2DEAA78" + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "StateMachineRoleDefaultPolicyDF1E6607", + "roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/StateMachine/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::StepFunctions::StateMachine", + "aws:cdk:cloudformation:props": { + "roleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "definitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Invoke Handler\",\"States\":{\"Invoke Handler\":{\"Next\":\"Check the job state\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Ref": "submitJobLambdaCurrentVersionC751A4B121e776736cfe6b8b23a046fa837cb34d" + }, + "\",\"Payload.$\":\"$\"}},\"Check the job state\":{\"Next\":\"Job Complete?\",\"Retry\":[{\"ErrorEquals\":[\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2}],\"Type\":\"Task\",\"ResultSelector\":{\"status.$\":\"$.Payload.status\"},\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Ref": "checkJobStateLambdaAliasIntegTestF2DEAA78" + }, + "\",\"Payload.$\":\"$\"}},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Final step\"}]},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"Received a status that was not 200\",\"Cause\":\"Job Failed\"},\"Final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}" + ] + ] + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.CfnStateMachine", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-stepfunctions.StateMachine", + "version": "0.0.0" + } + }, + "stateMachineArn": { + "id": "stateMachineArn", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/stateMachineArn", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "Service-principalMap": { + "id": "Service-principalMap", + "path": "aws-stepfunctions-tasks-lambda-invoke-integ/Service-principalMap", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnMapping", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "StepFunctionsInvokeLambdaQualifiersTest": { + "id": "StepFunctionsInvokeLambdaQualifiersTest", + "path": "StepFunctionsInvokeLambdaQualifiersTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "StepFunctionsInvokeLambdaQualifiersTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "StepFunctionsInvokeLambdaQualifiersTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.33" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "StepFunctionsInvokeLambdaQualifiersTest/DefaultTest/DeployAssert", + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts index c76288702488f..7851fba00b040 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts @@ -1,3 +1,4 @@ +import { Match, Template } from '@aws-cdk/assertions'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; @@ -391,4 +392,90 @@ describe('LambdaInvoke', () => { }); }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,WAIT_FOR_TASK_TOKEN. Received: RUN_JOB/); }); + + describe('TaskPolicy Resources', () => { + + test('invoke a Function', () => { + // WHEN + const task = new LambdaInvoke(stack, 'Task', { + lambdaFunction, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Alias', 0); + Template.fromStack(stack).resourceCountIs('AWS::Lambda::Version', 0); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: Match.objectLike({ + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [Match.anyValue(), Match.anyValue()], + }, + ], + }), + }); + }); + + test('invoke an Alias', () => { + // WHEN + const alias = lambdaFunction.addAlias('test'); + const task = new LambdaInvoke(stack, 'Task', { + lambdaFunction: alias, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Alias', { + Name: 'test', + }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: Match.objectLike({ + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: { + Ref: Match.anyValue(), + }, + }, + ], + }), + }); + }); + + test('invoke a Version', () => { + // WHEN + const version = lambdaFunction.currentVersion; + const task = new LambdaInvoke(stack, 'Task', { + lambdaFunction: version, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Version', { + FunctionName: { + Ref: 'Fn9270CBC0', + }, + }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: Match.objectLike({ + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [Match.anyValue(), Match.anyValue()], + }, + ], + }), + }); + }); + }); });