diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index 7665ab83df606..e70e30ad97881 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -14,7 +14,9 @@ import codecommit = require('@aws-cdk/aws-codecommit'); const repo = new codecommit.Repository(this, 'MyRepo', { repositoryName: 'foo' }); new codebuild.Project(this, 'MyFirstCodeCommitProject', { - source: new codebuild.CodeCommitSource(repo) + source: new codebuild.CodeCommitSource({ + repository: repo, + }), }); ``` @@ -28,7 +30,10 @@ import s3 = require('@aws-cdk/aws-s3'); const bucket = new s3.Bucket(this, 'MyBucket'); new codebuild.Project(this, 'MyProject', { - source: new codebuild.S3BucketSource(bucket, 'path/to/source.zip') + source: new codebuild.S3BucketSource({ + bucket: bucket, + path: 'path/to/file.zip', + }), }); ``` @@ -119,3 +124,134 @@ To define CloudWatch event rules for build projects, use one of the `onXxx` meth const rule = project.onStateChange('BuildStateChange'); rule.addTarget(lambdaFunction); ``` + +### Secondary sources and artifacts + +CodeBuild Projects can get their sources from multiple places, +and produce multiple outputs. For example: + +```ts +const project = new codebuild.Project(this, 'MyProject', { + secondarySources: [ + new codebuild.CodeCommitSource({ + identifier: 'source2', + repository: repo, + }), + ], + secondaryArtifacts: [ + new codebuild.S3BucketBuildArtifacts({ + identifier: 'artifact2', + bucket: bucket, + path: 'some/path', + name: 'file.zip', + }), + ], + // ... +}); +``` + +Note that the `identifier` property is required for both secondary sources and artifacts. + +The contents of the secondary source will be available to the build under the directory +specified by the `CODEBUILD_SRC_DIR_` environment variable +(so, `CODEBUILD_SRC_DIR_source2` in the above case). + +The secondary artifacts have their own section in the buildspec, +under the regular `artifacts` one. +Each secondary artifact has its own section, +beginning with their identifier. + +So, a buildspec for the above Project could look something like this: + +```ts +const project = new codebuild.Project(this, 'MyProject', { + // secondary sources and artifacts as above... + buildSpec: { + version: '0.2', + phases: { + build: { + commands: [ + 'cd $CODEBUILD_SRC_DIR_source2', + 'touch output2.txt', + ], + }, + }, + artifacts: { + 'secondary-artifacts': { + 'artifact2': { + 'base-directory': '$CODEBUILD_SRC_DIR_source2', + 'files': [ + 'output2.txt', + ], + }, + }, + }, + }, +}); +``` + +#### Multiple inputs and outputs in CodePipeline + +When you want to have multiple inputs and/or outputs for a Project used in a Pipeline, +instead of using the `secondarySources` and `secondaryArtifacts` properties, +you need to use the `additionalInputArtifacts` and `additionalOutputArtifactNames` +properties of the CodeBuild CodePipeline Actions. +Example: + +```ts +const sourceStage = pipeline.addStage('Source'); +const sourceAction1 = repository1.addToPipeline(sourceStage, 'Source1'); +const sourceAction2 = repository2.addToPipeline(sourceStage, 'Source2', { + outputArtifactName: 'source2', +}); + +const buildStage = pipeline.addStage('Build'); +const buildAction = project.addBuildToPipeline(buildStage, 'Build', { + inputArtifact: sourceAction1.outputArtifact, + outputArtifactName: 'artifact1', // for better buildspec readability - see below + additionalInputArtifacts: [ + sourceAction2.outputArtifact, // this is where 'source2' comes from + ], + additionalOutputArtifactNames: [ + 'artifact2', + ], +}); +``` + +**Note**: when a CodeBuild Action in a Pipeline has more than one output, +it will only use the `secondary-artifacts` field of the buildspec, +never the primary output specification directly under `artifacts`. +Because of that, it pays to name even your primary output artifact on the Pipeline, +like we did above, so that you know what name to use in the buildspec. + +Example buildspec for the above project: + +```ts +const project = new codebuild.PipelineProject(this, 'MyProject', { + buildSpec: { + version: '0.2', + phases: { + build: { + commands: [ + // By default, you're in a directory with the contents of the repository from sourceAction1. + // Use the CODEBUILD_SRC_DIR_source2 environment variable + // to get a path to the directory with the contents of the second input repository. + ], + }, + }, + artifacts: { + 'secondary-artifacts': { + 'artifact1': { + // primary Action output artifact, + // available as buildAction.outputArtifact + }, + 'artifact2': { + // additional output artifact, + // available as buildAction.additionalOutputArtifact('artifact2') + }, + }, + }, + }, + // ... +}); +``` diff --git a/packages/@aws-cdk/aws-codebuild/lib/artifacts.ts b/packages/@aws-cdk/aws-codebuild/lib/artifacts.ts index 2e0f306a016be..586d52b5e3e78 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/artifacts.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/artifacts.ts @@ -2,26 +2,80 @@ import s3 = require('@aws-cdk/aws-s3'); import { cloudformation } from './codebuild.generated'; import { Project } from './project'; +/** + * Properties common to all Artifacts classes. + */ +export interface BuildArtifactsProps { + /** + * The artifact identifier. + * This property is required on secondary artifacts. + */ + identifier?: string; +} + +/** + * Artifacts definition for a CodeBuild Project. + */ export abstract class BuildArtifacts { - public abstract toArtifactsJSON(): cloudformation.ProjectResource.ArtifactsProperty; - public bind(_project: Project) { + public readonly identifier?: string; + protected abstract readonly type: string; + + constructor(props: BuildArtifactsProps) { + this.identifier = props.identifier; + } + + public _bind(_project: Project) { return; } + + public toArtifactsJSON(): cloudformation.ProjectResource.ArtifactsProperty { + const artifactsProp = this.toArtifactsProperty(); + return { + artifactIdentifier: this.identifier, + type: this.type, + ...artifactsProp, + }; + } + + protected toArtifactsProperty(): any { + return { + }; + } } +/** + * A `NO_ARTIFACTS` CodeBuild Project Artifact definition. + * This is the default artifact type, + * if none was specified when creating the Project + * (and the source was not specified to be CodePipeline). + * *Note*: the `NO_ARTIFACTS` type cannot be used as a secondary artifact, + * and because of that, you're not allowed to specify an identifier for it. + */ export class NoBuildArtifacts extends BuildArtifacts { - public toArtifactsJSON(): cloudformation.ProjectResource.ArtifactsProperty { - return { type: 'NO_ARTIFACTS' }; + protected readonly type = 'NO_ARTIFACTS'; + + constructor() { + super({}); } } +/** + * CodePipeline Artifact definition for a CodeBuild Project. + * *Note*: this type cannot be used as a secondary artifact, + * and because of that, you're not allowed to specify an identifier for it. + */ export class CodePipelineBuildArtifacts extends BuildArtifacts { - public toArtifactsJSON(): cloudformation.ProjectResource.ArtifactsProperty { - return { type: 'CODEPIPELINE' }; + protected readonly type = 'CODEPIPELINE'; + + constructor() { + super({}); } } -export interface S3BucketBuildArtifactsProps { +/** + * Construction properties for {@link S3BucketBuildArtifacts}. + */ +export interface S3BucketBuildArtifactsProps extends BuildArtifactsProps { /** * The name of the output bucket. */ @@ -37,8 +91,8 @@ export interface S3BucketBuildArtifactsProps { /** * The name of the build output ZIP file or folder inside the bucket. * - * The full S3 object key will be "/build-ID/" or - * "/" depending on whether `includeBuildId` is set to true. + * The full S3 object key will be "//" or + * "/" depending on whether `includeBuildID` is set to true. */ name: string; @@ -59,39 +113,27 @@ export interface S3BucketBuildArtifactsProps { packageZip?: boolean; } +/** + * S3 Artifact definition for a CodeBuild Project. + */ export class S3BucketBuildArtifacts extends BuildArtifacts { + protected readonly type = 'S3'; + constructor(private readonly props: S3BucketBuildArtifactsProps) { - super(); + super(props); } - public bind(project: Project) { + public _bind(project: Project) { this.props.bucket.grantReadWrite(project.role); } - public toArtifactsJSON(): cloudformation.ProjectResource.ArtifactsProperty { + protected toArtifactsProperty(): any { return { - type: 'S3', location: this.props.bucket.bucketName, path: this.props.path, - namespaceType: this.parseNamespaceType(this.props.includeBuildID), + namespaceType: this.props.includeBuildID === false ? 'NONE' : 'BUILD_ID', name: this.props.name, - packaging: this.parsePackaging(this.props.packageZip), + packaging: this.props.packageZip === false ? 'NONE' : 'ZIP', }; } - - private parseNamespaceType(includeBuildID?: boolean) { - if (includeBuildID != null) { - return includeBuildID ? 'BUILD_ID' : 'NONE'; - } else { - return 'BUILD_ID'; - } - } - - private parsePackaging(packageZip?: boolean) { - if (packageZip != null) { - return packageZip ? 'ZIP' : 'NONE'; - } else { - return 'ZIP'; - } - } } diff --git a/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts index dfb99c76c49f3..6dccda0066a05 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts @@ -3,12 +3,30 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { ProjectRef } from './project'; +/** + * Common construction properties of all CodeBuild Pipeline Actions. + */ +export interface CommonCodeBuildActionProps { + /** + * The list of additional input Artifacts for this Action. + */ + additionalInputArtifacts?: codepipeline.Artifact[]; + + /** + * The list of names for additional output Artifacts for this Action. + * The resulting output artifacts can be accessed with the `additionalOutputArtifacts` + * method of the Action. + */ + additionalOutputArtifactNames?: string[]; +} + /** * Common properties for creating {@link PipelineBuildAction} - * either directly, through its constructor, * or through {@link ProjectRef#addBuildToPipeline}. */ -export interface CommonPipelineBuildActionProps extends codepipeline.CommonActionProps { +export interface CommonPipelineBuildActionProps extends CommonCodeBuildActionProps, + codepipeline.CommonActionProps { /** * The source to use as input for this build. * @@ -45,6 +63,7 @@ export class PipelineBuildAction extends codepipeline.BuildAction { super(parent, name, { provider: 'CodeBuild', + artifactBounds: { minInputs: 1, maxInputs: 5, minOutputs: 0, maxOutputs: 5 }, configuration: { ProjectName: props.project.projectName, }, @@ -52,6 +71,38 @@ export class PipelineBuildAction extends codepipeline.BuildAction { }); setCodeBuildNeededPermissions(props.stage, props.project, true); + + handleAdditionalInputOutputArtifacts(props, this, + // pass functions to get around protected members + (artifact) => this.addInputArtifact(artifact), + (artifactName) => this.addOutputArtifact(artifactName)); + } + + /** + * Returns the additional output artifacts defined for this Action. + * Their names will be taken from the {@link CommonCodeBuildActionProps#additionalOutputArtifactNames} + * property. + * + * @returns all additional output artifacts defined for this Action + * @see #additionalOutputArtifact + */ + public additionalOutputArtifacts(): codepipeline.Artifact[] { + return this._outputArtifacts.slice(1); + } + + /** + * Returns the additional output artifact with the given name, + * or throws an exception if an artifact with that name was not found + * in the additonal output artifacts. + * The names are defined by the {@link CommonCodeBuildActionProps#additionalOutputArtifactNames} + * property. + * + * @param name the name of the artifact to find + * @returns the artifact with the given name + * @see #additionalOutputArtifacts + */ + public additionalOutputArtifact(name: string): codepipeline.Artifact { + return findOutputArtifact(this.additionalOutputArtifacts(), name); } } @@ -60,7 +111,8 @@ export class PipelineBuildAction extends codepipeline.BuildAction { * either directly, through its constructor, * or through {@link ProjectRef#addTestToPipeline}. */ -export interface CommonPipelineTestActionProps extends codepipeline.CommonActionProps { +export interface CommonPipelineTestActionProps extends CommonCodeBuildActionProps, + codepipeline.CommonActionProps { /** * The source to use as input for this test. * @@ -69,7 +121,7 @@ export interface CommonPipelineTestActionProps extends codepipeline.CommonAction inputArtifact?: codepipeline.Artifact; /** - * The optional name of the output artifact. + * The optional name of the primary output artifact. * If you provide a value here, * then the `outputArtifact` property of your Action will be non-null. * If you don't, `outputArtifact` will be `null`. @@ -94,6 +146,7 @@ export class PipelineTestAction extends codepipeline.TestAction { constructor(parent: cdk.Construct, name: string, props: PipelineTestActionProps) { super(parent, name, { provider: 'CodeBuild', + artifactBounds: { minInputs: 1, maxInputs: 5, minOutputs: 0, maxOutputs: 5 }, configuration: { ProjectName: props.project.projectName, }, @@ -102,6 +155,40 @@ export class PipelineTestAction extends codepipeline.TestAction { // the Action needs write permissions only if it's producing an output artifact setCodeBuildNeededPermissions(props.stage, props.project, !!props.outputArtifactName); + + handleAdditionalInputOutputArtifacts(props, this, + // pass functions to get around protected members + (artifact) => this.addInputArtifact(artifact), + (artifactName) => this.addOutputArtifact(artifactName)); + } + + /** + * Returns the additional output artifacts defined for this Action. + * Their names will be taken from the {@link CommonCodeBuildActionProps#additionalOutputArtifactNames} + * property. + * + * @returns all additional output artifacts defined for this Action + * @see #additionalOutputArtifact + */ + public additionalOutputArtifacts(): codepipeline.Artifact[] { + return this.outputArtifact === undefined + ? this._outputArtifacts + : this._outputArtifacts.slice(1); + } + + /** + * Returns the additional output artifact with the given name, + * or throws an exception if an artifact with that name was not found + * in the additonal output artifacts. + * The names are defined by the {@link CommonCodeBuildActionProps#additionalOutputArtifactNames} + * property. + * + * @param name the name of the artifact to find + * @returns the artifact with the given name + * @see #additionalOutputArtifacts + */ + public additionalOutputArtifact(name: string): codepipeline.Artifact { + return findOutputArtifact(this.additionalOutputArtifacts(), name); } } @@ -123,3 +210,28 @@ function setCodeBuildNeededPermissions(stage: codepipeline.IStage, project: Proj stage.pipeline.grantBucketRead(project.role); } } + +function handleAdditionalInputOutputArtifacts(props: CommonCodeBuildActionProps, action: codepipeline.Action, + addInputArtifact: (_: codepipeline.Artifact) => void, + addOutputArtifact: (_: string) => void) { + if ((props.additionalInputArtifacts || []).length > 0) { + // we have to set the primary source in the configuration + action.configuration.PrimarySource = action._inputArtifacts[0].name; + // add the additional artifacts + for (const additionalInputArtifact of props.additionalInputArtifacts || []) { + addInputArtifact(additionalInputArtifact); + } + } + + for (const additionalArtifactName of props.additionalOutputArtifactNames || []) { + addOutputArtifact(additionalArtifactName); + } +} + +function findOutputArtifact(artifacts: codepipeline.Artifact[], name: string): codepipeline.Artifact { + const ret = artifacts.find((artifact) => artifact.name === name); + if (!ret) { + throw new Error(`Could not find output artifact with name '${name}'`); + } + return ret; +} diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 1e1981f28b2e4..134a336a783ae 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -12,7 +12,7 @@ import { CommonPipelineBuildActionProps, CommonPipelineTestActionProps, PipelineBuildAction, PipelineTestAction } from './pipeline-actions'; -import { BuildSource, NoSource } from './source'; +import { BuildSource, NoSource, SourceType } from './source'; const CODEPIPELINE_TYPE = 'CODEPIPELINE'; const S3_BUCKET_ENV = 'SCRIPT_S3_BUCKET'; @@ -422,6 +422,8 @@ export interface CommonProjectProps { export interface ProjectProps extends CommonProjectProps { /** * The source of the build. + * *Note*: if {@link NoSource} is given as the source, + * then you need to provide an explicit `buildSpec`. * * @default NoSource */ @@ -434,6 +436,24 @@ export interface ProjectProps extends CommonProjectProps { * @default NoBuildArtifacts */ artifacts?: BuildArtifacts; + + /** + * The secondary sources for the Project. + * Can be also added after the Project has been created by using the {@link Project#addSecondarySource} method. + * + * @default [] + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-multi-in-out.html + */ + secondarySources?: BuildSource[]; + + /** + * The secondary artifacts for the Project. + * Can also be added after the Project has been created by using the {@link Project#addSecondaryArtifact} method. + * + * @default [] + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-multi-in-out.html + */ + secondaryArtifacts?: BuildArtifacts[]; } /** @@ -457,6 +477,8 @@ export class Project extends ProjectRef { private readonly source: BuildSource; private readonly buildImage: IBuildImage; + private readonly _secondarySources: BuildSource[]; + private readonly _secondaryArtifacts: BuildArtifacts[]; constructor(parent: cdk.Construct, name: string, props: ProjectProps) { super(parent, name); @@ -485,10 +507,10 @@ export class Project extends ProjectRef { // 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); + this.source._bind(this); const artifacts = this.parseArtifacts(props); - artifacts.bind(this); + artifacts._bind(this); // Inject download commands for asset if requested const environmentVariables = props.environmentVariables || {}; @@ -509,6 +531,18 @@ export class Project extends ProjectRef { // We have to pretty-print the buildspec, otherwise // CodeBuild will not recognize it as an inline buildspec. sourceJson.buildSpec = JSON.stringify(buildSpec, undefined, 2); // Literal buildspec + } else if (this.source.type === SourceType.None) { + throw new Error("If the Project's source is NoSource, you need to provide a buildSpec"); + } + + this._secondarySources = []; + for (const secondarySource of props.secondarySources || []) { + this.addSecondarySource(secondarySource); + } + + this._secondaryArtifacts = []; + for (const secondaryArtifact of props.secondaryArtifacts || []) { + this.addSecondaryArtifact(secondaryArtifact); } this.validateCodePipelineSettings(artifacts); @@ -524,6 +558,8 @@ export class Project extends ProjectRef { cache, name: props.projectName, timeoutInMinutes: props.timeout, + secondarySources: new cdk.Token(() => this.renderSecondarySources()), + secondaryArtifacts: new cdk.Token(() => this.renderSecondaryArtifacts()), }); this.projectArn = resource.projectArn; @@ -532,6 +568,24 @@ export class Project extends ProjectRef { this.addToRolePolicy(this.createLoggingPermission()); } + /** + * @override + */ + public validate(): string[] { + const ret = new Array(); + if (this.source.type === SourceType.CodePipeline) { + if (this._secondarySources.length > 0) { + ret.push('A Project with a CodePipeline Source cannot have secondary sources. ' + + "Use the CodeBuild Pipeline Actions' `additionalInputArtifacts` property instead"); + } + if (this._secondaryArtifacts.length > 0) { + ret.push('A Project with a CodePipeline Source cannot have secondary artifacts. ' + + "Use the CodeBuild Pipeline Actions' `additionalOutputArtifactNames` property instead"); + } + } + return ret; + } + /** * Add a permission only if there's a policy attached. * @param statement The permissions statement to add @@ -542,6 +596,34 @@ export class Project extends ProjectRef { } } + /** + * Adds a secondary source to the Project. + * + * @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: BuildSource): void { + if (!secondarySource.identifier) { + throw new Error('The identifier attribute is mandatory for secondary sources'); + } + secondarySource._bind(this); + this._secondarySources.push(secondarySource); + } + + /** + * Adds a secondary artifact to the Project. + * + * @param secondaryArtifact the artifact to add as a secondary artifact + * @see https://docs.aws.amazon.com/codebuild/latest/userguide/sample-multi-in-out.html + */ + public addSecondaryArtifact(secondaryArtifact: BuildArtifacts): any { + if (!secondaryArtifact.identifier) { + throw new Error("The identifier attribute is mandatory for secondary artifacts"); + } + secondaryArtifact._bind(this); + this._secondaryArtifacts.push(secondaryArtifact); + } + private createLoggingPermission() { const logGroupArn = cdk.ArnUtils.fromComponents({ service: 'logs', @@ -599,6 +681,18 @@ export class Project extends ProjectRef { }; } + private renderSecondarySources(): cloudformation.ProjectResource.SourceProperty[] | undefined { + return this._secondarySources.length === 0 + ? undefined + : this._secondarySources.map((secondarySource) => secondarySource.toSourceJSON()); + } + + private renderSecondaryArtifacts(): cloudformation.ProjectResource.ArtifactsProperty[] | undefined { + return this._secondaryArtifacts.length === 0 + ? undefined + : this._secondaryArtifacts.map((secondaryArtifact) => secondaryArtifact.toArtifactsJSON()); + } + private parseArtifacts(props: ProjectProps) { if (props.artifacts) { return props.artifacts; diff --git a/packages/@aws-cdk/aws-codebuild/lib/source.ts b/packages/@aws-cdk/aws-codebuild/lib/source.ts index 9c4ebb15ec0c4..7e4d9bd2b1150 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/source.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/source.ts @@ -6,73 +6,150 @@ import { cloudformation } from './codebuild.generated'; import { Project } from './project'; /** - * Source Provider definition for a CodeBuild project - * TODO: Abstract class should be an interface + * Properties common to all Source classes. + */ +export interface BuildSourceProps { + /** + * The source identifier. + * This property is required on secondary sources. + */ + identifier?: string; +} + +/** + * Source provider definition for a CodeBuild Project. */ export abstract class BuildSource { + public readonly identifier?: string; + public abstract readonly type: SourceType; + + constructor(props: BuildSourceProps) { + this.identifier = props.identifier; + } + /** * 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. */ - public bind(_project: Project) { + public _bind(_project: Project) { + // by default, do nothing return; } - public abstract toSourceJSON(): cloudformation.ProjectResource.SourceProperty; + public toSourceJSON(): cloudformation.ProjectResource.SourceProperty { + const sourceProp = this.toSourceProperty(); + return { + sourceIdentifier: this.identifier, + type: this.type, + ...sourceProp, + }; + } + + protected toSourceProperty(): any { + return { + }; + } } +/** + * A `NO_SOURCE` CodeBuild Project Source definition. + * This is the default source type, + * if none was specified when creating the Project. + * *Note*: the `NO_SOURCE` type cannot be used as a secondary source, + * and because of that, you're not allowed to specify an identifier for it. + */ export class NoSource extends BuildSource { + public readonly type: SourceType = SourceType.None; + constructor() { - super(); + super({}); } +} - public toSourceJSON(): cloudformation.ProjectResource.SourceProperty { - return { - type: SourceType.None, - }; - } +/** + * Construction properties for {@link CodeCommitSource}. + */ +export interface CodeCommitSourceProps extends BuildSourceProps { + repository: codecommit.RepositoryRef; } /** - * CodeCommit Source definition for a CodeBuild project + * CodeCommit Source definition for a CodeBuild project. */ export class CodeCommitSource extends BuildSource { - constructor(private readonly repo: codecommit.RepositoryRef) { - super(); + public readonly type: SourceType = SourceType.CodeCommit; + private readonly repo: codecommit.RepositoryRef; + + constructor(props: CodeCommitSourceProps) { + super(props); + this.repo = props.repository; } - public bind(project: Project) { + public _bind(project: Project) { // https://docs.aws.amazon.com/codebuild/latest/userguide/setting-up.html project.addToRolePolicy(new iam.PolicyStatement() .addAction('codecommit:GitPull') .addResource(this.repo.repositoryArn)); } - public toSourceJSON(): cloudformation.ProjectResource.SourceProperty { + protected toSourceProperty(): any { return { - type: SourceType.CodeCommit, location: this.repo.repositoryCloneUrlHttp }; } } /** - * CodePipeline Source definition for a CodeBuild project + * Construction properties for {@link S3BucketSource}. */ -export class CodePipelineSource extends BuildSource { - public toSourceJSON(): cloudformation.ProjectResource.SourceProperty { +export interface S3BucketSourceProps extends BuildSourceProps { + bucket: s3.BucketRef; + path: string; +} + +/** + * S3 bucket definition for a CodeBuild project. + */ +export class S3BucketSource extends BuildSource { + public readonly type: SourceType = SourceType.S3; + private readonly bucket: s3.BucketRef; + private readonly path: string; + + constructor(props: S3BucketSourceProps) { + super(props); + this.bucket = props.bucket; + this.path = props.path; + } + + public _bind(project: Project) { + this.bucket.grantRead(project.role); + } + + protected toSourceProperty(): any { return { - type: SourceType.CodePipeline + location: `${this.bucket.bucketName}/${this.path}`, }; } +} + +/** + * CodePipeline Source definition for a CodeBuild Project. + * *Note*: this type cannot be used as a secondary source, + * and because of that, you're not allowed to specify an identifier for it. + */ +export class CodePipelineSource extends BuildSource { + public readonly type: SourceType = SourceType.CodePipeline; - public bind(_project: Project) { - // TODO: permissions on the pipeline bucket? + constructor() { + super({}); } } -export interface GithubSourceProps { +/** + * Construction properties for {@link GitHubSource} and {@link GitHubEnterpriseSource}. + */ +export interface GitHubSourceProps extends BuildSourceProps { /** * The git url to clone for this code build project. */ @@ -82,85 +159,76 @@ export interface GithubSourceProps { * The oAuthToken used to authenticate when cloning source git repo. */ oauthToken: cdk.Secret; - } /** - * GitHub Source definition for a CodeBuild project + * GitHub Source definition for a CodeBuild project. */ export class GitHubSource extends BuildSource { - private cloneUrl: string; - private oauthToken: cdk.Secret; - constructor(props: GithubSourceProps) { - super(); + public readonly type: SourceType = SourceType.GitHub; + private readonly cloneUrl: string; + private readonly oauthToken: cdk.Secret; + + constructor(props: GitHubSourceProps) { + super(props); this.cloneUrl = props.cloneUrl; this.oauthToken = props.oauthToken; } - public toSourceJSON(): cloudformation.ProjectResource.SourceProperty { + protected toSourceProperty(): any { return { - type: SourceType.GitHub, - auth: this.oauthToken != null ? { type: 'OAUTH', resource: this.oauthToken } : undefined, - location: this.cloneUrl + auth: { type: 'OAUTH', resource: this.oauthToken }, + location: this.cloneUrl, }; } } /** - * GitHub Enterprise Source definition for a CodeBuild project + * GitHub Enterprise Source definition for a CodeBuild project. */ export class GitHubEnterpriseSource extends BuildSource { - private cloneUrl: string; - private oauthToken: cdk.Secret; - constructor(props: GithubSourceProps) { - super(); + public readonly type: SourceType = SourceType.GitHubEnterPrise; + private readonly cloneUrl: string; + private readonly oauthToken: cdk.Secret; + + constructor(props: GitHubSourceProps) { + super(props); this.cloneUrl = props.cloneUrl; this.oauthToken = props.oauthToken; } - public toSourceJSON(): cloudformation.ProjectResource.SourceProperty { + protected toSourceProperty(): any { return { - type: SourceType.GitHubEnterPrise, + auth: { type: 'OAUTH', resource: this.oauthToken }, location: this.cloneUrl, - auth: this.oauthToken != null ? { type: 'OAUTH', resource: this.oauthToken } : undefined, }; } } /** - * BitBucket Source definition for a CodeBuild project + * Construction properties for {@link BitBucketSource}. */ -export class BitBucketSource extends BuildSource { - constructor(private readonly httpsCloneUrl: string) { - super(); - this.httpsCloneUrl = httpsCloneUrl; - } - public toSourceJSON(): cloudformation.ProjectResource.SourceProperty { - return { - type: SourceType.BitBucket, - location: this.httpsCloneUrl - }; - } +export interface BitBucketSourceProps extends BuildSourceProps { + httpsCloneUrl: string; } /** - * S3 bucket definition for a CodeBuild project. + * BitBucket Source definition for a CodeBuild project. */ -export class S3BucketSource extends BuildSource { - constructor(private readonly bucket: s3.BucketRef, private readonly path: string) { - super(); +export class BitBucketSource extends BuildSource { + public readonly type: SourceType = SourceType.BitBucket; + private readonly httpsCloneUrl: any; + + constructor(props: BitBucketSourceProps) { + super(props); + this.httpsCloneUrl = props.httpsCloneUrl; } - public toSourceJSON(): cloudformation.ProjectResource.SourceProperty { + protected toSourceProperty(): any { return { - type: SourceType.S3, - location: new cdk.FnConcat(this.bucket.bucketName, '/', this.path) + location: this.httpsCloneUrl }; } - - public bind(project: Project) { - this.bucket.grantRead(project.role); - } } /** diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-bucket.ts b/packages/@aws-cdk/aws-codebuild/test/integ.project-bucket.ts index 65e919df967fd..85d7a4e9d7056 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-bucket.ts +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-bucket.ts @@ -10,7 +10,10 @@ const stack = new cdk.Stack(app, 'aws-cdk-codebuild'); const bucket = new s3.Bucket(stack, 'MyBucket'); new codebuild.Project(stack, 'MyProject', { - source: new codebuild.S3BucketSource(bucket, 'path/to/my/source.zip'), + source: new codebuild.S3BucketSource({ + bucket, + path: 'path/to/my/source.zip', + }), environment: { computeType: codebuild.ComputeType.Large } diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-events.ts b/packages/@aws-cdk/aws-codebuild/test/integ.project-events.ts index 213421c45ea09..e71f9e62da944 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-events.ts +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-events.ts @@ -10,7 +10,9 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-cdk-codebuild-events'); const repo = new codecommit.Repository(stack, 'MyRepo', { repositoryName: 'aws-cdk-codebuild-events' }); -const project = new Project(stack, 'MyProject', { source: new CodeCommitSource(repo) }); +const project = new Project(stack, 'MyProject', { + source: new CodeCommitSource({ repository: repo }), +}); const queue = new sqs.Queue(stack, 'MyQueue'); diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json new file mode 100644 index 0000000000000..8329aa4928b13 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json @@ -0,0 +1,214 @@ +{ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket" + }, + "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": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "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" + } + ] + } + }, + "MyProject39F7B0AE": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/ubuntu-base:14.04", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectRole9BBE5233", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\"\n}", + "Type": "NO_SOURCE" + }, + "SecondaryArtifacts": [ + { + "ArtifactIdentifier": "AddArtifact1", + "Location": { + "Ref": "MyBucketF68F3FF0" + }, + "Name": "name", + "NamespaceType": "BUILD_ID", + "Packaging": "ZIP", + "Path": "another/path", + "Type": "S3" + } + ], + "SecondarySources": [ + { + "Location": { + "Fn::Join": [ + "", + [ + { + "Ref": "MyBucketF68F3FF0" + }, + "/some/path" + ] + ] + }, + "SourceIdentifier": "AddSource1", + "Type": "S3" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.ts b/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.ts new file mode 100644 index 0000000000000..73544c086b2c4 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.ts @@ -0,0 +1,32 @@ +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import codebuild = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codebuild-secondary-sources-artifacts'); + +const bucket = new s3.Bucket(stack, 'MyBucket'); + +new codebuild.Project(stack, 'MyProject', { + buildSpec: { + version: '0.2', + }, + secondarySources: [ + new codebuild.S3BucketSource({ + bucket, + path: 'some/path', + identifier: 'AddSource1', + }), + ], + secondaryArtifacts: [ + new codebuild.S3BucketBuildArtifacts({ + bucket, + path: 'another/path', + name: 'name', + identifier: 'AddArtifact1', + }), + ], +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index 6452330369a97..dbd3e4abf3cd4 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -142,7 +142,7 @@ export = { const repo = new codecommit.Repository(stack, 'MyRepo', { repositoryName: 'hello-cdk' }); - const source = new codebuild.CodeCommitSource(repo); + const source = new codebuild.CodeCommitSource({ repository: repo }); new codebuild.Project(stack, 'MyProject', { source @@ -295,7 +295,10 @@ export = { const bucket = new s3.Bucket(stack, 'MyBucket'); new codebuild.Project(stack, 'MyProject', { - source: new codebuild.S3BucketSource(bucket, 'path/to/source.zip'), + source: new codebuild.S3BucketSource({ + bucket, + path: 'path/to/source.zip', + }), environment: { buildImage: codebuild.WindowsBuildImage.WIN_SERVER_CORE_2016_BASE, }, @@ -463,13 +466,26 @@ export = { } }); test.done(); - } + }, + 'fail creating a Project when no build spec is given'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new codebuild.Project(stack, 'MyProject', { + }); + }, /buildSpec/); + + test.done(); + }, }, 'using timeout and path in S3 artifacts sets it correctly'(test: Test) { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Bucket'); new codebuild.Project(stack, 'Project', { + buildSpec: { + version: '0.2', + }, artifacts: new codebuild.S3BucketBuildArtifacts({ path: 'some/path', name: 'some_name', @@ -490,6 +506,144 @@ export = { test.done(); }, + 'secondary sources': { + 'require providing an identifier when creating a Project'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new codebuild.Project(stack, 'MyProject', { + buildSpec: { + version: '0.2', + }, + secondarySources: [ + new codebuild.CodePipelineSource(), + ], + }); + }, /identifier/); + + test.done(); + }, + + 'are not allowed for a Project with CodePipeline as Source'(test: Test) { + const stack = new cdk.Stack(); + const project = new codebuild.Project(stack, 'MyProject', { + source: new codebuild.CodePipelineSource(), + }); + + project.addSecondarySource(new codebuild.S3BucketSource({ + bucket: new s3.Bucket(stack, 'MyBucket'), + path: 'some/path', + identifier: 'id', + })); + + test.throws(() => { + expect(stack); + }, /secondary sources/); + + test.done(); + }, + + 'added with an identifer after the Project has been created are rendered in the template'(test: Test) { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'MyBucket'); + const project = new codebuild.Project(stack, 'MyProject', { + source: new codebuild.S3BucketSource({ + bucket, + path: 'some/path', + }), + }); + + project.addSecondarySource(new codebuild.S3BucketSource({ + bucket, + path: 'another/path', + identifier: 'source1', + })); + + expect(stack).to(haveResource('AWS::CodeBuild::Project', { + "SecondarySources": [ + { + "SourceIdentifier": "source1", + "Type": "S3", + }, + ], + })); + + test.done(); + }, + }, + + 'secondary artifacts': { + 'require providing an identifier when creating a Project'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new codebuild.Project(stack, 'MyProject', { + buildSpec: { + version: '0.2', + }, + secondaryArtifacts: [ + new codebuild.S3BucketBuildArtifacts({ + bucket: new s3.Bucket(stack, 'MyBucket'), + path: 'some/path', + name: 'name', + }), + ], + }); + }, /identifier/); + + test.done(); + }, + + 'are not allowed for a Project with CodePipeline as Source'(test: Test) { + const stack = new cdk.Stack(); + const project = new codebuild.Project(stack, 'MyProject', { + source: new codebuild.CodePipelineSource(), + }); + + project.addSecondaryArtifact(new codebuild.S3BucketBuildArtifacts({ + bucket: new s3.Bucket(stack, 'MyBucket'), + path: 'some/path', + name: 'name', + identifier: 'id', + })); + + test.throws(() => { + expect(stack); + }, /secondary artifacts/); + + test.done(); + }, + + 'added with an identifer after the Project has been created are rendered in the template'(test: Test) { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'MyBucket'); + const project = new codebuild.Project(stack, 'MyProject', { + source: new codebuild.S3BucketSource({ + bucket, + path: 'some/path', + }), + }); + + project.addSecondaryArtifact(new codebuild.S3BucketBuildArtifacts({ + bucket, + path: 'another/path', + name: 'name', + identifier: 'artifact1', + })); + + expect(stack).to(haveResource('AWS::CodeBuild::Project', { + "SecondaryArtifacts": [ + { + "ArtifactIdentifier": "artifact1", + "Type": "S3", + }, + ], + })); + + test.done(); + }, + }, + 'artifacts': { 'CodePipeline': { 'both source and artifacs are set to CodePipeline'(test: Test) { @@ -564,7 +718,9 @@ export = { }), /Both source and artifacts must be set to CodePipeline/); test.throws(() => new codebuild.Project(stack, 'YourProject', { - source: new codebuild.CodeCommitSource(new codecommit.Repository(stack, 'MyRepo', { repositoryName: 'boo' })), + source: new codebuild.CodeCommitSource({ + repository: new codecommit.Repository(stack, 'MyRepo', { repositoryName: 'boo' }) + }), artifacts: new codebuild.CodePipelineBuildArtifacts() }), /Both source and artifacts must be set to CodePipeline/); diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/build-action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/build-action.ts index 441b9a4d4445f..da078f34a4bd1 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/build-action.ts @@ -1,5 +1,5 @@ import cdk = require("@aws-cdk/cdk"); -import { Action, ActionCategory, CommonActionConstructProps, CommonActionProps } from "./action"; +import { Action, ActionArtifactBounds, ActionCategory, CommonActionConstructProps, CommonActionProps } from "./action"; import { Artifact } from "./artifact"; /** @@ -16,6 +16,11 @@ export interface BuildActionProps extends CommonActionProps, CommonActionConstru */ provider: string; + /** + * The upper and lower bounds on the number of input and output artifacts for this Action. + */ + artifactBounds: ActionArtifactBounds; + /** * The source action owner (could be 'AWS', 'ThirdParty' or 'Custom'). * @@ -48,7 +53,6 @@ export abstract class BuildAction extends Action { constructor(parent: cdk.Construct, name: string, props: BuildActionProps) { super(parent, name, { category: ActionCategory.Build, - artifactBounds: { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 1 }, ...props, }); diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts index 1fd10d2cd2ab3..1d20efab9c9ac 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts @@ -1,5 +1,5 @@ import cdk = require("@aws-cdk/cdk"); -import { Action, ActionCategory, CommonActionConstructProps, CommonActionProps } from "./action"; +import { Action, ActionArtifactBounds, ActionCategory, CommonActionConstructProps, CommonActionProps } from "./action"; import { Artifact } from "./artifact"; /** @@ -30,6 +30,11 @@ export interface TestActionProps extends CommonActionProps, CommonActionConstruc */ provider: string; + /** + * The upper and lower bounds on the number of input and output artifacts for this Action. + */ + artifactBounds: ActionArtifactBounds; + /** * The source action owner (could be 'AWS', 'ThirdParty' or 'Custom'). * @@ -62,7 +67,6 @@ export abstract class TestAction extends Action { constructor(parent: cdk.Construct, name: string, props: TestActionProps) { super(parent, name, { category: ActionCategory.Test, - artifactBounds: { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 0 }, ...props, }); diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json new file mode 100644 index 0000000000000..3c94a18f54246 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json @@ -0,0 +1,602 @@ +{ + "Resources": { + "MyRepoF4F48043": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "MyIntegTestTempRepo", + "Triggers": [] + } + }, + "MyRepoawscdkcodepipelinecodebuildmultipleinputsoutputsPipeline314D3A85EventRule9F75D675": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.codecommit" + ], + "resources": [ + { + "Fn::GetAtt": [ + "MyRepoF4F48043", + "Arn" + ] + } + ], + "detail-type": [ + "CodeCommit Repository State Change" + ], + "detail": { + "event": [ + "referenceUpdated" + ], + "referenceName": [ + "master" + ] + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + }, + "Id": "Pipeline", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineEventsRole46BEEA7C", + "Arn" + ] + } + } + ] + } + }, + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + } + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyRepoF4F48043", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyBuildProject30DB9D6E", + "Arn" + ] + } + }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyBuildProject30DB9D6E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "ArtifactStore": { + "Location": { + "Ref": "MyBucketF68F3FF0" + }, + "Type": "S3" + }, + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1" + }, + "Configuration": { + "RepositoryName": { + "Fn::GetAtt": [ + "MyRepoF4F48043", + "Name" + ] + }, + "BranchName": "master", + "PollForSourceChanges": false + }, + "InputArtifacts": [], + "Name": "Source1", + "OutputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinecodebuildmultipleinputsoutputsMyRepoSource1FB3F9DF8" + } + ], + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "MyBucketF68F3FF0" + }, + "S3ObjectKey": "some/path", + "PollForSourceChanges": true + }, + "InputArtifacts": [], + "Name": "Source2", + "OutputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinecodebuildmultipleinputsoutputsMyBucketSource22F03F24C" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "MyBuildProject30DB9D6E" + }, + "PrimarySource": "Artifact_awscdkcodepipelinecodebuildmultipleinputsoutputsMyRepoSource1FB3F9DF8" + }, + "InputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinecodebuildmultipleinputsoutputsMyRepoSource1FB3F9DF8" + }, + { + "Name": "Artifact_awscdkcodepipelinecodebuildmultipleinputsoutputsMyBucketSource22F03F24C" + } + ], + "Name": "Build1", + "OutputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinecodebuildmultipleinputsoutputsMyBuildProjectBuild121179895" + }, + { + "Name": "CustomOutput1" + } + ], + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Test", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "MyBuildProject30DB9D6E" + }, + "PrimarySource": "Artifact_awscdkcodepipelinecodebuildmultipleinputsoutputsMyBucketSource22F03F24C" + }, + "InputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinecodebuildmultipleinputsoutputsMyBucketSource22F03F24C" + }, + { + "Name": "Artifact_awscdkcodepipelinecodebuildmultipleinputsoutputsMyRepoSource1FB3F9DF8" + } + ], + "Name": "Build2", + "OutputArtifacts": [ + { + "Name": "CustomOutput2" + } + ], + "RunOrder": 1 + } + ], + "Name": "Build" + } + ] + }, + "DependsOn": [ + "PipelineRoleD68726F7", + "PipelineRoleDefaultPolicyC7A05455" + ] + }, + "PipelineEventsRole46BEEA7C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineEventsRoleDefaultPolicyFF4FCCE0": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "codepipeline:StartPipelineExecution", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "Roles": [ + { + "Ref": "PipelineEventsRole46BEEA7C" + } + ] + } + }, + "MyBuildProjectRole6B7E2258": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyBuildProjectRoleDefaultPolicy5604AA87": { + "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": "MyBuildProject30DB9D6E" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyBuildProject30DB9D6E" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyBuildProjectRoleDefaultPolicy5604AA87", + "Roles": [ + { + "Ref": "MyBuildProjectRole6B7E2258" + } + ] + } + }, + "MyBuildProject30DB9D6E": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/ubuntu-base:14.04", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyBuildProjectRole6B7E2258", + "Arn" + ] + }, + "Source": { + "Type": "CODEPIPELINE" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.ts new file mode 100644 index 0000000000000..0c2810277473f --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-build-multiple-inputs-outputs.ts @@ -0,0 +1,63 @@ +import codebuild = require('@aws-cdk/aws-codebuild'); +import codecommit = require('@aws-cdk/aws-codecommit'); +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import codepipeline = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-codebuild-multiple-inputs-outputs'); + +const repository = new codecommit.Repository(stack, 'MyRepo', { + repositoryName: 'MyIntegTestTempRepo', +}); +const bucket = new s3.Bucket(stack, 'MyBucket', { + versioned: true, +}); + +const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + artifactBucket: bucket, +}); + +const sourceStage = pipeline.addStage('Source'); +const sourceAction1 = repository.addToPipeline(sourceStage, 'Source1'); +const sourceAction2 = bucket.addToPipeline(sourceStage, 'Source2', { + bucketKey: 'some/path', +}); + +const project = new codebuild.PipelineProject(stack, 'MyBuildProject'); +const buildStage = pipeline.addStage('Build'); +const buildAction = project.addBuildToPipeline(buildStage, 'Build1', { + inputArtifact: sourceAction1.outputArtifact, + additionalInputArtifacts: [ + sourceAction2.outputArtifact, + ], + additionalOutputArtifactNames: [ + 'CustomOutput1', + ], +}); +const testAction = project.addTestToPipeline(buildStage, 'Build2', { + inputArtifact: sourceAction2.outputArtifact, + additionalInputArtifacts: [ + sourceAction1.outputArtifact, + ], + additionalOutputArtifactNames: [ + 'CustomOutput2', + ], +}); + +// some assertions on the Action helper methods +if (buildAction.additionalOutputArtifacts().length !== 1) { + throw new Error(`Expected build Action to have 1 additional output artifact, but was: ${buildAction.additionalOutputArtifacts()}`); +} +buildAction.additionalOutputArtifact('CustomOutput1'); // that it doesn't throw + +if (testAction.outputArtifact) { + throw new Error(`Expected test Action output Artifact to be undefined, was: ${testAction.outputArtifact}`); +} +if (testAction.additionalOutputArtifacts().length !== 1) { + throw new Error(`Expected test Action to have 1 additional output artifact, but was: ${testAction.additionalOutputArtifacts()}`); +} +testAction.additionalOutputArtifact('CustomOutput2'); // that it doesn't throw + +app.run();