Skip to content

Commit 2ba40d1

Browse files
authoredNov 23, 2021
fix(s3-deployment): updating memoryLimit or vpc results in stack update failure (#17530)
This fixes the issue where updating the memoryLimit or vpc of the BucketDeployment resource would result in a stack update failure. In order to fix this the ID of the BucketDeployment CustomResource includes info on the memoryLimit and vpc in the same way that the Lambda function does. This means that when either of these values are updated, the BucketDeployment CustomResource will be recreated along with the Lambda function. If anyone is setting `retainOnDelete=false` (default is `true`) then this change would result in the data in the bucket being deleted. In order to avoid this scenario, this PR introduces a bucket tag that controls whether or not a BucketDeployment resource can delete data from the bucket. The BucketDeployment resource will now add a tag to the deployment bucket with a format like `aws-cdk:cr-owned:{keyPrefix}:{uniqueHash}`. For example: ``` { Key: 'aws-cdk:cr-owned:deploy/here/:240D17B3', Value: 'true', } ``` Each bucket+keyPrefix can be "owned" by 1 or more BucketDeployment resources. Since there are some scenarios where multiple BucketDeployment resources can deploy to the same bucket and key prefix (e.g. using include/exclude) we also append part of the id to make the key unique. As long as a bucket+keyPrefix is "owned" by a BucketDeployment resource, another CR cannot delete data. There are a couple of scenarios where this comes into play. 1. If the LogicalResourceId of the CustomResource changes (e.g. memoryLimit is updated) CloudFormation will first issue a 'Create' to create the new CustomResource and will update the Tag on the bucket. CloudFormation will then issue a 'Delete' on the old CustomResource and since the new CR "owns" the Bucket+keyPrefix (indicated by the presence of the tag), the old CR will not delete the contents of the bucket 2. If the BucketDeployment resource is deleted _and_ it is the only CR for that bucket+keyPrefix then CloudFormation will first remove the tag from the bucket and then issue a "Delete" to the CR. Since there are no tags indicating that this bucket+keyPrefix is "owned" then it will delete the contents. 3. If the BucketDeployment resource is deleted _and_ it is *not* the only CR for that bucket:keyPrefix then CloudFormation will first remove the tag from the bucket and then issue a "Delete" to the CR. Since there are other CRs that also "own" that bucket+keyPrefix (indicated by the presence of tags), there will still be a tag on the bucket and the contents will not be removed. The contents will only be removed after _all_ BucketDeployment resource that "own" the bucket+keyPrefix have been removed. 4. If the BucketDeployment resource _and_ the S3 Bucket are both removed, then CloudFormation will first issue a "Delete" to the CR and since there is a tag on the bucket the contents will not be removed. If you want the contents of the bucket to be removed on bucket deletion, then `autoDeleteObjects` property should be set to true on the Bucket. Scenario 3 & 4 are changes to the existing functionality in that they now do *not* delete data in some scenarios when they did previously. fixes #7128 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 765c274 commit 2ba40d1

10 files changed

+1090
-82
lines changed
 

‎packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.expected.json

+194-13
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,181 @@
88
"BlockPublicPolicy": true,
99
"IgnorePublicAcls": true,
1010
"RestrictPublicBuckets": true
11+
},
12+
"Tags": [
13+
{
14+
"Key": "aws-cdk:auto-delete-objects",
15+
"Value": "true"
16+
},
17+
{
18+
"Key": "aws-cdk:cr-owned:f8f0a91c",
19+
"Value": "true"
20+
}
21+
]
22+
},
23+
"UpdateReplacePolicy": "Delete",
24+
"DeletionPolicy": "Delete"
25+
},
26+
"BucketPolicyE9A3008A": {
27+
"Type": "AWS::S3::BucketPolicy",
28+
"Properties": {
29+
"Bucket": {
30+
"Ref": "Bucket83908E77"
31+
},
32+
"PolicyDocument": {
33+
"Statement": [
34+
{
35+
"Action": [
36+
"s3:GetBucket*",
37+
"s3:List*",
38+
"s3:DeleteObject*"
39+
],
40+
"Effect": "Allow",
41+
"Principal": {
42+
"AWS": {
43+
"Fn::GetAtt": [
44+
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
45+
"Arn"
46+
]
47+
}
48+
},
49+
"Resource": [
50+
{
51+
"Fn::GetAtt": [
52+
"Bucket83908E77",
53+
"Arn"
54+
]
55+
},
56+
{
57+
"Fn::Join": [
58+
"",
59+
[
60+
{
61+
"Fn::GetAtt": [
62+
"Bucket83908E77",
63+
"Arn"
64+
]
65+
},
66+
"/*"
67+
]
68+
]
69+
}
70+
]
71+
}
72+
],
73+
"Version": "2012-10-17"
74+
}
75+
}
76+
},
77+
"BucketAutoDeleteObjectsCustomResourceBAFD23C2": {
78+
"Type": "Custom::S3AutoDeleteObjects",
79+
"Properties": {
80+
"ServiceToken": {
81+
"Fn::GetAtt": [
82+
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F",
83+
"Arn"
84+
]
85+
},
86+
"BucketName": {
87+
"Ref": "Bucket83908E77"
1188
}
1289
},
90+
"DependsOn": [
91+
"BucketPolicyE9A3008A"
92+
],
1393
"UpdateReplacePolicy": "Delete",
1494
"DeletionPolicy": "Delete"
1595
},
96+
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": {
97+
"Type": "AWS::IAM::Role",
98+
"Properties": {
99+
"AssumeRolePolicyDocument": {
100+
"Version": "2012-10-17",
101+
"Statement": [
102+
{
103+
"Action": "sts:AssumeRole",
104+
"Effect": "Allow",
105+
"Principal": {
106+
"Service": "lambda.amazonaws.com"
107+
}
108+
}
109+
]
110+
},
111+
"ManagedPolicyArns": [
112+
{
113+
"Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
114+
}
115+
]
116+
}
117+
},
118+
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": {
119+
"Type": "AWS::Lambda::Function",
120+
"Properties": {
121+
"Code": {
122+
"S3Bucket": {
123+
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C"
124+
},
125+
"S3Key": {
126+
"Fn::Join": [
127+
"",
128+
[
129+
{
130+
"Fn::Select": [
131+
0,
132+
{
133+
"Fn::Split": [
134+
"||",
135+
{
136+
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6"
137+
}
138+
]
139+
}
140+
]
141+
},
142+
{
143+
"Fn::Select": [
144+
1,
145+
{
146+
"Fn::Split": [
147+
"||",
148+
{
149+
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6"
150+
}
151+
]
152+
}
153+
]
154+
}
155+
]
156+
]
157+
}
158+
},
159+
"Timeout": 900,
160+
"MemorySize": 128,
161+
"Handler": "__entrypoint__.handler",
162+
"Role": {
163+
"Fn::GetAtt": [
164+
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
165+
"Arn"
166+
]
167+
},
168+
"Runtime": "nodejs12.x",
169+
"Description": {
170+
"Fn::Join": [
171+
"",
172+
[
173+
"Lambda function for auto-deleting objects in ",
174+
{
175+
"Ref": "Bucket83908E77"
176+
},
177+
" S3 bucket."
178+
]
179+
]
180+
}
181+
},
182+
"DependsOn": [
183+
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092"
184+
]
185+
},
16186
"Vpc8378EB38": {
17187
"Type": "AWS::EC2::VPC",
18188
"Properties": {
@@ -1087,7 +1257,6 @@
10871257
"DestinationBucketName": {
10881258
"Ref": "Bucket83908E77"
10891259
},
1090-
"RetainOnDelete": false,
10911260
"Prune": true
10921261
},
10931262
"UpdateReplacePolicy": "Delete",
@@ -1219,7 +1388,7 @@
12191388
"Properties": {
12201389
"Code": {
12211390
"S3Bucket": {
1222-
"Ref": "AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3BucketD1AD544E"
1391+
"Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0"
12231392
},
12241393
"S3Key": {
12251394
"Fn::Join": [
@@ -1232,7 +1401,7 @@
12321401
"Fn::Split": [
12331402
"||",
12341403
{
1235-
"Ref": "AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3VersionKey93A19D70"
1404+
"Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C"
12361405
}
12371406
]
12381407
}
@@ -1245,7 +1414,7 @@
12451414
"Fn::Split": [
12461415
"||",
12471416
{
1248-
"Ref": "AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3VersionKey93A19D70"
1417+
"Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C"
12491418
}
12501419
]
12511420
}
@@ -1332,9 +1501,17 @@
13321501
}
13331502
},
13341503
"Parameters": {
1335-
"SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": {
1336-
"Type": "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>",
1337-
"Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id"
1504+
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C": {
1505+
"Type": "String",
1506+
"Description": "S3 bucket for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
1507+
},
1508+
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6": {
1509+
"Type": "String",
1510+
"Description": "S3 key for asset version \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
1511+
},
1512+
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709ArtifactHash17D48178": {
1513+
"Type": "String",
1514+
"Description": "Artifact hash for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
13381515
},
13391516
"AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7": {
13401517
"Type": "String",
@@ -1348,17 +1525,17 @@
13481525
"Type": "String",
13491526
"Description": "Artifact hash for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\""
13501527
},
1351-
"AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3BucketD1AD544E": {
1528+
"AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0": {
13521529
"Type": "String",
1353-
"Description": "S3 bucket for asset \"a3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8\""
1530+
"Description": "S3 bucket for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\""
13541531
},
1355-
"AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3VersionKey93A19D70": {
1532+
"AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C": {
13561533
"Type": "String",
1357-
"Description": "S3 key for asset version \"a3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8\""
1534+
"Description": "S3 key for asset version \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\""
13581535
},
1359-
"AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8ArtifactHash238275D6": {
1536+
"AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cArtifactHashBA6352EA": {
13601537
"Type": "String",
1361-
"Description": "Artifact hash for asset \"a3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8\""
1538+
"Description": "Artifact hash for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\""
13621539
},
13631540
"AssetParameters972240f9dd6e036a93d5f081af9a24315b2053828ac049b3b19b2fa12d7ae64aS3Bucket1F1A8472": {
13641541
"Type": "String",
@@ -1383,6 +1560,10 @@
13831560
"AssetParameters872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724dArtifactHashC2522C05": {
13841561
"Type": "String",
13851562
"Description": "Artifact hash for asset \"872561bf078edd1685d50c9ff821cdd60d2b2ddfb0013c4087e79bf2bb50724d\""
1563+
},
1564+
"SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": {
1565+
"Type": "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>",
1566+
"Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id"
13861567
}
13871568
}
13881569
}

‎packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ');
1313
const bucket = new s3.Bucket(stack, 'Bucket', {
1414
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
1515
removalPolicy: cdk.RemovalPolicy.DESTROY,
16+
autoDeleteObjects: true,
1617
});
1718
const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 });
1819

