11// eslint-disable-next-line import/no-extraneous-dependencies
2- import { S3 , CloudFormation } from 'aws-sdk' ;
3- import * as yaml from 'yaml' ;
4-
5- /**
6- * The custom resource should clear the bucket in one of the following cases:
7- *
8- * - The stack is deleted
9- * - The target bucket is removed from the template
10- * - The target bucket is replaced
11- * - The target bucket is created in a deployment that gets rolled back
12- *
13- * In particular, it should NOT clear the bucket in a case the custom resource is deleted
14- * but the target bucket is unaffected. This could happen in the following cases:
15- *
16- * - The autoDelete feature used to be turned on, and now gets turned off (leads to removal
17- * of the CR from the template, without affecting the bucket)
18- * - The autoDelete feature used to be turned off, now gets turned on, but the deployment
19- * gets rolled back (leads to creation and immediate deletion of the CR, without
20- * affecting the bucket).
21- *
22- * The only cases where we might misclassify is when the CR gets deleted. To
23- * determine whether or not we should empty the bucket, during a `Delete` event
24- * we will look at the stack state and depending on the state of the stack
25- * (rolling forward or rolling backward), compare the OLD and NEW templates to
26- * determine whether the bucket should be present in the final state:
27- *
28- * - ROLL FORWARD: delete contents if the bucket is not in the NEW template
29- * - ROLLBACK: delete contents if the bucket is not in the OLD template
30- */
2+ import { S3 } from 'aws-sdk' ;
313
324const s3 = new S3 ( ) ;
33- const cfn = new CloudFormation ( ) ;
345
356export async function handler ( event : AWSLambda . CloudFormationCustomResourceEvent ) {
367 switch ( event . RequestType ) {
@@ -39,7 +10,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
3910 case 'Update' :
4011 return onUpdate ( event ) ;
4112 case 'Delete' :
42- return onDelete ( event . StackId , event . ResourceProperties ?. BucketLogicalId , event . ResourceProperties ?. BucketName ) ;
13+ return onDelete ( event . ResourceProperties ?. BucketName ) ;
4314 }
4415}
4516
@@ -53,7 +24,7 @@ async function onUpdate(event: AWSLambda.CloudFormationCustomResourceEvent) {
5324 and create a new one with the new name. So we have to delete the contents of the
5425 bucket so that this operation does not fail. */
5526 if ( bucketNameHasChanged ) {
56- return emptyBucket ( oldBucketName ) ;
27+ return onDelete ( oldBucketName ) ;
5728 }
5829}
5930
@@ -77,58 +48,9 @@ async function emptyBucket(bucketName: string) {
7748 }
7849}
7950
80- async function onDelete ( stackId : string , logicalId ?: string , bucketName ?: string ) {
51+ async function onDelete ( bucketName ?: string ) {
8152 if ( ! bucketName ) {
8253 throw new Error ( 'No BucketName was provided.' ) ;
8354 }
84- if ( ! logicalId ) {
85- throw new Error ( 'No Logical ID was provided.' ) ;
86- }
87- if ( await isBucketAboutToBeDeleted ( stackId , logicalId ) ) {
88- await emptyBucket ( bucketName ) ;
89- }
55+ await emptyBucket ( bucketName ) ;
9056}
91-
92- /**
93- * Go and inspect CloudFormation to see if the target bucket is about to be deleted
94- */
95- async function isBucketAboutToBeDeleted ( stackId : string , logicalId : string ) {
96- const stackResponse = await cfn . describeStacks ( { StackName : stackId } ) . promise ( ) ;
97- if ( ! stackResponse . Stacks ?. [ 0 ] ) {
98- throw new Error ( `Could not find stack with ID: ${ stackId } ` ) ;
99- }
100- const stackStatus = stackResponse . Stacks [ 0 ] . StackStatus ;
101- process . stdout . write ( `Stack status: ${ stackStatus } \n` ) ;
102-
103- // Case 1: the stack failed creation.
104- // Case 2: the stack is being deleted.
105- // In both cases, by definition the bucket will go bye-bye.
106- if ( stackStatus === 'ROLLBACK_IN_PROGRESS' || stackStatus === 'DELETE_IN_PROGRESS' ) {
107- return true ;
108- }
109-
110- // Case 3: we're cleaning up after a successful rollforward.
111- // Case 4: we're rolling back a failed update.
112- // In both cases, either the bucket is also being deleted here, or it's just
113- // the CR that's being deleted.
114- // `GetTemplate` will show us the template we are moving to ('new' in case 3,
115- // 'old' in case 4). We will check if the bucket is in the template returned
116- // by `GetTemplate` to see if we need to clean it.
117- const destinationTemplateResponse = await cfn . getTemplate ( { StackName : stackId , TemplateStage : 'Processed' } ) . promise ( ) ;
118- let template ;
119- try {
120- template = yaml . parse ( destinationTemplateResponse . TemplateBody ?? '{}' , {
121- schema : 'core' ,
122- } ) ;
123- } catch ( e ) {
124- throw new Error ( `Unable to parse CloudFormation template (is it not YAML?): ${ destinationTemplateResponse . TemplateBody } ` ) ;
125- }
126-
127- if ( logicalId in ( template . Resources ?? { } ) ) {
128- process . stdout . write ( `Bucket ${ logicalId } is in target template, so NOT cleaning.\n` ) ;
129- return false ;
130- } else {
131- process . stdout . write ( `Bucket ${ logicalId } is NOT in target template, so cleaning.\n` ) ;
132- return true ;
133- }
134- }
0 commit comments