@@ -110,69 +110,28 @@ const requestCertificate = async function (requestId, domainName, subjectAlterna
110
110
111
111
console . log ( 'Waiting for ACM to provide DNS records for validation...' ) ;
112
112
113
- let records ;
114
- for ( let attempt = 0 ; attempt < maxAttempts && ! records ; attempt ++ ) {
113
+ let records = [ ] ;
114
+ for ( let attempt = 0 ; attempt < maxAttempts && ! records . length ; attempt ++ ) {
115
115
const { Certificate } = await acm . describeCertificate ( {
116
116
CertificateArn : reqCertResponse . CertificateArn
117
117
} ) . promise ( ) ;
118
- const options = Certificate . DomainValidationOptions || [ ] ;
119
- // Ensure all records are ready; there is (at least a theory there's) a chance of a partial response here in rare cases.
120
- if ( options . length > 0 && options . every ( opt => opt && ! ! opt . ResourceRecord ) ) {
121
- // some alternative names will produce the same validation record
122
- // as the main domain (eg. example.com + *.example.com)
123
- // filtering duplicates to avoid errors with adding the same record
124
- // to the route53 zone twice
125
- const unique = options
126
- . map ( ( val ) => val . ResourceRecord )
127
- . reduce ( ( acc , cur ) => {
128
- acc [ cur . Name ] = cur ;
129
- return acc ;
130
- } , { } ) ;
131
- records = Object . keys ( unique ) . sort ( ) . map ( key => unique [ key ] ) ;
132
- } else {
118
+
119
+ records = getDomainValidationRecords ( Certificate ) ;
120
+ if ( ! records . length ) {
133
121
// Exponential backoff with jitter based on 200ms base
134
122
// component of backoff fixed to ensure minimum total wait time on
135
123
// slow targets.
136
124
const base = Math . pow ( 2 , attempt ) ;
137
125
await sleep ( random ( ) * base * 50 + base * 150 ) ;
138
126
}
139
127
}
140
- if ( ! records ) {
128
+ if ( ! records . length ) {
141
129
throw new Error ( `Response from describeCertificate did not contain DomainValidationOptions after ${ maxAttempts } attempts.` )
142
130
}
143
131
144
-
145
132
console . log ( `Upserting ${ records . length } DNS records into zone ${ hostedZoneId } :` ) ;
146
133
147
- const changeBatch = await route53 . changeResourceRecordSets ( {
148
- ChangeBatch : {
149
- Changes : records . map ( ( record ) => {
150
- console . log ( `${ record . Name } ${ record . Type } ${ record . Value } ` )
151
- return {
152
- Action : 'UPSERT' ,
153
- ResourceRecordSet : {
154
- Name : record . Name ,
155
- Type : record . Type ,
156
- TTL : 60 ,
157
- ResourceRecords : [ {
158
- Value : record . Value
159
- } ]
160
- }
161
- } ;
162
- } ) ,
163
- } ,
164
- HostedZoneId : hostedZoneId
165
- } ) . promise ( ) ;
166
-
167
- console . log ( 'Waiting for DNS records to commit...' ) ;
168
- await route53 . waitFor ( 'resourceRecordSetsChanged' , {
169
- // Wait up to 5 minutes
170
- $waiter : {
171
- delay : 30 ,
172
- maxAttempts : 10
173
- } ,
174
- Id : changeBatch . ChangeInfo . Id
175
- } ) . promise ( ) ;
134
+ await commitRoute53Records ( route53 , records , hostedZoneId ) ;
176
135
177
136
console . log ( 'Waiting for validation...' ) ;
178
137
await acm . waitFor ( 'certificateValidated' , {
@@ -193,47 +152,126 @@ const requestCertificate = async function (requestId, domainName, subjectAlterna
193
152
*
194
153
* @param {string } arn The certificate ARN
195
154
*/
196
- const deleteCertificate = async function ( arn , region ) {
155
+ const deleteCertificate = async function ( arn , region , hostedZoneId , route53Endpoint , cleanupRecords ) {
197
156
const acm = new aws . ACM ( { region } ) ;
157
+ const route53 = route53Endpoint ? new aws . Route53 ( { endpoint : route53Endpoint } ) : new aws . Route53 ( ) ;
158
+ if ( waiter ) {
159
+ // Used by the test suite, since waiters aren't mockable yet
160
+ route53 . waitFor = acm . waitFor = waiter ;
161
+ }
198
162
199
163
try {
200
164
console . log ( `Waiting for certificate ${ arn } to become unused` ) ;
201
165
202
166
let inUseByResources ;
167
+ let records = [ ] ;
203
168
for ( let attempt = 0 ; attempt < maxAttempts ; attempt ++ ) {
204
169
const { Certificate } = await acm . describeCertificate ( {
205
170
CertificateArn : arn
206
171
} ) . promise ( ) ;
207
172
173
+ if ( cleanupRecords ) {
174
+ records = getDomainValidationRecords ( Certificate ) ;
175
+ }
208
176
inUseByResources = Certificate . InUseBy || [ ] ;
209
177
210
- if ( inUseByResources . length ) {
178
+ if ( inUseByResources . length || ! records . length ) {
211
179
// Exponential backoff with jitter based on 200ms base
212
180
// component of backoff fixed to ensure minimum total wait time on
213
181
// slow targets.
214
182
const base = Math . pow ( 2 , attempt ) ;
215
183
await sleep ( random ( ) * base * 50 + base * 150 ) ;
216
184
} else {
217
- break
185
+ break ;
218
186
}
219
187
}
220
188
221
189
if ( inUseByResources . length ) {
222
190
throw new Error ( `Response from describeCertificate did not contain an empty InUseBy list after ${ maxAttempts } attempts.` )
223
191
}
192
+ if ( cleanupRecords && ! records . length ) {
193
+ throw new Error ( `Response from describeCertificate did not contain DomainValidationOptions after ${ maxAttempts } attempts.` )
194
+ }
224
195
225
196
console . log ( `Deleting certificate ${ arn } ` ) ;
226
197
227
198
await acm . deleteCertificate ( {
228
199
CertificateArn : arn
229
200
} ) . promise ( ) ;
201
+
202
+ if ( cleanupRecords ) {
203
+ console . log ( `Deleting ${ records . length } DNS records from zone ${ hostedZoneId } :` ) ;
204
+
205
+ await commitRoute53Records ( route53 , records , hostedZoneId , 'DELETE' ) ;
206
+ }
207
+
230
208
} catch ( err ) {
231
209
if ( err . name !== 'ResourceNotFoundException' ) {
232
210
throw err ;
233
211
}
234
212
}
235
213
} ;
236
214
215
+ /**
216
+ * Retrieve the unique domain validation options as records to be upserted (or deleted) from Route53.
217
+ *
218
+ * Returns an empty array ([]) if the domain validation options is empty or the records are not yet ready.
219
+ */
220
+ function getDomainValidationRecords ( certificate ) {
221
+ const options = certificate . DomainValidationOptions || [ ] ;
222
+ // Ensure all records are ready; there is (at least a theory there's) a chance of a partial response here in rare cases.
223
+ if ( options . length > 0 && options . every ( opt => opt && ! ! opt . ResourceRecord ) ) {
224
+ // some alternative names will produce the same validation record
225
+ // as the main domain (eg. example.com + *.example.com)
226
+ // filtering duplicates to avoid errors with adding the same record
227
+ // to the route53 zone twice
228
+ const unique = options
229
+ . map ( ( val ) => val . ResourceRecord )
230
+ . reduce ( ( acc , cur ) => {
231
+ acc [ cur . Name ] = cur ;
232
+ return acc ;
233
+ } , { } ) ;
234
+ return Object . keys ( unique ) . sort ( ) . map ( key => unique [ key ] ) ;
235
+ }
236
+ return [ ] ;
237
+ }
238
+
239
+ /**
240
+ * Execute Route53 ChangeResourceRecordSets for a set of records within a Hosted Zone,
241
+ * and wait for the records to commit. Defaults to an 'UPSERT' action.
242
+ */
243
+ async function commitRoute53Records ( route53 , records , hostedZoneId , action = 'UPSERT' ) {
244
+ const changeBatch = await route53 . changeResourceRecordSets ( {
245
+ ChangeBatch : {
246
+ Changes : records . map ( ( record ) => {
247
+ console . log ( `${ record . Name } ${ record . Type } ${ record . Value } ` ) ;
248
+ return {
249
+ Action : action ,
250
+ ResourceRecordSet : {
251
+ Name : record . Name ,
252
+ Type : record . Type ,
253
+ TTL : 60 ,
254
+ ResourceRecords : [ {
255
+ Value : record . Value
256
+ } ]
257
+ }
258
+ } ;
259
+ } ) ,
260
+ } ,
261
+ HostedZoneId : hostedZoneId
262
+ } ) . promise ( ) ;
263
+
264
+ console . log ( 'Waiting for DNS records to commit...' ) ;
265
+ await route53 . waitFor ( 'resourceRecordSetsChanged' , {
266
+ // Wait up to 5 minutes
267
+ $waiter : {
268
+ delay : 30 ,
269
+ maxAttempts : 10
270
+ } ,
271
+ Id : changeBatch . ChangeInfo . Id
272
+ } ) . promise ( ) ;
273
+ }
274
+
237
275
/**
238
276
* Main handler, invoked by Lambda
239
277
*/
@@ -262,7 +300,13 @@ exports.certificateRequestHandler = async function (event, context) {
262
300
// If the resource didn't create correctly, the physical resource ID won't be the
263
301
// certificate ARN, so don't try to delete it in that case.
264
302
if ( physicalResourceId . startsWith ( 'arn:' ) ) {
265
- await deleteCertificate ( physicalResourceId , event . ResourceProperties . Region ) ;
303
+ await deleteCertificate (
304
+ physicalResourceId ,
305
+ event . ResourceProperties . Region ,
306
+ event . ResourceProperties . HostedZoneId ,
307
+ event . ResourceProperties . Route53Endpoint ,
308
+ event . ResourceProperties . CleanupRecords === "true" ,
309
+ ) ;
266
310
}
267
311
break ;
268
312
default :
0 commit comments