@@ -47,7 +48,6 @@ const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition', {
4748
// deploy an envfile to S3 and delete when the bucket is deleted
4849
const envFileDeployment = new s3deployment.BucketDeployment(stack, 'EnvFileDeployment', {
4950
destinationBucket: bucket,
50-
retainOnDelete: false,
5151
sources: [s3deployment.Source.asset(path.join(__dirname, '../demo-envfiles'))],
5252
});
5353

‎packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts

+74-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { ISource, SourceConfig } from './source';
1515
// eslint-disable-next-line no-duplicate-imports, import/order
1616
import { Construct as CoreConstruct } from '@aws-cdk/core';
1717

18+
// tag key has a limit of 128 characters
19+
const CUSTOM_RESOURCE_OWNER_TAG = 'aws-cdk:cr-owned';
1820
/**
1921
* Properties for `BucketDeployment`.
2022
*/
@@ -32,6 +34,8 @@ export interface BucketDeploymentProps {
3234
/**
3335
* Key prefix in the destination bucket.
3436
*
37+
* Must be <=104 characters
38+
*
3539
* @default "/" (unzip to root of the destination bucket)
3640
*/
3741
readonly destinationKeyPrefix?: string;
@@ -292,7 +296,7 @@ export class BucketDeployment extends CoreConstruct {
292296
filesystem: accessPoint ? lambda.FileSystem.fromEfsAccessPoint(
293297
accessPoint,
294298
mountPath,
295-
): undefined,
299+
) : undefined,
296300
});
297301

