From 8f1a5e8fb0ce062c6297963cc6644330a61feeed Mon Sep 17 00:00:00 2001 From: Thorsten Hoeger Date: Thu, 31 Jan 2019 09:08:01 +0100 Subject: [PATCH] feat(s3): Add DeployAction for codepipeline (#1596) --- .../integ.pipeline-s3-deploy.expected.json | 223 ++++++++++++++++++ .../test/integ.pipeline-s3-deploy.ts | 30 +++ packages/@aws-cdk/aws-s3/README.md | 20 ++ packages/@aws-cdk/aws-s3/lib/bucket.ts | 24 +- packages/@aws-cdk/aws-s3/lib/index.ts | 2 +- ...pipeline-action.ts => pipeline-actions.ts} | 63 ++++- 6 files changed, 358 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-s3-deploy.expected.json create mode 100644 packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-s3-deploy.ts rename packages/@aws-cdk/aws-s3/lib/{pipeline-action.ts => pipeline-actions.ts} (58%) diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-s3-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-s3-deploy.expected.json new file mode 100644 index 0000000000000..c490f7a2cd023 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-s3-deploy.expected.json @@ -0,0 +1,223 @@ +{ + "Resources": { + "PipelineArtifactsBucket22248F97": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + }, + "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": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "DeployBucket67E2C076", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "DeployBucket67E2C076", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "PipelineBucketB967BD35" + }, + "S3ObjectKey": "key" + }, + "InputArtifacts": [], + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "BucketName": { + "Ref": "DeployBucket67E2C076" + }, + "Extract": "true" + }, + "InputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "Name": "DeployAction", + "OutputArtifacts": [], + "RunOrder": 1 + } + ], + "Name": "Deploy" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "PipelineRoleD68726F7", + "PipelineRoleDefaultPolicyC7A05455" + ] + }, + "PipelineBucketB967BD35": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + } + }, + "DeployBucket67E2C076": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + } + } +} diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-s3-deploy.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-s3-deploy.ts new file mode 100644 index 0000000000000..dc0943aa77ac5 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-s3-deploy.ts @@ -0,0 +1,30 @@ +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-s3-deploy'); + +const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); + +const sourceStage = new codepipeline.Stage(pipeline, 'Source', { pipeline }); +const bucket = new s3.Bucket(stack, 'PipelineBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.Destroy, +}); +const sourceAction = new s3.PipelineSourceAction(stack, 'Source', { + stage: sourceStage, + outputArtifactName: 'SourceArtifact', + bucket, + bucketKey: 'key', +}); + +const deployBucket = new s3.Bucket(stack, 'DeployBucket', {}); + +const deployStage = new codepipeline.Stage(pipeline, 'Deploy', { pipeline }); +deployBucket.addToPipelineAsDeploy(deployStage, 'DeployAction', { + inputArtifact: sourceAction.outputArtifact, +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 343795f6b2815..3de0245cdb76e 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -132,6 +132,26 @@ const sourceAction = sourceBucket.addToPipeline(sourceStage, 'S3Source', { }); ``` +### Buckets as deploy targets in CodePipeline + +This package also defines an Action that allows you to use a +Bucket as a deployment target in CodePipeline: + +```ts +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import s3 = require('@aws-cdk/aws-s3'); + +const targetBucket = new s3.Bucket(this, 'MyBucket', {}); + +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const deployStage = pipeline.addStage('Deploy'); +const deployAction = new s3.PipelineDeployAction(this, 'S3Deploy', { + stage: deployStage, + bucket: targetBucket, + inputArtifact: sourceAction.outputArtifact, +}); +``` + ### Sharing buckets between stacks To use a bucket in a different stack in the same CDK application, pass the object to the other stack: diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index b33445332bbab..e10dedb078c2f 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -7,7 +7,10 @@ import cdk = require('@aws-cdk/cdk'); import { BucketPolicy } from './bucket-policy'; import { BucketNotifications } from './notifications-resource'; import perms = require('./perms'); -import { CommonPipelineSourceActionProps, PipelineSourceAction } from './pipeline-action'; +import { + CommonPipelineDeployActionProps, CommonPipelineSourceActionProps, + PipelineDeployAction, PipelineSourceAction +} from './pipeline-actions'; import { LifecycleRule } from './rule'; import { CfnBucket } from './s3.generated'; import { parseBucketArn, parseBucketName } from './util'; @@ -64,6 +67,17 @@ export interface IBucket extends cdk.IConstruct { */ addToPipeline(stage: actions.IStage, name: string, props: CommonPipelineSourceActionProps): PipelineSourceAction; + /** + * Convenience method for creating a new {@link PipelineDeployAction}, + * 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 optional properties of the new Action + * @returns the newly created {@link PipelineDeployAction} + */ + addToPipelineAsDeploy(stage: actions.IStage, name: string, props?: CommonPipelineDeployActionProps): PipelineDeployAction; + /** * Adds a statement to the resource policy for a principal (i.e. * account/role/service) to perform actions on this bucket and/or it's @@ -301,6 +315,14 @@ export abstract class BucketBase extends cdk.Construct implements IBucket { }); } + public addToPipelineAsDeploy(stage: actions.IStage, name: string, props: CommonPipelineDeployActionProps = {}): PipelineDeployAction { + return new PipelineDeployAction(this, name, { + stage, + bucket: this, + ...props, + }); + } + public onPutObject(name: string, target?: events.IEventRuleTarget, path?: string): events.EventRule { const eventRule = new events.EventRule(this, name, { eventPattern: { diff --git a/packages/@aws-cdk/aws-s3/lib/index.ts b/packages/@aws-cdk/aws-s3/lib/index.ts index 40bb9e593e176..f7d6f0d21696e 100644 --- a/packages/@aws-cdk/aws-s3/lib/index.ts +++ b/packages/@aws-cdk/aws-s3/lib/index.ts @@ -1,6 +1,6 @@ export * from './bucket'; export * from './bucket-policy'; -export * from './pipeline-action'; +export * from './pipeline-actions'; export * from './rule'; // AWS::S3 CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts b/packages/@aws-cdk/aws-s3/lib/pipeline-actions.ts similarity index 58% rename from packages/@aws-cdk/aws-s3/lib/pipeline-action.ts rename to packages/@aws-cdk/aws-s3/lib/pipeline-actions.ts index 44ab1f1ed1c1e..6dbb6fcfd24c0 100644 --- a/packages/@aws-cdk/aws-s3/lib/pipeline-action.ts +++ b/packages/@aws-cdk/aws-s3/lib/pipeline-actions.ts @@ -38,8 +38,7 @@ export interface CommonPipelineSourceActionProps extends codepipeline.CommonActi /** * Construction properties of the {@link PipelineSourceAction S3 source Action}. */ -export interface PipelineSourceActionProps extends CommonPipelineSourceActionProps, - codepipeline.CommonActionConstructProps { +export interface PipelineSourceActionProps extends CommonPipelineSourceActionProps, codepipeline.CommonActionConstructProps { /** * The Amazon S3 bucket that stores the source code */ @@ -70,3 +69,63 @@ export class PipelineSourceAction extends codepipeline.SourceAction { props.bucket.grantRead(props.stage.pipeline.role); } } + +/** + * Common properties for creating {@link PipelineDeployAction} - + * either directly, through its constructor, + * or through {@link IBucket#addToPipelineAsDeploy}. + */ +export interface CommonPipelineDeployActionProps extends codepipeline.CommonActionProps { + /** + * Should the deploy action extract the artifact before deploying to Amazon S3. + * + * @default true + */ + extract?: boolean; + + /** + * The key of the target object. This is required if extract is false. + */ + objectKey?: string; + + /** + * The inputArtifact to deploy to Amazon S3. + */ + inputArtifact?: codepipeline.Artifact; +} + +/** + * Construction properties of the {@link PipelineDeployAction S3 deploy Action}. + */ +export interface PipelineDeployActionProps extends CommonPipelineDeployActionProps, + codepipeline.CommonActionConstructProps { + /** + * The Amazon S3 bucket that is the deploy target. + */ + bucket: IBucket; +} + +/** + * Deploys the sourceArtifact to Amazon S3. + */ +export class PipelineDeployAction extends codepipeline.DeployAction { + constructor(scope: cdk.Construct, id: string, props: PipelineDeployActionProps) { + super(scope, id, { + provider: 'S3', + artifactBounds: { + minInputs: 1, + maxInputs: 1, + minOutputs: 0, + maxOutputs: 0, + }, + configuration: { + BucketName: props.bucket.bucketName, + Extract: (props.extract === false) ? 'false' : 'true', + ObjectKey: props.objectKey, + }, + ...props, + }); + // pipeline needs permissions to write to the S3 bucket + props.bucket.grantWrite(props.stage.pipeline.role); + } +}