diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index c51b263279e34..8dc9d1db9f9ec 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -305,6 +305,38 @@ new codebuild.Project(this, 'Project', { }) ``` +## Logs + +CodeBuild lets you specify an S3 Bucket, CloudWatch Log Group or both to receive logs from your projects. + +By default, logs will go to cloudwatch. + +### CloudWatch Logs Example + +```typescript +new codebuild.Project(this, 'Project', { + logging: { + cloudWatch: { + logGroup: new cloudwatch.LogGroup(this, `MyLogGroup`), + } + }, + ... +}) +``` + +### S3 Logs Example + +```typescript +new codebuild.Project(this, 'Project', { + logging: { + s3: { + bucket: new s3.Bucket(this, `LogBucket`) + } + }, + ... +}) +``` + ## Credentials CodeBuild allows you to store credentials used when communicating with various sources, diff --git a/packages/@aws-cdk/aws-codebuild/lib/index.ts b/packages/@aws-cdk/aws-codebuild/lib/index.ts index d1d0907ec734d..96731b2130043 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/index.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/index.ts @@ -1,6 +1,7 @@ export * from './events'; export * from './pipeline-project'; export * from './project'; +export * from './project-logs'; export * from './report-group'; export * from './source'; export * from './source-credentials'; diff --git a/packages/@aws-cdk/aws-codebuild/lib/project-logs.ts b/packages/@aws-cdk/aws-codebuild/lib/project-logs.ts new file mode 100644 index 0000000000000..d2613462d6330 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/lib/project-logs.ts @@ -0,0 +1,78 @@ +import * as logs from '@aws-cdk/aws-logs'; +import * as s3 from '@aws-cdk/aws-s3'; + +/** + * Information about logs built to an S3 bucket for a build project. + */ +export interface S3LoggingOptions { + /** + * Encrypt the S3 build log output + * + * @default true + */ + readonly encrypted?: boolean; + + /** + * The S3 Bucket to send logs to + */ + readonly bucket: s3.IBucket; + + /** + * The path prefix for S3 logs + * + * @default - no prefix + */ + readonly prefix?: string; + + /** + * The current status of the logs in Amazon CloudWatch Logs for a build project + * + * @default true + */ + readonly enabled?: boolean; +} + +/** + * Information about logs built to a CloudWatch Log Group for a build project. + */ +export interface CloudWatchLoggingOptions { + /** + * The Log Group to send logs to + * + * @default - no log group specified + */ + readonly logGroup?: logs.ILogGroup; + + /** + * The prefix of the stream name of the Amazon CloudWatch Logs + * + * @default - no prefix + */ + readonly prefix?: string; + + /** + * The current status of the logs in Amazon CloudWatch Logs for a build project + * + * @default true + */ + readonly enabled?: boolean; +} + +/** + * Information about logs for the build project. A project can create logs in Amazon CloudWatch Logs, an S3 bucket, or both. + */ +export interface LoggingOptions { + /** + * Information about logs built to an S3 bucket for a build project. + * + * @default - disabled + */ + readonly s3?: S3LoggingOptions; + + /** + * Information about Amazon CloudWatch Logs for a build project. + * + * @default - enabled + */ + readonly cloudWatch?: CloudWatchLoggingOptions; +} diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 8bfc8f38e09cf..8973dc8f5ea04 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -18,6 +18,7 @@ import { IFileSystemLocation } from './file-location'; import { NoArtifacts } from './no-artifacts'; import { NoSource } from './no-source'; import { runScriptLinuxBuildSpec, S3_BUCKET_ENV, S3_KEY_ENV } from './private/run-script-linux-build-spec'; +import { LoggingOptions } from './project-logs'; import { renderReportGroupArn } from './report-group-utils'; import { ISource } from './source'; import { CODEPIPELINE_SOURCE_ARTIFACTS_TYPE, NO_SOURCE_TYPE } from './source-types'; @@ -538,6 +539,13 @@ export interface CommonProjectProps { * @see https://docs.aws.amazon.com/codebuild/latest/userguide/test-report-group-naming.html */ readonly grantReportGroupPermissions?: boolean; + + /** + * Information about logs for the build project. A project can create logs in Amazon CloudWatch Logs, an S3 bucket, or both. + * + * @default - no log configuration is set + */ + readonly logging?: LoggingOptions; } export interface ProjectProps extends CommonProjectProps { @@ -771,6 +779,7 @@ export class Project extends ProjectBase { triggers: sourceConfig.buildTriggers, sourceVersion: sourceConfig.sourceVersion, vpcConfig: this.configureVpc(props), + logsConfig: this.renderLoggingConfiguration(props.logging), }); this.addVpcRequiredPermissions(props, resource); @@ -1036,6 +1045,44 @@ export class Project extends ProjectBase { }; } + private renderLoggingConfiguration(props: LoggingOptions | undefined): CfnProject.LogsConfigProperty | undefined { + if (props === undefined) { + return undefined; + }; + + let s3Config: CfnProject.S3LogsConfigProperty|undefined = undefined; + let cloudwatchConfig: CfnProject.CloudWatchLogsConfigProperty|undefined = undefined; + + if (props.s3) { + const s3Logs = props.s3; + s3Config = { + status: (s3Logs.enabled ?? true) ? 'ENABLED' : 'DISABLED', + location: `${s3Logs.bucket.bucketName}/${s3Logs.prefix}`, + encryptionDisabled: s3Logs.encrypted, + }; + } + + if (props.cloudWatch) { + const cloudWatchLogs = props.cloudWatch; + const status = (cloudWatchLogs.enabled ?? true) ? 'ENABLED' : 'DISABLED'; + + if (status === 'ENABLED' && !(cloudWatchLogs.logGroup)) { + throw new Error('Specifying a LogGroup is required if CloudWatch logging for CodeBuild is enabled'); + } + + cloudwatchConfig = { + status, + groupName: cloudWatchLogs.logGroup?.logGroupName, + streamName: cloudWatchLogs.prefix, + }; + } + + return { + s3Logs: s3Config, + cloudWatchLogs: cloudwatchConfig, + }; + } + private addVpcRequiredPermissions(props: ProjectProps, project: CfnProject): void { if (!props.vpc || !this.role) { return; diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 206e810609e9d..1c7ff7ae66cd3 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -96,6 +96,7 @@ "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", @@ -114,6 +115,7 @@ "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/test/test.project.ts b/packages/@aws-cdk/aws-codebuild/test/test.project.ts index f892f8b30eac2..439427588afdb 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.project.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.project.ts @@ -1,6 +1,7 @@ import { countResources, expect, haveResource, haveResourceLike, objectLike, not, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; @@ -600,5 +601,141 @@ export = { test.done(); }, + + 'logs config - cloudWatch'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const logGroup = logs.LogGroup.fromLogGroupName(stack, 'LogGroup', 'MyLogGroupName'); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.s3({ + bucket: new s3.Bucket(stack, 'Bucket'), + path: 'path', + }), + logging: { + cloudWatch: { + logGroup, + prefix: '/my-logs', + }, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + LogsConfig: objectLike({ + CloudWatchLogs: { + GroupName: 'MyLogGroupName', + Status: 'ENABLED', + StreamName: '/my-logs', + }, + }), + })); + + test.done(); + }, + + 'logs config - cloudWatch disabled'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.s3({ + bucket: new s3.Bucket(stack, 'Bucket'), + path: 'path', + }), + logging: { + cloudWatch: { + enabled: false, + }, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + LogsConfig: objectLike({ + CloudWatchLogs: { + Status: 'DISABLED', + }, + }), + })); + + test.done(); + }, + + 'logs config - s3'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const bucket = s3.Bucket.fromBucketName(stack, 'LogBucket', 'MyBucketName'); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.s3({ + bucket: new s3.Bucket(stack, 'Bucket'), + path: 'path', + }), + logging: { + s3: { + bucket, + prefix: 'my-logs', + }, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + LogsConfig: objectLike({ + S3Logs: { + Location: 'MyBucketName/my-logs', + Status: 'ENABLED', + }, + }), + })); + + test.done(); + }, + + 'logs config - cloudWatch and s3'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const bucket = s3.Bucket.fromBucketName(stack, 'LogBucket2', 'MyBucketName'); + const logGroup = logs.LogGroup.fromLogGroupName(stack, 'LogGroup2', 'MyLogGroupName'); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.s3({ + bucket: new s3.Bucket(stack, 'Bucket'), + path: 'path', + }), + logging: { + cloudWatch: { + logGroup, + prefix: '/my-logs', + }, + s3: { + bucket, + prefix: 'my-logs', + }, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + LogsConfig: objectLike({ + CloudWatchLogs: { + GroupName: 'MyLogGroupName', + Status: 'ENABLED', + StreamName: '/my-logs', + }, + S3Logs: { + Location: 'MyBucketName/my-logs', + Status: 'ENABLED', + }, + }), + })); + + test.done(); + }, }, };