@@ -14,6 +14,7 @@ let waiter;
14
14
let sleep = defaultSleep ;
15
15
let random = Math . random ;
16
16
let maxAttempts = 10 ;
17
+ let domainTypes ;
17
18
18
19
/**
19
20
* Upload a CloudFormation response object to S3.
@@ -85,36 +86,34 @@ let report = function (
85
86
* Lastly, the function exits until the certificate is validated.
86
87
*
87
88
* @param {string } requestId the CloudFormation request ID
88
- * @param {string } appName the name of the application
89
- * @param {string } envName the name of the environment
90
- * @param {string } domainName the Common Name (CN) field for the requested certificate
91
- * @param {string } subjectAlternativeNames additional FQDNs to be included in the
92
- * Subject Alternative Name extension of the requested certificate
89
+ * @param {string } appName the application name
90
+ * @param {string } envName the environment name
91
+ * @param {string } certDomain the domain of the certificate
92
+ * @param {string } aliases the custom domain aliases
93
93
* @param {string } envHostedZoneId the environment Route53 Hosted Zone ID
94
94
* @param {string } rootDnsRole the IAM role ARN that can manage domainName
95
- * @param {string } isAliasEnabled whether alias is enabled
96
95
* @returns {string } Validated certificate ARN
97
96
*/
98
97
const requestCertificate = async function (
99
98
requestId ,
100
99
appName ,
101
100
envName ,
102
- domainName ,
103
- subjectAlternativeNames ,
101
+ certDomain ,
102
+ aliases ,
104
103
envHostedZoneId ,
105
104
rootDnsRole ,
106
- isAliasEnabled ,
107
105
region
108
106
) {
109
107
const crypto = require ( "crypto" ) ;
110
108
const [ acm , envRoute53 , appRoute53 ] = clients ( region , rootDnsRole ) ;
111
- var sansToUse =
112
- isAliasEnabled === "false"
113
- ? [ `*.${ envName } .${ appName } .${ domainName } ` ]
114
- : subjectAlternativeNames ;
109
+ // For backward compatiblity.
110
+ const sansToUse = [ `*.${ certDomain } ` ] ;
111
+ for ( const alias of aliases ) {
112
+ sansToUse . push ( alias ) ;
113
+ }
115
114
const reqCertResponse = await acm
116
115
. requestCertificate ( {
117
- DomainName : ` ${ envName } . ${ appName } . ${ domainName } ` ,
116
+ DomainName : certDomain ,
118
117
SubjectAlternativeNames : sansToUse ,
119
118
IdempotencyToken : crypto
120
119
. createHash ( "sha256" )
@@ -170,9 +169,6 @@ const requestCertificate = async function (
170
169
await updateHostedZoneRecords (
171
170
"UPSERT" ,
172
171
options ,
173
- envName ,
174
- appName ,
175
- domainName ,
176
172
envRoute53 ,
177
173
appRoute53 ,
178
174
envHostedZoneId
@@ -195,17 +191,15 @@ const requestCertificate = async function (
195
191
const updateHostedZoneRecords = async function (
196
192
action ,
197
193
options ,
198
- envName ,
199
- appName ,
200
- domainName ,
201
194
envRoute53 ,
202
195
appRoute53 ,
203
196
envHostedZoneId
204
197
) {
205
198
const promises = [ ] ;
206
199
for ( const option of options ) {
207
- switch ( option . DomainName ) {
208
- case `${ envName } .${ appName } .${ domainName } ` :
200
+ const domainType = await getDomainType ( option . DomainName ) ;
201
+ switch ( domainType ) {
202
+ case domainTypes . EnvDomainZone :
209
203
promises . push (
210
204
validateDomain ( {
211
205
route53 : envRoute53 ,
@@ -216,23 +210,23 @@ const updateHostedZoneRecords = async function (
216
210
} )
217
211
) ;
218
212
break ;
219
- case ` ${ appName } . ${ domainName } ` :
213
+ case domainTypes . AppDomainZone :
220
214
promises . push (
221
215
validateDomain ( {
222
216
route53 : appRoute53 ,
223
217
record : option . ResourceRecord ,
224
218
action : action ,
225
- domainName : ` ${ appName } . ${ domainName } ` ,
219
+ domainName : domainType . domain ,
226
220
} )
227
221
) ;
228
222
break ;
229
- case domainName :
223
+ case domainTypes . RootDomainZone :
230
224
promises . push (
231
225
validateDomain ( {
232
226
route53 : appRoute53 ,
233
227
record : option . ResourceRecord ,
234
228
action : action ,
235
- domainName : domainName ,
229
+ domainName : domainType . domain ,
236
230
} )
237
231
) ;
238
232
break ;
@@ -250,9 +244,7 @@ const updateHostedZoneRecords = async function (
250
244
// if there is no other certificate using the record.
251
245
const deleteHostedZoneRecords = async function (
252
246
options ,
253
- envName ,
254
- appName ,
255
- domainName ,
247
+ certDomain ,
256
248
envRoute53 ,
257
249
appRoute53 ,
258
250
acm ,
@@ -265,10 +257,7 @@ const deleteHostedZoneRecords = async function (
265
257
isLegacyCert = true ;
266
258
}
267
259
268
- const certsWithEnvDomain = await numOfGeneratedCertificates (
269
- acm ,
270
- `${ envName } .${ appName } .${ domainName } `
271
- ) ;
260
+ const certsWithEnvDomain = await numOfGeneratedCertificates ( acm , certDomain ) ;
272
261
const isLastOne = certsWithEnvDomain === 1 ;
273
262
274
263
const newOptions = [ ] ;
@@ -293,19 +282,28 @@ const deleteHostedZoneRecords = async function (
293
282
// we'll remove validation CNAME records only for app and root hosted zone,
294
283
// since the legacy cert still needs the validation record in the env hosted zone.
295
284
for ( const option of options ) {
296
- if ( option . DomainName === ` ${ envName } .${ appName } . ${ domainName } `) {
285
+ if ( option . DomainName === certDomain || option . DomainName === `* .${ certDomain } `) {
297
286
continue ;
298
287
}
299
288
newOptions . push ( option ) ;
300
289
}
301
290
break ;
302
291
}
292
+ // Make sure DNS validation records are unique. For example: "example.com" and "*.example.com"
293
+ // might have the same DNS validation record.
294
+ const filteredOption = [ ] ;
295
+ var uniqueValidateRecordNames = new Set ( ) ;
296
+ for ( const option of newOptions ) {
297
+ var id = `${ option . ResourceRecord . Name } ${ option . ResourceRecord . Value } ` ;
298
+ if ( uniqueValidateRecordNames . has ( id ) ) {
299
+ continue ;
300
+ }
301
+ uniqueValidateRecordNames . add ( id ) ;
302
+ filteredOption . push ( option ) ;
303
+ }
303
304
await updateHostedZoneRecords (
304
305
"DELETE" ,
305
- newOptions ,
306
- envName ,
307
- appName ,
308
- domainName ,
306
+ filteredOption ,
309
307
envRoute53 ,
310
308
appRoute53 ,
311
309
envHostedZoneId
@@ -380,12 +378,13 @@ const validateDomain = async function ({
380
378
* If the certificate does not exist, the function will return normally.
381
379
*
382
380
* @param {string } arn The certificate ARN
381
+ * @param {string } certDomain the domain of the certificate
382
+ * @param {string } envHostedZoneId the environment Route53 Hosted Zone ID
383
+ * @param {string } rootDnsRole the IAM role ARN that can manage domainName
383
384
*/
384
385
const deleteCertificate = async function (
385
386
arn ,
386
- appName ,
387
- envName ,
388
- domainName ,
387
+ certDomain ,
389
388
region ,
390
389
envHostedZoneId ,
391
390
rootDnsRole
@@ -421,7 +420,6 @@ const deleteCertificate = async function (
421
420
break ;
422
421
}
423
422
}
424
-
425
423
if ( inUseByResources . length ) {
426
424
throw new Error (
427
425
`Certificate still in use after checking for ${ maxAttempts } attempts.`
@@ -430,9 +428,7 @@ const deleteCertificate = async function (
430
428
431
429
await deleteHostedZoneRecords (
432
430
options ,
433
- envName ,
434
- appName ,
435
- domainName ,
431
+ certDomain ,
436
432
envRoute53 ,
437
433
appRoute53 ,
438
434
acm ,
@@ -496,6 +492,38 @@ const updateRecords = function (
496
492
. promise ( ) ;
497
493
} ;
498
494
495
+ // getAllAliases gets all aliases out from a string. For example:
496
+ // {"frontend": ["test.foobar.com", "foobar.com"], "api": ["api.foobar.com"]} will return
497
+ // ["test.foobar.com", "foobar.com", "api.foobar.com"].
498
+ const getAllAliases = function ( aliases ) {
499
+ let obj ;
500
+ try {
501
+ obj = JSON . parse ( aliases || "{}" ) ;
502
+ } catch ( error ) {
503
+ throw new Error ( `Cannot parse ${ aliases } into JSON format.` ) ;
504
+ }
505
+ var aliasList = [ ] ;
506
+ for ( var m in obj ) {
507
+ aliasList . push ( ...obj [ m ] ) ;
508
+ }
509
+ return new Set ( aliasList . filter ( function ( itm ) {
510
+ return getDomainType ( itm ) != domainTypes . OtherDomainZone ;
511
+ } ) ) ;
512
+ } ;
513
+
514
+ const getDomainType = function ( alias ) {
515
+ if ( domainTypes . EnvDomainZone . regex . test ( alias ) ) {
516
+ return domainTypes . EnvDomainZone ;
517
+ }
518
+ if ( domainTypes . AppDomainZone . regex . test ( alias ) ) {
519
+ return domainTypes . AppDomainZone ;
520
+ }
521
+ if ( domainTypes . RootDomainZone . regex . test ( alias ) ) {
522
+ return domainTypes . RootDomainZone ;
523
+ }
524
+ return domainTypes . OtherDomainZone ;
525
+ } ;
526
+
499
527
const clients = function ( region , rootDnsRole ) {
500
528
const acm = new aws . ACM ( {
501
529
region,
@@ -522,20 +550,64 @@ exports.certificateRequestHandler = async function (event, context) {
522
550
var physicalResourceId ;
523
551
var certificateArn ;
524
552
const props = event . ResourceProperties ;
553
+ const [ app , env , domain ] = [ props . AppName , props . EnvName , props . DomainName ] ;
554
+ domainTypes = {
555
+ EnvDomainZone : {
556
+ regex : new RegExp ( `^([^\.]+\.)?${ env } .${ app } .${ domain } ` ) ,
557
+ domain : `${ env } .${ app } .${ domain } ` ,
558
+ } ,
559
+ AppDomainZone : {
560
+ regex : new RegExp ( `^([^\.]+\.)?${ app } .${ domain } ` ) ,
561
+ domain : `${ app } .${ domain } ` ,
562
+ } ,
563
+ RootDomainZone : {
564
+ regex : new RegExp ( `^([^\.]+\.)?${ domain } ` ) ,
565
+ domain : `${ domain } ` ,
566
+ } ,
567
+ OtherDomainZone : { } ,
568
+ } ;
525
569
526
570
try {
571
+ var certDomain = `${ props . EnvName } .${ props . AppName } .${ props . DomainName } ` ;
572
+ var aliases = await getAllAliases ( props . Aliases ) ;
527
573
switch ( event . RequestType ) {
528
574
case "Create" :
575
+ certificateArn = await requestCertificate (
576
+ event . RequestId ,
577
+ props . AppName ,
578
+ props . EnvName ,
579
+ certDomain ,
580
+ aliases ,
581
+ props . EnvHostedZoneId ,
582
+ props . RootDNSRole ,
583
+ props . Region
584
+ ) ;
585
+ responseData . Arn = physicalResourceId = certificateArn ;
586
+ break ;
529
587
case "Update" :
588
+ // Exit early if cert doesn't change.
589
+ if ( event . OldResourceProperties ) {
590
+ var prevAliases = await getAllAliases (
591
+ event . OldResourceProperties . Aliases
592
+ ) ;
593
+ var aliasesToDelete = [ ...prevAliases ] . filter ( function ( itm ) {
594
+ return ! aliases . has ( itm ) ;
595
+ } ) ;
596
+ var aliasesToAdd = [ ...aliases ] . filter ( function ( itm ) {
597
+ return ! prevAliases . has ( itm ) ;
598
+ } ) ;
599
+ if ( aliasesToAdd . length + aliasesToDelete . length === 0 ) {
600
+ break ;
601
+ }
602
+ }
530
603
certificateArn = await requestCertificate (
531
604
event . RequestId ,
532
605
props . AppName ,
533
606
props . EnvName ,
534
- props . DomainName ,
535
- props . SubjectAlternativeNames ,
607
+ certDomain ,
608
+ aliases ,
536
609
props . EnvHostedZoneId ,
537
610
props . RootDNSRole ,
538
- props . IsAliasEnabled ,
539
611
props . Region
540
612
) ;
541
613
responseData . Arn = physicalResourceId = certificateArn ;
@@ -547,9 +619,7 @@ exports.certificateRequestHandler = async function (event, context) {
547
619
if ( physicalResourceId . startsWith ( "arn:" ) ) {
548
620
await deleteCertificate (
549
621
physicalResourceId ,
550
- props . AppName ,
551
- props . EnvName ,
552
- props . DomainName ,
622
+ certDomain ,
553
623
props . Region ,
554
624
props . EnvHostedZoneId ,
555
625
props . RootDNSRole
@@ -559,7 +629,6 @@ exports.certificateRequestHandler = async function (event, context) {
559
629
default :
560
630
throw new Error ( `Unsupported request type ${ event . RequestType } ` ) ;
561
631
}
562
-
563
632
await report ( event , context , "SUCCESS" , physicalResourceId , responseData ) ;
564
633
} catch ( err ) {
565
634
console . log ( `Caught error ${ err } .` ) ;
0 commit comments