├── .yarnrc ├── test ├── functions │ ├── python-init-error │ │ ├── handler.py │ │ └── out │ ├── provided-bash-echo │ │ ├── handler.sh │ │ └── bootstrap │ ├── nodejs-env.zip │ ├── nodejs-env │ │ └── index.js │ ├── nodejs-pid │ │ └── index.js │ ├── nodejs-init-error │ │ ├── handler.js │ │ └── out │ ├── nodejs-echo │ │ └── handler.js │ ├── nodejs-version │ │ └── handler.js │ ├── nodejs-handled-error │ │ └── handler.js │ ├── nodejs-exit │ │ └── handler.js │ ├── python-hello │ │ └── hello.py │ ├── python-version │ │ └── handler.py │ ├── nodejs-eval │ │ └── handler.js │ ├── go-echo │ │ └── handler.go │ └── provided-bash-invalid-version │ │ └── bootstrap ├── go-build.sh ├── pkg-invoke.js └── test.ts ├── src ├── providers │ ├── docker │ │ └── index.ts │ ├── index.ts │ └── native │ │ └── index.ts ├── runtimes │ ├── provided │ │ ├── bootstrap │ │ └── bootstrap.ts │ ├── go1.x │ │ ├── bootstrap.ts │ │ ├── index.ts │ │ └── bootstrap.go │ ├── nodejs6.10 │ │ ├── bootstrap.ts │ │ ├── bootstrap │ │ └── index.ts │ ├── nodejs8.10 │ │ ├── bootstrap.ts │ │ ├── bootstrap │ │ └── index.ts │ ├── python2.7 │ │ ├── bootstrap.ts │ │ ├── bootstrap │ │ └── index.ts │ ├── python3.6 │ │ ├── bootstrap.ts │ │ ├── bootstrap │ │ └── index.ts │ ├── python3.7 │ │ ├── bootstrap.ts │ │ ├── bootstrap │ │ └── index.ts │ ├── python │ │ ├── bootstrap │ │ ├── bootstrap.ts │ │ └── bootstrap.py │ └── nodejs │ │ ├── bootstrap │ │ └── bootstrap.ts ├── once.ts ├── deferred.ts ├── errors.ts ├── install-python.ts ├── install-node.ts ├── types.ts ├── unzip.ts ├── index.ts ├── runtime-server.ts └── runtimes.ts ├── util ├── cp.sh ├── python │ ├── copy.sh │ ├── build-all.sh │ ├── cpython-enable-openssl.patch │ └── Dockerfile ├── pack.sh └── build.sh ├── example └── dump │ └── index.js ├── .circleci ├── install-go.sh └── config.yml ├── .gitignore ├── tsconfig.json ├── err.py ├── azure-pipelines.yml ├── .editorconfig ├── package.json └── README.md /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix "" 2 | -------------------------------------------------------------------------------- /test/functions/python-init-error/handler.py: -------------------------------------------------------------------------------- 1 | 10 * (1/0) 2 | -------------------------------------------------------------------------------- /src/providers/docker/index.ts: -------------------------------------------------------------------------------- 1 | export default function createProvider() {} 2 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | import native from './native'; 2 | export { native }; 3 | -------------------------------------------------------------------------------- /test/functions/python-init-error/out: -------------------------------------------------------------------------------- 1 | {"errorMessage": "module initialization error"} -------------------------------------------------------------------------------- /test/functions/provided-bash-echo/handler.sh: -------------------------------------------------------------------------------- 1 | handler () { 2 | set -e 3 | echo "$1" >&2 4 | } 5 | -------------------------------------------------------------------------------- /test/functions/nodejs-env.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/fun/master/test/functions/nodejs-env.zip -------------------------------------------------------------------------------- /util/cp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | mkdir -p "$2/$(dirname "$1")" 4 | cp -a "$1" "$2/$1" 5 | -------------------------------------------------------------------------------- /test/functions/nodejs-env/index.js: -------------------------------------------------------------------------------- 1 | exports.env = (event, context, callback) => { 2 | callback(null, process.env); 3 | }; 4 | -------------------------------------------------------------------------------- /test/functions/nodejs-pid/index.js: -------------------------------------------------------------------------------- 1 | exports.pid = (event, context, callback) => { 2 | callback(null, process.pid); 3 | }; 4 | -------------------------------------------------------------------------------- /test/functions/nodejs-init-error/handler.js: -------------------------------------------------------------------------------- 1 | // This test immediately throws an error upon bootup 2 | throw new Error('I crashed!'); 3 | -------------------------------------------------------------------------------- /src/runtimes/provided/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Delegate out to the provided `bootstrap` file 3 | exec "$LAMBDA_TASK_ROOT/bootstrap" "$@" 4 | -------------------------------------------------------------------------------- /test/functions/nodejs-echo/handler.js: -------------------------------------------------------------------------------- 1 | exports.handler = (event, context, callback) => { 2 | callback(null, { event, context }); 3 | }; 4 | -------------------------------------------------------------------------------- /test/functions/nodejs-version/handler.js: -------------------------------------------------------------------------------- 1 | exports.handler = (event, context, callback) => { 2 | callback(null, process.versions); 3 | }; 4 | -------------------------------------------------------------------------------- /test/functions/nodejs-handled-error/handler.js: -------------------------------------------------------------------------------- 1 | exports.handler = (event, context, callback) => { 2 | callback(new Error('handled error')); 3 | }; 4 | -------------------------------------------------------------------------------- /test/functions/nodejs-exit/handler.js: -------------------------------------------------------------------------------- 1 | exports.handler = ({ exit }, context) => { 2 | if (exit) { 3 | process.exit(1); 4 | } else { 5 | return { hi: true}; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /example/dump/index.js: -------------------------------------------------------------------------------- 1 | exports.handler = function (event, context) { 2 | return { isResponse: true, event, context, env: process.env, versions: process.versions, date: new Date().toString() }; 3 | }; 4 | -------------------------------------------------------------------------------- /src/runtimes/go1.x/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { spawn } from 'child_process'; 3 | 4 | const bootstrap = join(__dirname, 'bootstrap'); 5 | spawn(bootstrap, [], { stdio: 'inherit' }); 6 | -------------------------------------------------------------------------------- /test/functions/python-hello/hello.py: -------------------------------------------------------------------------------- 1 | def hello_handler(event, context): 2 | message = 'Hello {} {}!'.format(event['first_name'], event['last_name']) 3 | return { 4 | 'message' : message 5 | } 6 | -------------------------------------------------------------------------------- /test/go-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | export GOPATH="$HOME/go" 4 | go get github.com/aws/aws-lambda-go/lambda 5 | go build -o test/functions/go-echo/handler test/functions/go-echo/handler.go 6 | -------------------------------------------------------------------------------- /test/functions/python-version/handler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import platform 3 | 4 | def handler(event, context): 5 | return { 6 | 'sys.version': sys.version, 7 | 'platform.python_version': platform.python_version() 8 | } 9 | -------------------------------------------------------------------------------- /.circleci/install-go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | if [ "$(uname)" = "Darwin" ]; then 4 | platform=darwin 5 | else 6 | platform=linux 7 | fi 8 | curl -sfLS "https://dl.google.com/go/go1.12.$platform-amd64.tar.gz" | tar zxv --strip-components=1 -C /usr/local 9 | -------------------------------------------------------------------------------- /test/functions/nodejs-eval/handler.js: -------------------------------------------------------------------------------- 1 | exports.handler = ({ error, code }, context, callback) => { 2 | if (typeof error === 'string') { 3 | callback(new Error(error)); 4 | } else { 5 | const result = eval(code); 6 | callback(null, { code, result }); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/functions/go-echo/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/aws/aws-lambda-go/lambda" 5 | ) 6 | 7 | func HandleEvent(v interface{}) (interface{}, error) { 8 | return v, nil 9 | } 10 | 11 | func main() { 12 | lambda.Start(HandleEvent) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | handler 2 | node_modules 3 | __pycache__ 4 | *.swp 5 | /dist 6 | /?.js 7 | /examples/*/*.zip 8 | /test/functions/*/*.zip 9 | /test/functions/*/*.pyc 10 | /test/functions/*/out 11 | /.nyc_output 12 | /zeit-fun-*.tgz 13 | /pkg-invoke 14 | /util/python/python-binaries 15 | -------------------------------------------------------------------------------- /src/runtimes/nodejs6.10/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { spawn } from 'child_process'; 3 | 4 | const nodeBin = join(__dirname, 'bin', 'node'); 5 | const bootstrap = join(__dirname, '..', 'nodejs', 'bootstrap.js'); 6 | spawn(nodeBin, [ bootstrap ], { stdio: 'inherit' }); 7 | -------------------------------------------------------------------------------- /src/runtimes/nodejs8.10/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { spawn } from 'child_process'; 3 | 4 | const nodeBin = join(__dirname, 'bin', 'node'); 5 | const bootstrap = join(__dirname, '..', 'nodejs', 'bootstrap.js'); 6 | spawn(nodeBin, [ bootstrap ], { stdio: 'inherit' }); 7 | -------------------------------------------------------------------------------- /util/python/copy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | cd /binaries 5 | tarball="python-${PYTHON_VERSION}-linux-x64.tar" 6 | tar cvf "$tarball" "python-${PYTHON_VERSION}" 7 | 8 | echo Gzipping "$tarball" 9 | gzip -9 "$tarball" 10 | 11 | mv -v "$tarball.gz" /python-binaries 12 | -------------------------------------------------------------------------------- /src/runtimes/python2.7/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { spawn } from 'child_process'; 3 | 4 | const pythonBin = join(__dirname, 'bin', 'python'); 5 | const bootstrap = join(__dirname, '..', 'python', 'bootstrap.py'); 6 | spawn(pythonBin, [ bootstrap ], { stdio: 'inherit' }); 7 | -------------------------------------------------------------------------------- /src/runtimes/python3.6/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { spawn } from 'child_process'; 3 | 4 | const pythonBin = join(__dirname, 'bin', 'python'); 5 | const bootstrap = join(__dirname, '..', 'python', 'bootstrap.py'); 6 | spawn(pythonBin, [ bootstrap ], { stdio: 'inherit' }); 7 | -------------------------------------------------------------------------------- /src/runtimes/python3.7/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { spawn } from 'child_process'; 3 | 4 | const pythonBin = join(__dirname, 'bin', 'python'); 5 | const bootstrap = join(__dirname, '..', 'python', 'bootstrap.py'); 6 | spawn(pythonBin, [ bootstrap ], { stdio: 'inherit' }); 7 | -------------------------------------------------------------------------------- /src/runtimes/provided/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { spawn } from 'child_process'; 3 | 4 | // Delegate out to the provided `bootstrap` file within the lambda 5 | const bootstrap = join(process.env.LAMBDA_TASK_ROOT, 'bootstrap'); 6 | spawn(bootstrap, [], { stdio: 'inherit' }); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "sourceMap": true, 7 | "declaration": true 8 | }, 9 | "include": [ 10 | "src/**/*", 11 | "test/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /util/pack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # This is only for now-cli `now dev` progress while the `@zeit/fun` 5 | # package remains private for now. 6 | 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | cd "$DIR/.." 9 | mv README.md .readme.tmp 10 | npm pack 11 | mv .readme.tmp README.md 12 | -------------------------------------------------------------------------------- /src/runtimes/python/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # `PYTHONPATH` is *not* a restricted env var, so only set the 5 | # default one if the user did not provide one of their own 6 | if [ -z "${PYTHONPATH-}" ]; then 7 | export PYTHONPATH="$LAMBDA_RUNTIME_DIR" 8 | fi 9 | 10 | exec python "$LAMBDA_RUNTIME_DIR/bootstrap.py" 11 | -------------------------------------------------------------------------------- /test/functions/nodejs-init-error/out: -------------------------------------------------------------------------------- 1 | {"errorMessage":"I crashed!","errorType":"Error","stackTrace":["Module._compile (module.js:652:30)","Object.Module._extensions..js (module.js:663:10)","Module.load (module.js:565:32)","tryModuleLoad (module.js:505:12)","Function.Module._load (module.js:497:3)","Module.require (module.js:596:17)","require (internal/module.js:11:18)"]} -------------------------------------------------------------------------------- /src/runtimes/python2.7/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Ensure the downloaded Python version is used 5 | export PATH="$LAMBDA_RUNTIME_DIR/bin:$PATH" 6 | 7 | # Execute the "python" runtime bootstrap 8 | export LAMBDA_RUNTIME_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")"/../python >/dev/null 2>&1 && pwd )" 9 | exec "$LAMBDA_RUNTIME_DIR/bootstrap" "$@" 10 | -------------------------------------------------------------------------------- /src/runtimes/python3.6/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Ensure the downloaded Python version is used 5 | export PATH="$LAMBDA_RUNTIME_DIR/bin:$PATH" 6 | 7 | # Execute the "python" runtime bootstrap 8 | export LAMBDA_RUNTIME_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")"/../python >/dev/null 2>&1 && pwd )" 9 | exec "$LAMBDA_RUNTIME_DIR/bootstrap" "$@" 10 | -------------------------------------------------------------------------------- /src/runtimes/python3.7/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Ensure the downloaded Python version is used 5 | export PATH="$LAMBDA_RUNTIME_DIR/bin:$PATH" 6 | 7 | # Execute the "python" runtime bootstrap 8 | export LAMBDA_RUNTIME_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")"/../python >/dev/null 2>&1 && pwd )" 9 | exec "$LAMBDA_RUNTIME_DIR/bootstrap" "$@" 10 | -------------------------------------------------------------------------------- /src/runtimes/nodejs6.10/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Ensure the downloaded Node.js version is used 5 | export PATH="$LAMBDA_RUNTIME_DIR/bin:$PATH" 6 | 7 | # Execute the "nodejs" runtime bootstrap 8 | export LAMBDA_RUNTIME_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")"/../nodejs >/dev/null 2>&1 && pwd )" 9 | exec "$LAMBDA_RUNTIME_DIR/bootstrap" "$@" 10 | -------------------------------------------------------------------------------- /src/runtimes/nodejs8.10/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Ensure the downloaded Node.js version is used 5 | export PATH="$LAMBDA_RUNTIME_DIR/bin:$PATH" 6 | 7 | # Execute the "nodejs" runtime bootstrap 8 | export LAMBDA_RUNTIME_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")"/../nodejs >/dev/null 2>&1 && pwd )" 9 | exec "$LAMBDA_RUNTIME_DIR/bootstrap" "$@" 10 | -------------------------------------------------------------------------------- /src/runtimes/nodejs6.10/index.ts: -------------------------------------------------------------------------------- 1 | import { Runtime } from '../../types'; 2 | import { installNode } from '../../install-node'; 3 | import { runtimes, initializeRuntime } from '../../runtimes'; 4 | 5 | export async function init({ cacheDir }: Runtime): Promise { 6 | await Promise.all([ 7 | initializeRuntime(runtimes.nodejs), 8 | installNode(cacheDir, '6.10.0') 9 | ]); 10 | } 11 | -------------------------------------------------------------------------------- /src/runtimes/nodejs8.10/index.ts: -------------------------------------------------------------------------------- 1 | import { Runtime } from '../../types'; 2 | import { installNode } from '../../install-node'; 3 | import { runtimes, initializeRuntime } from '../../runtimes'; 4 | 5 | export async function init({ cacheDir }: Runtime): Promise { 6 | await Promise.all([ 7 | initializeRuntime(runtimes.nodejs), 8 | installNode(cacheDir, '8.10.0') 9 | ]); 10 | } 11 | -------------------------------------------------------------------------------- /src/runtimes/python2.7/index.ts: -------------------------------------------------------------------------------- 1 | import { Runtime } from '../../types'; 2 | import { installPython } from '../../install-python'; 3 | import { runtimes, initializeRuntime } from '../../runtimes'; 4 | 5 | export async function init({ cacheDir }: Runtime): Promise { 6 | await Promise.all([ 7 | initializeRuntime(runtimes.python), 8 | installPython(cacheDir, '2.7.12') 9 | ]); 10 | } 11 | -------------------------------------------------------------------------------- /src/runtimes/python3.6/index.ts: -------------------------------------------------------------------------------- 1 | import { Runtime } from '../../types'; 2 | import { installPython } from '../../install-python'; 3 | import { runtimes, initializeRuntime } from '../../runtimes'; 4 | 5 | export async function init({ cacheDir }: Runtime): Promise { 6 | await Promise.all([ 7 | initializeRuntime(runtimes.python), 8 | installPython(cacheDir, '3.6.8') 9 | ]); 10 | } 11 | -------------------------------------------------------------------------------- /src/runtimes/python3.7/index.ts: -------------------------------------------------------------------------------- 1 | import { Runtime } from '../../types'; 2 | import { installPython } from '../../install-python'; 3 | import { runtimes, initializeRuntime } from '../../runtimes'; 4 | 5 | export async function init({ cacheDir }: Runtime): Promise { 6 | await Promise.all([ 7 | initializeRuntime(runtimes.python), 8 | installPython(cacheDir, '3.7.2') 9 | ]); 10 | } 11 | -------------------------------------------------------------------------------- /util/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 4 | DIST=dist 5 | 6 | # Clean up previous build 7 | rm -rf "$DIST" 8 | 9 | echo '* Compiling TypeScript files to `.js`' >&2 10 | tsc 11 | 12 | echo '* Copying non-TypeScript files into the `dist` dir' >&2 13 | find src test -type f ! -iname '*.ts' -exec "$DIR/cp.sh" {} dist \; 14 | -------------------------------------------------------------------------------- /err.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | 4 | try: 5 | someFunction() 6 | except: 7 | ex = sys.exc_info()[1] 8 | print(ex.__class__.__name__) 9 | #print '\n'.join(traceback.format_exc().split('\n')[:-2]) 10 | #print traceback.format_stack()[0] 11 | #template = "An exception of type {0} occurred. Arguments:\n{1!r}" 12 | #message = template.format(type(ex).__name__, ex.args) 13 | #print message 14 | -------------------------------------------------------------------------------- /util/python/build-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | for version in 2.7.12 3.6.8 3.7.2; do 5 | echo "Building Python $version" 6 | 7 | # Build for Linux x64 8 | docker build \ 9 | --tag python-linux-${version} \ 10 | --build-arg PYTHON_VERSION=${version} \ 11 | . 12 | 13 | docker run \ 14 | --rm \ 15 | --volume "$PWD/python-binaries:/python-binaries" \ 16 | "python-linux-${version}" 17 | done 18 | -------------------------------------------------------------------------------- /src/once.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | export function once(emitter: EventEmitter, name: string): Promise { 4 | return new Promise((resolve, reject) => { 5 | function cleanup() {} 6 | function onEvent(arg: T) { 7 | cleanup(); 8 | resolve(arg); 9 | } 10 | function onError(err: Error) { 11 | cleanup(); 12 | reject(err); 13 | } 14 | emitter.on(name, onEvent); 15 | emitter.on('error', onError); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/runtimes/python/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { spawn } from 'child_process'; 3 | 4 | // `PYTHONPATH` is *not* a restricted env var, so only set the 5 | // default one if the user did not provide one of their own 6 | if (!process.env.PYTHONPATH) { 7 | process.env.PYTHONPATH = process.env.LAMBDA_RUNTIME_DIR; 8 | } 9 | 10 | const bootstrap = join(__dirname, 'bootstrap.py'); 11 | spawn('python', [ bootstrap ], { stdio: 'inherit' }); 12 | -------------------------------------------------------------------------------- /src/deferred.ts: -------------------------------------------------------------------------------- 1 | export interface Deferred { 2 | promise: Promise; 3 | resolve: (value?: T | PromiseLike) => void; 4 | reject: (reason?: any) => void; 5 | } 6 | 7 | export function createDeferred(): Deferred { 8 | let r; 9 | let j; 10 | const promise = new Promise( 11 | ( 12 | resolve: (value?: T | PromiseLike) => void, 13 | reject: (reason?: any) => void 14 | ): void => { 15 | r = resolve; 16 | j = reject; 17 | } 18 | ); 19 | return { promise, resolve: r, reject: j }; 20 | } 21 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'Ubuntu-16.04' 11 | 12 | steps: 13 | - script: echo Hello, world! 14 | displayName: 'Run a one-line script' 15 | 16 | - script: | 17 | echo Add other tasks to build, test, and deploy your project. 18 | echo See https://aka.ms/yaml 19 | displayName: 'Run a multi-line script' 20 | -------------------------------------------------------------------------------- /test/pkg-invoke.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { readFileSync } = require('fs'); 3 | const { createFunction } = require('../'); 4 | 5 | async function main() { 6 | let fn; 7 | try { 8 | fn = await createFunction({ 9 | Code: { 10 | Directory: join(process.cwd(), '/functions/go-echo') 11 | }, 12 | Handler: 'handler', 13 | Runtime: 'go1.x' 14 | }); 15 | const res = await fn({ hello: 'world' }); 16 | console.log(JSON.stringify(res)); 17 | } finally { 18 | if (fn) { 19 | await fn.destroy(); 20 | } 21 | } 22 | } 23 | 24 | main().catch(err => { 25 | console.error(err); 26 | process.exit(1); 27 | }); 28 | -------------------------------------------------------------------------------- /src/runtimes/nodejs/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Credit: https://github.com/lambci/node-custom-lambda/blob/master/v10.x/bootstrap 5 | 6 | # `NODE_PATH` is *not* a restricted env var, so only set the 7 | # default one if the user did not provide one of their own 8 | if [ -z "${NODE_PATH-}" ]; then 9 | export NODE_PATH="/opt/nodejs/node8/node_modules:/opt/nodejs/node_modules:${LAMBDA_RUNTIME_DIR}/node_modules:${LAMBDA_RUNTIME_DIR}:${LAMBDA_TASK_ROOT}" 10 | fi 11 | 12 | exec node \ 13 | --expose-gc \ 14 | --max-semi-space-size=$((AWS_LAMBDA_FUNCTION_MEMORY_SIZE * 5 / 100)) \ 15 | --max-old-space-size=$((AWS_LAMBDA_FUNCTION_MEMORY_SIZE * 90 / 100)) \ 16 | "$LAMBDA_RUNTIME_DIR/bootstrap.js" 17 | -------------------------------------------------------------------------------- /util/python/cpython-enable-openssl.patch: -------------------------------------------------------------------------------- 1 | --- a/Modules/Setup 2015-03-15 07:33:09.093498063 +0000 2 | +++ b/Modules/Setup 2015-03-15 07:33:43.436659171 +0000 3 | @@ -204,10 +204,10 @@ 4 | 5 | # Socket module helper for SSL support; you must comment out the other 6 | # socket line above, and possibly edit the SSL variable: 7 | -#SSL=/usr/local/ssl 8 | -#_ssl _ssl.c \ 9 | -# -DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl \ 10 | -# -L$(SSL)/lib -lssl -lcrypto 11 | +SSL=/build/openssl-TKTK 12 | +_ssl _ssl.c \ 13 | + -DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl \ 14 | + -L$(SSL) -lssl -lcrypto 15 | 16 | # The crypt module is now disabled by default because it breaks builds 17 | # on many systems (where -lcrypt is needed), e.g. Linux (I believe). 18 | -------------------------------------------------------------------------------- /util/python/Dockerfile: -------------------------------------------------------------------------------- 1 | #FROM tootallnate/osx-cross 2 | FROM lambci/lambda:build-provided 3 | 4 | # Compile Python 5 | WORKDIR /usr/src/python 6 | ARG PYTHON_VERSION="2.7.12" 7 | RUN curl -sfLS "https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz" | tar xzv --strip-components=1 8 | RUN ./configure \ 9 | --host="${CHOST}" \ 10 | --build="${CBUILD}" \ 11 | --prefix="/binaries/python-${PYTHON_VERSION}" 12 | RUN make 13 | RUN make install 14 | RUN ln -sf "python${PYTHON_VERSION:0:1}" "/binaries/python-${PYTHON_VERSION}/bin/python" 15 | 16 | WORKDIR "/binaries/python-${PYTHON_VERSION}" 17 | RUN find . -name '*.pyc' -exec rm -rfv {} \; 18 | RUN find . -name '__pycache__' -print0 | xargs -0 rm -rfv 19 | 20 | ENV PYTHON_VERSION="${PYTHON_VERSION}" 21 | COPY ./copy.sh / 22 | 23 | CMD ["/copy.sh"] 24 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Subclassing `Error` in TypeScript: 3 | * https://stackoverflow.com/a/41102306/376773 4 | */ 5 | 6 | interface LambdaErrorPayload { 7 | errorMessage?: string; 8 | errorType?: string; 9 | stackTrace?: string | string[]; 10 | } 11 | 12 | export class LambdaError extends Error { 13 | constructor(data: LambdaErrorPayload = {}) { 14 | super(data.errorMessage || 'Unspecified runtime initialization error'); 15 | Object.setPrototypeOf(this, new.target.prototype); 16 | 17 | Object.defineProperty(this, 'name', { 18 | value: data.errorType || this.constructor.name 19 | }); 20 | 21 | if (Array.isArray(data.stackTrace)) { 22 | this.stack = [ 23 | `${this.name}: ${this.message}`, 24 | ...data.stackTrace 25 | ].join('\n'); 26 | } else if (typeof data.stackTrace === 'string') { 27 | this.stack = data.stackTrace; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | tab_width = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [{*.json,*.json.example,*.gyp,*.yml,*.yaml,*.workflow}] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [{*.py,*.asm}] 17 | indent_style = space 18 | 19 | [*.py] 20 | indent_size = 4 21 | 22 | [*.asm] 23 | indent_size = 8 24 | 25 | [*.md] 26 | trim_trailing_whitespace = false 27 | 28 | # Ideal settings - some plugins might support these. 29 | [*.js] 30 | quote_type = single 31 | 32 | [{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}] 33 | curly_bracket_next_line = false 34 | spaces_around_operators = true 35 | spaces_around_brackets = outside 36 | # close enough to 1TB 37 | indent_brace_style = K&R 38 | -------------------------------------------------------------------------------- /src/install-python.ts: -------------------------------------------------------------------------------- 1 | import { extract } from 'tar'; 2 | import fetch from 'node-fetch'; 3 | import createDebug from 'debug'; 4 | import { createGunzip } from 'zlib'; 5 | 6 | const debug = createDebug('@zeit/fun:install-python'); 7 | 8 | export function generatePythonTarballUrl( 9 | version: string, 10 | platform: string = process.platform, 11 | arch: string = process.arch 12 | ): string { 13 | return `https://python-binaries.zeit.sh/python-${version}-${platform}-${arch}.tar.gz`; 14 | } 15 | 16 | export async function installPython( 17 | dest: string, 18 | version: string, 19 | platform: string = process.platform, 20 | arch: string = process.arch 21 | ): Promise { 22 | const tarballUrl = generatePythonTarballUrl(version, platform, arch); 23 | debug('Downloading Python %s tarball %o', version, tarballUrl); 24 | const res = await fetch(tarballUrl); 25 | if (!res.ok) { 26 | throw new Error(`HTTP request failed: ${res.status}`); 27 | } 28 | return new Promise((resolve, reject) => { 29 | debug('Extracting Python %s tarball to %o', version, dest); 30 | res.body 31 | .pipe(createGunzip()) 32 | .pipe(extract({ strip: 1, C: dest })) 33 | .on('error', reject) 34 | .on('end', resolve); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/runtimes/go1.x/index.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as execa from 'execa'; 3 | import createDebug from 'debug'; 4 | import { Runtime } from '../../types'; 5 | import { copyFile, mkdirp, remove } from 'fs-extra'; 6 | 7 | const debug = createDebug('@zeit/fun:runtimes/go1.x'); 8 | 9 | function _go(opts) { 10 | return function go(...args) { 11 | debug('Exec %o', `go ${args.join(' ')}`); 12 | return execa('go', args, { stdio: 'inherit', ...opts }); 13 | }; 14 | } 15 | 16 | export async function init({ cacheDir }: Runtime): Promise { 17 | const source = join(cacheDir, 'bootstrap.go'); 18 | 19 | // Prepare a temporary `$GOPATH` 20 | const GOPATH = join(cacheDir, 'go'); 21 | 22 | // The source code must reside in `$GOPATH/src` for `go get` to work 23 | const bootstrapDir = join(GOPATH, 'src', 'bootstrap'); 24 | await mkdirp(bootstrapDir); 25 | await copyFile(source, join(bootstrapDir, 'bootstrap.go')); 26 | 27 | const go = _go({ cwd: bootstrapDir, env: { ...process.env, GOPATH } }); 28 | const bootstrap = join(cacheDir, 'bootstrap'); 29 | debug('Compiling Go runtime binary %o -> %o', source, bootstrap); 30 | await go('get'); 31 | await go('build', '-o', bootstrap, 'bootstrap.go'); 32 | 33 | // Clean up `$GOPATH` from the cacheDir 34 | await remove(GOPATH); 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zeit/fun", 3 | "version": "0.8.0", 4 | "description": "Local Lambda development environment", 5 | "main": "dist/src/index", 6 | "typings": "dist/src/index", 7 | "license": "MIT", 8 | "repository": "zeit/fun", 9 | "scripts": { 10 | "prebuild": "rimraf dist", 11 | "build": "tsc", 12 | "postbuild": "cpy --parents src test '!**/*.ts' dist", 13 | "test": "echo \"Node.js version: $(node -v)\\n\" && yarn build && best --include dist/test/test.js --verbose", 14 | "test-codecov": "nyc npm test", 15 | "report-codecov": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 16 | "lint:staged": "lint-staged", 17 | "prettier": "prettier --write --single-quote './{src,test}/**/*.ts'", 18 | "prepublishOnly": "npm run build && rm -rf dist/test" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "dependencies": { 24 | "async-listen": "1.0.0", 25 | "cache-or-tmp-directory": "1.0.0", 26 | "debug": "4.1.1", 27 | "execa": "1.0.0", 28 | "fs-extra": "7.0.1", 29 | "generic-pool": "3.4.2", 30 | "micro": "9.3.3", 31 | "ms": "2.1.1", 32 | "node-fetch": "2.3.0", 33 | "path-match": "1.2.4", 34 | "promisepipe": "3.0.0", 35 | "stat-mode": "0.3.0", 36 | "stream-to-promise": "2.2.0", 37 | "tar": "4.4.8", 38 | "uid-promise": "1.0.0", 39 | "uuid": "3.3.2", 40 | "yauzl-promise": "2.1.3" 41 | }, 42 | "devDependencies": { 43 | "@types/generic-pool": "3.1.9", 44 | "@types/node": "10.12.18", 45 | "@types/tar": "4.0.0", 46 | "@types/yauzl-promise": "2.1.0", 47 | "@zeit/best": "0.5.1", 48 | "codecov": "3.1.0", 49 | "cpy-cli": "2.0.0", 50 | "lint-staged": "8.1.0", 51 | "nyc": "13.2.0", 52 | "pkg": "4.3.7", 53 | "pre-commit": "1.2.2", 54 | "prettier": "1.15.3", 55 | "rimraf": "2.6.3", 56 | "source-map-support": "0.5.10", 57 | "typescript": "3.2.2" 58 | }, 59 | "pre-commit": "lint:staged", 60 | "lint-staged": { 61 | "*.ts": [ 62 | "prettier --write --single-quote", 63 | "git add" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/install-node.ts: -------------------------------------------------------------------------------- 1 | import { extract } from 'tar'; 2 | import pipe from 'promisepipe'; 3 | import fetch from 'node-fetch'; 4 | import createDebug from 'debug'; 5 | import { createGunzip } from 'zlib'; 6 | import { basename, join } from 'path'; 7 | import { createWriteStream, mkdirp } from 'fs-extra'; 8 | import { unzip, zipFromFile } from './unzip'; 9 | 10 | const debug = createDebug('@zeit/fun:install-node'); 11 | 12 | export function generateNodeTarballUrl( 13 | version: string, 14 | platform: string = process.platform, 15 | arch: string = process.arch 16 | ): string { 17 | if (!version.startsWith('v')) { 18 | version = `v${version}`; 19 | } 20 | let ext: string; 21 | let plat: string = platform; 22 | if (platform === 'win32') { 23 | ext = 'zip'; 24 | plat = 'win'; 25 | } else { 26 | ext = 'tar.gz'; 27 | } 28 | return `https://nodejs.org/dist/${version}/node-${version}-${plat}-${arch}.${ext}`; 29 | } 30 | 31 | export async function installNode( 32 | dest: string, 33 | version: string, 34 | platform: string = process.platform, 35 | arch: string = process.arch 36 | ): Promise { 37 | const tarballUrl = generateNodeTarballUrl(version, platform, arch); 38 | debug('Downloading Node.js %s tarball %o', version, tarballUrl); 39 | const res = await fetch(tarballUrl); 40 | if (!res.ok) { 41 | throw new Error(`HTTP request failed: ${res.status}`); 42 | } 43 | if (platform === 'win32') { 44 | // Put it in the `bin` dir for consistency with the tarballs 45 | const finalDest = join(dest, 'bin'); 46 | const zipName = basename(tarballUrl); 47 | const zipPath = join(dest, zipName); 48 | 49 | debug('Saving Node.js %s zip file to %o', version, zipPath); 50 | await pipe( 51 | res.body, 52 | createWriteStream(zipPath) 53 | ); 54 | 55 | debug('Extracting Node.js %s zip file to %o', version, finalDest); 56 | const zipFile = await zipFromFile(zipPath); 57 | await unzip(zipFile, finalDest, { strip: 1 }); 58 | } else { 59 | debug('Extracting Node.js %s tarball to %o', version, dest); 60 | await pipe( 61 | res.body, 62 | createGunzip(), 63 | extract({ strip: 1, C: dest }) 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test-linux: 4 | docker: 5 | - image: circleci/node:10 6 | steps: 7 | - run: 8 | name: Create ~/.npmrc file 9 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 10 | - checkout 11 | - run: 12 | name: Install Go 13 | command: sudo ./.circleci/install-go.sh 14 | - run: 15 | name: Install Node.js 16 | command: curl -sSL https://install-node.now.sh/10 | sudo bash -s -- --yes 17 | - restore_cache: 18 | key: dependency-cache-linux-{{ checksum "package.json" }} 19 | - run: 20 | name: Install '@zeit/fun' Dependencies 21 | command: yarn 22 | - save_cache: 23 | key: dependency-cache-linux-{{ checksum "package.json" }} 24 | paths: 25 | - node_modules 26 | - run: 27 | name: Compile Go Test Functions 28 | command: ./test/go-build.sh 29 | - run: 30 | name: Run Tests 31 | command: DEBUG=*fun* yarn test 32 | test-macos: 33 | macos: 34 | xcode: "9.3.0" 35 | steps: 36 | - run: 37 | name: Create ~/.npmrc file 38 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 39 | - checkout 40 | - run: 41 | name: Install Go 42 | command: sudo ./.circleci/install-go.sh 43 | - run: 44 | name: Install Node.js 45 | command: curl -sSL https://install-node.now.sh/10 | sudo bash -s -- --yes 46 | - restore_cache: 47 | key: dependency-cache-macos-{{ checksum "package.json" }} 48 | - run: 49 | name: Install '@zeit/fun' Dependencies 50 | command: yarn 51 | - save_cache: 52 | key: dependency-cache-macos-{{ checksum "package.json" }} 53 | paths: 54 | - node_modules 55 | - run: 56 | name: Compile Go Test Functions 57 | command: ./test/go-build.sh 58 | - run: 59 | name: Run Tests 60 | command: DEBUG=*fun* yarn test 61 | workflows: 62 | version: 2 63 | test: 64 | jobs: 65 | - test-linux 66 | - test-macos 67 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess } from 'child_process'; 2 | 3 | export interface LambdaParams { 4 | FunctionName?: string; 5 | Code: { ZipFile?: Buffer | string; Directory?: string }; 6 | Handler: string; 7 | Runtime: string; // nodejs | nodejs4.3 | nodejs6.10 | nodejs8.10 | java8 | python2.7 | python3.6 | python3.7 | dotnetcore1.0 | dotnetcore2.0 | dotnetcore2.1 | nodejs4.3-edge | go1.x | ruby2.5 | provided 8 | Provider?: string; // native | docker 9 | Environment?: { Variables: object }; 10 | MemorySize?: number; // The amount of memory that your function has access to. Increasing the function's memory also increases it's CPU allocation. The default value is 128 MB. The value must be a multiple of 64 MB. 11 | Region?: string; // AWS Region name (used for generating the fake ARN, etc.) 12 | Timeout?: number; // The amount of time that Lambda allows a function to run before terminating it. The default is 3 seconds. The maximum allowed value is 900 seconds. 13 | } 14 | 15 | export type InvokePayload = Buffer | Blob | string; 16 | 17 | // https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax 18 | export interface InvokeParams { 19 | InvocationType: 'RequestResponse' | 'Event' | 'DryRun'; 20 | Payload?: InvokePayload; 21 | } 22 | 23 | // https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_ResponseSyntax 24 | export interface InvokeResult { 25 | StatusCode: number; 26 | FunctionError?: 'Handled' | 'Unhandled'; 27 | LogResult?: string; 28 | Payload: InvokePayload; 29 | ExecutedVersion?: string; 30 | } 31 | 32 | export interface Provider { 33 | invoke(params: InvokeParams): Promise; 34 | destroy(): Promise; 35 | } 36 | 37 | export interface Runtime { 38 | name: string; 39 | runtimeDir: string; 40 | cacheDir?: string; 41 | init?(runtime: Runtime): Promise; 42 | } 43 | 44 | export interface Lambda { 45 | (payload?: string | object): Promise; 46 | invoke(params: InvokeParams): Promise; 47 | destroy(): Promise; 48 | params: LambdaParams; 49 | runtime: Runtime; 50 | provider: Provider; 51 | functionName: string; 52 | memorySize: number; 53 | version: string; 54 | region: string; 55 | arn: string; 56 | timeout: number; 57 | extractedDir?: string; 58 | } 59 | -------------------------------------------------------------------------------- /src/unzip.ts: -------------------------------------------------------------------------------- 1 | import { tmpdir } from 'os'; 2 | import * as Mode from 'stat-mode'; 3 | import pipe from 'promisepipe'; 4 | import createDebug from 'debug'; 5 | import { dirname, basename, join } from 'path'; 6 | import { createWriteStream, mkdirp, symlink, unlink } from 'fs-extra'; 7 | import * as streamToPromise from 'stream-to-promise'; 8 | import { 9 | Entry, 10 | ZipFile, 11 | open as zipFromFile, 12 | fromBuffer as zipFromBuffer 13 | } from 'yauzl-promise'; 14 | 15 | export { zipFromFile, zipFromBuffer, ZipFile }; 16 | 17 | const debug = createDebug('@zeit/fun:unzip'); 18 | 19 | export async function unzipToTemp( 20 | data: Buffer | string, 21 | tmpDir: string = tmpdir() 22 | ): Promise { 23 | const dir = join( 24 | tmpDir, 25 | `zeit-fun-${Math.random() 26 | .toString(16) 27 | .substring(2)}` 28 | ); 29 | let zip: ZipFile; 30 | if (Buffer.isBuffer(data)) { 31 | debug('Unzipping buffer (length=%o) to temp dir %o', data.length, dir); 32 | zip = await zipFromBuffer(data); 33 | } else { 34 | debug('Unzipping %o to temp dir %o', data, dir); 35 | zip = await zipFromFile(data); 36 | } 37 | await unzip(zip, dir); 38 | await zip.close(); 39 | debug('Finished unzipping to %o', dir); 40 | return dir; 41 | } 42 | 43 | const getMode = (entry: Entry) => 44 | new Mode({ mode: entry.externalFileAttributes >>> 16 }); 45 | 46 | interface UnzipOptions { 47 | strip?: number; 48 | }; 49 | 50 | export async function unzip(zipFile: ZipFile, dir: string, opts: UnzipOptions = {}): Promise { 51 | let entry: Entry; 52 | const strip = opts.strip || 0; 53 | while ((entry = await zipFile.readEntry()) !== null) { 54 | const fileName = strip === 0 ? entry.fileName : entry.fileName.split('/').slice(strip).join('/'); 55 | const destPath = join(dir, fileName); 56 | if (/\/$/.test(entry.fileName)) { 57 | debug('Creating directory %o', destPath); 58 | await mkdirp(destPath); 59 | } else { 60 | const [entryStream] = await Promise.all([ 61 | entry.openReadStream(), 62 | // ensure parent directory exists 63 | mkdirp(dirname(destPath)) 64 | ]); 65 | const mode = getMode(entry); 66 | if (mode.isSymbolicLink()) { 67 | const linkDest = String(await streamToPromise(entryStream)); 68 | debug('Creating symboling link %o to %o', destPath, linkDest); 69 | await symlink(linkDest, destPath); 70 | } else { 71 | const modeOctal = mode.toOctal(); 72 | const modeVal = parseInt(modeOctal, 8); 73 | if (modeVal === 0) { 74 | debug('Unzipping file to %o', destPath); 75 | } else { 76 | debug( 77 | 'Unzipping file to %o with mode %s (%s)', 78 | destPath, 79 | modeOctal, 80 | String(mode) 81 | ); 82 | } 83 | try { 84 | await unlink(destPath); 85 | } catch (err) { 86 | if (err.code !== 'ENOENT') { 87 | throw err; 88 | } 89 | } 90 | const destStream = createWriteStream(destPath, { 91 | mode: modeVal 92 | }); 93 | await pipe( 94 | entryStream, 95 | destStream 96 | ); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/functions/provided-bash-echo/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -uo pipefail 4 | 5 | # Constants 6 | RUNTIME_PATH="2018-06-01/runtime" 7 | mkdir -p /tmp/.aws 8 | export HOME="/tmp" 9 | 10 | # Send initialization error to Lambda API 11 | sendInitError () { 12 | ERROR_MESSAGE=$1 13 | ERROR_TYPE=$2 14 | ERROR="{\"errorMessage\": \"$ERROR_MESSAGE\", \"errorType\": \"$ERROR_TYPE\"}" 15 | curl -sS -X POST -d "$ERROR" "http://${AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/init/error" > /dev/null 16 | } 17 | 18 | # Send runtime error to Lambda API 19 | sendRuntimeError () { 20 | REQUEST_ID=$1 21 | ERROR_MESSAGE=$2 22 | ERROR_TYPE=$3 23 | STACK_TRACE=$4 24 | ERROR="{\"errorMessage\": \"$ERROR_MESSAGE\", \"errorType\": \"$ERROR_TYPE\", \"stackTrace\": \"$STACK_TRACE\"}" 25 | curl -sS -X POST -d "$ERROR" "http://${AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/invocation/${REQUEST_ID}/error" > /dev/null 26 | } 27 | 28 | # Send successful response to Lambda API 29 | sendResponse () { 30 | REQUEST_ID=$1 31 | REQUEST_RESPONSE=$2 32 | curl -sS -X POST -d "$REQUEST_RESPONSE" "http://${AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/invocation/${REQUEST_ID}/response" > /dev/null 33 | } 34 | 35 | # Make sure handler file exists 36 | if [[ ! -f $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh" ]]; then 37 | sendInitError "Failed to load handler '$(echo $_HANDLER | cut -d. -f2)' from module '$(echo $_HANDLER | cut -d. -f1)'. File '$(echo $_HANDLER | cut -d. -f1).sh' does not exist." "InvalidHandlerException" 38 | exit 1 39 | fi 40 | 41 | # Initialization 42 | SOURCE_RESPONSE="$(mktemp)" 43 | source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh" > $SOURCE_RESPONSE 2>&1 44 | if [[ $? -eq "0" ]]; then 45 | rm -f -- "$SOURCE_RESPONSE" 46 | else 47 | sendInitError "Failed to source file '$(echo $_HANDLER | cut -d. -f1).sh'. $(cat $SOURCE_RESPONSE)" "InvalidHandlerException" 48 | exit 1 49 | fi 50 | 51 | # Make sure handler function exists 52 | type "$(echo $_HANDLER | cut -d. -f2)" > /dev/null 2>&1 53 | if [[ ! $? -eq "0" ]]; then 54 | sendInitError "Failed to load handler '$(echo $_HANDLER | cut -d. -f2)' from module '$(echo $_HANDLER | cut -d. -f1)'. Function '$(echo $_HANDLER | cut -d. -f2)' does not exist." "InvalidHandlerException" 55 | exit 1 56 | fi 57 | 58 | # Processing 59 | while true 60 | do 61 | HEADERS="/tmp/headers-$(date +'%s')" 62 | RESPONSE="/tmp/response-$(date +'%s')" 63 | touch $HEADERS 64 | touch $RESPONSE 65 | EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/invocation/next") 66 | REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) 67 | # Export some additional context 68 | export AWS_LAMBDA_REQUEST_ID=$REQUEST_ID 69 | export AWS_LAMBDA_DEADLINE_MS=$(grep -Fi Lambda-Runtime-Deadline-Ms "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) 70 | export AWS_LAMBDA_FUNCTION_ARN=$(grep -Fi Lambda-Runtime-Invoked-Function-Arn "$HEADERS" | cut -d" " -f2) 71 | export AWS_LAMBDA_TRACE_ID=$(grep -Fi Lambda-Runtime-Trace-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) 72 | # Execute the user function 73 | $(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA" >&1 2> $RESPONSE | cat 74 | EXIT_CODE=$? 75 | # Respond to Lambda API 76 | if [[ $EXIT_CODE -eq "0" ]]; then 77 | sendResponse "$REQUEST_ID" "$(cat $RESPONSE)" 78 | else 79 | # Log error to stdout as well 80 | cat $RESPONSE 81 | sendRuntimeError "$REQUEST_ID" "Exited with code $EXIT_CODE" "RuntimeErrorException" "$(cat $RESPONSE)" 82 | fi 83 | # Clean up 84 | rm -f -- "$HEADERS" 85 | rm -f -- "$RESPONSE" 86 | unset HEADERS 87 | unset RESPONSE 88 | done -------------------------------------------------------------------------------- /test/functions/provided-bash-invalid-version/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -uo pipefail 4 | 5 | # Constants 6 | RUNTIME_PATH="2017-01-01/runtime" 7 | mkdir -p /tmp/.aws 8 | export HOME="/tmp" 9 | 10 | # Send initialization error to Lambda API 11 | sendInitError () { 12 | ERROR_MESSAGE=$1 13 | ERROR_TYPE=$2 14 | ERROR="{\"errorMessage\": \"$ERROR_MESSAGE\", \"errorType\": \"$ERROR_TYPE\"}" 15 | curl -sS -X POST -d "$ERROR" "http://${AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/init/error" > /dev/null 16 | } 17 | 18 | # Send runtime error to Lambda API 19 | sendRuntimeError () { 20 | REQUEST_ID=$1 21 | ERROR_MESSAGE=$2 22 | ERROR_TYPE=$3 23 | STACK_TRACE=$4 24 | ERROR="{\"errorMessage\": \"$ERROR_MESSAGE\", \"errorType\": \"$ERROR_TYPE\", \"stackTrace\": \"$STACK_TRACE\"}" 25 | curl -sS -X POST -d "$ERROR" "http://${AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/invocation/${REQUEST_ID}/error" > /dev/null 26 | } 27 | 28 | # Send successful response to Lambda API 29 | sendResponse () { 30 | REQUEST_ID=$1 31 | REQUEST_RESPONSE=$2 32 | curl -sS -X POST -d "$REQUEST_RESPONSE" "http://${AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/invocation/${REQUEST_ID}/response" > /dev/null 33 | } 34 | 35 | # Make sure handler file exists 36 | if [[ ! -f $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh" ]]; then 37 | sendInitError "Failed to load handler '$(echo $_HANDLER | cut -d. -f2)' from module '$(echo $_HANDLER | cut -d. -f1)'. File '$(echo $_HANDLER | cut -d. -f1).sh' does not exist." "InvalidHandlerException" 38 | exit 1 39 | fi 40 | 41 | # Initialization 42 | SOURCE_RESPONSE="$(mktemp)" 43 | source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh" > $SOURCE_RESPONSE 2>&1 44 | if [[ $? -eq "0" ]]; then 45 | rm -f -- "$SOURCE_RESPONSE" 46 | else 47 | sendInitError "Failed to source file '$(echo $_HANDLER | cut -d. -f1).sh'. $(cat $SOURCE_RESPONSE)" "InvalidHandlerException" 48 | exit 1 49 | fi 50 | 51 | # Make sure handler function exists 52 | type "$(echo $_HANDLER | cut -d. -f2)" > /dev/null 2>&1 53 | if [[ ! $? -eq "0" ]]; then 54 | sendInitError "Failed to load handler '$(echo $_HANDLER | cut -d. -f2)' from module '$(echo $_HANDLER | cut -d. -f1)'. Function '$(echo $_HANDLER | cut -d. -f2)' does not exist." "InvalidHandlerException" 55 | exit 1 56 | fi 57 | 58 | # Processing 59 | while true 60 | do 61 | HEADERS="/tmp/headers-$(date +'%s')" 62 | RESPONSE="/tmp/response-$(date +'%s')" 63 | touch $HEADERS 64 | touch $RESPONSE 65 | EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/${RUNTIME_PATH}/invocation/next") 66 | REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) 67 | # Export some additional context 68 | export AWS_LAMBDA_REQUEST_ID=$REQUEST_ID 69 | export AWS_LAMBDA_DEADLINE_MS=$(grep -Fi Lambda-Runtime-Deadline-Ms "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) 70 | export AWS_LAMBDA_FUNCTION_ARN=$(grep -Fi Lambda-Runtime-Invoked-Function-Arn "$HEADERS" | cut -d" " -f2) 71 | export AWS_LAMBDA_TRACE_ID=$(grep -Fi Lambda-Runtime-Trace-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) 72 | # Execute the user function 73 | $(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA" >&1 2> $RESPONSE | cat 74 | EXIT_CODE=5 75 | # Respond to Lambda API 76 | if [[ $EXIT_CODE -eq "0" ]]; then 77 | sendResponse "$REQUEST_ID" "$(cat $RESPONSE)" 78 | else 79 | # Log error to stdout as well 80 | cat $RESPONSE 81 | sendRuntimeError "$REQUEST_ID" "Exited with code $EXIT_CODE" "RuntimeErrorException" "$(cat $RESPONSE)" 82 | fi 83 | # Clean up 84 | rm -f -- "$HEADERS" 85 | rm -f -- "$RESPONSE" 86 | unset HEADERS 87 | unset RESPONSE 88 | done 89 | -------------------------------------------------------------------------------- /src/runtimes/python/bootstrap.py: -------------------------------------------------------------------------------- 1 | # Parts of this runtime based off of: 2 | # https://gist.github.com/avoidik/78ddc7854c7b88607f7cf56db3e591e5 3 | 4 | import os 5 | import sys 6 | import json 7 | import importlib 8 | 9 | is_python_3 = sys.version_info > (3, 0) 10 | 11 | if is_python_3: 12 | import urllib.request 13 | else: 14 | import urllib2 15 | 16 | 17 | class LambdaRequest: 18 | def __init__(self, path, data=None): 19 | req = None 20 | runtime_path = '/2018-06-01/runtime/' 21 | url = ( 22 | 'http://' 23 | + os.environ.get( 24 | 'AWS_LAMBDA_RUNTIME_API', '127.0.0.1:3000' 25 | ) 26 | + runtime_path 27 | + path 28 | ) 29 | 30 | if is_python_3: 31 | req = urllib.request.urlopen(url, data) 32 | else: 33 | req = urllib2.urlopen(url, data) 34 | 35 | info = req.info() 36 | body = req.read() 37 | 38 | if is_python_3: 39 | body = body.decode(encoding='UTF-8') 40 | 41 | self.status_code = req.getcode() 42 | self.body = body 43 | self.info = info 44 | 45 | def get_header(self, name): 46 | if is_python_3: 47 | return self.info.get(name) 48 | else: 49 | return self.info.getheader(name) 50 | 51 | def get_json_body(self): 52 | return json.loads(self.body) 53 | 54 | 55 | def lambda_runtime_next_invocation(): 56 | res = LambdaRequest('invocation/next') 57 | 58 | if res.status_code != 200: 59 | raise Exception( 60 | 'Unexpected /invocation/next response: ' 61 | + res.body 62 | ) 63 | 64 | x_amzn_trace_id = res.get_header('Lambda-Runtime-Trace-Id') 65 | if x_amzn_trace_id != None: 66 | os.environ['_X_AMZN_TRACE_ID'] = x_amzn_trace_id 67 | elif '_X_AMZN_TRACE_ID' in os.environ: 68 | del os.environ['_X_AMZN_TRACE_ID'] 69 | 70 | aws_request_id = res.get_header('Lambda-Runtime-Aws-Request-Id') 71 | 72 | context = { 73 | # TODO: fill this out 74 | 'aws_request_id': aws_request_id 75 | } 76 | 77 | event = res.get_json_body() 78 | 79 | return (event, context) 80 | 81 | 82 | def lambda_runtime_invoke_response(result, context): 83 | body = json.dumps(result, separators=(',', ':')).encode( 84 | encoding='UTF-8' 85 | ) 86 | res = LambdaRequest( 87 | 'invocation/' 88 | + context['aws_request_id'] 89 | + '/response', 90 | body, 91 | ) 92 | if res.status_code != 202: 93 | raise Exception( 94 | 'Unexpected /invocation/response response: ' 95 | + res.body 96 | ) 97 | 98 | 99 | def lambda_runtime_invoke_error(err, context): 100 | body = json.dumps(err, separators=(',', ':')).encode( 101 | encoding='UTF-8' 102 | ) 103 | res = LambdaRequest( 104 | 'invocation/' 105 | + context['aws_request_id'] 106 | + '/error', 107 | body, 108 | ) 109 | 110 | 111 | def lambda_runtime_get_handler(): 112 | (module_name, handler_name) = os.environ['_HANDLER'].split('.') 113 | mod = importlib.import_module(module_name) 114 | # TODO: invoke `__init__`? 115 | return getattr(mod, handler_name) 116 | 117 | 118 | def lambda_runtime_main(): 119 | if not is_python_3: 120 | reload(sys) 121 | sys.setdefaultencoding('utf-8') 122 | 123 | sys.path.insert( 124 | 0, os.environ.get('LAMBDA_TASK_ROOT', '/var/task') 125 | ) 126 | 127 | fn = lambda_runtime_get_handler() 128 | 129 | while True: 130 | (event, context) = lambda_runtime_next_invocation() 131 | # print(event) 132 | # print(context) 133 | result = None 134 | try: 135 | result = fn(event, context) 136 | except: 137 | err = str(sys.exc_info()[0]) 138 | print(err) 139 | lambda_runtime_invoke_error( 140 | {'error': err}, context 141 | ) 142 | else: 143 | lambda_runtime_invoke_response(result, context) 144 | 145 | 146 | if __name__ == '__main__': 147 | lambda_runtime_main() 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ƒun 2 | 3 | [![CircleCI](https://circleci.com/gh/zeit/fun/tree/master.svg?style=svg&circle-token=8df270134881b60f9ec91f47f5268e0b5cce2acd)](https://circleci.com/gh/zeit/fun/tree/master) [![codecov](https://codecov.io/gh/zeit/fun/branch/master/graph/badge.svg?token=6bZSbITKbj)](https://codecov.io/gh/zeit/fun) 4 | 5 | Local serverless function λ development runtime. 6 | 7 | * Programmatic. A TypeScript API is exposed to trigger invocations. 8 | * Provider agnostic. AWS Lambda + other cloud providers planned. 9 | * Runtime agnostic. Node, go, python and custom runtime APIs. 10 | * Platform agnostic. Functions can be executed natively (e.g. macOS) or via Docker. 11 | * Zero setup needed. ƒun acquires the necessary runtime files (e.g. `node`). 12 | 13 | 14 | ## Example 15 | 16 | Given a Lambda function like this one: 17 | 18 | ```js 19 | // index.js 20 | exports.handler = function(event, context, callback) { 21 | callback(null, { hello: 'world' }); 22 | }; 23 | ``` 24 | 25 | You can invoke this function locally using the code below: 26 | 27 | ```js 28 | const { createFunction } = require('@zeit/fun'); 29 | 30 | async function main() { 31 | // Starts up the necessary server to be able to invoke the function 32 | const fn = await createFunction({ 33 | Code: { 34 | // `ZipFile` works, or an already unzipped directory may be specified 35 | Directory: __dirname + '/example' 36 | }, 37 | Handler: 'index.handler', 38 | Runtime: 'nodejs8.10', 39 | Environment: { 40 | Variables: { 41 | HELLO: 'world' 42 | } 43 | }, 44 | MemorySize: 512 45 | }); 46 | 47 | // Invoke the function with a custom payload. A new instance of the function 48 | // will be initialized if there is not an available one ready to process. 49 | const res = await fn({ hello: 'world' }); 50 | 51 | console.log(res); 52 | // Prints: { hello: 'world' } 53 | 54 | // Once we are done with the function, destroy it so that the processes are 55 | // cleaned up, and the API server is shut down (useful for hot-reloading). 56 | await fn.destroy(); 57 | } 58 | 59 | main().catch(console.error); 60 | ``` 61 | 62 | 63 | ## Providers 64 | 65 | ƒun has a concept of pluggable "providers", which are responsible for 66 | creating, freezing, unfreezing and shutting down the processes that execute the 67 | Lambda function. 68 | 69 | ### `native` 70 | 71 | The `native` provider executes Lambda functions directly on the machine executing 72 | ƒun. This provides an execution environment that closely resembles the 73 | real Lambda environment, with some key differences that are documented here: 74 | 75 | * Lambdas processes are ran as your own user, not the `sbx_user1051` user. 76 | * Processes are *not* sandboxed nor chrooted, so do not rely on hard-coded 77 | locations like `/var/task`, `/var/runtime`, `/opt`, etc. Instead, your 78 | function code should use the environment variables that represent these 79 | locations (namely `LAMBDA_TASK_ROOT` and `LAMBDA_RUNTIME_DIR`). 80 | * Processes are frozen by sending the `SIGSTOP` signal to the lambda process, 81 | and unfrozen by sending the `SIGCONT` signal, not using the [cgroup freezer][]. 82 | * Lambdas that compile to native executables (i.e. Go) will need to be compiled 83 | for your operating system. So if you are on macOS, then the binary needs to be 84 | executable on macOS. 85 | 86 | ### `docker` 87 | 88 | A `docker` provider is planned, but not yet implemented. This will allow for an 89 | execution environment that more closely matches the AWS Lambda environment, 90 | including the ability to execute Linux x64 binaries / shared libraries. 91 | 92 | 93 | ## Runtimes 94 | 95 | ƒun aims to support all runtimes that AWS Lambda provides. Currently 96 | implemented are: 97 | 98 | * `nodejs` for Node.js Lambda functions using the system `node` binary 99 | * `nodejs6.10` for Node.js Lambda functions using a downloaded Node v6.10.0 binary 100 | * `nodejs8.10` for Node.js Lambda functions using a downloaded Node v8.10.0 binary 101 | * `python` for Python Lambda functions using the system `python` binary 102 | * `python2.7` for Python Lambda functions using a downloaded Python v2.7.12 binary 103 | * `python3.6` for Python Lambda functions using a downloaded Python v3.6.8 binary 104 | * `python3.7` for Python Lambda functions using a downloaded Python v3.7.2 binary 105 | * `go1.x` for Lambda functions written in Go - binary must be compiled for your platform 106 | * `provided` for [custom runtimes][] 107 | 108 | [cgroup freezer]: https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt 109 | [custom runtimes]: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html 110 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Lambda, 3 | LambdaParams, 4 | InvokeParams, 5 | InvokeResult, 6 | Runtime 7 | } from './types'; 8 | import createDebug from 'debug'; 9 | import { remove } from 'fs-extra'; 10 | import { basename } from 'path'; 11 | import * as listen from 'async-listen'; 12 | import { unzipToTemp } from './unzip'; 13 | import { LambdaError } from './errors'; 14 | import * as providers from './providers'; 15 | import { RuntimeServer } from './runtime-server'; 16 | import { funCacheDir, runtimes, initializeRuntime } from './runtimes'; 17 | 18 | const debug = createDebug('@zeit/fun:index'); 19 | 20 | export { 21 | Lambda, 22 | LambdaParams, 23 | InvokeParams, 24 | InvokeResult, 25 | runtimes, 26 | providers, 27 | funCacheDir, 28 | initializeRuntime 29 | }; 30 | 31 | // Environment variable names that AWS Lambda does not allow to be overridden. 32 | // https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html#lambda-environment-variables 33 | const reservedEnvVars = new Set([ 34 | '_HANDLER', 35 | 'LAMBDA_TASK_ROOT', 36 | 'LAMBDA_RUNTIME_DIR', 37 | 'AWS_EXECUTION_ENV', 38 | 'AWS_DEFAULT_REGION', 39 | 'AWS_REGION', 40 | 'AWS_LAMBDA_LOG_GROUP_NAME', 41 | 'AWS_LAMBDA_LOG_STREAM_NAME', 42 | 'AWS_LAMBDA_FUNCTION_NAME', 43 | 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', 44 | 'AWS_LAMBDA_FUNCTION_VERSION', 45 | 'AWS_ACCESS_KEY', 46 | 'AWS_ACCESS_KEY_ID', 47 | 'AWS_SECRET_KEY', 48 | 'AWS_SECRET_ACCESS_KEY', 49 | 'AWS_SESSION_TOKEN', 50 | 'TZ' 51 | ]); 52 | 53 | export class ValidationError extends Error { 54 | reserved?: string[]; 55 | 56 | constructor(message?: string) { 57 | super(message); 58 | 59 | // Restore prototype chain (see https://stackoverflow.com/a/41102306/376773) 60 | this.name = new.target.name; 61 | const actualProto = new.target.prototype; 62 | Object.setPrototypeOf(this, actualProto); 63 | } 64 | } 65 | 66 | export async function createFunction(params: LambdaParams): Promise { 67 | const Provider = providers[params.Provider || 'native']; 68 | if (!Provider) { 69 | throw new TypeError(`Provider "${params.Provider}" is not implemented`); 70 | } 71 | 72 | const runtime: Runtime = runtimes[params.Runtime]; 73 | if (!runtime) { 74 | throw new TypeError(`Runtime "${params.Runtime}" is not implemented`); 75 | } 76 | await initializeRuntime(runtime); 77 | 78 | const envVars = (params.Environment && params.Environment.Variables) || {}; 79 | const reserved = Object.keys(envVars).filter(name => { 80 | return reservedEnvVars.has(name.toUpperCase()); 81 | }); 82 | if (reserved.length > 0) { 83 | const err = new ValidationError( 84 | `The following environment variables can not be configured: ${reserved.join( 85 | ', ' 86 | )}` 87 | ); 88 | err.reserved = reserved; 89 | throw err; 90 | } 91 | 92 | const fn: Lambda = async function( 93 | payload?: string | object 94 | ): Promise { 95 | const result = await fn.invoke({ 96 | InvocationType: 'RequestResponse', 97 | Payload: JSON.stringify(payload) 98 | }); 99 | let resultPayload = result.Payload; 100 | if (typeof resultPayload !== 'string') { 101 | // For Buffer / Blob 102 | resultPayload = String(resultPayload); 103 | } 104 | const parsedPayload = JSON.parse(resultPayload); 105 | if (result.FunctionError) { 106 | throw new LambdaError(parsedPayload); 107 | } else { 108 | return parsedPayload; 109 | } 110 | }; 111 | 112 | fn.params = params; 113 | fn.runtime = runtime; 114 | fn.destroy = destroy.bind(null, fn); 115 | fn.invoke = invoke.bind(null, fn); 116 | 117 | fn.functionName = params.FunctionName; 118 | fn.region = params.Region || 'us-west-1'; 119 | fn.version = '$LATEST'; 120 | fn.arn = ''; 121 | fn.timeout = typeof params.Timeout === 'number' ? params.Timeout : 3; 122 | fn.memorySize = 123 | typeof params.MemorySize === 'number' ? params.MemorySize : 128; 124 | 125 | debug('Creating provider %o', Provider.name); 126 | fn.provider = new Provider(fn); 127 | 128 | if (params.Code.ZipFile) { 129 | fn.extractedDir = await unzipToTemp(params.Code.ZipFile); 130 | } 131 | 132 | return fn; 133 | } 134 | 135 | export async function invoke( 136 | fn: Lambda, 137 | params: InvokeParams 138 | ): Promise { 139 | debug('Invoking function %o', fn.functionName); 140 | const result = await fn.provider.invoke(params); 141 | return result; 142 | } 143 | 144 | export async function destroy(fn: Lambda): Promise { 145 | const ops = [fn.provider.destroy()]; 146 | if (fn.extractedDir) { 147 | debug( 148 | 'Deleting directory %o for function %o', 149 | fn.extractedDir, 150 | fn.functionName 151 | ); 152 | ops.push(remove(fn.extractedDir)); 153 | } 154 | await Promise.all(ops); 155 | } 156 | 157 | export async function cleanCacheDir(): Promise { 158 | debug('Deleting fun cache directory %o', funCacheDir); 159 | await remove(funCacheDir); 160 | } 161 | -------------------------------------------------------------------------------- /src/runtime-server.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid/v4'; 2 | import { parse } from 'url'; 3 | import { Server } from 'http'; 4 | import createDebug from 'debug'; 5 | import { run, text } from 'micro'; 6 | import * as createPathMatch from 'path-match'; 7 | 8 | import { once } from './once'; 9 | import { createDeferred, Deferred } from './deferred'; 10 | import { Lambda, InvokeParams, InvokeResult } from './types'; 11 | 12 | const pathMatch = createPathMatch(); 13 | const match = pathMatch('/:version/runtime/:subject/:target/:action?'); 14 | const debug = createDebug('@zeit/fun:runtime-server'); 15 | 16 | function send404(res) { 17 | res.statusCode = 404; 18 | res.end(); 19 | } 20 | 21 | export class RuntimeServer extends Server { 22 | public version: string; 23 | public initDeferred: Deferred; 24 | public resultDeferred: Deferred; 25 | private nextDeferred: Deferred; 26 | private invokeDeferred: Deferred; 27 | private lambda: Lambda; 28 | private currentRequestId: string; 29 | 30 | constructor(fn: Lambda) { 31 | super(); 32 | this.version = '2018-06-01'; 33 | 34 | const serve = this.serve.bind(this); 35 | this.on('request', (req, res) => run(req, res, serve)); 36 | 37 | this.lambda = fn; 38 | this.initDeferred = createDeferred(); 39 | this.resetInvocationState(); 40 | } 41 | 42 | resetInvocationState() { 43 | this.nextDeferred = createDeferred(); 44 | this.invokeDeferred = null; 45 | this.resultDeferred = null; 46 | this.currentRequestId = null; 47 | } 48 | 49 | async serve(req, res): Promise { 50 | debug('%s %s', req.method, req.url); 51 | 52 | let err; 53 | const params = match(parse(req.url).pathname); 54 | if (!params) { 55 | return send404(res); 56 | } 57 | 58 | const { version, subject, target, action } = params; 59 | if (this.version !== version) { 60 | debug( 61 | 'Invalid API version, expected %o but got %o', 62 | this.version, 63 | version 64 | ); 65 | return send404(res); 66 | } 67 | //console.error({ url: req.url, headers: req.headers, params }); 68 | 69 | // Routing logic 70 | if (subject === 'invocation') { 71 | if (target === 'next') { 72 | return this.handleNextInvocation(req, res); 73 | } else { 74 | // Assume it's an "AwsRequestId" 75 | if (action === 'response') { 76 | return this.handleInvocationResponse(req, res, target); 77 | } else if (action === 'error') { 78 | return this.handleInvocationError(req, res, target); 79 | } else { 80 | return send404(res); 81 | } 82 | } 83 | } else if (subject === 'init') { 84 | if (target === 'error') { 85 | return this.handleInitializationError(req, res); 86 | } else { 87 | return send404(res); 88 | } 89 | } else { 90 | return send404(res); 91 | } 92 | } 93 | 94 | async handleNextInvocation(req, res): Promise { 95 | const { initDeferred } = this; 96 | if (initDeferred) { 97 | debug('Runtime successfully initialized'); 98 | this.initDeferred = null; 99 | initDeferred.resolve(); 100 | } 101 | 102 | this.invokeDeferred = createDeferred(); 103 | this.resultDeferred = createDeferred(); 104 | this.nextDeferred.resolve(); 105 | this.nextDeferred = null; 106 | 107 | debug('Waiting for the `invoke()` function to be called'); 108 | req.setTimeout(0); // disable default 2 minute socket timeout 109 | const params = await this.invokeDeferred.promise; 110 | const requestId = uuid(); 111 | this.currentRequestId = requestId; 112 | 113 | // TODO: use dynamic values from lambda params 114 | const deadline = 5000; 115 | const functionArn = 116 | 'arn:aws:lambda:us-west-1:977805900156:function:nate-dump'; 117 | res.setHeader('Lambda-Runtime-Aws-Request-Id', requestId); 118 | res.setHeader('Lambda-Runtime-Invoked-Function-Arn', functionArn); 119 | res.setHeader('Lambda-Runtime-Deadline-Ms', String(deadline)); 120 | const finish = once(res, 'finish'); 121 | res.end(params.Payload); 122 | await finish; 123 | } 124 | 125 | async handleInvocationResponse(req, res, requestId: string): Promise { 126 | // `RequestResponse` = 200 127 | // `Event` = 202 128 | // `DryRun` = 204 129 | const statusCode = 200; 130 | const payload: InvokeResult = { 131 | StatusCode: statusCode, 132 | ExecutedVersion: '$LATEST', 133 | Payload: await text(req) 134 | }; 135 | 136 | res.statusCode = 202; 137 | const finish = once(res, 'finish'); 138 | res.end(); 139 | await finish; 140 | 141 | this.resultDeferred.resolve(payload); 142 | this.resetInvocationState(); 143 | } 144 | 145 | async handleInvocationError(req, res, requestId: string): Promise { 146 | const statusCode = 200; 147 | const payload: InvokeResult = { 148 | StatusCode: statusCode, 149 | FunctionError: 'Handled', 150 | ExecutedVersion: '$LATEST', 151 | Payload: await text(req) 152 | }; 153 | 154 | res.statusCode = 202; 155 | const finish = once(res, 'finish'); 156 | res.end(); 157 | await finish; 158 | 159 | this.resultDeferred.resolve(payload); 160 | this.resetInvocationState(); 161 | } 162 | 163 | async handleInitializationError(req, res): Promise { 164 | const statusCode = 200; 165 | const payload: InvokeResult = { 166 | StatusCode: statusCode, 167 | FunctionError: 'Unhandled', 168 | ExecutedVersion: '$LATEST', 169 | Payload: await text(req) 170 | }; 171 | 172 | res.statusCode = 202; 173 | const finish = once(res, 'finish'); 174 | res.end(); 175 | await finish; 176 | 177 | this.initDeferred.resolve(payload); 178 | } 179 | 180 | async invoke( 181 | params: InvokeParams = { InvocationType: 'RequestResponse' } 182 | ): Promise { 183 | if (this.nextDeferred) { 184 | debug('Waiting for `next` invocation request from runtime'); 185 | await this.nextDeferred.promise; 186 | } 187 | if (!params.Payload) { 188 | params.Payload = '{}'; 189 | } 190 | this.invokeDeferred.resolve(params); 191 | const result = await this.resultDeferred.promise; 192 | return result; 193 | } 194 | 195 | close(callback?: Function): this { 196 | if (this.resultDeferred) { 197 | const statusCode = 200; 198 | this.resultDeferred.resolve({ 199 | StatusCode: statusCode, 200 | FunctionError: 'Unhandled', 201 | ExecutedVersion: '$LATEST', 202 | Payload: JSON.stringify({ 203 | errorMessage: `RequestId: ${ 204 | this.currentRequestId 205 | } Process exited before completing request` 206 | }) 207 | }); 208 | } 209 | super.close(callback); 210 | return this; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/runtimes/nodejs/bootstrap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Credit: https://github.com/lambci/node-custom-lambda/blob/master/v10.x/bootstrap.js 3 | */ 4 | import * as http from 'http'; 5 | 6 | interface LambdaEvent {} 7 | 8 | interface LambdaContext { 9 | callbackWaitsForEmptyEventLoop: boolean; 10 | logGroupName: string; 11 | logStreamName: string; 12 | functionName: string; 13 | memoryLimitInMB: string; 14 | functionVersion: string; 15 | invokeid: string; 16 | awsRequestId: string; 17 | invokedFunctionArn?: string; 18 | getRemainingTimeInMillis(): number; 19 | clientContext?: object; 20 | identity?: string | object; 21 | } 22 | 23 | interface HttpResult { 24 | statusCode: number; 25 | headers: object; 26 | body: string; 27 | } 28 | 29 | type HandlerFunction = ( 30 | event: LambdaEvent, 31 | context?: LambdaContext 32 | ) => Promise; 33 | 34 | const RUNTIME_PATH = '/2018-06-01/runtime'; 35 | 36 | const { 37 | AWS_LAMBDA_FUNCTION_NAME, 38 | AWS_LAMBDA_FUNCTION_VERSION, 39 | AWS_LAMBDA_FUNCTION_MEMORY_SIZE, 40 | AWS_LAMBDA_LOG_GROUP_NAME, 41 | AWS_LAMBDA_LOG_STREAM_NAME, 42 | LAMBDA_TASK_ROOT, 43 | _HANDLER, 44 | AWS_LAMBDA_RUNTIME_API 45 | } = process.env; 46 | 47 | delete process.env.SHLVL; 48 | 49 | const [HOST, PORT] = AWS_LAMBDA_RUNTIME_API.split(':'); 50 | 51 | start(); 52 | 53 | // Simple `util.promisify()` polyfill for Node 6.x 54 | function promisify(fn) { 55 | return function(...args) { 56 | return new Promise((resolve, reject) => { 57 | args.push((err, result) => { 58 | if (err) return reject(err); 59 | resolve(result); 60 | }); 61 | fn.apply(this, args); 62 | }); 63 | }; 64 | } 65 | 66 | async function start(): Promise { 67 | let handler; 68 | try { 69 | handler = getHandler(); 70 | } catch (e) { 71 | await initError(e); 72 | return process.exit(1); 73 | } 74 | try { 75 | await processEvents(handler); 76 | } catch (e) { 77 | console.error(e); 78 | return process.exit(1); 79 | } 80 | } 81 | 82 | async function processEvents(handler): Promise { 83 | while (true) { 84 | const { event, context } = await nextInvocation(); 85 | let result; 86 | try { 87 | result = await handler(event, context); 88 | } catch (e) { 89 | await invokeError(e, context); 90 | continue; 91 | } 92 | await invokeResponse(result, context); 93 | } 94 | } 95 | 96 | async function initError(err) { 97 | return postError(`${RUNTIME_PATH}/init/error`, err); 98 | } 99 | 100 | async function nextInvocation() { 101 | const res = await request({ path: `${RUNTIME_PATH}/invocation/next` }); 102 | 103 | if (res.statusCode !== 200) { 104 | throw new Error( 105 | `Unexpected /invocation/next response: ${JSON.stringify(res)}` 106 | ); 107 | } 108 | 109 | if (res.headers['lambda-runtime-trace-id']) { 110 | process.env._X_AMZN_TRACE_ID = res.headers['lambda-runtime-trace-id']; 111 | } else { 112 | delete process.env._X_AMZN_TRACE_ID; 113 | } 114 | 115 | const deadlineMs = Number(res.headers['lambda-runtime-deadline-ms']); 116 | const awsRequestId = res.headers['lambda-runtime-aws-request-id']; 117 | 118 | const context: LambdaContext = { 119 | callbackWaitsForEmptyEventLoop: false, 120 | logGroupName: AWS_LAMBDA_LOG_GROUP_NAME, 121 | logStreamName: AWS_LAMBDA_LOG_STREAM_NAME, 122 | functionName: AWS_LAMBDA_FUNCTION_NAME, 123 | memoryLimitInMB: AWS_LAMBDA_FUNCTION_MEMORY_SIZE, 124 | functionVersion: AWS_LAMBDA_FUNCTION_VERSION, 125 | invokeid: awsRequestId, 126 | awsRequestId, 127 | invokedFunctionArn: res.headers['lambda-runtime-invoked-function-arn'], 128 | getRemainingTimeInMillis: () => deadlineMs - Date.now() 129 | }; 130 | 131 | if (res.headers['lambda-runtime-client-context']) { 132 | context.clientContext = JSON.parse( 133 | res.headers['lambda-runtime-client-context'] 134 | ); 135 | } 136 | 137 | if (res.headers['lambda-runtime-cognito-identity']) { 138 | context.identity = JSON.parse( 139 | res.headers['lambda-runtime-cognito-identity'] 140 | ); 141 | } 142 | 143 | const event = JSON.parse(res.body); 144 | 145 | return { event, context }; 146 | } 147 | 148 | async function invokeResponse(result, context) { 149 | const res = await request({ 150 | method: 'POST', 151 | path: `${RUNTIME_PATH}/invocation/${context.awsRequestId}/response`, 152 | body: JSON.stringify(result) 153 | }); 154 | if (res.statusCode !== 202) { 155 | throw new Error( 156 | `Unexpected /invocation/response response: ${JSON.stringify(res)}` 157 | ); 158 | } 159 | } 160 | 161 | async function invokeError(err, context) { 162 | return postError( 163 | `${RUNTIME_PATH}/invocation/${context.awsRequestId}/error`, 164 | err 165 | ); 166 | } 167 | 168 | async function postError(path, err) { 169 | const lambdaErr = toLambdaErr(err); 170 | const res = await request({ 171 | method: 'POST', 172 | path, 173 | headers: { 174 | 'Content-Type': 'application/json', 175 | 'Lambda-Runtime-Function-Error-Type': lambdaErr.errorType 176 | }, 177 | body: JSON.stringify(lambdaErr) 178 | }); 179 | if (res.statusCode !== 202) { 180 | throw new Error(`Unexpected ${path} response: ${JSON.stringify(res)}`); 181 | } 182 | } 183 | 184 | function getHandler(): HandlerFunction { 185 | const appParts = _HANDLER.split('.'); 186 | 187 | if (appParts.length !== 2) { 188 | throw new Error(`Bad handler ${_HANDLER}`); 189 | } 190 | 191 | const [modulePath, handlerName] = appParts; 192 | 193 | let app; 194 | try { 195 | app = require(`${LAMBDA_TASK_ROOT}/${modulePath}`); 196 | } catch (e) { 197 | if (e.code === 'MODULE_NOT_FOUND') { 198 | throw new Error(`Unable to import module '${modulePath}'`); 199 | } 200 | throw e; 201 | } 202 | 203 | const userHandler = app[handlerName]; 204 | 205 | if (userHandler == null) { 206 | throw new Error( 207 | `Handler '${handlerName}' missing on module '${modulePath}'` 208 | ); 209 | } else if (typeof userHandler !== 'function') { 210 | throw new Error( 211 | `Handler '${handlerName}' from '${modulePath}' is not a function` 212 | ); 213 | } 214 | 215 | return userHandler.length >= 3 ? promisify(userHandler) : userHandler; 216 | } 217 | 218 | async function request(options): Promise { 219 | options.host = HOST; 220 | options.port = PORT; 221 | 222 | return new Promise((resolve, reject) => { 223 | const req = http.request(options, res => { 224 | const bufs = []; 225 | res.on('data', data => bufs.push(data)); 226 | res.on('end', () => 227 | resolve({ 228 | statusCode: res.statusCode, 229 | headers: res.headers, 230 | body: Buffer.concat(bufs).toString('utf8') 231 | }) 232 | ); 233 | res.on('error', reject); 234 | }); 235 | req.on('error', reject); 236 | req.end(options.body); 237 | }); 238 | } 239 | 240 | function toLambdaErr({ name, message, stack }) { 241 | return { 242 | errorType: name, 243 | errorMessage: message, 244 | stackTrace: (stack || '').split('\n').slice(1) 245 | }; 246 | } 247 | -------------------------------------------------------------------------------- /src/runtimes.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import createDebug from 'debug'; 3 | import { createHash, Hash } from 'crypto'; 4 | import * as cachedir from 'cache-or-tmp-directory'; 5 | import { lstat, mkdirp, readdir, remove, readFile, writeFile } from 'fs-extra'; 6 | 7 | import { Runtime } from './types'; 8 | import * as go1x from './runtimes/go1.x'; 9 | import * as nodejs6 from './runtimes/nodejs6.10'; 10 | import * as nodejs8 from './runtimes/nodejs8.10'; 11 | import * as python27 from './runtimes/python2.7'; 12 | import * as python36 from './runtimes/python3.6'; 13 | import * as python37 from './runtimes/python3.7'; 14 | 15 | const debug = createDebug('@zeit/fun:runtimes'); 16 | const runtimesDir = join(__dirname, 'runtimes'); 17 | 18 | interface Runtimes { 19 | [name: string]: Runtime; 20 | } 21 | 22 | interface RuntimeImpl { 23 | init?(runtime: Runtime): Promise; 24 | } 25 | 26 | export const runtimes: Runtimes = {}; 27 | 28 | export const funCacheDir = cachedir('co.zeit.fun'); 29 | 30 | function createRuntime( 31 | runtimes: Runtimes, 32 | name: string, 33 | mod?: RuntimeImpl 34 | ): void { 35 | const runtime: Runtime = { 36 | name, 37 | runtimeDir: join(runtimesDir, name), 38 | ...mod 39 | }; 40 | runtimes[name] = runtime; 41 | } 42 | 43 | createRuntime(runtimes, 'provided'); 44 | createRuntime(runtimes, 'go1.x', go1x); 45 | createRuntime(runtimes, 'nodejs'); 46 | createRuntime(runtimes, 'nodejs6.10', nodejs6); 47 | createRuntime(runtimes, 'nodejs8.10', nodejs8); 48 | createRuntime(runtimes, 'python'); 49 | createRuntime(runtimes, 'python2.7', python27); 50 | createRuntime(runtimes, 'python3.6', python36); 51 | createRuntime(runtimes, 'python3.7', python37); 52 | 53 | /** 54 | * Reads the file path `f` as an ascii string. 55 | * Returns `null` if the file does not exist. 56 | */ 57 | async function getCachedRuntimeSha(f: string): Promise { 58 | try { 59 | return await readFile(f, 'ascii'); 60 | } catch (err) { 61 | if (err.code === 'ENOENT') { 62 | return null; 63 | } 64 | throw err; 65 | } 66 | } 67 | 68 | const runtimeShaPromises: Map> = new Map(); 69 | 70 | /** 71 | * Calculates a sha256 of the files provided for a runtime. If any of the 72 | * `bootstrap` or other dependent files change then the shasum will be 73 | * different and the user's existing runtime cache will be invalidated. 74 | */ 75 | async function _calculateRuntimeSha(src: string): Promise { 76 | debug('calculateRuntimeSha(%o)', src); 77 | const hash = createHash('sha256'); 78 | await calculateRuntimeShaDir(src, hash); 79 | const sha = hash.digest('hex'); 80 | debug('Calculated runtime sha for %o: %o', src, sha); 81 | return sha; 82 | } 83 | async function calculateRuntimeShaDir(src: string, hash: Hash): Promise { 84 | const entries = await readdir(src); 85 | for (const entry of entries) { 86 | const srcPath = join(src, entry); 87 | const s = await lstat(srcPath); 88 | if (s.isDirectory()) { 89 | await calculateRuntimeShaDir(srcPath, hash); 90 | } else { 91 | const contents = await readFile(srcPath); 92 | hash.update(contents); 93 | } 94 | } 95 | } 96 | function calculateRuntimeSha(src: string): Promise { 97 | // The sha calculation promise gets memoized because the runtime code 98 | // won't be changing (it's within a published npm module, after all) 99 | let p = runtimeShaPromises.get(src); 100 | if (!p) { 101 | p = _calculateRuntimeSha(src); 102 | runtimeShaPromises.set(src, p); 103 | } 104 | return p; 105 | } 106 | 107 | /** 108 | * Until https://github.com/zeit/pkg/issues/639 is resolved, we have to 109 | * implement the `copy()` operation without relying on `fs.copyFile()`. 110 | */ 111 | async function copy(src: string, dest: string): Promise { 112 | debug('copy(%o, %o)', src, dest); 113 | const [entries] = await Promise.all([readdir(src), mkdirp(dest)]); 114 | debug('Entries: %o', entries); 115 | 116 | for (const entry of entries) { 117 | const srcPath = join(src, entry); 118 | const destPath = join(dest, entry); 119 | const s = await lstat(srcPath); 120 | if (s.isDirectory()) { 121 | await copy(srcPath, destPath); 122 | } else { 123 | const contents = await readFile(srcPath); 124 | await writeFile(destPath, contents, { mode: s.mode }); 125 | } 126 | } 127 | } 128 | 129 | // The Promises map is to ensure that a runtime is only initialized once 130 | const initPromises: Map> = new Map(); 131 | 132 | async function _initializeRuntime(runtime: Runtime): Promise { 133 | const cacheDir = join(funCacheDir, 'runtimes', runtime.name); 134 | const cacheShaFile = join(cacheDir, '.cache-sha'); 135 | const [cachedRuntimeSha, runtimeSha] = await Promise.all([ 136 | getCachedRuntimeSha(cacheShaFile), 137 | calculateRuntimeSha(runtime.runtimeDir) 138 | ]); 139 | runtime.cacheDir = cacheDir; 140 | if (cachedRuntimeSha === runtimeSha) { 141 | debug( 142 | 'Runtime %o is already initialized at %o', 143 | runtime.name, 144 | cacheDir 145 | ); 146 | } else { 147 | debug('Initializing %o runtime at %o', runtime.name, cacheDir); 148 | try { 149 | await mkdirp(cacheDir); 150 | 151 | // The runtime directory is copied from the module dir to the cache 152 | // dir. This is so that when compiled through `pkg`, then the 153 | // bootstrap files exist on a real file system so that `execve()` 154 | // works as expected. 155 | await copy(runtime.runtimeDir, cacheDir); 156 | 157 | // Perform any runtime-specific initialization logic 158 | if (typeof runtime.init === 'function') { 159 | await runtime.init(runtime); 160 | } 161 | 162 | await writeFile(join(cacheDir, '.cache-sha'), runtimeSha); 163 | } catch (err) { 164 | debug( 165 | 'Runtime %o `init()` failed %o. Cleaning up cache dir %o', 166 | runtime.name, 167 | err, 168 | cacheDir 169 | ); 170 | try { 171 | await remove(cacheDir); 172 | } catch (err2) { 173 | debug('Cleaning up cache dir failed: %o', err2); 174 | } 175 | throw err; 176 | } 177 | } 178 | } 179 | 180 | export async function initializeRuntime( 181 | target: string | Runtime 182 | ): Promise { 183 | let runtime: Runtime; 184 | if (typeof target === 'string') { 185 | runtime = runtimes[target]; 186 | if (!runtime) { 187 | throw new Error(`Could not find runtime with name "${target}"`); 188 | } 189 | } else { 190 | runtime = target; 191 | } 192 | 193 | let p = initPromises.get(runtime); 194 | if (p) { 195 | await p; 196 | } else { 197 | p = _initializeRuntime(runtime); 198 | initPromises.set(runtime, p); 199 | 200 | try { 201 | await p; 202 | } finally { 203 | // Once the initialization is complete, remove the Promise. This is so that 204 | // in case the cache is deleted during runtime, and then another Lambda 205 | // function is created, the in-memory cache doesn't think the runtime is 206 | // already initialized and will check the filesystem cache again. 207 | initPromises.delete(runtime); 208 | } 209 | } 210 | 211 | return runtime; 212 | } 213 | -------------------------------------------------------------------------------- /src/providers/native/index.ts: -------------------------------------------------------------------------------- 1 | import * as ms from 'ms'; 2 | import * as uuid from 'uuid/v4'; 3 | import createDebug from 'debug'; 4 | import { AddressInfo } from 'net'; 5 | import * as listen from 'async-listen'; 6 | import { Pool, createPool } from 'generic-pool'; 7 | import { delimiter, basename, join, resolve } from 'path'; 8 | import { ChildProcess, spawn } from 'child_process'; 9 | import { RuntimeServer } from '../../runtime-server'; 10 | import { 11 | LambdaParams, 12 | InvokeParams, 13 | InvokeResult, 14 | Lambda, 15 | Provider 16 | } from '../../types'; 17 | 18 | const isWin = process.platform === 'win32'; 19 | const debug = createDebug('@zeit/fun:providers/native'); 20 | 21 | export default class NativeProvider implements Provider { 22 | pool: Pool; 23 | lambda: Lambda; 24 | params: LambdaParams; 25 | runtimeApis: WeakMap; 26 | 27 | constructor(fn: Lambda, params: LambdaParams) { 28 | const factory = { 29 | create: this.createProcess.bind(this), 30 | destroy: this.destroyProcess.bind(this) 31 | }; 32 | const opts = { 33 | min: 0, 34 | max: 10, 35 | acquireTimeoutMillis: ms('5s') 36 | 37 | // XXX: These 3 options are commented out because they cause 38 | // the tests to never complete (doesn't exit cleanly). 39 | 40 | // How often to check if a process needs to be shut down due to not 41 | // being invoked 42 | //evictionRunIntervalMillis: ms('10s'), 43 | 44 | // How long a process is allowed to stay alive without being invoked 45 | //idleTimeoutMillis: ms('15s') 46 | }; 47 | this.lambda = fn; 48 | this.params = params; 49 | this.runtimeApis = new WeakMap(); 50 | this.pool = createPool(factory, opts); 51 | this.pool.on('factoryCreateError', err => { 52 | console.error('factoryCreateError', { err }); 53 | }); 54 | this.pool.on('factoryDestroyError', err => { 55 | console.error('factoryDestroyError', { err }); 56 | }); 57 | } 58 | 59 | async createProcess(): Promise { 60 | const { runtime, params, region, version, extractedDir } = this.lambda; 61 | const binDir = join(runtime.cacheDir, 'bin'); 62 | const bootstrap = join( 63 | runtime.cacheDir, 64 | isWin ? 'bootstrap.js' : 'bootstrap' 65 | ); 66 | 67 | const server = new RuntimeServer(this.lambda); 68 | await listen(server, 0, '127.0.0.1'); 69 | const { port } = server.address() as AddressInfo; 70 | 71 | debug('Creating process %o', bootstrap); 72 | const taskDir = resolve(extractedDir || params.Code.Directory); 73 | const functionName = params.FunctionName || basename(taskDir); 74 | const memorySize = 75 | typeof params.MemorySize === 'number' ? params.MemorySize : 128; 76 | const logGroupName = `aws/lambda/${functionName}`; 77 | const logStreamName = `2019/01/12/[${version}]${uuid().replace( 78 | /\-/g, 79 | '' 80 | )}`; 81 | 82 | // https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html 83 | const env = { 84 | // Non-reserved env vars (can overwrite with params) 85 | PATH: `${binDir}${delimiter}${process.env.PATH}`, 86 | LANG: 'en_US.UTF-8', 87 | 88 | // User env vars 89 | ...(params.Environment && params.Environment.Variables), 90 | 91 | // Restricted env vars 92 | _HANDLER: params.Handler, 93 | AWS_REGION: region, 94 | AWS_DEFAULT_REGION: region, 95 | AWS_EXECUTION_ENV: `AWS_Lambda_${params.Runtime}`, 96 | AWS_LAMBDA_FUNCTION_NAME: functionName, 97 | AWS_LAMBDA_FUNCTION_VERSION: version, 98 | AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(params.MemorySize || 128), 99 | AWS_LAMBDA_RUNTIME_API: `127.0.0.1:${port}`, 100 | AWS_LAMBDA_LOG_GROUP_NAME: logGroupName, 101 | AWS_LAMBDA_LOG_STREAM_NAME: logStreamName, 102 | LAMBDA_RUNTIME_DIR: runtime.cacheDir, 103 | LAMBDA_TASK_ROOT: taskDir, 104 | TZ: ':UTC' 105 | }; 106 | 107 | let bin: string = bootstrap; 108 | const args: string[] = []; 109 | if (isWin) { 110 | args.push(bootstrap); 111 | bin = process.execPath; 112 | } 113 | 114 | const proc = spawn(bin, args, { 115 | env, 116 | cwd: taskDir, 117 | stdio: ['ignore', 'inherit', 'inherit'] 118 | }); 119 | this.runtimeApis.set(proc, server); 120 | 121 | proc.on('exit', async (code, signal) => { 122 | debug( 123 | 'Process (pid=%o) exited with code %o, signal %o', 124 | proc.pid, 125 | code, 126 | signal 127 | ); 128 | const server = this.runtimeApis.get(proc); 129 | if (server) { 130 | debug('Shutting down Runtime API for %o', proc.pid); 131 | server.close(); 132 | this.runtimeApis.delete(proc); 133 | } else { 134 | debug( 135 | 'No Runtime API server associated with process %o. This SHOULD NOT happen!', 136 | proc.pid 137 | ); 138 | } 139 | }); 140 | 141 | return proc; 142 | } 143 | 144 | async destroyProcess(proc: ChildProcess): Promise { 145 | try { 146 | // Unfreeze the process first so it is able to process the `SIGTERM` 147 | // signal and exit cleanly (clean up child processes, etc.) 148 | this.unfreezeProcess(proc); 149 | 150 | debug('Stopping process %o', proc.pid); 151 | process.kill(proc.pid, 'SIGTERM'); 152 | } catch (err) { 153 | // ESRCH means that the process ID no longer exists, which is fine 154 | // in this case since we're shutting down the process anyways 155 | if (err.code !== 'ESRCH') { 156 | throw err; 157 | } 158 | } 159 | } 160 | 161 | freezeProcess(proc: ChildProcess) { 162 | // `SIGSTOP` is not supported on Windows 163 | if (!isWin) { 164 | debug('Freezing process %o', proc.pid); 165 | process.kill(proc.pid, 'SIGSTOP'); 166 | } 167 | } 168 | 169 | unfreezeProcess(proc: ChildProcess) { 170 | // `SIGCONT` is not supported on Windows 171 | if (!isWin) { 172 | debug('Unfreezing process %o', proc.pid); 173 | process.kill(proc.pid, 'SIGCONT'); 174 | } 175 | } 176 | 177 | async invoke(params: InvokeParams): Promise { 178 | let result: InvokeResult; 179 | const proc = await this.pool.acquire(); 180 | const server = this.runtimeApis.get(proc); 181 | 182 | if (server.initDeferred) { 183 | // The lambda process has just booted up, so wait for the 184 | // initialization API call to come in before proceeding 185 | debug('Waiting for init on process %o', proc.pid); 186 | const initError = await server.initDeferred.promise; 187 | if (initError) { 188 | debug( 189 | 'Lambda got initialization error on process %o', 190 | proc.pid 191 | ); 192 | // An error happend during initialization, so remove the 193 | // process from the pool and return the error to the caller 194 | await this.pool.destroy(proc); 195 | return initError; 196 | } 197 | debug('Lambda is initialized for process %o', proc.pid); 198 | } else { 199 | // The lambda process is being re-used for a subsequent 200 | // invocation, so unfreeze the process first 201 | this.unfreezeProcess(proc); 202 | } 203 | 204 | try { 205 | result = await server.invoke(params); 206 | } catch (err) { 207 | result = { 208 | StatusCode: 200, 209 | FunctionError: 'Unhandled', 210 | ExecutedVersion: '$LATEST', 211 | // TODO: make this into a `server.createError()` function 212 | Payload: JSON.stringify({ 213 | errorMessage: err.message 214 | }) 215 | }; 216 | } 217 | 218 | if (result.FunctionError === 'Unhandled') { 219 | // An "Unhandled" error means either init error or the process 220 | // exited before sending the response. In either case, the process 221 | // is unhealthy and needs to be removed from the pool 222 | await this.pool.destroy(proc); 223 | } else { 224 | // Either a successful response, or a "Handled" error. 225 | // The process may be re-used for the next invocation. 226 | this.freezeProcess(proc); 227 | await this.pool.release(proc); 228 | } 229 | 230 | return result; 231 | } 232 | 233 | async destroy(): Promise { 234 | debug('Draining pool'); 235 | await this.pool.drain(); 236 | this.pool.clear(); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/runtimes/go1.x/bootstrap.go: -------------------------------------------------------------------------------- 1 | // Credits: 2 | // https://git.io/fh2AB 3 | // https://binx.io/blog/2018/12/03/aws-lambda-custom-bootstrap-in-go 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "encoding/hex" 10 | "fmt" 11 | "github.com/aws/aws-lambda-go/lambda/messages" 12 | "github.com/phayes/freeport" 13 | "io/ioutil" 14 | "log" 15 | "math/rand" 16 | "net" 17 | "net/http" 18 | "net/rpc" 19 | "os" 20 | "os/exec" 21 | "os/signal" 22 | "path" 23 | "reflect" 24 | "strconv" 25 | "syscall" 26 | "time" 27 | ) 28 | 29 | func main() { 30 | rand.Seed(time.Now().UTC().UnixNano()) 31 | 32 | mockContext := &MockLambdaContext{ 33 | FnName: getEnv("AWS_LAMBDA_FUNCTION_NAME", "test"), 34 | Handler: getEnv("AWS_LAMBDA_FUNCTION_HANDLER", getEnv("_HANDLER", "handler")), 35 | Version: getEnv("AWS_LAMBDA_FUNCTION_VERSION", "$LATEST"), 36 | MemSize: getEnv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "1536"), 37 | Timeout: getEnv("AWS_LAMBDA_FUNCTION_TIMEOUT", "300"), 38 | Region: getEnv("AWS_REGION", getEnv("AWS_DEFAULT_REGION", "us-east-1")), 39 | AccountId: getEnv("AWS_ACCOUNT_ID", strconv.FormatInt(int64(rand.Int31()), 10)), 40 | Start: time.Now(), 41 | Pid: 1, 42 | } 43 | mockContext.ParseTimeout() 44 | 45 | awsAccessKey := getEnv("AWS_ACCESS_KEY", getEnv("AWS_ACCESS_KEY_ID", "SOME_ACCESS_KEY_ID")) 46 | awsSecretKey := getEnv("AWS_SECRET_KEY", getEnv("AWS_SECRET_ACCESS_KEY", "SOME_SECRET_ACCESS_KEY")) 47 | awsSessionToken := getEnv("AWS_SESSION_TOKEN", os.Getenv("AWS_SECURITY_TOKEN")) 48 | taskRoot := getEnv("LAMBDA_TASK_ROOT", "/var/task") 49 | 50 | handlerPath := path.Join(taskRoot, mockContext.Handler) 51 | 52 | os.Setenv("AWS_LAMBDA_FUNCTION_NAME", mockContext.FnName) 53 | os.Setenv("AWS_LAMBDA_FUNCTION_VERSION", mockContext.Version) 54 | os.Setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", mockContext.MemSize) 55 | os.Setenv("AWS_LAMBDA_LOG_GROUP_NAME", "/aws/lambda/"+mockContext.FnName) 56 | os.Setenv("AWS_LAMBDA_LOG_STREAM_NAME", logStreamName(mockContext.Version)) 57 | os.Setenv("AWS_REGION", mockContext.Region) 58 | os.Setenv("AWS_DEFAULT_REGION", mockContext.Region) 59 | os.Setenv("_HANDLER", mockContext.Handler) 60 | 61 | port, err := freeport.GetFreePort() 62 | if err != nil { 63 | log.Fatal(fmt.Errorf("Freeport Error %s", err)) 64 | } 65 | portStr := strconv.Itoa(port) 66 | 67 | var cmd *exec.Cmd 68 | cmd = exec.Command(handlerPath) 69 | 70 | cmd.Env = append(os.Environ(), 71 | "_LAMBDA_SERVER_PORT="+portStr, 72 | "AWS_ACCESS_KEY="+awsAccessKey, 73 | "AWS_ACCESS_KEY_ID="+awsAccessKey, 74 | "AWS_SECRET_KEY="+awsSecretKey, 75 | "AWS_SECRET_ACCESS_KEY="+awsSecretKey, 76 | ) 77 | 78 | if len(awsSessionToken) > 0 { 79 | cmd.Env = append(cmd.Env, 80 | "AWS_SESSION_TOKEN="+awsSessionToken, 81 | "AWS_SECURITY_TOKEN="+awsSessionToken, 82 | ) 83 | } 84 | cmd.Stdout = os.Stdout 85 | cmd.Stderr = os.Stderr 86 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 87 | 88 | if err = cmd.Start(); err != nil { 89 | defer abortRequest(mockContext, err) 90 | return 91 | } 92 | 93 | mockContext.Pid = cmd.Process.Pid 94 | p, _ := os.FindProcess(-mockContext.Pid) 95 | defer p.Signal(syscall.SIGKILL) 96 | 97 | // Terminate the child process upon SIGINT / SIGTERM 98 | c := make(chan os.Signal, 1) 99 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 100 | go func() { 101 | <-c 102 | p, _ := os.FindProcess(-mockContext.Pid) 103 | p.Signal(syscall.SIGKILL) 104 | 105 | os.Exit(0) 106 | }() 107 | 108 | var conn net.Conn 109 | for { 110 | conn, err = net.Dial("tcp", ":"+portStr) 111 | if mockContext.HasExpired() { 112 | defer abortRequest(mockContext, mockContext.TimeoutErr()) 113 | return 114 | } 115 | if err == nil { 116 | break 117 | } 118 | if oerr, ok := err.(*net.OpError); ok { 119 | // Connection refused, try again 120 | if oerr.Op == "dial" && oerr.Net == "tcp" { 121 | time.Sleep(5 * time.Millisecond) 122 | continue 123 | } 124 | } 125 | defer abortRequest(mockContext, err) 126 | return 127 | } 128 | 129 | mockContext.Rpc = rpc.NewClient(conn) 130 | 131 | for { 132 | err = mockContext.Rpc.Call("Function.Ping", messages.PingRequest{}, &messages.PingResponse{}) 133 | if mockContext.HasExpired() { 134 | defer abortRequest(mockContext, mockContext.TimeoutErr()) 135 | return 136 | } 137 | if err == nil { 138 | break 139 | } 140 | time.Sleep(5 * time.Millisecond) 141 | } 142 | 143 | // XXX: The Go runtime seems to amortize the startup time, reset it here 144 | mockContext.Start = time.Now() 145 | 146 | // If we got to here then the handler process has initialized successfully 147 | mockContext.ProcessEvents() 148 | } 149 | 150 | func abortRequest(mockContext *MockLambdaContext, err error) { 151 | log.Fatal(err) 152 | } 153 | 154 | func getEnv(key, fallback string) string { 155 | value := os.Getenv(key) 156 | if value != "" { 157 | return value 158 | } 159 | return fallback 160 | } 161 | 162 | func logStreamName(version string) string { 163 | randBuf := make([]byte, 16) 164 | rand.Read(randBuf) 165 | 166 | hexBuf := make([]byte, hex.EncodedLen(len(randBuf))) 167 | hex.Encode(hexBuf, randBuf) 168 | 169 | return time.Now().Format("2006/01/02") + "/[" + version + "]" + string(hexBuf) 170 | } 171 | 172 | func getErrorType(err interface{}) string { 173 | if errorType := reflect.TypeOf(err); errorType.Kind() == reflect.Ptr { 174 | return errorType.Elem().Name() 175 | } else { 176 | return errorType.Name() 177 | } 178 | } 179 | 180 | type LambdaError struct { 181 | Message string `json:"errorMessage"` 182 | Type string `json:"errorType,omitempty"` 183 | StackTrace []*messages.InvokeResponse_Error_StackFrame `json:"stackTrace,omitempty"` 184 | } 185 | 186 | type MockLambdaContext struct { 187 | Pid int 188 | FnName string 189 | Handler string 190 | Version string 191 | MemSize string 192 | Timeout string 193 | Region string 194 | AccountId string 195 | Start time.Time 196 | TimeoutDuration time.Duration 197 | Reply *messages.InvokeResponse 198 | Rpc *rpc.Client 199 | } 200 | 201 | func (mc *MockLambdaContext) ProcessEvents() { 202 | awsLambdaRuntimeApi := os.Getenv("AWS_LAMBDA_RUNTIME_API") 203 | if awsLambdaRuntimeApi == "" { 204 | panic("Missing: 'AWS_LAMBDA_RUNTIME_API'") 205 | } 206 | for { 207 | // get the next event 208 | requestUrl := fmt.Sprintf("http://%s/2018-06-01/runtime/invocation/next", awsLambdaRuntimeApi) 209 | resp, err := http.Get(requestUrl) 210 | if err != nil { 211 | log.Fatal(fmt.Errorf("Error getting next invocation: %v", err)) 212 | } 213 | 214 | requestId := resp.Header.Get("Lambda-Runtime-Aws-Request-Id") 215 | eventData, err := ioutil.ReadAll(resp.Body) 216 | if err != nil { 217 | log.Fatal(fmt.Errorf("Error reading body: %s", err)) 218 | } 219 | 220 | err = mc.Rpc.Call("Function.Invoke", mc.Request(requestId, eventData), &mc.Reply) 221 | if err != nil { 222 | log.Fatal(fmt.Errorf("Error invoking RPC call: %s", err)) 223 | } 224 | 225 | responseUrl := fmt.Sprintf("http://%s/2018-06-01/runtime/invocation/%s/response", awsLambdaRuntimeApi, requestId) 226 | req, err := http.NewRequest("POST", responseUrl, bytes.NewBuffer(mc.Reply.Payload)) 227 | if err != nil { 228 | log.Fatal(fmt.Errorf("Error creating response HTTP request: %s", err)) 229 | } 230 | req.Header.Set("Content-Type", "application/json") 231 | 232 | client := &http.Client{} 233 | client.Timeout = time.Second * 1 234 | _, err = client.Do(req) 235 | if err != nil { 236 | log.Fatal(fmt.Errorf("Error sending response: %s", err)) 237 | } 238 | } 239 | } 240 | 241 | func (mc *MockLambdaContext) ParseTimeout() { 242 | timeoutDuration, err := time.ParseDuration(mc.Timeout + "s") 243 | if err != nil { 244 | panic(err) 245 | } 246 | mc.TimeoutDuration = timeoutDuration 247 | } 248 | 249 | func (mc *MockLambdaContext) Deadline() time.Time { 250 | return mc.Start.Add(mc.TimeoutDuration) 251 | } 252 | 253 | func (mc *MockLambdaContext) HasExpired() bool { 254 | return time.Now().After(mc.Deadline()) 255 | } 256 | 257 | func (mc *MockLambdaContext) Request(requestId string, payload []byte) *messages.InvokeRequest { 258 | return &messages.InvokeRequest{ 259 | Payload: payload, 260 | RequestId: requestId, 261 | XAmznTraceId: getEnv("_X_AMZN_TRACE_ID", ""), 262 | InvokedFunctionArn: getEnv("AWS_LAMBDA_FUNCTION_INVOKED_ARN", ""), 263 | Deadline: messages.InvokeRequest_Timestamp{ 264 | Seconds: mc.Deadline().Unix(), 265 | Nanos: int64(mc.Deadline().Nanosecond()), 266 | }, 267 | } 268 | } 269 | 270 | func (mc *MockLambdaContext) TimeoutErr() error { 271 | return fmt.Errorf("%s %s Task timed out after %s.00 seconds", time.Now().Format("2006-01-02T15:04:05.999Z"), 272 | "1234", mc.Timeout) 273 | } 274 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import { basename, join } from 'path'; 2 | import { tmpdir } from 'os'; 3 | import * as execa from 'execa'; 4 | import * as assert from 'assert'; 5 | import { mkdirp, remove, readdir, readFile, stat } from 'fs-extra'; 6 | import { 7 | funCacheDir, 8 | initializeRuntime, 9 | cleanCacheDir, 10 | createFunction, 11 | ValidationError 12 | } from '../src'; 13 | import { generateNodeTarballUrl, installNode } from '../src/install-node'; 14 | import { generatePythonTarballUrl, installPython } from '../src/install-python'; 15 | import { LambdaError } from '../src/errors'; 16 | 17 | const isWin = process.platform === 'win32'; 18 | 19 | export function test_funCacheDir() { 20 | assert.equal('string', typeof funCacheDir); 21 | } 22 | 23 | export function test_LambdaError() { 24 | const err = new LambdaError({ 25 | errorType: 'InitError', 26 | errorMessage: 'I crashed!', 27 | stackTrace: [ 28 | ' at Object. (/Code/zeit/fun/test/functions/nodejs-init-error/handler.js:2:7)', 29 | ' at Module._compile (module.js:652:30)', 30 | ' at Object.Module._extensions..js (module.js:663:10)', 31 | ' at Module.load (module.js:565:32)', 32 | ' at tryModuleLoad (module.js:505:12)', 33 | ' at Function.Module._load (module.js:497:3)', 34 | ' at Module.require (module.js:596:17)', 35 | ' at require (internal/module.js:11:18)', 36 | ' at getHandler (/Library/Caches/co.zeit.fun/runtimes/nodejs/bootstrap.js:151:15)', 37 | ' at /Library/Caches/co.zeit.fun/runtimes/nodejs/bootstrap.js:37:23' 38 | ] 39 | }); 40 | assert.equal('InitError', err.name); 41 | assert.equal('I crashed!', err.message); 42 | assert(err.stack.includes('nodejs-init-error/handler.js')); 43 | } 44 | 45 | // `install-node.ts` tests 46 | export function test_install_node_tarball_url_darwin() { 47 | assert.equal( 48 | 'https://nodejs.org/dist/v8.10.0/node-v8.10.0-darwin-x64.tar.gz', 49 | generateNodeTarballUrl('8.10.0', 'darwin', 'x64') 50 | ); 51 | } 52 | 53 | export function test_install_node_tarball_url_windows() { 54 | assert.equal( 55 | 'https://nodejs.org/dist/v8.10.0/node-v8.10.0-win-x64.zip', 56 | generateNodeTarballUrl('8.10.0', 'win32', 'x64') 57 | ); 58 | } 59 | 60 | export async function test_install_node() { 61 | const version = 'v10.0.0'; 62 | const dest = join( 63 | tmpdir(), 64 | `install-node-${Math.random() 65 | .toString(16) 66 | .substring(2)}` 67 | ); 68 | await mkdirp(dest); 69 | try { 70 | await installNode(dest, version); 71 | const res = await execa(join(dest, 'bin/node'), [ 72 | '-p', 73 | 'process.version' 74 | ]); 75 | assert.equal(res.stdout.trim(), version); 76 | } finally { 77 | // Clean up 78 | try { 79 | await remove(dest); 80 | } catch (err) { 81 | // On Windows EPERM can happen due to anti-virus software like Windows Defender. 82 | // There's nothing that we can do about it so don't fail the test case when it happens. 83 | if (err.code !== 'EPERM') { 84 | throw err; 85 | } 86 | } 87 | } 88 | } 89 | 90 | // `install-python.ts` tests 91 | export function test_install_python_tarball_url() { 92 | assert.equal( 93 | 'https://python-binaries.zeit.sh/python-2.7.12-darwin-x64.tar.gz', 94 | generatePythonTarballUrl('2.7.12', 'darwin', 'x64') 95 | ); 96 | } 97 | 98 | export async function test_install_python() { 99 | const version = '3.6.8'; 100 | const dest = join( 101 | tmpdir(), 102 | `install-python-${Math.random() 103 | .toString(16) 104 | .substring(2)}` 105 | ); 106 | await mkdirp(dest); 107 | try { 108 | await installPython(dest, version); 109 | const res = await execa(join(dest, 'bin/python'), [ 110 | '-c', 111 | 'import platform; print(platform.python_version())' 112 | ]); 113 | assert.equal(res.stdout.trim(), version); 114 | } finally { 115 | // Clean up 116 | //await remove(dest); 117 | } 118 | } 119 | 120 | // Validation 121 | export const test_lambda_properties = async () => { 122 | const fn = await createFunction({ 123 | Code: { 124 | Directory: __dirname + '/functions/nodejs-echo' 125 | }, 126 | Handler: 'handler.handler', 127 | Runtime: 'nodejs', 128 | Environment: { 129 | Variables: { 130 | HELLO: 'world' 131 | } 132 | } 133 | }); 134 | assert.equal(fn.version, '$LATEST'); 135 | //assert.equal(fn.functionName, 'nodejs-echo'); 136 | }; 137 | 138 | export const test_reserved_env = async () => { 139 | let err; 140 | try { 141 | await createFunction({ 142 | Code: { 143 | Directory: __dirname + '/functions/nodejs-echo' 144 | }, 145 | Handler: 'handler.handler', 146 | Runtime: 'nodejs', 147 | Environment: { 148 | Variables: { 149 | AWS_REGION: 'foo', 150 | TZ: 'US/Pacific' 151 | } 152 | } 153 | }); 154 | } catch (_err) { 155 | err = _err; 156 | } 157 | assert(err); 158 | assert(err instanceof ValidationError); 159 | assert.equal(err.name, 'ValidationError'); 160 | assert.deepEqual(err.reserved, ['AWS_REGION', 'TZ']); 161 | assert.equal( 162 | err.toString(), 163 | 'ValidationError: The following environment variables can not be configured: AWS_REGION, TZ' 164 | ); 165 | }; 166 | 167 | // Initialization 168 | export const test_initialize_runtime_with_string = async () => { 169 | const runtime = await initializeRuntime('nodejs8.10'); 170 | assert.equal(typeof runtime.cacheDir, 'string'); 171 | const nodeName = isWin ? 'node.exe' : 'node'; 172 | const nodeStat = await stat(join(runtime.cacheDir, 'bin', nodeName)); 173 | assert(nodeStat.isFile()); 174 | }; 175 | 176 | export const test_initialize_runtime_with_invalid_name = async () => { 177 | let err: Error; 178 | try { 179 | await initializeRuntime('node8.10'); 180 | } catch (_err) { 181 | err = _err; 182 | } 183 | assert(err); 184 | assert.equal('Could not find runtime with name "node8.10"', err.message); 185 | }; 186 | 187 | // Invocation 188 | function testInvoke(fnPromise, test) { 189 | return async function() { 190 | const fn = await fnPromise(); 191 | try { 192 | await test(fn); 193 | } finally { 194 | await fn.destroy(); 195 | } 196 | }; 197 | } 198 | 199 | export const test_nodejs_event = testInvoke( 200 | () => 201 | createFunction({ 202 | Code: { 203 | Directory: __dirname + '/functions/nodejs-echo' 204 | }, 205 | Handler: 'handler.handler', 206 | Runtime: 'nodejs' 207 | }), 208 | async fn => { 209 | const res = await fn.invoke({ 210 | Payload: JSON.stringify({ hello: 'world' }) 211 | }); 212 | assert.equal(res.StatusCode, 200); 213 | assert.equal(res.ExecutedVersion, '$LATEST'); 214 | assert.equal(typeof res.Payload, 'string'); 215 | const payload = JSON.parse(String(res.Payload)); 216 | assert.deepEqual(payload.event, { hello: 'world' }); 217 | } 218 | ); 219 | 220 | export const test_nodejs_no_payload = testInvoke( 221 | () => 222 | createFunction({ 223 | Code: { 224 | Directory: __dirname + '/functions/nodejs-echo' 225 | }, 226 | Handler: 'handler.handler', 227 | Runtime: 'nodejs' 228 | }), 229 | async fn => { 230 | const res = await fn.invoke(); 231 | assert.equal(typeof res.Payload, 'string'); 232 | const payload = JSON.parse(String(res.Payload)); 233 | assert.deepEqual(payload.event, {}); 234 | } 235 | ); 236 | 237 | export const test_nodejs_context = testInvoke( 238 | () => 239 | createFunction({ 240 | Code: { 241 | Directory: __dirname + '/functions/nodejs-echo' 242 | }, 243 | Handler: 'handler.handler', 244 | Runtime: 'nodejs' 245 | }), 246 | async fn => { 247 | const res = await fn.invoke(); 248 | const { context } = JSON.parse(String(res.Payload)); 249 | assert.equal(context.logGroupName, 'aws/lambda/nodejs-echo'); 250 | assert.equal(context.functionName, 'nodejs-echo'); 251 | assert.equal(context.memoryLimitInMB, '128'); 252 | assert.equal(context.functionVersion, '$LATEST'); 253 | } 254 | ); 255 | 256 | export const test_env_vars = testInvoke( 257 | () => 258 | createFunction({ 259 | Code: { 260 | Directory: __dirname + '/functions/nodejs-env' 261 | }, 262 | Handler: 'index.env', 263 | Runtime: 'nodejs' 264 | }), 265 | async fn => { 266 | const res = await fn.invoke(); 267 | assert.equal(typeof res.Payload, 'string'); 268 | const env = JSON.parse(String(res.Payload)); 269 | assert(env.LAMBDA_TASK_ROOT.length > 0); 270 | assert(env.LAMBDA_RUNTIME_DIR.length > 0); 271 | assert.equal(env.TZ, ':UTC'); 272 | assert.equal(env.LANG, 'en_US.UTF-8'); 273 | assert.equal(env._HANDLER, 'index.env'); 274 | assert.equal(env.AWS_LAMBDA_FUNCTION_VERSION, '$LATEST'); 275 | assert.equal(env.AWS_EXECUTION_ENV, 'AWS_Lambda_nodejs'); 276 | assert.equal(env.AWS_LAMBDA_FUNCTION_NAME, 'nodejs-env'); 277 | assert.equal(env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, '128'); 278 | } 279 | ); 280 | 281 | export const test_double_invoke_serial = testInvoke( 282 | () => 283 | createFunction({ 284 | Code: { 285 | Directory: __dirname + '/functions/nodejs-pid' 286 | }, 287 | Handler: 'index.pid', 288 | Runtime: 'nodejs' 289 | }), 290 | async fn => { 291 | let res; 292 | 293 | // Invoke once, will fire up a new lambda process 294 | res = await fn.invoke(); 295 | assert.equal(typeof res.Payload, 'string'); 296 | const pid = JSON.parse(String(res.Payload)); 297 | assert.equal(typeof pid, 'number'); 298 | assert.notEqual(pid, process.pid); 299 | 300 | // Invoke a second time, the same lambda process will be used 301 | res = await fn.invoke(); 302 | assert.equal(typeof res.Payload, 'string'); 303 | const pid2 = JSON.parse(String(res.Payload)); 304 | assert.equal(pid, pid2); 305 | } 306 | ); 307 | 308 | export const test_double_invoke_parallel = testInvoke( 309 | () => 310 | createFunction({ 311 | Code: { 312 | Directory: __dirname + '/functions/nodejs-pid' 313 | }, 314 | Handler: 'index.pid', 315 | Runtime: 'nodejs' 316 | }), 317 | async fn => { 318 | const [res1, res2] = await Promise.all([fn.invoke(), fn.invoke()]); 319 | const pid1 = JSON.parse(String(res1.Payload)); 320 | const pid2 = JSON.parse(String(res2.Payload)); 321 | assert.notEqual(pid1, process.pid); 322 | assert.notEqual(pid2, process.pid); 323 | 324 | // This assert always passed on my MacBook 12", but is flaky on 325 | // CircleCI's containers due to the second worker process not yet 326 | // being in an initialized state. 327 | // assert.notEqual(pid1, pid2); 328 | } 329 | ); 330 | 331 | export const test_lambda_invoke = testInvoke( 332 | () => 333 | createFunction({ 334 | Code: { 335 | Directory: __dirname + '/functions/nodejs-echo' 336 | }, 337 | Handler: 'handler.handler', 338 | Runtime: 'nodejs' 339 | }), 340 | async fn => { 341 | const payload = await fn({ hello: 'world' }); 342 | assert.deepEqual(payload.event, { hello: 'world' }); 343 | } 344 | ); 345 | 346 | // `fun` should be resilient to its runtime cache being wiped away during 347 | // runtime. At least, in between function creations. Consider a user running 348 | // `now dev cache clean` while a `now dev` server is running, and then the 349 | // user does a hard refresh to re-create the Lambda functions. 350 | interface Hello { 351 | event: { 352 | hello: string; 353 | }; 354 | } 355 | export const test_clean_cache_dir_recovery = async () => { 356 | await cleanCacheDir(); 357 | const fn = await createFunction({ 358 | Code: { 359 | Directory: __dirname + '/functions/nodejs-echo' 360 | }, 361 | Handler: 'handler.handler', 362 | Runtime: 'nodejs' 363 | }); 364 | try { 365 | const payload = await fn({ hello: 'world' }); 366 | assert.deepEqual(payload.event, { hello: 'world' }); 367 | } finally { 368 | await fn.destroy(); 369 | } 370 | }; 371 | 372 | // `provided` runtime 373 | export const test_provided_bash_echo = testInvoke( 374 | () => 375 | createFunction({ 376 | Code: { 377 | Directory: __dirname + '/functions/provided-bash-echo' 378 | }, 379 | Handler: 'handler.handler', 380 | Runtime: 'provided' 381 | }), 382 | async fn => { 383 | const payload = await fn({ hello: 'world' }); 384 | assert.deepEqual(payload, { hello: 'world' }); 385 | } 386 | ); 387 | 388 | // `nodejs6.10` runtime 389 | export const test_nodejs610_version = testInvoke( 390 | () => 391 | createFunction({ 392 | Code: { 393 | Directory: __dirname + '/functions/nodejs-version' 394 | }, 395 | Handler: 'handler.handler', 396 | Runtime: 'nodejs6.10' 397 | }), 398 | async fn => { 399 | const versions = await fn({ hello: 'world' }); 400 | assert.equal(versions.node, '6.10.0'); 401 | } 402 | ); 403 | 404 | // `nodejs8.10` runtime 405 | export const test_nodejs810_version = testInvoke( 406 | () => 407 | createFunction({ 408 | Code: { 409 | Directory: __dirname + '/functions/nodejs-version' 410 | }, 411 | Handler: 'handler.handler', 412 | Runtime: 'nodejs8.10' 413 | }), 414 | async fn => { 415 | const versions = await fn({ hello: 'world' }); 416 | assert.equal(versions.node, '8.10.0'); 417 | } 418 | ); 419 | 420 | export const test_nodejs810_handled_error = testInvoke( 421 | () => 422 | createFunction({ 423 | Code: { 424 | Directory: __dirname + '/functions/nodejs-eval' 425 | }, 426 | Handler: 'handler.handler', 427 | Runtime: 'nodejs8.10' 428 | }), 429 | async fn => { 430 | let err; 431 | const error = 'this is a handled error'; 432 | try { 433 | await fn({ error }); 434 | } catch (_err) { 435 | err = _err; 436 | } 437 | assert(err); 438 | assert.equal(err.message, error); 439 | 440 | const { result } = await fn({ code: '1 + 1' }); 441 | assert.equal(result, 2); 442 | } 443 | ); 444 | 445 | export const test_nodejs810_exit_in_handler = testInvoke( 446 | () => 447 | createFunction({ 448 | Code: { 449 | Directory: __dirname + '/functions/nodejs-eval' 450 | }, 451 | Handler: 'handler.handler', 452 | Runtime: 'nodejs8.10' 453 | }), 454 | async fn => { 455 | let err; 456 | try { 457 | await fn({ code: 'process.exit(5)' }); 458 | } catch (_err) { 459 | err = _err; 460 | } 461 | assert(err); 462 | assert( 463 | err.message.includes('Process exited before completing request') 464 | ); 465 | } 466 | ); 467 | 468 | // `go1.x` runtime 469 | export const test_go1x_echo = testInvoke( 470 | () => 471 | createFunction({ 472 | Code: { 473 | Directory: __dirname + '/functions/go-echo' 474 | }, 475 | Handler: 'handler', 476 | Runtime: 'go1.x' 477 | }), 478 | async fn => { 479 | const payload = await fn({ hello: 'world' }); 480 | assert.deepEqual(payload, { hello: 'world' }); 481 | } 482 | ); 483 | 484 | // `python` runtime 485 | export const test_python_hello = testInvoke( 486 | () => 487 | createFunction({ 488 | Code: { 489 | Directory: __dirname + '/functions/python-hello' 490 | }, 491 | Handler: 'hello.hello_handler', 492 | Runtime: 'python' 493 | }), 494 | async fn => { 495 | const payload = await fn({ first_name: 'John', last_name: 'Smith' }); 496 | assert.deepEqual(payload, { message: 'Hello John Smith!' }); 497 | } 498 | ); 499 | 500 | // `python2.7` runtime 501 | export const test_python27_version = testInvoke( 502 | () => 503 | createFunction({ 504 | Code: { 505 | Directory: __dirname + '/functions/python-version' 506 | }, 507 | Handler: 'handler.handler', 508 | Runtime: 'python2.7' 509 | }), 510 | async fn => { 511 | const payload = await fn(); 512 | assert.equal(payload['platform.python_version'], '2.7.12'); 513 | } 514 | ); 515 | 516 | // `python3.6` runtime 517 | export const test_python36_version = testInvoke( 518 | () => 519 | createFunction({ 520 | Code: { 521 | Directory: __dirname + '/functions/python-version' 522 | }, 523 | Handler: 'handler.handler', 524 | Runtime: 'python3.6' 525 | }), 526 | async fn => { 527 | const payload = await fn(); 528 | assert.equal(payload['platform.python_version'], '3.6.8'); 529 | } 530 | ); 531 | 532 | // `python3.7` runtime 533 | export const test_python37_version = testInvoke( 534 | () => 535 | createFunction({ 536 | Code: { 537 | Directory: __dirname + '/functions/python-version' 538 | }, 539 | Handler: 'handler.handler', 540 | Runtime: 'python3.7' 541 | }), 542 | async fn => { 543 | const payload = await fn(); 544 | assert.equal(payload['platform.python_version'], '3.7.2'); 545 | } 546 | ); 547 | 548 | // `ZipFile` Buffer support 549 | export const test_lambda_zip_file_buffer = testInvoke( 550 | async () => { 551 | return await createFunction({ 552 | Code: { 553 | ZipFile: await readFile(__dirname + '/functions/nodejs-env.zip') 554 | }, 555 | Handler: 'index.env', 556 | Runtime: 'nodejs', 557 | Environment: { 558 | Variables: { 559 | HELLO: 'world' 560 | } 561 | } 562 | }); 563 | }, 564 | async fn => { 565 | const env = await fn(); 566 | assert.equal(env.HELLO, 'world'); 567 | // Assert that the `TASK_ROOT` dir includes the "zeit-fun-" prefix 568 | assert(/^zeit-fun-/.test(basename(env.LAMBDA_TASK_ROOT))); 569 | } 570 | ); 571 | 572 | // `ZipFile` string support 573 | export const test_lambda_zip_file_string = testInvoke( 574 | () => 575 | createFunction({ 576 | Code: { 577 | ZipFile: __dirname + '/functions/nodejs-env.zip' 578 | }, 579 | Handler: 'index.env', 580 | Runtime: 'nodejs', 581 | Environment: { 582 | Variables: { 583 | HELLO: 'world' 584 | } 585 | } 586 | }), 587 | async fn => { 588 | const env = await fn(); 589 | assert.equal(env.HELLO, 'world'); 590 | // Assert that the `TASK_ROOT` dir includes the "zeit-fun-" prefix 591 | assert(/^zeit-fun-/.test(basename(env.LAMBDA_TASK_ROOT))); 592 | } 593 | ); 594 | 595 | // `pkg` compilation support 596 | export const test_pkg_support = async () => { 597 | const root = require.resolve('pkg').replace(/\/node_modules(.*)$/, ''); 598 | const pkg = join(root, 'node_modules/.bin/pkg'); 599 | await execa(pkg, ['-t', 'node8', 'test/pkg-invoke.js'], { 600 | cwd: root 601 | }); 602 | const output = await execa.stdout(join(root, 'pkg-invoke'), { 603 | cwd: __dirname, 604 | stdio: ['ignore', 'pipe', 'inherit'] 605 | }); 606 | assert.equal(JSON.parse(output).hello, 'world'); 607 | }; 608 | --------------------------------------------------------------------------------