From ef7e20b06e5cd952603d032954a0a0c2eda636fb Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 14 Dec 2018 18:24:51 -0800 Subject: [PATCH] feat(aws-codepipeline): support notifications on the ManualApprovalAction. Fixes #1222 --- packages/@aws-cdk/aws-codepipeline/README.md | 21 ++ .../lib/manual-approval-action.ts | 51 ++++- .../@aws-cdk/aws-codepipeline/package.json | 5 +- ...teg.pipeline-manual-approval.expected.json | 197 ++++++++++++++++++ .../test/integ.pipeline-manual-approval.ts | 26 +++ .../aws-codepipeline/test/test.pipeline.ts | 15 ++ 6 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.expected.json create mode 100644 packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.ts diff --git a/packages/@aws-cdk/aws-codepipeline/README.md b/packages/@aws-cdk/aws-codepipeline/README.md index bc9e7bc842599..b86bad6267dff 100644 --- a/packages/@aws-cdk/aws-codepipeline/README.md +++ b/packages/@aws-cdk/aws-codepipeline/README.md @@ -74,6 +74,27 @@ new codepipeline.JenkinsBuildAction(this, 'Jenkins_Build', { }); ``` +#### Manual approval Action + +This package contains an Action that stops the Pipeline until someone manually clicks the approve button: + +```typescript +const manualApprovalAction = new codepipeline.ManualApprovalAction(this, 'Approve', { + stage: approveStage, + notificationTopic: new sns.Topic(this, 'Topic'), // optional + notifyEmails: [ + 'some_email@example.com', + ], // optional + additionalInformation: 'additional info', // optional +}); +// `manualApprovalAction.notificationTopic` can be used to access the Topic +``` + +If the `notificationTopic` has not been provided, +but `notifyEmails` were, +a new SNS Topic will be created +(and accessible through the `notificationTopic` property of the Action). + ### Cross-region CodePipelines You can also use the cross-region feature to deploy resources diff --git a/packages/@aws-cdk/aws-codepipeline/lib/manual-approval-action.ts b/packages/@aws-cdk/aws-codepipeline/lib/manual-approval-action.ts index 60031f1de5c59..63089a872ed42 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/manual-approval-action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/manual-approval-action.ts @@ -1,21 +1,70 @@ import actions = require('@aws-cdk/aws-codepipeline-api'); +import sns = require('@aws-cdk/aws-sns'); import cdk = require('@aws-cdk/cdk'); -// tslint:disable-next-line:no-empty-interface +/** + * Construction properties of the {@link ManualApprovalAction}. + */ export interface ManualApprovalActionProps extends actions.CommonActionProps, actions.CommonActionConstructProps { + /** + * Optional SNS topic to send notifications to when an approval is pending. + */ + notificationTopic?: sns.TopicRef; + + /** + * A list of email addresses to subscribe to notifications when this Action is pending approval. + * If this has been provided, but not `notificationTopic`, + * a new Topic will be created. + */ + notifyEmails?: string[]; + + /** + * Any additional information that you want to include in the notification email message. + */ + additionalInformation?: string; } /** * Manual approval action. */ export class ManualApprovalAction extends actions.Action { + /** + * The SNS Topic passed when constructing the Action. + * If no Topic was passed, but `notifyEmails` were provided, + * a new Topic will be created. + */ + public readonly notificationTopic?: sns.TopicRef; + constructor(parent: cdk.Construct, name: string, props: ManualApprovalActionProps) { super(parent, name, { category: actions.ActionCategory.Approval, provider: 'Manual', artifactBounds: { minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0 }, + configuration: new cdk.Token(() => this.actionConfiguration(props)), ...props, }); + + if (props.notificationTopic) { + this.notificationTopic = props.notificationTopic; + } else if ((props.notifyEmails || []).length > 0) { + this.notificationTopic = new sns.Topic(this, 'TopicResource'); + } + + if (this.notificationTopic) { + this.notificationTopic.grantPublish(props.stage.pipeline.role); + for (const notifyEmail of props.notifyEmails || []) { + this.notificationTopic.subscribeEmail(`Subscription-${notifyEmail}`, notifyEmail); + } + } + } + + private actionConfiguration(props: ManualApprovalActionProps): any { + return this.notificationTopic + ? { + NotificationArn: this.notificationTopic.topicArn, + CustomData: props.additionalInformation, + } + : undefined; } } diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 2f63ad46d460f..549c99ce07e87 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -65,7 +65,6 @@ "@aws-cdk/aws-codedeploy": "^0.21.0", "@aws-cdk/aws-ecr": "^0.21.0", "@aws-cdk/aws-lambda": "^0.21.0", - "@aws-cdk/aws-sns": "^0.21.0", "cdk-build-tools": "^0.21.0", "cdk-integ-tools": "^0.21.0", "cfn2ts": "^0.21.0", @@ -76,6 +75,7 @@ "@aws-cdk/aws-events": "^0.21.0", "@aws-cdk/aws-iam": "^0.21.0", "@aws-cdk/aws-s3": "^0.21.0", + "@aws-cdk/aws-sns": "^0.21.0", "@aws-cdk/cdk": "^0.21.0" }, "homepage": "https://github.com/awslabs/aws-cdk", @@ -84,9 +84,10 @@ "@aws-cdk/aws-events": "^0.21.0", "@aws-cdk/aws-iam": "^0.21.0", "@aws-cdk/aws-s3": "^0.21.0", + "@aws-cdk/aws-sns": "^0.21.0", "@aws-cdk/cdk": "^0.21.0" }, "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.expected.json new file mode 100644 index 0000000000000..6015e4df4dd50 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.expected.json @@ -0,0 +1,197 @@ +{ + "Resources": { + "Bucket83908E77": { + "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": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "ManualApprovalTopicResource300641E2" + } + } + ], + "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": "Bucket83908E77" + }, + "S3ObjectKey": "file.zip", + "PollForSourceChanges": true + }, + "InputArtifacts": [], + "Name": "S3", + "OutputArtifacts": [ + { + "Name": "Artifact_awscdkcodepipelinemanualapprovalBucketS39750AFE7" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Approval", + "Owner": "AWS", + "Provider": "Manual", + "Version": "1" + }, + "Configuration": { + "NotificationArn": { + "Ref": "ManualApprovalTopicResource300641E2" + } + }, + "InputArtifacts": [], + "Name": "ManualApproval", + "OutputArtifacts": [], + "RunOrder": 1 + } + ], + "Name": "Approve" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "Bucket83908E77" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "PipelineRoleD68726F7", + "PipelineRoleDefaultPolicyC7A05455" + ] + }, + "ManualApprovalTopicResource300641E2": { + "Type": "AWS::SNS::Topic" + }, + "ManualApprovalTopicResourceSubscriptionadamruka85gmailcomBACEE98E": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": "adamruka85@gmail.com", + "Protocol": "email", + "TopicArn": { + "Ref": "ManualApprovalTopicResource300641E2" + } + } + } + } +} diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.ts new file mode 100644 index 0000000000000..90feeac7133df --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-manual-approval.ts @@ -0,0 +1,26 @@ +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-manual-approval'); + +const bucket = new s3.Bucket(stack, 'Bucket'); + +const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + artifactBucket: bucket, +}); + +const sourceStage = pipeline.addStage('Source'); +bucket.addToPipeline(sourceStage, 'S3', { + bucketKey: 'file.zip', +}); + +const approveStage = pipeline.addStage('Approve'); +new codepipeline.ManualApprovalAction(stack, 'ManualApproval', { + stage: approveStage, + notifyEmails: ['adamruka85@gmail.com'] +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts index 83177515392b3..ad8e26d2ea907 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts @@ -215,6 +215,21 @@ export = { test.done(); }, + 'manual approval Action': { + 'allows passing an SNS Topic when constructing it'(test: Test) { + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + const manualApprovalAction = new codepipeline.ManualApprovalAction(stack, 'Approve', { + stage: stageForTesting(stack), + notificationTopic: topic, + }); + + test.equal(manualApprovalAction.notificationTopic, topic); + + test.done(); + }, + }, + 'PipelineProject': { 'with a custom Project Name': { 'sets the source and artifacts to CodePipeline'(test: Test) {