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