diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 568e7f957f2e7..600ee8fdd03af 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -28,6 +28,8 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Evaluate Expression](#evaluate-expression) - [Batch](#batch) - [SubmitJob](#submitjob) +- [CodeBuild](#codebuild) + - [StartBuild](#startbuild) - [DynamoDB](#dynamodb) - [GetItem](#getitem) - [PutItem](#putitem) @@ -235,6 +237,45 @@ const task = new tasks.BatchSubmitJob(this, 'Submit Job', { }); ``` +## CodeBuild + +Step Functions supports [CodeBuild](https://docs.aws.amazon.com/step-functions/latest/dg/connect-codebuild.html) through the service integration pattern. + +### StartBuild + +[StartBuild](https://docs.aws.amazon.com/codebuild/latest/APIReference/API_StartBuild.html) starts a CodeBuild Project by Project Name. + +```ts +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; + +const codebuildProject = new codebuild.Project(stack, 'Project', { + projectName: 'MyTestProject', + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + build: { + commands: [ + 'echo "Hello, CodeBuild!"', + ], + }, + }, + }), +}); + +const task = new tasks.CodeBuildStartBuild(stack, 'Task', { + project: codebuildProject, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + environmentVariablesOverride: { + ZONE: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.stringAt('$.envVariables.zone'), + }, + }, +}); +``` + ## DynamoDB You can call DynamoDB APIs from a `Task` state. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/codebuild/start-build.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/codebuild/start-build.ts new file mode 100644 index 0000000000000..f4341a3e7c521 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/codebuild/start-build.ts @@ -0,0 +1,109 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for CodeBuildStartBuild + */ +export interface CodeBuildStartBuildProps extends sfn.TaskStateBaseProps { + /** + * CodeBuild project to start + */ + readonly project: codebuild.IProject; + /** + * A set of environment variables to be used for this build only. + * + * @default - the latest environment variables already defined in the build project. + */ + readonly environmentVariablesOverride?: { [name: string]: codebuild.BuildEnvironmentVariable }; +} + +/** + * Start a CodeBuild Build as a task + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-codebuild.html + */ +export class CodeBuildStartBuild extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: cdk.Construct, id: string, private readonly props: CodeBuildStartBuildProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, CodeBuildStartBuild.SUPPORTED_INTEGRATION_PATTERNS); + + this.taskMetrics = { + metricPrefixSingular: 'CodeBuildProject', + metricPrefixPlural: 'CodeBuildProjects', + metricDimensions: { + ProjectArn: this.props.project.projectArn, + }, + }; + + this.taskPolicies = this.configurePolicyStatements(); + } + + private configurePolicyStatements(): iam.PolicyStatement[] { + let policyStatements = [ + new iam.PolicyStatement({ + resources: [this.props.project.projectArn], + actions: [ + 'codebuild:StartBuild', + 'codebuild:StopBuild', + ], + }), + ]; + + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + cdk.Stack.of(this).formatArn({ + service: 'events', + resource: 'rule/StepFunctionsGetEventForCodeBuildStartBuildRule', + }), + ], + }), + ); + } + + return policyStatements; + } + + /** + * Provides the CodeBuild StartBuild service integration task configuration + */ + /** + * @internal + */ + protected _renderTask(): any { + return { + Resource: integrationResourceArn('codebuild', 'startBuild', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ProjectName: this.props.project.projectName, + EnvironmentVariablesOverride: this.props.environmentVariablesOverride + ? this.serializeEnvVariables(this.props.environmentVariablesOverride) + : undefined, + }), + }; + } + + private serializeEnvVariables(environmentVariables: { [name: string]: codebuild.BuildEnvironmentVariable }) { + return Object.keys(environmentVariables).map(name => ({ + Name: name, + Type: environmentVariables[name].type || codebuild.BuildEnvironmentVariableType.PLAINTEXT, + Value: environmentVariables[name].value, + })); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index ee034e389ad96..a0e97e9df77ca 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -34,3 +34,4 @@ export * from './dynamodb/put-item'; export * from './dynamodb/update-item'; export * from './dynamodb/delete-item'; export * from './dynamodb/shared-types'; +export * from './codebuild/start-build'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 6196dd98e9a72..822215fd74708 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -71,6 +71,7 @@ "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-codebuild": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", @@ -92,6 +93,7 @@ "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-codebuild": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json new file mode 100644 index 0000000000000..9720f657969e2 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json @@ -0,0 +1,259 @@ +{ + "Resources": { + "ProjectRole4CCB274E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ProjectRoleDefaultPolicy7F29461B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "ProjectC78D97AD" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "ProjectC78D97AD" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":report-group/", + { + "Ref": "ProjectC78D97AD" + }, + "-*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ProjectRoleDefaultPolicy7F29461B", + "Roles": [ + { + "Ref": "ProjectRole4CCB274E" + } + ] + } + }, + "ProjectC78D97AD": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "EnvironmentVariables": [ + { + "Name": "zone", + "Type": "PLAINTEXT", + "Value": "defaultZone" + } + ], + "Image": "aws/codebuild/standard:1.0", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "ProjectRole4CCB274E", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"echo \\\"Hello, CodeBuild!\\\"\"\n ]\n }\n }\n}", + "Type": "NO_SOURCE" + }, + "Name": "MyTestProject" + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ProjectC78D97AD", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"bar\":\"SomeValue\"},\"Next\":\"build-task\"},\"build-task\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::codebuild:startBuild\",\"Parameters\":{\"ProjectName\":\"", + { + "Ref": "ProjectC78D97AD" + }, + "\",\"EnvironmentVariablesOverride\":[{\"Name\":\"ZONE\",\"Type\":\"PLAINTEXT\",\"Value.$\":\"$.envVariables.zone\"}]}}}}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "ProjectName": { + "Value": { + "Ref": "ProjectC78D97AD" + } + }, + "StateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.ts new file mode 100644 index 0000000000000..bf33a46907833 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.ts @@ -0,0 +1,68 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * * aws codebuild list-builds-for-project --project-name : should return a list of projects with size greater than 0 + * * + * * aws codebuild batch-get-builds --ids --query 'builds[0].buildStatus': wait until the status is 'SUCCEEDED' + * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED + */ + +class StartBuildStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props: cdk.StackProps = {}) { + super(scope, id, props); + + let project = new codebuild.Project(this, 'Project', { + projectName: 'MyTestProject', + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + build: { + commands: [ + 'echo "Hello, CodeBuild!"', + ], + }, + }, + }), + environmentVariables: { + zone: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: 'defaultZone', + }, + }, + }); + + let startBuild = new tasks.CodeBuildStartBuild(this, 'build-task', { + project: project, + environmentVariablesOverride: { + ZONE: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.stringAt('$.envVariables.zone'), + }, + }, + }); + + const definition = new sfn.Pass(this, 'Start', { + result: sfn.Result.fromObject({ bar: 'SomeValue' }), + }).next(startBuild); + + const stateMachine = new sfn.StateMachine(this, 'StateMachine', { + definition, + }); + + new cdk.CfnOutput(this, 'ProjectName', { + value: project.projectName, + }); + new cdk.CfnOutput(this, 'StateMachineArn', { + value: stateMachine.stateMachineArn, + }); + } +} + +const app = new cdk.App(); +new StartBuildStack(app, 'aws-stepfunctions-tasks-codebuild-start-build-integ'); +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/start-build.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/start-build.test.ts new file mode 100644 index 0000000000000..0c71117392c5e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/start-build.test.ts @@ -0,0 +1,157 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { CodeBuildStartBuild } from '../../lib'; + +let stack: cdk.Stack; +let codebuildProject: codebuild.Project; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + + codebuildProject = new codebuild.Project(stack, 'Project', { + projectName: 'MyTestProject', + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + build: { + commands: [ + 'echo "Hello, CodeBuild!"', + ], + }, + }, + }), + }); +}); + +test('Task with only the required parameters', () => { + // WHEN + const task = new CodeBuildStartBuild(stack, 'Task', { + project: codebuildProject, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::codebuild:startBuild.sync', + ], + ], + }, + End: true, + Parameters: { + ProjectName: { + Ref: 'ProjectC78D97AD', + }, + }, + }); +}); + +test('Task with all the parameters', () => { + // WHEN + const task = new CodeBuildStartBuild(stack, 'Task', { + project: codebuildProject, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + environmentVariablesOverride: { + env: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: 'prod', + }, + }, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::codebuild:startBuild.sync', + ], + ], + }, + End: true, + Parameters: { + ProjectName: { + Ref: 'ProjectC78D97AD', + }, + EnvironmentVariablesOverride: [ + { + Name: 'env', + Type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + Value: 'prod', + }, + ], + }, + }); +}); + +test('supports tokens', () => { + // WHEN + const task = new CodeBuildStartBuild(stack, 'Task', { + project: codebuildProject, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + environmentVariablesOverride: { + ZONE: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.stringAt('$.envVariables.zone'), + }, + }, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::codebuild:startBuild.sync', + ], + ], + }, + End: true, + Parameters: { + ProjectName: { + Ref: 'ProjectC78D97AD', + }, + EnvironmentVariablesOverride: [ + { + 'Name': 'ZONE', + 'Type': codebuild.BuildEnvironmentVariableType.PLAINTEXT, + 'Value.$': '$.envVariables.zone', + }, + ], + }, + }); +}); + + +test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { + expect(() => { + new CodeBuildStartBuild(stack, 'Task', { + project: codebuildProject, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow( + /Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/, + ); +});