Skip to content

Commit 859248a

Browse files
authored
feat(cloudformation): Add removalPolicy on CustomResource (#2770)
Allows controlling the removal policy of custom resources, as these can occasionally be stateful (depending on the handler implementation).
1 parent 83eee09 commit 859248a

File tree

19 files changed

+154
-56
lines changed

19 files changed

+154
-56
lines changed

packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
7979
await respond('SUCCESS', 'OK', physicalResourceId, data);
8080
} catch (e) {
8181
console.log(e);
82-
await respond('FAILED', e.message, context.logStreamName, {});
82+
await respond('FAILED', e.message || 'Internal Error', context.logStreamName, {});
8383
}
8484

8585
function respond(responseStatus: string, reason: string, physicalResourceId: string, data: any) {

packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import lambda = require('@aws-cdk/aws-lambda');
22
import sns = require('@aws-cdk/aws-sns');
3-
import { CfnResource, Construct, Resource } from '@aws-cdk/cdk';
3+
import { CfnResource, Construct, RemovalPolicy, Resource } from '@aws-cdk/cdk';
44
import { CfnCustomResource } from './cloudformation.generated';
55

66
/**
@@ -21,9 +21,7 @@ export class CustomResourceProvider {
2121
*/
2222
public static topic(topic: sns.ITopic) { return new CustomResourceProvider(topic.topicArn); }
2323

24-
private constructor(public readonly serviceToken: string) {
25-
26-
}
24+
private constructor(public readonly serviceToken: string) {}
2725
}
2826

2927
/**
@@ -67,6 +65,13 @@ export interface CustomResourceProps {
6765
* @default - AWS::CloudFormation::CustomResource
6866
*/
6967
readonly resourceType?: string;
68+
69+
/**
70+
* The policy to apply when this resource is removed from the application.
71+
*
72+
* @default cdk.RemovalPolicy.Destroy
73+
*/
74+
readonly removalPolicy?: RemovalPolicy;
7075
}
7176