298302
const handlerRole = handler.role;
@@ -309,7 +313,8 @@ export class BucketDeployment extends CoreConstruct {
309313
}));
310314
}
311315

312-
new cdk.CustomResource(this, 'CustomResource', {
316+
const crUniqueId = `CustomResource${this.renderUniqueId(props.memoryLimit, props.vpc)}`;
317+
const cr = new cdk.CustomResource(this, crUniqueId, {
313318
serviceToken: handler.functionArn,
314319
resourceType: 'Custom::CDKBucketDeployment',
315320
properties: {
@@ -328,10 +333,65 @@ export class BucketDeployment extends CoreConstruct {
328333
},
329334
});
330335

336+
let prefix: string = props.destinationKeyPrefix ?
337+
`:${props.destinationKeyPrefix}` :
338+
'';
339+
prefix += `:${cr.node.addr.substr(-8)}`;
340+
const tagKey = CUSTOM_RESOURCE_OWNER_TAG + prefix;
341+
342+
// destinationKeyPrefix can be 104 characters before we hit
343+
// the tag key limit of 128
344+
// '/this/is/a/random/key/prefix/that/is/a/lot/of/characters/do/we/think/that/it/will/ever/be/this/long?????'
345+
// better to throw an error here than wait for CloudFormation to fail
346+
if (tagKey.length > 128) {
347+
throw new Error('The BucketDeployment construct requires that the "destinationKeyPrefix" be <=104 characters');
348+
}
349+
350+
/*
351+
* This will add a tag to the deployment bucket in the format of
352+
* `aws-cdk:cr-owned:{keyPrefix}:{uniqueHash}`
353+
*
354+
* For example:
355+
* {
356+
* Key: 'aws-cdk:cr-owned:deploy/here/:240D17B3',
357+
* Value: 'true',
358+
* }
359+
*
360+
* This will allow for scenarios where there is a single S3 Bucket that has multiple
361+
* BucketDeployment resources deploying to it. Each bucket + keyPrefix can be "owned" by
362+
* 1 or more BucketDeployment resources. Since there are some scenarios where multiple BucketDeployment
363+
* resources can deploy to the same bucket and key prefix (e.g. using include/exclude) we
364+
* also append part of the id to make the key unique.
365+
*
366+
* As long as a bucket + keyPrefix is "owned" by a BucketDeployment resource, another CR
367+
* cannot delete data. There are a couple of scenarios where this comes into play.
368+
*
369+
* 1. If the LogicalResourceId of the CustomResource changes (e.g. the crUniqueId changes)
370+
* CloudFormation will first issue a 'Create' to create the new CustomResource and will
371+
* update the Tag on the bucket. CloudFormation will then issue a 'Delete' on the old CustomResource
372+
* and since the new CR "owns" the Bucket+keyPrefix it will not delete the contents of the bucket
373+
*
374+
* 2. If the BucketDeployment resource is deleted _and_ it is the only CR for that bucket+keyPrefix
375+
* then CloudFormation will first remove the tag from the bucket and then issue a "Delete" to the
376+
* CR. Since there are no tags indicating that this bucket+keyPrefix is "owned" then it will delete
377+
* the contents.
378+
*
379+
* 3. If the BucketDeployment resource is deleted _and_ it is *not* the only CR for that bucket:keyPrefix
380+
* then CloudFormation will first remove the tag from the bucket and then issue a "Delete" to the CR.
381+
* Since there are other CRs that also "own" that bucket+keyPrefix there will still be a tag on the bucket
382+
* and the contents will not be removed.
383+
*
384+
* 4. If the BucketDeployment resource _and_ the S3 Bucket are both removed, then CloudFormation will first
385+
* issue a "Delete" to the CR and since there is a tag on the bucket the contents will not be removed. If you
386+
* want the contents of the bucket to be removed on bucket deletion, then `autoDeleteObjects` property should
387+
* be set to true on the Bucket.
388+
*/
389+
cdk.Tags.of(props.destinationBucket).add(tagKey, 'true');
390+
331391
}
332392

333-
private renderSingletonUuid(memoryLimit?: number, vpc?: ec2.IVpc) {
334-
let uuid = '8693BB64-9689-44B6-9AAF-B0CC9EB8756C';
393+
private renderUniqueId(memoryLimit?: number, vpc?: ec2.IVpc) {
394+
let uuid = '';
335395

336396
// if user specify a custom memory limit, define another singleton handler
337397
// with this configuration. otherwise, it won't be possible to use multiple
@@ -355,6 +415,14 @@ export class BucketDeployment extends CoreConstruct {
355415
return uuid;
356416
}
357417

418+
private renderSingletonUuid(memoryLimit?: number, vpc?: ec2.IVpc) {
419+
let uuid = '8693BB64-9689-44B6-9AAF-B0CC9EB8756C';
420+
421+
uuid += this.renderUniqueId(memoryLimit, vpc);
422+
423+
return uuid;
424+
}
425+
358426
/**
359427
* Function to get/create a stack singleton instance of EFS FileSystem per vpc.
360428
*
@@ -453,7 +521,7 @@ export class CacheControl {
453521
* The raw cache control setting.
454522
*/
455523
public readonly value: any,
456-
) {}
524+
) { }
457525
}
458526

459527
/**
@@ -551,7 +619,7 @@ export class Expires {
551619
* The raw expiration date expression.
552620
*/
553621
public readonly value: any,
554-
) {}
622+
) { }
555623
}
556624

