1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ "use strict" ;
4
+
5
+ const aws = require ( "aws-sdk" ) ;
6
+
7
+ // These are used for test purposes only
8
+ let defaultResponseURL ;
9
+ let defaultLogGroup ;
10
+ let defaultLogStream ;
11
+
12
+ /**
13
+ * Upload a CloudFormation response object to S3.
14
+ *
15
+ * @param {object } event the Lambda event payload received by the handler function
16
+ * @param {object } context the Lambda context received by the handler function
17
+ * @param {string } responseStatus the response status, either 'SUCCESS' or 'FAILED'
18
+ * @param {string } physicalResourceId CloudFormation physical resource ID
19
+ * @param {object } [responseData] arbitrary response data object
20
+ * @param {string } [reason] reason for failure, if any, to convey to the user
21
+ * @returns {Promise } Promise that is resolved on success, or rejected on connection error or HTTP error response
22
+ */
23
+ let report = function (
24
+ event ,
25
+ context ,
26
+ responseStatus ,
27
+ physicalResourceId ,
28
+ responseData ,
29
+ reason
30
+ ) {
31
+ return new Promise ( ( resolve , reject ) => {
32
+ const https = require ( "https" ) ;
33
+ const { URL } = require ( "url" ) ;
34
+
35
+ var responseBody = JSON . stringify ( {
36
+ Status : responseStatus ,
37
+ Reason : reason ,
38
+ PhysicalResourceId : physicalResourceId || context . logStreamName ,
39
+ StackId : event . StackId ,
40
+ RequestId : event . RequestId ,
41
+ LogicalResourceId : event . LogicalResourceId ,
42
+ Data : responseData ,
43
+ } ) ;
44
+
45
+ const parsedUrl = new URL ( event . ResponseURL || defaultResponseURL ) ;
46
+ const options = {
47
+ hostname : parsedUrl . hostname ,
48
+ port : 443 ,
49
+ path : parsedUrl . pathname + parsedUrl . search ,
50
+ method : "PUT" ,
51
+ headers : {
52
+ "Content-Type" : "" ,
53
+ "Content-Length" : responseBody . length ,
54
+ } ,
55
+ } ;
56
+
57
+ https
58
+ . request ( options )
59
+ . on ( "error" , reject )
60
+ . on ( "response" , ( res ) => {
61
+ res . resume ( ) ;
62
+ if ( res . statusCode >= 400 ) {
63
+ reject ( new Error ( `Error ${ res . statusCode } : ${ res . statusMessage } ` ) ) ;
64
+ } else {
65
+ resolve ( ) ;
66
+ }
67
+ } )
68
+ . end ( responseBody , "utf8" ) ;
69
+ } ) ;
70
+ } ;
71
+
72
+ /**
73
+ * Delete all objects in a bucket.
74
+ *
75
+ * @param {string } bucketName Name of the bucket to be cleaned.
76
+ */
77
+ const cleanBucket = async function ( bucketName ) {
78
+ const s3 = new aws . S3 ( ) ;
79
+ // Make sure the bucket exists.
80
+ try {
81
+ await s3 . headBucket ( { Bucket : bucketName } ) . promise ( ) ;
82
+ } catch ( err ) {
83
+ if ( err . name === "ResourceNotFoundException" ) {
84
+ return ;
85
+ }
86
+ throw err ;
87
+ }
88
+ const listObjectVersionsParam = {
89
+ Bucket : bucketName
90
+ }
91
+ while ( true ) {
92
+ const listResp = await s3 . listObjectVersions ( listObjectVersionsParam ) . promise ( ) ;
93
+ // After deleting other versions, remove delete markers version.
94
+ // For info on "delete marker": https://docs.aws.amazon.com/AmazonS3/latest/dev/DeleteMarker.html
95
+ let objectsToDelete = [
96
+ ...listResp . Versions . map ( version => ( { Key : version . Key , VersionId : version . VersionId } ) ) ,
97
+ ...listResp . DeleteMarkers . map ( marker => ( { Key : marker . Key , VersionId : marker . VersionId } ) )
98
+ ] ;
99
+ if ( objectsToDelete . length === 0 ) {
100
+ return
101
+ }
102
+ const delResp = await s3 . deleteObjects ( {
103
+ Bucket : bucketName ,
104
+ Delete : {
105
+ Objects : objectsToDelete ,
106
+ Quiet : true
107
+ }
108
+ } ) . promise ( )
109
+ if ( delResp . Errors . length > 0 ) {
110
+ throw new AggregateError ( [ new Error ( `${ delResp . Errors . length } /${ objectsToDelete . length } objects failed to delete` ) ,
111
+ new Error ( `first failed on key "${ delResp . Errors [ 0 ] . Key } ": ${ delResp . Errors [ 0 ] . Message } ` ) ] ) ;
112
+ }
113
+ if ( ! listResp . IsTruncated ) {
114
+ return
115
+ }
116
+ listObjectVersionsParam . KeyMarker = listResp . NextKeyMarker
117
+ listObjectVersionsParam . VersionIdMarker = listResp . NextVersionIdMarker
118
+ }
119
+ } ;
120
+
121
+ /**
122
+ * Correct desired count handler, invoked by Lambda.
123
+ */
124
+ exports . handler = async function ( event , context ) {
125
+ var responseData = { } ;
126
+ const props = event . ResourceProperties ;
127
+ const physicalResourceId = event . PhysicalResourceId || `bucket-cleaner-${ event . LogicalResourceId } ` ;
128
+
129
+ try {
130
+ switch ( event . RequestType ) {
131
+ case "Create" :
132
+ case "Update" :
133
+ break ;
134
+ case "Delete" :
135
+ await cleanBucket ( props . BucketName ) ;
136
+ break ;
137
+ default :
138
+ throw new Error ( `Unsupported request type ${ event . RequestType } ` ) ;
139
+ }
140
+ await report ( event , context , "SUCCESS" , physicalResourceId , responseData ) ;
141
+ } catch ( err ) {
142
+ console . log ( `Caught error ${ err } .` ) ;
143
+ await report (
144
+ event ,
145
+ context ,
146
+ "FAILED" ,
147
+ physicalResourceId ,
148
+ null ,
149
+ `${ err . message } (Log: ${ defaultLogGroup || context . logGroupName } /${ defaultLogStream || context . logStreamName
150
+ } )`
151
+ ) ;
152
+ }
153
+ } ;
154
+
155
+ /**
156
+ * @private
157
+ */
158
+ exports . withDefaultResponseURL = function ( url ) {
159
+ defaultResponseURL = url ;
160
+ } ;
161
+
162
+ /**
163
+ * @private
164
+ */
165
+ exports . withDefaultLogStream = function ( logStream ) {
166
+ defaultLogStream = logStream ;
167
+ } ;
168
+
169
+ /**
170
+ * @private
171
+ */
172
+ exports . withDefaultLogGroup = function ( logGroup ) {
173
+ defaultLogGroup = logGroup ;
174
+ } ;
175
+
176
+ class AggregateError extends Error {
177
+ #errors;
178
+ name = "AggregateError" ;
179
+ constructor ( errors ) {
180
+ let message = errors
181
+ . map ( error =>
182
+ String ( error ) ,
183
+ )
184
+ . join ( "\n" ) ;
185
+ super ( message ) ;
186
+ this . #errors = errors ;
187
+ }
188
+ get errors ( ) {
189
+ return [ ...this . #errors] ;
190
+ }
191
+ }
0 commit comments