7277
/**
@@ -91,6 +96,8 @@ export class CustomResource extends Resource {
9196
...uppercaseProperties(props.properties || {})
9297
}
9398
});
99+
100+
this.resource.applyRemovalPolicy(props.removalPolicy, { default: RemovalPolicy.Destroy });
94101
}
95102

96103
public getAtt(attributeName: string) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"devDependencies": {
6666
"@aws-cdk/assert": "^0.34.0",
6767
"@aws-cdk/aws-events": "^0.34.0",
68+
"@aws-cdk/aws-ssm": "^0.34.0",
6869
"@types/aws-lambda": "^8.10.26",
6970
"@types/nock": "^10.0.3",
7071
"@types/sinon": "^7.0.12",
@@ -104,4 +105,4 @@
104105
]
105106
},
106107
"stability": "experimental"
107-
}
108+
}

packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"Ref": "TopicBFC7AF6E"
3939
}
4040
}
41-
}
41+
},
42+
"DeletionPolicy": "Delete"
4243
},
4344
"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": {
4445
"Type": "AWS::IAM::Role",
@@ -186,6 +187,17 @@
186187
"action": "listTopics",
187188
"physicalResourceIdPath": "Topics.0.TopicArn"
188189
}
190+
},
191+
"DependsOn": [
192+
"TopicBFC7AF6E"
193+
],
194+
"DeletionPolicy": "Delete"
195+
},
196+
"DummyParameter53662B67": {
197+
"Type": "AWS::SSM::Parameter",
198+
"Properties": {
199+
"Type": "String",
200+
"Value": "1337"
189201
}
190202
},
191203
"GetParameter42B0A00E": {
@@ -201,7 +213,9 @@
201213
"service": "SSM",
202214
"action": "getParameter",
203215
"parameters": {
204-
"Name": "my-parameter",
216+
"Name": {
217+
"Ref": "DummyParameter53662B67"
218+
},
205219
"WithDecryption": true
206220
},
207221
"physicalResourceIdPath": "Parameter.ARN"
@@ -210,12 +224,15 @@
210224
"service": "SSM",
211225
"action": "getParameter",
212226
"parameters": {
213-
"Name": "my-parameter",
227+
"Name": {
228+
"Ref": "DummyParameter53662B67"
229+
},
214230
"WithDecryption": true
215231
},
216232
"physicalResourceIdPath": "Parameter.ARN"
217233
}
218-
}
234+
},
235+
"DeletionPolicy": "Delete"
219236
}
220237
},
221238
"Parameters": {

packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env node
22
import sns = require('@aws-cdk/aws-sns');
3+
import ssm = require('@aws-cdk/aws-ssm');
34
import cdk = require('@aws-cdk/cdk');
45
import { AwsCustomResource } from '../lib';
56

@@ -28,13 +29,17 @@ const listTopics = new AwsCustomResource(stack, 'ListTopics', {
2829
physicalResourceIdPath: 'Topics.0.TopicArn'
2930
}
3031
});
32+
listTopics.node.addDependency(topic);
3133

34+
const ssmParameter = new ssm.StringParameter(stack, 'DummyParameter', {
35+
stringValue: '1337',
36+
});
3237
const getParameter = new AwsCustomResource(stack, 'GetParameter', {
3338
onUpdate: {
3439
service: 'SSM',
3540
action: 'getParameter',
3641
parameters: {
37-
Name: 'my-parameter',
42+
Name: ssmParameter.parameterName,
3843
WithDecryption: true
3944
},
4045
physicalResourceIdPath: 'Parameter.ARN'

packages/@aws-cdk/aws-cloudformation/test/integ.trivial-lambda-resource.expected.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
]
1111
},
1212
"Message": "CustomResource says hello"
13-
}
13+
},
14+
"DeletionPolicy": "Delete"
1415
},
1516
"SingletonLambdaf7d4f7304ee111e89c2dfa7ae01bbebcServiceRoleFE9ABB04": {
1617
"Type": "AWS::IAM::Role",

packages/@aws-cdk/aws-cloudformation/test/test.resource.ts

Lines changed: 83 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,65 @@
1-
import { expect, haveResource } from '@aws-cdk/assert';
1+
import { expect, haveResource, ResourcePart } from '@aws-cdk/assert';
22
import lambda = require('@aws-cdk/aws-lambda');
33
import sns = require('@aws-cdk/aws-sns');
44
import cdk = require('@aws-cdk/cdk');
5-
import { Test } from 'nodeunit';
5+
import { Test, testCase } from 'nodeunit';
66
import { CustomResource, CustomResourceProvider } from '../lib';
77

88
// tslint:disable:object-literal-key-quotes
99

10-
export = {
10+
export = testCase({
11+
'custom resources honor removalPolicy': {
12+
'unspecified (aka .Destroy)'(test: Test) {
13+
// GIVEN
14+
const app = new cdk.App();
15+
const stack = new cdk.Stack(app, 'Test');
16+
17+
// WHEN
18+
new TestCustomResource(stack, 'Custom');
19+
20+
// THEN
21+
expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', {}, ResourcePart.CompleteDefinition));
22+
test.equal(app.synth().tryGetArtifact(stack.stackName)!.findMetadataByType('aws:cdk:protected').length, 0);
23+
24+
test.done();
25+
},
26+
27+
'.Destroy'(test: Test) {
28+
// GIVEN
29+
const app = new cdk.App();
30+
const stack = new cdk.Stack(app, 'Test');
31+
32+
// WHEN
33+
new TestCustomResource(stack, 'Custom', { removalPolicy: cdk.RemovalPolicy.Destroy });
34+
35+
// THEN
36+
expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', {}, ResourcePart.CompleteDefinition));
37+
test.equal(app.synth().tryGetArtifact(stack.stackName)!.findMetadataByType('aws:cdk:protected').length, 0);
38+
39+
test.done();
40+
},
41+
42+
'.Retain'(test: Test) {
43+
// GIVEN
44+
const app = new cdk.App();
45+
const stack = new cdk.Stack(app, 'Test');
46+
47+
// WHEN
48+
new TestCustomResource(stack, 'Custom', { removalPolicy: cdk.RemovalPolicy.Retain });
49+
50+
// THEN
51+
expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', {
52+
DeletionPolicy: 'Retain',
53+
}, ResourcePart.CompleteDefinition));
54+
55+
test.done();
56+
},
57+
},
58+
1159
'custom resource is added twice, lambda is added once'(test: Test) {
1260
// GIVEN
13-
const stack = new cdk.Stack();
61+
const app = new cdk.App();
62+
const stack = new cdk.Stack(app, 'Test');
1463

1564
// WHEN
1665
new TestCustomResource(stack, 'Custom1');
@@ -61,34 +110,37 @@ export = {
61110
]
62111
},
63112
"Custom1D319B237": {
64-
"Type": "AWS::CloudFormation::CustomResource",
65-
"Properties": {
66-
"ServiceToken": {
67-
"Fn::GetAtt": [
68-
"SingletonLambdaTestCustomResourceProviderA9255269",
69-
"Arn"
70-
]
113+
"Type": "AWS::CloudFormation::CustomResource",
114+
"DeletionPolicy": "Delete",
115+
"Properties": {
116+
"ServiceToken": {
117+
"Fn::GetAtt": [
118+
"SingletonLambdaTestCustomResourceProviderA9255269",
119+
"Arn"
120+
]
121+
}
71122
}
72-
}
73123
},
74124
"Custom2DD5FB44D": {
75-
"Type": "AWS::CloudFormation::CustomResource",
76-
"Properties": {
77-
"ServiceToken": {
78-
"Fn::GetAtt": [
79-
"SingletonLambdaTestCustomResourceProviderA9255269",
80-
"Arn"
81-
]
125+
"Type": "AWS::CloudFormation::CustomResource",
126+
"DeletionPolicy": "Delete",
127+
"Properties": {
128+
"ServiceToken": {
129+
"Fn::GetAtt": [
130+
"SingletonLambdaTestCustomResourceProviderA9255269",
131+
"Arn"
132+
]
133+
}
82134
}
83135
}
84-
}
85136
}
86137
});
87138
test.done();
88139
},
89140

90141
'custom resources can specify a resource type that starts with Custom::'(test: Test) {
91-
const stack = new cdk.Stack();
142+
const app = new cdk.App();
143+
const stack = new cdk.Stack(app, 'Test');
92144
new CustomResource(stack, 'MyCustomResource', {
93145
resourceType: 'Custom::MyCustomResourceType',
94146
provider: CustomResourceProvider.topic(new sns.Topic(stack, 'Provider'))
@@ -99,7 +151,8 @@ export = {
99151

100152
'fails if custom resource type is invalid': {
101153
'does not start with "Custom::"'(test: Test) {
102-
const stack = new cdk.Stack();
154+
const app = new cdk.App();
155+
const stack = new cdk.Stack(app, 'Test');
103156

104157
test.throws(() => {
105158
new CustomResource(stack, 'MyCustomResource', {
@@ -112,7 +165,8 @@ export = {
112165
},
113166

114167
'has invalid characters'(test: Test) {
115-
const stack = new cdk.Stack();
168+
const app = new cdk.App();
169+
const stack = new cdk.Stack(app, 'Test');
116170

117171
test.throws(() => {
118172
new CustomResource(stack, 'MyCustomResource', {
@@ -125,7 +179,8 @@ export = {
125179
},
126180

127181
'is longer than 60 characters'(test: Test) {
128-
const stack = new cdk.Stack();
182+
const app = new cdk.App();
183+
const stack = new cdk.Stack(app, 'Test');
129184

130185
test.throws(() => {
131186
new CustomResource(stack, 'MyCustomResource', {
@@ -138,10 +193,10 @@ export = {
138193
},
139194

140195
},
141-
};
196+
});
142197

143198
class TestCustomResource extends cdk.Construct {
144-
constructor(scope: cdk.Construct, id: string) {
199+
constructor(scope: cdk.Construct, id: string, opts: { removalPolicy?: cdk.RemovalPolicy } = {}) {
145200
super(scope, id);
146201

147202
const singletonLambda = new lambda.SingletonFunction(this, 'Lambda', {
@@ -153,7 +208,8 @@ class TestCustomResource extends cdk.Construct {
153208
});
154209

155210
new CustomResource(this, 'Resource', {
156-
provider: CustomResourceProvider.lambda(singletonLambda)
211+
...opts,
212+
provider: CustomResourceProvider.lambda(singletonLambda),
157213
});
158214
}
159215
}

packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
"DependsOn": [
7272
"AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
7373
"AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
74-
]
74+
],
75+
"DeletionPolicy": "Delete"
7576
},
7677
"AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": {
7778
"Type": "AWS::IAM::Role",

packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@
232232
],
233233
"ResourceType": "Custom::DynamoGlobalTableCoordinator",
234234
"TableName": "integrationtest"
235-
}
235+
},
236+
"DeletionPolicy": "Delete"
236237
}
237238
},
238239
"Parameters": {

packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.expected.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"DependsOn": [
4545
"AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
4646
"AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
47-
]
47+
],
48+
"DeletionPolicy": "Delete"
4849
},
4950
"AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": {
5051
"Type": "AWS::IAM::Role",

0 commit comments

Comments
 (0)