├── .babelrc ├── .eslintignore ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── docker-compose.yml ├── package.json └── src ├── abstractBaseClass.js ├── createLambdaContext.js ├── index.js ├── kinesisConsumer.js └── localstack.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["transform-runtime"] 4 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: eslint:recommended 2 | 3 | env: 4 | browser: false # browser global variables. 5 | node: true # Node.js global variables and Node.js-specific rules. 6 | amd: false # defines require() and define() as global variables as per the amd spec. 7 | mocha: true # adds all of the Mocha testing global variables. 8 | jasmine: false # adds all of the Jasmine testing global variables for version 1.3 and 2.0. 9 | phantomjs: false # phantomjs global variables. 10 | jquery: false # jquery global variables. 11 | prototypejs: false # prototypejs global variables. 12 | shelljs: false # shelljs global variables. 13 | es6: true 14 | 15 | parserOptions: 16 | sourceType: module 17 | ecmaVersion: 8 18 | ecmaFeatures: 19 | binaryLiterals: false # enable binary literals 20 | blockBindings: false # enable let and const (aka block bindings) 21 | defaultParams: false # enable default function parameters 22 | forOf: false # enable for-of loops 23 | generators: false # enable generators 24 | objectLiteralComputedProperties: false # enable computed object literal property names 25 | objectLiteralDuplicateProperties: false # enable duplicate object literal properties in strict mode 26 | objectLiteralShorthandMethods: false # enable object literal shorthand methods 27 | objectLiteralShorthandProperties: false # enable object literal shorthand properties 28 | octalLiterals: false # enable octal literals 29 | regexUFlag: false # enable the regular expression u flag 30 | regexYFlag: false # enable the regular expression y flag 31 | templateStrings: false # enable template strings 32 | unicodeCodePointEscapes: false # enable code point escapes 33 | jsx: false # enable JSX 34 | 35 | rules: 36 | no-unused-vars: 0 37 | no-undef: 0 38 | no-console: 0 39 | quotes: [1, "single", "avoid-escape"] 40 | semi: 2 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | node_modules/ 3 | coverage/ 4 | lib/ 5 | spec/ 6 | serverlessOfflineLocalstack.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | spec/ 3 | src/ 4 | serverlessOfflineLocalstack.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | 5 | before_deploy: 6 | - npm run build 7 | 8 | deploy: 9 | provider: npm 10 | email: travis@travisci.org 11 | skip_cleanup: true 12 | api_key: 13 | secure: iqaBYjHGtv8M7VpD4mY+ajO825d0Cop5qMM4SdSHBoKHHKiW9ZikW+5zDb26/7xVqScvqDWuTZnXF2UyuBt78v5dXj/kznn+pa+KnR+hBQ5eXKa/k3M1ProaKVk3TdOsw64o8FhPx/GZ0KaIya7whEEEG0F7Wdbh2346OIFBAy3sNI54ozTce6peLpttfQjuMqMhfh8fchvTKw2WkqweTINhSVCL1WC6labQ+QDIZ+O/UwLUg9f8yISyxvxT7WbPSFbS7u16vxThHwR8xzMpX8GhVDH7Vr2pQqQqQiwvkA2WJxDvRh/KOVeUa2moK4OutS6uLL6ubpZ4Yh+aIHOG8YeEB9/k3hKJYjLfi9/eUQZ5bI8c8xZjlW48KfIPYy4InTkLkdfPitGOOK/yfl9v8DmUcGqAbziyWoJagbxt5dHahp1ldXWIL9Cmesb80+NWY765g6TTRchutOZnVzvoeG6DJ18g7JO8PqJFMWwtIw7AVQpx/n0Jf1OIa2zZ8bLU3/V573dI0el49nHcrEODJFOBpdnQoNGIitRRRtqIGQB9L9O8o8azpCBA/0PM1BMpR/V32lg91ta1IWDv+zT6eAkDRqSYie7uKojp26c7EJV96RN6FOw9FYUh1JOL2Ir1VxL3gr5eFm5zqTqaiJfJfc3zqNgTLA+p+9TWvI4Y/D0= 14 | on: 15 | branches: 16 | only: 17 | - master -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-offline-localstack 2 | 3 | [Serverless](https://serverless.com/) Plugin to support running against [Localstack](https://github.com/localstack/localstack). 4 | 5 | This plugin allows any requests to AWS to be redirected to a running Localstack instance. It also supports a poller to consume messages 6 | off of a Kinesis stream. 7 | 8 | WARNING: This plugin is very much a WIP 9 | 10 | Pre-requisites: 11 | * [Localstack](https://github.com/localstack/localstack) 12 | * [Serverless Offline > 3.0.0](https://github.com/dherault/serverless-offline) 13 | * [Serverless Webpack > 3.0.0](https://github.com/serverless-heaven/serverless-webpack) 14 | 15 | ## Installation 16 | 17 | The easiest way to get started is to install via npm. 18 | 19 | npm install --save-dev serverless-offline-localstack 20 | 21 | ## Configuring Serverless 22 | 23 | There are two ways to configure the plugin, via a JSON file or via serverless.yml. There are two supported methods for 24 | configuring the endpoints, globally via the "host" property, or individually. These properties may be mixed, allowing for 25 | global override support while also override specific endpoints. 26 | 27 | A "host" or individual endpoints must be configured or this plugin will be deactivated. 28 | 29 | #### Configuring endpoints via serverless.yml 30 | 31 | ``` 32 | service: myService 33 | 34 | plugins: 35 | - serverless-offline-localstack 36 | 37 | custom: 38 | serverlessOfflineLocalstack: 39 | host: http://localhost 40 | kinesis: 41 | enabled: true 42 | intervalMillis: 5000 43 | endpoints: 44 | S3: http://localhost:4572 45 | DynamoDB: http://localhost:4570 46 | CloudFormation: http://localhost:4581 47 | Elasticsearch: http://localhost:4571 48 | ES: http://localhost:4578 49 | SNS: http://localhost:4575 50 | SQS: http://localhost:4576 51 | Lambda: http://localhost:4574 52 | Kinesis: http://localhost:4568 53 | ``` 54 | 55 | #### Configuring endpoints via JSON 56 | 57 | ``` 58 | service: myService 59 | 60 | plugins: 61 | - serverless-offline-localstack 62 | 63 | custom: 64 | serverlessOfflineLocalstack: 65 | kinesis: 66 | enabled: true 67 | intervalMillis: 5000 68 | endpointFile: path/to/file.json 69 | ``` 70 | 71 | ## Configuring the AWS SDK 72 | 73 | To configure the AWS SDK with these endpoints, use the following as soon as possible in your entry point 74 | 75 | ``` 76 | const AWS = require('aws-sdk'); 77 | const ServerlessOfflineLocalstack = require('serverless-offline-localstack'); 78 | ServerlessOfflineLocalstack.configureAWS(AWS); 79 | ``` 80 | 81 | ## Localstack 82 | 83 | For full documentation, see https://github.com/localstack/localstack 84 | 85 | #### Installing via PIP 86 | 87 | The easiest way to get started with Localstack is to install it via Python's pip. 88 | 89 | ``` 90 | pip install localstack 91 | ``` 92 | 93 | ### Running Localstack 94 | 95 | There are multiple ways to run Localstack. 96 | 97 | #### Starting Localstack via Docker 98 | 99 | If Localstack is installed via pip 100 | 101 | ``` 102 | localstack start --docker 103 | ``` 104 | 105 | #### Starting Localstack without Docker 106 | 107 | If Localstack is installed via pip 108 | 109 | ``` 110 | localstack start 111 | ``` 112 | 113 | ### Optional Debug Flag 114 | 115 | An optional debug flag is supported via serverless.yml that will enable additional debug logs. 116 | 117 | ``` 118 | custom: 119 | serverlessOfflineLocalstack: 120 | debug: true 121 | ``` 122 | 123 | ### The following projects were used as a guideline and their license should be followed as well: 124 | 125 | - https://github.com/temyers/serverless-localstack 126 | - https://github.com/DopplerLabs/serverless-plugin-offline-kinesis-events -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | serverless-node: 4 | image: node:latest 5 | working_dir: /app 6 | volumes: 7 | - .:/app 8 | - ~/.aws/:/root/.aws 9 | environment: 10 | - AWS_ACCESS_KEY_ID 11 | - AWS_SECRET_ACCESS_KEY 12 | - AWS_PROFILE 13 | - AWS_SESSION_TOKEN 14 | - AWS_SECURITY_TOKEN 15 | localstack: 16 | image: atlassianlabs/localstack 17 | ports: 18 | - "4567-4582:4567-4582" 19 | - "8080:8080" 20 | environment: 21 | # Only start a subset of services required for testing. 22 | - SERVICES=s3,sns,sqs,apigateway,lambda,dynamodb,dynamodbstreams,cloudformation 23 | # - DEBUG=${DEBUG- } 24 | # - DATA_DIR=${DATA_DIR- } 25 | # - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- } 26 | # - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- } 27 | # - DOCKER_HOST=unix:///var/run/docker.sock 28 | # volumes: 29 | # - "${TMP_DIR:-/tmp/localstack}:${TMP_DIR:-/tmp/localstack}" 30 | # - "/var/run/docker.sock:/var/run/docker.sock" 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-offline-localstack", 3 | "version": "0.0.3", 4 | "main": "./lib/index.js", 5 | "keywords": [ 6 | "hooks", 7 | "plugin", 8 | "serverless", 9 | "serverless-offline", 10 | "localstack" 11 | ], 12 | "author": "Adrian Jagnanan", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+ssh://git@github.com/lookinglass-in/serverless-offline-localstack" 16 | }, 17 | "scripts": { 18 | "lint": "eslint .", 19 | "lintFix": "eslint . --fix", 20 | "test": "npm -v", 21 | "test1": "mocha --compilers js:babel-core/register --colors -w ./test/*.spec.js", 22 | "build": "npm run lint && npm run build:node", 23 | "build:node": "cross-env BABEL_ENV=production babel src --out-dir lib", 24 | "dev": "cross-env BABEL_ENV=production babel src --out-dir lib -w" 25 | }, 26 | "dependencies": { 27 | "babel-runtime": "6.26.0", 28 | "aws-sdk": "2.128.0", 29 | "bluebird": "3.5.1" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "6.26.0", 33 | "babel-core": "6.26.0", 34 | "babel-loader": "7.1.2", 35 | "babel-plugin-transform-runtime": "6.23.0", 36 | "babel-preset-env": "1.6.0", 37 | "chai": "4.1.2", 38 | "cross-env": "5.0.5", 39 | "eslint": "4.8.0", 40 | "eslint-loader": "1.9.0", 41 | "fs-extra": "4.0.2", 42 | "jasmine": "2.8.0", 43 | "mocha": "4.0.1", 44 | "nodemon": "1.12.1", 45 | "serverless": "1.23.0", 46 | "sinon": "4.0.1", 47 | "source-map-support": "0.5.0", 48 | "webpack": "3.6.0", 49 | "webpack-node-externals": "1.6.0" 50 | }, 51 | "license": "ISC", 52 | "description": "A serverless plugin to redirected AWS requests to a running Localstack instance." 53 | } 54 | -------------------------------------------------------------------------------- /src/abstractBaseClass.js: -------------------------------------------------------------------------------- 1 | export default class AbstractBaseClass { 2 | 3 | constructor(serverless, options) { 4 | this.serverless = serverless; 5 | this.options = options; 6 | this.isDebug = false; 7 | } 8 | 9 | setDebug(debug) { 10 | this.isDebug = debug; 11 | } 12 | 13 | log(msg) { 14 | this.serverless.cli.log.call(this.serverless.cli, msg); 15 | } 16 | 17 | debug(msg) { 18 | if (this.isDebug) { 19 | this.log(msg); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/createLambdaContext.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | Mimicks the lambda context object 5 | http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html 6 | */ 7 | export default function createLambdaContext(fun, cb) { 8 | 9 | const functionName = fun.name; 10 | const endTime = new Date().getTime() + (fun.timeout ? fun.timeout * 1000 : 6000); 11 | const done = typeof cb === 'function' ? cb : ((x, y) => x || y); // eslint-disable-line no-extra-parens 12 | 13 | return { 14 | /* Methods */ 15 | done, 16 | succeed: res => done(null, res), 17 | fail: err => done(err, null), 18 | getRemainingTimeInMillis: () => endTime - new Date().getTime(), 19 | 20 | /* Properties */ 21 | functionName, 22 | memoryLimitInMB: fun.memorySize, 23 | functionVersion: `offline_functionVersion_for_${functionName}`, 24 | invokedFunctionArn: `offline_invokedFunctionArn_for_${functionName}`, 25 | awsRequestId: `offline_awsRequestId_${Math.random().toString(10).slice(2)}`, 26 | logGroupName: `offline_logGroupName_for_${functionName}`, 27 | logStreamName: `offline_logStreamName_for_${functionName}`, 28 | identity: {}, 29 | clientContext: {}, 30 | }; 31 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as Promise from 'bluebird'; 4 | 5 | import Localstack from './localstack'; 6 | import KinesisConsumer from './kinesisConsumer'; 7 | 8 | class ServerlessOfflineLocalstackPlugin { 9 | 10 | constructor(serverless, options) { 11 | this.serverless = serverless; 12 | this.options = options; 13 | 14 | this.localstack = new Localstack(serverless, options); 15 | this.kinesisConsumer = new KinesisConsumer(serverless, options); 16 | 17 | this.commands = { 18 | deploy: {} 19 | }; 20 | 21 | this.hooks = { 22 | 'before:invoke:local:invoke': () => Promise.bind(this.localstack) 23 | .then(this.localstack.reconfigureAWS), 24 | 'webpack:invoke:invoke': () => Promise.bind(this.localstack) 25 | .then(this.localstack.reconfigureAWS), 26 | 'before:offline:start:init': () => Promise.resolve( 27 | Promise.bind(this.localstack).then(this.localstack.reconfigureAWS), 28 | ).then( 29 | Promise.bind(this.kinesisConsumer).then(this.kinesisConsumer.runWatcher) 30 | ) 31 | }; 32 | } 33 | 34 | static configureAWS(AWS) { 35 | Localstack.configureAWS(AWS); 36 | } 37 | } 38 | 39 | module.exports = ServerlessOfflineLocalstackPlugin; 40 | -------------------------------------------------------------------------------- /src/kinesisConsumer.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Promise = require('bluebird'); 3 | 4 | import AbstractBaseClass from './abstractBaseClass'; 5 | import createLambdaContext from './createLambdaContext'; 6 | 7 | /** 8 | * Based on ServerlessWebpack.run 9 | * @param serverless 10 | * @param slsWebpack 11 | * @param stats 12 | * @param functionName 13 | */ 14 | function getRunnableLambda(serverless, slsWebpack, stats, functionName) { 15 | return (event) => { 16 | // need to setup env vars first 17 | const originalEnvironment = _.extend({}, process.env); 18 | process.env = _.extend({}, serverless.service.provider.environment, serverless.service.functions[functionName].environment, originalEnvironment); 19 | 20 | const compileOutputPaths = slsWebpack.compileOutputPaths; // returns an array, but it should only be 1 21 | const handler = require(compileOutputPaths[0])[functionName]; 22 | const context = createLambdaContext(serverless.service.functions[functionName]); 23 | return new Promise( 24 | (resolve, reject) => handler( 25 | event, 26 | context, 27 | (err, res) => { 28 | if (err) { 29 | reject(err); 30 | } else { 31 | resolve(res); 32 | } 33 | } 34 | )); 35 | }; 36 | } 37 | 38 | const MAX_CONSECUTIVE_ERRORS = 10; 39 | 40 | export default class KinesisConsumer extends AbstractBaseClass { 41 | 42 | constructor(serverless, options) { 43 | super(serverless, options); 44 | 45 | this.log('Configuring serverless offline -> kinesis consumer'); 46 | 47 | this.awsProvider = serverless.getProvider('aws'); 48 | this.config = serverless.service.custom && serverless.service.custom.serverlessOfflineLocalstack || {}; 49 | super.setDebug(this.config.debug); 50 | } 51 | 52 | static async createRegistry(serverless) { 53 | // Get a handle on the compiled functions 54 | // TODO(msills): Do not rely on this plugin. 55 | const slsWebpack = _.find(serverless.pluginManager.plugins, p => p.constructor.name === 'ServerlessWebpack'); 56 | const compileStats = await slsWebpack.compile(); 57 | 58 | const registry = {}; 59 | for (const functionName of _.keys(serverless.service.functions)) { 60 | const func = serverless.service.functions[functionName]; 61 | // Get the list of streams for the function 62 | const streamEvents = _.filter(func.events || [], e => 'stream' in e); 63 | for (const s of streamEvents) { 64 | const streamName = s.stream.arn.split('/').slice(-1)[0]; 65 | registry[streamName] = registry[streamName] || []; 66 | registry[streamName].push(getRunnableLambda(serverless, slsWebpack, compileStats, functionName)); 67 | } 68 | } 69 | return registry; 70 | } 71 | 72 | static async _repollStreams(kinesis, streamIterators) { 73 | // this.debug(`Polling Kinesis streams: ${JSON.stringify(_.keys(streamIterators))}`); 74 | for (const name of _.keys(streamIterators)) { 75 | if (streamIterators[name] === null) { 76 | // this.log(`Iterator for stream '${name}' + is closed`); 77 | } 78 | } 79 | // Get the new values for each stream 80 | // name -> [fetch result] 81 | return Promise.props( 82 | _.mapValues( 83 | streamIterators, 84 | iter => kinesis.getRecords({ 85 | ShardIterator: iter, 86 | Limit: 100 87 | }).promise())); 88 | } 89 | 90 | static async _runLambdas(streamResults, registry) { 91 | // Wait for the functions to execute 92 | await Promise.all(_.chain(streamResults) 93 | .entries() 94 | .flatMap(([name, result]) => { 95 | // this.debug(`Stream '${name}' returned ${result.Records.length} records`); 96 | // Parse the records 97 | const records = _.map(result.Records, r => { 98 | const data = r.Data; 99 | // try { 100 | // return JSON.parse(data.toString()) 101 | // } catch (err) { 102 | // return data; 103 | // } 104 | return data; 105 | }); 106 | // Apply the functions that use that stream 107 | return registry[name].map(f => f({Records: records})); 108 | }) 109 | .value()); 110 | } 111 | 112 | async runWatcher() { 113 | if (this.config.kinesis && this.config.kinesis.enabled) { 114 | this.log('Enabling poller'); 115 | 116 | // Create the Kinesis client 117 | const kinesis = new this.awsProvider.sdk.Kinesis(); 118 | 119 | // Load the registry 120 | const registry = await KinesisConsumer.createRegistry(this.serverless); 121 | // Get the first shard for every element in the registry 122 | // Right now, the stream iterators are local to this run. Eventually, we'll persist this somewhere 123 | let streamIterators = await Promise.props( 124 | _.chain(registry) 125 | // Grab keys 126 | .keys() 127 | // Map to [name, stream description promise] 128 | .map(name => [name, kinesis.describeStream({StreamName: name}).promise()]) 129 | // Map to [name, iterator promise] 130 | .map(([name, descP]) => { 131 | const iterP = descP.then(desc => kinesis.getShardIterator({ 132 | ShardId: desc.StreamDescription.Shards[0].ShardId, 133 | ShardIteratorType: 'TRIM_HORIZON', 134 | StreamName: name 135 | }).promise()); 136 | return [name, iterP]; 137 | }) 138 | // Back to an object 139 | .fromPairs() 140 | // Extract iterators 141 | .mapValues(iterP => iterP.then(iter => iter.ShardIterator)) 142 | // Grab the value 143 | .value()); 144 | 145 | let consecutiveErrors = 0; 146 | while (true) { // eslint-disable-line no-constant-condition 147 | this.debug(`Polling Kinesis streams: ${JSON.stringify(_.keys(registry))}`); 148 | // Repoll the streams 149 | const streamResults = await KinesisConsumer._repollStreams(kinesis, streamIterators); // eslint-disable-line 150 | try { 151 | await KinesisConsumer._runLambdas(streamResults, registry); // eslint-disable-line 152 | } catch (err) { 153 | consecutiveErrors += 1; 154 | if (consecutiveErrors > MAX_CONSECUTIVE_ERRORS) { 155 | this.log(`Exceeded maximum number of consecutive errors (${MAX_CONSECUTIVE_ERRORS})`); 156 | throw err; 157 | } 158 | this.log(`Failed to run Lambdas with error ${err.stack}. Continuing`); 159 | } finally { 160 | // Update the stream iterators 161 | streamIterators = _.mapValues(streamResults, result => result.NextShardIterator); 162 | } 163 | 164 | // Wait a bit 165 | await Promise.delay(this.config.kinesis.intervalMillis); // eslint-disable-line no-await-in-loop 166 | } 167 | } 168 | } 169 | } -------------------------------------------------------------------------------- /src/localstack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | const AWS = require('aws-sdk'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | import AbstractBaseClass from './abstractBaseClass'; 9 | 10 | const configFilePath = 'node_modules/serverless-offline-localstack/serverlessOfflineLocalstack.json'; 11 | 12 | export default class Localstack extends AbstractBaseClass { 13 | 14 | constructor(serverless, options) { 15 | super(serverless, options); 16 | 17 | this.log('Configuring serverless offline -> localstack'); 18 | 19 | this.config = serverless.service.custom && serverless.service.custom.serverlessOfflineLocalstack || {}; 20 | super.setDebug(this.config.debug); 21 | this.endpoints = this.config.endpoints || {}; 22 | this.endpointFile = this.config.endpointFile; 23 | 24 | this.AWS_SERVICES = { 25 | 'apigateway': 4567, 26 | 'cloudformation': 4581, 27 | 'cloudwatch': 4582, 28 | 'lambda': 4574, 29 | 'dynamodb': 4567, 30 | 's3': 4572, 31 | 'ses': 4579, 32 | 'sns': 4575, 33 | 'sqs': 4576 34 | }; 35 | 36 | if (this.endpointFile) { 37 | this.loadEndpointsFromDisk(this.endpointFile); 38 | } 39 | 40 | // Intercept Provider requests 41 | this.awsProvider = serverless.getProvider('aws'); 42 | this.awsProviderRequest = this.awsProvider.request.bind(this.awsProvider); 43 | this.awsProvider.request = this.interceptRequest.bind(this); 44 | } 45 | 46 | reconfigureAWS() { 47 | const host = this.config.host; 48 | const region = this.config.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1'; 49 | const accessKeyId = this.config.accessKeyId || process.env.AWS_ACCESS_KEY_ID || 'none'; 50 | const secretAccessKey = this.config.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY || 'none'; 51 | 52 | let configChanges = {}; 53 | 54 | // If a host has been configured, override each service 55 | if (host) { 56 | for (const service of Object.keys(this.AWS_SERVICES)) { 57 | const port = this.AWS_SERVICES[service]; 58 | const url = `${host}:${port}`; 59 | 60 | this.debug(`Reconfiguring service ${service} to use ${url}`); 61 | configChanges[service.toLowerCase()] = {endpoint: url}; 62 | } 63 | } 64 | 65 | // Override specific endpoints if specified 66 | if (this.endpoints) { 67 | for (const service of Object.keys(this.endpoints)) { 68 | const url = this.endpoints[service]; 69 | 70 | this.debug(`Reconfiguring service ${service} to use ${url}`); 71 | configChanges[service.toLowerCase()] = {endpoint: url}; 72 | } 73 | } 74 | 75 | // set additional required properties 76 | configChanges['region'] = region; 77 | configChanges['accessKeyId'] = accessKeyId; 78 | configChanges['secretAccessKey'] = secretAccessKey; 79 | 80 | this.debug('Final configuration: ' + JSON.stringify(configChanges)); 81 | // configure the serverless aws sdk 82 | this.awsProvider.sdk.config.update(configChanges); 83 | this.writeConfigs(configChanges); 84 | } 85 | 86 | loadEndpointsFromDisk(endpointFile) { 87 | let endpointJson; 88 | 89 | this.debug('Loading endpointJson from ' + endpointFile); 90 | 91 | try { 92 | endpointJson = JSON.parse(fs.readFileSync(endpointFile)); 93 | } catch (err) { 94 | throw new ReferenceError(`Endpoint: "${this.endpointFile}" is invalid: ${err}`); 95 | } 96 | 97 | for (const key of Object.keys(endpointJson)) { 98 | this.debug('Intercepting service ' + key); 99 | this.endpoints[key] = endpointJson[key]; 100 | } 101 | } 102 | 103 | interceptRequest(service, method, params) { 104 | // Template validation is not supported in LocalStack 105 | if (method === 'validateTemplate') { 106 | this.log('Skipping template validation: Unsupported in Localstack'); 107 | return Promise.resolve(''); 108 | } 109 | 110 | if (AWS.config[service]) { 111 | this.debug(`Using custom endpoint for ${service}: ${endpoint}`); 112 | 113 | if (AWS.config['s3'] && params.TemplateURL) { 114 | this.debug(`Overriding S3 templateUrl to ${AWS.config.s3.endpoint}`); 115 | params.TemplateURL = params.TemplateURL.replace(/https:\/\/s3.amazonaws.com/, AWS.config['s3']); 116 | } 117 | } 118 | 119 | return this.awsProviderRequest(service, method, params); 120 | } 121 | 122 | static configureAWS(AWSp) { 123 | const contents = fs.readFileSync(configFilePath); 124 | let configChanges = JSON.parse(contents); 125 | AWSp.config.update(configChanges); 126 | } 127 | 128 | writeConfigs(configChanges) { 129 | fs.writeFile(configFilePath, JSON.stringify(configChanges), function (err) { 130 | if (err) { 131 | throw err; 132 | } 133 | } 134 | ); 135 | } 136 | } --------------------------------------------------------------------------------