├── .github
└── workflows
│ └── build-new.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── bootstrap
├── build-catalog.mjs
├── build-new.mjs
├── build-one.mjs
├── delete-catalog.mjs
└── regions.json
└── test
├── bootstrap.test.js
├── handlers
├── asyncHelloWorld.js
├── asyncWithAsyncException.js
├── asyncWithSyncException.js
├── es6
│ ├── asyncHelloWorld.js
│ ├── package.json
│ └── syncHelloWorld.js
├── function.js
├── handlerInitializationError.js
├── missingHandlerExport.js
├── syncHandlerReturnsPromise.js
├── syncHandlerWrongArity.js
├── syncHelloWorld.js
├── syncWithAsyncException.js
└── syncWithSyncException.js
├── runtime.js
└── runtime.test.js
/.github/workflows/build-new.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Deploy Lambda Layers For New Node.js Releases
5 |
6 | on:
7 | workflow_dispatch:
8 | schedule:
9 | - cron: "0 */6 * * *"
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Upgrade aws-cli
18 | run: |
19 | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
20 | unzip awscliv2.zip >/dev/null
21 | sudo ./aws/install --update
22 | /usr/local/bin/aws --version
23 | aws --version
24 | - name: Use Node.js
25 | uses: actions/setup-node@v2
26 | with:
27 | node-version: "16.x"
28 | - name: Check for new Node.js releases and build new layers
29 | env:
30 | SLACK_URL: ${{ secrets.SLACK_URL }}
31 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_072 }}
32 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_072 }}
33 | run: |
34 | npm i -g zx
35 | npm ci
36 | npm test
37 | npm run build:new
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | node_modules/
3 | examples/
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v1.2.0
2 |
3 | - **Enhancement**. Support for Lambda handlers as ES6 modules in Node.js >= 14.
4 | - **Enhancement**. Parallel build of Lambda layers of a single Node.js version for all regions.
5 | - **Fix**. Remove arity requirement of async Lambda handlers.
6 |
7 | # v1.1.0
8 |
9 | - **Enhancement**. Bundle the entire release of Node.js, including npm and npx, into the custom layer.
10 |
11 | # v1.0.0
12 |
13 | - Initial release
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Fusebit
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
NOTE: The project has been discontinued. The underlying AWS Lambda layer storage will no longer be accessible after 1/15/2023.
2 |
3 |
4 |
5 | # Run Any Node.js Version in AWS Lambda
6 |
7 | Everynode allows you to run **any version of Node.js in AWS Lambda**, in any commercial AWS region. We add support for new Node.js versions within **six hours** of the release.
8 |
9 |
10 |
11 | - Create and run AWS Lambda functions using any version of Node.js >= 11.
12 | - **New releases of Node.js are supported within 6 hours**.
13 | - Deploy to any commercial AWS region.
14 | - No magic or tricks - open source.
15 | - Free forever, created and maintained for developers like you by developers at [Fusebit](https://fusebit.io).
16 |
17 | ## Quickstart
18 |
19 | Let's deploy a _Hello, World_ Lambda function using Node.js 17.5.0 to us-west-1.
20 |
21 | First, create the Lambda deployment package:
22 |
23 | ```bash
24 | cat > function.js < {
26 | callback(null, { message: "Hello from Node " + process.version });
27 | };
28 | EOF
29 |
30 | zip function.zip function.js
31 | ```
32 |
33 | Next, create the _hello17_ Lambda function in us-west-1 that uses a custom Lambda runtime with Node.js v17.5.0 provided by Fusebit:
34 |
35 | ```bash
36 | # Get the ARN of the custom runtime layer containg Node.js 17.5.0 for us-west-1
37 | LAYER=$(curl https://cdn.fusebit.io/everynode/layers.txt --no-progress-meter | grep 'us-west-1 17.5.0' | awk '{ print $3 }')
38 |
39 | # Create a Lambda function using Node.js 17.5.0
40 | aws lambda create-function --function-name hello17 \
41 | --layers $LAYER \
42 | --region us-west-1 \
43 | --zip-file fileb://function.zip \
44 | --handler function.handler \
45 | --runtime provided \
46 | --role {iam-role-arn}
47 | ```
48 |
49 | Last, call the function:
50 |
51 | ```bash
52 | aws lambda invoke --function-name hello17 response.json
53 | cat response.json
54 | ```
55 |
56 | And voila, welcome to Node.js v17.5.0 in AWS Lambda:
57 |
58 | ```json
59 | { "message": "Hello from Node v17.5.0" }
60 | ```
61 |
62 | ## Any Region, Any Node.js Version, One Lambda
63 |
64 | The Everynode project provides pre-built [AWS Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) that contain [custom AWS Lambda runtimes](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) for every Node.js version >=11 in all commercial AWS regions. When you want to create a Lambda function using a specific Node.js version in a specific AWS region, you need to choose the right AWS layer for it.
65 |
66 | Each combination of AWS region and Node.js version has a distinct layer ARN you need to use when deploying a Lambda function to that region. You can find the ARN of the layer you need from the catalog we publish:
67 |
68 | - In the JSON format, at [https://cdn.fusebit.io/everynode/layers.json](https://cdn.fusebit.io/everynode/layers.json)
69 | - In the text format, at [https://cdn.fusebit.io/everynode/layers.txt](https://cdn.fusebit.io/everynode/layers.txt)
70 |
71 | Lambda layers for new Node.js versions are published generally within 6 hours after the Node.js release.
72 |
73 | The JSON format of the catalog is convenient for programmatic use from your application. The text format is convenient for scripting. For example, you can get the AWS Lambda layer ARN for Node.js v17.4.0 in region us-east-1 with:
74 |
75 | ```bash
76 | LAYER=$(curl https://cdn.fusebit.io/everynode/layers.txt --no-progress-meter | grep 'us-east-1 17.4.0' | awk '{ print $3 }')
77 | echo $LAYER
78 | ```
79 |
80 | Once you have the ARN of the layer matching your desired Node.js version and AWS region, you can provide it to the `--layers` option of the `aws lambda create-function` call, or specify it in the `Layers` array when making a direct API request.
81 |
82 | ## FAQ
83 |
84 | **Is it really free?**
85 |
86 | Yes. Support for any version of Node.js in AWS Lambda is a by-product of the engineering behind [Fusebit](https://fusebit.io), our developer-friendly integration platform that helps devs add integrations to their apps.
87 |
88 | **How can I get in touch with feedback, questions, etc?**
89 |
90 | You can [join our community Slack, Discord, or e-mail us](https://fusebit.io/contact/). You can also reach us on Twitter [@fusebitio](https://twitter.com/fusebitio). You can also file an issue in this repo.
91 |
92 | **How do I report an issue?**
93 |
94 | [File an issue](https://github.com/fusebit/everynode/issues). Or better still, submit a PR.
95 |
96 | **What's included in the custom runtime?**
97 |
98 | Only the full release of Node.js (including `node`, `npm`, and `npx`). In particular, the `aws-sdk` module is **not** included. If you need to use it, you must include it in your Lambda deployment package.
99 |
100 | **Is ES6 supported?**
101 |
102 | Yes, Lambda handlers can be implemented as ES6 modules in Node.js >= 14.
103 |
104 | **Are you mining bitcoins in my AWS account?**
105 |
106 | We try not to. But since Everynode is OSS, you can check yourself. You can even deploy your own copies of the custom Lambda layers to your own AWS account.
107 |
108 | **Do you have cool stickers?**
109 |
110 | Yes. Get in touch and we will send you some.
111 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@fusebit/everynode",
3 | "version": "1.2.1",
4 | "description": "Run Any Node.js Version in AWS Lambda",
5 | "license": "MIT",
6 | "author": "https://fusebit.io",
7 | "devDependencies": {
8 | "semver": "^7.3.5",
9 | "express": "4.17.1",
10 | "jest": "^24.9.0",
11 | "prettier": "^2.0.5",
12 | "superagent": "6.1.0"
13 | },
14 | "scripts": {
15 | "test": "jest --colors",
16 | "coverage": "jest --colors --coverage",
17 | "prettier": "prettier --write ./src ./test",
18 | "build:one": "./src/build-one.mjs",
19 | "build:catalog": "./src/build-catalog.mjs",
20 | "build:new": "./src/build-new.mjs",
21 | "clean": "rm -rf ./build"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/bootstrap:
--------------------------------------------------------------------------------
1 | #!/opt/node/bin/node
2 |
3 | process.env.PATH = "/opt/node/bin:" + process.env.PATH;
4 |
5 | const Http = require("http");
6 | const agent = new Http.Agent({ keepAlive: true, maxSockets: 1 });
7 |
8 | const { AWS_LAMBDA_RUNTIME_API, LAMBDA_TASK_ROOT, _HANDLER } = process.env;
9 | const debuggingEnabled =
10 | !!process.env.EVERYNODE_DEBUG &&
11 | !!(isNaN(process.env.EVERYNODE_DEBUG) || +process.env.EVERYNODE_DEBUG);
12 |
13 | const original = {
14 | console: {
15 | log: debuggingEnabled ? console.log : () => {},
16 | },
17 | http: {
18 | request: Http.request,
19 | },
20 | };
21 |
22 | original.console.log.bind(console);
23 | original.http.request.bind(Http);
24 | original.console.log("EVERYNODE BOOTSTRAP HANDLER INIT", process.env);
25 |
26 | // HTTP GET and POST request helper
27 | const request = async (method, path, headers, body) =>
28 | new Promise((resolve, reject) => {
29 | const options = { agent, headers, method };
30 | const url = `http://${AWS_LAMBDA_RUNTIME_API}${path}`;
31 | const req = original.http.request(url, options, (res) => {
32 | if (res.statusCode !== 200 && res.statusCode !== 202) {
33 | return reject(
34 | new Error(
35 | `Unexpected response from Runtime API at ${method} ${path}: HTTP ${res.statusCode}.`
36 | )
37 | );
38 | }
39 | res.setEncoding("utf8");
40 | res.body = undefined;
41 | res.on("data", (chunk) => {
42 | res.body = (res.body || "") + chunk;
43 | });
44 | res.on("end", () => {
45 | try {
46 | res.body = res.body !== undefined && JSON.parse(res.body);
47 | } catch (e) {
48 | return reject(
49 | new Error(
50 | `Response from Runtime API at ${method} ${path} is not in the JSON format: ${e.message}`
51 | )
52 | );
53 | }
54 | return resolve(res);
55 | });
56 | });
57 | if (body !== undefined) {
58 | req.setHeader("content-type", "application/json");
59 | }
60 | return body !== undefined ? req.end(body) : req.end();
61 | });
62 |
63 | // Unhandled exception logic
64 | let currentRequestId;
65 | let uncaughtError;
66 | const handleUncaughtException = async (error) => {
67 | original.console.log(
68 | "EVERYNODE LAYER BOOTSTRAP UNCAUGHT ERROR",
69 | error.stack || error.message || error
70 | );
71 | uncaughtError = error || new Error("Unknown error");
72 | if (currentRequestId) {
73 | const errorType = "Handler.UnhandledError";
74 | await request(
75 | "POST",
76 | `/2018-06-01/runtime/invocation/${currentRequestId}/error`,
77 | {
78 | "Lambda-Runtime-Function-Error-Type": errorType,
79 | },
80 | JSON.stringify({
81 | errorMessage: (error && error.message) || "Unknown error",
82 | errorType,
83 | stackTrace: ((error && error.stack) || "").split("\n"),
84 | })
85 | );
86 | }
87 | await new Promise(() => setTimeout(() => process.exit(13), 200));
88 | };
89 | process.once("uncaughtException", handleUncaughtException);
90 |
91 | // Create request context as per https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html
92 | const createContext = (headers) => {
93 | const deadlineMs = Number.parseInt(headers["lambda-runtime-deadline-ms"], 10);
94 | const parseOrUndefined = (text) => {
95 | try {
96 | return JSON.parse(text);
97 | } catch (e) {
98 | return undefined;
99 | }
100 | };
101 | const context = {
102 | deadlineMs,
103 | getRemainingTimeInMillis: () => deadlineMs - Date.now(),
104 | identity: parseOrUndefined(headers["lambda-runtime-cognito-identity"]),
105 | clientContext: parseOrUndefined(headers["lambda-runtime-client-context"]),
106 | functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
107 | functionVersion: process.env.AWS_LAMBDA_FUNCTION_VERSION,
108 | invokedFunctionArn: headers["lambda-runtime-invoked-function-arn"],
109 | memoryLimitInMB: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
110 | awsRequestId: headers["lambda-runtime-aws-request-id"],
111 | logGroupName: process.env.AWS_LAMBDA_LOG_GROUP_NAME,
112 | logStreamName: process.env.AWS_LAMBDA_LOG_STREAM_NAME,
113 | };
114 | return context;
115 | };
116 |
117 | (async function () {
118 | // Load the Lamba function handler
119 | let handler;
120 | let isAsync;
121 | let errorType = "Runtime.ErrorLoadingHandler";
122 | try {
123 | const [fileName, exportName] = _HANDLER.split(".");
124 | if (!fileName || !exportName) {
125 | errorType = "Runtime.NoSuchHandler";
126 | throw new Error(
127 | `The _HANDLER value '${_HANDLER}' must be in the {fileName}.{exportName} format.`
128 | );
129 | }
130 | try {
131 | const major = +process.versions.node.split(".")[0];
132 | const moduleName = `${LAMBDA_TASK_ROOT}/${fileName}.js`;
133 | if (major >= 14) {
134 | // Load as either CJS or ES6
135 | const module = await import(moduleName);
136 | handler =
137 | module[exportName] || (module.default && module.default[exportName]);
138 | } else {
139 | // Load as CJS
140 | const module = require(moduleName);
141 | handler = module[exportName];
142 | }
143 | if (typeof handler !== "function") {
144 | errorType = "Runtime.WrongHandlerType";
145 | throw new Error(
146 | `The handler '${exportName}' is not a function: ${typeof handler}`
147 | );
148 | }
149 | isAsync = handler.constructor.name === "AsyncFunction";
150 | } catch (e) {
151 | throw new Error(`Error loading handler '${_HANDLER}': ${e.message}`);
152 | }
153 | } catch (e) {
154 | await request(
155 | "POST",
156 | "/2018-06-01/runtime/init/error",
157 | {
158 | "Lambda-Runtime-Function-Error-Type": errorType,
159 | },
160 | JSON.stringify({
161 | errorMessage: e.message,
162 | errorType,
163 | stackTrace: (e.stack || "").split("\n"),
164 | })
165 | );
166 | process.exit(12);
167 | }
168 |
169 | // In an endless loop, fetch the next request and process it
170 | while (!uncaughtError) {
171 | // Get the next request from Runtime API
172 | const { headers, body } = await request(
173 | "GET",
174 | "/2018-06-01/runtime/invocation/next"
175 | );
176 | const requestId = headers["lambda-runtime-aws-request-id"];
177 | const traceId = headers["lambda-runtime-trace-id"];
178 | if (traceId) {
179 | process.env._X_AMZN_TRACE_ID = traceId;
180 | } else {
181 | delete process.env._X_AMZN_TRACE_ID;
182 | }
183 | const context = createContext(headers);
184 | let error;
185 | let data;
186 | // Run the handler of the Lambda function
187 | original.console.log("EVERYNODE BOOTSTRAP HANDLER CALL");
188 | try {
189 | currentRequestId = requestId;
190 | if (isAsync) {
191 | data = await handler(body, context);
192 | } else {
193 | data = await new Promise(async (resolve, reject) => {
194 | let isDone;
195 | const done = (error, data) => {
196 | if (!isDone) {
197 | isDone = true;
198 | return error ? reject(error) : resolve(data);
199 | }
200 | };
201 | let tmp;
202 | try {
203 | tmp = handler(body, context, done);
204 | } catch (e) {
205 | return reject(e);
206 | }
207 | // Sync functions may return a Promise instead of calling a callback
208 | if (tmp && typeof tmp.then === 'function') {
209 | let data;
210 | let error;
211 | try {
212 | data = await tmp;
213 | } catch (e) {
214 | error = e;
215 | }
216 | done(error, data);
217 | }
218 | });
219 | }
220 | data = data !== undefined && JSON.stringify(data);
221 | } catch (e) {
222 | error = e;
223 | } finally {
224 | currentRequestId = undefined;
225 | }
226 | // Send the response or error back to Runtime API
227 | original.console.log(
228 | "EVERYNODE BOOTSTRAP HANDLER FINISHED",
229 | error && (error.stack || error.message || error)
230 | );
231 | if (error) {
232 | const errorType = "Handler.Error";
233 | await request(
234 | "POST",
235 | `/2018-06-01/runtime/invocation/${requestId}/error`,
236 | {
237 | "Lambda-Runtime-Function-Error-Type": errorType,
238 | },
239 | JSON.stringify({
240 | errorMessage: error.message || "Unknown error",
241 | errorType,
242 | stackTrace: (error.stack || "").split("\n"),
243 | })
244 | );
245 | } else {
246 | await request(
247 | "POST",
248 | `/2018-06-01/runtime/invocation/${requestId}/response`,
249 | undefined,
250 | data
251 | );
252 | }
253 | }
254 | })();
255 |
--------------------------------------------------------------------------------
/src/build-catalog.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env zx
2 |
3 | // Builds a catalog of layers across regions and publishes to Fusebit CDN
4 | // at https://cdn.fusebit.io/everynode/layers.json
5 |
6 | const Semver = require("semver");
7 | let [_, __, ___, regions, versionSelector] = process.argv;
8 | versionSelector = versionSelector || ">=11";
9 | regions = regions ? regions.split(",") : require("./regions.json");
10 | const Fs = require("fs");
11 |
12 | const getLayers = async (region) => {
13 | const versions = {}; // version -> arn
14 | let nextToken;
15 | do {
16 | const result = JSON.parse(
17 | await $`aws lambda list-layers --region ${region} ${
18 | (nextToken && `--starting-token ${nextToken}`) || ``
19 | }`
20 | );
21 | if (Array.isArray(result.Layers)) {
22 | result.Layers.forEach((layer) => {
23 | if (
24 | layer.LatestMatchingVersion &&
25 | layer.LatestMatchingVersion.Description?.match(
26 | /\@fusebit\/everynode\@/
27 | )
28 | ) {
29 | const version =
30 | layer.LatestMatchingVersion.Description?.match(
31 | /node\@([^\s]+)/
32 | )?.[1];
33 | if (version && Semver.satisfies(version, versionSelector)) {
34 | versions[version] = layer.LatestMatchingVersion.LayerVersionArn;
35 | }
36 | }
37 | });
38 | nextToken = result.NextToken;
39 | }
40 | } while (nextToken);
41 | return versions;
42 | };
43 |
44 | const catalogUpdate = {}; // region -> version -> arn
45 | for (const region of regions) {
46 | catalogUpdate[region] = await getLayers(region);
47 | }
48 |
49 | const dir = `${__dirname}/../build`;
50 | await $`mkdir -p ${dir}`;
51 |
52 | // Upload JSON catalog
53 | Fs.writeFileSync(`${dir}/layers.json`, JSON.stringify(catalogUpdate, null, 2));
54 | await $`aws s3 cp ${dir}/layers.json s3://fusebit-io-cdn/everynode/layers.json --content-type application/json --cache-control no-cache --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers full=id=332f10bace808ea274aecc80e667990adc92dc993a597a42622105dc1f0050bf --region us-east-1`;
55 |
56 | // Upload TXT catalog to be used for scripting
57 | // curl https://cdn.fusebit.io/everynode/layers.txt --no-progress-meter | grep 'us-west-1 17.4.0' | awk '{ print $3 }'
58 | const lines = [];
59 | Object.keys(catalogUpdate).forEach((region) =>
60 | Object.keys(catalogUpdate[region]).forEach((version) => {
61 | lines.push(`${region} ${version} ${catalogUpdate[region][version]}`);
62 | })
63 | );
64 | Fs.writeFileSync(`${dir}/layers.txt`, lines.join("\n"));
65 | await $`aws s3 cp ${dir}/layers.txt s3://fusebit-io-cdn/everynode/layers.txt --content-type text/plain --cache-control no-cache --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers full=id=332f10bace808ea274aecc80e667990adc92dc993a597a42622105dc1f0050bf --region us-east-1`;
66 |
67 | if (process.env.SLACK_URL) {
68 | await fetch(process.env.SLACK_URL, {
69 | method: "post",
70 | body: JSON.stringify({
71 | text: `:rocket: Updated layer catalog on CDN:\nhttps://cdn.fusebit.io/everynode/layers.json\nhttps://cdn.fusebit.io/everynode/layers.txt`,
72 | }),
73 | headers: { "Content-Type": "application/json" },
74 | });
75 | }
76 |
77 | await sleep(2000); // Give CDN a chance to catch up
78 |
79 | console.log(catalogUpdate);
80 |
--------------------------------------------------------------------------------
/src/build-new.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env zx
2 |
3 | // Checks for new releases of Node.js and publishes a layer for them across regions
4 |
5 | try {
6 | const Semver = require("semver");
7 | let [_, __, ___, versionSelector, regions] = process.argv;
8 | versionSelector = versionSelector || ">=11";
9 | regions = regions ? regions.split(",") : require("./regions.json");
10 | const forceBuild = process.env.EVERYNODE_FORCE_BUILD == 1;
11 |
12 | console.log(
13 | forceBuild ? "REBUILDING ALL" : "BUILDING NEW",
14 | "NODE.JS VERSIONS",
15 | versionSelector,
16 | "IN REGIONS",
17 | regions.join(", ")
18 | );
19 |
20 | const sendToSlack = async (text) => {
21 | if (process.env.SLACK_URL) {
22 | return fetch(process.env.SLACK_URL, {
23 | method: "post",
24 | body: JSON.stringify({ text }),
25 | headers: { "Content-Type": "application/json" },
26 | });
27 | }
28 | };
29 |
30 | const getMissingLayers = async (availableVersions, forceBuild) => {
31 | // Get current catalog
32 | let publishedLayers = await fetch(
33 | "https://cdn.fusebit.io/everynode/layers.json"
34 | );
35 | if (publishedLayers.ok) {
36 | publishedLayers = JSON.parse(await publishedLayers.text());
37 | } else {
38 | throw new Error("Error getting published layers");
39 | }
40 | console.log("Published layers:", JSON.stringify(publishedLayers, null, 2));
41 |
42 | // Compute missing layers and regions
43 | const missingLayers = {};
44 | regions.forEach((region) =>
45 | availableVersions.forEach((version) => {
46 | if (!publishedLayers[region]?.[version] || forceBuild) {
47 | missingLayers[version] = missingLayers[version] || [];
48 | missingLayers[version].push(region);
49 | }
50 | })
51 | );
52 |
53 | return missingLayers;
54 | };
55 |
56 | // Get available Node.js versions
57 | let listing = await fetch("https://nodejs.org/dist/");
58 | if (listing.ok) {
59 | listing = await listing.text();
60 | } else {
61 | throw new Error("Error getting Node.js listing");
62 | }
63 | let availableVersions = {};
64 | listing.match(/v\d+\.\d+\.\d+\//g).forEach((v) => {
65 | const version = v.match(/(\d+\.\d+\.\d+)/)[1];
66 | if (Semver.satisfies(version, versionSelector)) {
67 | availableVersions[version] = 1;
68 | }
69 | });
70 | availableVersions = Object.keys(availableVersions);
71 | console.log("Published Node.js versions:", JSON.stringify(availableVersions));
72 |
73 | // Get missing layers
74 | const missingLayers = await getMissingLayers(availableVersions, forceBuild);
75 | console.log(
76 | "Missing layers and regions:",
77 | JSON.stringify(missingLayers, null, 2)
78 | );
79 |
80 | // Build missing layers
81 | let progress = 0;
82 | const missingVersions = Object.keys(missingLayers);
83 | for (let version of missingVersions) {
84 | const missingRegions = missingLayers[version].join(",");
85 | const percent = Math.floor((progress++ / missingVersions.length) * 100);
86 | console.log(`${percent}% Building v${version} for ${missingRegions}...`);
87 | const { exitCode } = await nothrow(
88 | $`${__dirname}/build-one.mjs ${version} ${missingRegions}`
89 | );
90 | }
91 |
92 | // Update catalog
93 |
94 | if (missingVersions.length > 0) {
95 | console.log("Updating catalog...");
96 | await nothrow($`${__dirname}/build-catalog.mjs`);
97 | }
98 |
99 | // Get missing layers again to cross-check
100 | const stillMissingLayers = await getMissingLayers(availableVersions, false);
101 | const newLayers = missingLayers;
102 | Object.keys(stillMissingLayers).forEach((version) => {
103 | const stillMissingRegions = stillMissingLayers[version];
104 | stillMissingRegions.forEach((region) => {
105 | if (newLayers[version]) {
106 | const index = newLayers[version].indexOf(region);
107 | if (index > -1) {
108 | newLayers[version].splice(index, 1);
109 | }
110 | }
111 | if (newLayers[version].length === 0) {
112 | delete newLayers[version];
113 | }
114 | });
115 | });
116 |
117 | if (Object.keys(newLayers).length > 0) {
118 | console.log("Newly published layers", JSON.stringify(newLayers, null, 2));
119 | } else {
120 | await sendToSlack(
121 | `:information_source: There are no new Node.js releases since the last run. Latest Node.js version is *v${
122 | Semver.sort(availableVersions)[availableVersions.length - 1]
123 | }*.`
124 | );
125 | console.log("No new layers were published");
126 | }
127 | if (Object.keys(stillMissingLayers).length > 0) {
128 | console.log(
129 | "Error publishing layers",
130 | JSON.stringify(stillMissingLayers, null, 2)
131 | );
132 | await sendToSlack(
133 | `:sos: There was an error publishing the following layers:\n${JSON.stringify(
134 | stillMissingLayers,
135 | null,
136 | 2
137 | )}`
138 | );
139 | process.exit(1);
140 | } else {
141 | console.log("Completed with no errors");
142 | }
143 | } catch (e) {
144 | await sendToSlack(
145 | `:sos: Error running the script:\n${
146 | e?.stack || e?.message || e || "Unknown error"
147 | }`
148 | );
149 | throw e;
150 | }
151 |
--------------------------------------------------------------------------------
/src/build-one.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env zx
2 |
3 | // Builds a Node.js runtime layer for the specified Node.js version
4 | // and deploys it to the specified regions (or all regions from regions.json
5 | // if no regions are specified explicitly)
6 |
7 | const thisVersion = require("../package.json").version;
8 | let [_, __, ___, version, regions] = process.argv;
9 | if (!version) {
10 | throw new Error(
11 | "Usage: build-one.mjs {nodejs-version} [{comma-separated-list-of-aws-regions}]"
12 | );
13 | }
14 | regions = regions ? regions.split(",") : require("./regions.json");
15 | const dir = `${__dirname}/../build/${version}`;
16 | const zip = `node-${version}-everynode-${thisVersion}.zip`;
17 | const layer = `node-${version.replace(/\./g, "_")}`;
18 |
19 | // Download Node.js version and and build Lambda runtime layer ZIP
20 | await $`rm -rf ${dir}; mkdir -p ${dir}`;
21 | process.chdir(dir);
22 | await $`curl https://nodejs.org/dist/v${version}/node-v${version}-linux-x64.tar.gz --output node.tar.gz`;
23 | await $`tar -xvf node.tar.gz; mv node-v${version}-linux-x64 node`;
24 | await $`zip -j ${zip} ../../src/bootstrap; zip -r -y ${zip} node`;
25 |
26 | // For each region, upload to S3, create layer version, and set permissions
27 | const results = {};
28 | const regionDeployment = regions.map(
29 | (region) =>
30 | new Promise(async (resolve) => {
31 | const bucket = `everynode.${region}.fusebit.io`;
32 | console.log(`Deploying ${version} to ${region}...`);
33 | try {
34 | let { exitCode } = await nothrow(
35 | $`aws s3 ls s3://${bucket} --region ${region}`
36 | );
37 | if (exitCode) {
38 | await $`aws s3 mb s3://${bucket} --region ${region}`;
39 | }
40 | await $`aws s3 cp ${zip} s3://${bucket}/${zip} --region ${region}`;
41 | let { stdout: layerVersion } =
42 | await $`aws lambda publish-layer-version --layer-name ${layer} --content S3Bucket=${bucket},S3Key=${zip} --compatible-runtimes provided --description 'node@${version} @fusebit/everynode@${thisVersion}' --region ${region}`;
43 | layerVersion = JSON.parse(layerVersion);
44 | await $`aws lambda add-layer-version-permission --layer-name ${layer} --version-number ${layerVersion.Version} --principal "*" --statement-id publish --action lambda:GetLayerVersion --region ${region}`;
45 | results[region] = layerVersion.LayerVersionArn;
46 | } catch (e) {
47 | console.log(`Error deploying ${version} to ${region}`);
48 | }
49 | resolve();
50 | })
51 | );
52 | await Promise.all(regionDeployment);
53 |
54 | await $`rm -rf ${dir}`;
55 |
56 | if (process.env.SLACK_URL) {
57 | await fetch(process.env.SLACK_URL, {
58 | method: "post",
59 | body: JSON.stringify({
60 | text: `:rocket: Shipped AWS Lambda layers for Node.js *v${version}*:\n${Object.keys(
61 | results
62 | )
63 | .sort()
64 | .map((region) => `*${region}*: ${results[region]}\n`)}`,
65 | }),
66 | headers: { "Content-Type": "application/json" },
67 | });
68 | }
69 | console.log(JSON.stringify(results, null, 2));
70 |
--------------------------------------------------------------------------------
/src/delete-catalog.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env zx
2 |
3 | // Deletes catalog from CDN
4 |
5 | // Empty JSON catalog
6 | let upload = $`aws s3 cp - s3://fusebit-io-cdn/everynode/layers.json --content-type application/json --cache-control no-cache --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers full=id=332f10bace808ea274aecc80e667990adc92dc993a597a42622105dc1f0050bf`;
7 | upload.stdin.write("{}");
8 | upload.stdin.end();
9 | await upload;
10 |
11 | // Empty TXT catalog
12 | upload = $`aws s3 cp - s3://fusebit-io-cdn/everynode/layers.txt --content-type text/plain --cache-control no-cache --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers full=id=332f10bace808ea274aecc80e667990adc92dc993a597a42622105dc1f0050bf`;
13 | upload.stdin.write("");
14 | upload.stdin.end();
15 | await upload;
16 |
--------------------------------------------------------------------------------
/src/regions.json:
--------------------------------------------------------------------------------
1 | [
2 | "us-east-2",
3 | "us-east-1",
4 | "us-west-1",
5 | "us-west-2",
6 | "af-south-1",
7 | "ap-east-1",
8 | "ap-southeast-3",
9 | "ap-south-1",
10 | "ap-northeast-3",
11 | "ap-northeast-2",
12 | "ap-southeast-1",
13 | "ap-southeast-2",
14 | "ap-northeast-1",
15 | "ca-central-1",
16 | "eu-central-1",
17 | "eu-west-1",
18 | "eu-west-2",
19 | "eu-south-1",
20 | "eu-west-3",
21 | "eu-north-1",
22 | "me-south-1",
23 | "sa-east-1"
24 | ]
25 |
--------------------------------------------------------------------------------
/test/bootstrap.test.js:
--------------------------------------------------------------------------------
1 | const createRuntimeApi = require("./runtime");
2 | const Superagent = require("superagent");
3 | const Path = require("path");
4 | const spawn = require("child_process").spawn;
5 |
6 | const itif = (condition) => (condition ? it : it.skip);
7 | const major = +process.versions.node.split(".")[0];
8 |
9 | const successResult = {
10 | method: "POST",
11 | path: "/2018-06-01/runtime/invocation/1234/response",
12 | headers: {
13 | "content-type": "application/json",
14 | },
15 | payload: {
16 | env: {
17 | _HANDLER: expect.stringContaining(".handler"),
18 | _X_AMZN_TRACE_ID: "Lambda-Runtime-Trace-Id",
19 | },
20 | event: {
21 | foo: "bar",
22 | },
23 | context: {
24 | deadlineMs: 5678,
25 | invokedFunctionArn: "Lambda-Runtime-Invoked-Function-Arn",
26 | awsRequestId: "1234",
27 | },
28 | },
29 | };
30 |
31 | const syncErrorResult = {
32 | method: "POST",
33 | path: "/2018-06-01/runtime/invocation/1234/error",
34 | headers: {
35 | "lambda-runtime-function-error-type": "Handler.Error",
36 | "content-type": "application/json",
37 | },
38 | payload: {
39 | errorMessage: "An Error",
40 | errorType: "Handler.Error",
41 | stackTrace: expect.arrayContaining(["Error: An Error"]),
42 | },
43 | };
44 |
45 | const asyncErrorResult = {
46 | method: "POST",
47 | path: "/2018-06-01/runtime/invocation/1234/error",
48 | headers: {
49 | "lambda-runtime-function-error-type": "Handler.UnhandledError",
50 | "content-type": "application/json",
51 | },
52 | payload: {
53 | errorMessage: "An Error",
54 | errorType: "Handler.UnhandledError",
55 | stackTrace: expect.arrayContaining(["Error: An Error"]),
56 | },
57 | };
58 |
59 | const runBootstrap = async (env) => {
60 | return new Promise((resolve, reject) => {
61 | try {
62 | // console.log("RUNNING BOOTSTRAP", process.argv[0], env);
63 | const child = spawn(
64 | process.argv[0],
65 | [Path.join(__dirname, "..", "src", "bootstrap")],
66 | {
67 | env,
68 | stdio: ["pipe", "inherit", "inherit"],
69 | }
70 | );
71 | let isDone = false;
72 | let timeout = setTimeout(() => !isDone && child.kill(), 500);
73 | const done = (error) => {
74 | if (isDone) return;
75 | clearTimeout(timeout);
76 | timeout = undefined;
77 | isDone = true;
78 | return error ? reject(error) : resolve();
79 | };
80 | child.on("exit", () => done());
81 | child.on("error", (e) => done(e));
82 | } catch (e) {
83 | reject(e);
84 | }
85 | });
86 | };
87 |
88 | describe("Bootstrap", () => {
89 | let runtimeApi;
90 |
91 | const createEnv = (handler, es6) => {
92 | const env = {
93 | _HANDLER: `${handler}.handler`,
94 | LAMBDA_TASK_ROOT: Path.join(__dirname, "handlers", es6 ? "es6" : ""),
95 | AWS_LAMBDA_RUNTIME_API: runtimeApi.host,
96 | _X_AMZN_TRACE_ID: "1234",
97 | };
98 | return env;
99 | };
100 |
101 | beforeAll(async () => {
102 | runtimeApi = await createRuntimeApi();
103 | });
104 |
105 | afterAll(async () => {
106 | runtimeApi && runtimeApi.close();
107 | });
108 |
109 | afterEach(async () => {
110 | runtimeApi.reset();
111 | });
112 |
113 | test("asyncHelloWorld", async () => {
114 | const env = createEnv("asyncHelloWorld");
115 | await runBootstrap(env);
116 | const trace = runtimeApi.getTrace();
117 | // console.log("TRACE", JSON.stringify(trace, null, 2));
118 | expect(Array.isArray(trace)).toBe(true);
119 | expect(trace.length).toBe(2);
120 | expect(trace[0]).toMatchObject({
121 | request: { method: "GET" },
122 | response: { status: 200 },
123 | });
124 | expect(trace[1]).toMatchObject({
125 | request: successResult,
126 | response: { status: 202 },
127 | });
128 | });
129 |
130 | itif(major >= 14)("asyncHelloWorld ES6", async () => {
131 | const env = createEnv("asyncHelloWorld", true);
132 | await runBootstrap(env);
133 | const trace = runtimeApi.getTrace();
134 | // console.log("TRACE", JSON.stringify(trace, null, 2));
135 | expect(Array.isArray(trace)).toBe(true);
136 | expect(trace.length).toBe(2);
137 | expect(trace[0]).toMatchObject({
138 | request: { method: "GET" },
139 | response: { status: 200 },
140 | });
141 | expect(trace[1]).toMatchObject({
142 | request: successResult,
143 | response: { status: 202 },
144 | });
145 | });
146 |
147 | test("two events", async () => {
148 | runtimeApi.reset([
149 | { payload: { eventNo: 1 } },
150 | { payload: { eventNo: 2 } },
151 | ]);
152 | const env = createEnv("asyncHelloWorld");
153 | await runBootstrap(env);
154 | const trace = runtimeApi.getTrace();
155 | // console.log("TRACE", JSON.stringify(trace, null, 2));
156 | expect(Array.isArray(trace)).toBe(true);
157 | expect(trace.length).toBe(4);
158 | expect(trace[0]).toMatchObject({
159 | request: { method: "GET" },
160 | response: { status: 200 },
161 | });
162 | expect(trace[1]).toMatchObject({
163 | request: {
164 | ...successResult,
165 | payload: { ...successResult.payload, event: { eventNo: 1 } },
166 | },
167 | response: { status: 202 },
168 | });
169 | expect(trace[2]).toMatchObject({
170 | request: { method: "GET" },
171 | response: { status: 200 },
172 | });
173 | expect(trace[3]).toMatchObject({
174 | request: {
175 | ...successResult,
176 | payload: { ...successResult.payload, event: { eventNo: 2 } },
177 | },
178 | response: { status: 202 },
179 | });
180 | });
181 |
182 | test("syncHelloWorld", async () => {
183 | const env = createEnv("syncHelloWorld");
184 | await runBootstrap(env);
185 | const trace = runtimeApi.getTrace();
186 | // console.log("TRACE", JSON.stringify(trace, null, 2));
187 | expect(Array.isArray(trace)).toBe(true);
188 | expect(trace.length).toBe(2);
189 | expect(trace[0]).toMatchObject({
190 | request: { method: "GET" },
191 | response: { status: 200 },
192 | });
193 | expect(trace[1]).toMatchObject({
194 | request: successResult,
195 | response: { status: 202 },
196 | });
197 | });
198 |
199 | itif(major >= 14)("syncHelloWorld ES6", async () => {
200 | const env = createEnv("syncHelloWorld", true);
201 | await runBootstrap(env);
202 | const trace = runtimeApi.getTrace();
203 | // console.log("TRACE", JSON.stringify(trace, null, 2));
204 | expect(Array.isArray(trace)).toBe(true);
205 | expect(trace.length).toBe(2);
206 | expect(trace[0]).toMatchObject({
207 | request: { method: "GET" },
208 | response: { status: 200 },
209 | });
210 | expect(trace[1]).toMatchObject({
211 | request: successResult,
212 | response: { status: 202 },
213 | });
214 | });
215 |
216 | test("asyncWithSyncException", async () => {
217 | const env = createEnv("asyncWithSyncException");
218 | await runBootstrap(env);
219 | const trace = runtimeApi.getTrace();
220 | // console.log("TRACE", JSON.stringify(trace, null, 2));
221 | expect(Array.isArray(trace)).toBe(true);
222 | expect(trace.length).toBe(2);
223 | expect(trace[0]).toMatchObject({
224 | request: { method: "GET" },
225 | response: { status: 200 },
226 | });
227 | expect(trace[1]).toMatchObject({
228 | request: syncErrorResult,
229 | response: { status: 202 },
230 | });
231 | });
232 |
233 | test("syncWithSyncException", async () => {
234 | const env = createEnv("syncWithSyncException");
235 | await runBootstrap(env);
236 | const trace = runtimeApi.getTrace();
237 | // console.log("TRACE", JSON.stringify(trace, null, 2));
238 | expect(Array.isArray(trace)).toBe(true);
239 | expect(trace.length).toBe(2);
240 | expect(trace[0]).toMatchObject({
241 | request: { method: "GET" },
242 | response: { status: 200 },
243 | });
244 | expect(trace[1]).toMatchObject({
245 | request: syncErrorResult,
246 | response: { status: 202 },
247 | });
248 | });
249 |
250 | test("asyncWithAsyncException", async () => {
251 | const env = createEnv("asyncWithAsyncException");
252 | await runBootstrap(env);
253 | const trace = runtimeApi.getTrace();
254 | // console.log("TRACE", JSON.stringify(trace, null, 2));
255 | expect(Array.isArray(trace)).toBe(true);
256 | expect(trace.length).toBe(2);
257 | expect(trace[0]).toMatchObject({
258 | request: { method: "GET" },
259 | response: { status: 200 },
260 | });
261 | expect(trace[1]).toMatchObject({
262 | request: asyncErrorResult,
263 | response: { status: 202 },
264 | });
265 | });
266 |
267 | test("syncWithAsyncException", async () => {
268 | const env = createEnv("syncWithAsyncException");
269 | await runBootstrap(env);
270 | const trace = runtimeApi.getTrace();
271 | // console.log("TRACE", JSON.stringify(trace, null, 2));
272 | expect(Array.isArray(trace)).toBe(true);
273 | expect(trace.length).toBe(2);
274 | expect(trace[0]).toMatchObject({
275 | request: { method: "GET" },
276 | response: { status: 200 },
277 | });
278 | expect(trace[1]).toMatchObject({
279 | request: asyncErrorResult,
280 | response: { status: 202 },
281 | });
282 | });
283 |
284 | test("missingHandlerFile", async () => {
285 | const env = createEnv("missingHandlerFile");
286 | await runBootstrap(env);
287 | const trace = runtimeApi.getTrace();
288 | // console.log("TRACE", JSON.stringify(trace, null, 2));
289 | expect(Array.isArray(trace)).toBe(true);
290 | expect(trace.length).toBe(1);
291 | expect(trace[0]).toMatchObject({
292 | request: {
293 | method: "POST",
294 | path: "/2018-06-01/runtime/init/error",
295 | headers: {
296 | "lambda-runtime-function-error-type": "Runtime.ErrorLoadingHandler",
297 | "content-type": "application/json",
298 | },
299 | payload: {
300 | errorMessage: expect.stringContaining(
301 | "Error loading handler 'missingHandlerFile.handler': Cannot find module"
302 | ),
303 | errorType: "Runtime.ErrorLoadingHandler",
304 | stackTrace: expect.arrayContaining([
305 | expect.stringContaining(
306 | "Error loading handler 'missingHandlerFile.handler': Cannot find module"
307 | ),
308 | ]),
309 | },
310 | },
311 | response: { status: 202 },
312 | });
313 | });
314 |
315 | test("missingHandlerExport", async () => {
316 | const env = createEnv("missingHandlerExport");
317 | await runBootstrap(env);
318 | const trace = runtimeApi.getTrace();
319 | // console.log("TRACE", JSON.stringify(trace, null, 2));
320 | expect(Array.isArray(trace)).toBe(true);
321 | expect(trace.length).toBe(1);
322 | expect(trace[0]).toMatchObject({
323 | request: {
324 | method: "POST",
325 | path: "/2018-06-01/runtime/init/error",
326 | headers: {
327 | "lambda-runtime-function-error-type": "Runtime.WrongHandlerType",
328 | "content-type": "application/json",
329 | },
330 | payload: {
331 | errorMessage: expect.stringContaining(
332 | "Error loading handler 'missingHandlerExport.handler': The handler 'handler' is not a function: undefined"
333 | ),
334 | errorType: "Runtime.WrongHandlerType",
335 | stackTrace: expect.arrayContaining([
336 | expect.stringContaining(
337 | "Error loading handler 'missingHandlerExport.handler': The handler 'handler' is not a function: undefined"
338 | ),
339 | ]),
340 | },
341 | },
342 | response: { status: 202 },
343 | });
344 | });
345 |
346 | test("syncHandlerReturnsPromise", async () => {
347 | const env = createEnv("syncHandlerReturnsPromise");
348 | await runBootstrap(env);
349 | const trace = runtimeApi.getTrace();
350 | // console.log("TRACE", JSON.stringify(trace, null, 2));
351 | expect(Array.isArray(trace)).toBe(true);
352 | expect(trace.length).toBe(2);
353 | expect(trace[0]).toMatchObject({
354 | request: { method: "GET" },
355 | response: { status: 200 },
356 | });
357 | expect(trace[1]).toMatchObject({
358 | request: successResult,
359 | response: { status: 202 },
360 | });
361 | });
362 |
363 | test("handlerInitializationError", async () => {
364 | const env = createEnv("handlerInitializationError");
365 | await runBootstrap(env);
366 | const trace = runtimeApi.getTrace();
367 | // console.log("TRACE", JSON.stringify(trace, null, 2));
368 | expect(Array.isArray(trace)).toBe(true);
369 | expect(trace.length).toBe(1);
370 | expect(trace[0]).toMatchObject({
371 | request: {
372 | method: "POST",
373 | path: "/2018-06-01/runtime/init/error",
374 | headers: {
375 | "lambda-runtime-function-error-type": "Runtime.ErrorLoadingHandler",
376 | "content-type": "application/json",
377 | },
378 | payload: {
379 | errorMessage: expect.stringContaining(
380 | "Error loading handler 'handlerInitializationError.handler': An Error"
381 | ),
382 | errorType: "Runtime.ErrorLoadingHandler",
383 | stackTrace: expect.arrayContaining([
384 | expect.stringContaining(
385 | "Error loading handler 'handlerInitializationError.handler': An Error"
386 | ),
387 | ]),
388 | },
389 | },
390 | response: { status: 202 },
391 | });
392 | });
393 | });
394 |
--------------------------------------------------------------------------------
/test/handlers/asyncHelloWorld.js:
--------------------------------------------------------------------------------
1 | exports.handler = async (event, context) => {
2 | return { env: process.env, event, context };
3 | };
4 |
--------------------------------------------------------------------------------
/test/handlers/asyncWithAsyncException.js:
--------------------------------------------------------------------------------
1 | exports.handler = async (event, context) => {
2 | return new Promise((resolve, reject) => {
3 | setTimeout(() => {
4 | throw new Error("An Error");
5 | }, 10);
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/test/handlers/asyncWithSyncException.js:
--------------------------------------------------------------------------------
1 | exports.handler = async (event, context) => {
2 | throw new Error("An Error");
3 | };
4 |
--------------------------------------------------------------------------------
/test/handlers/es6/asyncHelloWorld.js:
--------------------------------------------------------------------------------
1 | export async function handler(event, context) {
2 | return { env: process.env, event, context };
3 | }
4 |
--------------------------------------------------------------------------------
/test/handlers/es6/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/test/handlers/es6/syncHelloWorld.js:
--------------------------------------------------------------------------------
1 | export function handler(event, context, callback) {
2 | return callback(null, { env: process.env, event, context });
3 | }
4 |
--------------------------------------------------------------------------------
/test/handlers/function.js:
--------------------------------------------------------------------------------
1 | exports.handler = (event, context, callback) => {
2 | callback(null, { message: `Hello from Node ${process.version}` });
3 | };
4 |
--------------------------------------------------------------------------------
/test/handlers/handlerInitializationError.js:
--------------------------------------------------------------------------------
1 | exports.handler = (event, context, callback) => {
2 | return callback(null, { env: process.env, event, context });
3 | };
4 |
5 | throw new Error("An Error");
6 |
--------------------------------------------------------------------------------
/test/handlers/missingHandlerExport.js:
--------------------------------------------------------------------------------
1 | exports.somerandommethod = () => {};
2 |
--------------------------------------------------------------------------------
/test/handlers/syncHandlerReturnsPromise.js:
--------------------------------------------------------------------------------
1 | exports.handler = (event, context) => {
2 | return new Promise((r) => r({ env: process.env, event, context }));
3 | };
4 |
--------------------------------------------------------------------------------
/test/handlers/syncHandlerWrongArity.js:
--------------------------------------------------------------------------------
1 | exports.handler = (event, context) => {};
2 |
--------------------------------------------------------------------------------
/test/handlers/syncHelloWorld.js:
--------------------------------------------------------------------------------
1 | exports.handler = (event, context, callback) => {
2 | return callback(null, { env: process.env, event, context });
3 | };
4 |
--------------------------------------------------------------------------------
/test/handlers/syncWithAsyncException.js:
--------------------------------------------------------------------------------
1 | exports.handler = (event, context, callback) => {
2 | setTimeout(() => {
3 | throw new Error("An Error");
4 | }, 10);
5 | };
6 |
--------------------------------------------------------------------------------
/test/handlers/syncWithSyncException.js:
--------------------------------------------------------------------------------
1 | exports.handler = (event, context, callback) => {
2 | throw new Error("An Error");
3 | };
4 |
--------------------------------------------------------------------------------
/test/runtime.js:
--------------------------------------------------------------------------------
1 | const Express = require("express");
2 | const Http = require("http");
3 |
4 | const defaultHeaders = {
5 | "Lambda-Runtime-Aws-Request-Id": "1234",
6 | "Lambda-Runtime-Deadline-Ms": "5678",
7 | "Lambda-Runtime-Invoked-Function-Arn": "Lambda-Runtime-Invoked-Function-Arn",
8 | "Lambda-Runtime-Trace-Id": "Lambda-Runtime-Trace-Id",
9 | "Lambda-Runtime-Client-Context": "Lambda-Runtime-Client-Context",
10 | "Lambda-Runtime-Cognito-Identity": "Lambda-Runtime-Cognito-Identity",
11 | };
12 | const defaultPayload = { foo: "bar" };
13 |
14 | module.exports = async () => {
15 | const app = Express();
16 | let trace = [];
17 | let events = [{ payload: defaultPayload }];
18 |
19 | app.get("/test", (req, res) => res.json({ ok: true }));
20 |
21 | app.get("/2018-06-01/runtime/invocation/next", (req, res) => {
22 | const event = events.shift();
23 | if (!event) {
24 | // No more events to send, "hang" the request
25 | return;
26 | }
27 | const headers = { ...defaultHeaders, ...event.headers };
28 | const payload = event.payload;
29 | trace.push({
30 | request: { method: "GET", path: req.path },
31 | response: { status: 200, headers, payload },
32 | });
33 | res.set(headers);
34 | res.json(payload);
35 | });
36 |
37 | app.post(
38 | [
39 | "/2018-06-01/runtime/invocation/:requestId/response",
40 | "/2018-06-01/runtime/invocation/:requestId/error",
41 | "/2018-06-01/runtime/init/error",
42 | ],
43 | Express.json(),
44 | (req, res) => {
45 | trace.push({
46 | request: {
47 | method: "POST",
48 | path: req.path,
49 | headers: req.headers,
50 | payload: req.body,
51 | },
52 | response: { status: 202 },
53 | });
54 | res.status(202);
55 | res.end();
56 | }
57 | );
58 |
59 | let tmp = Http.createServer(app);
60 | return new Promise((resolve, reject) => {
61 | tmp.on("error", reject);
62 | const port = Math.floor(Math.random() * 1000) + 3000;
63 | tmp.listen(port, () => {
64 | tmp.removeListener("error", reject);
65 | resolve({
66 | host: `localhost:${port}`,
67 | close: () => tmp.close(),
68 | getTrace: () => trace,
69 | reset: (newEvents) => {
70 | trace = [];
71 | events = newEvents || [{ payload: defaultPayload }];
72 | },
73 | defaultPayload,
74 | defaultHeaders,
75 | });
76 | });
77 | });
78 | };
79 |
--------------------------------------------------------------------------------
/test/runtime.test.js:
--------------------------------------------------------------------------------
1 | const createRuntimeApi = require("./runtime");
2 | const Superagent = require("superagent");
3 |
4 | describe("Runtime API", () => {
5 | let runtimeApi;
6 |
7 | beforeAll(async () => {
8 | runtimeApi = await createRuntimeApi();
9 | });
10 |
11 | afterAll(async () => {
12 | runtimeApi && runtimeApi.close();
13 | });
14 |
15 | afterEach(async () => {
16 | runtimeApi.reset();
17 | });
18 |
19 | test("Runtime API service works", async () => {
20 | const url = `http://${runtimeApi.host}/test`;
21 | const response = await Superagent.get(url);
22 | expect(response.status).toBe(200);
23 | expect(response.body).toMatchObject({ ok: true });
24 | });
25 |
26 | test("Runtime API get invocation works", async () => {
27 | const url = `http://${runtimeApi.host}/2018-06-01/runtime/invocation/next`;
28 | const headers = runtimeApi.defaultHeaders;
29 | const payload = runtimeApi.defaultPayload;
30 | const headersLowecase = {};
31 | Object.keys(headers).forEach(
32 | (h) => (headersLowecase[h.toLowerCase()] = headers[h])
33 | );
34 | const response = await Superagent.get(url);
35 | expect(response.status).toBe(200);
36 | expect(response.headers).toMatchObject(headersLowecase);
37 | expect(response.body).toMatchObject(payload);
38 | const trace = runtimeApi.getTrace();
39 | expect(Array.isArray(trace)).toBe(true);
40 | expect(trace.length).toBe(1);
41 | expect(trace[0]).toMatchObject({
42 | request: { method: "GET" },
43 | response: { status: 200, headers, payload },
44 | });
45 | });
46 |
47 | test("Runtime API invocation response works", async () => {
48 | const url = `http://${runtimeApi.host}/2018-06-01/runtime/invocation/1234/response`;
49 | const payload = { result: "baz" };
50 | const response = await Superagent.post(url).send(payload);
51 | expect(response.status).toBe(202);
52 | const trace = runtimeApi.getTrace();
53 | expect(Array.isArray(trace)).toBe(true);
54 | expect(trace.length).toBe(1);
55 | expect(trace[0]).toMatchObject({
56 | request: { method: "POST", payload },
57 | response: { status: 202 },
58 | });
59 | });
60 |
61 | test("Runtime API invocation error works", async () => {
62 | const url = `http://${runtimeApi.host}/2018-06-01/runtime/invocation/1234/error`;
63 | const headers = {
64 | "Lambda-Runtime-Function-Error-Type": "Runtime.NoSuchHandler",
65 | };
66 | const headersLowercase = {};
67 | Object.keys(headers).forEach(
68 | (h) => (headersLowercase[h.toLowerCase()] = headers[h])
69 | );
70 | const payload = {
71 | errorMessage: "Error parsing event data.",
72 | errorType: "InvalidEventDataException",
73 | stackTrace: [],
74 | };
75 | const response = await Superagent.post(url).set(headers).send(payload);
76 | expect(response.status).toBe(202);
77 | const trace = runtimeApi.getTrace();
78 | expect(Array.isArray(trace)).toBe(true);
79 | expect(trace.length).toBe(1);
80 | expect(trace[0]).toMatchObject({
81 | request: { method: "POST", payload, headers: headersLowercase },
82 | response: { status: 202 },
83 | });
84 | });
85 |
86 | test("Runtime API init error works", async () => {
87 | const url = `http://${runtimeApi.host}/2018-06-01/runtime/init/error`;
88 | const headers = {
89 | "Lambda-Runtime-Function-Error-Type": "Runtime.NoSuchHandler",
90 | };
91 | const headersLowercase = {};
92 | Object.keys(headers).forEach(
93 | (h) => (headersLowercase[h.toLowerCase()] = headers[h])
94 | );
95 | const payload = {
96 | errorMessage: "Error parsing event data.",
97 | errorType: "InvalidEventDataException",
98 | stackTrace: [],
99 | };
100 | const response = await Superagent.post(url).set(headers).send(payload);
101 | expect(response.status).toBe(202);
102 | const trace = runtimeApi.getTrace();
103 | expect(Array.isArray(trace)).toBe(true);
104 | expect(trace.length).toBe(1);
105 | expect(trace[0]).toMatchObject({
106 | request: { method: "POST", payload, headers: headersLowercase },
107 | response: { status: 202 },
108 | });
109 | });
110 | });
111 |
--------------------------------------------------------------------------------