serverless-offline 9.1.0 → 9.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -34
- package/package.json +4 -4
- package/src/ServerlessOffline.js +2 -6
- package/src/config/constants.js +1 -1
- package/src/config/defaultOptions.js +1 -1
- package/src/events/http/Http.js +4 -4
- package/src/events/http/HttpServer.js +181 -159
- package/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js +2 -12
- package/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +2 -11
- package/src/events/schedule/Schedule.js +3 -3
- package/src/lambda/LambdaFunction.js +1 -1
- package/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +10 -6
- package/src/lambda/handler-runner/docker-runner/DockerContainer.js +3 -1
- package/src/lambda/handler-runner/go-runner/GoRunner.js +19 -14
- package/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +12 -6
- package/src/lambda/handler-runner/java-runner/JavaRunner.js +5 -3
- package/src/lambda/handler-runner/python-runner/PythonRunner.js +5 -4
- package/src/lambda/handler-runner/ruby-runner/RubyRunner.js +5 -3
- package/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +2 -2
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<a href="https://www.npmjs.com/package/serverless-offline">
|
|
8
8
|
<img src="https://img.shields.io/npm/v/serverless-offline.svg?style=flat-square">
|
|
9
9
|
</a>
|
|
10
|
-
<a href="https://github.com/dherault/serverless-offline/actions
|
|
11
|
-
<img src="https://img.shields.io/github/workflow/status/dherault/serverless-offline/
|
|
10
|
+
<a href="https://github.com/dherault/serverless-offline/actions/workflows/integrate.yml">
|
|
11
|
+
<img src="https://img.shields.io/github/workflow/status/dherault/serverless-offline/Integrate">
|
|
12
12
|
</a>
|
|
13
13
|
<img src="https://img.shields.io/node/v/serverless-offline.svg?style=flat-square">
|
|
14
14
|
<a href="https://github.com/serverless/serverless">
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
This [Serverless](https://github.com/serverless/serverless) plugin emulates [AWS λ](https://aws.amazon.com/lambda) and [API Gateway](https://aws.amazon.com/api-gateway) on your local machine to speed up your development cycles.
|
|
30
30
|
To do so, it starts an HTTP server that handles the request's lifecycle like APIG does and invokes your handlers.
|
|
31
31
|
|
|
32
|
-
**Features
|
|
32
|
+
**Features**
|
|
33
33
|
|
|
34
|
-
- [Node.js](https://nodejs.org), [Python](https://www.python.org), [Ruby](https://www.ruby-lang.org)
|
|
34
|
+
- [Node.js](https://nodejs.org), [Python](https://www.python.org), [Ruby](https://www.ruby-lang.org), [Go](https://golang.org), [Java](https://www.java.com) (incl. [Kotlin](https://kotlinlang.org), [Groovy](https://groovy-lang.org), [Scala](https://www.scala-lang.org)) λ runtimes.
|
|
35
35
|
- Velocity templates support.
|
|
36
36
|
- Lazy loading of your handler files.
|
|
37
37
|
- And more: integrations, authorizers, proxies, timeouts, responseParameters, HTTPS, CORS, etc...
|
|
@@ -42,6 +42,7 @@ This plugin is updated by its users, I just do maintenance and ensure that PRs a
|
|
|
42
42
|
|
|
43
43
|
- [Installation](#installation)
|
|
44
44
|
- [Usage and command line options](#usage-and-command-line-options)
|
|
45
|
+
- [Run modes](#run-modes)
|
|
45
46
|
- [Usage with `invoke`](#usage-with-invoke)
|
|
46
47
|
- [The `process.env.IS_OFFLINE` variable](#the-processenvis_offline-variable)
|
|
47
48
|
- [Docker and Layers](#docker-and-layers)
|
|
@@ -84,7 +85,7 @@ Then inside your project's `serverless.yml` file add following entry to the plug
|
|
|
84
85
|
|
|
85
86
|
It should look something like this:
|
|
86
87
|
|
|
87
|
-
```
|
|
88
|
+
```yml
|
|
88
89
|
plugins:
|
|
89
90
|
- serverless-offline
|
|
90
91
|
```
|
|
@@ -146,13 +147,12 @@ All CLI options are optional:
|
|
|
146
147
|
|
|
147
148
|
Any of the CLI options can be added to your `serverless.yml`. For example:
|
|
148
149
|
|
|
149
|
-
```
|
|
150
|
+
```yml
|
|
150
151
|
custom:
|
|
151
152
|
serverless-offline:
|
|
152
|
-
httpsProtocol:
|
|
153
|
+
httpsProtocol: 'dev-certs'
|
|
153
154
|
httpPort: 4000
|
|
154
|
-
|
|
155
|
-
foo: "bar"
|
|
155
|
+
foo: 'bar'
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
Options passed on the command line override YAML options.
|
|
@@ -164,18 +164,64 @@ By default you can send your requests to `http://localhost:3000/`. Please note t
|
|
|
164
164
|
But if you send an `application/x-www-form-urlencoded` or a `multipart/form-data` body with an `application/json` (or no) Content-Type, API Gateway won't parse your data (you'll get the ugly raw as input), whereas the plugin will answer 400 (malformed JSON).
|
|
165
165
|
Please consider explicitly setting your requests' Content-Type and using separate templates.
|
|
166
166
|
|
|
167
|
+
## Run modes
|
|
168
|
+
|
|
169
|
+
### node.js
|
|
170
|
+
|
|
171
|
+
Lambda handlers for the `node.js` runtime can run in different execution modes with `serverless-offline` and they have subtle differences with a variety of pros and cons. they are mutually exclusive and it is planned to combine the flags into one single flag in the future.
|
|
172
|
+
|
|
173
|
+
#### worker-threads (default)
|
|
174
|
+
|
|
175
|
+
- handlers run in their own context
|
|
176
|
+
- memory is not being shared between handlers, memory consumption is therefore higher
|
|
177
|
+
- memory is being released when handlers reload or after usage
|
|
178
|
+
- environment (process.env) is not being shared across handlers
|
|
179
|
+
- global state is not being shared across handlers
|
|
180
|
+
- easy debugging
|
|
181
|
+
|
|
182
|
+
#### in-process
|
|
183
|
+
|
|
184
|
+
- handlers run in the same context (instance) as `serverless` and `serverless-offline`
|
|
185
|
+
- memory is being shared across lambda handlers as well as with `serverless` and `serverless-offline`
|
|
186
|
+
- no reloading capabilities as it is [currently] not possible to implement for commonjs handlers (without memory leaks) and for esm handlers
|
|
187
|
+
- environment (process.env) is being shared across handlers as well as with `serverless` and `serverless-offline`
|
|
188
|
+
- global state is being shared across lambda handlers as well as with `serverless` and `serverless-offline`
|
|
189
|
+
- easy debugging
|
|
190
|
+
|
|
191
|
+
#### child-processes
|
|
192
|
+
|
|
193
|
+
- handlers run in a separate node.js instance
|
|
194
|
+
- memory is not being shared between handlers, memory consumption is therefore higher
|
|
195
|
+
- memory is being released when handlers reload or after usage
|
|
196
|
+
- environment (process.env) is not being shared across handlers
|
|
197
|
+
- global state is not being shared across handlers
|
|
198
|
+
- debugging more complicated
|
|
199
|
+
|
|
200
|
+
#### docker
|
|
201
|
+
|
|
202
|
+
- handlers run in a docker container
|
|
203
|
+
- memory is not being shared between handlers, memory consumption is therefore higher
|
|
204
|
+
- memory is being released when handlers reload or after usage
|
|
205
|
+
- environment (process.env) is not being shared across handlers
|
|
206
|
+
- global state is not being shared across handlers
|
|
207
|
+
- debugging more complicated
|
|
208
|
+
|
|
209
|
+
### Python, Ruby, Go, Java (incl. Kotlin, Groovy, Scala)
|
|
210
|
+
|
|
211
|
+
the Lambda handler process is running in a child process.
|
|
212
|
+
|
|
167
213
|
## Usage with `invoke`
|
|
168
214
|
|
|
169
|
-
To use `Lambda.invoke` you need to set the lambda endpoint to the serverless-offline endpoint:
|
|
215
|
+
To use `Lambda.invoke` you need to set the lambda endpoint to the `serverless-offline` endpoint:
|
|
170
216
|
|
|
171
217
|
```js
|
|
218
|
+
const { env } = require('node:process')
|
|
172
219
|
const { Lambda } = require('aws-sdk')
|
|
173
220
|
|
|
174
221
|
const lambda = new Lambda({
|
|
175
222
|
apiVersion: '2015-03-31',
|
|
176
|
-
// endpoint needs to be set only if it deviates from the default
|
|
177
|
-
|
|
178
|
-
endpoint: process.env.SOME_VARIABLE
|
|
223
|
+
// endpoint needs to be set only if it deviates from the default
|
|
224
|
+
endpoint: env.IS_OFFLINE
|
|
179
225
|
? 'http://localhost:3002'
|
|
180
226
|
: 'https://lambda.us-east-1.amazonaws.com',
|
|
181
227
|
})
|
|
@@ -184,15 +230,33 @@ const lambda = new Lambda({
|
|
|
184
230
|
All your lambdas can then be invoked in a handler using
|
|
185
231
|
|
|
186
232
|
```js
|
|
233
|
+
const { Buffer } = require('node:buffer')
|
|
234
|
+
const { Lambda } = require('aws-sdk')
|
|
235
|
+
|
|
236
|
+
const { stringify } = JSON
|
|
237
|
+
|
|
238
|
+
const lambda = new Lambda({
|
|
239
|
+
apiVersion: '2015-03-31',
|
|
240
|
+
endpoint: 'http://localhost:3002',
|
|
241
|
+
})
|
|
242
|
+
|
|
187
243
|
exports.handler = async function () {
|
|
244
|
+
const clientContextData = stringify({ foo: 'foo' })
|
|
245
|
+
|
|
188
246
|
const params = {
|
|
247
|
+
ClientContext: Buffer.from(clientContextData).toString('base64'),
|
|
189
248
|
// FunctionName is composed of: service name - stage - function name, e.g.
|
|
190
249
|
FunctionName: 'myServiceName-dev-invokedHandler',
|
|
191
250
|
InvocationType: 'RequestResponse',
|
|
192
|
-
Payload:
|
|
251
|
+
Payload: stringify({ data: 'foo' }),
|
|
193
252
|
}
|
|
194
253
|
|
|
195
254
|
const response = await lambda.invoke(params).promise()
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
body: stringify(response),
|
|
258
|
+
statusCode: 200,
|
|
259
|
+
}
|
|
196
260
|
}
|
|
197
261
|
```
|
|
198
262
|
|
|
@@ -244,7 +308,7 @@ to calling it via `aws-sdk`.
|
|
|
244
308
|
|
|
245
309
|
## The `process.env.IS_OFFLINE` variable
|
|
246
310
|
|
|
247
|
-
Will be `"true"` in your handlers
|
|
311
|
+
Will be `"true"` in your handlers when using `serverless-offline`.
|
|
248
312
|
|
|
249
313
|
## Docker and Layers
|
|
250
314
|
|
|
@@ -326,11 +390,11 @@ Only [custom authorizers](https://aws.amazon.com/blogs/compute/introducing-custo
|
|
|
326
390
|
|
|
327
391
|
The Custom authorizer is passed an `event` object as below:
|
|
328
392
|
|
|
329
|
-
```
|
|
393
|
+
```js
|
|
330
394
|
{
|
|
331
|
-
"type": "TOKEN",
|
|
332
395
|
"authorizationToken": "<Incoming bearer token>",
|
|
333
|
-
"methodArn": "arn:aws:execute-api:<Region id>:<Account id>:<API id>/<Stage>/<Method>/<Resource path>"
|
|
396
|
+
"methodArn": "arn:aws:execute-api:<Region id>:<Account id>:<API id>/<Stage>/<Method>/<Resource path>",
|
|
397
|
+
"type": "TOKEN"
|
|
334
398
|
}
|
|
335
399
|
```
|
|
336
400
|
|
|
@@ -338,11 +402,11 @@ The `methodArn` does not include the Account id or API id.
|
|
|
338
402
|
|
|
339
403
|
The plugin only supports retrieving Tokens from headers. You can configure the header as below:
|
|
340
404
|
|
|
341
|
-
```
|
|
405
|
+
```js
|
|
342
406
|
"authorizer": {
|
|
343
|
-
"
|
|
407
|
+
"authorizerResultTtlInSeconds": "0",
|
|
344
408
|
"identitySource": "method.request.header.Authorization", // or method.request.header.SomeOtherHeader
|
|
345
|
-
"
|
|
409
|
+
"type": "TOKEN"
|
|
346
410
|
}
|
|
347
411
|
```
|
|
348
412
|
|
|
@@ -370,14 +434,14 @@ If your authentication needs are custom and not satisfied by the existing capabi
|
|
|
370
434
|
```js
|
|
371
435
|
module.exports = function (endpoint, functionKey, method, path) {
|
|
372
436
|
return {
|
|
373
|
-
name: 'your strategy name',
|
|
374
|
-
scheme: 'your scheme name',
|
|
375
|
-
|
|
376
437
|
getAuthenticateFunction: () => ({
|
|
377
438
|
async authenticate(request, h) {
|
|
378
439
|
// your implementation
|
|
379
440
|
},
|
|
380
441
|
}),
|
|
442
|
+
|
|
443
|
+
name: 'your strategy name',
|
|
444
|
+
scheme: 'your scheme name',
|
|
381
445
|
}
|
|
382
446
|
}
|
|
383
447
|
```
|
|
@@ -441,7 +505,7 @@ Now let's make a request with this body: `{ "id": 1 }`
|
|
|
441
505
|
|
|
442
506
|
AWS parses the event as such:
|
|
443
507
|
|
|
444
|
-
```
|
|
508
|
+
```js
|
|
445
509
|
{
|
|
446
510
|
"payload": {
|
|
447
511
|
"id": 1
|
|
@@ -453,7 +517,7 @@ AWS parses the event as such:
|
|
|
453
517
|
|
|
454
518
|
Whereas Offline parses:
|
|
455
519
|
|
|
456
|
-
```
|
|
520
|
+
```js
|
|
457
521
|
{
|
|
458
522
|
"payload": {
|
|
459
523
|
"id": 1
|
|
@@ -505,7 +569,7 @@ Works out of the box. See examples in the manual_test directory.
|
|
|
505
569
|
|
|
506
570
|
Example of enabling proxy:
|
|
507
571
|
|
|
508
|
-
```
|
|
572
|
+
```yml
|
|
509
573
|
custom:
|
|
510
574
|
serverless-offline:
|
|
511
575
|
resourceRoutes: true
|
|
@@ -513,18 +577,18 @@ custom:
|
|
|
513
577
|
|
|
514
578
|
or
|
|
515
579
|
|
|
516
|
-
```
|
|
580
|
+
```yml
|
|
517
581
|
YourCloudFormationMethodId:
|
|
518
|
-
Type: AWS::ApiGateway::Method
|
|
519
582
|
Properties:
|
|
520
583
|
......
|
|
521
584
|
Integration:
|
|
522
585
|
Type: HTTP_PROXY
|
|
523
586
|
Uri: 'https://s3-${self:custom.region}.amazonaws.com/${self:custom.yourBucketName}/{proxy}'
|
|
524
587
|
......
|
|
588
|
+
Type: AWS::ApiGateway::Method
|
|
525
589
|
```
|
|
526
590
|
|
|
527
|
-
```
|
|
591
|
+
```yml
|
|
528
592
|
custom:
|
|
529
593
|
serverless-offline:
|
|
530
594
|
resourceRoutes:
|
|
@@ -542,7 +606,7 @@ May not work properly. Please PR. (Difficulty: hard?)
|
|
|
542
606
|
|
|
543
607
|
Example response velocity template:
|
|
544
608
|
|
|
545
|
-
```
|
|
609
|
+
```js
|
|
546
610
|
"responseParameters": {
|
|
547
611
|
"method.response.header.X-Powered-By": "Serverless", // a string
|
|
548
612
|
"method.response.header.Warning": "integration.response.body", // the whole response
|
|
@@ -559,7 +623,9 @@ Usage in order to send messages back to clients:
|
|
|
559
623
|
Or,
|
|
560
624
|
|
|
561
625
|
```js
|
|
562
|
-
const
|
|
626
|
+
const { ApiGatewayManagementApi } = require('aws-sdk')
|
|
627
|
+
|
|
628
|
+
const apiGatewayManagementApi = new ApiGatewayManagementApi({
|
|
563
629
|
apiVersion: '2018-11-29',
|
|
564
630
|
endpoint: 'http://localhost:3001',
|
|
565
631
|
});
|
|
@@ -648,7 +714,7 @@ You can change this profile directly in the code or by setting proper environmen
|
|
|
648
714
|
## Simulation quality
|
|
649
715
|
|
|
650
716
|
This plugin simulates API Gateway for many practical purposes, good enough for development - but is not a perfect simulator.
|
|
651
|
-
Specifically, Lambda currently runs on Node.js
|
|
717
|
+
Specifically, Lambda currently runs on Node.js v12.x, v14.x and v16.x ([AWS Docs](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html)), whereas _Offline_ runs on your own runtime where no memory limits are enforced.
|
|
652
718
|
|
|
653
719
|
## Usage with other plugins
|
|
654
720
|
|
|
@@ -661,7 +727,7 @@ Plugins are executed in order, so plugins that process your code or add resource
|
|
|
661
727
|
|
|
662
728
|
For example:
|
|
663
729
|
|
|
664
|
-
```
|
|
730
|
+
```yml
|
|
665
731
|
plugins:
|
|
666
732
|
- serverless-middleware # modifies some of your handler based on configuration
|
|
667
733
|
- serverless-webpack # package your javascript handlers using webpack
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dedicatedTo": "Blue, a great migrating bird.",
|
|
3
3
|
"name": "serverless-offline",
|
|
4
|
-
"version": "9.1.
|
|
4
|
+
"version": "9.1.3",
|
|
5
5
|
"description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./src/index.js",
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
"@hapi/boom": "^10.0.0",
|
|
195
195
|
"@hapi/h2o2": "^9.1.0",
|
|
196
196
|
"@hapi/hapi": "^20.2.2",
|
|
197
|
-
"aws-sdk": "^2.
|
|
197
|
+
"aws-sdk": "^2.1186.0",
|
|
198
198
|
"boxen": "^7.0.0",
|
|
199
199
|
"chalk": "^5.0.1",
|
|
200
200
|
"execa": "^6.1.0",
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
"jsonwebtoken": "^8.5.1",
|
|
207
207
|
"jszip": "^3.10.0",
|
|
208
208
|
"luxon": "^3.0.1",
|
|
209
|
-
"node-fetch": "^3.2.
|
|
209
|
+
"node-fetch": "^3.2.10",
|
|
210
210
|
"node-schedule": "^2.1.0",
|
|
211
211
|
"object.hasown": "^1.1.1",
|
|
212
212
|
"p-memoize": "^7.1.0",
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
},
|
|
217
217
|
"devDependencies": {
|
|
218
218
|
"archiver": "^5.3.1",
|
|
219
|
-
"eslint": "^8.
|
|
219
|
+
"eslint": "^8.21.0",
|
|
220
220
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
221
221
|
"eslint-config-prettier": "^8.5.0",
|
|
222
222
|
"eslint-plugin-import": "^2.25.4",
|
package/src/ServerlessOffline.js
CHANGED
|
@@ -168,7 +168,7 @@ export default class ServerlessOffline {
|
|
|
168
168
|
|
|
169
169
|
this.#http = new Http(this.#serverless, this.#options, this.#lambda)
|
|
170
170
|
|
|
171
|
-
await this.#http.
|
|
171
|
+
await this.#http.createServer()
|
|
172
172
|
|
|
173
173
|
this.#http.create(events)
|
|
174
174
|
|
|
@@ -236,12 +236,8 @@ export default class ServerlessOffline {
|
|
|
236
236
|
.replace(/\s/g, '')
|
|
237
237
|
.split(',')
|
|
238
238
|
|
|
239
|
-
if (this.#options.corsDisallowCredentials) {
|
|
240
|
-
this.#options.corsAllowCredentials = false
|
|
241
|
-
}
|
|
242
|
-
|
|
243
239
|
this.#options.corsConfig = {
|
|
244
|
-
credentials: this.#options.
|
|
240
|
+
credentials: !this.#options.corsDisallowCredentials,
|
|
245
241
|
exposedHeaders: this.#options.corsExposedHeaders,
|
|
246
242
|
headers: this.#options.corsAllowHeaders,
|
|
247
243
|
origin: this.#options.corsAllowOrigin,
|
package/src/config/constants.js
CHANGED
|
@@ -9,7 +9,7 @@ export const DEFAULT_LAMBDA_RUNTIME = 'nodejs14.x'
|
|
|
9
9
|
// https://docs.aws.amazon.com/lambda/latest/dg/limits.html
|
|
10
10
|
export const DEFAULT_LAMBDA_MEMORY_SIZE = 1024
|
|
11
11
|
// default function timeout in seconds
|
|
12
|
-
export const DEFAULT_LAMBDA_TIMEOUT =
|
|
12
|
+
export const DEFAULT_LAMBDA_TIMEOUT = 6 // 6 seconds
|
|
13
13
|
|
|
14
14
|
// timeout for all connections to be closed
|
|
15
15
|
export const SERVER_SHUTDOWN_TIMEOUT = 5000
|
|
@@ -2,9 +2,9 @@ import { createApiKey } from '../utils/index.js'
|
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
apiKey: createApiKey(),
|
|
5
|
-
corsAllowCredentials: true, // TODO no CLI option
|
|
6
5
|
corsAllowHeaders: 'accept,content-type,x-api-key,authorization',
|
|
7
6
|
corsAllowOrigin: '*',
|
|
7
|
+
corsDisallowCredentials: true,
|
|
8
8
|
corsExposedHeaders: 'WWW-Authenticate,Server-Authorization',
|
|
9
9
|
disableCookieValidation: false,
|
|
10
10
|
disableScheduledEvents: false,
|
package/src/events/http/Http.js
CHANGED
|
@@ -17,6 +17,10 @@ export default class Http {
|
|
|
17
17
|
return this.#httpServer.stop(timeout)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
async createServer() {
|
|
21
|
+
await this.#httpServer.createServer()
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
#createEvent(functionKey, rawHttpEventDefinition, handler) {
|
|
21
25
|
const httpEvent = new HttpEventDefinition(rawHttpEventDefinition)
|
|
22
26
|
|
|
@@ -39,10 +43,6 @@ export default class Http {
|
|
|
39
43
|
this.#httpServer.create404Route()
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
registerPlugins() {
|
|
43
|
-
return this.#httpServer.registerPlugins()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
46
|
// TEMP FIXME quick fix to expose gateway server for testing, look for better solution
|
|
47
47
|
getServer() {
|
|
48
48
|
return this.#httpServer.getServer()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
4
|
import { join, resolve } from 'node:path'
|
|
5
5
|
import { exit } from 'node:process'
|
|
@@ -47,7 +47,9 @@ export default class HttpServer {
|
|
|
47
47
|
this.#lambda = lambda
|
|
48
48
|
this.#options = options
|
|
49
49
|
this.#serverless = serverless
|
|
50
|
+
}
|
|
50
51
|
|
|
52
|
+
async createServer() {
|
|
51
53
|
const {
|
|
52
54
|
enforceSecureCookies,
|
|
53
55
|
host,
|
|
@@ -80,14 +82,20 @@ export default class HttpServer {
|
|
|
80
82
|
// HTTPS support
|
|
81
83
|
if (typeof httpsProtocol === 'string' && httpsProtocol.length > 0) {
|
|
82
84
|
serverOptions.tls = {
|
|
83
|
-
cert:
|
|
84
|
-
key:
|
|
85
|
+
cert: readFile(resolve(httpsProtocol, 'cert.pem'), 'ascii'),
|
|
86
|
+
key: readFile(resolve(httpsProtocol, 'key.pem'), 'ascii'),
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
// Hapijs server creation
|
|
89
91
|
this.#server = new Server(serverOptions)
|
|
90
92
|
|
|
93
|
+
try {
|
|
94
|
+
await this.#server.register([h2o2])
|
|
95
|
+
} catch (err) {
|
|
96
|
+
log.error(err)
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
// Enable CORS preflight response
|
|
92
100
|
this.#server.ext('onPreResponse', (request, h) => {
|
|
93
101
|
if (request.headers.origin) {
|
|
@@ -205,14 +213,6 @@ export default class HttpServer {
|
|
|
205
213
|
})
|
|
206
214
|
}
|
|
207
215
|
|
|
208
|
-
async registerPlugins() {
|
|
209
|
-
try {
|
|
210
|
-
await this.#server.register([h2o2])
|
|
211
|
-
} catch (err) {
|
|
212
|
-
log.error(err)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
216
|
#logPluginIssue() {
|
|
217
217
|
log.notice(
|
|
218
218
|
'If you think this is an issue with the plugin please submit it, thanks!\nhttps://github.com/dherault/serverless-offline/issues',
|
|
@@ -260,7 +260,7 @@ export default class HttpServer {
|
|
|
260
260
|
log.debug(`Creating Authorization scheme for ${authKey}`)
|
|
261
261
|
|
|
262
262
|
// Create the Auth Scheme for the endpoint
|
|
263
|
-
const scheme = createJWTAuthScheme(jwtSettings
|
|
263
|
+
const scheme = createJWTAuthScheme(jwtSettings)
|
|
264
264
|
|
|
265
265
|
// Set the auth scheme and strategy on the server
|
|
266
266
|
this.#server.auth.scheme(authSchemeName, scheme)
|
|
@@ -338,6 +338,7 @@ export default class HttpServer {
|
|
|
338
338
|
* /tests/integration/custom-authentication
|
|
339
339
|
*/
|
|
340
340
|
const customizations = this.#serverless.service.custom
|
|
341
|
+
|
|
341
342
|
if (
|
|
342
343
|
customizations &&
|
|
343
344
|
customizations.offline?.customAuthenticationProvider
|
|
@@ -350,11 +351,13 @@ export default class HttpServer {
|
|
|
350
351
|
)
|
|
351
352
|
|
|
352
353
|
const strategy = provider(endpoint, functionKey, method, path)
|
|
354
|
+
|
|
353
355
|
this.#server.auth.scheme(
|
|
354
356
|
strategy.scheme,
|
|
355
357
|
strategy.getAuthenticateFunction,
|
|
356
358
|
)
|
|
357
359
|
this.#server.auth.strategy(strategy.name, strategy.scheme)
|
|
360
|
+
|
|
358
361
|
return strategy.name
|
|
359
362
|
}
|
|
360
363
|
|
|
@@ -363,164 +366,44 @@ export default class HttpServer {
|
|
|
363
366
|
? null
|
|
364
367
|
: this.#configureJWTAuthorization(endpoint, functionKey, method, path) ||
|
|
365
368
|
this.#configureAuthorization(endpoint, functionKey, method, path)
|
|
369
|
+
|
|
366
370
|
return authStrategyName
|
|
367
371
|
}
|
|
368
372
|
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
let method
|
|
373
|
-
let path
|
|
374
|
-
let hapiPath
|
|
375
|
-
|
|
376
|
-
if (httpEvent.isHttpApi) {
|
|
377
|
-
if (httpEvent.routeKey === '$default') {
|
|
378
|
-
method = 'ANY'
|
|
379
|
-
path = httpEvent.routeKey
|
|
380
|
-
hapiPath = '/{default*}'
|
|
381
|
-
} else {
|
|
382
|
-
;[method, path] = httpEvent.routeKey.split(' ')
|
|
383
|
-
hapiPath = generateHapiPath(
|
|
384
|
-
path,
|
|
385
|
-
{
|
|
386
|
-
...this.#options,
|
|
387
|
-
noPrependStageInUrl: true, // Serverless always uses the $default stage
|
|
388
|
-
},
|
|
389
|
-
this.#serverless,
|
|
390
|
-
)
|
|
391
|
-
}
|
|
392
|
-
} else {
|
|
393
|
-
method = httpEvent.method.toUpperCase()
|
|
394
|
-
;({ path } = httpEvent)
|
|
395
|
-
hapiPath = generateHapiPath(path, this.#options, this.#serverless)
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const endpoint = new Endpoint(
|
|
399
|
-
join(this.#serverless.config.servicePath, handlerPath),
|
|
400
|
-
httpEvent,
|
|
401
|
-
).generate()
|
|
402
|
-
|
|
403
|
-
const stage = endpoint.isHttpApi
|
|
404
|
-
? '$default'
|
|
405
|
-
: this.#options.stage || this.#serverless.service.provider.stage
|
|
406
|
-
const protectedRoutes = []
|
|
407
|
-
|
|
408
|
-
if (httpEvent.private) {
|
|
409
|
-
protectedRoutes.push(`${method}#${hapiPath}`)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const { host, httpPort, httpsProtocol } = this.#options
|
|
413
|
-
const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${httpPort}`
|
|
414
|
-
|
|
415
|
-
this.#terminalInfo.push({
|
|
416
|
-
invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
|
|
417
|
-
method,
|
|
418
|
-
path: hapiPath,
|
|
419
|
-
server,
|
|
420
|
-
stage:
|
|
421
|
-
endpoint.isHttpApi || this.#options.noPrependStageInUrl ? null : stage,
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
const authStrategyName = this.#setAuthorizationStrategy(
|
|
373
|
+
#createHapiHandler(params) {
|
|
374
|
+
const {
|
|
375
|
+
additionalRequestContext,
|
|
425
376
|
endpoint,
|
|
426
377
|
functionKey,
|
|
378
|
+
hapiMethod,
|
|
379
|
+
hapiPath,
|
|
427
380
|
method,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
let cors = null
|
|
432
|
-
if (endpoint.cors) {
|
|
433
|
-
cors = {
|
|
434
|
-
credentials:
|
|
435
|
-
endpoint.cors.credentials || this.#options.corsConfig.credentials,
|
|
436
|
-
exposedHeaders: this.#options.corsConfig.exposedHeaders,
|
|
437
|
-
headers: endpoint.cors.headers || this.#options.corsConfig.headers,
|
|
438
|
-
origin: endpoint.cors.origins || this.#options.corsConfig.origin,
|
|
439
|
-
}
|
|
440
|
-
} else if (
|
|
441
|
-
this.#serverless.service.provider.httpApi &&
|
|
442
|
-
this.#serverless.service.provider.httpApi.cors
|
|
443
|
-
) {
|
|
444
|
-
const httpApiCors = getHttpApiCorsConfig(
|
|
445
|
-
this.#serverless.service.provider.httpApi.cors,
|
|
446
|
-
this,
|
|
447
|
-
)
|
|
448
|
-
cors = {
|
|
449
|
-
credentials: httpApiCors.allowCredentials,
|
|
450
|
-
exposedHeaders: httpApiCors.exposedResponseHeaders || [],
|
|
451
|
-
headers: httpApiCors.allowedHeaders || [],
|
|
452
|
-
maxAge: httpApiCors.maxAge,
|
|
453
|
-
origin: httpApiCors.allowedOrigins || [],
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const hapiMethod = method === 'ANY' ? '*' : method
|
|
458
|
-
|
|
459
|
-
const state = this.#options.disableCookieValidation
|
|
460
|
-
? {
|
|
461
|
-
failAction: 'ignore',
|
|
462
|
-
parse: false,
|
|
463
|
-
}
|
|
464
|
-
: {
|
|
465
|
-
failAction: 'error',
|
|
466
|
-
parse: true,
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const hapiOptions = {
|
|
470
|
-
auth: authStrategyName,
|
|
471
|
-
cors,
|
|
472
|
-
state,
|
|
473
|
-
timeout: { socket: false },
|
|
474
|
-
}
|
|
381
|
+
protectedRoute,
|
|
382
|
+
stage,
|
|
383
|
+
} = params
|
|
475
384
|
|
|
476
|
-
|
|
477
|
-
// for more details, check https://github.com/dherault/serverless-offline/issues/204
|
|
478
|
-
if (hapiMethod === 'HEAD') {
|
|
479
|
-
log.notice(
|
|
480
|
-
'HEAD method event detected. Skipping HAPI server route mapping',
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
return
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (hapiMethod !== 'HEAD' && hapiMethod !== 'GET') {
|
|
487
|
-
// maxBytes: Increase request size from 1MB default limit to 10MB.
|
|
488
|
-
// Cf AWS API GW payload limits.
|
|
489
|
-
hapiOptions.payload = {
|
|
490
|
-
maxBytes: 1024 * 1024 * 10,
|
|
491
|
-
parse: false,
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const additionalRequestContext = {}
|
|
496
|
-
if (httpEvent.operationId) {
|
|
497
|
-
additionalRequestContext.operationName = httpEvent.operationId
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
hapiOptions.tags = ['api']
|
|
501
|
-
|
|
502
|
-
const hapiHandler = async (request, h) => {
|
|
385
|
+
return async (request, h) => {
|
|
503
386
|
const requestPath =
|
|
504
387
|
endpoint.isHttpApi || this.#options.noPrependStageInUrl
|
|
505
388
|
? request.path
|
|
506
389
|
: request.path.substr(`/${stage}`.length)
|
|
507
390
|
|
|
508
|
-
//
|
|
391
|
+
// payload processing
|
|
509
392
|
const encoding = detectEncoding(request)
|
|
510
393
|
|
|
511
394
|
request.payload = request.payload && request.payload.toString(encoding)
|
|
512
395
|
request.rawPayload = request.payload
|
|
513
396
|
|
|
514
|
-
//
|
|
397
|
+
// incomming request message
|
|
515
398
|
log.notice()
|
|
516
399
|
|
|
517
400
|
log.notice()
|
|
518
401
|
log.notice(`${method} ${request.path} (λ: ${functionKey})`)
|
|
519
402
|
|
|
520
|
-
//
|
|
403
|
+
// check for APIKey
|
|
521
404
|
if (
|
|
522
|
-
(
|
|
523
|
-
|
|
405
|
+
(protectedRoute === `${hapiMethod}#${hapiPath}` ||
|
|
406
|
+
protectedRoute === `ANY#${hapiPath}`) &&
|
|
524
407
|
!this.#options.noAuth
|
|
525
408
|
) {
|
|
526
409
|
const errorResponse = () =>
|
|
@@ -641,24 +524,18 @@ export default class HttpServer {
|
|
|
641
524
|
event = request.payload || {}
|
|
642
525
|
}
|
|
643
526
|
} else if (integration === 'AWS_PROXY') {
|
|
644
|
-
const stageVariables = this.#serverless.service.custom
|
|
645
|
-
? this.#serverless.service.custom.stageVariables
|
|
646
|
-
: null
|
|
647
|
-
|
|
648
527
|
const lambdaProxyIntegrationEvent =
|
|
649
528
|
endpoint.isHttpApi && endpoint.payload === '2.0'
|
|
650
529
|
? new LambdaProxyIntegrationEventV2(
|
|
651
530
|
request,
|
|
652
531
|
stage,
|
|
653
532
|
endpoint.routeKey,
|
|
654
|
-
stageVariables,
|
|
655
533
|
additionalRequestContext,
|
|
656
534
|
)
|
|
657
535
|
: new LambdaProxyIntegrationEvent(
|
|
658
536
|
request,
|
|
659
537
|
stage,
|
|
660
538
|
requestPath,
|
|
661
|
-
stageVariables,
|
|
662
539
|
endpoint.isHttpApi ? endpoint.routeKey : null,
|
|
663
540
|
additionalRequestContext,
|
|
664
541
|
)
|
|
@@ -710,8 +587,7 @@ export default class HttpServer {
|
|
|
710
587
|
|
|
711
588
|
const errorMessage = (err.message || err).toString()
|
|
712
589
|
|
|
713
|
-
const
|
|
714
|
-
const found = errorMessage.match(re)
|
|
590
|
+
const found = errorMessage.match(/\[(\d{3})]/)
|
|
715
591
|
|
|
716
592
|
if (found && found.length > 1) {
|
|
717
593
|
;[, errorStatusCode] = found
|
|
@@ -862,7 +738,9 @@ export default class HttpServer {
|
|
|
862
738
|
).getContext()
|
|
863
739
|
|
|
864
740
|
result = renderVelocityTemplateObject(
|
|
865
|
-
{
|
|
741
|
+
{
|
|
742
|
+
root: responseTemplate,
|
|
743
|
+
},
|
|
866
744
|
reponseContext,
|
|
867
745
|
).root
|
|
868
746
|
} catch (error) {
|
|
@@ -971,7 +849,9 @@ export default class HttpServer {
|
|
|
971
849
|
headerValue.forEach((value) => {
|
|
972
850
|
// it looks like Hapi doesn't support multiple headers with the same name,
|
|
973
851
|
// appending values is the closest we can come to the AWS behavior.
|
|
974
|
-
response.header(headerKey, value, {
|
|
852
|
+
response.header(headerKey, value, {
|
|
853
|
+
append: true,
|
|
854
|
+
})
|
|
975
855
|
})
|
|
976
856
|
}
|
|
977
857
|
})
|
|
@@ -1009,7 +889,6 @@ export default class HttpServer {
|
|
|
1009
889
|
}
|
|
1010
890
|
}
|
|
1011
891
|
|
|
1012
|
-
// Log response
|
|
1013
892
|
let whatToLog = result
|
|
1014
893
|
|
|
1015
894
|
try {
|
|
@@ -1024,9 +903,152 @@ export default class HttpServer {
|
|
|
1024
903
|
}
|
|
1025
904
|
}
|
|
1026
905
|
|
|
1027
|
-
// Bon voyage!
|
|
1028
906
|
return response
|
|
1029
907
|
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
createRoutes(functionKey, httpEvent, handler) {
|
|
911
|
+
const [handlerPath] = splitHandlerPathAndName(handler)
|
|
912
|
+
|
|
913
|
+
let method
|
|
914
|
+
let path
|
|
915
|
+
let hapiPath
|
|
916
|
+
|
|
917
|
+
if (httpEvent.isHttpApi) {
|
|
918
|
+
if (httpEvent.routeKey === '$default') {
|
|
919
|
+
method = 'ANY'
|
|
920
|
+
path = httpEvent.routeKey
|
|
921
|
+
hapiPath = '/{default*}'
|
|
922
|
+
} else {
|
|
923
|
+
;[method, path] = httpEvent.routeKey.split(' ')
|
|
924
|
+
hapiPath = generateHapiPath(
|
|
925
|
+
path,
|
|
926
|
+
{
|
|
927
|
+
...this.#options,
|
|
928
|
+
noPrependStageInUrl: true, // Serverless always uses the $default stage
|
|
929
|
+
},
|
|
930
|
+
this.#serverless,
|
|
931
|
+
)
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
934
|
+
method = httpEvent.method.toUpperCase()
|
|
935
|
+
;({ path } = httpEvent)
|
|
936
|
+
hapiPath = generateHapiPath(path, this.#options, this.#serverless)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const endpoint = new Endpoint(
|
|
940
|
+
join(this.#serverless.config.servicePath, handlerPath),
|
|
941
|
+
httpEvent,
|
|
942
|
+
).generate()
|
|
943
|
+
|
|
944
|
+
const stage = endpoint.isHttpApi
|
|
945
|
+
? '$default'
|
|
946
|
+
: this.#options.stage || this.#serverless.service.provider.stage
|
|
947
|
+
|
|
948
|
+
const protectedRoute = httpEvent.private
|
|
949
|
+
? `${method}#${hapiPath}`
|
|
950
|
+
: undefined
|
|
951
|
+
|
|
952
|
+
const { host, httpPort, httpsProtocol } = this.#options
|
|
953
|
+
const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${httpPort}`
|
|
954
|
+
|
|
955
|
+
this.#terminalInfo.push({
|
|
956
|
+
invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
|
|
957
|
+
method,
|
|
958
|
+
path: hapiPath,
|
|
959
|
+
server,
|
|
960
|
+
stage:
|
|
961
|
+
endpoint.isHttpApi || this.#options.noPrependStageInUrl ? null : stage,
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
const authStrategyName = this.#setAuthorizationStrategy(
|
|
965
|
+
endpoint,
|
|
966
|
+
functionKey,
|
|
967
|
+
method,
|
|
968
|
+
path,
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
let cors = null
|
|
972
|
+
if (endpoint.cors) {
|
|
973
|
+
cors = {
|
|
974
|
+
credentials:
|
|
975
|
+
endpoint.cors.credentials || this.#options.corsConfig.credentials,
|
|
976
|
+
exposedHeaders: this.#options.corsConfig.exposedHeaders,
|
|
977
|
+
headers: endpoint.cors.headers || this.#options.corsConfig.headers,
|
|
978
|
+
origin: endpoint.cors.origins || this.#options.corsConfig.origin,
|
|
979
|
+
}
|
|
980
|
+
} else if (
|
|
981
|
+
this.#serverless.service.provider.httpApi &&
|
|
982
|
+
this.#serverless.service.provider.httpApi.cors
|
|
983
|
+
) {
|
|
984
|
+
const httpApiCors = getHttpApiCorsConfig(
|
|
985
|
+
this.#serverless.service.provider.httpApi.cors,
|
|
986
|
+
this,
|
|
987
|
+
)
|
|
988
|
+
cors = {
|
|
989
|
+
credentials: httpApiCors.allowCredentials,
|
|
990
|
+
exposedHeaders: httpApiCors.exposedResponseHeaders || [],
|
|
991
|
+
headers: httpApiCors.allowedHeaders || [],
|
|
992
|
+
maxAge: httpApiCors.maxAge,
|
|
993
|
+
origin: httpApiCors.allowedOrigins || [],
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const hapiMethod = method === 'ANY' ? '*' : method
|
|
998
|
+
|
|
999
|
+
const state = this.#options.disableCookieValidation
|
|
1000
|
+
? {
|
|
1001
|
+
failAction: 'ignore',
|
|
1002
|
+
parse: false,
|
|
1003
|
+
}
|
|
1004
|
+
: {
|
|
1005
|
+
failAction: 'error',
|
|
1006
|
+
parse: true,
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const hapiOptions = {
|
|
1010
|
+
auth: authStrategyName,
|
|
1011
|
+
cors,
|
|
1012
|
+
state,
|
|
1013
|
+
timeout: { socket: false },
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
|
|
1017
|
+
// for more details, check https://github.com/dherault/serverless-offline/issues/204
|
|
1018
|
+
if (hapiMethod === 'HEAD') {
|
|
1019
|
+
log.notice(
|
|
1020
|
+
'HEAD method event detected. Skipping HAPI server route mapping',
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
return
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (hapiMethod !== 'HEAD' && hapiMethod !== 'GET') {
|
|
1027
|
+
// maxBytes: Increase request size from 1MB default limit to 10MB.
|
|
1028
|
+
// Cf AWS API GW payload limits.
|
|
1029
|
+
hapiOptions.payload = {
|
|
1030
|
+
maxBytes: 1024 * 1024 * 10,
|
|
1031
|
+
parse: false,
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const additionalRequestContext = {}
|
|
1036
|
+
if (httpEvent.operationId) {
|
|
1037
|
+
additionalRequestContext.operationName = httpEvent.operationId
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
hapiOptions.tags = ['api']
|
|
1041
|
+
|
|
1042
|
+
const hapiHandler = this.#createHapiHandler({
|
|
1043
|
+
additionalRequestContext,
|
|
1044
|
+
endpoint,
|
|
1045
|
+
functionKey,
|
|
1046
|
+
hapiMethod,
|
|
1047
|
+
hapiPath,
|
|
1048
|
+
method,
|
|
1049
|
+
protectedRoute,
|
|
1050
|
+
stage,
|
|
1051
|
+
})
|
|
1030
1052
|
|
|
1031
1053
|
this.#server.route({
|
|
1032
1054
|
handler: hapiHandler,
|
|
@@ -30,22 +30,12 @@ export default class LambdaProxyIntegrationEvent {
|
|
|
30
30
|
|
|
31
31
|
#stage = null
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
constructor(
|
|
36
|
-
request,
|
|
37
|
-
stage,
|
|
38
|
-
path,
|
|
39
|
-
stageVariables,
|
|
40
|
-
routeKey,
|
|
41
|
-
additionalRequestContext,
|
|
42
|
-
) {
|
|
33
|
+
constructor(request, stage, path, routeKey, additionalRequestContext) {
|
|
43
34
|
this.#additionalRequestContext = additionalRequestContext || {}
|
|
44
35
|
this.#path = path
|
|
45
36
|
this.#routeKey = routeKey
|
|
46
37
|
this.#request = request
|
|
47
38
|
this.#stage = stage
|
|
48
|
-
this.#stageVariables = stageVariables
|
|
49
39
|
}
|
|
50
40
|
|
|
51
41
|
create() {
|
|
@@ -227,7 +217,7 @@ export default class LambdaProxyIntegrationEvent {
|
|
|
227
217
|
stage: this.#stage,
|
|
228
218
|
},
|
|
229
219
|
resource,
|
|
230
|
-
stageVariables:
|
|
220
|
+
stageVariables: null,
|
|
231
221
|
}
|
|
232
222
|
}
|
|
233
223
|
}
|
|
@@ -24,20 +24,11 @@ export default class LambdaProxyIntegrationEventV2 {
|
|
|
24
24
|
|
|
25
25
|
#stage = null
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
constructor(
|
|
30
|
-
request,
|
|
31
|
-
stage,
|
|
32
|
-
routeKey,
|
|
33
|
-
stageVariables,
|
|
34
|
-
additionalRequestContext,
|
|
35
|
-
) {
|
|
27
|
+
constructor(request, stage, routeKey, additionalRequestContext) {
|
|
36
28
|
this.#additionalRequestContext = additionalRequestContext || {}
|
|
37
29
|
this.#routeKey = routeKey
|
|
38
30
|
this.#request = request
|
|
39
31
|
this.#stage = stage
|
|
40
|
-
this.#stageVariables = stageVariables
|
|
41
32
|
}
|
|
42
33
|
|
|
43
34
|
create() {
|
|
@@ -183,7 +174,7 @@ export default class LambdaProxyIntegrationEventV2 {
|
|
|
183
174
|
timeEpoch: requestTimeEpoch,
|
|
184
175
|
},
|
|
185
176
|
routeKey: this.#routeKey,
|
|
186
|
-
stageVariables:
|
|
177
|
+
stageVariables: null,
|
|
187
178
|
version: '2.0',
|
|
188
179
|
}
|
|
189
180
|
}
|
|
@@ -39,9 +39,9 @@ export default class Schedule {
|
|
|
39
39
|
const cron = this.#convertExpressionToCron(entry)
|
|
40
40
|
|
|
41
41
|
log.notice(
|
|
42
|
-
`Scheduling [${functionKey}] cron: [${cron}]
|
|
43
|
-
input
|
|
44
|
-
|
|
42
|
+
`Scheduling [${functionKey}] cron: [${cron}]${
|
|
43
|
+
input ? ` input: ${stringify(input)}` : ''
|
|
44
|
+
}`,
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
nodeSchedule.scheduleJob(cron, async () => {
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
} from '../config/index.js'
|
|
16
16
|
import { createUniqueId, splitHandlerPathAndName } from '../utils/index.js'
|
|
17
17
|
|
|
18
|
-
const { entries, fromEntries } = Object
|
|
19
18
|
const { ceil } = Math
|
|
19
|
+
const { entries, fromEntries } = Object
|
|
20
20
|
|
|
21
21
|
export default class LambdaFunction {
|
|
22
22
|
#artifact = null
|
|
@@ -41,14 +41,18 @@ export default class ChildProcessRunner {
|
|
|
41
41
|
},
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
let message
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
message = new Promise((res, rej) => {
|
|
48
|
+
childProcess.on('message', (data) => {
|
|
49
|
+
if (data.error) rej(data.error)
|
|
50
|
+
else res(data)
|
|
51
|
+
})
|
|
48
52
|
})
|
|
49
|
-
}
|
|
53
|
+
} finally {
|
|
50
54
|
childProcess.kill()
|
|
51
|
-
}
|
|
55
|
+
}
|
|
52
56
|
|
|
53
57
|
childProcess.send({
|
|
54
58
|
context,
|
|
@@ -12,6 +12,8 @@ import pRetry from 'p-retry'
|
|
|
12
12
|
import DockerImage from './DockerImage.js'
|
|
13
13
|
|
|
14
14
|
const { stringify } = JSON
|
|
15
|
+
const { floor, log: mathLog } = Math
|
|
16
|
+
const { parseFloat } = Number
|
|
15
17
|
const { entries, hasOwn } = Object
|
|
16
18
|
|
|
17
19
|
export default class DockerContainer {
|
|
@@ -402,7 +404,7 @@ export default class DockerContainer {
|
|
|
402
404
|
const dm = decimals < 0 ? 0 : decimals
|
|
403
405
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
404
406
|
|
|
405
|
-
const i =
|
|
407
|
+
const i = floor(mathLog(bytes) / mathLog(k))
|
|
406
408
|
|
|
407
409
|
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
|
408
410
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, rmdir, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { EOL } from 'node:os'
|
|
3
|
-
import { sep, resolve, parse as pathParse } from 'node:path'
|
|
4
3
|
import process, { chdir, cwd } from 'node:process'
|
|
4
|
+
import { parse as pathParse, resolve, sep } from 'node:path'
|
|
5
5
|
import { log } from '@serverless/utils/log.js'
|
|
6
|
-
import { execa
|
|
6
|
+
import { execa } from 'execa'
|
|
7
7
|
|
|
8
8
|
const { parse, stringify } = JSON
|
|
9
9
|
|
|
10
|
-
const PAYLOAD_IDENTIFIER = 'offline_payload'
|
|
11
|
-
|
|
12
10
|
export default class GoRunner {
|
|
11
|
+
static #payloadIdentifier = 'offline_payload'
|
|
12
|
+
|
|
13
13
|
#codeDir = null
|
|
14
14
|
|
|
15
15
|
#env = null
|
|
@@ -18,10 +18,10 @@ export default class GoRunner {
|
|
|
18
18
|
|
|
19
19
|
#handlerPath = null
|
|
20
20
|
|
|
21
|
-
#tmpPath = null
|
|
22
|
-
|
|
23
21
|
#tmpFile = null
|
|
24
22
|
|
|
23
|
+
#tmpPath = null
|
|
24
|
+
|
|
25
25
|
constructor(funOptions, env) {
|
|
26
26
|
const { handlerPath, codeDir } = funOptions
|
|
27
27
|
|
|
@@ -34,8 +34,10 @@ export default class GoRunner {
|
|
|
34
34
|
try {
|
|
35
35
|
// refresh go.mod
|
|
36
36
|
await rm(this.#tmpFile)
|
|
37
|
-
|
|
38
|
-
await rmdir(this.#tmpPath, {
|
|
37
|
+
await execa('go', ['mod', 'tidy'])
|
|
38
|
+
await rmdir(this.#tmpPath, {
|
|
39
|
+
recursive: true,
|
|
40
|
+
})
|
|
39
41
|
} catch {
|
|
40
42
|
// @ignore
|
|
41
43
|
}
|
|
@@ -49,12 +51,10 @@ export default class GoRunner {
|
|
|
49
51
|
let payload
|
|
50
52
|
|
|
51
53
|
for (const item of value.split(EOL)) {
|
|
52
|
-
if (item.
|
|
53
|
-
logs.push(item)
|
|
54
|
-
} else if (item.indexOf(PAYLOAD_IDENTIFIER) !== -1) {
|
|
54
|
+
if (item.includes(GoRunner.#payloadIdentifier)) {
|
|
55
55
|
try {
|
|
56
56
|
const {
|
|
57
|
-
|
|
57
|
+
[GoRunner.#payloadIdentifier]: { error, success },
|
|
58
58
|
} = parse(item)
|
|
59
59
|
|
|
60
60
|
if (success) {
|
|
@@ -65,6 +65,8 @@ export default class GoRunner {
|
|
|
65
65
|
} catch {
|
|
66
66
|
// @ignore
|
|
67
67
|
}
|
|
68
|
+
} else {
|
|
69
|
+
logs.push(item)
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -122,8 +124,11 @@ export default class GoRunner {
|
|
|
122
124
|
chdir(cwdPath.substring(0, cwdPath.indexOf('main.go')))
|
|
123
125
|
|
|
124
126
|
// Make sure we have the mock-lambda runner
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
await execa('go', [
|
|
128
|
+
'get',
|
|
129
|
+
'github.com/icarus-sullivan/mock-lambda@e065469',
|
|
130
|
+
])
|
|
131
|
+
await execa('go', ['build'])
|
|
127
132
|
} catch {
|
|
128
133
|
// @ignore
|
|
129
134
|
}
|
|
@@ -3,6 +3,7 @@ import { performance } from 'node:perf_hooks'
|
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import { log } from '@serverless/utils/log.js'
|
|
5
5
|
|
|
6
|
+
const { floor } = Math
|
|
6
7
|
const { assign } = Object
|
|
7
8
|
|
|
8
9
|
const require = createRequire(import.meta.url)
|
|
@@ -49,7 +50,6 @@ export default class InProcessRunner {
|
|
|
49
50
|
let handler
|
|
50
51
|
|
|
51
52
|
try {
|
|
52
|
-
// const { [this.#handlerName]: handler } = await import(this.#handlerPath)
|
|
53
53
|
// eslint-disable-next-line import/no-dynamic-require
|
|
54
54
|
;({ [this.#handlerName]: handler } = require(this.#handlerPath))
|
|
55
55
|
} catch (err) {
|
|
@@ -86,15 +86,21 @@ export default class InProcessRunner {
|
|
|
86
86
|
// create new immutable object
|
|
87
87
|
const lambdaContext = {
|
|
88
88
|
...context,
|
|
89
|
-
done
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
done(err, data) {
|
|
90
|
+
callback(err, data)
|
|
91
|
+
},
|
|
92
|
+
fail(err) {
|
|
93
|
+
callback(err)
|
|
94
|
+
},
|
|
95
|
+
getRemainingTimeInMillis() {
|
|
92
96
|
const timeLeft = executionTimeout - performance.now()
|
|
93
97
|
|
|
94
98
|
// just return 0 for now if we are beyond alotted time (timeout)
|
|
95
|
-
return timeLeft > 0 ? timeLeft : 0
|
|
99
|
+
return timeLeft > 0 ? floor(timeLeft) : 0
|
|
100
|
+
},
|
|
101
|
+
succeed(res) {
|
|
102
|
+
callback(null, res)
|
|
96
103
|
},
|
|
97
|
-
succeed: (res) => callback(null, res),
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
let result
|
|
@@ -4,9 +4,11 @@ import { log } from '@serverless/utils/log.js'
|
|
|
4
4
|
import { invokeJavaLocal } from 'java-invoke-local'
|
|
5
5
|
|
|
6
6
|
const { parse, stringify } = JSON
|
|
7
|
-
const {
|
|
7
|
+
const { hasOwn } = Object
|
|
8
8
|
|
|
9
9
|
export default class JavaRunner {
|
|
10
|
+
static #payloadIdentifier = '__offline_payload__'
|
|
11
|
+
|
|
10
12
|
#deployPackage = null
|
|
11
13
|
|
|
12
14
|
#env = null
|
|
@@ -45,9 +47,9 @@ export default class JavaRunner {
|
|
|
45
47
|
if (
|
|
46
48
|
json &&
|
|
47
49
|
typeof json === 'object' &&
|
|
48
|
-
|
|
50
|
+
hasOwn(json, JavaRunner.#payloadIdentifier)
|
|
49
51
|
) {
|
|
50
|
-
return json
|
|
52
|
+
return json[JavaRunner.#payloadIdentifier]
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -7,12 +7,13 @@ import { fileURLToPath } from 'node:url'
|
|
|
7
7
|
import { log } from '@serverless/utils/log.js'
|
|
8
8
|
|
|
9
9
|
const { parse, stringify } = JSON
|
|
10
|
-
const { assign } = Object
|
|
11
|
-
const { has } = Reflect
|
|
10
|
+
const { assign, hasOwn } = Object
|
|
12
11
|
|
|
13
12
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
13
|
|
|
15
14
|
export default class PythonRunner {
|
|
15
|
+
static #payloadIdentifier = '__offline_payload__'
|
|
16
|
+
|
|
16
17
|
#env = null
|
|
17
18
|
|
|
18
19
|
#handlerName = null
|
|
@@ -83,9 +84,9 @@ export default class PythonRunner {
|
|
|
83
84
|
if (
|
|
84
85
|
json &&
|
|
85
86
|
typeof json === 'object' &&
|
|
86
|
-
|
|
87
|
+
hasOwn(json, PythonRunner.#payloadIdentifier)
|
|
87
88
|
) {
|
|
88
|
-
payload = json
|
|
89
|
+
payload = json[PythonRunner.#payloadIdentifier]
|
|
89
90
|
// everything else is print(), logging, ...
|
|
90
91
|
} else {
|
|
91
92
|
log.notice(item)
|
|
@@ -6,11 +6,13 @@ import { log } from '@serverless/utils/log.js'
|
|
|
6
6
|
import { execa } from 'execa'
|
|
7
7
|
|
|
8
8
|
const { parse, stringify } = JSON
|
|
9
|
-
const {
|
|
9
|
+
const { hasOwn } = Object
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
12
|
|
|
13
13
|
export default class RubyRunner {
|
|
14
|
+
static #payloadIdentifier = '__offline_payload__'
|
|
15
|
+
|
|
14
16
|
#env = null
|
|
15
17
|
|
|
16
18
|
#handlerName = null
|
|
@@ -47,9 +49,9 @@ export default class RubyRunner {
|
|
|
47
49
|
if (
|
|
48
50
|
json &&
|
|
49
51
|
typeof json === 'object' &&
|
|
50
|
-
|
|
52
|
+
hasOwn(json, RubyRunner.#payloadIdentifier)
|
|
51
53
|
) {
|
|
52
|
-
payload = json
|
|
54
|
+
payload = json[RubyRunner.#payloadIdentifier]
|
|
53
55
|
} else {
|
|
54
56
|
log.notice(item)
|
|
55
57
|
}
|
|
@@ -2,10 +2,10 @@ import { env } from 'node:process'
|
|
|
2
2
|
import { parentPort, workerData } from 'node:worker_threads'
|
|
3
3
|
import InProcessRunner from '../in-process-runner/index.js'
|
|
4
4
|
|
|
5
|
-
const { functionKey, handlerName, handlerPath } = workerData
|
|
5
|
+
const { functionKey, handlerName, handlerPath, timeout } = workerData
|
|
6
6
|
|
|
7
7
|
parentPort.on('message', async (messageData) => {
|
|
8
|
-
const { context, event, port
|
|
8
|
+
const { context, event, port } = messageData
|
|
9
9
|
|
|
10
10
|
// TODO we could probably cache this in the module scope?
|
|
11
11
|
const inProcessRunner = new InProcessRunner(
|