diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index 7eff891a9ca59..9f35a81656b56 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -600,3 +600,20 @@ new codebuild.Project(stack, 'MyProject', { Here's a CodeBuild project with a simple example that creates a project mounted on AWS EFS: [Minimal Example](./test/integ.project-file-system-location.ts) + +## Batch builds + +To enable batch builds you should call `enableBatchBuilds()` on the project instance. + +It returns an object containing the batch service role that was created, +or `undefined` if batch builds could not be enabled, for example if the project was imported. + +```ts +import * as codebuild from '@aws-cdk/aws-codebuild'; + +const project = new codebuild.Project(this, 'MyProject', { ... }); + +if (project.enableBatchBuilds()) { + console.log('Batch builds were enabled'); +} +``` diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 796422b379782..c98718bd744bf 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -28,6 +28,14 @@ import { CODEPIPELINE_SOURCE_ARTIFACTS_TYPE, NO_SOURCE_TYPE } from './source-typ // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; +/** + * The type returned from {@link IProject#enableBatchBuilds}. + */ +export interface BatchBuildConfig { + /** The IAM batch service Role of this Project. */ + readonly role: iam.IRole; +} + export interface IProject extends IResource, iam.IGrantable, ec2.IConnectable { /** * The ARN of this Project. @@ -44,6 +52,14 @@ export interface IProject extends IResource, iam.IGrantable, ec2.IConnectable { /** The IAM service Role of this Project. Undefined for imported Projects. */ readonly role?: iam.IRole; + /** + * Enable batch builds. + * + * Returns an object contining the batch service role if batch builds + * could be enabled. + */ + enableBatchBuilds(): BatchBuildConfig | undefined; + addToRolePolicy(policyStatement: iam.PolicyStatement): void; /** @@ -196,6 +212,10 @@ abstract class ProjectBase extends Resource implements IProject { return this._connections; } + public enableBatchBuilds(): BatchBuildConfig | undefined { + return undefined; + } + /** * Add a permission only if there's a policy attached. * @param statement The permissions statement to add @@ -729,6 +749,7 @@ export class Project extends ProjectBase { private readonly _secondaryArtifacts: CfnProject.ArtifactsProperty[]; private _encryptionKey?: kms.IKey; private readonly _fileSystemLocations: CfnProject.ProjectFileSystemLocationProperty[]; + private _batchServiceRole?: iam.Role; constructor(scope: Construct, id: string, props: ProjectProps) { super(scope, id, { @@ -813,6 +834,14 @@ export class Project extends ProjectBase { sourceVersion: sourceConfig.sourceVersion, vpcConfig: this.configureVpc(props), logsConfig: this.renderLoggingConfiguration(props.logging), + buildBatchConfig: Lazy.any({ + produce: () => { + const config: CfnProject.ProjectBuildBatchConfigProperty | undefined = this._batchServiceRole ? { + serviceRole: this._batchServiceRole.roleArn, + } : undefined; + return config; + }, + }), }); this.addVpcRequiredPermissions(props, resource); @@ -853,6 +882,27 @@ export class Project extends ProjectBase { } } + public enableBatchBuilds(): BatchBuildConfig | undefined { + if (!this._batchServiceRole) { + this._batchServiceRole = new iam.Role(this, 'BatchServiceRole', { + assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'), + }); + this._batchServiceRole.addToPrincipalPolicy(new iam.PolicyStatement({ + resources: [Lazy.string({ + produce: () => this.projectArn, + })], + actions: [ + 'codebuild:StartBuild', + 'codebuild:StopBuild', + 'codebuild:RetryBuild', + ], + })); + } + return { + role: this._batchServiceRole, + }; + } + /** * Adds a secondary source to the Project. * @@ -1139,8 +1189,8 @@ export class Project extends ProjectBase { return undefined; } - let s3Config: CfnProject.S3LogsConfigProperty|undefined = undefined; - let cloudwatchConfig: CfnProject.CloudWatchLogsConfigProperty|undefined = undefined; + let s3Config: CfnProject.S3LogsConfigProperty | undefined = undefined; + let cloudwatchConfig: CfnProject.CloudWatchLogsConfigProperty | undefined = undefined; if (props.s3) { const s3Logs = props.s3; @@ -1350,10 +1400,10 @@ export interface IBuildImage { } /** Optional arguments to {@link IBuildImage.binder} - currently empty. */ -export interface BuildImageBindOptions {} +export interface BuildImageBindOptions { } /** The return type from {@link IBuildImage.binder} - currently empty. */ -export interface BuildImageConfig {} +export interface BuildImageConfig { } // @deprecated(not in tsdoc on purpose): add bind() to IBuildImage // and get rid of IBindableBuildImage diff --git a/packages/@aws-cdk/aws-codebuild/lib/source.ts b/packages/@aws-cdk/aws-codebuild/lib/source.ts index b8002f2db9e36..7706328a92bc7 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/source.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/source.ts @@ -485,6 +485,8 @@ interface ThirdPartyGitSourceProps extends GitSourceProps { /** * Trigger a batch build from a webhook instead of a standard one. * + * Enabling this will enable batch builds on the CodeBuild project. + * * @default false */ readonly webhookTriggersBatchBuild?: boolean; @@ -518,7 +520,7 @@ abstract class ThirdPartyGitSource extends GitSource { this.webhookTriggersBatchBuild = props.webhookTriggersBatchBuild; } - public bind(_scope: CoreConstruct, _project: IProject): SourceConfig { + public bind(_scope: CoreConstruct, project: IProject): SourceConfig { const anyFilterGroupsProvided = this.webhookFilters.length > 0; const webhook = this.webhook === undefined ? (anyFilterGroupsProvided ? true : undefined) : this.webhook; @@ -530,7 +532,12 @@ abstract class ThirdPartyGitSource extends GitSource { throw new Error('`webhookTriggersBatchBuild` cannot be used when `webhook` is `false`'); } - const superConfig = super.bind(_scope, _project); + const superConfig = super.bind(_scope, project); + + if (this.webhookTriggersBatchBuild) { + project.enableBatchBuilds(); + } + return { sourceProperty: { ...superConfig.sourceProperty, diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.expected.json new file mode 100644 index 0000000000000..b58305bddf2f3 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.expected.json @@ -0,0 +1,188 @@ +{ + "Resources": { + "MyProjectRole9BBE5233": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyProjectRoleDefaultPolicyB19B7C29": { + "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": "MyProject39F7B0AE" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectRoleDefaultPolicyB19B7C29", + "Roles": [ + { + "Ref": "MyProjectRole9BBE5233" + } + ] + } + }, + "MyProjectBatchServiceRole6B35CF0E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyProjectBatchServiceRoleDefaultPolicy7A0E5721": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:StartBuild", + "codebuild:StopBuild", + "codebuild:RetryBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyProject39F7B0AE", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectBatchServiceRoleDefaultPolicy7A0E5721", + "Roles": [ + { + "Ref": "MyProjectBatchServiceRole6B35CF0E" + } + ] + } + }, + "MyProject39F7B0AE": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectRole9BBE5233", + "Arn" + ] + }, + "Source": { + "Location": "https://github.com/aws/aws-cdk.git", + "ReportBuildStatus": false, + "Type": "GITHUB" + }, + "BuildBatchConfig": { + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectBatchServiceRole6B35CF0E", + "Arn" + ] + } + }, + "EncryptionKey": "alias/aws/s3", + "Triggers": { + "BuildType": "BUILD_BATCH", + "FilterGroups": [ + [ + { + "Pattern": "PUSH", + "Type": "EVENT" + } + ] + ], + "Webhook": true + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.ts b/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.ts new file mode 100644 index 0000000000000..d8fd006956035 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.ts @@ -0,0 +1,29 @@ +import * as cdk from '@aws-cdk/core'; +import * as codebuild from '../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string) { + super(scope, id); + + const source = codebuild.Source.gitHub({ + owner: 'aws', + repo: 'aws-cdk', + reportBuildStatus: false, + webhook: true, + webhookTriggersBatchBuild: true, + webhookFilters: [ + codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH), + ], + }); + new codebuild.Project(this, 'MyProject', { + source, + grantReportGroupPermissions: false, + }); + } +} + +const app = new cdk.App(); + +new TestStack(app, 'test-codebuild-github-webhook-batch'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index 5a7766918c67b..d6341771d71b7 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -712,6 +712,51 @@ export = { Webhook: true, BuildType: 'BUILD_BATCH', }, + BuildBatchConfig: { + ServiceRole: { + 'Fn::GetAtt': [ + 'ProjectBatchServiceRoleF97A1CFB', + 'Arn', + ], + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'codebuild:StartBuild', + 'codebuild:StopBuild', + 'codebuild:RetryBuild', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'ProjectC78D97AD', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, })); test.done(); @@ -1275,12 +1320,11 @@ export = { }); expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { - 'Artifacts': - { - 'Name': ABSENT, - 'ArtifactIdentifier': 'artifact1', - 'OverrideArtifactName': true, - }, + 'Artifacts': { + 'Name': ABSENT, + 'ArtifactIdentifier': 'artifact1', + 'OverrideArtifactName': true, + }, })); test.done(); @@ -1302,12 +1346,11 @@ export = { }); expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { - 'Artifacts': - { - 'ArtifactIdentifier': 'artifact1', - 'Name': 'specificname', - 'OverrideArtifactName': ABSENT, - }, + 'Artifacts': { + 'ArtifactIdentifier': 'artifact1', + 'Name': 'specificname', + 'OverrideArtifactName': ABSENT, + }, })); test.done(); @@ -1481,7 +1524,7 @@ export = { '', [ '111', - { twotwotwo: '222' }, + { twotwotwo: '222' }, ], ], }, @@ -1862,4 +1905,70 @@ export = { }, }, }, + + 'enableBatchBuilds()'(test: Test) { + const stack = new cdk.Stack(); + + const project = new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'testowner', + repo: 'testrepo', + }), + }); + + const returnVal = project.enableBatchBuilds(); + if (!returnVal?.role) { + throw new Error('Expecting return value with role'); + } + + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + BuildBatchConfig: { + ServiceRole: { + 'Fn::GetAtt': [ + 'ProjectBatchServiceRoleF97A1CFB', + 'Arn', + ], + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'codebuild:StartBuild', + 'codebuild:StopBuild', + 'codebuild:RetryBuild', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'ProjectC78D97AD', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index 468684664cc5f..238efebae4658 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -97,6 +97,8 @@ export interface CodeBuildActionProps extends codepipeline.CommonAwsActionProps /** * Trigger a batch build. * + * Enabling this will enable batch builds on the CodeBuild project. + * * @default false */ readonly executeBatchBuild?: boolean; @@ -213,6 +215,7 @@ export class CodeBuildAction extends Action { } if (this.props.executeBatchBuild) { configuration.BatchEnabled = 'true'; + this.props.project.enableBatchBuilds(); } return { configuration, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json index d489a5712eeba..13357ba22c85b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json @@ -480,8 +480,63 @@ "Source": { "Type": "CODEPIPELINE" }, + "BuildBatchConfig": { + "ServiceRole": { + "Fn::GetAtt": [ + "MyBuildProjectBatchServiceRole531F3056", + "Arn" + ] + } + }, "EncryptionKey": "alias/aws/s3" } + }, + "MyBuildProjectBatchServiceRole531F3056": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyBuildProjectBatchServiceRoleDefaultPolicy816785FC": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:StartBuild", + "codebuild:StopBuild", + "codebuild:RetryBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyBuildProject30DB9D6E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyBuildProjectBatchServiceRoleDefaultPolicy816785FC", + "Roles": [ + { + "Ref": "MyBuildProjectBatchServiceRole531F3056" + } + ] + } } } }