Skip to content

Commit 068fa46

Browse files
authored
feat(aws-codepipeline): support notifications on the ManualApprovalAction. (#1368)
Fixes #1222
1 parent fbd7728 commit 068fa46

File tree

6 files changed

+312
-3
lines changed

6 files changed

+312
-3
lines changed

packages/@aws-cdk/aws-codepipeline/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,27 @@ new codepipeline.JenkinsBuildAction(this, 'Jenkins_Build', {
7474
});
7575
```
7676

77+
#### Manual approval Action
78+
79+
This package contains an Action that stops the Pipeline until someone manually clicks the approve button:
80+
81+
```typescript
82+
const manualApprovalAction = new codepipeline.ManualApprovalAction(this, 'Approve', {
83+
stage: approveStage,
84+
notificationTopic: new sns.Topic(this, 'Topic'), // optional
85+
notifyEmails: [
86+
'some_email@example.com',
87+
], // optional
88+
additionalInformation: 'additional info', // optional
89+
});
90+
// `manualApprovalAction.notificationTopic` can be used to access the Topic
91+
```
92+
93+
If the `notificationTopic` has not been provided,
94+
but `notifyEmails` were,
95+
a new SNS Topic will be created
96+
(and accessible through the `notificationTopic` property of the Action).
97+
7798
### Cross-region CodePipelines
7899

79100
You can also use the cross-region feature to deploy resources
Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,70 @@
11
import actions = require('@aws-cdk/aws-codepipeline-api');
2+
import sns = require('@aws-cdk/aws-sns');
23
import cdk = require('@aws-cdk/cdk');
34

4-
// tslint:disable-next-line:no-empty-interface
5+
/**
6+
* Construction properties of the {@link ManualApprovalAction}.
7+
*/
58
export interface ManualApprovalActionProps extends actions.CommonActionProps,
69
actions.CommonActionConstructProps {
10+
/**
11+
* Optional SNS topic to send notifications to when an approval is pending.
12+
*/
13+
notificationTopic?: sns.TopicRef;
14+
15+
/**
16+
* A list of email addresses to subscribe to notifications when this Action is pending approval.
17+
* If this has been provided, but not `notificationTopic`,
18+
* a new Topic will be created.
19+
*/
20+
notifyEmails?: string[];
21+
22+
/**
23+
* Any additional information that you want to include in the notification email message.
24+
*/
25+
additionalInformation?: string;
726
}
827

928
/**
1029
* Manual approval action.
1130
*/
1231
export class ManualApprovalAction extends actions.Action {
32+
/**
33+
* The SNS Topic passed when constructing the Action.
34+
* If no Topic was passed, but `notifyEmails` were provided,
35+
* a new Topic will be created.
36+
*/
37+
public readonly notificationTopic?: sns.TopicRef;
38+
1339
constructor(parent: cdk.Construct, name: string, props: ManualApprovalActionProps) {
1440
super(parent, name, {
1541
category: actions.ActionCategory.Approval,
1642
provider: 'Manual',
1743
artifactBounds: { minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0 },
44+
configuration: new cdk.Token(() => this.actionConfiguration(props)),
1845
...props,
1946
});
47+
48+
if (props.notificationTopic) {
49+
this.notificationTopic = props.notificationTopic;
50+
} else if ((props.notifyEmails || []).length > 0) {
51+
this.notificationTopic = new sns.Topic(this, 'TopicResource');
52+
}
53+
54+
if (this.notificationTopic) {
55+
this.notificationTopic.grantPublish(props.stage.pipeline.role);
56+
for (const notifyEmail of props.notifyEmails || []) {
57+
this.notificationTopic.subscribeEmail(`Subscription-${notifyEmail}`, notifyEmail);
58+
}
59+
}
60+
}
61+
62+
private actionConfiguration(props: ManualApprovalActionProps): any {
63+
return this.notificationTopic
64+
? {
65+
NotificationArn: this.notificationTopic.topicArn,
66+
CustomData: props.additionalInformation,
67+
}
68+
: undefined;
2069
}
2170
}

packages/@aws-cdk/aws-codepipeline/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
"@aws-cdk/aws-codedeploy": "^0.21.0",
6666
"@aws-cdk/aws-ecr": "^0.21.0",
6767
"@aws-cdk/aws-lambda": "^0.21.0",
68-
"@aws-cdk/aws-sns": "^0.21.0",
6968
"cdk-build-tools": "^0.21.0",
7069
"cdk-integ-tools": "^0.21.0",
7170
"cfn2ts": "^0.21.0",
@@ -76,6 +75,7 @@
7675
"@aws-cdk/aws-events": "^0.21.0",
7776
"@aws-cdk/aws-iam": "^0.21.0",
7877
"@aws-cdk/aws-s3": "^0.21.0",
78+
"@aws-cdk/aws-sns": "^0.21.0",
7979
"@aws-cdk/cdk": "^0.21.0"
8080
},
8181
"homepage": "https://github.com/awslabs/aws-cdk",
@@ -84,9 +84,10 @@
8484
"@aws-cdk/aws-events": "^0.21.0",
8585
"@aws-cdk/aws-iam": "^0.21.0",
8686
"@aws-cdk/aws-s3": "^0.21.0",
87+
"@aws-cdk/aws-sns": "^0.21.0",
8788
"@aws-cdk/cdk": "^0.21.0"
8889
},
8990
"engines": {
9091
"node": ">= 8.10.0"
9192
}
92-
}
93+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
{
2+
"Resources": {
3+
"Bucket83908E77": {
4+
"Type": "AWS::S3::Bucket",
5+
"DeletionPolicy": "Retain"
6+
},
7+
"PipelineRoleD68726F7": {
8+
"Type": "AWS::IAM::Role",
9+
"Properties": {
10+
"AssumeRolePolicyDocument": {
11+
"Statement": [
12+
{
13+
"Action": "sts:AssumeRole",
14+
"Effect": "Allow",
15+
"Principal": {
16+
"Service": "codepipeline.amazonaws.com"
17+
}
18+
}
19+
],
20+
"Version": "2012-10-17"
21+
}
22+
}
23+
},
24+
"PipelineRoleDefaultPolicyC7A05455": {
25+
"Type": "AWS::IAM::Policy",
26+
"Properties": {
27+
"PolicyDocument": {
28+
"Statement": [
29+
{
30+
"Action": [
31+
"s3:GetObject*",
32+
"s3:GetBucket*",
33+
"s3:List*",
34+
"s3:DeleteObject*",
35+
"s3:PutObject*",
36+
"s3:Abort*"
37+
],
38+
"Effect": "Allow",
39+
"Resource": [
40+
{
41+
"Fn::GetAtt": [
42+
"Bucket83908E77",
43+
"Arn"
44+
]
45+
},
46+
{
47+
"Fn::Join": [
48+
"",
49+
[
50+
{
51+
"Fn::GetAtt": [
52+
"Bucket83908E77",
53+
"Arn"
54+
]
55+
},
56+
"/*"
57+
]
58+
]
59+
}
60+
]
61+
},
62+
{
63+
"Action": [
64+
"s3:GetObject*",
65+
"s3:GetBucket*",
66+
"s3:List*"
67+
],
68+
"Effect": "Allow",
69+
"Resource": [
70+
{
71+
"Fn::GetAtt": [
72+
"Bucket83908E77",
73+
"Arn"
74+
]
75+
},
76+
{
77+
"Fn::Join": [
78+
"",
79+
[
80+
{
81+
"Fn::GetAtt": [
82+
"Bucket83908E77",
83+
"Arn"
84+
]
85+
},
86+
"/*"
87+
]
88+
]
89+
}
90+
]
91+
},
92+
{
93+
"Action": "sns:Publish",
94+
"Effect": "Allow",
95+
"Resource": {
96+
"Ref": "ManualApprovalTopicResource300641E2"
97+
}
98+
}
99+
],
100+
"Version": "2012-10-17"
101+
},
102+
"PolicyName": "PipelineRoleDefaultPolicyC7A05455",
103+
"Roles": [
104+
{
105+
"Ref": "PipelineRoleD68726F7"
106+
}
107+
]
108+
}
109+
},
110+
"PipelineC660917D": {
111+
"Type": "AWS::CodePipeline::Pipeline",
112+
"Properties": {
113+
"RoleArn": {
114+
"Fn::GetAtt": [
115+
"PipelineRoleD68726F7",
116+
"Arn"
117+
]
118+
},
119+
"Stages": [
120+
{
121+
"Actions": [
122+
{
123+
"ActionTypeId": {
124+
"Category": "Source",
125+
"Owner": "AWS",
126+
"Provider": "S3",
127+
"Version": "1"
128+
},
129+
"Configuration": {
130+
"S3Bucket": {
131+
"Ref": "Bucket83908E77"
132+
},
133+
"S3ObjectKey": "file.zip",
134+
"PollForSourceChanges": true
135+
},
136+
"InputArtifacts": [],
137+
"Name": "S3",
138+
"OutputArtifacts": [
139+
{
140+
"Name": "Artifact_awscdkcodepipelinemanualapprovalBucketS39750AFE7"
141+
}
142+
],
143+
"RunOrder": 1
144+
}
145+
],
146+
"Name": "Source"
147+
},
148+
{
149+
"Actions": [
150+
{
151+
"ActionTypeId": {
152+
"Category": "Approval",
153+
"Owner": "AWS",
154+
"Provider": "Manual",
155+
"Version": "1"
156+
},
157+
"Configuration": {
158+
"NotificationArn": {
159+
"Ref": "ManualApprovalTopicResource300641E2"
160+
}
161+
},
162+
"InputArtifacts": [],
163+
"Name": "ManualApproval",
164+
"OutputArtifacts": [],
165+
"RunOrder": 1
166+
}
167+
],
168+
"Name": "Approve"
169+
}
170+
],
171+
"ArtifactStore": {
172+
"Location": {
173+
"Ref": "Bucket83908E77"
174+
},
175+
"Type": "S3"
176+
}
177+
},
178+
"DependsOn": [
179+
"PipelineRoleD68726F7",
180+
"PipelineRoleDefaultPolicyC7A05455"
181+
]
182+
},
183+
"ManualApprovalTopicResource300641E2": {
184+
"Type": "AWS::SNS::Topic"
185+
},
186+
"ManualApprovalTopicResourceSubscriptionadamruka85gmailcomBACEE98E": {
187+
"Type": "AWS::SNS::Subscription",
188+
"Properties": {
189+
"Endpoint": "adamruka85@gmail.com",
190+
"Protocol": "email",
191+
"TopicArn": {
192+
"Ref": "ManualApprovalTopicResource300641E2"
193+
}
194+
}
195+
}
196+
}
197+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import s3 = require('@aws-cdk/aws-s3');
2+
import cdk = require('@aws-cdk/cdk');
3+
import codepipeline = require('../lib');
4+
5+
const app = new cdk.App();
6+
7+
const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-manual-approval');
8+
9+
const bucket = new s3.Bucket(stack, 'Bucket');
10+
11+
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', {
12+
artifactBucket: bucket,
13+
});
14+
15+
const sourceStage = pipeline.addStage('Source');
16+
bucket.addToPipeline(sourceStage, 'S3', {
17+
bucketKey: 'file.zip',
18+
});
19+
20+
const approveStage = pipeline.addStage('Approve');
21+
new codepipeline.ManualApprovalAction(stack, 'ManualApproval', {
22+
stage: approveStage,
23+
notifyEmails: ['adamruka85@gmail.com']
24+
});
25+
26+
app.run();

packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,21 @@ export = {
215215
test.done();
216216
},
217217

218+
'manual approval Action': {
219+
'allows passing an SNS Topic when constructing it'(test: Test) {
220+
const stack = new cdk.Stack();
221+
const topic = new sns.Topic(stack, 'Topic');
222+
const manualApprovalAction = new codepipeline.ManualApprovalAction(stack, 'Approve', {
223+
stage: stageForTesting(stack),
224+
notificationTopic: topic,
225+
});
226+
227+
test.equal(manualApprovalAction.notificationTopic, topic);
228+
229+
test.done();
230+
},
231+
},
232+
218233
'PipelineProject': {
219234
'with a custom Project Name': {
220235
'sets the source and artifacts to CodePipeline'(test: Test) {

0 commit comments

Comments
 (0)