FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
lambda-tools/src/lambda.ts at master · lifeomic/lambda-tools · GitHub
Uh oh!
There was an error while loading.
Please reload this page
.
lifeomic
/
lambda-tools
Public
Notifications
You must be signed in to change notification settings
Fork
7
Star
23
Code
Issues
1
Pull requests
21
Actions
Projects
Security and quality
0
Insights
Additional navigation options
Code
Issues
Pull requests
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
lambda-tools
/
src
/
lambda.ts
Copy path
More file actions
More file actions
Latest commit
History
History
History
375 lines (312 loc) · 11.3 KB
Breadcrumbs
lambda-tools
/
src
/
lambda.ts
Copy path
File metadata and controls
375 lines (312 loc) · 11.3 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
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
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
import
{
Alpha
}
from
'@lifeomic/alpha'
;
import
assert
from
'assert'
;
import
Docker
from
'dockerode'
;
import
{
v4
as
uuid
}
from
'uuid'
;
import
tmp
from
'tmp-promise'
;
import
fs
from
'fs-extra'
;
import
unzip
from
'unzipper'
;
import
isObjectLike
from
'lodash/isObjectLike'
;
import
{
promisify
}
from
'util'
;
import
{
Handler
}
from
"aws-lambda"
;
import
{
AxiosRequestConfig
}
from
"axios"
;
import
{
TestInterface
}
from
"ava"
;
import
flatten
from
'lodash/flatten'
;
import
{
executeContainerCommand
,
ensureImage
}
from
'./docker'
;
import
{
getLogger
}
from
'./utils/logging'
;
import
webpack
from
'./webpack'
;
const
logger
=
getLogger
(
'lambda'
)
;
const
LAMBDA_TOOLS_WORK_PREFIX
=
'.lambda-tools-work'
;
const
LAMBDA_IMAGE
=
'lambci/lambda:nodejs12.x'
;
export
interface
Environment
{
[
key
:
string
]
:
string
|
number
|
boolean
|
null
|
undefined
;
}
interface
FinalConfig
{
container
:
string
;
environment
?:
Environment
;
handler
:
string
;
image
?:
string
;
mountpoint
?:
string
;
zipfile
?:
string
;
mountpointParent
?:
string
;
network
?:
string
;
service
?:
string
;
}
export
type
LambdaConfigOptions
=
Partial
<
FinalConfig
>
;
// null or undefined value means 'delete this variable'. Docker deletes variables that only have the key, without '=value'
const
createEnvironmentVariables
=
(
environment
:
Environment
)
=>
Object
.
entries
(
environment
)
.
map
(
(
[
key
,
value
]
)
=>
value
===
null
||
value
===
undefined
?
key
:
`
${
key
}
=
${
value
}
`
)
;
const
convertEvent
=
(
event
?:
any
)
=>
{
if
(
isObjectLike
(
event
)
)
{
return
JSON
.
stringify
(
event
)
;
}
return
`
${
event
}
`
;
}
;
export
async
function
destroyLambdaExecutionEnvironment
(
environment
:
ExecutionEnvironment
)
{
if
(
!
environment
)
{
return
;
}
const
{
container
,
network
,
cleanupMountpoint
}
=
environment
;
if
(
cleanupMountpoint
)
{
await
cleanupMountpoint
(
)
;
}
if
(
container
)
{
await
container
.
stop
(
)
;
logger
.
debug
(
`Stopped container
${
container
.
id
}
`
)
;
}
if
(
network
)
{
await
network
.
remove
(
)
;
logger
.
debug
(
`Removed network
${
network
.
id
}
`
)
;
}
}
export
async
function
getEntrypoint
(
docker
:
Docker
,
imageName
:
string
)
:
Promise
<
string
[
]
>
{
const
image
=
await
docker
.
getImage
(
imageName
)
.
inspect
(
)
;
const
entryPoint
=
image
.
ContainerConfig
?.
Entrypoint
??
image
.
Config
.
Entrypoint
;
if
(
entryPoint
)
{
return
flatten
(
[
entryPoint
]
)
;
}
else
{
const
parentImageName
=
image
.
Parent
;
assert
(
parentImageName
,
`The image
${
imageName
}
has no entrypoint and no parent image`
)
;
return
getEntrypoint
(
docker
,
parentImageName
)
;
}
}
export
class
LambdaRunner
{
private
environment
:
string
[
]
;
private
docker
:
Docker
;
constructor
(
private
container
:
string
,
environment
:
Environment
|
undefined
,
private
handler
:
string
)
{
this
.
docker
=
new
Docker
(
)
;
this
.
environment
=
environment
?
createEnvironmentVariables
(
environment
)
:
[
]
;
this
.
environment
.
push
(
'DOCKER_LAMBDA_USE_STDIN=1'
)
;
}
async
invoke
(
event
?:
any
)
{
const
command
=
await
this
.
buildCommand
(
)
;
const
container
=
await
this
.
getContainer
(
)
;
const
environment
=
this
.
environment
;
const
{
stderr
,
stdout
}
=
event
===
undefined
?
{
stdout
:
'Skipping execution of an undefined event. In the past this would have been an Unexpected token error\n{}'
,
stderr
:
''
}
:
await
executeContainerCommand
(
{
container
,
command
,
environment
,
stdin
:
convertEvent
(
event
)
}
)
;
const
output
=
stdout
.
toString
(
'utf8'
)
.
trim
(
)
;
const
split
=
output
.
lastIndexOf
(
'\n'
)
;
const
result
=
output
.
substring
(
split
+
1
)
;
logger
.
debug
(
'container output was:\n'
,
output
)
;
logger
.
debug
(
'container error was:\n'
,
stderr
.
toString
(
'utf8'
)
.
trim
(
)
)
;
// istanbul ignore next
return
JSON
.
parse
(
result
||
'{}'
)
;
}
private
async
buildCommand
(
)
:
Promise
<
string
[
]
>
{
const
container
=
await
this
.
getContainer
(
)
;
const
description
=
await
container
.
inspect
(
)
;
const
entrypoint
=
await
getEntrypoint
(
this
.
docker
,
description
.
Image
)
;
return
entrypoint
.
slice
(
)
.
concat
(
this
.
handler
)
;
}
private
async
getContainer
(
)
{
return
this
.
docker
.
getContainer
(
this
.
container
)
;
}
}
export
interface
AlphaClientConfig
{
container
:
string
;
environment
?:
Environment
;
handler
:
string
;
}
export
class
AlphaClient
extends
Alpha
{
public
raw
:
Handler
;
constructor
(
{
container
,
environment
,
handler
}
:
AlphaClientConfig
)
{
const
runner
=
new
LambdaRunner
(
container
,
environment
,
handler
)
;
const
fn
:
Handler
=
async
function
handler
(
event
,
context
,
callback
)
{
try
{
callback
(
null
,
await
runner
.
invoke
(
event
)
)
;
}
catch
(
error
)
{
callback
(
error
)
;
}
}
;
super
(
fn
as
any
)
;
this
.
raw
=
promisify
(
fn
)
;
}
graphql
<
T
=
any
>
(
path
:
string
,
query
:
any
,
variables
:
any
,
config
?:
AxiosRequestConfig
)
{
return
this
.
post
<
T
>
(
path
,
{
query
,
variables
}
,
config
)
;
}
}
const
globalOptions
:
LambdaConfigOptions
=
{
}
;
export
const
getGlobalOptions
=
(
)
=>
Object
.
assign
(
{
}
,
globalOptions
)
;
export
const
build
=
webpack
;
async
function
buildMountpointFromZipfile
(
zipfile
:
string
,
mountpointParent
?:
string
)
{
// It would be simpler if the standard TMPDIR directory could be used
// to extract the zip files, but Docker on Mac is often not configured with
// access to the Mac's /var temp directory location
const
baseDir
=
mountpointParent
||
process
.
cwd
(
)
;
const
tempDir
=
await
tmp
.
dir
(
{
dir
:
baseDir
,
mode
:
0o755
,
prefix
:
LAMBDA_TOOLS_WORK_PREFIX
}
)
;
const
tempDirName
=
tempDir
.
path
;
const
cleanup
=
async
(
)
=>
{
// Delete unzipped files
await
fs
.
emptyDir
(
tempDirName
)
;
await
tempDir
.
cleanup
(
)
;
}
;
try
{
// eslint-disable-next-line security/detect-non-literal-fs-filename
const
fsStream
=
fs
.
createReadStream
(
zipfile
)
;
const
unzipper
=
fsStream
.
pipe
(
unzip
.
Extract
(
{
path
:
tempDirName
}
)
)
;
await
new
Promise
(
(
resolve
,
reject
)
=>
{
const
endOnError
=
(
error
:
Error
)
=>
reject
(
error
)
;
unzipper
.
on
(
'close'
,
(
)
=>
resolve
(
undefined
)
)
;
fsStream
.
on
(
'error'
,
endOnError
)
;
unzipper
.
on
(
'error'
,
endOnError
)
;
}
)
;
return
{
mountpoint
:
tempDirName
,
cleanup
}
;
}
catch
(
e
)
{
await
cleanup
(
)
;
throw
e
;
}
}
export
const
useNewContainer
=
(
{
environment
,
mountpoint
,
zipfile
,
mountpointParent
,
handler
,
image
,
useComposeNetwork
}
:
Pick
<
LambdaConfigOptions
,
'environment'
|
'mountpoint'
|
'zipfile'
|
'mountpointParent'
|
'handler'
|
'image'
>
&
{
useComposeNetwork
?:
boolean
}
)
=>
{
const
network
=
useComposeNetwork
?
`
${
process
.
env
.
COMPOSE_PROJECT_NAME
}
_default`
:
undefined
;
Object
.
assign
(
globalOptions
,
{
environment
,
handler
,
image
,
mountpoint
,
zipfile
,
mountpointParent
,
network
}
)
;
}
;
export
const
useComposeContainer
=
(
{
environment
,
service
,
handler
}
:
Pick
<
LambdaConfigOptions
,
'environment'
|
'handler'
>
&
{
service
:
string
}
)
=>
{
const
container
=
`
${
process
.
env
.
COMPOSE_PROJECT_NAME
}
_
${
service
}
_1`
;
Object
.
assign
(
globalOptions
,
{
container
,
environment
,
handler
}
)
;
}
;
interface
ExecutionEnvironment
{
cleanupMountpoint
?:
(
)
=>
Promise
<
any
>
;
network
?:
Docker
.
Network
;
container
?:
Docker
.
Container
;
}
export
async
function
createLambdaExecutionEnvironment
(
options
:
FinalConfig
)
:
Promise
<
ExecutionEnvironment
>
{
const
{
environment
=
{
}
,
image
=
LAMBDA_IMAGE
,
zipfile
,
network
:
networkId
,
mountpointParent
}
=
options
;
let
{
mountpoint
}
=
options
;
if
(
mountpoint
&&
zipfile
)
{
throw
new
Error
(
'Only one of mountpoint or zipfile can be provided'
)
;
}
const
executionEnvironment
:
ExecutionEnvironment
=
{
}
;
if
(
zipfile
)
{
const
zipMount
=
await
buildMountpointFromZipfile
(
zipfile
,
mountpointParent
)
;
mountpoint
=
zipMount
.
mountpoint
;
executionEnvironment
.
cleanupMountpoint
=
zipMount
.
cleanup
;
}
assert
(
!
(
mountpoint
&&
options
.
service
)
,
'A mountpoint cannot be used with a compose service'
)
;
if
(
mountpoint
)
{
const
docker
=
new
Docker
(
)
;
try
{
await
ensureImage
(
docker
,
image
)
;
}
catch
(
error
)
{
logger
.
error
(
'Unable to get image'
,
JSON
.
stringify
(
{
error
}
,
null
,
2
)
)
;
await
destroyLambdaExecutionEnvironment
(
executionEnvironment
)
;
throw
error
;
}
try
{
if
(
!
networkId
)
{
executionEnvironment
.
network
=
await
docker
.
createNetwork
(
{
Internal
:
true
,
Name
:
uuid
(
)
}
)
;
logger
.
debug
(
`Created network
${
executionEnvironment
.
network
!
.
id
}
`
)
}
}
catch
(
error
)
{
logger
.
error
(
'Unable to create network'
,
JSON
.
stringify
(
{
error
}
,
null
,
2
)
)
;
await
destroyLambdaExecutionEnvironment
(
executionEnvironment
)
;
throw
error
;
}
try
{
executionEnvironment
.
container
=
await
docker
.
createContainer
(
{
Entrypoint
:
'sh'
,
Env
:
createEnvironmentVariables
(
environment
)
,
HostConfig
:
{
AutoRemove
:
true
,
Binds
:
[
`
${
mountpoint
}
:/var/task`
,
]
,
NetworkMode
:
networkId
||
executionEnvironment
.
network
!
.
id
,
}
,
Image
:
image
,
OpenStdin
:
true
,
Volumes
:
{
'/var/task'
:
{
}
,
}
,
}
)
;
logger
.
debug
(
`Created container
${
executionEnvironment
.
container
.
id
}
`
)
}
catch
(
error
)
{
logger
.
error
(
'Unable to create container'
,
JSON
.
stringify
(
{
error
}
,
null
,
2
)
)
;
await
destroyLambdaExecutionEnvironment
(
executionEnvironment
)
;
throw
error
;
}
try
{
await
executionEnvironment
.
container
.
start
(
)
;
}
catch
(
error
)
{
logger
.
error
(
'Unable to start container'
,
JSON
.
stringify
(
{
error
,
container
:
executionEnvironment
.
container
.
id
}
,
null
,
2
)
)
;
await
destroyLambdaExecutionEnvironment
(
executionEnvironment
)
;
throw
error
;
}
}
return
executionEnvironment
;
}
export
interface
LambdaHooks
{
beforeAll
(
)
:
Promise
<
void
>
;
beforeEach
(
)
:
Promise
<
AlphaClient
>
;
afterAll
(
)
:
Promise
<
void
>
;
}
export
function
useLambdaHooks
(
localOptions
:
LambdaConfigOptions
)
:
LambdaHooks
{
const
impliedOptions
:
Partial
<
FinalConfig
>
=
{
}
;
let
executionEnvironment
:
ExecutionEnvironment
=
{
}
;
const
getOptions
=
(
)
:
FinalConfig
=>
Object
.
assign
<
{
}
,
LambdaConfigOptions
,
LambdaConfigOptions
,
LambdaConfigOptions
>
(
{
}
,
globalOptions
,
impliedOptions
,
localOptions
)
as
FinalConfig
;
async
function
beforeAll
(
)
{
executionEnvironment
=
await
createLambdaExecutionEnvironment
(
getOptions
(
)
)
;
if
(
executionEnvironment
.
container
)
{
impliedOptions
.
container
=
executionEnvironment
.
container
.
id
;
}
}
async
function
afterAll
(
)
{
await
destroyLambdaExecutionEnvironment
(
executionEnvironment
)
;
}
async
function
beforeEach
(
)
{
const
{
container
,
environment
,
handler
}
=
getOptions
(
)
;
return
new
AlphaClient
(
{
container
,
environment
,
handler
}
)
;
}
return
{
beforeAll
,
beforeEach
,
afterAll
}
;
}
export
interface
LambdaTestContext
{
lambda
:
AlphaClient
;
}
export
const
useLambda
=
(
anyTest
:
TestInterface
,
localOptions
:
LambdaConfigOptions
=
{
}
)
=>
{
// The base ava test doesn't have context, and has to be cast.
// This allows clients to send in the default ava export, and they can cast later or before.
const
test
=
anyTest
as
TestInterface
<
LambdaTestContext
>
;
const
hooks
=
useLambdaHooks
(
localOptions
)
;
test
.
serial
.
before
(
async
(
)
=>
{
await
hooks
.
beforeAll
(
)
;
}
)
;
test
.
serial
.
after
.
always
(
async
(
)
=>
{
await
hooks
.
afterAll
(
)
;
}
)
;
test
.
serial
.
beforeEach
(
async
(
test
)
=>
{
test
.
context
.
lambda
=
await
hooks
.
beforeEach
(
)
;
}
)
;
}
;
Back
|
FazBrowse Home
|
New Git URL