diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 47a328144669c..c66a356d0c2da 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -14,10 +14,9 @@ import { CfnProject } from './codebuild.generated'; import { CodePipelineArtifacts } from './codepipeline-artifacts'; import { NoArtifacts } from './no-artifacts'; import { NoSource } from './no-source'; -import { Source } from './source'; +import { ISource } from './source'; import { CODEPIPELINE_SOURCE_ARTIFACTS_TYPE, NO_SOURCE_TYPE } from './source-types'; -const CODEPIPELINE_TYPE = 'CODEPIPELINE'; const S3_BUCKET_ENV = 'SCRIPT_S3_BUCKET'; const S3_KEY_ENV = 'SCRIPT_S3_KEY'; @@ -37,6 +36,8 @@ export interface IProject extends IResource, iam.IGrantable, ec2.IConnectable { /** The IAM service Role of this Project. Undefined for imported Projects. */ readonly role?: iam.IRole; + addToRolePolicy(policyStatement: iam.PolicyStatement): void; + /** * Defines a CloudWatch event rule triggered when something happens with this project. * @@ -187,6 +188,16 @@ abstract class ProjectBase extends Resource implements IProject { return this._connections; } + /** + * Add a permission only if there's a policy attached. + * @param statement The permissions statement to add + */ + public addToRolePolicy(statement: iam.PolicyStatement) { + if (this.role) { + this.role.addToPolicy(statement); + } + } + /** * Defines a CloudWatch event rule triggered when something happens with this project. * @@ -526,7 +537,7 @@ export interface ProjectProps extends CommonProjectProps { * * @default - NoSource */ - readonly source?: Source; + readonly source?: ISource; /** * Defines where build artifacts will be stored. @@ -543,7 +554,7 @@ export interface ProjectProps extends CommonProjectProps { * @default - No secondary sources. * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-multi-in-out.html */ - readonly secondarySources?: Source[]; + readonly secondarySources?: ISource[]; /** * The secondary artifacts for the Project. @@ -632,9 +643,9 @@ export class Project extends ProjectBase { */ public readonly projectName: string; - private readonly source: Source; + private readonly source: ISource; private readonly buildImage: IBuildImage; - private readonly _secondarySources: Source[]; + private readonly _secondarySources: CfnProject.SourceProperty[]; private readonly _secondaryArtifacts: Artifacts[]; constructor(scope: Construct, id: string, props: ProjectProps) { @@ -657,9 +668,16 @@ export class Project extends ProjectBase { // let source "bind" to the project. this usually involves granting permissions // for the code build role to interact with the source. this.source = props.source || new NoSource(); - this.source._bind(this); + const sourceConfig = this.source.bind(this, this); + if (props.badge && !this.source.badgeSupported) { + throw new Error(`Badge is not supported for source type ${this.source.type}`); + } - const artifacts = this.parseArtifacts(props); + const artifacts = props.artifacts + ? props.artifacts + : (this.source.type === CODEPIPELINE_SOURCE_ARTIFACTS_TYPE + ? new CodePipelineArtifacts() + : new NoArtifacts()); artifacts._bind(this); const cache = props.cache || Cache.none(); @@ -670,7 +688,6 @@ export class Project extends ProjectBase { // Inject download commands for asset if requested const environmentVariables = props.environmentVariables || {}; let buildSpec = props.buildSpec; - if (props.buildScript) { environmentVariables[S3_BUCKET_ENV] = { value: props.buildScript.s3BucketName }; environmentVariables[S3_KEY_ENV] = { value: props.buildScript.s3ObjectKey }; @@ -679,23 +696,9 @@ export class Project extends ProjectBase { buildSpec = buildSpec ? mergeBuildSpecs(buildSpec, runScript) : runScript; props.buildScript.grantRead(this.role); } - - // Render the source and add in the buildspec - const renderSource = () => { - if (props.badge && !this.source.badgeSupported) { - throw new Error(`Badge is not supported for source type ${this.source.type}`); - } - - if (this.source.type === NO_SOURCE_TYPE && (buildSpec === undefined || !buildSpec.isImmediate)) { - throw new Error("If the Project's source is NoSource, you need to provide a concrete buildSpec"); - } - - const sourceJson = this.source._toSourceJSON(); - return { - ...sourceJson, - buildSpec: buildSpec && buildSpec.toBuildSpec() - }; - }; + if (this.source.type === NO_SOURCE_TYPE && (buildSpec === undefined || !buildSpec.isImmediate)) { + throw new Error("If the Project's source is NoSource, you need to provide a concrete buildSpec"); + } this._secondarySources = []; for (const secondarySource of props.secondarySources || []) { @@ -711,7 +714,10 @@ export class Project extends ProjectBase { const resource = new CfnProject(this, 'Resource', { description: props.description, - source: renderSource(), + source: { + ...sourceConfig.sourceProperty, + buildSpec: buildSpec && buildSpec.toBuildSpec() + }, artifacts: artifacts.toArtifactsJSON(), serviceRole: this.role.roleArn, environment: this.renderEnvironment(props.environment, environmentVariables), @@ -722,7 +728,7 @@ export class Project extends ProjectBase { timeoutInMinutes: props.timeout, secondarySources: Lazy.anyValue({ produce: () => this.renderSecondarySources() }), secondaryArtifacts: Lazy.anyValue({ produce: () => this.renderSecondaryArtifacts() }), - triggers: this.source._buildTriggers(), + triggers: sourceConfig.buildTriggers, vpcConfig: this.configureVpc(props), }); @@ -745,16 +751,6 @@ export class Project extends ProjectBase { } } - /** - * Add a permission only if there's a policy attached. - * @param statement The permissions statement to add - */ - public addToRolePolicy(statement: iam.PolicyStatement) { - if (this.role) { - this.role.addToPolicy(statement); - } - } - /** * Add a permission only if there's a policy attached. * @param statement The permissions statement to add @@ -775,12 +771,11 @@ export class Project extends ProjectBase { * @param secondarySource the source to add as a secondary source * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-multi-in-out.html */ - public addSecondarySource(secondarySource: Source): void { + public addSecondarySource(secondarySource: ISource): void { if (!secondarySource.identifier) { throw new Error('The identifier attribute is mandatory for secondary sources'); } - secondarySource._bind(this); - this._secondarySources.push(secondarySource); + this._secondarySources.push(secondarySource.bind(this, this).sourceProperty); } /** @@ -874,7 +869,7 @@ export class Project extends ProjectBase { private renderSecondarySources(): CfnProject.SourceProperty[] | undefined { return this._secondarySources.length === 0 ? undefined - : this._secondarySources.map((secondarySource) => secondarySource._toSourceJSON()); + : this._secondarySources; } private renderSecondaryArtifacts(): CfnProject.ArtifactsProperty[] | undefined { @@ -940,23 +935,13 @@ export class Project extends ProjectBase { }; } - private parseArtifacts(props: ProjectProps) { - if (props.artifacts) { - return props.artifacts; - } - if (this.source._toSourceJSON().type === CODEPIPELINE_TYPE) { - return new CodePipelineArtifacts(); - } else { - return new NoArtifacts(); - } - } - private validateCodePipelineSettings(artifacts: Artifacts) { - const sourceType = this.source._toSourceJSON().type; + const sourceType = this.source.type; const artifactsType = artifacts.toArtifactsJSON().type; - if ((sourceType === CODEPIPELINE_TYPE || artifactsType === CODEPIPELINE_TYPE) && - (sourceType !== artifactsType)) { + if ((sourceType === CODEPIPELINE_SOURCE_ARTIFACTS_TYPE || + artifactsType === CODEPIPELINE_SOURCE_ARTIFACTS_TYPE) && + (sourceType !== artifactsType)) { throw new Error('Both source and artifacts must be set to CodePipeline'); } } diff --git a/packages/@aws-cdk/aws-codebuild/lib/source.ts b/packages/@aws-cdk/aws-codebuild/lib/source.ts index 27d4fd155dd75..476975d81b35e 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/source.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/source.ts @@ -1,8 +1,9 @@ import codecommit = require('@aws-cdk/aws-codecommit'); import iam = require('@aws-cdk/aws-iam'); import s3 = require('@aws-cdk/aws-s3'); +import { Construct } from '@aws-cdk/cdk'; import { CfnProject } from './codebuild.generated'; -import { Project } from './project'; +import { IProject } from './project'; import { BITBUCKET_SOURCE_TYPE, CODECOMMIT_SOURCE_TYPE, @@ -11,6 +12,29 @@ import { S3_SOURCE_TYPE } from './source-types'; +/** + * The type returned from {@link ISource#bind}. + */ +export interface SourceConfig { + readonly sourceProperty: CfnProject.SourceProperty; + + readonly buildTriggers?: CfnProject.ProjectTriggersProperty; +} + +/** + * The abstract interface of a CodeBuild source. + * Implemented by {@link Source}. + */ +export interface ISource { + readonly identifier?: string; + + readonly type: string; + + readonly badgeSupported: boolean; + + bind(scope: Construct, project: IProject): SourceConfig; +} + /** * Properties common to all Source classes. */ @@ -25,7 +49,7 @@ export interface SourceProps { /** * Source provider definition for a CodeBuild Project. */ -export abstract class Source { +export abstract class Source implements ISource { public static s3(props: S3SourceProps): S3Source { return new S3Source(props); } @@ -50,7 +74,7 @@ export abstract class Source { public abstract readonly type: string; public readonly badgeSupported: boolean = false; - constructor(props: SourceProps) { + protected constructor(props: SourceProps) { this.identifier = props.identifier; } @@ -58,31 +82,13 @@ export abstract class Source { * Called by the project when the source is added so that the source can perform * binding operations on the source. For example, it can grant permissions to the * code build project to read from the S3 bucket. - * - * @internal */ - public _bind(_project: Project) { - // by default, do nothing - return; - } - - /** @internal */ - public _toSourceJSON(): CfnProject.SourceProperty { - const sourceProp = this.toSourceProperty(); - return { - sourceIdentifier: this.identifier, - type: this.type, - ...sourceProp, - }; - } - - /** @internal */ - public _buildTriggers(): CfnProject.ProjectTriggersProperty | undefined { - return undefined; - } - - protected toSourceProperty(): any { + public bind(_scope: Construct, _project: IProject): SourceConfig { return { + sourceProperty: { + sourceIdentifier: this.identifier, + type: this.type, + } }; } } @@ -111,11 +117,13 @@ abstract class GitSource extends Source { this.cloneDepth = props.cloneDepth; } - /** @internal */ - public _toSourceJSON(): CfnProject.SourceProperty { + public bind(_scope: Construct, _project: IProject): SourceConfig { + const superConfig = super.bind(_scope, _project); return { - ...super._toSourceJSON(), - gitCloneDepth: this.cloneDepth + sourceProperty: { + ...superConfig.sourceProperty, + gitCloneDepth: this.cloneDepth, + }, }; } } @@ -435,21 +443,20 @@ abstract class ThirdPartyGitSource extends GitSource { this.webhookFilters = props.webhookFilters || []; } - /** @internal */ - public _buildTriggers(): CfnProject.ProjectTriggersProperty | undefined { + public bind(_scope: Construct, _project: IProject): SourceConfig { const anyFilterGroupsProvided = this.webhookFilters.length > 0; const webhook = this.webhook === undefined ? (anyFilterGroupsProvided ? true : undefined) : this.webhook; - return webhook === undefined ? undefined : { - webhook, - filterGroups: anyFilterGroupsProvided ? this.webhookFilters.map(fg => fg._toJson()) : undefined, - }; - } - /** @internal */ - public _toSourceJSON(): CfnProject.SourceProperty { + const superConfig = super.bind(_scope, _project); return { - ...super._toSourceJSON(), - reportBuildStatus: this.reportBuildStatus, + sourceProperty: { + ...superConfig.sourceProperty, + reportBuildStatus: this.reportBuildStatus, + }, + buildTriggers: webhook === undefined ? undefined : { + webhook, + filterGroups: anyFilterGroupsProvided ? this.webhookFilters.map(fg => fg._toJson()) : undefined, + } }; } } @@ -473,19 +480,18 @@ export class CodeCommitSource extends GitSource { this.repo = props.repository; } - /** - * @internal - */ - public _bind(project: Project) { + public bind(_scope: Construct, project: IProject): SourceConfig { // https://docs.aws.amazon.com/codebuild/latest/userguide/setting-up.html project.addToRolePolicy(new iam.PolicyStatement() .addAction('codecommit:GitPull') .addResource(this.repo.repositoryArn)); - } - protected toSourceProperty(): any { + const superConfig = super.bind(_scope, project); return { - location: this.repo.repositoryCloneUrlHttp + sourceProperty: { + ...superConfig.sourceProperty, + location: this.repo.repositoryCloneUrlHttp, + }, }; } } @@ -512,16 +518,15 @@ export class S3Source extends Source { this.path = props.path; } - /** - * @internal - */ - public _bind(project: Project) { + public bind(_scope: Construct, project: IProject): SourceConfig { this.bucket.grantRead(project); - } - protected toSourceProperty(): any { + const superConfig = super.bind(_scope, project); return { - location: `${this.bucket.bucketName}/${this.path}`, + sourceProperty: { + ...superConfig.sourceProperty, + location: `${this.bucket.bucketName}/${this.path}`, + }, }; } } @@ -557,9 +562,14 @@ export class GitHubSource extends ThirdPartyGitSource { this.httpsCloneUrl = `https://github.com/${props.owner}/${props.repo}.git`; } - protected toSourceProperty(): any { + public bind(_scope: Construct, project: IProject): SourceConfig { + const superConfig = super.bind(_scope, project); return { - location: this.httpsCloneUrl, + sourceProperty: { + ...superConfig.sourceProperty, + location: this.httpsCloneUrl, + }, + buildTriggers: superConfig.buildTriggers, }; } } @@ -595,10 +605,15 @@ export class GitHubEnterpriseSource extends ThirdPartyGitSource { this.ignoreSslErrors = props.ignoreSslErrors; } - protected toSourceProperty(): any { + public bind(_scope: Construct, _project: IProject): SourceConfig { + const superConfig = super.bind(_scope, _project); return { - location: this.httpsCloneUrl, - insecureSsl: this.ignoreSslErrors, + sourceProperty: { + ...superConfig.sourceProperty, + location: this.httpsCloneUrl, + insecureSsl: this.ignoreSslErrors, + }, + buildTriggers: superConfig.buildTriggers, }; } } @@ -634,8 +649,7 @@ export class BitBucketSource extends ThirdPartyGitSource { this.httpsCloneUrl = `https://bitbucket.org/${props.owner}/${props.repo}.git`; } - /** @internal */ - public _buildTriggers(): CfnProject.ProjectTriggersProperty | undefined { + public bind(_scope: Construct, _project: IProject): SourceConfig { // BitBucket sources don't support the PULL_REQUEST_REOPENED event action if (this.anyWebhookFilterContainsPrReopenedEventAction()) { throw new Error('BitBucket sources do not support the PULL_REQUEST_REOPENED webhook event action'); @@ -646,12 +660,13 @@ export class BitBucketSource extends ThirdPartyGitSource { throw new Error('BitBucket sources do not support file path conditions for webhook filters'); } - return super._buildTriggers(); - } - - protected toSourceProperty(): any { + const superConfig = super.bind(_scope, _project); return { - location: this.httpsCloneUrl + sourceProperty: { + ...superConfig.sourceProperty, + location: this.httpsCloneUrl, + }, + buildTriggers: superConfig.buildTriggers, }; }