diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +node_modules diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..9df2bd34 --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +base +examples +nodejs +nodejs4.3 +python2.7 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b534b3b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2016 Michael Hart + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..7143ad6e --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +docker-lambda +------------- + +A sandboxed local environment that replicates the live [AWS Lambda](https://aws.amazon.com/lambda/) +environment almost identically – including installed software and libraries, +file structure and permissions, environment variables, context objects and +behaviors – even the user and running process are the same. + +You can use it for testing your functions in the same strict Lambda environment, +knowing that they'll exhibit the same behavior when deployed live. You can +also use it to compile native dependencies knowing that you're linking to the +same library versions that exist on AWS Lambda and then deploy using +the [AWS CLI](https://aws.amazon.com/cli/). + +This project consists of a set of Docker images for each of the supported Lambda runtimes +(Node.js 0.10 and 4.3, Python 2.7\* and Java 8\*) – as well as build +images that include packages like gcc-c++, git, zip and the aws-cli for +compiling and deploying. + +There's also an npm module to make it convenient to invoke from Node.js + +\* NB: Python 2.7 and Java 8 test runners are not yet complete, but both +languages are installed in the images so can be manually tested + +Prerequisites +------------- + +You'll need [Docker](https://www.docker.com) installed + +Example +------- + +You can perform actions with the current directory using the `-v` arg with +`docker run` – logging goes to stderr and the callback result goes to stdout: + +```console +# Test an index.handler function from the current directory on Node.js v4.3 +docker run -v "$PWD":/var/task lambci/lambda + +# If using a function other than index.handler, with a custom event +docker run -v "$PWD":/var/task lambci/lambda index.myHandler '{"some": "event"}' + +# Use the original Node.js v0.10 runtime +docker run -v "$PWD":/var/task lambci/lambda:nodejs + +# To compile native deps in node_modules (runs `npm rebuild`) +docker run -v "$PWD":/var/task lambci/lambda:build + +# Run custom commands on the build container +docker run lambci/lambda:build java -version + +# To run an interactive session on the build container +docker run -it lambci/lambda:build bash +``` + +Using the Node.js module (`npm install docker-lambda`) – for example in tests: + +```js +var dockerLambda = require('docker-lambda') + +// Spawns synchronously, uses current dir – will throw if it fails +var lambdaCallbackResult = dockerLambda({event: {some: 'event'}}) + +// Manually specify directory and custom args +lambdaCallbackResult = dockerLambda({taskDir: __dirname, dockerArgs: ['-m', '1.5G']}) +``` + +Create your own Docker image for finer control: + +```dockerfile +FROM lambci/lambda:build + +ENV AWS_DEFAULT_REGION us-east-1 + +ADD . . + +RUN npm install + +CMD cat .lambdaignore | xargs zip -9qyr lambda.zip . -x && \ + aws lambda update-function-code --function-name mylambda --zip-file fileb://lambda.zip + +# docker build -t mylambda . +# docker run -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY mylambda +``` + + +Questions +--------- + +* *When should I use this?* + + When you want fast local reproducibility. When you don't want to spin up an + Amazon Linux EC2 instance (indeed, network aside, this is closer to the real + Lambda environment because there are a number of different files, permissions + and libraries on a default Amazon Linux instance). When you don't want to + invoke a live Lambda just to test your Lambda package – you can do it locally + from your dev machine or run tests on your CI system (assuming it has Docker + support!) + + +* *Wut, how?* + + By tarring the full filesystem in Lambda, uploading that to S3, and then + piping into Docker to create a new image from scratch – then creating + mock modules that will be required/included in place of the actual native + modules that communicate with the real Lambda coordinating services. Only the + native modules are mocked out – the actual parent JS/PY runner files are left + alone, so their behaviors don't need to be replicated (like the + overriding of `console.log`, and custom defined properties like + `callbackWaitsForEmptyEventLoop`) + +* *What's missing from the images?* + + Hard to tell – anything that's not readable – so at least `/root/*` – + but probably a little more than that – hopefully nothing important, after all, + it's not readable by Lambda, so how could it be! + +* *Is it really necessary to replicate exactly to this degree?* + + Not for many scenarios – some compiled Linux binaries work out of the box + and a CentOS Docker image can compile some binaries that work on Lambda too, + for example – but for testing it's great to be able to reliably verify + permissions issues, library linking issues, etc. + +* *What's this got to do with LambCI?* + + Technically nothing – it's just been incredibly useful during the building + and testing of LambCI. + +Documentation +------------ + +TODO + +lambci/lambda + - uses ENTRYPOINT, override with `--entrypoint` +lambci/lambda:build + - uses CMD + + 'AWS_LAMBDA_FUNCTION_NAME', + 'AWS_LAMBDA_FUNCTION_VERSION', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', + 'AWS_LAMBDA_FUNCTION_TIMEOUT', + 'AWS_LAMBDA_FUNCTION_HANDLER', + 'AWS_LAMBDA_EVENT_BODY', + + 'AWS_REGION', + 'AWS_DEFAULT_REGION', + 'AWS_ACCOUNT_ID', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_SESSION_TOKEN', + diff --git a/base/Dockerfile b/base/Dockerfile new file mode 100644 index 00000000..32c968dc --- /dev/null +++ b/base/Dockerfile @@ -0,0 +1,17 @@ +FROM lambci/lambda-base + +ENV PATH=/usr/local/lib64/node-v4.3.x/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + LD_LIBRARY_PATH=/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib + +WORKDIR /var/task + +ADD yum.conf /etc/yum.conf + +# A couple of packages are either missing critical-ish files, or didn't make it into the tar +# Reinstalling filesystem might not succeed fully, but continue anyway +RUN yum reinstall -y filesystem; \ + yum reinstall -y shadow-utils && \ + yum install -y aws-cli zip git vim docker gcc-c++ clang openssl-devel cmake autoconf automake libtool && \ + rm -rf /var/cache/yum /var/lib/rpm/__db.* && \ + > /var/log/yum.log + diff --git a/base/create-base.sh b/base/create-base.sh new file mode 100755 index 00000000..0633bb60 --- /dev/null +++ b/base/create-base.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +IMAGE_NAME=lambci/lambda-base + +curl http://lambci.s3.amazonaws.com/fs/nodejs4.3.tgz | gzip -d | docker import - $IMAGE_NAME + +curl http://lambci.s3.amazonaws.com/fs/nodejs.tgz -o ../nodejs/run/nodejs.tgz +cp ../nodejs/run/nodejs.tgz ../nodejs/build/ + +curl http://lambci.s3.amazonaws.com/fs/python2.7.tgz -o ../python2.7/run/python2.7.tgz +cp ../python2.7/run/python2.7.tgz ../python2.7/build/ + +echo "Sandbox user is: $(docker run $IMAGE_NAME stat -c '%U' /tmp)" + diff --git a/base/create-build.sh b/base/create-build.sh new file mode 100755 index 00000000..27f0265a --- /dev/null +++ b/base/create-build.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +IMAGE_NAME=lambci/lambda-base:build + +docker build $BUILD_ARG -t ${IMAGE_NAME} . + diff --git a/base/dump-nodejs.js b/base/dump-nodejs.js new file mode 100644 index 00000000..6a94ea33 --- /dev/null +++ b/base/dump-nodejs.js @@ -0,0 +1,64 @@ +var fs = require('fs') +var spawn = require('child_process').spawn +var AWS = require('aws-sdk') +var s3 = new AWS.S3() + +exports.handler = function(event, context) { + var filename = 'nodejs.tgz' + var cmd = 'tar -cvpzf /tmp/' + filename + ' --numeric-owner --ignore-failed-read /var/runtime' + + var child = spawn('sh', ['-c', event.cmd || cmd]) + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', console.log.bind(console)) + child.stderr.on('data', console.error.bind(console)) + child.on('error', context.done.bind(context)) + + child.on('close', function() { + if (event.cmd) return context.done() + + console.log('Zipping done! Uploading...') + + s3.upload({ + Bucket: 'lambci', + Key: 'fs/' + filename, + Body: fs.createReadStream('/tmp/' + filename), + ACL: 'public-read', + }, function(err, data) { + if (err) return context.done(err) + + console.log('Uploading done!') + + console.log(process.execPath) + console.log(process.execArgv) + console.log(process.argv) + console.log(process.cwd()) + console.log(__filename) + console.log(process.env) + + context.done(null, data) + }) + }) +} + +// /usr/bin/node +// [ '--max-old-space-size=1229', '--max-new-space-size=153', '--max-executable-size=153' ] +// [ 'node', '/var/runtime/node_modules/.bin/awslambda' ] +// /var/task +// /var/task/index.js +// { +// PATH: '/usr/local/bin:/usr/bin/:/bin', +// LAMBDA_TASK_ROOT: '/var/task', +// LAMBDA_RUNTIME_DIR: '/var/runtime', +// AWS_REGION: 'us-east-1', +// AWS_DEFAULT_REGION: 'us-east-1', +// AWS_LAMBDA_LOG_GROUP_NAME: '/aws/lambda/dump-nodejs', +// AWS_LAMBDA_LOG_STREAM_NAME: '2016/05/18/[$LATEST]85da517...0ec8b49e', +// AWS_LAMBDA_FUNCTION_NAME: 'dump-nodejs', AWS_LAMBDA_FUNCTION_MEMORY_SIZE: '1536', +// AWS_LAMBDA_FUNCTION_VERSION: '$LATEST', +// LD_LIBRARY_PATH: '/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib', +// NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules', +// AWS_ACCESS_KEY_ID: 'ASIA...C37A', +// AWS_SECRET_ACCESS_KEY: 'JZvD...BDZ4L', +// AWS_SESSION_TOKEN: 'FQoDYXdzEMb//////////...0oog7bzuQU=' +// } diff --git a/base/dump-nodejs43.js b/base/dump-nodejs43.js new file mode 100644 index 00000000..09a8bb26 --- /dev/null +++ b/base/dump-nodejs43.js @@ -0,0 +1,67 @@ +var fs = require('fs') +var spawn = require('child_process').spawn +var AWS = require('aws-sdk') +var s3 = new AWS.S3() + +exports.handler = function(event, context, cb) { + var filename = 'nodejs4.3.tgz' + var cmd = 'tar -cvpzf /tmp/' + filename + ' -C / ' + + '--exclude=/proc --exclude=/sys --exclude=/tmp/* --exclude=/var/task/* ' + + '--numeric-owner --ignore-failed-read /' + + var child = spawn('sh', ['-c', event.cmd || cmd]) + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', console.log.bind(console)) + child.stderr.on('data', console.error.bind(console)) + child.on('error', cb) + + child.on('close', function() { + if (event.cmd) return cb() + + console.log('Zipping done! Uploading...') + + s3.upload({ + Bucket: 'lambci', + Key: 'fs/' + filename, + Body: fs.createReadStream('/tmp/' + filename), + ACL: 'public-read', + }, function(err, data) { + if (err) return cb(err) + + console.log('Uploading done!') + + console.log(process.execPath) + console.log(process.execArgv) + console.log(process.argv) + console.log(process.cwd()) + console.log(__filename) + console.log(process.env) + + cb(null, data) + }) + }) +} + +// /usr/local/lib64/node-v4.3.x/bin/node +// [ '--max-old-space-size=1229', '--max-semi-space-size=76', '--max-executable-size=153' ] +// [ '/usr/local/lib64/node-v4.3.x/bin/node', '/var/runtime/node_modules/awslambda/index.js' ] +// /var/task +// /var/task/index.js +// { +// PATH: '/usr/local/lib64/node-v4.3.x/bin:/usr/local/bin:/usr/bin/:/bin', +// LD_LIBRARY_PATH: '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib', +// NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules', +// LAMBDA_TASK_ROOT: '/var/task', +// LAMBDA_RUNTIME_DIR: '/var/runtime', +// AWS_REGION: 'us-east-1', +// AWS_DEFAULT_REGION: 'us-east-1', +// AWS_LAMBDA_LOG_GROUP_NAME: '/aws/lambda/dump-nodejs43', +// AWS_LAMBDA_LOG_STREAM_NAME: '2016/05/18/[$LATEST]c079a84d433534434534ef0ddc99d00f', +// AWS_LAMBDA_FUNCTION_NAME: 'dump-nodejs43', +// AWS_LAMBDA_FUNCTION_MEMORY_SIZE: '1536', +// AWS_LAMBDA_FUNCTION_VERSION: '$LATEST', +// AWS_ACCESS_KEY_ID: 'ASIA...C37A', +// AWS_SECRET_ACCESS_KEY: 'JZvD...BDZ4L', +// AWS_SESSION_TOKEN: 'FQoDYXdzEMb//////////...0oog7bzuQU=' +// } diff --git a/base/dump-python27.py b/base/dump-python27.py new file mode 100644 index 00000000..d7193f25 --- /dev/null +++ b/base/dump-python27.py @@ -0,0 +1,62 @@ +from __future__ import print_function + +import os +import sys +import subprocess +import boto3 +from boto3.s3.transfer import S3Transfer + +client = boto3.client('s3') +transfer = S3Transfer(client) + +def lambda_handler(event, context): + if ('cmd' in event): + return subprocess.call(['sh', '-c', event['cmd']]) + + filename = 'python2.7.tgz' + cmd = 'tar -cvpzf /tmp/{} --numeric-owner --ignore-failed-read /var/runtime'.format(filename) + + subprocess.call(['sh', '-c', cmd]) + + print('Zipping done! Uploading...') + + data = transfer.upload_file('/tmp/' + filename, 'lambci', 'fs/' + filename, + extra_args={'ACL': 'public-read'}) + + print('Uploading done!') + + print(sys.executable) + print(sys.argv) + print(os.getcwd()) + print(__file__) + print(os.environ) + + return data + +# /usr/bin/python2.7 +# ['/var/runtime/awslambda/bootstrap.py'] +# /var/task +# /var/task/lambda_function.py +# { +# 'PATH': '/usr/local/bin:/usr/bin/:/bin', +# 'LD_LIBRARY_PATH': '/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib', +# 'PYTHONPATH': '/var/runtime', +# 'AWS_REGION': 'us-east-1', +# 'AWS_DEFAULT_REGION': 'us-east-1', +# 'AWS_ACCESS_KEY_ID': 'ASIA...C37A', +# 'AWS_SECRET_ACCESS_KEY': 'JZvD...BDZ4L', +# 'AWS_SESSION_TOKEN': 'FQoDYXdzEMb//////////...0oog7bzuQU=', +# 'AWS_SECURITY_TOKEN': 'FQoDYXdzEMb//////////...0oog7bzuQU=', +# 'LAMBDA_CONSOLE_SOCKET': '16', +# 'LAMBDA_SHARED_MEM_FD': '11', +# 'LAMBDA_LOG_FD': '9', +# 'LAMBDA_CONTROL_SOCKET': '14', +# 'LAMBDA_RUNTIME_DIR': '/var/runtime', +# 'LAMBDA_RUNTIME_LOAD_TIME': '1530232235231', +# 'LAMBDA_TASK_ROOT': '/var/task', +# 'AWS_LAMBDA_LOG_GROUP_NAME': '/aws/lambda/dump-python27', +# 'AWS_LAMBDA_LOG_STREAM_NAME': '2016/05/18/[$LATEST]27e5a905...392c2c0b', +# 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE': '1536', +# 'AWS_LAMBDA_FUNCTION_VERSION': '$LATEST', +# 'AWS_LAMBDA_FUNCTION_NAME': 'dump-python27' +# } diff --git a/base/test.sh b/base/test.sh new file mode 100755 index 00000000..0d459085 --- /dev/null +++ b/base/test.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +CMD="BUILD_ONLY=true npm install --build-from-source \ + bcrypt \ + bignum \ + grpc \ + hiredis \ + ibm_db \ + iconv \ + kerberos \ + leveldown \ + murmurhash-native \ + nodegit \ + node-cmake \ + realm \ + serialport \ + snappy \ + sqlite3 \ + unix-dgram \ + v8-profiler \ + websocket \ + webworker-threads \ + x509 \ + node-sass && \ + cd node_modules/node-sass && npm install && node scripts/build -f +" + +docker run -e npm_config_unsafe-perm=true lambci/lambda-base:build sh -c "$CMD" && echo "Success!" diff --git a/base/yum.conf b/base/yum.conf new file mode 100644 index 00000000..fb82db07 --- /dev/null +++ b/base/yum.conf @@ -0,0 +1,12 @@ +[main] +keepcache=0 + +releasever=2015.09 + +[epel] +name=Extra Packages for Enterprise Linux 6 - $basearch +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=$basearch +keepcache=0 +enabled=1 +gpgcheck=0 + diff --git a/examples/docker-build/Dockerfile b/examples/docker-build/Dockerfile new file mode 100644 index 00000000..2e69a011 --- /dev/null +++ b/examples/docker-build/Dockerfile @@ -0,0 +1,15 @@ +FROM lambci/lambda + +ENV AWS_LAMBDA_FUNCTION_NAME=docker-build \ + AWS_LAMBDA_FUNCTION_VERSION=2 \ + AWS_LAMBDA_FUNCTION_MEMORY_SIZE=384 \ + AWS_LAMBDA_FUNCTION_TIMEOUT=60 \ + AWS_REGION=us-east-1 + +ADD . . + +# If we want to match permissions in /var/task exactly... +USER root +RUN chown -R slicer:497 . +USER sbx_user1051 + diff --git a/examples/docker-build/index.js b/examples/docker-build/index.js new file mode 100644 index 00000000..1e1a9059 --- /dev/null +++ b/examples/docker-build/index.js @@ -0,0 +1,35 @@ +var execSync = require('child_process').execSync + +// Intended to show that a built image will have the correct permissions in /var/task +// docker build -t build-test . && docker run build-test + +exports.handler = function(event, context, cb) { + + console.log(process.execPath) + console.log(process.execArgv) + console.log(process.argv) + console.log(process.cwd()) + console.log(process.mainModule.filename) + console.log(__filename) + console.log(process.env) + console.log(process.getuid()) + console.log(process.getgid()) + console.log(process.geteuid()) + console.log(process.getegid()) + console.log(process.getgroups()) + console.log(process.umask()) + + console.log(event) + + console.log(context) + + context.callbackWaitsForEmptyEventLoop = false + + console.log(context.getRemainingTimeInMillis()) + + console.log(execSync('ls -l /var/task', {encoding: 'utf8'})) + + cb() +} + + diff --git a/examples/docker-build/package.json b/examples/docker-build/package.json new file mode 100644 index 00000000..1fbea680 --- /dev/null +++ b/examples/docker-build/package.json @@ -0,0 +1,11 @@ +{ + "name": "docker-build", + "version": "1.0.0", + "main": "index.js", + "engines" : { + "node" : ">=0.11.12" + }, + "scripts": { + "test": "docker build -t build-test . && docker run build-test" + } +} diff --git a/examples/docker-run-0.10/index.js b/examples/docker-run-0.10/index.js new file mode 100644 index 00000000..255f3865 --- /dev/null +++ b/examples/docker-run-0.10/index.js @@ -0,0 +1,27 @@ +// Just a test lambda, run with: +// docker run -v "$PWD":/var/task lambci/lambda:nodejs + +exports.handler = function(event, context) { + + console.log(process.execPath) + console.log(process.execArgv) + console.log(process.argv) + console.log(process.cwd()) + console.log(process.mainModule.filename) + console.log(__filename) + console.log(process.env) + console.log(process.getuid()) + console.log(process.getgid()) + console.log(process.getgroups()) + console.log(process.umask()) + + console.log(event) + + console.log(context) + + console.log(context.getRemainingTimeInMillis()) + + context.done() +} + + diff --git a/examples/docker-run-0.10/package.json b/examples/docker-run-0.10/package.json new file mode 100644 index 00000000..24c9f9c3 --- /dev/null +++ b/examples/docker-run-0.10/package.json @@ -0,0 +1,9 @@ +{ + "name": "docker-run-0.10", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "docker run -v \"$PWD\":/var/task lambci/lambda:nodejs index.handler '{\"some\": \"event\"}'" + } +} + diff --git a/examples/docker-run/index.js b/examples/docker-run/index.js new file mode 100644 index 00000000..58655126 --- /dev/null +++ b/examples/docker-run/index.js @@ -0,0 +1,30 @@ +// Just a test lambda, run with: +// docker run -v "$PWD":/var/task lambci/lambda + +exports.handler = function(event, context, cb) { + + console.log(process.execPath) + console.log(process.execArgv) + console.log(process.argv) + console.log(process.cwd()) + console.log(process.mainModule.filename) + console.log(__filename) + console.log(process.env) + console.log(process.getuid()) + console.log(process.getgid()) + console.log(process.geteuid()) + console.log(process.getegid()) + console.log(process.getgroups()) + console.log(process.umask()) + + console.log(event) + + console.log(context) + + context.callbackWaitsForEmptyEventLoop = false + + console.log(context.getRemainingTimeInMillis()) + + cb() +} + diff --git a/examples/docker-run/package.json b/examples/docker-run/package.json new file mode 100644 index 00000000..77375882 --- /dev/null +++ b/examples/docker-run/package.json @@ -0,0 +1,8 @@ +{ + "name": "docker-run", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "docker run -v \"$PWD\":/var/task lambci/lambda index.handler '{\"some\": \"event\"}'" + } +} diff --git a/examples/module-native/index.js b/examples/module-native/index.js new file mode 100644 index 00000000..e562eae5 --- /dev/null +++ b/examples/module-native/index.js @@ -0,0 +1,17 @@ +var bcrypt = require('bcrypt') + +// Hashed password for "lamda-docker" +var HASHED_PASS = '$2a$10$w9.BRCsnWXv5f.eUGD2fieT.wfLV9.rSJFC/2bzz3sahJdCLaYs0K' + +exports.handler = function(event, context, cb) { + console.log('hello?') + bcrypt.compare(event.password, HASHED_PASS, function(err, res) { + cb(err, res ? 'Matches!' : 'NopeNopeNope') + }) +} + +// Just to test this locally: +if (require.main === module) { + exports.handler({password: 'lambda-docker'}, {}, console.log) + exports.handler({password: 'lambda-mocker'}, {}, console.log) +} diff --git a/examples/module-native/package.json b/examples/module-native/package.json new file mode 100644 index 00000000..78f05179 --- /dev/null +++ b/examples/module-native/package.json @@ -0,0 +1,12 @@ +{ + "name": "module-native", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "build": "docker run -v \"$PWD\":/var/task lambci/lambda-build", + "test": "node test.js" + }, + "dependencies": { + "bcrypt": "^0.8.6" + } +} diff --git a/examples/module-native/test.js b/examples/module-native/test.js new file mode 100644 index 00000000..fbcd0254 --- /dev/null +++ b/examples/module-native/test.js @@ -0,0 +1,10 @@ +var dockerLambda = require('..') + +var match = dockerLambda({event: {password: 'lambda-docker'}}) + +console.log(match == 'Matches!' ? 'Match Passed' : 'Match Failed: ' + match) + + +var nonMatch = dockerLambda({event: {password: 'lambda-mocker'}}) + +console.log(nonMatch == 'NopeNopeNope' ? 'Non-Match Passed' : 'Non-Match Failed: ' + nonMatch) diff --git a/index.js b/index.js new file mode 100644 index 00000000..2c4e3ef6 --- /dev/null +++ b/index.js @@ -0,0 +1,57 @@ +var spawnSync = require('child_process').spawnSync + +var ENV_VARS = [ + 'AWS_REGION', + 'AWS_DEFAULT_REGION', + 'AWS_ACCOUNT_ID', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_SESSION_TOKEN', + 'AWS_LAMBDA_FUNCTION_NAME', + 'AWS_LAMBDA_FUNCTION_VERSION', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', + 'AWS_LAMBDA_FUNCTION_TIMEOUT', + 'AWS_LAMBDA_FUNCTION_HANDLER', + 'AWS_LAMBDA_EVENT_BODY', +] +var ENV_ARGS = [].concat.apply([], ENV_VARS.map(function(x) { return ['-e', x] })) + +// Will spawn `docker run` synchronously and return stdout +module.exports = function runSync(options) { + options = options || {} + var dockerImage = options.dockerImage || 'lambci/lambda' + var handler = options.handler || 'index.handler' + var event = options.event || {} + var taskDir = options.taskDir == null ? process.cwd() : options.taskDir + var cleanUp = options.cleanUp == null ? true : options.cleanUp + var addEnvVars = options.addEnvVars || false + var dockerArgs = options.dockerArgs || [] + var spawnOptions = options.spawnOptions || {encoding: 'utf8'} + var returnSpawnResult = options.returnSpawnResult || false + + var args = ['run'] + .concat(taskDir ? ['-v', taskDir + ':/var/task'] : []) + .concat(cleanUp ? ['--rm'] : []) + .concat(addEnvVars ? ENV_ARGS : []) + .concat(dockerArgs) + .concat([dockerImage, handler, JSON.stringify(event)]) + + var spawnResult = spawnSync('docker', args, spawnOptions) + + if (returnSpawnResult) { + return spawnResult + } + + if (spawnResult.error || spawnResult.status !== 0) { + var err = spawnResult.error + if (!err) { + err = new Error(spawnResult.stdout || spawnResult.stderr) + err.code = spawnResult.status + err.stdout = spawnResult.stdout + err.stderr = spawnResult.stderr + } + throw err + } + + return JSON.parse(spawnResult.stdout) +} diff --git a/nodejs/build/Dockerfile b/nodejs/build/Dockerfile new file mode 100644 index 00000000..bba81494 --- /dev/null +++ b/nodejs/build/Dockerfile @@ -0,0 +1,12 @@ +FROM lambci/lambda-base:build + +ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + LD_LIBRARY_PATH=/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib \ + NODE_PATH=/var/runtime:/var/task:/var/runtime/node_modules \ + npm_config_unsafe-perm=true + +RUN rm -rf /var/runtime + +ADD nodejs.tgz / + +CMD ["npm", "rebuild"] diff --git a/nodejs/build/nodejs.tgz b/nodejs/build/nodejs.tgz new file mode 100644 index 00000000..4fc891fd Binary files /dev/null and b/nodejs/build/nodejs.tgz differ diff --git a/nodejs/run/Dockerfile b/nodejs/run/Dockerfile new file mode 100644 index 00000000..7b4e71db --- /dev/null +++ b/nodejs/run/Dockerfile @@ -0,0 +1,24 @@ +FROM lambci/lambda-base + +ENV PATH=/usr/local/bin:/usr/bin/:/bin \ + LD_LIBRARY_PATH=/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib \ + NODE_PATH=/var/runtime:/var/task:/var/runtime/node_modules \ + LAMBDA_TASK_ROOT=/var/task \ + LAMBDA_RUNTIME_DIR=/var/runtime + +RUN rm -rf /var/runtime + +ADD nodejs.tgz / + +ADD awslambda-mock.js /var/runtime/node_modules/awslambda/build/Release/awslambda.js + +# Not sure why permissions don't work just by modifying the owner +RUN rm -rf /tmp && mkdir /tmp && chown -R sbx_user1051:495 /tmp && chmod 700 /tmp + +WORKDIR /var/task + +USER sbx_user1051 + +ENTRYPOINT ["node", "--max-old-space-size=1229", "--max-new-space-size=153", "--max-executable-size=153", \ + "/var/runtime/node_modules/.bin/awslambda"] + diff --git a/nodejs/run/awslambda-mock.js b/nodejs/run/awslambda-mock.js new file mode 100644 index 00000000..15d0b915 --- /dev/null +++ b/nodejs/run/awslambda-mock.js @@ -0,0 +1,140 @@ +var crypto = require('crypto') + +var HANDLER = process.argv[2] || process.env.AWS_LAMBDA_FUNCTION_HANDLER || 'index.handler' +var EVENT_BODY = process.argv[3] || process.env.AWS_LAMBDA_EVENT_BODY || '{}' + +var FN_NAME = process.env.AWS_LAMBDA_FUNCTION_NAME || 'test' +var VERSION = process.env.AWS_LAMBDA_FUNCTION_VERSION || '$LATEST' +var MEM_SIZE = process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || 1536 +var TIMEOUT = process.env.AWS_LAMBDA_FUNCTION_TIMEOUT || 300 +var REGION = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1' +var ACCOUNT_ID = process.env.AWS_ACCOUNT_ID || randomAccountId() +var ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || 'SOME_ACCESS_KEY_ID' +var SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || 'SOME_SECRET_ACCESS_KEY' +var SESSION_TOKEN = process.env.AWS_SESSION_TOKEN || 'SOME_SESSION_TOKEN' + +function consoleLog(str) { + process.stderr.write(formatConsole(str)) +} + +function systemLog(str) { + process.stderr.write(formatSystem(str) + '\n') +} + +function systemErr(str) { + process.stderr.write(formatErr(str) + '\n') +} + +function handleResult(resultStr) { + process.stdout.write(resultStr) +} + +// Don't think this can be done in the Docker image +process.umask(2) + +process.env.AWS_LAMBDA_FUNCTION_NAME = FN_NAME +process.env.AWS_LAMBDA_FUNCTION_VERSION = VERSION +process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = MEM_SIZE +process.env.AWS_LAMBDA_LOG_GROUP_NAME = '/aws/lambda/' + FN_NAME +process.env.AWS_LAMBDA_LOG_STREAM_NAME = new Date().toISOString().slice(0, 10).replace(/-/g, '/') + + '/[' + VERSION + ']' + crypto.randomBytes(16).toString('hex') +process.env.AWS_REGION = REGION +process.env.AWS_DEFAULT_REGION = REGION + +var OPTIONS = { + invokeid: uuid(), + handler: HANDLER, + suppress_init: true, + mode: 'event', + sockfd: -1, + credentials: { + key: ACCESS_KEY_ID, + secret: SECRET_ACCESS_KEY, + session: SESSION_TOKEN, + }, + eventbody: EVENT_BODY, + contextobjects: { + clientcontext: undefined, // JSON string + cognitoidentityid: undefined, + cognitopoolid: undefined, + }, + invokedfunctionarn: arn(REGION, ACCOUNT_ID, FN_NAME), +} + +var invoked = false +var errored = false +var start = null + +module.exports = { + init_runtime: function() { return OPTIONS }, + wait_for_invoke_nb: function(fn) { + if (invoked) return + systemLog('START RequestId: ' + OPTIONS.invokeid + ' Version: ' + VERSION) + start = process.hrtime() + invoked = true + fn(OPTIONS) + }, + report_running: function(invokeId) {}, // eslint-disable-line no-unused-vars + report_done: function(invokeId, errType, resultStr) { + if (!invoked) return + var diffMs = hrTimeMs(process.hrtime(start)) + var billedMs = Math.min(100 * (Math.floor(diffMs / 100) + 1), TIMEOUT * 1000) + systemLog('END RequestId: ' + invokeId) + systemLog([ + 'REPORT RequestId: ' + invokeId, + 'Duration: ' + diffMs.toFixed(2) + ' ms', + 'Billed Duration: ' + billedMs + ' ms', + 'Memory Size: ' + MEM_SIZE + ' MB', + 'Max Memory Used: ' + Math.round(process.memoryUsage().rss / (1024 * 1024)) + ' MB', + '', + ].join('\t')) + if (typeof resultStr == 'string') { + handleResult(resultStr) + } + process.exit(errored || errType ? 1 : 0) + }, + report_fault: function(invokeId, msg, errName, errStack) { + errored = true + systemErr(msg + (errName ? ': ' + errName : '')) + if (errStack) systemErr(errStack) + }, + get_remaining_time: function() { + return (TIMEOUT * 1000) - Math.floor(hrTimeMs(process.hrtime(start))) + }, + send_console_logs: consoleLog, + max_logger_error_size: 256 * 1024, +} + +function formatConsole(str) { + return str.replace(/^[0-9TZ:\.\-]+\t[0-9a-f\-]+\t/, '\033[34m$&\u001b[0m') +} + +function formatSystem(str) { + return '\033[32m' + str + '\033[0m' +} + +function formatErr(str) { + return '\033[31m' + str + '\033[0m' +} + +function hrTimeMs(hrtime) { + return (hrtime[0] * 1e9 + hrtime[1]) / 1e6 +} + +// Approximates the look of a v1 UUID +function uuid() { + return crypto.randomBytes(4).toString('hex') + '-' + + crypto.randomBytes(2).toString('hex') + '-' + + crypto.randomBytes(2).toString('hex').replace(/^./, '1') + '-' + + crypto.randomBytes(2).toString('hex') + '-' + + crypto.randomBytes(6).toString('hex') +} + +function randomAccountId() { + return String(0x100000000 * Math.random()) +} + +function arn(region, accountId, fnName) { + return 'arn:aws:lambda:' + region + ':' + accountId.replace(/[^\d]/g, '') + ':function:' + fnName +} + diff --git a/nodejs/run/nodejs.tgz b/nodejs/run/nodejs.tgz new file mode 100644 index 00000000..4fc891fd Binary files /dev/null and b/nodejs/run/nodejs.tgz differ diff --git a/nodejs4.3/build/Dockerfile b/nodejs4.3/build/Dockerfile new file mode 100644 index 00000000..06389b6f --- /dev/null +++ b/nodejs4.3/build/Dockerfile @@ -0,0 +1,8 @@ +FROM lambci/lambda-base:build + +ENV PATH=/usr/local/lib64/node-v4.3.x/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + LD_LIBRARY_PATH=/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib \ + NODE_PATH=/var/runtime:/var/task:/var/runtime/node_modules \ + npm_config_unsafe-perm=true + +CMD ["npm", "rebuild"] diff --git a/nodejs4.3/run/Dockerfile b/nodejs4.3/run/Dockerfile new file mode 100644 index 00000000..347bdfac --- /dev/null +++ b/nodejs4.3/run/Dockerfile @@ -0,0 +1,20 @@ +FROM lambci/lambda-base + +ENV PATH=/usr/local/lib64/node-v4.3.x/bin:/usr/local/bin:/usr/bin/:/bin \ + LD_LIBRARY_PATH=/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib \ + NODE_PATH=/var/runtime:/var/task:/var/runtime/node_modules \ + LAMBDA_TASK_ROOT=/var/task \ + LAMBDA_RUNTIME_DIR=/var/runtime + +ADD awslambda-mock.js /var/runtime/node_modules/awslambda/build/Release/awslambda.js + +# Not sure why permissions don't work just by modifying the owner +RUN rm -rf /tmp && mkdir /tmp && chown -R sbx_user1051:495 /tmp && chmod 700 /tmp + +WORKDIR /var/task + +USER sbx_user1051 + +ENTRYPOINT ["/usr/local/lib64/node-v4.3.x/bin/node", "--max-old-space-size=1229", "--max-semi-space-size=76", "--max-executable-size=153", \ + "/var/runtime/node_modules/awslambda/index.js"] + diff --git a/nodejs4.3/run/awslambda-mock.js b/nodejs4.3/run/awslambda-mock.js new file mode 100644 index 00000000..697eb894 --- /dev/null +++ b/nodejs4.3/run/awslambda-mock.js @@ -0,0 +1,142 @@ +var crypto = require('crypto') + +var HANDLER = process.argv[2] || process.env.AWS_LAMBDA_FUNCTION_HANDLER || 'index.handler' +var EVENT_BODY = process.argv[3] || process.env.AWS_LAMBDA_EVENT_BODY || '{}' + +var FN_NAME = process.env.AWS_LAMBDA_FUNCTION_NAME || 'test' +var VERSION = process.env.AWS_LAMBDA_FUNCTION_VERSION || '$LATEST' +var MEM_SIZE = process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || 1536 +var TIMEOUT = process.env.AWS_LAMBDA_FUNCTION_TIMEOUT || 300 +var REGION = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1' +var ACCOUNT_ID = process.env.AWS_ACCOUNT_ID || randomAccountId() +var ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || 'SOME_ACCESS_KEY_ID' +var SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || 'SOME_SECRET_ACCESS_KEY' +var SESSION_TOKEN = process.env.AWS_SESSION_TOKEN || 'SOME_SESSION_TOKEN' + +function consoleLog(str) { + process.stderr.write(formatConsole(str)) +} + +function systemLog(str) { + process.stderr.write(formatSystem(str) + '\n') +} + +function systemErr(str) { + process.stderr.write(formatErr(str) + '\n') +} + +function handleResult(resultStr) { + process.stdout.write(resultStr) +} + +// Don't think this can be done in the Docker image +process.umask(2) + +process.env.AWS_LAMBDA_FUNCTION_NAME = FN_NAME +process.env.AWS_LAMBDA_FUNCTION_VERSION = VERSION +process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = MEM_SIZE +process.env.AWS_LAMBDA_LOG_GROUP_NAME = '/aws/lambda/' + FN_NAME +process.env.AWS_LAMBDA_LOG_STREAM_NAME = new Date().toISOString().slice(0, 10).replace(/-/g, '/') + + '/[' + VERSION + ']' + crypto.randomBytes(16).toString('hex') +process.env.AWS_REGION = REGION +process.env.AWS_DEFAULT_REGION = REGION + +var OPTIONS = { + initInvokeId: uuid(), + invokeId: uuid(), + handler: HANDLER, + suppressInit: true, + credentials: { + key: ACCESS_KEY_ID, + secret: SECRET_ACCESS_KEY, + session: SESSION_TOKEN, + }, + eventBody: EVENT_BODY, + contextObjects: { + // clientContext: '{}', + // cognitoIdentityId: undefined, + // cognitoPoolId: undefined, + }, + invokedFunctionArn: arn(REGION, ACCOUNT_ID, FN_NAME), +} + +// Some weird spelling error in the source? +OPTIONS.invokeid = OPTIONS.invokeId + +var invoked = false +var errored = false +var start = null + +module.exports = { + initRuntime: function() { return OPTIONS }, + waitForInvoke: function(fn) { + if (invoked) return + systemLog('START RequestId: ' + OPTIONS.invokeId + ' Version: ' + VERSION) + start = process.hrtime() + invoked = true + fn(OPTIONS) + }, + reportRunning: function(invokeId) {}, // eslint-disable-line no-unused-vars + reportDone: function(invokeId, errType, resultStr) { + if (!invoked) return + var diffMs = hrTimeMs(process.hrtime(start)) + var billedMs = Math.min(100 * (Math.floor(diffMs / 100) + 1), TIMEOUT * 1000) + systemLog('END RequestId: ' + invokeId) + systemLog([ + 'REPORT RequestId: ' + invokeId, + 'Duration: ' + diffMs.toFixed(2) + ' ms', + 'Billed Duration: ' + billedMs + ' ms', + 'Memory Size: ' + MEM_SIZE + ' MB', + 'Max Memory Used: ' + Math.round(process.memoryUsage().rss / (1024 * 1024)) + ' MB', + '', + ].join('\t')) + if (typeof resultStr == 'string') { + handleResult(resultStr) + } + process.exit(errored || errType ? 1 : 0) + }, + reportFault: function(invokeId, msg, errName, errStack) { + errored = true + systemErr(msg + (errName ? ': ' + errName : '')) + if (errStack) systemErr(errStack) + }, + getRemainingTime: function() { + return (TIMEOUT * 1000) - Math.floor(hrTimeMs(process.hrtime(start))) + }, + sendConsoleLogs: consoleLog, + maxLoggerErrorSize: 256 * 1024, +} + +function formatConsole(str) { + return str.replace(/^[0-9TZ:\.\-]+\t[0-9a-f\-]+\t/, '\033[34m$&\u001b[0m') +} + +function formatSystem(str) { + return '\033[32m' + str + '\033[0m' +} + +function formatErr(str) { + return '\033[31m' + str + '\033[0m' +} + +function hrTimeMs(hrtime) { + return (hrtime[0] * 1e9 + hrtime[1]) / 1e6 +} + +// Approximates the look of a v1 UUID +function uuid() { + return crypto.randomBytes(4).toString('hex') + '-' + + crypto.randomBytes(2).toString('hex') + '-' + + crypto.randomBytes(2).toString('hex').replace(/^./, '1') + '-' + + crypto.randomBytes(2).toString('hex') + '-' + + crypto.randomBytes(6).toString('hex') +} + +function randomAccountId() { + return String(0x100000000 * Math.random()) +} + +function arn(region, accountId, fnName) { + return 'arn:aws:lambda:' + region + ':' + accountId.replace(/[^\d]/g, '') + ':function:' + fnName +} + diff --git a/nodejs4.3/run/index.js b/nodejs4.3/run/index.js new file mode 100644 index 00000000..5d54c152 --- /dev/null +++ b/nodejs4.3/run/index.js @@ -0,0 +1,19 @@ +// Just a test lambda, run with: +// docker run -v "$PWD":/var/task lambci/lambda + +exports.handler = function(event, context, cb) { + + console.log(process.execPath) + console.log(process.execArgv) + console.log(process.argv) + console.log(process.cwd()) + console.log(__filename) + console.log(process.env) + + console.log(context) + + console.log(context.getRemainingTimeInMillis()) + + cb() +} + diff --git a/package.json b/package.json new file mode 100644 index 00000000..638b84b6 --- /dev/null +++ b/package.json @@ -0,0 +1,118 @@ +{ + "name": "docker-lambda", + "version": "0.9.0", + "description": "A Docker image and test runner that (very closely) mimics the live AWS Lambda environment", + "main": "index.js", + "scripts": { + "test": "node test.js" + }, + "directories": { + "example": "examples" + }, + "repository": "lambci/docker-lambda", + "author": "Michael Hart ", + "license": "MIT", + "engines" : { + "node" : ">=0.11.12" + }, + "devDependencies": { + "should": "^8.4.0" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "node": true + }, + "rules": { + "no-console": 0, + "no-mixed-requires": 0, + "no-underscore-dangle": 0, + "no-shadow": 0, + "no-use-before-define": [ + 2, + "nofunc" + ], + "camelcase": [ + 2, + { + "properties": "never" + } + ], + "curly": 0, + "eqeqeq": 0, + "new-parens": 0, + "quotes": [ + 2, + "single", + "avoid-escape" + ], + "semi": [ + 2, + "never" + ], + "strict": 0, + "no-empty-character-class": 2, + "no-extra-parens": [ + 2, + "functions" + ], + "no-floating-decimal": 2, + "no-lonely-if": 2, + "no-self-compare": 2, + "no-throw-literal": 2, + "no-unused-vars": 2, + "array-bracket-spacing": [ + 2, + "never" + ], + "brace-style": [ + 2, + "1tbs", + { + "allowSingleLine": true + } + ], + "comma-dangle": [ + 2, + "always-multiline" + ], + "comma-style": [ + 2, + "last" + ], + "consistent-this": [ + 2, + "self" + ], + "object-curly-spacing": [ + 2, + "never" + ], + "operator-assignment": [ + 2, + "always" + ], + "operator-linebreak": [ + 2, + "after" + ], + "keyword-spacing": 2, + "space-before-blocks": [ + 2, + "always" + ], + "space-before-function-paren": [ + 2, + "never" + ], + "space-in-parens": [ + 2, + "never" + ], + "spaced-comment": [ + 2, + "always" + ] + } + } +} diff --git a/python2.7/build/Dockerfile b/python2.7/build/Dockerfile new file mode 100644 index 00000000..fc681c56 --- /dev/null +++ b/python2.7/build/Dockerfile @@ -0,0 +1,10 @@ +FROM lambci/lambda-base:build + +ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + LD_LIBRARY_PATH=/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib \ + PYTHONPATH=/var/runtime + +RUN rm -rf /var/runtime + +ADD python2.7.tgz / + diff --git a/python2.7/build/python2.7.tgz b/python2.7/build/python2.7.tgz new file mode 100644 index 00000000..124c03fd Binary files /dev/null and b/python2.7/build/python2.7.tgz differ diff --git a/python2.7/run/Dockerfile b/python2.7/run/Dockerfile new file mode 100644 index 00000000..40519434 --- /dev/null +++ b/python2.7/run/Dockerfile @@ -0,0 +1,29 @@ +FROM lambci/lambda-base + +ENV PATH=/usr/local/bin:/usr/bin/:/bin \ + LD_LIBRARY_PATH=/lib64:/usr/lib64:/var/runtime:/var/task:/var/task/lib \ + PYTHONPATH=/var/runtime \ + LAMBDA_TASK_ROOT=/var/task \ + LAMBDA_RUNTIME_DIR=/var/runtime \ + LAMBDA_CONSOLE_SOCKET=16 \ + LAMBDA_SHARED_MEM_FD=11 \ + LAMBDA_LOG_FD=9 \ + LAMBDA_CONTROL_SOCKET=14 \ + LAMBDA_RUNTIME_LOAD_TIME=1530232235231 + +RUN rm -rf /var/runtime + +ADD python2.7.tgz / + +RUN rm /var/runtime/awslambda/runtime.so +ADD runtime-mock.py /var/runtime/awslambda/runtime.py + +# Not sure why permissions don't work just by modifying the owner +RUN rm -rf /tmp && mkdir /tmp && chown -R sbx_user1051:495 /tmp && chmod 700 /tmp + +WORKDIR /var/task + +USER sbx_user1051 + +ENTRYPOINT ["/usr/bin/python2.7", "/var/runtime/awslambda/bootstrap.py"] + diff --git a/python2.7/run/python2.7.tgz b/python2.7/run/python2.7.tgz new file mode 100644 index 00000000..124c03fd Binary files /dev/null and b/python2.7/run/python2.7.tgz differ diff --git a/python2.7/run/runtime-mock.py b/python2.7/run/runtime-mock.py new file mode 100644 index 00000000..a2093cea --- /dev/null +++ b/python2.7/run/runtime-mock.py @@ -0,0 +1,41 @@ +from __future__ import print_function +import sys + +orig_stdout = sys.stdout +orig_stderr = sys.stderr + +# TODO: finish this + +def recv_start(ctrl_sock): + sys.stdout = orig_stdout + sys.stderr = orig_stderr + print("recv_start") + return (invokeid, mode, handler, suppress_init, credentials) + +def report_running(invokeid): + print("report_running") + return + +def receive_invoke(ctrl_sock): + print("receive_invoke") + return (invokeid, data_sock, credentials, event_body, context_objs, invoked_function_arn) + +def report_fault(invokeid, msg, except_value, trace): + print("report_fault") + return + +def report_done(invokeid, errortype, result): + print("report_done") + return + +def log_bytes(msg, fileno): + print(msg) + return + +def get_remaining_time(): + print("get_remaining_time") + return + +def send_console_message(msg): + print(msg) + return diff --git a/test.js b/test.js new file mode 100644 index 00000000..f67cc554 --- /dev/null +++ b/test.js @@ -0,0 +1,102 @@ +require('should') +require('child_process').spawnSync = mockSpawnSync + +var dockerLambda = require('.') + +var captured = {} +var mockReturn +function mockSpawnSync(cmd, args, options) { + captured.cmd = cmd + captured.args = args + captured.options = options + return mockReturn +} +function resetMock(returnVal) { + mockReturn = returnVal || {status: 0, stdout: '{}'} +} + +// Should return defaults if calling with no options +resetMock() +var result = dockerLambda() +captured.cmd.should.equal('docker') +captured.args.should.eql([ + 'run', + '-v', + __dirname + ':/var/task', + '--rm', + 'lambci/lambda', + 'index.handler', + '{}', +]) +captured.options.should.eql({encoding: 'utf8'}) +result.should.eql({}) + +// Should use env vars if asked to +resetMock() +result = dockerLambda({addEnvVars: true}) +captured.cmd.should.equal('docker') +captured.args.should.eql([ + 'run', + '-v', + __dirname + ':/var/task', + '--rm', + '-e', + 'AWS_REGION', + '-e', + 'AWS_DEFAULT_REGION', + '-e', + 'AWS_ACCOUNT_ID', + '-e', + 'AWS_ACCESS_KEY_ID', + '-e', + 'AWS_SECRET_ACCESS_KEY', + '-e', + 'AWS_SESSION_TOKEN', + '-e', + 'AWS_LAMBDA_FUNCTION_NAME', + '-e', + 'AWS_LAMBDA_FUNCTION_VERSION', + '-e', + 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', + '-e', + 'AWS_LAMBDA_FUNCTION_TIMEOUT', + '-e', + 'AWS_LAMBDA_FUNCTION_HANDLER', + '-e', + 'AWS_LAMBDA_EVENT_BODY', + 'lambci/lambda', + 'index.handler', + '{}', +]) +captured.options.should.eql({encoding: 'utf8'}) +result.should.eql({}) + +// Should return spawn result if asked to +resetMock({status: 0, stdout: 'null'}) +result = dockerLambda({returnSpawnResult: true}) +result.should.eql({status: 0, stdout: 'null'}) + +// Should throw error if spawn returns error +resetMock({error: new Error('Something went wrong')}) +var err +try { + result = dockerLambda() +} catch (e) { + err = e +} +err.should.eql(new Error('Something went wrong')) + +// Should throw error if spawn process dies +resetMock({status: 1, stdout: 'wtf', stderr: 'ftw'}) +try { + result = dockerLambda() +} catch (e) { + err = e +} +var expectedErr = new Error('wtf') +expectedErr.code = 1 +expectedErr.stdout = 'wtf' +expectedErr.stderr = 'ftw' +err.should.eql(expectedErr) + +console.log('All Passed!')