Skip to content

Commit

Permalink
feat(aws-codebuild): Introduce a CodePipeline test Action.
Browse files Browse the repository at this point in the history
  • Loading branch information
skinny85 committed Oct 11, 2018
1 parent 3d91c93 commit 451e852
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ class StageDouble implements cpapi.IStage, cpapi.IInternalStage {
this.pipelineRole = pipelineRole;
}

public grantPipelineBucketRead() {
throw new Error('Unsupported');
}

public grantPipelineBucketReadWrite() {
throw new Error('Unsupported');
}
Expand Down
19 changes: 18 additions & 1 deletion packages/@aws-cdk/aws-codebuild/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,24 @@ You can also add the Project to the Pipeline directly:
```ts
// equivalent to the code above:
project.addBuildToPipeline(buildStage, 'CodeBuild');
const buildAction = project.addBuildToPipeline(buildStage, 'CodeBuild');
```
In addition to the build Action,
there is also a test Action.
It works very similarly to the build Action,
the only difference is that the test Action never produces any output artifacts.
Examples:
```ts
new codebuild.PipelineTestAction(this, 'CodeBuild', {
stage: buildStage,
project,
});

// equivalent to the code above:
project.addTestToPipeline(buildStage, 'CodeBuild');
```
### Using Project as an event target
Expand Down
80 changes: 61 additions & 19 deletions packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,33 +44,75 @@ export class PipelineBuildAction extends codepipeline.BuildAction {
// https://qiita.com/ikeisuke/items/2fbc0b80b9bbd981b41f

super(parent, name, {
stage: props.stage,
runOrder: props.runOrder,
provider: 'CodeBuild',
inputArtifact: props.inputArtifact,
outputArtifactName: props.outputArtifactName,
configuration: {
ProjectName: props.project.projectName
}
ProjectName: props.project.projectName,
},
...props,
});

const actions = [
setCodeBuildNeededPermissions(props.stage, props.project, true);
}
}

/**
* Common properties for creating {@link PipelineTestAction} -
* either directly, through its constructor,
* or through {@link ProjectRef#addTestToPipeline}.
*/
export interface CommonPipelineTestActionProps extends codepipeline.CommonActionProps {
/**
* The source to use as input for this test.
*
* @default CodePipeline will use the output of the last Action from a previous Stage as input
*/
inputArtifact?: codepipeline.Artifact;
}

/**
* Construction properties of the {@link PipelineTestAction CodeBuild test CodePipeline Action}.
*/
export interface PipelineTestActionProps extends CommonPipelineTestActionProps,
codepipeline.CommonActionConstructProps {
/**
* The build Project.
*/
project: ProjectRef;
}

export class PipelineTestAction extends codepipeline.TestAction {
constructor(parent: cdk.Construct, name: string, props: PipelineTestActionProps) {
super(parent, name, {
provider: 'CodeBuild',
configuration: {
ProjectName: props.project.projectName,
},
...props,
});

// since test Actions never produce any output,
// we only need read access to the Pipeline's Bucket
setCodeBuildNeededPermissions(props.stage, props.project, false);
}
}

function setCodeBuildNeededPermissions(stage: codepipeline.IStage, project: ProjectRef,
needsPipelineBucketWrite: boolean) {
// grant the Pipeline role the required permissions to this Project
stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
.addResource(project.projectArn)
.addActions(
'codebuild:BatchGetBuilds',
'codebuild:StartBuild',
'codebuild:StopBuild',
];

props.stage.pipelineRole.addToPolicy(new iam.PolicyStatement()
.addResource(props.project.projectArn)
.addActions(...actions));
));

// allow codebuild to read and write artifacts to the pipline's artifact bucket.
if (props.project.role) {
props.stage.grantPipelineBucketReadWrite(props.project.role);
// allow the Project access to the Pipline's artifact Bucket
if (project.role) {
if (needsPipelineBucketWrite) {
stage.grantPipelineBucketReadWrite(project.role);
} else {
stage.grantPipelineBucketRead(project.role);
}

// policy must be added as a dependency to the pipeline!!
// TODO: grants - build.addResourcePermission() and also make sure permission
// includes the pipeline role AWS principal.
}
}
22 changes: 21 additions & 1 deletion packages/@aws-cdk/aws-codebuild/lib/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/cdk');
import { BuildArtifacts, CodePipelineBuildArtifacts, NoBuildArtifacts } from './artifacts';
import { cloudformation } from './codebuild.generated';
import { CommonPipelineBuildActionProps, PipelineBuildAction } from './pipeline-actions';
import {
CommonPipelineBuildActionProps, CommonPipelineTestActionProps,
PipelineBuildAction, PipelineTestAction
} from './pipeline-actions';
import { BuildSource, NoSource } from './source';

const CODEPIPELINE_TYPE = 'CODEPIPELINE';
Expand Down Expand Up @@ -97,6 +100,23 @@ export abstract class ProjectRef extends cdk.Construct implements events.IEventR
});
}

