From 770f9aab2e946ca6817f92613c911c8e49c170a7 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 19 Oct 2018 17:13:21 -0700 Subject: [PATCH] feat(aws-codebuild): Introduce a CodePipeline test Action. (#873) --- .../test/test.pipeline-actions.ts | 4 + packages/@aws-cdk/aws-codebuild/README.md | 25 +++++- .../aws-codebuild/lib/pipeline-actions.ts | 89 +++++++++++++++---- .../@aws-cdk/aws-codebuild/lib/project.ts | 22 ++++- .../aws-codepipeline-api/lib/action.ts | 25 ++---- .../aws-codepipeline-api/lib/index.ts | 1 + .../aws-codepipeline-api/lib/test-action.ts | 74 +++++++++++++++ .../@aws-cdk/aws-codepipeline/lib/stage.ts | 4 + ...g.pipeline-code-commit-build.expected.json | 66 ++++++++++++++ .../test/integ.pipeline-code-commit-build.ts | 1 + 10 files changed, 271 insertions(+), 40 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 2b7efef528937..071e80fc76253 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.pipeline-actions.ts @@ -196,6 +196,10 @@ class StageDouble implements cpapi.IStage, cpapi.IInternalStage { this.pipelineRole = pipelineRole; } + public grantPipelineBucketRead() { + throw new Error('Unsupported'); + } + public grantPipelineBucketReadWrite() { throw new Error('Unsupported'); } diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index c477fdd6beeba..7665ab83df606 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -75,7 +75,30 @@ 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 does not always produce an output artifact. + +Examples: + +```ts +new codebuild.PipelineTestAction(this, 'IntegrationTest', { + stage: buildStage, + project, + // outputArtifactName is optional - if you don't specify it, + // the Action will have an undefined `outputArtifact` property + outputArtifactName: 'IntegrationTestOutput', +}); + +// equivalent to the code above: +project.addTestToPipeline(buildStage, 'IntegrationTest', { + // of course, this property is optional here as well + outputArtifactName: 'IntegrationTestOutput', +}); ``` ### Using Project as an event target diff --git a/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts b/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts index 6a9d0a2aed24d..4969851ee7b0b 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/pipeline-actions.ts @@ -44,33 +44,84 @@ 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; + + /** + * The optional name of the 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`. + * + * @default the Action will not have an output artifact + */ + outputArtifactName?: string; +} + +/** + * 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, + }); + + // the Action needs write permissions only if it's producing an output artifact + setCodeBuildNeededPermissions(props.stage, props.project, !!props.outputArtifactName); + } +} + +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 7886d9d33c566..fdb2573271636 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, 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 d3fa2ba3b3ea4..c43d013e4ac3e 100644 --- a/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/action.ts @@ -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. * @@ -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 }, { 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..1fd10d2cd2ab3 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-api/lib/test-action.ts @@ -0,0 +1,74 @@ +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 optional name of the 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`. + * + * @default the Action will not have an output artifact + */ + outputArtifactName?: string; + + /** + * 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 don't have to have an output artifact. + * + * You should never need to use this class directly, + * instead preferring the concrete implementations, + * like {@link codebuild.PipelineTestAction}. + */ +export abstract class TestAction extends Action { + public readonly outputArtifact?: Artifact; + + 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); + if (props.outputArtifactName) { + this.outputArtifact = this.addOutputArtifact(props.outputArtifactName); + } + } +} diff --git a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts index 665992e32d757..05e4df56f81cd 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/stage.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/stage.ts @@ -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); } 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 f8b7694208759..d2e75b8691c6d 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", @@ -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" @@ -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" 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 41baeac27cab0..28292f090f095 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 @@ -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();