1
1
const { getCloudFrontDomainName } = require ( "./cloudFront" ) ;
2
2
const { getCertificateArn, requestCertificateWithDNS} = require ( "./acm" ) ;
3
3
4
- const entity = 'Fullstack'
5
-
6
4
const getHostedZoneForDomain = async ( awsClient , domainName ) => {
7
5
const r53response = await awsClient . request ( 'Route53' , 'listHostedZones' , { } ) ,
8
6
hostedZone = r53response . HostedZones
9
7
. find ( hostedZone => `${ domainName } .` . includes ( hostedZone . Name ) ) ;
10
8
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.`;
12
10
13
11
return hostedZone ;
14
12
} ;
@@ -29,114 +27,177 @@ const waitForChange = async (checkChange) => {
29
27
return isChangeComplete
30
28
} else {
31
29
await new Promise ( r => setTimeout ( r , 1000 ) ) ;
32
- return await waitForChange ( checkChange , serverless ) ;
33
- } ;
30
+ return await waitForChange ( checkChange ) ;
31
+ }
34
32
} ;
35
33
36
- const entryExists = async ( awsClient , hostedZone , domainName , target ) => {
34
+ const filterExistingAlias = async ( awsClient , hostedZone , target ) => {
37
35
const requestParams = {
38
36
HostedZoneId : hostedZone . Id
39
37
} ,
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 } .` ) )
42
41
43
- return sets . find ( set => set . Name === ` ${ domainName } .` && set . AliasTarget ?. DNSName === ` ${ target } .` ) ;
44
- }
42
+ return { ... hostedZone , domains : filteredDomains } ;
43
+ } ;
45
44
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' ) ,
48
63
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
+ }
68
96
}
69
97
}
70
- }
71
- ]
72
- }
73
- } ,
74
- changeRecordResult = await awsClient . request ( 'Route53' , 'changeResourceRecordSets' , changeRecordParams ) ;
98
+ ) )
99
+ }
100
+ } ,
101
+ changeRecordResult = await awsClient . request ( 'Route53' , 'changeResourceRecordSets' , changeRecordParams ) ;
75
102
76
- // wait for DNS entry
77
- await waitForChange ( ( ) => checkChangeStatus ( awsClient , changeRecordResult . ChangeInfo ) ) ;
103
+ // wait for DNS entry
104
+ await waitForChange ( ( ) => checkChangeStatus ( awsClient , changeRecordResult . ChangeInfo ) ) ;
78
105
79
- serverless . cli . log ( `ALIAS ${ domainName } -> ${ target } successfully added.` , entity ) ;
106
+ serverless . cli . log ( `ALIAS ${ hostedZone . domains } -> ${ target } successfully added.` ) ;
80
107
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
+ },
88
113
89
- serverless.cli.log(err )
90
- serverless.cli.log(waitForRecordResult)
91
- */
114
+ {err, waitForRecordResult} = await awsClient.request('Route53', 'waitFor', 'resourceRecordSetsChanged', waitForRecordParams )
115
+ */
116
+ } ) ) ;
92
117
} ;
93
118
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
105
150
} ,
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
+ }
123
184
}
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.` ) ;
136
197
return certificateArn ;
137
198
} ;
138
199
139
200
module . exports = {
140
- addAliasRecord ,
201
+ addCloudFrontAlias ,
141
202
setupCertificate
142
- } ;
203
+ } ;
0 commit comments