-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathutils.js
345 lines (298 loc) · 9.69 KB
/
utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
const AWS = require('aws-sdk')
const { log, logdev } = require('../../printers/index')
const conf = {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION,
// may, may not be defined
// sessionToken: process.env.AWS_SESSION_TOKEN || undefined,
}
if (process.env.AWS_SESSION_TOKEN != null && process.env.AWS_SESSION_TOKEN !== 'undefined') {
conf.sessionToken = process.env.AWS_SESSION_TOKEN
}
AWS.config.update(conf)
/**
* @description Creates a new REGIONAL API in "region" named "apiName"
* @param {string} apiName Name of API
* @param {string} apiRegion
* @returns {Promise<{apiId: string, apiUrl: string}>} Id and URL of the endpoint
*/
async function createApi(apiName, apiRegion) {
const apigatewayv2 = new AWS.ApiGatewayV2({
apiVersion: '2018-11-29',
region: apiRegion,
})
// @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ApiGatewayV2.html#createApi-property
const createApiParams = {
Name: apiName,
ProtocolType: 'HTTP',
// TODO regional is default, but how the to set to EDGE later on?
// EndpointType: 'REGIONAL', // invalid field
// Target: targetlambdaArn, // TODO
CorsConfiguration: {
AllowMethods: [
'POST',
'GET',
],
AllowOrigins: [
'*',
],
},
}
const createApiRes = await apigatewayv2.createApi(createApiParams).promise()
const res = {
apiId: createApiRes.ApiId,
apiUrl: createApiRes.ApiEndpoint,
}
return res
}
/**
*
* @param {string} apiId
* @param {string} apiRegion
* @returns {Promise<void>}
*/
async function deleteApi(apiId, apiRegion) {
const apigatewayv2 = new AWS.ApiGatewayV2({
apiVersion: '2018-11-29',
region: apiRegion,
})
const deleteApiParams = {
ApiId: apiId,
}
await apigatewayv2.deleteApi(deleteApiParams).promise()
}
/**
* @description Returns the IntegrationId of the integration that matches 'name', or null.
* If multiple exist, it returns the IntegrationId of the first one in the list.
* @param {*} apiId
* @param {*} apiRegion
* @param {*} name
*/
async function getIntegrationId(apiId, apiRegion, name) {
// On amazon, integration names are not unique,
// but HF treats the as unique
// and always reuses them
const apigatewayv2 = new AWS.ApiGatewayV2({
apiVersion: '2018-11-29',
region: apiRegion,
})
const getParams = {
ApiId: apiId,
MaxResults: '9999',
}
// Get all integrations
let res = await apigatewayv2.getIntegrations(getParams).promise()
res = res.Items
// Only integrations that match name
.filter((el) => el.IntegrationUri.split(':')[-1] === name)
// Just take the first one
res = res[0]
if (res && res.IntegrationId) {
return res.IntegrationId
} else {
return null
}
}
/**
* @description Gets route Ids of GET and POST methods that match routePath
* @param {*} apiId
* @param {*} apiRegion
* @param {*} routePath
* @returns {[ { AuthorizationType: string, RouteId: string, RouteKey: string }]}
*/
async function getGETPOSTRoutesAt(apiId, apiRegion, routePath) {
const apigatewayv2 = new AWS.ApiGatewayV2({
apiVersion: '2018-11-29',
region: apiRegion,
})
// TODO Amazon might return a paginated response here (?)
// In that case with many routes, the route we look for may not be on first page
const params = {
ApiId: apiId,
MaxResults: '9999', // string according to docs and it works... uuh?
}
const res = await apigatewayv2.getRoutes(params).promise()
const matchingRoutes = res.Items
.filter((item) => item.RouteKey && item.RouteKey.includes(routePath) === true)
// only GET and POST ones
.filter((item) => /GET|POST/.test(item.RouteKey) === true)
return matchingRoutes
}
/**
* Creates or update GET and POST routes with an integration.
* If only one of GET or POST routes exist (user likely deleted one of them),
* it updates that one.
* Otherwise, it creates new both GET, POST routes.
* Use createDefaultAutodeployStage too when creating, so that changes are made public
* @param {*} apiId
* @param {*} apiRegion
* @param {*} routePath '/endpoint-1' for example
* @param {*} integrationId
*/
async function setRoute(apiId, apiRegion, routePath, integrationId) {
const apigatewayv2 = new AWS.ApiGatewayV2({
apiVersion: '2018-11-29',
region: apiRegion,
})
// Get route ids of GET & POST at that routePath
const routes = await getGETPOSTRoutesAt(apiId, apiRegion, routePath)
if (routes.length > 0) {
/// ////////////////////////////////////////////////
// Update routes (GET, POST) with integrationId
/// ////////////////////////////////////////////////
await Promise.all(
routes.map(async (r) => {
// skip if integration id is set correctly already
if (r.Target && r.Target === `integrations/${integrationId}`) {
return
}
const updateRouteParams = {
ApiId: apiId,
AuthorizationType: 'NONE',
RouteId: r.RouteId,
RouteKey: r.RouteKey,
Target: `integrations/${integrationId}`,
}
try {
const updateRes = await apigatewayv2.updateRoute(updateRouteParams).promise()
} catch (e) {
// This happens eg when there is only one of GET OR POST route (routes.length > 0)
// Usually when the user deliberately deleted one of the
// Ignore, as it's likely intented
}
}),
)
} else {
/// ////////////////////////////////////////////////
// Create routes (GET, POST) with integrationId
/// ////////////////////////////////////////////////
// Create GET route
const createGETRouteParams = {
ApiId: apiId,
AuthorizationType: 'NONE',
RouteKey: `GET ${routePath}`,
Target: `integrations/${integrationId}`,
}
const createGETRes = await apigatewayv2.createRoute(createGETRouteParams).promise()
// Create POST route
const createPOSTRouteParams = { ...createGETRouteParams }
createPOSTRouteParams.RouteKey = `POST ${routePath}`
const createPOSTRes = await apigatewayv2.createRoute(createPOSTRouteParams).promise()
}
}
/**
* Creates a (possibly duplicate) integration that can be attached.
* Before creating, check getIntegrationId to avoid duplicates
* @param {*} apiId
* @param {*} apiRegion
* @param {string} targetLambdaArn For instance a Lambda ARN
* @returns {string} IntegrationId
*/
async function createIntegration(apiId, apiRegion, targetLambdaArn) {
const apigatewayv2 = new AWS.ApiGatewayV2({
apiVersion: '2018-11-29',
region: apiRegion,
})
const params = {
ApiId: apiId,
IntegrationType: 'AWS_PROXY',
IntegrationUri: targetLambdaArn,
PayloadFormatVersion: '2.0',
}
const res = await apigatewayv2.createIntegration(params).promise()
const integrationId = res.IntegrationId
return integrationId
}
/**
* @description Returns ApiId and ApiEndpoint of a regional API gateway API
* with the name "apiName", in "apiRegion".
* If multiple APIs exist with that name, it warns, and uses the first one in the received list.
* If none exist, it returns null.
* @param {string} apiName
* @param {string} apiRegion
* @returns {Promise<{apiId: string, apiUrl: string}>} Details of the API, or null
*/
async function getApiDetails(apiName, apiRegion) {
// Check if API with that name exists
// Follows Hyperform conv: same name implies identical, for lambdas, and api endpoints etc
const apigatewayv2 = new AWS.ApiGatewayV2({
apiVersion: '2018-11-29',
region: apiRegion,
})
const getApisParams = {
MaxResults: '9999',
}
const res = await apigatewayv2.getApis(getApisParams).promise()
const matchingApis = res.Items.filter((item) => item.Name === apiName)
if (matchingApis.length === 0) {
// none exist
return null
}
if (matchingApis.length >= 2) {
log(`Multiple (${matchingApis.length}) APIs found with same name ${apiName}. Using first one`)
}
// just take first one
// Hyperform convention is there's only one with any given name
const apiDetails = {
apiId: matchingApis[0].ApiId,
apiUrl: matchingApis[0].ApiEndpoint,
}
return apiDetails
}
/**
*
* @param {*} apiId
* @param {*} apiRegion
* @throws If $default stage already exists
*/
async function createDefaultAutodeployStage(apiId, apiRegion) {
const apigatewayv2 = new AWS.ApiGatewayV2({
apiVersion: '2018-11-29',
region: apiRegion,
})
const params = {
ApiId: apiId,
StageName: '$default',
AutoDeploy: true,
}
const res = await apigatewayv2.createStage(params).promise()
}
/**
* @description Add permssion to allow API gateway to invoke given Lambda
* @param {string} lambdaName Name of Lambda
* @param {string} region Region of Lambda
* @returns {Promise<void>}
*/
async function allowApiGatewayToInvokeLambda(lambdaName, region) {
const lambda = new AWS.Lambda({
region: region,
apiVersion: '2015-03-31',
})
const addPermissionParams = {
Action: 'lambda:InvokeFunction',
FunctionName: lambdaName,
Principal: 'apigateway.amazonaws.com',
// TODO SourceArn https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda.html#addPermission-property
StatementId: `hf-stmnt-${lambdaName}`,
}
try {
await lambda.addPermission(addPermissionParams).promise()
} catch (e) {
if (e.code === 'ResourceConflictException') {
// API Gateway can already access that lambda (happens on all subsequent deploys), cool
} else {
logdev(`addpermission: some other error: ${e}`)
throw e
}
}
}
module.exports = {
createApi,
_only_for_testing_deleteApi: deleteApi,
allowApiGatewayToInvokeLambda,
getApiDetails,
createIntegration,
setRoute,
createDefaultAutodeployStage,
}