serverless-offline 13.3.4 → 13.4.0
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/package.json +3 -3
- package/src/config/constants.js +2 -0
- package/src/config/supportedRuntimes.js +30 -9
- package/src/lambda/LambdaFunction.js +8 -0
- package/src/lambda/handler-runner/HandlerRunner.js +1 -1
- package/src/lambda/handler-runner/docker-runner/DockerContainer.js +27 -38
- package/src/lambda/handler-runner/docker-runner/DockerImage.js +1 -5
- package/src/lambda/handler-runner/docker-runner/DockerRunner.js +2 -2
- package/src/lambda/handler-runner/docker-runner/DockerRuntime.js +33 -0
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": "13.
|
|
4
|
+
"version": "13.4.0",
|
|
5
5
|
"description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"test:cov": "NODE_OPTIONS='--experimental-loader @istanbuljs/esm-loader-hook' nyc --reporter=html npm test",
|
|
23
23
|
"test:node": "TEST=node mocha --require ./tests/mochaHooks.cjs",
|
|
24
24
|
"test:unit": "TEST=unit mocha --require ./tests/mochaHooks.cjs",
|
|
25
|
-
"test:e2e": "TEST=e2e mocha --require ./tests/mochaHooks.cjs"
|
|
25
|
+
"test:e2e": "TEST=e2e mocha --require ./tests/mochaHooks.cjs",
|
|
26
|
+
"test:docker": "TEST=docker mocha --require ./tests/mochaHooks.cjs"
|
|
26
27
|
},
|
|
27
28
|
"repository": {
|
|
28
29
|
"type": "git",
|
|
@@ -96,7 +97,6 @@
|
|
|
96
97
|
"luxon": "^3.4.4",
|
|
97
98
|
"node-schedule": "^2.1.1",
|
|
98
99
|
"p-memoize": "^7.1.1",
|
|
99
|
-
"p-retry": "^6.2.0",
|
|
100
100
|
"velocityjs": "^2.0.6",
|
|
101
101
|
"ws": "^8.16.0"
|
|
102
102
|
},
|
package/src/config/constants.js
CHANGED
|
@@ -6,6 +6,8 @@ export const CUSTOM_OPTION = "serverless-offline"
|
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_LAMBDA_RUNTIME = "nodejs14.x"
|
|
8
8
|
|
|
9
|
+
export const DEFAULT_LAMBDA_ARCHITECTURE = "arm64"
|
|
10
|
+
|
|
9
11
|
// https://docs.aws.amazon.com/lambda/latest/dg/limits.html
|
|
10
12
|
export const DEFAULT_LAMBDA_MEMORY_SIZE = 1024
|
|
11
13
|
// default function timeout in seconds
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable sort-keys */
|
|
1
2
|
// native runtime support for AWS
|
|
2
3
|
// https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html
|
|
3
4
|
|
|
@@ -6,6 +7,32 @@
|
|
|
6
7
|
// 'dotnet6',
|
|
7
8
|
// ])
|
|
8
9
|
|
|
10
|
+
const X86_64 = "x86_64"
|
|
11
|
+
const ARM64 = "arm64"
|
|
12
|
+
|
|
13
|
+
export const supportedRuntimesArchitecture = {
|
|
14
|
+
"nodejs14.x": [ARM64, X86_64],
|
|
15
|
+
"nodejs16.x": [ARM64, X86_64],
|
|
16
|
+
"nodejs18.x": [ARM64, X86_64],
|
|
17
|
+
"nodejs20.x": [ARM64, X86_64],
|
|
18
|
+
"python3.7": [X86_64],
|
|
19
|
+
"python3.8": [ARM64, X86_64],
|
|
20
|
+
"python3.9": [ARM64, X86_64],
|
|
21
|
+
"python3.10": [ARM64, X86_64],
|
|
22
|
+
"python3.11": [ARM64, X86_64],
|
|
23
|
+
"ruby2.7": [ARM64, X86_64],
|
|
24
|
+
"ruby3.2": [ARM64, X86_64],
|
|
25
|
+
java8: [X86_64],
|
|
26
|
+
"java8.al2": [ARM64, X86_64],
|
|
27
|
+
java11: [ARM64, X86_64],
|
|
28
|
+
java17: [ARM64, X86_64],
|
|
29
|
+
"go1.x": [X86_64],
|
|
30
|
+
"dotnetcore3.1": [ARM64, X86_64],
|
|
31
|
+
provided: [X86_64],
|
|
32
|
+
dotnet6: [ARM64, X86_64],
|
|
33
|
+
"provided.al2": [ARM64, X86_64],
|
|
34
|
+
}
|
|
35
|
+
|
|
9
36
|
// GO
|
|
10
37
|
export const supportedGo = new Set(["go1.x"])
|
|
11
38
|
|
|
@@ -35,8 +62,6 @@ export const supportedPython = new Set([
|
|
|
35
62
|
// RUBY
|
|
36
63
|
export const supportedRuby = new Set(["ruby2.7", "ruby3.2"])
|
|
37
64
|
|
|
38
|
-
// deprecated runtimes
|
|
39
|
-
// https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html
|
|
40
65
|
export const supportedRuntimes = new Set([
|
|
41
66
|
// ...supportedDotnetcore,
|
|
42
67
|
...supportedGo,
|
|
@@ -47,10 +72,6 @@ export const supportedRuntimes = new Set([
|
|
|
47
72
|
...supportedRuby,
|
|
48
73
|
])
|
|
49
74
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"nodejs18.x",
|
|
54
|
-
"nodejs20.x",
|
|
55
|
-
"python3.9",
|
|
56
|
-
])
|
|
75
|
+
// deprecated runtimes
|
|
76
|
+
// https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html
|
|
77
|
+
export const unsupportedDockerRuntimes = new Set([])
|
|
@@ -10,6 +10,7 @@ import jszip from "jszip"
|
|
|
10
10
|
import HandlerRunner from "./handler-runner/index.js"
|
|
11
11
|
import LambdaContext from "./LambdaContext.js"
|
|
12
12
|
import {
|
|
13
|
+
DEFAULT_LAMBDA_ARCHITECTURE,
|
|
13
14
|
DEFAULT_LAMBDA_MEMORY_SIZE,
|
|
14
15
|
DEFAULT_LAMBDA_RUNTIME,
|
|
15
16
|
DEFAULT_LAMBDA_TIMEOUT,
|
|
@@ -57,6 +58,8 @@ export default class LambdaFunction {
|
|
|
57
58
|
|
|
58
59
|
#runtime = null
|
|
59
60
|
|
|
61
|
+
#architecture = null
|
|
62
|
+
|
|
60
63
|
#status = "IDLE" // can be 'BUSY' or 'IDLE'
|
|
61
64
|
|
|
62
65
|
#timeout = null
|
|
@@ -91,6 +94,10 @@ export default class LambdaFunction {
|
|
|
91
94
|
|
|
92
95
|
this.#runtime =
|
|
93
96
|
functionDefinition.runtime ?? provider.runtime ?? DEFAULT_LAMBDA_RUNTIME
|
|
97
|
+
this.#architecture =
|
|
98
|
+
functionDefinition.architecture ??
|
|
99
|
+
provider.architecture ??
|
|
100
|
+
DEFAULT_LAMBDA_ARCHITECTURE
|
|
94
101
|
|
|
95
102
|
this.#timeout =
|
|
96
103
|
(functionDefinition.timeout ??
|
|
@@ -136,6 +143,7 @@ export default class LambdaFunction {
|
|
|
136
143
|
|
|
137
144
|
// TEMP
|
|
138
145
|
const funOptions = {
|
|
146
|
+
architecture: this.#architecture,
|
|
139
147
|
codeDir: this.#codeDir,
|
|
140
148
|
functionKey,
|
|
141
149
|
functionName: name,
|
|
@@ -32,7 +32,7 @@ export default class HandlerRunner {
|
|
|
32
32
|
if (useDocker) {
|
|
33
33
|
if (unsupportedDockerRuntimes.has(runtime)) {
|
|
34
34
|
log.warning(
|
|
35
|
-
`"${runtime}" runtime is not supported with docker. See https://github.com/
|
|
35
|
+
`"${runtime}" runtime is not supported with docker. See https://github.com/aws/aws-lambda-base-images`,
|
|
36
36
|
)
|
|
37
37
|
throw new Error("Unsupported runtime")
|
|
38
38
|
}
|
|
@@ -9,8 +9,8 @@ import { execa } from "execa"
|
|
|
9
9
|
import { ensureDir, pathExists } from "fs-extra"
|
|
10
10
|
import isWsl from "is-wsl"
|
|
11
11
|
import jszip from "jszip"
|
|
12
|
-
import pRetry from "p-retry"
|
|
13
12
|
import DockerImage from "./DockerImage.js"
|
|
13
|
+
import Runtime from "./DockerRuntime.js"
|
|
14
14
|
|
|
15
15
|
const { stringify } = JSON
|
|
16
16
|
const { floor, log: mathLog } = Math
|
|
@@ -24,8 +24,6 @@ export default class DockerContainer {
|
|
|
24
24
|
|
|
25
25
|
#env = null
|
|
26
26
|
|
|
27
|
-
#functionKey = null
|
|
28
|
-
|
|
29
27
|
#handler = null
|
|
30
28
|
|
|
31
29
|
#image = null
|
|
@@ -42,13 +40,15 @@ export default class DockerContainer {
|
|
|
42
40
|
|
|
43
41
|
#runtime = null
|
|
44
42
|
|
|
43
|
+
#architecture = null
|
|
44
|
+
|
|
45
45
|
#servicePath = null
|
|
46
46
|
|
|
47
47
|
constructor(
|
|
48
48
|
env,
|
|
49
|
-
functionKey,
|
|
50
49
|
handler,
|
|
51
50
|
runtime,
|
|
51
|
+
architecture,
|
|
52
52
|
layers,
|
|
53
53
|
provider,
|
|
54
54
|
servicePath,
|
|
@@ -56,23 +56,24 @@ export default class DockerContainer {
|
|
|
56
56
|
) {
|
|
57
57
|
this.#dockerOptions = dockerOptions
|
|
58
58
|
this.#env = env
|
|
59
|
-
this.#functionKey = functionKey
|
|
60
59
|
this.#handler = handler
|
|
61
|
-
this.#imageNameTag = this.#baseImage(runtime)
|
|
60
|
+
this.#imageNameTag = this.#baseImage(runtime, architecture)
|
|
62
61
|
this.#image = new DockerImage(this.#imageNameTag)
|
|
63
62
|
this.#layers = layers
|
|
64
63
|
this.#provider = provider
|
|
65
64
|
this.#runtime = runtime
|
|
65
|
+
this.#architecture = architecture
|
|
66
66
|
this.#servicePath = servicePath
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
#baseImage(runtime) {
|
|
70
|
-
|
|
69
|
+
#baseImage(runtime, architecture) {
|
|
70
|
+
const runtimeImageTag = new Runtime().getImageNameTag(runtime, architecture)
|
|
71
|
+
// # Gets the ECR image format like `python:3.7` or `nodejs:16-x86_64`
|
|
72
|
+
return `public.ecr.aws/lambda/${runtimeImageTag}`
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
async start(codeDir) {
|
|
74
76
|
await this.#image.pull()
|
|
75
|
-
|
|
76
77
|
log.debug("Run Docker container...")
|
|
77
78
|
|
|
78
79
|
let permissions = "ro"
|
|
@@ -85,19 +86,17 @@ export default class DockerContainer {
|
|
|
85
86
|
"-v",
|
|
86
87
|
`${codeDir}:/var/task:${permissions},delegated`,
|
|
87
88
|
"-p",
|
|
88
|
-
|
|
89
|
+
8080,
|
|
89
90
|
"-e",
|
|
90
91
|
"DOCKER_LAMBDA_STAY_OPEN=1", // API mode
|
|
91
92
|
"-e",
|
|
92
93
|
"DOCKER_LAMBDA_WATCH=1", // Watch mode
|
|
93
94
|
]
|
|
94
|
-
|
|
95
95
|
if (this.#layers.length > 0) {
|
|
96
96
|
log.verbose(`Found layers, checking provider type`)
|
|
97
97
|
|
|
98
98
|
if (this.#provider.name.toLowerCase() === "aws") {
|
|
99
99
|
let layerDir = this.#dockerOptions.layersDir
|
|
100
|
-
|
|
101
100
|
if (!layerDir) {
|
|
102
101
|
layerDir = join(this.#servicePath, ".serverless-offline", "layers")
|
|
103
102
|
}
|
|
@@ -135,12 +134,22 @@ export default class DockerContainer {
|
|
|
135
134
|
this.#dockerOptions.hostServicePath,
|
|
136
135
|
)
|
|
137
136
|
}
|
|
138
|
-
dockerArgs.push("-v", `${layerDir}:/
|
|
137
|
+
dockerArgs.push("-v", `${layerDir}:/var/runtime:ro,delegated`)
|
|
139
138
|
} else {
|
|
140
139
|
log.warning(
|
|
141
140
|
`Provider ${this.#provider.name} is Unsupported. Layers are only supported on aws.`,
|
|
142
141
|
)
|
|
143
142
|
}
|
|
143
|
+
} else {
|
|
144
|
+
log.debug("Looking for bootstrap file")
|
|
145
|
+
const bootstrapDir = join(this.#servicePath, "bootstrap")
|
|
146
|
+
if (await pathExists(bootstrapDir)) {
|
|
147
|
+
log.debug(`Found bootstrap file at ${bootstrapDir}`)
|
|
148
|
+
dockerArgs.push(
|
|
149
|
+
"-v",
|
|
150
|
+
`${bootstrapDir}:/var/runtime/bootstrap:ro,delegated`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
entries(this.#env).forEach(([key, value]) => {
|
|
@@ -176,7 +185,7 @@ export default class DockerContainer {
|
|
|
176
185
|
const str = String(data)
|
|
177
186
|
log.error(str)
|
|
178
187
|
|
|
179
|
-
if (str.includes("
|
|
188
|
+
if (str.includes("(cwd=/var/task, handler=)")) {
|
|
180
189
|
resolve()
|
|
181
190
|
}
|
|
182
191
|
})
|
|
@@ -195,12 +204,12 @@ export default class DockerContainer {
|
|
|
195
204
|
// NOTE: `docker port` may output multiple lines.
|
|
196
205
|
//
|
|
197
206
|
// e.g.:
|
|
198
|
-
//
|
|
199
|
-
//
|
|
207
|
+
// 8080/tcp -> 0.0.0.0:49153
|
|
208
|
+
// 8080/tcp -> :::49153
|
|
200
209
|
//
|
|
201
210
|
// Parse each line until it finds the mapped port.
|
|
202
211
|
for (const line of dockerPortOutput.split("\n")) {
|
|
203
|
-
const result = line.match(/^
|
|
212
|
+
const result = line.match(/^8080\/tcp -> (.*):(\d+)$/)
|
|
204
213
|
if (result && result.length > 2) {
|
|
205
214
|
;[, , containerPort] = result
|
|
206
215
|
break
|
|
@@ -212,15 +221,6 @@ export default class DockerContainer {
|
|
|
212
221
|
|
|
213
222
|
this.#containerId = containerId
|
|
214
223
|
this.#port = containerPort
|
|
215
|
-
|
|
216
|
-
await pRetry(() => this.#ping(), {
|
|
217
|
-
// default,
|
|
218
|
-
factor: 2,
|
|
219
|
-
// milliseconds
|
|
220
|
-
minTimeout: 10,
|
|
221
|
-
// default
|
|
222
|
-
retries: 10,
|
|
223
|
-
})
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
async #downloadLayer(layerArn, layerDir) {
|
|
@@ -344,19 +344,8 @@ export default class DockerContainer {
|
|
|
344
344
|
return gateway.split("/")[0]
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
async #ping() {
|
|
348
|
-
const url = `http://${this.#dockerOptions.host}:${this.#port}/2018-06-01/ping`
|
|
349
|
-
const res = await fetch(url)
|
|
350
|
-
|
|
351
|
-
if (!res.ok) {
|
|
352
|
-
throw new Error(`Failed to fetch from ${url} with ${res.statusText}`)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return res.text()
|
|
356
|
-
}
|
|
357
|
-
|
|
358
347
|
async request(event) {
|
|
359
|
-
const url = `http://${this.#dockerOptions.host}:${this.#port}/2015-03-31/functions
|
|
348
|
+
const url = `http://${this.#dockerOptions.host}:${this.#port}/2015-03-31/functions/function/invocations`
|
|
360
349
|
|
|
361
350
|
const res = await fetch(url, {
|
|
362
351
|
body: stringify(event),
|
|
@@ -17,11 +17,7 @@ export default class DockerImage {
|
|
|
17
17
|
log.debug(`Downloading base Docker image... (${imageNameTag})`)
|
|
18
18
|
|
|
19
19
|
try {
|
|
20
|
-
await execa("docker", [
|
|
21
|
-
"pull",
|
|
22
|
-
"--disable-content-trust=false",
|
|
23
|
-
imageNameTag,
|
|
24
|
-
])
|
|
20
|
+
await execa("docker", ["pull", imageNameTag])
|
|
25
21
|
} catch (err) {
|
|
26
22
|
log.error(err.stderr)
|
|
27
23
|
|
|
@@ -9,9 +9,9 @@ export default class DockerRunner {
|
|
|
9
9
|
constructor(funOptions, env, dockerOptions) {
|
|
10
10
|
const {
|
|
11
11
|
codeDir,
|
|
12
|
-
functionKey,
|
|
13
12
|
handler,
|
|
14
13
|
runtime,
|
|
14
|
+
architecture,
|
|
15
15
|
layers,
|
|
16
16
|
provider,
|
|
17
17
|
servicePath,
|
|
@@ -31,9 +31,9 @@ export default class DockerRunner {
|
|
|
31
31
|
|
|
32
32
|
this.#container = new DockerContainer(
|
|
33
33
|
env,
|
|
34
|
-
functionKey,
|
|
35
34
|
handler,
|
|
36
35
|
runtime,
|
|
36
|
+
architecture,
|
|
37
37
|
layers,
|
|
38
38
|
provider,
|
|
39
39
|
servicePath,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { supportedRuntimesArchitecture } from "../../../config/supportedRuntimes.js"
|
|
2
|
+
|
|
3
|
+
export default class Runtime {
|
|
4
|
+
getImageNameTag(runtime, architecture) {
|
|
5
|
+
let runtimeImageTag = ""
|
|
6
|
+
|
|
7
|
+
if (runtime === this.provided) {
|
|
8
|
+
runtimeImageTag = "provided:alami"
|
|
9
|
+
} else if (runtime.startsWith("provided")) {
|
|
10
|
+
runtimeImageTag = runtime.replace(".", ":")
|
|
11
|
+
} else if (runtime.startsWith("dotnet")) {
|
|
12
|
+
runtimeImageTag = runtime.replace("dotnet", "dotnet:")
|
|
13
|
+
} else {
|
|
14
|
+
const match = /^([a-z]+)(\d[\d.a-z]*)$/.exec(runtime)
|
|
15
|
+
runtimeImageTag = match ? `${match[1]}:${match[2]}` : runtime
|
|
16
|
+
runtimeImageTag = runtimeImageTag.replace(".x", "")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (this.hasRuntimeMultiArchImage(runtime)) {
|
|
20
|
+
runtimeImageTag = `${runtimeImageTag}-${architecture}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return runtimeImageTag
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
hasRuntimeMultiArchImage(runtime) {
|
|
27
|
+
return (
|
|
28
|
+
(supportedRuntimesArchitecture[runtime] &&
|
|
29
|
+
supportedRuntimesArchitecture[runtime].length > 1) ||
|
|
30
|
+
false
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|