From 22b0557a2bcdb91148ad0e2e2e1c26f097504c21 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 8 Oct 2018 13:26:10 -0700 Subject: [PATCH] feat(aws-codebuild): Introduce a CodePipeline test Action. --- .../test/test.pipeline-actions.ts | 4 ++ .../aws-codebuild/lib/pipeline-actions.ts | 69 +++++++++++++++---- .../@aws-cdk/aws-codebuild/lib/project.ts | 22 +++++- .../aws-codepipeline-api/lib/action.ts | 26 ++----- .../aws-codepipeline-api/lib/index.ts | 1 + .../aws-codepipeline-api/lib/test-action.ts | 42 +++++++++++ .../@aws-cdk/aws-codepipeline/lib/stage.ts | 4 ++ ...g.pipeline-code-commit-build.expected.json | 66 ++++++++++++++++++ .../test/integ.pipeline-code-commit-build.ts | 4 ++ 9 files changed, 205 insertions(+), 33 deletions(-) create mode 100644 packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts index 24b8044c4e362..21ff22d10c924 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -158,6 +158,10 @@ class StageDouble implements cpapi.IStage { this.pipelineRole = pipelineRole; } + public grantPipelineBucketRead() { + throw new Error('Unsupported'); + } + public grantPipelineBucketReadWrite() { throw new Error('Unsupported'); } diff --git a/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts index 083132babbcf2..6b4a1b0bde340 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts @@ -48,23 +48,66 @@ export class PipelineBuildAction extends codepipeline.BuildAction { } }); - 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 { + /** + * The source to use as input for this test. + */ + inputArtifact: codepipeline.Artifact; +} + +/** + * Construction properties of the {@link PipelineTestAction CodeBuild test CodePipeline Action}. + */ +export interface PipelineTestActionProps extends CommonPipelineTestActionProps, codepipeline.CommonActionProps { + /** + * The build Project. + */ + project: ProjectRef; +} + +export class PipelineTestAction extends codepipeline.TestAction { + constructor(parent: cdk.Construct, name: string, props: PipelineTestActionProps) { + super(parent, name, { + stage: props.stage, + provider: 'CodeBuild', + inputArtifact: props.inputArtifact, + configuration: { + ProjectName: props.project.projectName + }, + }); + + // 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. } } diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 11c7f63c0e7fb..0b0d54d242ffa 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -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'; @@ -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.parent!, 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 diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts index 6430ad95c6ba4..d596e2c3ce76c 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts @@ -55,6 +55,13 @@ export interface IStage { */ readonly pipelineRole: iam.Role; + /** + * 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. * @@ -203,25 +210,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 }, { diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/index.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/index.ts index 74b5c6eb9d8a4..96229c6f95a9f 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/index.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/index.ts @@ -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'; diff --git a/packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts b/packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts new file mode 100644 index 0000000000000..0c8a6a9024087 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts @@ -0,0 +1,42 @@ +import cdk = require("@aws-cdk/cdk"); +import { Action, ActionCategory, CommonActionProps } from "./action"; +import { Artifact } from "./artifact"; + +/** + * Construction properties of the low-level {@link TestAction test Action}. + */ +export interface TestActionProps extends CommonActionProps { + /** + * The source to use as input for this test. + */ + inputArtifact: Artifact; + + /** + * The service provider that the action calls. + * + * @example 'CodeBuild' + */ + provider: 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; +} + +export abstract class TestAction extends Action { + constructor(parent: cdk.Construct, name: string, props: TestActionProps) { + super(parent, name, { + stage: props.stage, + category: ActionCategory.Test, + artifactBounds: { minInputs: 1, maxInputs: 1, minOutputs: 0, maxOutputs: 0 }, + provider: props.provider, + configuration: props.configuration, + }); + + this.addInputArtifact(props.inputArtifact); + } +} diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index 44c7c699577a5..8d0fb848f0831 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -104,6 +104,10 @@ export class Stage extends cdk.Construct implements actions.IStage { 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); } diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json index 4088ef1fa556d..9a2d2081d58e5 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.expected.json @@ -83,6 +83,20 @@ ] } }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyBuildProject30DB9D6E", + "Arn" + ] + } + }, { "Action": [ "codebuild:BatchGetBuilds", @@ -177,6 +191,27 @@ "Name": "build", "OutputArtifacts": [], "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Test", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "MyBuildProject30DB9D6E" + } + }, + "InputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "Name": "test", + "OutputArtifacts": [], + "RunOrder": 2 } ], "Name": "build" @@ -326,6 +361,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" diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.ts index 0fc1b075f2bf8..8614c139442ce 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-commit-build.ts @@ -28,5 +28,9 @@ const buildStage = new codepipeline.Stage(pipeline, 'build', { pipeline }); project.addBuildToPipeline(buildStage, 'build', { inputArtifact: source.artifact, }); +const testAction = project.addTestToPipeline(buildStage, 'test', { + inputArtifact: source.artifact, +}); +testAction.runOrder = 2; app.run();