557625
/**

‎packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
logger.setLevel(logging.INFO)
1616

1717
cloudfront = boto3.client('cloudfront')
18+
s3 = boto3.client('s3')
1819

1920
CFN_SUCCESS = "SUCCESS"
2021
CFN_FAILED = "FAILED"
2122
ENV_KEY_MOUNT_PATH = "MOUNT_PATH"
2223

24+
CUSTOM_RESOURCE_OWNER_TAG = "aws-cdk:cr-owned"
25+
2326
def handler(event, context):
2427

2528
def cfn_error(message=None):
@@ -68,9 +71,9 @@ def cfn_error(message=None):
6871

6972
s3_source_zips = map(lambda name, key: "s3://%s/%s" % (name, key), source_bucket_names, source_object_keys)
7073
s3_dest = "s3://%s/%s" % (dest_bucket_name, dest_bucket_prefix)
71-
7274
old_s3_dest = "s3://%s/%s" % (old_props.get("DestinationBucketName", ""), old_props.get("DestinationBucketKeyPrefix", ""))
7375

76+
7477
# obviously this is not
7578
if old_s3_dest == "s3:///":
7679
old_s3_dest = None
@@ -89,7 +92,8 @@ def cfn_error(message=None):
8992

9093
# delete or create/update (only if "retain_on_delete" is false)
9194
if request_type == "Delete" and not retain_on_delete:
92-
aws_command("s3", "rm", s3_dest, "--recursive")
95+
if not bucket_owned(dest_bucket_name, dest_bucket_prefix):
96+
aws_command("s3", "rm", s3_dest, "--recursive")
9397

9498
# if we are updating without retention and the destination changed, delete first
9599
if request_type == "Update" and not retain_on_delete and old_s3_dest != s3_dest:
@@ -233,3 +237,21 @@ def cfn_send(event, context, responseStatus, responseData={}, physicalResourceId
233237
except Exception as e:
234238
logger.error("| unable to send response to CloudFormation")
235239
logger.exception(e)
240+
241+
242+
#---------------------------------------------------------------------------------------------------
243+
# check if bucket is owned by a custom resource
244+
# if it is then we don't want to delete content
245+
def bucket_owned(bucketName, keyPrefix):
246+
tag = CUSTOM_RESOURCE_OWNER_TAG
247+
if keyPrefix != "":
248+
tag = tag + ':' + keyPrefix
249+
try:
250+
request = s3.get_bucket_tagging(
251+
Bucket=bucketName,
252+
)
253+
return any((x["Key"].startswith(tag)) for x in request["TagSet"])
254+
except Exception as e:
255+
logger.info("| error getting tags from bucket")
256+
logger.exception(e)
257+
return False

‎packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts

+118
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import '@aws-cdk/assert-internal/jest';
22
import * as path from 'path';
3+
import { MatchStyle, objectLike } from '@aws-cdk/assert-internal';
34
import * as cloudfront from '@aws-cdk/aws-cloudfront';
45
import * as ec2 from '@aws-cdk/aws-ec2';
56
import * as iam from '@aws-cdk/aws-iam';
@@ -922,3 +923,120 @@ test('deployment allows vpc and subnets to be implicitly supplied to lambda', ()
922923
},
923924
});
924925
});
926+
927+
test('resource id includes memory and vpc', () => {
928+
929+
// GIVEN
930+
const stack = new cdk.Stack();
931+
const bucket = new s3.Bucket(stack, 'Dest');
932+
const vpc: ec2.IVpc = new ec2.Vpc(stack, 'SomeVpc2', {});
933+
934+
// WHEN
935+
new s3deploy.BucketDeployment(stack, 'DeployWithVpc2', {
936+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
937+
destinationBucket: bucket,
938+
vpc,
939+
memoryLimit: 256,
940+
});
941+
942+
// THEN
943+
expect(stack).toMatchTemplate({
944+
Resources: objectLike({
945+
DeployWithVpc2CustomResource256MiBc8a39596cb8641929fcf6a288bc9db5ab7b0f656ad3C5F6E78: objectLike({
946+
Type: 'Custom::CDKBucketDeployment',
947+
}),
948+
}),
949+
}, MatchStyle.SUPERSET);
950+
});
951+
952+
test('bucket includes custom resource owner tag', () => {
953+
954+
// GIVEN
955+
const stack = new cdk.Stack();
956+
const bucket = new s3.Bucket(stack, 'Dest');
957+
const vpc: ec2.IVpc = new ec2.Vpc(stack, 'SomeVpc2', {});
958+
959+
// WHEN
960+
new s3deploy.BucketDeployment(stack, 'DeployWithVpc2', {
961+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
962+
destinationBucket: bucket,
963+
destinationKeyPrefix: '/a/b/c',
964+
vpc,
965+
memoryLimit: 256,
966+
});
967+
968+
// THEN
969+
expect(stack).toHaveResource('AWS::S3::Bucket', {
970+
Tags: [{
971+
Key: 'aws-cdk:cr-owned:/a/b/c:971e1fa8',
972+
Value: 'true',
973+
}],
974+
});
975+
});
976+
977+
test('throws if destinationKeyPrefix is too long', () => {
978+
979+
// GIVEN
980+
const stack = new cdk.Stack();
981+
const bucket = new s3.Bucket(stack, 'Dest');
982+
983+
// WHEN
984+
expect(() => new s3deploy.BucketDeployment(stack, 'DeployWithVpc2', {
985+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
986+
destinationBucket: bucket,
987+
destinationKeyPrefix: '/this/is/a/random/key/prefix/that/is/a/lot/of/characters/do/we/think/that/it/will/ever/be/this/long??????',
988+
memoryLimit: 256,
989+
})).toThrow(/The BucketDeployment construct requires that/);
990+
991+
});
992+
993+
test('bucket has multiple deployments', () => {
994+
995+
// GIVEN
996+
const stack = new cdk.Stack();
997+
const bucket = new s3.Bucket(stack, 'Dest');
998+
const vpc: ec2.IVpc = new ec2.Vpc(stack, 'SomeVpc2', {});
999+
1000+
// WHEN
1001+
new s3deploy.BucketDeployment(stack, 'DeployWithVpc2', {
1002+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
1003+
destinationBucket: bucket,
1004+
destinationKeyPrefix: '/a/b/c',
1005+
vpc,
1006+
memoryLimit: 256,
1007+
});
1008+
1009+
new s3deploy.BucketDeployment(stack, 'DeployWithVpc2Exclude', {
1010+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'), {
1011+
exclude: ['index.html'],
1012+
})],
1013+
destinationBucket: bucket,
1014+
destinationKeyPrefix: '/a/b/c',
1015+
vpc,
1016+
memoryLimit: 256,
1017+
});
1018+
1019+
new s3deploy.BucketDeployment(stack, 'DeployWithVpc3', {
1020+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
1021+
destinationBucket: bucket,
1022+
destinationKeyPrefix: '/x/z',
1023+
});
1024+
1025+
// THEN
1026+
expect(stack).toHaveResource('AWS::S3::Bucket', {
1027+
Tags: [
1028+
{
1029+
Key: 'aws-cdk:cr-owned:/a/b/c:6da0a4ab',
1030+
Value: 'true',
1031+
},
1032+
{
1033+
Key: 'aws-cdk:cr-owned:/a/b/c:971e1fa8',
1034+
Value: 'true',
1035+
},
1036+
{
1037+
Key: 'aws-cdk:cr-owned:/x/z:2db04622',
1038+
Value: 'true',
1039+
},
1040+
],
1041+
});
1042+
});

‎packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json

+193-9
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,181 @@
22
"Resources": {
33
"Destination3E3DC043D": {
44
"Type": "AWS::S3::Bucket",
5+
"Properties": {
6+
"Tags": [
7+
{
8+
"Key": "aws-cdk:auto-delete-objects",
9+
"Value": "true"
10+
},
11+
{
12+
"Key": "aws-cdk:cr-owned:4685d093",
13+
"Value": "true"
14+
}
15+
]
16+
},
17+
"UpdateReplacePolicy": "Delete",
18+
"DeletionPolicy": "Delete"
19+
},
20+
"Destination3Policy685DA6C5": {
21+
"Type": "AWS::S3::BucketPolicy",
22+
"Properties": {
23+
"Bucket": {
24+
"Ref": "Destination3E3DC043D"
25+
},
26+
"PolicyDocument": {
27+
"Statement": [
28+
{
29+
"Action": [
30+
"s3:GetBucket*",
31+
"s3:List*",
32+
"s3:DeleteObject*"
33+
],
34+
"Effect": "Allow",
35+
"Principal": {
36+
"AWS": {
37+
"Fn::GetAtt": [
38+
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
39+
"Arn"
40+
]
41+
}
42+
},
43+
"Resource": [
44+
{
45+
"Fn::GetAtt": [
46+
"Destination3E3DC043D",
47+
"Arn"
48+
]
49+
},
50+
{
51+
"Fn::Join": [
52+
"",
53+
[
54+
{
55+
"Fn::GetAtt": [
56+
"Destination3E3DC043D",
57+
"Arn"
58+
]
59+
},
60+
"/*"
61+
]
62+
]
63+
}
64+
]
65+
}
66+
],
67+
"Version": "2012-10-17"
68+
}
69+
}
70+
},
71+
"Destination3AutoDeleteObjectsCustomResourceC26EC7B7": {
72+
"Type": "Custom::S3AutoDeleteObjects",
73+
"Properties": {
74+
"ServiceToken": {
75+
"Fn::GetAtt": [
76+
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F",
77+
"Arn"
78+
]
79+
},
80+
"BucketName": {
81+
"Ref": "Destination3E3DC043D"
82+
}
83+
},
84+
"DependsOn": [
85+
"Destination3Policy685DA6C5"
86+
],
587
"UpdateReplacePolicy": "Delete",
688
"DeletionPolicy": "Delete"
789
},
90+
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": {
91+
"Type": "AWS::IAM::Role",
92+
"Properties": {
93+
"AssumeRolePolicyDocument": {
94+
"Version": "2012-10-17",
95+
"Statement": [
96+
{
97+
"Action": "sts:AssumeRole",
98+
"Effect": "Allow",
99+
"Principal": {
100+
"Service": "lambda.amazonaws.com"
101+
}
102+
}
103+
]
104+
},
105+
"ManagedPolicyArns": [
106+
{
107+
"Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
108+
}
109+
]
110+
}
111+
},
112+
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": {
113+
"Type": "AWS::Lambda::Function",
114+
"Properties": {
115+
"Code": {
116+
"S3Bucket": {
117+
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C"
118+
},
119+
"S3Key": {
120+
"Fn::Join": [
121+
"",
122+
[
123+
{
124+
"Fn::Select": [
125+
0,
126+
{
127+
"Fn::Split": [
128+
"||",
129+
{
130+
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6"
131+
}
132+
]
133+
}
134+
]
135+
},
136+
{
137+
"Fn::Select": [
138+
1,
139+
{
140+
"Fn::Split": [
141+
"||",
142+
{
143+
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6"
144+
}
145+
]
146+
}
147+
]
148+
}
149+
]
150+
]
151+
}
152+
},
153+
"Timeout": 900,
154+
"MemorySize": 128,
155+
"Handler": "__entrypoint__.handler",
156+
"Role": {
157+
"Fn::GetAtt": [
158+
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
159+
"Arn"
160+
]
161+
},
162+
"Runtime": "nodejs12.x",
163+
"Description": {
164+
"Fn::Join": [
165+
"",
166+
[
167+
"Lambda function for auto-deleting objects in ",
168+
{
169+
"Ref": "Destination3E3DC043D"
170+
},
171+
" S3 bucket."
172+
]
173+
]
174+
}
175+
},
176+
"DependsOn": [
177+
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092"
178+
]
179+
},
8180
"DistributionCFDistribution882A7313": {
9181
"Type": "AWS::CloudFront::Distribution",
10182
"Properties": {
@@ -295,7 +467,7 @@
295467
"Properties": {
296468
"Code": {
297469
"S3Bucket": {
298-
"Ref": "AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3BucketD1AD544E"
470+
"Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0"
299471
},
300472
"S3Key": {
301473
"Fn::Join": [
@@ -308,7 +480,7 @@
308480
"Fn::Split": [
309481
"||",
310482
{
311-
"Ref": "AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3VersionKey93A19D70"
483+
"Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C"
312484
}
313485
]
314486
}
@@ -321,7 +493,7 @@
321493
"Fn::Split": [
322494
"||",
323495
{
324-
"Ref": "AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3VersionKey93A19D70"
496+
"Ref": "AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C"
325497
}
326498
]
327499
}
@@ -353,6 +525,18 @@
353525
}
354526
},
355527
"Parameters": {
528+
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C": {
529+
"Type": "String",
530+
"Description": "S3 bucket for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
531+
},
532+
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6": {
533+
"Type": "String",
534+
"Description": "S3 key for asset version \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
535+
},
536+
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709ArtifactHash17D48178": {
537+
"Type": "String",
538+
"Description": "Artifact hash for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
539+
},
356540
"AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7": {
357541
"Type": "String",
358542
"Description": "S3 bucket for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\""
@@ -365,17 +549,17 @@
365549
"Type": "String",
366550
"Description": "Artifact hash for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\""
367551
},
368-
"AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3BucketD1AD544E": {
552+
"AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3Bucket1BE31DB0": {
369553
"Type": "String",
370-
"Description": "S3 bucket for asset \"a3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8\""
554+
"Description": "S3 bucket for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\""
371555
},
372-
"AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8S3VersionKey93A19D70": {
556+
"AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cS3VersionKeyDC38E49C": {
373557
"Type": "String",
374-
"Description": "S3 key for asset version \"a3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8\""
558+
"Description": "S3 key for asset version \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\""
375559
},
376-
"AssetParametersa3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8ArtifactHash238275D6": {
560+
"AssetParameters983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4cArtifactHashBA6352EA": {
377561
"Type": "String",
378-
"Description": "Artifact hash for asset \"a3058ccb468d757ebb89df5363a1c20f5307c6911136f29d00e1a68c9b2aa7e8\""
562+
"Description": "Artifact hash for asset \"983c442a2fe823a8b4ebb18d241a5150ae15103dacbf3f038c7c6343e565aa4c\""
379563
},
380564
"AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A": {
381565
"Type": "String",

‎packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class TestBucketDeployment extends cdk.Stack {
1010

1111
const bucket = new s3.Bucket(this, 'Destination3', {
1212
removalPolicy: cdk.RemovalPolicy.DESTROY,
13+
autoDeleteObjects: true, // needed for integration test cleanup
1314
});
1415
const distribution = new cloudfront.CloudFrontWebDistribution(this, 'Distribution', {
1516
originConfigs: [

‎packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json

+375-17
Large diffs are not rendered by default.

‎packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class TestBucketDeployment extends cdk.Stack {
1212
websiteIndexDocument: 'index.html',
1313
publicReadAccess: false,
1414
removalPolicy: cdk.RemovalPolicy.DESTROY,
15+
autoDeleteObjects: true, // needed for integration test cleanup
1516
});
1617

1718
new s3deploy.BucketDeployment(this, 'DeployMe', {
@@ -29,7 +30,10 @@ class TestBucketDeployment extends cdk.Stack {
2930
retainOnDelete: false, // default is true, which will block the integration test cleanup
3031
});
3132

32-
const bucket2 = new s3.Bucket(this, 'Destination2');
33+
const bucket2 = new s3.Bucket(this, 'Destination2', {
34+
removalPolicy: cdk.RemovalPolicy.DESTROY,
35+
autoDeleteObjects: true, // needed for integration test cleanup
36+
});
3337

3438
new s3deploy.BucketDeployment(this, 'DeployWithPrefix', {
3539
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
@@ -38,7 +42,10 @@ class TestBucketDeployment extends cdk.Stack {
3842
retainOnDelete: false, // default is true, which will block the integration test cleanup
3943
});
4044

41-
const bucket3 = new s3.Bucket(this, 'Destination3');
45+
const bucket3 = new s3.Bucket(this, 'Destination3', {
46+
removalPolicy: cdk.RemovalPolicy.DESTROY,
47+
autoDeleteObjects: true, // needed for integration test cleanup
48+
});
4249

4350
new s3deploy.BucketDeployment(this, 'DeployWithMetadata', {
4451
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],

‎packages/@aws-cdk/aws-s3-deployment/test/lambda/test.py

+101-32
Original file line numberDiff line numberDiff line change
@@ -242,23 +242,54 @@ def test_create_update_with_metadata(self):
242242
)
243243

244244
def test_delete_no_retain(self):
245-
invoke_handler("Delete", {
246-
"SourceBucketNames": ["<source-bucket>"],
247-
"SourceObjectKeys": ["<source-object-key>"],
248-
"DestinationBucketName": "<dest-bucket-name>",
249-
"RetainOnDelete": "false"
250-
}, physical_id="<physicalid>")
245+
def mock_make_api_call(self, operation_name, kwarg):
246+
if operation_name == 'GetBucketTagging':
247+
assert kwarg['Bucket'] == '<dest-bucket-name>'
248+
return {'TagSet': [{'Key': 'random', 'Value': '<logical-resource-id>'}]}
249+
raise ClientError({'Error': {'Code': '500', 'Message': 'Unsupported operation'}}, operation_name)
250+
with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
251+
invoke_handler("Delete", {
252+
"SourceBucketNames": ["<source-bucket>"],
253+
"SourceObjectKeys": ["<source-object-key>"],
254+
"DestinationBucketName": "<dest-bucket-name>",
255+
"RetainOnDelete": "false"
256+
}, physical_id="<physicalid>")
251257

252258
self.assertAwsCommands(["s3", "rm", "s3://<dest-bucket-name>/", "--recursive"])
253259

260+
# In a replace the logcal id of the custom resource will change
261+
# so the custom resource that gets the Delete event will no longer
262+
# "own" the bucket
263+
def test_replace_no_retain(self):
264+
def mock_make_api_call(self, operation_name, kwarg):
265+
if operation_name == 'GetBucketTagging':
266+
assert kwarg['Bucket'] == '<dest-bucket-name>'
267+
return {'TagSet': [{'Key': 'aws-cdk:cr-owned:-bucket>', 'Value': '<some-other-logical-id>'}]}
268+
raise ClientError({'Error': {'Code': '500', 'Message': 'Unsupported operation'}}, operation_name)
269+
with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
270+
invoke_handler("Delete", {
271+
"SourceBucketNames": ["<source-bucket>"],
272+
"SourceObjectKeys": ["<source-object-key>"],
273+
"DestinationBucketName": "<dest-bucket-name>",
274+
"RetainOnDelete": "false"
275+
}, physical_id="<physicalid>")
276+
277+
self.assertAwsCommands()
278+
254279
def test_delete_with_dest_key(self):
255-
invoke_handler("Delete", {
256-
"SourceBucketNames": ["<source-bucket>"],
257-
"SourceObjectKeys": ["<source-object-key>"],
258-
"DestinationBucketName": "<dest-bucket-name>",
259-
"DestinationBucketKeyPrefix": "<dest-key-prefix>",
260-
"RetainOnDelete": "false"
261-
}, physical_id="<physicalid>")
280+
def mock_make_api_call(self, operation_name, kwarg):
281+
if operation_name == 'GetBucketTagging':
282+
assert kwarg['Bucket'] == '<dest-bucket-name>'
283+
return {'TagSet': [{'Key': 'random-key', 'Value': '<logical-resource-id>'}]}
284+
raise ClientError({'Error': {'Code': '500', 'Message': 'Unsupported operation'}}, operation_name)
285+
with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
286+
invoke_handler("Delete", {
287+
"SourceBucketNames": ["<source-bucket>"],
288+
"SourceObjectKeys": ["<source-object-key>"],
289+
"DestinationBucketName": "<dest-bucket-name>",
290+
"DestinationBucketKeyPrefix": "<dest-key-prefix>",
291+
"RetainOnDelete": "false"
292+
}, physical_id="<physicalid>")
262293

263294
self.assertAwsCommands(["s3", "rm", "s3://<dest-bucket-name>/<dest-key-prefix>", "--recursive"])
264295

@@ -285,12 +316,18 @@ def test_delete_with_retain_implicit_default(self):
285316
self.assertAwsCommands()
286317

287318
def test_delete_with_retain_explicitly_false(self):
288-
invoke_handler("Delete", {
289-
"SourceBucketNames": ["<source-bucket>"],
290-
"SourceObjectKeys": ["<source-object-key>"],
291-
"DestinationBucketName": "<dest-bucket-name>",
292-
"RetainOnDelete": "false"
293-
}, physical_id="<physicalid>")
319+
def mock_make_api_call(self, operation_name, kwarg):
320+
if operation_name == 'GetBucketTagging':
321+
assert kwarg['Bucket'] == '<dest-bucket-name>'
322+
return {'TagSet': [{'Key': 'random-key', 'Value': '<logical-resource-id>'}]}
323+
raise ClientError({'Error': {'Code': '500', 'Message': 'Unsupported operation'}}, operation_name)
324+
with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
325+
invoke_handler("Delete", {
326+
"SourceBucketNames": ["<source-bucket>"],
327+
"SourceObjectKeys": ["<source-object-key>"],
328+
"DestinationBucketName": "<dest-bucket-name>",
329+
"RetainOnDelete": "false"
330+
}, physical_id="<physicalid>")
294331

295332
self.assertAwsCommands(
296333
["s3", "rm", "s3://<dest-bucket-name>/", "--recursive"]
@@ -466,6 +503,7 @@ def test_update_new_dest_prefix_retain_implicit(self):
466503
#
467504

468505
def test_physical_id_allocated_on_create_and_reused_afterwards(self):
506+
469507
create_resp = invoke_handler("Create", {
470508
"SourceBucketNames": ["<source-bucket>"],
471509
"SourceObjectKeys": ["<source-object-key>"],
@@ -487,12 +525,17 @@ def test_physical_id_allocated_on_create_and_reused_afterwards(self):
487525
self.assertEqual(update_resp['PhysicalResourceId'], phid)
488526

489527
# now issue a delete, and make sure this also applies
490-
delete_resp = invoke_handler("Delete", {
491-
"SourceBucketNames": ["<source-bucket>"],
492-
"SourceObjectKeys": ["<source-object-key>"],
493-
"DestinationBucketName": "<dest-bucket-name>",
494-
"RetainOnDelete": "false"
495-
}, physical_id=phid)
528+
def mock_make_api_call(self, operation_name, kwarg):
529+
if operation_name == 'GetBucketTagging':
530+
return {'TagSet': [{'Key': 'aws-cdk:cr-owned:-bucket>', 'Value': '<logical-resource-id>'}]}
531+
raise ClientError({'Error': {'Code': '500', 'Message': 'Unsupported operation'}}, operation_name)
532+
with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
533+
delete_resp = invoke_handler("Delete", {
534+
"SourceBucketNames": ["<source-bucket>"],
535+
"SourceObjectKeys": ["<source-object-key>"],
536+
"DestinationBucketName": "<dest-bucket-name>",
537+
"RetainOnDelete": "false"
538+
}, physical_id=phid)
496539
self.assertEqual(delete_resp['PhysicalResourceId'], phid)
497540

498541
def test_fails_when_physical_id_not_present_in_update(self):
@@ -507,16 +550,42 @@ def test_fails_when_physical_id_not_present_in_update(self):
507550
self.assertEqual(update_resp['Reason'], "invalid request: request type is 'Update' but 'PhysicalResourceId' is not defined")
508551

509552
def test_fails_when_physical_id_not_present_in_delete(self):
510-
update_resp = invoke_handler("Delete", {
511-
"SourceBucketNames": ["<source-bucket>"],
512-
"SourceObjectKeys": ["<source-object-key>"],
513-
"DestinationBucketName": "<new-dest-bucket-name>",
514-
}, old_resource_props={
515-
"DestinationBucketName": "<dest-bucket-name>",
516-
}, expected_status="FAILED")
553+
def mock_make_api_call(self, operation_name, kwarg):
554+
if operation_name == 'GetBucketTagging':
555+
return {'TagSet': [{'Key': 'aws-cdk:cr-owned:-bucket>', 'Value': '<logical-resource-id>'}]}
556+
raise ClientError({'Error': {'Code': '500', 'Message': 'Unsupported operation'}}, operation_name)
557+
558+
with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
559+
update_resp = invoke_handler("Delete", {
560+
"SourceBucketNames": ["<source-bucket>"],
561+
"SourceObjectKeys": ["<source-object-key>"],
562+
"DestinationBucketName": "<new-dest-bucket-name>",
563+
}, old_resource_props={
564+
"DestinationBucketName": "<dest-bucket-name>",
565+
}, expected_status="FAILED")
517566

518567
self.assertEqual(update_resp['Reason'], "invalid request: request type is 'Delete' but 'PhysicalResourceId' is not defined")
519568

569+
# no bucket tags removes content
570+
def test_no_tags_on_bucket(self):
571+
def mock_make_api_call(self, operation_name, kwarg):
572+
if operation_name == 'GetBucketTagging':
573+
raise ClientError({'Error': {'Code': 'NoSuchTagSet', 'Message': 'The TagSet does not exist'}}, operation_name)
574+
raise ClientError({'Error': {'Code': '500', 'Message': 'Unsupported operation'}}, operation_name)
575+
576+
with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
577+
invoke_handler("Delete", {
578+
"SourceBucketNames": ["<source-bucket>"],
579+
"SourceObjectKeys": ["<source-object-key>"],
580+
"DestinationBucketName": "<dest-bucket-name>",
581+
"RetainOnDelete": "false"
582+
}, physical_id="<physicalid>")
583+
584+
self.assertAwsCommands(
585+
["s3", "rm", "s3://<dest-bucket-name>/", "--recursive"]
586+
)
587+
588+
520589

521590
# asserts that a given list of "aws xxx" commands have been invoked (in order)
522591
def assertAwsCommands(self, *expected):

0 commit comments

Comments
 (0)
Please sign in to comment.