├── .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 | Fusebit 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 | Run Any Node.js Version in AWS Lambda 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 | --------------------------------------------------------------------------------