Skip to content

Commit

Permalink
feat(aws-codebuild): add support for additional sources and artifact …
Browse files Browse the repository at this point in the history
…in Projects.

This also adds support for multiple input and output artifacts in the CodeBuild
CodePipeline Actions.

BREAKING CHANGE: this changes the way CodeBuild Sources are constructed
(we moved away from multiple parameters in the constructor,
in favor of the more idiomatic property interface).
  • Loading branch information
skinny85 committed Nov 8, 2018
1 parent e404316 commit 7fbf9b7
Show file tree
Hide file tree
Showing 14 changed files with 1,530 additions and 113 deletions.
109 changes: 107 additions & 2 deletions packages/@aws-cdk/aws-codebuild/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
});
```

Expand All @@ -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',
}),
});
```

Expand Down Expand Up @@ -119,3 +124,103 @@ 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 Project 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',
}),
],
// ...
});
```
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_<identifier>` 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
Unfortunately, you cannot use neither `secondarySources` nor `secondaryArtifacts`
on a Project used in CodePipeline.
However, you can get the same effect by multiple inputs and outputs when creating your CodeBuild CodePipeline Action.
So, to achieve a similar build to what was shown above,
you could do something like the following in CodePipeline:
```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',
],
});
// use buildAction.additionalOutputArtifacts as inputs to later stages...
```
**Note**: when a CodeBuild Action in a Pipeline has more than one input,
it will only use the `secondary-artifacts` field of the buildspec,
never the primary 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.
104 changes: 73 additions & 31 deletions packages/@aws-cdk/aws-codebuild/lib/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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 "<path>/build-ID/<name>" or
* "<path>/<artifactsName>" depending on whether `includeBuildId` is set to true.
* The full S3 object key will be "<path>/<build-id>/<name>" or
* "<path>/<name>" depending on whether `includeBuildID` is set to true.
*/
name: string;

Expand All @@ -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';
}
}
}
64 changes: 61 additions & 3 deletions packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -45,13 +63,23 @@ 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,
},
...props,
});

setCodeBuildNeededPermissions(props.stage, props.project, true);

handleAdditionalInputOutputArtifacts(props, this,
// pass functions to get around protected members
(artifact) => this.addInputArtifact(artifact),
(artifactName) => this.addOutputArtifact(artifactName));
}

public additionalOutputArtifacts(): codepipeline.Artifact[] {
return this._outputArtifacts.slice(1);
}
}

Expand All @@ -60,7 +88,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.
*
Expand All @@ -69,7 +98,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`.
Expand All @@ -94,6 +123,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,
},
Expand All @@ -102,6 +132,17 @@ 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));
}

public additionalOutputArtifacts(): codepipeline.Artifact[] {
return this.outputArtifact === undefined
? this._outputArtifacts
: this._outputArtifacts.slice(1);
}
}

Expand All @@ -123,3 +164,20 @@ 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);
}
}
Loading

0 comments on commit 7fbf9b7

Please sign in to comment.