11const  {  getCloudFrontDomainName }  =  require ( "./cloudFront" ) ; 
22const  { getCertificateArn,  requestCertificateWithDNS}  =  require ( "./acm" ) ; 
33
4- const  entity  =  'Fullstack' 
5- 
64const  getHostedZoneForDomain  =  async  ( awsClient ,  domainName )  =>  { 
75    const  r53response  =  await  awsClient . request ( 'Route53' ,  'listHostedZones' ,  { } ) , 
86        hostedZone  =  r53response . HostedZones 
97            . find ( hostedZone  =>  `${ domainName }  .` . includes ( hostedZone . Name ) ) ; 
108
11-     if  ( ! hostedZone )  throw  `Domain is not managed by AWS, you will have to add a record for ${ domainName }   manually.` ; 
9+     // if (!hostedZone) throw `Domain is not managed by AWS, you will have to add a record for ${domainName} manually.`;
1210
1311    return  hostedZone ; 
1412} ; 
@@ -29,114 +27,177 @@ const waitForChange = async (checkChange) => {
2927        return  isChangeComplete 
3028    }  else  { 
3129        await  new  Promise ( r  =>  setTimeout ( r ,  1000 ) ) ; 
32-         return  await  waitForChange ( checkChange ,   serverless ) ; 
33-     } ; 
30+         return  await  waitForChange ( checkChange ) ; 
31+     } 
3432} ; 
3533
36- const  entryExists  =  async  ( awsClient ,  hostedZone ,   domainName ,  target )  =>  { 
34+ const  filterExistingAlias  =  async  ( awsClient ,  hostedZone ,  target )  =>  { 
3735    const  requestParams  =  { 
3836            HostedZoneId : hostedZone . Id 
3937        } , 
40-         r53response  =  await  awsClient . request ( 'Route53' ,  'listResourceRecordSets' ,  requestParams ) 
41-         sets  =  r53response . ResourceRecordSets ; 
38+         r53response  =  await  awsClient . request ( 'Route53' ,  'listResourceRecordSets' ,  requestParams ) , 
39+         sets  =  r53response . ResourceRecordSets , 
40+         filteredDomains  =  hostedZone . domains . filter ( domain  =>  ! sets . find ( set  =>  set . Name  ===  `${ domain }  .`  &&  set . AliasTarget ?. DNSName  ===  `${ target }  .` ) ) 
4241
43-     return  sets . find ( set   =>   set . Name   ===   ` ${ domainName } .`   &&   set . AliasTarget ?. DNSName   ===   ` ${ target } .` ) ; 
44- } 
42+     return  { ... hostedZone ,   domains :  filteredDomains } ; 
43+ } ; 
4544
46- const  addAliasRecord  =  async  ( serverless ,  domainName )  =>  { 
47-     const  awsClient  =  serverless . getProvider ( 'aws' ) 
45+ const  groupDomainsByHostedZone  =  async  ( awsClient ,  domains )  => 
46+     // Get hosted Zone for each domain, group domains by HZ.Id using .reduce and extract values 
47+     Object . values ( 
48+         await  domains . reduce ( async  ( promisedAccumulator ,  domain )  =>  { 
49+             const  hostedZones  =  await  promisedAccumulator ; 
50+             const  hostedZone  =  await  getHostedZoneForDomain ( awsClient ,  domain ) ; 
51+             if  ( hostedZones [ hostedZone ?. Id ] )  hostedZones [ hostedZone ?. Id ] . domains . push ( domain ) ; 
52+             else  hostedZones [ hostedZone ?. Id ]  =  { ...hostedZone ,  domains : [ domain ] } ; 
53+             return  hostedZones ; 
54+         } ,  { } ) 
55+     ) ; 
56+ 
57+ const  addCloudFrontAlias  =  async  ( serverless ,  domains )  =>  { 
58+     if  ( ! Array . isArray ( domains ) )  { 
59+         domains  =  [ domains ] ; 
60+     } 
61+ 
62+     const  awsClient  =  serverless . getProvider ( 'aws' ) , 
4863        target  =  await  getCloudFrontDomainName ( serverless ) , 
49-         hostedZone  =  await  getHostedZoneForDomain ( awsClient ,  domainName ) ; 
50- 
51-     if  ( await  entryExists ( awsClient ,  hostedZone ,  domainName ,  target ) )  return ; 
52- 
53-     serverless . cli . log ( `Adding ALIAS record for ${ domainName }   to point to ${ target }  ...` ,  entity ) ; 
54- 
55-     const  changeRecordParams  =  { 
56-             HostedZoneId : hostedZone . Id , 
57-             ChangeBatch : { 
58-                 Changes : [ 
59-                     { 
60-                         Action : 'UPSERT' , 
61-                         ResourceRecordSet : { 
62-                             Name : domainName , 
63-                             Type : 'A' , 
64-                             AliasTarget : { 
65-                                 HostedZoneId : 'Z2FDTNDATAQYW2' ,  // global CloudFront HostedZoneId 
66-                                 DNSName : target , 
67-                                 EvaluateTargetHealth : false 
64+         domainsByHostedZones  =  await  groupDomainsByHostedZone ( awsClient ,  domains ) , 
65+         domainsWithoutHostedZone  =  domainsByHostedZones 
66+                 . filter ( hostedZone  =>  ! hostedZone . Id ) 
67+                 . reduce ( ( acc ,  hostedZone )  =>  [ ...acc ,  ...hostedZone . domains ] , [ ] ) , 
68+         filteredDomainsByHostedZones  =  ( await  Promise . all ( domainsByHostedZones 
69+                 . filter ( hostedZone  =>  ! ! hostedZone . Id ) 
70+                 . map ( hostedZone  =>  filterExistingAlias ( awsClient ,  hostedZone ,  target ) ) ) ) 
71+                 . filter ( hostedZone  =>  hostedZone . domains . length ) ; 
72+ 
73+     if  ( domainsWithoutHostedZone ?. length  >  0 )  
74+         serverless . cli . log ( `No hosted zones found for ${ domainsWithoutHostedZone }  , records pointing to` 
75+                             + ` ${ target }   will have to be added manually.` ,  "Route53" ,  { color : "orange" ,  underline : true } ) ; 
76+     
77+     await  Promise . all ( filteredDomainsByHostedZones . map ( async  hostedZone  =>  { 
78+         hostedZone . domains . forEach ( domain  =>  
79+             serverless . cli . log ( `Adding ALIAS record for ${ domain }   to point to ${ target }  ...` ) 
80+         ) ; 
81+         
82+         const  changeRecordParams  =  { 
83+                 HostedZoneId : hostedZone . Id , 
84+                 ChangeBatch : { 
85+                     Changes : hostedZone . domains . map ( domainName  =>  ( 
86+                         { 
87+                             Action : 'UPSERT' , 
88+                             ResourceRecordSet : { 
89+                                 Name : domainName , 
90+                                 Type : 'A' , 
91+                                 AliasTarget : { 
92+                                     HostedZoneId : 'Z2FDTNDATAQYW2' ,  // global CloudFront HostedZoneId 
93+                                     DNSName : target , 
94+                                     EvaluateTargetHealth : false 
95+                                 } 
6896                            } 
6997                        } 
70-                     } 
71-                 ] 
72-             } 
73-         } , 
74-         changeRecordResult  =  await  awsClient . request ( 'Route53' ,  'changeResourceRecordSets' ,  changeRecordParams ) ; 
98+                     ) ) 
99+                 } 
100+             } , 
101+             changeRecordResult  =  await  awsClient . request ( 'Route53' ,  'changeResourceRecordSets' ,  changeRecordParams ) ; 
75102
76-     // wait for DNS entry 
77-     await  waitForChange ( ( )  =>  checkChangeStatus ( awsClient ,  changeRecordResult . ChangeInfo ) ) ; 
103+          // wait for DNS entry 
104+          await  waitForChange ( ( )  =>  checkChangeStatus ( awsClient ,  changeRecordResult . ChangeInfo ) ) ; 
78105
79-     serverless . cli . log ( `ALIAS ${ domainName }   -> ${ target }   successfully added.` ,   entity ) ; 
106+          serverless . cli . log ( `ALIAS ${ hostedZone . domains }   -> ${ target }   successfully added.` ) ; 
80107
81-     // waitFor can't be called using Provider.request yet 
82-     /* 
83-     waitForRecordParams = { 
84-         Id: changeRecordResult.ChangeInfo.Id 
85-     }, 
86-      
87-     {err, waitForRecordResult} = await awsClient.request('Route53', 'waitFor', 'resourceRecordSetsChanged', waitForRecordParams) 
108+         // waitFor can't be called using Provider.request yet 
109+         /* 
110+         waitForRecordParams = { 
111+             Id: changeRecordResult.ChangeInfo.Id 
112+         }, 
88113
89-     serverless.cli.log(err ) 
90-     serverless.cli.log(waitForRecordResult)  
91-     */       
114+         {err, waitForRecordResult} = await awsClient.request('Route53', 'waitFor', 'resourceRecordSetsChanged', waitForRecordParams ) 
115+         */  
116+     } ) ) ; 
92117} ; 
93118
94- const  setupCertificate  =  async  ( serverless ,  domainName )  =>  { 
95-     const  existingCertificateArn  =  await  getCertificateArn ( serverless ,  domainName ) ; 
96-     if  ( existingCertificateArn )  return  existingCertificateArn ; 
97- 
98-     serverless . cli . log ( `Requesting certificate for ${ domainName }  ...` ,  entity ) ; 
99- 
100-     const  awsClient  =  serverless . getProvider ( 'aws' ) 
101-         hostedZone  =  await  getHostedZoneForDomain ( awsClient ,  domainName ) , 
102-         getCertificateRecord  =  async  ( serverless ,  domainName )  =>  { 
103-             const  certificaterequest  =  await  requestCertificateWithDNS ( serverless ,  domainName ) ; 
104-             return  certificaterequest . DomainValidationOptions [ 0 ] . ResourceRecord 
119+ const  groupResourceRecordsByHostedZone  =  async  ( awsClient ,  resourceRecords )  => 
120+     // Get hosted Zone for each resourcerecord, group resourcerecords by HZ.Id using .reduce and extract values 
121+     Object . values ( 
122+         await  resourceRecords . reduce ( async  ( promisedAccumulator ,  resourceRecord )  =>  { 
123+             const  hostedZones  =  await  promisedAccumulator ; 
124+             const  hostedZone  =  await  getHostedZoneForDomain ( awsClient ,  resourceRecord . Name ) ; 
125+             if  ( hostedZones [ hostedZone ?. Id ] )  hostedZones [ hostedZone ?. Id ] . resourceRecords . push ( resourceRecord ) ; 
126+             else  hostedZones [ hostedZone ?. Id ]  =  { ...hostedZone ,  resourceRecords : [ resourceRecord ] } ; 
127+             return  hostedZones ; 
128+         } ,  { } ) 
129+     ) ; 
130+ 
131+ const  setupCertificate  =  async  ( serverless ,  domains )  =>  { 
132+     const  existingCertificateArn  =  await  getCertificateArn ( serverless ,  domains ) ; 
133+     if  ( existingCertificateArn )  { 
134+         return  existingCertificateArn ; 
135+     } 
136+ 
137+     if  ( ! Array . isArray ( domains ) )  { 
138+         domains  =  [ domains ] ; 
139+     } 
140+ 
141+     serverless . cli . log ( `Requesting certificate for ${ domains }  ...` ) ; 
142+ 
143+     const  awsClient  =  serverless . getProvider ( 'aws' ) , 
144+         getCertificateRecords  =  async  ( serverless ,  domains )  =>  { 
145+             const  certificaterequest  =  await  requestCertificateWithDNS ( serverless ,  domains ) , 
146+                 resourceRecords  =  certificaterequest . DomainValidationOptions 
147+                         . filter ( validationOption  =>  validationOption . ValidationStatus  !==  "SUCCESS" ) 
148+                         . map ( validationOption  =>  validationOption . ResourceRecord ) ; 
149+             return  resourceRecords . every ( e  =>  ! ! e )  &&  resourceRecords . length  ===  domains . length  ? resourceRecords  : null 
105150        } , 
106-         // sometimes the ResourceRecord entry isn't immediately available, so we wait until it is 
107-         certificateResourceRecord  =  await  waitForChange ( ( )  =>  getCertificateRecord ( serverless ,  domainName ) ) , 
108-         changeRecordParams  =  { 
109-             HostedZoneId : hostedZone . Id , 
110-             ChangeBatch : { 
111-                 Changes : [ 
112-                     { 
113-                         Action : 'UPSERT' , 
114-                         ResourceRecordSet : { 
115-                             Name : certificateResourceRecord . Name , 
116-                             Type : certificateResourceRecord . Type , 
117-                             TTL : 60 , 
118-                             ResourceRecords : [ 
119-                                 { 
120-                                     Value : certificateResourceRecord . Value 
121-                                 } 
122-                             ] 
151+         // sometimes the ResourceRecords entries aren't immediately available, so we wait until they are 
152+         certificateResourceRecords  =  await  waitForChange ( ( )  =>  getCertificateRecords ( serverless ,  domains ) ) , 
153+         resourceRecordsByHostedZones  =  await  groupResourceRecordsByHostedZone ( awsClient ,  certificateResourceRecords ) , 
154+         resourceRecordsWithoutHostedZone  =  resourceRecordsByHostedZones 
155+                 . filter ( hostedZone  =>  ! hostedZone . Id ) 
156+                 . reduce ( ( acc ,  hostedZone )  =>  [ ...acc ,  ...hostedZone . resourceRecords ] , [ ] ) , 
157+         filteredResourceRecordsByHostedZones  =  resourceRecordsByHostedZones . filter ( hostedZone  =>  ! ! hostedZone . Id ) ; 
158+ 
159+     resourceRecordsWithoutHostedZone . forEach ( ( resourceRecord )  =>  { 
160+       serverless . cli . log ( 
161+         `Needs to be added manually: ${ resourceRecord . Type }   ${ resourceRecord . Name }   ${ resourceRecord . Value }  ` , 
162+         "Route53" , 
163+         {  color : "orange" ,  underline : true  } 
164+       ) ; 
165+     } ) ; 
166+ 
167+     await  Promise . all ( filteredResourceRecordsByHostedZones . map ( async  hostedZone  =>  { 
168+         const  changeRecordParams  =  { 
169+                 HostedZoneId : hostedZone . Id , 
170+                 ChangeBatch : { 
171+                     Changes : hostedZone . resourceRecords . map ( resourceRecord  =>  ( 
172+                         { 
173+                             Action : 'UPSERT' , 
174+                             ResourceRecordSet : { 
175+                                 Name : resourceRecord . Name , 
176+                                 Type : resourceRecord . Type , 
177+                                 TTL : 60 , 
178+                                 ResourceRecords : [ 
179+                                     { 
180+                                         Value : resourceRecord . Value 
181+                                     } 
182+                                 ] 
183+                             } 
123184                        } 
124-                     } 
125-                 ] 
126-             } 
127-         } , 
128-          changeRecordResult   =   await   awsClient . request ( 'Route53' ,   'changeResourceRecordSets' ,   changeRecordParams ) ; 
129- 
130-     // wait for DNS entry 
131-     await   waitForChange ( ( )   =>   checkChangeStatus ( awsClient ,   changeRecordResult . ChangeInfo ) ) ; 
132- 
133-     // wait  for issued  certificate
134-     const  certificateArn  =  await  waitForChange ( ( )  =>  getCertificateArn ( serverless ,  domainName ) ) ; 
135-     serverless . cli . log ( `Certificate for ${ domainName }   successfully issued.` ,   entity ) ; 
185+                     ) ) 
186+                 } 
187+             } , 
188+              changeRecordResult   =   await   awsClient . request ( 'Route53' ,   'changeResourceRecordSets' ,   changeRecordParams ) ; 
189+ 
190+          // wait for DNS entry 
191+          await   waitForChange ( ( )   =>   checkChangeStatus ( awsClient ,   changeRecordResult . ChangeInfo ) ) ; 
192+     } ) ) ; 
193+ 
194+     serverless . cli . log ( `Waiting  for certificate verification...` ) ; 
195+     const  certificateArn  =  await  waitForChange ( ( )  =>  getCertificateArn ( serverless ,  domains ) ) ; 
196+     serverless . cli . log ( `Certificate for ${ domains }   successfully issued.` ) ; 
136197    return  certificateArn ; 
137198} ; 
138199
139200module . exports  =  { 
140-     addAliasRecord , 
201+     addCloudFrontAlias , 
141202    setupCertificate
142- } ; 
203+ } ; 
0 commit comments