/**
* Convenience method for creating a new {@link PipelineTestAction} test Action,
* and adding it to the given Stage.
*
* @param stage the Pipeline Stage to add the new Action to
* @param name the name of the newly created Action
* @param props the properties of the new Action
* @returns the newly created {@link PipelineBuildAction} test Action
*/
public addTestToPipeline(stage: codepipeline.IStage, name: string, props: CommonPipelineTestActionProps = {}): PipelineTestAction {
return new PipelineTestAction(this, name, {
stage,
project: this,
...props,
});
}

/**
* Defines a CloudWatch event rule triggered when the build project state
* changes. You can filter specific build status events using an event
Expand Down
25 changes: 6 additions & 19 deletions packages/@aws-cdk/aws-codepipeline-api/lib/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ export interface IStage {
*/
readonly _internal: IInternalStage;

/* Grants read permissions to the Pipeline's S3 Bucket to the given Identity.
*
* @param identity the IAM Identity to grant the permissions to
*/
grantPipelineBucketRead(identity: iam.IPrincipal): void;

/**
* Grants read & write permissions to the Pipeline's S3 Bucket to the given Identity.
*
Expand Down Expand Up @@ -238,25 +244,6 @@ export abstract class Action extends cdk.Construct {
}
}

// export class TestAction extends Action {
// constructor(parent: Stage, name: string, provider: string, artifactBounds: ActionArtifactBounds, configuration?: any) {
// super(parent, name, {
// category: ActionCategory.Test,
// provider,
// artifactBounds,
// configuration
// });
// }
// }

// export class CodeBuildTest extends TestAction {
// constructor(parent: Stage, name: string, project: codebuild.ProjectArnAttribute) {
// super(parent, name, 'CodeBuild', { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 1 }, {
// ProjectName: project
// });
// }
// }

// export class ElasticBeanstalkDeploy extends DeployAction {
// constructor(parent: Stage, name: string, applicationName: string, environmentName: string) {
// super(parent, name, 'ElasticBeanstalk', { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 0 }, {
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codepipeline-api/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './action';
export * from './build-action';
export * from './deploy-action';
export * from './source-action';
export * from './test-action';
export * from './validation';
59 changes: 59 additions & 0 deletions packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import cdk = require("@aws-cdk/cdk");
import { Action, ActionCategory, CommonActionConstructProps, CommonActionProps } from "./action";
import { Artifact } from "./artifact";

/**
* Construction properties of the low-level {@link TestAction test Action}.
*/
export interface TestActionProps extends CommonActionProps, CommonActionConstructProps {
/**
* The source to use as input for this test.
*
* @default CodePipeline will use the output of the last Action from a previous Stage as input
*/
inputArtifact?: Artifact;

/**
* The service provider that the action calls.
*
* @example 'CodeBuild'
*/
provider: string;

/**
* The source action owner (could be 'AWS', 'ThirdParty' or 'Custom').
*
* @default 'AWS'
*/
owner?: string;

/**
* The action's configuration. These are key-value pairs that specify input values for an action.
* For more information, see the AWS CodePipeline User Guide.
*
* http://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#action-requirements
*/
configuration?: any;
}

/**
* The low-level test Action.
*
* Test Actions are very similar to build Actions -
* the difference is that test Actions never have any output artifacts.
*
* You should never need to use this class directly,
* instead preferring the concrete implementations,
* like {@link codebuild.PipelineTestAction}.
*/
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,
});

this.addInputArtifact(props.inputArtifact);
}
}
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ export class Stage extends cdk.Construct implements actions.IStage, actions.IInt
return this.validateHasActions();
}

public grantPipelineBucketRead(identity: iam.IPrincipal): void {
this.pipeline.artifactBucket.grantRead(identity);
}

public grantPipelineBucketReadWrite(identity: iam.IPrincipal): void {
this.pipeline.artifactBucket.grantReadWrite(identity);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@
]
}
},
{
"Action": [
"codebuild:BatchGetBuilds",
"codebuild:StartBuild",
"codebuild:StopBuild"
],
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"MyBuildProject30DB9D6E",
"Arn"
]
}
},
{
"Action": [
"codebuild:BatchGetBuilds",
Expand Down Expand Up @@ -181,6 +195,27 @@
}
],
"RunOrder": 1
},
{
"ActionTypeId": {
"Category": "Test",
"Owner": "AWS",
"Provider": "CodeBuild",
"Version": "1"
},
"Configuration": {
"ProjectName": {
"Ref": "MyBuildProject30DB9D6E"
}
},
"InputArtifacts": [
{
"Name": "SourceArtifact"
}
],
"Name": "test",
"OutputArtifacts": [],
"RunOrder": 1
}
],
"Name": "build"
Expand Down Expand Up @@ -330,6 +365,37 @@
]
}
]
},
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"PipelineArtifactsBucket22248F97",
"Arn"
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"PipelineArtifactsBucket22248F97",
"Arn"
]
},
"/",
"*"
]
]
}
]
}
],
"Version": "2012-10-17"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ const project = new codebuild.Project(stack, 'MyBuildProject', {

const buildStage = new codepipeline.Stage(pipeline, 'build', { pipeline });
project.addBuildToPipeline(buildStage, 'build');
project.addTestToPipeline(buildStage, 'test');

app.run();

0 comments on commit 451e852

Please sign in to comment.