├── .eslintrc.json ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── Dockerfile ├── README.md ├── docker-compose.yml ├── example └── service │ ├── handler.js │ └── serverless.yml ├── package-lock.json ├── package.json ├── spec ├── helpers │ └── services.js ├── integration │ └── integration.spec.js ├── support │ ├── jasmine.json │ └── jasmine_integration.json └── unit │ └── index.spec.js └── src └── index.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "rules": { 9 | "quotes": ["error", "single"], 10 | "semi": ["error", "always"], 11 | "indent": ["error", 2], 12 | "linebreak-style": ["error", "unix"], 13 | "no-console": "warn", 14 | "no-unused-vars": "warn", 15 | "no-alert": "error", 16 | "prefer-const": "error", 17 | "no-var": "error", 18 | "arrow-parens": ["error", "always"], 19 | "no-tabs": "error" 20 | }, 21 | "parserOptions": { 22 | "ecmaVersion": 2017 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Serverless LocalStack CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | serverless-localstack-test: 12 | name: Serverless LocalStack CI 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | sls-major-version: ["2", "3"] 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup NodeJS 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '18' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v3 28 | env: 29 | cache-name: cache-node-modules-v${{ matrix.sls-major-version }} 30 | with: 31 | path: ~/.npm 32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.sls-major-version }}-${{ hashFiles('**/package-lock.json') }} 33 | restore-keys: | 34 | ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.sls-major-version }}- 35 | 36 | - name: Install deps 37 | run: | 38 | npm install 39 | 40 | - name: Update SLS version to v3 41 | if: ${{ matrix.sls-major-version == '3' }} 42 | run: | 43 | npm install serverless@3 44 | 45 | - name: Check installed version 46 | env: 47 | EXPECTED_SLS_MAJOR_VERSION: ${{ matrix.sls-major-version }} 48 | run : | 49 | installed_sls_version=$(npm list | grep serverless@ | sed -E 's/.*serverless@(.*)/\1/') 50 | echo "installed serverless version: ${installed_sls_version}" 51 | if [ "${installed_sls_version:0:1}" != ${EXPECTED_SLS_MAJOR_VERSION} ]; then 52 | echo "expected version ${EXPECTED_SLS_MAJOR_VERSION}, but installed ${installed_sls_version}" 53 | exit 1 54 | fi 55 | 56 | - name: Start LocalStack 57 | run: | 58 | pip install localstack awscli-local[ver1] 59 | docker pull localstack/localstack 60 | localstack start -d 61 | localstack wait -t 30 62 | 63 | - name: Run Lint and Test 64 | run: | 65 | set -e 66 | npm run lint 67 | npm run test 68 | 69 | - name: Integration Tests 70 | run: | 71 | npm link 72 | sleep 5; SLS_DEBUG=1 npm run test:integration 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release NPM Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out the code 14 | uses: actions/checkout@v3 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | registry-url: "https://registry.npmjs.org" 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | 25 | - name: Publish to npm 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }} 28 | run: npm publish 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc 2 | /lib 3 | /node_modules 4 | example/service/.serverless 5 | example/service/.serverless_plugins 6 | .idea 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .gitignore 3 | .travis.yml 4 | bin/rename 5 | doc/ 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6.10 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | RUN npm install 6 | 7 | ENTRYPOINT '/bin/bash' 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/localstack/serverless-localstack.svg?branch=master)](https://travis-ci.org/localstack/serverless-localstack) 2 | 3 | # LocalStack Serverless Plugin 4 | 5 | [Serverless](https://serverless.com/) Plugin to support running against [Localstack](https://github.com/localstack/localstack). 6 | 7 | This plugin allows Serverless applications to be deployed and tested on your local machine. Any requests to AWS to be redirected to a running LocalStack instance. 8 | 9 | Pre-requisites: 10 | * LocalStack 11 | 12 | ## Installation 13 | 14 | The easiest way to get started is to install via npm. 15 | 16 | npm install -g serverless 17 | npm install --save-dev serverless-localstack 18 | 19 | ## Configuring 20 | 21 | The plugin can be configured via `serverless.yml`, or alternatively via environment variables. 22 | 23 | There are two supported methods for configuring the endpoints, globally via the 24 | `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 | ### Configuration via serverless.yml 30 | 31 | Please refer to the example configuration template below. (Please note that most configurations 32 | in the sample are optional and need not be specified.) 33 | 34 | ``` 35 | service: myService 36 | 37 | plugins: 38 | - serverless-localstack 39 | 40 | custom: 41 | localstack: 42 | stages: 43 | # list of stages for which the plugin should be enabled 44 | - local 45 | host: http://localhost # optional - LocalStack host to connect to 46 | edgePort: 4566 # optional - LocalStack edge port to connect to 47 | autostart: true # optional - Start LocalStack in Docker on Serverless deploy 48 | networks: #optional - attaches the list of networks to the localstack docker container after startup 49 | - host 50 | - overlay 51 | - my_custom_network 52 | lambda: 53 | # Enable this flag to improve performance 54 | mountCode: true # specify either "true", or a relative path to the root Lambda mount path 55 | docker: 56 | # Enable this flag to run "docker ..." commands as sudo 57 | sudo: False 58 | compose_file: /home/localstack_compose.yml # optional to use docker compose instead of docker or localstack cli 59 | stages: 60 | local: 61 | ... 62 | ``` 63 | 64 | ### Configuration via environment variables 65 | 66 | The following environment variables can be configured (taking precedence over the values in `serverless.yml`): 67 | * `AWS_ENDPOINT_URL`: LocalStack endpoint URL to connect to (default: `http://localhost:4566`). This is the recommended configuration, and replaces the deprecated config options (`EDGE_PORT`/`LOCALSTACK_HOSTNAME`/`USE_SSL`) below. 68 | * `EDGE_PORT`: LocalStack edge port to connect to (deprecated; default: `4566`) 69 | * `LOCALSTACK_HOSTNAME`: LocalStack host name to connect to (deprecated; default: `localhost`) 70 | * `USE_SSL`: Whether to use SSL/HTTPS when connecting to the LocalStack endpoint (deprecated) 71 | 72 | ### Activating the plugin for certain stages 73 | 74 | Note the `stages` attribute in the config above. The `serverless-localstack` plugin gets activated if either: 75 | 1. the serverless stage (explicitly defined or default stage "dev") is included in the `stages` config; or 76 | 2. serverless is invoked without a `--stage` flag (default stage "dev") and no `stages` config is provided 77 | 78 | ### Mounting Lambda code for better performance 79 | 80 | Note that the `localstack.lambda.mountCode` flag above will mount the local directory into 81 | the Docker container that runs the Lambda code in LocalStack. You can either specify the boolean 82 | value `true` (to mount the project root folder), or a relative path to the root Lambda mount path 83 | within your project (e.g., `./functions`). 84 | 85 | If you remove this flag, your Lambda code is deployed in the traditional way which is more in 86 | line with how things work in AWS, but also comes with a performance penalty: packaging the code, 87 | uploading it to the local S3 service, downloading it in the local Lambda API, extracting 88 | it, and finally copying/mounting it into a Docker container to run the Lambda. Mounting code 89 | from multiple projects is not supported with simple configuration, and you must use the 90 | `autostart` feature, as your code will be mounted in docker at start up. If you do need to 91 | mount code from multiple serverless projects, manually launch 92 | localstack with volumes specified. For example: 93 | 94 | ```sh 95 | localstack start --docker -d \ 96 | -v /path/to/project-a:/path/to/project-a \ 97 | -v /path/to/project-b:/path/to/project-b 98 | ``` 99 | 100 | If you use either `serverless-webpack`, `serverless-plugin-typescript`, or `serverless-esbuild`, `serverless-localstack` 101 | will detect it and modify the mount paths to point to your output directory. You will need to invoke 102 | the build command in order for the mounted code to be updated. (eg: `serverless webpack`). There is no 103 | `--watch` support for this out of the box, but could be accomplished using nodemon: 104 | 105 | ```sh 106 | npm i --save-dev nodemon 107 | ``` 108 | 109 | Webpack example's `package.json`: 110 | 111 | ```json 112 | "scripts": { 113 | "build": "serverless webpack --stage local", 114 | "deploy": "serverless deploy --stage local", 115 | "watch": "nodemon -w src -e '.*' -x 'npm run build'", 116 | "start": "npm run deploy && npm run watch" 117 | }, 118 | ``` 119 | 120 | ```sh 121 | npm run start 122 | ``` 123 | 124 | #### A note on using webpack 125 | 126 | `serverless-webpack` is supported, with code mounting. However, there are some assumptions 127 | and configuration requirements. First, your output directory must be `.webpack`. Second, you must retain 128 | your output directory contents. You can do this by modifying the `custom > webpack` portion of your 129 | serverless configuration file. 130 | 131 | ```yml 132 | custom: 133 | webpack: 134 | webpackConfig: webpack.config.js 135 | includeModules: true 136 | keepOutputDirectory: true 137 | localstack: 138 | stages: 139 | - local 140 | lambda: 141 | mountCode: true 142 | autostart: true 143 | ``` 144 | 145 | ### Environment Configurations 146 | 147 | * `LAMBDA_MOUNT_CWD`: Allow users to define a custom working directory for Lambda mounts. 148 | For example, when deploying a Serverless app in a Linux VM (that runs Docker) on a 149 | Windows host where the `-v :` flag to `docker run` requires us 150 | to specify a `local_dir` relative to the Windows host file system that is mounted 151 | into the VM (e.g., `"c:/users/guest/..."`). 152 | * `LAMBDA_EXECUTOR`: Executor type to use for running Lambda functions (default `docker`) - 153 | see [LocalStack repo](https://github.com/localstack/localstack) 154 | * `LAMBDA_REMOTE_DOCKER`: Whether to assume that we're running Lambda containers against 155 | a remote Docker daemon (default `false`) - see [LocalStack repo](https://github.com/localstack/localstack) 156 | * `BUCKET_MARKER_LOCAL`: Magic S3 bucket name for Lambda mount and [Hot Reloading](https://docs.localstack.cloud/user-guide/tools/lambda-tools/hot-reloading/). 157 | 158 | ### Only enable serverless-localstack for the listed stages 159 | * ```serverless deploy --stage local``` would deploy to LocalStack. 160 | * ```serverless deploy --stage production``` would deploy to aws. 161 | 162 | ``` 163 | service: myService 164 | 165 | plugins: 166 | - serverless-localstack 167 | 168 | custom: 169 | localstack: 170 | stages: 171 | - local 172 | - dev 173 | endpointFile: path/to/file.json 174 | ``` 175 | 176 | ## LocalStack 177 | 178 | For full documentation, please refer to https://github.com/localstack/localstack 179 | 180 | ## Contributing 181 | 182 | Setting up a development environment is easy using Serverless' plugin framework. 183 | 184 | ### Clone the Repo 185 | 186 | ``` 187 | git clone https://github.com/localstack/serverless-localstack 188 | ``` 189 | 190 | ### Setup your project 191 | 192 | ``` 193 | cd /path/to/serverless-localstack 194 | npm link 195 | 196 | cd myproject 197 | npm link serverless-localstack 198 | ``` 199 | 200 | ### Optional Debug Flag 201 | 202 | An optional debug flag is supported via `serverless.yml` that will enable additional debug logs. 203 | 204 | ``` 205 | custom: 206 | localstack: 207 | debug: true 208 | ``` 209 | 210 | ## Change Log 211 | * v1.3.1: prevent the mounting of code if the Lambda uses an ECR Image 212 | * v1.3.0: add support for built-in Esbuild in Serverless Framework v4 #267 213 | * v1.2.1: Fix custom-resource bucket compatibility with serverless >3.39.0, continue improving support for `AWS_ENDPOINT_URL` 214 | * v1.2.0: Add docker-compose config and fix autostart when plugin is not active 215 | * v1.1.3: Fix replacing host from environment variable `AWS_ENDPOINT_URL` 216 | * v1.1.2: Unify construction of target endpoint URL, add support for configuring `AWS_ENDPOINT_URL` 217 | * v1.1.1: Fix layer deployment if `mountCode` is enabled by always packaging and deploying 218 | * v1.1.0: Fix SSM environment variables resolving issues with serverless v3, change default for `BUCKET_MARKER_LOCAL` to `hot-reload` 219 | * v1.0.6: Add `BUCKET_MARKER_LOCAL` configuration for customizing S3 bucket for lambda mount and [Hot Reloading](https://docs.localstack.cloud/user-guide/tools/lambda-tools/hot-reloading/). 220 | * v1.0.5: Fix S3 Bucket LocationConstraint issue when the provider region is `us-east-1` 221 | * v1.0.4: Fix IPv4 fallback check to prevent IPv6 connection issue with `localhost` on macOS 222 | * v1.0.3: Set S3 Path addressing for internal Serverless Custom Resources - allow configuring S3 Events Notification for functions 223 | * v1.0.2: Add check to prevent IPv6 connection issue with `localhost` on MacOS 224 | * v1.0.1: Add support for Serverless projects with esbuild source config; enable config via environment variables 225 | * v1.0.0: Allow specifying path for mountCode, to point to a relative Lambda mount path 226 | * v0.4.36: Add patch to avoid "TypeError" in AwsDeploy plugin on Serverless v3.4.0+ 227 | * v0.4.35: Add config option to connect to additional Docker networks 228 | * v0.4.33: Fix parsing StepFunctions endpoint if the endpointInfo isn't defined 229 | * v0.4.32: Add endpoint to AWS credentials for compatibility with serverless-domain-manager plugin 230 | * v0.4.31: Fix format of API GW endpoints printed in stack output 231 | * v0.4.30: Fix plugin for use with Serverless version 2.30+ 232 | * v0.4.29: Add missing service endpoints to config 233 | * v0.4.28: Fix plugin activation for variable refs in profile names 234 | * v0.4.27: Fix loading of endpoints file with variable references to be resolved 235 | * v0.4.26: Fix resolution of template variables during plugin initialization 236 | * v0.4.25: Use single edge port instead of deprecated service-specific ports 237 | * v0.4.24: Fix resolving of stage/profiles via variable expansion 238 | * v0.4.23: Fix config loading to enable file imports; fix output of API endpoints if plugin is not activated; enable SSM and CF output refs by performing early plugin loading 239 | * v0.4.21: Fix integration with `serverless-plugin-typescript` when `mountCode` is enabled 240 | * v0.4.20: Use `LAMBDA_EXECUTOR`/`LAMBDA_REMOTE_DOCKER` configurations from environment 241 | * v0.4.19: Fix populating local test credentials in AWS provider 242 | * v0.4.18: Fix output of API Gateway endpoints; add port mappings; fix config init code 243 | * v0.4.17: Enable configuration of `$START_WEB` 244 | * v0.4.16: Add option for running Docker as sudo; add fix for downloadPackageArtifacts 245 | * v0.4.15: Enable plugin on aws:common:validate events 246 | * v0.4.14: Initialize LocalStack using hooks for each "before:" event 247 | * v0.4.13: Add endpoint for SSM; patch serverless-secrets plugin; allow customizing $DOCKER_FLAGS 248 | * v0.4.12: Fix Lambda packaging for `mountCode:false` 249 | * v0.4.11: Add polling loop for starting LocalStack in Docker 250 | * v0.4.8: Auto-create deployment bucket; autostart LocalStack in Docker 251 | * v0.4.7: Set S3 path addressing; add eslint to CI config 252 | * v0.4.6: Fix port mapping for service endpoints 253 | * v0.4.5: Fix config to activate or deactivate the plugin for certain stages 254 | * v0.4.4: Add `LAMBDA_MOUNT_CWD` configuration for customizing Lambda mount dir 255 | * v0.4.3: Support local mounting of Lambda code to improve performance 256 | * v0.4.0: Add support for local STS 257 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | localstack: 5 | container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}" 6 | image: localstack/localstack 7 | ports: 8 | - "127.0.0.1:4566:4566" # LocalStack Gateway 9 | - "127.0.0.1:4510-4559:4510-4559" # external services port range 10 | environment: 11 | # LocalStack configuration: https://docs.localstack.cloud/references/configuration/ 12 | - DEBUG=${DEBUG:-0} 13 | volumes: 14 | - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack" 15 | - "/var/run/docker.sock:/var/run/docker.sock" 16 | -------------------------------------------------------------------------------- /example/service/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.hello = (event, context, callback) => { 4 | process.stdout.write(event.Records[0].EventSource); 5 | process.stdout.write(event.Records[0].Sns.Message); 6 | callback(null, { message: 'Hello from SNS!', event }); 7 | }; 8 | -------------------------------------------------------------------------------- /example/service/serverless.yml: -------------------------------------------------------------------------------- 1 | service: aws-nodejs 2 | 3 | provider: 4 | name: aws 5 | profile: ${opt:profile, self:custom.profile} 6 | stage: ${opt:stage, self:custom.defaultStage} 7 | runtime: nodejs20.x 8 | lambdaHashingVersion: '20201221' 9 | 10 | custom: 11 | defaultStage: local 12 | profile: default 13 | localstack: 14 | debug: true 15 | stages: [local] 16 | autostart: true 17 | compose_file: /home/localstack/Projects/serverless-localstack/docker-compose.yml 18 | 19 | functions: 20 | hello: 21 | handler: handler.hello 22 | environment: 23 | SSM_VAR: ${ssm:abc} 24 | # CF_VAR: ${cf:def} 25 | 26 | plugins: 27 | - serverless-localstack 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-localstack", 3 | "version": "1.3.1", 4 | "description": "Connect Serverless to LocalStack!", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "lint": "node_modules/eslint/bin/eslint.js .", 8 | "lint-fix": "node_modules/eslint/bin/eslint.js . --fix", 9 | "test": "JASMINE_CONFIG_PATH=spec/support/jasmine.json jasmine", 10 | "test:watch": "JASMINE_CONFIG_PATH=spec/support/jasmine.json jasmine", 11 | "test:integration": "JASMINE_CONFIG_PATH=spec/support/jasmine_integration.json jasmine", 12 | "test:integration:watch": "JASMINE_CONFIG_PATH=spec/support/jasmine_integration.json nodemon -L -i spec/integrate ./node_modules/.bin/jasmine", 13 | "test:cover": "jasmine --coverage" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+ssh://git@github.com/localstack/serverless-localstack" 18 | }, 19 | "author": "LocalStack Team ", 20 | "contributors": [ 21 | "LocalStack Team ", 22 | "Waldemar Hummer (whummer)", 23 | "Ben Simon Hartung (bentsku)", 24 | "Joel Scheuner (joe4dev)", 25 | "temyers", 26 | "Justin McCormick ", 27 | "djKooks", 28 | "yohei1126" 29 | ], 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/localstack/serverless-localstack/issues" 33 | }, 34 | "homepage": "https://github.com/localstack/serverless-localstack", 35 | "dependencies": { 36 | "adm-zip": "^0.5.10", 37 | "aws-sdk": "^2.402.0", 38 | "es6-promisify": "^6.0.1" 39 | }, 40 | "devDependencies": { 41 | "chai": "^4.1.2", 42 | "chai-string": "^1.4.0", 43 | "eslint": "^8.56.0", 44 | "fs-extra": "^7.0.0", 45 | "jasmine": "^3.2.0", 46 | "js-yaml": ">=3.13.1", 47 | "json2yaml": "^1.1.0", 48 | "lodash": ">=4.17.13", 49 | "mixin-deep": ">=1.3.2", 50 | "nodemon": "^2.0.20", 51 | "prettier": "^3.2.5", 52 | "rimraf": "^2.6.2", 53 | "serverless": "^2.30.0", 54 | "set-value": ">=2.0.1", 55 | "sinon": "^6.2.0", 56 | "tempy": "^0.2.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /spec/helpers/services.js: -------------------------------------------------------------------------------- 1 | const tempy = require('tempy'); 2 | const execSync = require('child_process').execSync; 3 | const YAML = require('json2yaml'); 4 | const fs = require('fs-extra'); 5 | const path = require('path'); 6 | const serverlessExec = path.join( 7 | __dirname, 8 | '../../node_modules/.bin/serverless', 9 | ); 10 | const packageJson = require('../../package.json'); 11 | const rimraf = require('rimraf'); 12 | 13 | const debug = false; 14 | 15 | const defaultConfig = { 16 | service: 'aws-nodejs', 17 | provider: { 18 | name: 'aws', 19 | runtime: 'nodejs20.x', 20 | lambdaHashingVersion: '20201221', 21 | environment: { 22 | LAMBDA_STAGE: 23 | '${ssm:/${opt:stage, self:provider.stage}/lambda/common/LAMBDA_STAGE}', 24 | }, 25 | }, 26 | plugins: ['serverless-localstack'], 27 | custom: { 28 | localstack: { 29 | host: 'http://localhost', 30 | debug: debug, 31 | }, 32 | }, 33 | functions: { 34 | hello: { 35 | handler: 'handler.hello', 36 | }, 37 | }, 38 | }; 39 | 40 | const installPlugin = (dir) => { 41 | const pluginsDir = path.join(dir, '.serverless_plugins'); 42 | fs.mkdirsSync(pluginsDir); 43 | 44 | execSync('mkdir -p node_modules', { cwd: dir }); 45 | execSync(`ln -s ${__dirname}/../../ node_modules/${packageJson.name}`, { 46 | cwd: dir, 47 | }); 48 | }; 49 | 50 | const execServerless = (myArguments, dir) => { 51 | process.chdir(dir); 52 | 53 | execSync(`${serverlessExec} ${myArguments}`, { 54 | stdio: 'inherit', 55 | stderr: 'inherit', 56 | env: Object.assign({}, process.env, { 57 | AWS_ACCESS_KEY_ID: 1234, 58 | AWS_SECRET_ACCESS_KEY: 1234, 59 | PATH: process.env.PATH, 60 | SLS_DEBUG: debug ? '*' : '', 61 | }), 62 | }); 63 | }; 64 | 65 | exports.createService = (config, dir) => { 66 | dir = dir || tempy.directory(); 67 | config = Object.assign({}, defaultConfig, config); 68 | 69 | execServerless('create --template aws-nodejs', dir); 70 | 71 | fs.writeFileSync(`${dir}/serverless.yml`, YAML.stringify(config)); 72 | installPlugin(dir); 73 | 74 | return dir; 75 | }; 76 | 77 | exports.deployService = (dir) => { 78 | execServerless('deploy', dir); 79 | }; 80 | 81 | exports.removeService = (dir) => { 82 | rimraf.sync(dir); 83 | }; 84 | -------------------------------------------------------------------------------- /spec/integration/integration.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const services = require('../helpers/services'); 4 | 5 | const LONG_TIMEOUT = 30000; 6 | const AWS = require('aws-sdk'); 7 | 8 | // Set the region and endpoint in the config for LocalStack 9 | AWS.config.update({ 10 | region: 'us-east-1', 11 | endpoint: 'http://127.0.0.1:4566', 12 | }); 13 | AWS.config.credentials = new AWS.Credentials({ 14 | accessKeyId: 'test', 15 | secretAccessKey: 'test', 16 | }); 17 | 18 | const ssm = new AWS.SSM(); 19 | 20 | const params = { 21 | Name: '/dev/lambda/common/LAMBDA_STAGE', 22 | Type: 'String', 23 | Value: 'my-value', 24 | Overwrite: true, 25 | }; 26 | 27 | describe('LocalstackPlugin', () => { 28 | beforeEach(async () => { 29 | await ssm.putParameter(params).promise(); 30 | this.service = services.createService({}); 31 | }); 32 | 33 | afterEach(() => { 34 | services.removeService(this.service); 35 | }); 36 | 37 | it( 38 | 'should deploy a stack', 39 | () => { 40 | services.deployService(this.service); 41 | }, 42 | LONG_TIMEOUT, 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "unit/**/*.spec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /spec/support/jasmine_integration.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "integration/**/*.spec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /spec/unit/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const LocalstackPlugin = require('../../src/index'); 3 | const chai = require('chai'); 4 | const expect = require('chai').expect; 5 | const sinon = require('sinon'); 6 | const AWS = require('aws-sdk'); 7 | const Serverless = require('serverless'); 8 | let AwsProvider; 9 | try { 10 | AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider'); 11 | } catch (e) { 12 | AwsProvider = require('serverless/lib/plugins/aws/provider'); 13 | } 14 | 15 | chai.use(require('chai-string')); 16 | 17 | // Enable for more verbose logging 18 | const debug = false; 19 | 20 | describe('LocalstackPlugin', () => { 21 | let serverless; 22 | let awsProvider; 23 | let awsConfig; 24 | let instance; 25 | let sandbox; 26 | const defaultPluginState = {}; 27 | const config = { 28 | host: 'http://localhost', 29 | debug: debug, 30 | }; 31 | 32 | beforeEach(() => { 33 | sandbox = sinon.createSandbox(); 34 | serverless = new Serverless({ commands: ['deploy'], options: {} }); 35 | awsProvider = new AwsProvider(serverless, {}); 36 | awsConfig = new AWS.Config(); 37 | AWS.config = awsConfig; 38 | awsProvider.sdk = AWS; 39 | awsProvider.config = awsConfig; 40 | serverless.init(); 41 | serverless.setProvider('aws', awsProvider); 42 | if (serverless.cli) { 43 | serverless.cli.log = () => { 44 | if (debug) { 45 | console.log.apply(this, arguments); // eslint-disable-line no-console 46 | } 47 | }; 48 | } 49 | }); 50 | 51 | afterEach(() => { 52 | sandbox.restore(); 53 | }); 54 | 55 | const simulateBeforeDeployHooks = async function (instance) { 56 | instance.readConfig(); 57 | instance.activatePlugin(); 58 | instance.getStageVariable(); 59 | await instance.reconfigureAWS(); 60 | }; 61 | 62 | describe('#constructor()', () => { 63 | describe('with empty configuration', () => { 64 | beforeEach(async () => { 65 | serverless.service.custom = {}; 66 | instance = new LocalstackPlugin(serverless, defaultPluginState); 67 | await simulateBeforeDeployHooks(instance); 68 | }); 69 | 70 | it('should not set the endpoints', () => { 71 | expect(instance.endpoints).to.be.empty; 72 | }); 73 | 74 | it('should not set the endpoint file', () => { 75 | expect(instance.endpointFile).to.be.undefined; 76 | }); 77 | }); 78 | 79 | describe('with config file provided', () => { 80 | beforeEach(async () => { 81 | serverless.service.custom = { 82 | localstack: {}, 83 | }; 84 | instance = new LocalstackPlugin(serverless, defaultPluginState); 85 | await simulateBeforeDeployHooks(instance); 86 | }); 87 | 88 | it('should not set the endpoints if the stages config option does not include the deployment stage', async () => { 89 | serverless.service.custom.localstack.stages = ['production']; 90 | 91 | const plugin = new LocalstackPlugin(serverless, defaultPluginState); 92 | await simulateBeforeDeployHooks(plugin); 93 | expect(plugin.endpoints).to.be.empty; 94 | }); 95 | 96 | it('should set the endpoints if the stages config option includes the deployment stage', async () => { 97 | serverless.service.custom.localstack.stages = ['production', 'staging']; 98 | 99 | const plugin = new LocalstackPlugin(serverless, { 100 | stage: 'production', 101 | }); 102 | await simulateBeforeDeployHooks(plugin); 103 | 104 | expect(plugin.config.stages).to.deep.equal(['production', 'staging']); 105 | expect(plugin.config.stage).to.equal('production'); 106 | }); 107 | 108 | it('should fail if the endpoint file does not exist and the stages config option includes the deployment stage', () => { 109 | serverless.service.custom.localstack = { 110 | endpointFile: 'missing.json', 111 | stages: ['production'], 112 | }; 113 | 114 | const plugin = () => { 115 | const pluginInstance = new LocalstackPlugin(serverless, { 116 | stage: 'production', 117 | }); 118 | pluginInstance.readConfig(); 119 | }; 120 | 121 | expect(plugin).to.throw('Endpoint file "missing.json" is invalid:'); 122 | }); 123 | 124 | it('should not fail if the endpoint file does not exist when the stages config option does not include the deployment stage', () => { 125 | serverless.service.custom.localstack = { 126 | endpointFile: 'missing.json', 127 | stages: ['production'], 128 | }; 129 | 130 | const plugin = () => { 131 | const pluginInstance = new LocalstackPlugin(serverless, { 132 | stage: 'staging', 133 | }); 134 | pluginInstance.readConfig(); 135 | }; 136 | 137 | expect(plugin).to.not.throw('Endpoint file "missing.json" is invalid:'); 138 | }); 139 | 140 | it('should fail if the endpoint file is not json', () => { 141 | serverless.service.custom.localstack = { 142 | endpointFile: 'README.md', 143 | }; 144 | const plugin = () => { 145 | const pluginInstance = new LocalstackPlugin( 146 | serverless, 147 | defaultPluginState, 148 | ); 149 | pluginInstance.readConfig(); 150 | }; 151 | expect(plugin).to.throw(/Endpoint file "README.md" is invalid:/); 152 | }); 153 | }); 154 | }); 155 | 156 | describe('#request() bound on AWS provider', () => { 157 | beforeEach(() => { 158 | class FakeService { 159 | foo() { 160 | return this; 161 | } 162 | 163 | send() { 164 | return this; 165 | } 166 | } 167 | 168 | serverless.providers.aws.sdk.S3 = FakeService; 169 | serverless.service.custom = { 170 | localstack: {}, 171 | }; 172 | }); 173 | 174 | it('should overwrite the S3 hostname', async () => { 175 | const pathToTemplate = 'https://s3.amazonaws.com/path/to/template'; 176 | const request = sinon.stub(awsProvider, 'request'); 177 | instance = new LocalstackPlugin(serverless, defaultPluginState); 178 | await simulateBeforeDeployHooks(instance); 179 | 180 | await awsProvider.request('s3', 'foo', { 181 | TemplateURL: pathToTemplate, 182 | }); 183 | expect(request.called).to.be.true; 184 | const templateUrl = request.firstCall.args[2].TemplateURL; 185 | // url should either start with 'http://localhost' or 'http://127.0.0.1 186 | expect(templateUrl).to.satisfy( 187 | (url) => 188 | url === `${config.host}:4566/path/to/template` || 189 | url === 'http://127.0.0.1:4566/path/to/template', 190 | ); 191 | }); 192 | 193 | it('should overwrite the S3 hostname with the value from environment variable', async () => { 194 | // Set environment variable to overwrite the default host 195 | process.env.AWS_ENDPOINT_URL = 'http://localstack:4566'; 196 | 197 | const pathToTemplate = 'https://s3.amazonaws.com/path/to/template'; 198 | const request = sinon.stub(awsProvider, 'request'); 199 | instance = new LocalstackPlugin(serverless, defaultPluginState); 200 | await simulateBeforeDeployHooks(instance); 201 | 202 | await awsProvider.request('s3', 'foo', { 203 | TemplateURL: pathToTemplate, 204 | }); 205 | 206 | // Remove the environment variable again to not affect other tests 207 | delete process.env.AWS_ENDPOINT_URL; 208 | 209 | expect(request.called).to.be.true; 210 | const templateUrl = request.firstCall.args[2].TemplateURL; 211 | expect(templateUrl).to.equal('http://localstack:4566/path/to/template'); 212 | }); 213 | 214 | it('should not send validateTemplate calls to localstack', async () => { 215 | const request = sinon.stub(awsProvider, 'request'); 216 | instance = new LocalstackPlugin(serverless, defaultPluginState); 217 | await simulateBeforeDeployHooks(instance); 218 | 219 | await awsProvider.request('S3', 'validateTemplate', {}); 220 | 221 | expect(request.called).to.be.false; 222 | }); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const AdmZip = require('adm-zip'); 3 | const AWS = require('aws-sdk'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const net = require('net'); 7 | const { promisify } = require('es6-promisify'); 8 | const exec = promisify(require('child_process').exec); 9 | 10 | // Default stage used by Serverless 11 | const DEFAULT_STAGE = 'dev'; 12 | // Strings or other values considered to represent "true" 13 | const TRUE_VALUES = ['1', 'true', true]; 14 | // Plugin naming and build directory of serverless-plugin-typescript plugin 15 | const TS_PLUGIN_TSC = 'TypeScriptPlugin'; 16 | const TYPESCRIPT_PLUGIN_BUILD_DIR_TSC = '.build'; //TODO detect from tsconfig.json 17 | // Plugin naming and build directory of serverless-webpack plugin 18 | const TS_PLUGIN_WEBPACK = 'ServerlessWebpack'; 19 | const TYPESCRIPT_PLUGIN_BUILD_DIR_WEBPACK = '.webpack/service'; //TODO detect from webpack.config.js 20 | // Plugin naming and build directory of serverless-webpack plugin 21 | const TS_PLUGIN_ESBUILD = 'EsbuildServerlessPlugin'; 22 | const TYPESCRIPT_PLUGIN_BUILD_DIR_ESBUILD = '.esbuild/.build'; //TODO detect from esbuild.config.js 23 | // Plugin naming and build directory of esbuild built-in with Serverless Framework 24 | const TS_PLUGIN_BUILTIN_ESBUILD = 'Esbuild'; 25 | const TYPESCRIPT_PLUGIN_BUILD_DIR_BUILTIN_ESBUILD = '.serverless/build'; //TODO detect from esbuild.config.js 26 | 27 | // Default AWS endpoint URL 28 | const DEFAULT_AWS_ENDPOINT_URL = 'http://localhost:4566'; 29 | 30 | // Cache hostname to avoid unnecessary connection checks 31 | let resolvedHostname = undefined; 32 | 33 | const awsEndpointUrl = process.env.AWS_ENDPOINT_URL || DEFAULT_AWS_ENDPOINT_URL; 34 | 35 | class LocalstackPlugin { 36 | constructor(serverless, options) { 37 | this.serverless = serverless; 38 | this.options = options; 39 | this.hooks = { initialize: () => this.init() }; 40 | // Define a before-hook for all event types 41 | for (const event in this.serverless.pluginManager.hooks) { 42 | const doAdd = event.startsWith('before:'); 43 | if (doAdd && !this.hooks[event]) { 44 | this.hooks[event] = this.beforeEventHook.bind(this); 45 | } 46 | } 47 | // Define a hook for aws:info to fix output data 48 | this.hooks['aws:info:gatherData'] = this.fixOutputEndpoints.bind(this); 49 | 50 | // Define a hook for deploy:deploy to fix handler location for mounted lambda 51 | this.addHookInFirstPosition( 52 | 'deploy:deploy', 53 | this.patchTypeScriptPluginMountedCodeLocation, 54 | ); 55 | 56 | // Add a before hook for aws:common:validate and make sure it is in the very first position 57 | this.addHookInFirstPosition( 58 | 'before:aws:common:validate:validate', 59 | this.beforeEventHook, 60 | ); 61 | 62 | // Add a hook to fix TypeError when accessing undefined state attribute 63 | this.addHookInFirstPosition( 64 | 'before:aws:deploy:deploy:checkForChanges', 65 | this.beforeDeployCheckForChanges, 66 | ); 67 | 68 | const compileEventsHooks = 69 | this.serverless.pluginManager.hooks['package:compileEvents'] || []; 70 | compileEventsHooks.push({ 71 | pluginName: 'LocalstackPlugin', 72 | hook: this.patchCustomResourceLambdaS3ForcePathStyle.bind(this), 73 | }); 74 | 75 | this.awsServices = [ 76 | 'acm', 77 | 'amplify', 78 | 'apigateway', 79 | 'apigatewayv2', 80 | 'application-autoscaling', 81 | 'appsync', 82 | 'athena', 83 | 'autoscaling', 84 | 'batch', 85 | 'cloudformation', 86 | 'cloudfront', 87 | 'cloudsearch', 88 | 'cloudtrail', 89 | 'cloudwatch', 90 | 'cloudwatchlogs', 91 | 'codecommit', 92 | 'cognito-idp', 93 | 'cognito-identity', 94 | 'docdb', 95 | 'dynamodb', 96 | 'dynamodbstreams', 97 | 'ec2', 98 | 'ecr', 99 | 'ecs', 100 | 'eks', 101 | 'elasticache', 102 | 'elasticbeanstalk', 103 | 'elb', 104 | 'elbv2', 105 | 'emr', 106 | 'es', 107 | 'events', 108 | 'firehose', 109 | 'glacier', 110 | 'glue', 111 | 'iam', 112 | 'iot', 113 | 'iotanalytics', 114 | 'iotevents', 115 | 'iot-data', 116 | 'iot-jobs-data', 117 | 'kafka', 118 | 'kinesis', 119 | 'kinesisanalytics', 120 | 'kms', 121 | 'lambda', 122 | 'logs', 123 | 'mediastore', 124 | 'neptune', 125 | 'organizations', 126 | 'qldb', 127 | 'rds', 128 | 'redshift', 129 | 'route53', 130 | 's3', 131 | 's3control', 132 | 'sagemaker', 133 | 'sagemaker-runtime', 134 | 'secretsmanager', 135 | 'ses', 136 | 'sns', 137 | 'sqs', 138 | 'ssm', 139 | 'stepfunctions', 140 | 'sts', 141 | 'timestream', 142 | 'transfer', 143 | 'xray', 144 | ]; 145 | 146 | // Activate the synchronous parts of plugin config here in the constructor, but 147 | // run the async logic in enablePlugin(..) later via the hooks. 148 | this.activatePlugin(true); 149 | 150 | // If we're using webpack, we need to make sure we retain the compiler output directory 151 | if (this.detectTypescriptPluginType() === TS_PLUGIN_WEBPACK) { 152 | const p = this.serverless.pluginManager.plugins.find( 153 | (x) => x.constructor.name === TS_PLUGIN_WEBPACK, 154 | ); 155 | if ( 156 | this.shouldMountCode() && 157 | (!p || 158 | !p.serverless || 159 | !p.serverless.configurationInput || 160 | !p.serverless.configurationInput.custom || 161 | !p.serverless.configurationInput.custom.webpack || 162 | !p.serverless.configurationInput.custom.webpack.keepOutputDirectory) 163 | ) { 164 | throw new Error( 165 | 'When mounting Lambda code, you must retain webpack output directory. ' + 166 | 'Set custom.webpack.keepOutputDirectory to true.', 167 | ); 168 | } 169 | } 170 | } 171 | 172 | async init() { 173 | await this.reconfigureAWS(); 174 | } 175 | 176 | addHookInFirstPosition(eventName, hookFunction) { 177 | this.serverless.pluginManager.hooks[eventName] = 178 | this.serverless.pluginManager.hooks[eventName] || []; 179 | this.serverless.pluginManager.hooks[eventName].unshift({ 180 | pluginName: 'LocalstackPlugin', 181 | hook: hookFunction.bind(this, eventName), 182 | }); 183 | } 184 | 185 | activatePlugin(preHooks) { 186 | this.readConfig(preHooks); 187 | 188 | if (this.pluginActivated || !this.isActive()) { 189 | return Promise.resolve(); 190 | } 191 | 192 | // Intercept Provider requests 193 | if (!this.awsProviderRequest) { 194 | const awsProvider = this.getAwsProvider(); 195 | this.awsProviderRequest = awsProvider.request.bind(awsProvider); 196 | awsProvider.request = this.interceptRequest.bind(this); 197 | } 198 | 199 | // Patch plugin methods 200 | function compileFunction(functionName) { 201 | const functionObject = this.serverless.service.getFunction(functionName); 202 | if (functionObject.image || !this.shouldMountCode()) { 203 | return compileFunction._functionOriginal.apply(null, arguments); 204 | } 205 | functionObject.package = functionObject.package || {}; 206 | functionObject.package.artifact = __filename; 207 | return compileFunction._functionOriginal 208 | .apply(null, arguments) 209 | .then(() => { 210 | const resources = 211 | this.serverless.service.provider.compiledCloudFormationTemplate 212 | .Resources; 213 | Object.keys(resources).forEach((id) => { 214 | const res = resources[id]; 215 | if (res.Type === 'AWS::Lambda::Function') { 216 | res.Properties.Code.S3Bucket = 217 | process.env.BUCKET_MARKER_LOCAL || 'hot-reload'; // default changed to 'hot-reload' with LS v2 release 218 | res.Properties.Code.S3Key = process.cwd(); 219 | const mountCode = this.config.lambda.mountCode; 220 | if ( 221 | typeof mountCode === 'string' && 222 | mountCode.toLowerCase() !== 'true' 223 | ) { 224 | res.Properties.Code.S3Key = path.join( 225 | res.Properties.Code.S3Key, 226 | this.config.lambda.mountCode, 227 | ); 228 | } 229 | if (process.env.LAMBDA_MOUNT_CWD) { 230 | // Allow users to define a custom working directory for Lambda mounts. 231 | // For example, when deploying a Serverless app in a Linux VM (that runs Docker) on a 232 | // Windows host where the "-v :" flag to "docker run" requires us 233 | // to specify a "local_dir" relative to the Windows host file system that is mounted 234 | // into the VM (e.g., "c:/users/guest/..."). 235 | res.Properties.Code.S3Key = process.env.LAMBDA_MOUNT_CWD; 236 | } 237 | } 238 | }); 239 | }); 240 | } 241 | this.skipIfMountLambda( 242 | 'AwsCompileFunctions', 243 | 'compileFunction', 244 | compileFunction, 245 | ); 246 | this.skipIfMountLambda('AwsCompileFunctions', 'downloadPackageArtifacts'); 247 | this.skipIfMountLambda('AwsDeploy', 'extendedValidate'); 248 | if (this.detectTypescriptPluginType()) { 249 | this.skipIfMountLambda( 250 | this.detectTypescriptPluginType(), 251 | 'cleanup', 252 | null, 253 | [ 254 | 'after:package:createDeploymentArtifacts', 255 | 'after:deploy:function:packageFunction', 256 | ], 257 | ); 258 | } 259 | 260 | this.pluginActivated = true; 261 | } 262 | 263 | beforeEventHook() { 264 | if (this.pluginEnabled) { 265 | return Promise.resolve(); 266 | } 267 | 268 | this.activatePlugin(); 269 | 270 | this.pluginEnabled = true; 271 | return this.enablePlugin(); 272 | } 273 | 274 | beforeDeployCheckForChanges() { 275 | // patch to avoid "TypeError: reading 'console' of undefined" in plugins/aws/deploy/index.js on Sls v3.17.0+ 276 | const plugin = this.findPlugin('AwsDeploy'); 277 | if (plugin) { 278 | plugin.state = plugin.state || {}; 279 | } 280 | } 281 | 282 | enablePlugin() { 283 | // reconfigure AWS endpoints based on current stage variables 284 | this.getStageVariable(); 285 | 286 | return this.startLocalStack().then(() => { 287 | this.patchServerlessSecrets(); 288 | this.patchS3BucketLocationResponse(); 289 | this.patchS3CreateBucketLocationConstraint(); 290 | }); 291 | } 292 | 293 | // Convenience method for detecting JS/TS transpiler 294 | detectTypescriptPluginType() { 295 | if (this.findPlugin(TS_PLUGIN_TSC)) return TS_PLUGIN_TSC; 296 | if (this.findPlugin(TS_PLUGIN_WEBPACK)) return TS_PLUGIN_WEBPACK; 297 | if (this.findPlugin(TS_PLUGIN_ESBUILD)) return TS_PLUGIN_ESBUILD; 298 | const builtinEsbuildPlugin = this.findPlugin(TS_PLUGIN_BUILTIN_ESBUILD); 299 | if (builtinEsbuildPlugin && 300 | builtinEsbuildPlugin.constructor && 301 | typeof builtinEsbuildPlugin.constructor.WillEsBuildRun === 'function' && 302 | builtinEsbuildPlugin.constructor.WillEsBuildRun(this.serverless.configurationInput, this.serverless.serviceDir)) { 303 | return TS_PLUGIN_BUILTIN_ESBUILD; 304 | } 305 | return undefined; 306 | } 307 | 308 | // Convenience method for getting build directory of installed JS/TS transpiler 309 | getTSBuildDir() { 310 | const TS_PLUGIN = this.detectTypescriptPluginType(); 311 | if (TS_PLUGIN === TS_PLUGIN_TSC) return TYPESCRIPT_PLUGIN_BUILD_DIR_TSC; 312 | if (TS_PLUGIN === TS_PLUGIN_WEBPACK) 313 | return TYPESCRIPT_PLUGIN_BUILD_DIR_WEBPACK; 314 | if (TS_PLUGIN === TS_PLUGIN_ESBUILD) 315 | return TYPESCRIPT_PLUGIN_BUILD_DIR_ESBUILD; 316 | if (TS_PLUGIN === TS_PLUGIN_BUILTIN_ESBUILD) { 317 | return TYPESCRIPT_PLUGIN_BUILD_DIR_BUILTIN_ESBUILD; 318 | } 319 | return undefined; 320 | } 321 | 322 | findPlugin(name) { 323 | return this.serverless.pluginManager.plugins.find( 324 | (p) => p.constructor.name === name, 325 | ); 326 | } 327 | 328 | skipIfMountLambda(pluginName, functionName, overrideFunction, hookNames) { 329 | const plugin = this.findPlugin(pluginName); 330 | if (!plugin) { 331 | this.log('Warning: Unable to find plugin named: ' + pluginName); 332 | return; 333 | } 334 | if (!plugin[functionName]) { 335 | this.log( 336 | `Unable to find function ${functionName} on plugin ${pluginName}`, 337 | ); 338 | return; 339 | } 340 | const functionOriginal = plugin[functionName].bind(plugin); 341 | 342 | function overrideFunctionDefault() { 343 | if (this.shouldMountCode()) { 344 | const fqn = pluginName + '.' + functionName; 345 | this.log( 346 | 'Skip plugin function ' + fqn + ' (lambda.mountCode flag is enabled)', 347 | ); 348 | return Promise.resolve(); 349 | } 350 | return functionOriginal.apply(null, arguments); 351 | } 352 | 353 | overrideFunction = overrideFunction || overrideFunctionDefault; 354 | overrideFunction._functionOriginal = functionOriginal; 355 | const boundOverrideFunction = overrideFunction.bind(this); 356 | plugin[functionName] = boundOverrideFunction; 357 | 358 | // overwrite bound functions for specified hook names 359 | (hookNames || []).forEach((hookName) => { 360 | plugin.hooks[hookName] = boundOverrideFunction; 361 | const slsHooks = this.serverless.pluginManager.hooks[hookName] || []; 362 | slsHooks.forEach((hookItem) => { 363 | if (hookItem.pluginName === pluginName) { 364 | hookItem.hook = boundOverrideFunction; 365 | } 366 | }); 367 | }); 368 | } 369 | 370 | readConfig(preHooks) { 371 | if (this.configInitialized) { 372 | return; 373 | } 374 | 375 | const localstackConfig = 376 | (this.serverless.service.custom || {}).localstack || {}; 377 | this.config = Object.assign({}, this.options, localstackConfig); 378 | 379 | //Get the target deployment stage 380 | this.config.stage = ''; 381 | this.config.options_stage = this.options.stage || undefined; 382 | 383 | // read current stage variable - to determine whether to reconfigure AWS endpoints 384 | this.getStageVariable(); 385 | 386 | // If the target stage is listed in config.stages use the serverless-localstack-plugin 387 | // To keep default behavior if config.stages is undefined, then use serverless-localstack-plugin 388 | this.endpoints = this.endpoints || this.config.endpoints || {}; 389 | this.endpointFile = this.config.endpointFile; 390 | if (this.endpointFile && !this._endpointFileLoaded && this.isActive()) { 391 | try { 392 | this.loadEndpointsFromDisk(this.endpointFile); 393 | this._endpointFileLoaded = true; 394 | } catch (e) { 395 | if (!this.endpointFile.includes('${')) { 396 | throw e; 397 | } 398 | // Could be related to variable references not being resolved yet, and hence the endpoints file 399 | // name looks something like "${env:ENDPOINT_FILE}" -> this readConfig() function is called multiple 400 | // times from plugin hooks, hence we return here and expect that next time around it may work... 401 | return; 402 | } 403 | } 404 | 405 | this.configInitialized = this.configInitialized || !preHooks; 406 | } 407 | 408 | isActive() { 409 | // Activate the plugin if either: 410 | // (1) the serverless stage (explicitly defined or default stage "dev") is included in the `stages` config; or 411 | // (2) serverless is invoked without a --stage flag (default stage "dev") and no `stages` config is provided 412 | const effectiveStage = 413 | this.options.stage || this.config.stage || DEFAULT_STAGE; 414 | const noStageUsed = 415 | this.config.stages === undefined && effectiveStage == DEFAULT_STAGE; 416 | const includedInStages = 417 | this.config.stages && this.config.stages.includes(effectiveStage); 418 | return noStageUsed || includedInStages; 419 | } 420 | 421 | shouldMountCode() { 422 | return (this.config.lambda || {}).mountCode; 423 | } 424 | 425 | shouldRunDockerSudo() { 426 | return (this.config.docker || {}).sudo; 427 | } 428 | 429 | getStageVariable() { 430 | const customConfig = this.serverless.service.custom || {}; 431 | const providerConfig = this.serverless.service.provider || {}; 432 | this.debug('config.options_stage: ' + this.config.options_stage); 433 | this.debug('serverless.service.custom.stage: ' + customConfig.stage); 434 | this.debug('serverless.service.provider.stage: ' + providerConfig.stage); 435 | this.config.stage = 436 | this.config.options_stage || customConfig.stage || providerConfig.stage; 437 | this.debug('config.stage: ' + this.config.stage); 438 | } 439 | 440 | fixOutputEndpoints() { 441 | if (!this.isActive()) { 442 | return; 443 | } 444 | const plugin = this.findPlugin('AwsInfo'); 445 | const endpoints = plugin.gatheredData.info.endpoints || []; 446 | const edgePort = this.getEdgePort(); 447 | endpoints.forEach((entry, idx) => { 448 | // endpoint format for old Serverless versions 449 | const regex = /[^\s:]*:\/\/([^.]+)\.execute-api[^/]+\/([^/]+)(\/.*)?/g; 450 | const replace = `http://localhost:${edgePort}/restapis/$1/$2/_user_request_$3`; 451 | entry = entry.replace(regex, replace); 452 | // endpoint format for newer Serverless versions, e.g.: 453 | // - https://2e22431f.execute-api.us-east-1.localhost 454 | // - https://2e22431f.execute-api.us-east-1.localhost.localstack.cloud 455 | // - https://2e22431f.execute-api.us-east-1.amazonaws.com 456 | const regex2 = 457 | /[^\s:]*:\/\/([^.]+)\.execute-api\.[^/]+(([^/]+)(\/.*)?)?\/*$/g; 458 | const replace2 = `https://$1.execute-api.localhost.localstack.cloud:${edgePort}$2`; 459 | endpoints[idx] = entry.replace(regex2, replace2); 460 | }); 461 | 462 | // Replace ServerlessStepFunctions display 463 | this.stepFunctionsReplaceDisplay(); 464 | } 465 | 466 | /** 467 | * Start the LocalStack container in Docker, if it is not running yet. 468 | */ 469 | startLocalStack() { 470 | if (!(this.config.autostart && this.isActive())) { 471 | return Promise.resolve(); 472 | } 473 | 474 | const getContainer = () => { 475 | return exec('docker ps').then((stdout) => { 476 | const exists = stdout 477 | .split('\n') 478 | .filter( 479 | (line) => 480 | line.indexOf('localstack/localstack') >= 0 || 481 | line.indexOf('localstack/localstack-pro') >= 0 || 482 | line.indexOf('localstack_localstack') >= 0, 483 | ); 484 | if (exists.length) { 485 | return exists[0].replace('\t', ' ').split(' ')[0]; 486 | } 487 | }); 488 | }; 489 | 490 | const dockerStartupTimeoutMS = 1000 * 60 * 2; 491 | 492 | const checkStatus = (containerID, timeout) => { 493 | timeout = timeout || Date.now() + dockerStartupTimeoutMS; 494 | if (Date.now() > timeout) { 495 | this.log( 496 | 'Warning: Timeout when checking state of LocalStack container', 497 | ); 498 | return; 499 | } 500 | return this.sleep(4000).then(() => { 501 | this.log(`Checking state of LocalStack container ${containerID}`); 502 | return exec(`docker logs "${containerID}"`).then((logs) => { 503 | const ready = logs 504 | .split('\n') 505 | .filter((line) => line.indexOf('Ready.') >= 0); 506 | if (ready.length) { 507 | return Promise.resolve(); 508 | } 509 | return checkStatus(containerID, timeout); 510 | }); 511 | }); 512 | }; 513 | 514 | const addNetworks = async (containerID) => { 515 | if (this.config.networks) { 516 | for (const network in this.config.networks) { 517 | await exec( 518 | `docker network connect "${this.config.networks[network]}" ${containerID}`, 519 | ); 520 | } 521 | } 522 | return containerID; 523 | }; 524 | 525 | const startContainer = () => { 526 | this.log('Starting LocalStack in Docker. This can take a while.'); 527 | const cwd = process.cwd(); 528 | const env = this.clone(process.env); 529 | env.DEBUG = '1'; 530 | env.LAMBDA_EXECUTOR = env.LAMBDA_EXECUTOR || 'docker'; 531 | env.LAMBDA_REMOTE_DOCKER = env.LAMBDA_REMOTE_DOCKER || '0'; 532 | env.DOCKER_FLAGS = (env.DOCKER_FLAGS || '') + ` -v ${cwd}:${cwd}`; 533 | env.START_WEB = env.START_WEB || '0'; 534 | const maxBuffer = +env.EXEC_MAXBUFFER || 50 * 1000 * 1000; // 50mb buffer to handle output 535 | if (this.shouldRunDockerSudo()) { 536 | env.DOCKER_CMD = 'sudo docker'; 537 | } 538 | const options = { env: env, maxBuffer }; 539 | return exec('localstack start -d', options) 540 | .then(getContainer) 541 | .then((containerID) => addNetworks(containerID)) 542 | .then((containerID) => checkStatus(containerID)); 543 | }; 544 | 545 | const startCompose = () => { 546 | this.log( 547 | 'Starting LocalStack using the provided docker-compose file. This can take a while.', 548 | ); 549 | return exec( 550 | `docker-compose -f ${this.config.docker.compose_file} up -d`, 551 | ).then(getContainer); 552 | }; 553 | 554 | return getContainer().then((containerID) => { 555 | if (containerID) { 556 | return; 557 | } 558 | 559 | if (this.config.docker && this.config.docker.compose_file) { 560 | return startCompose(); 561 | } 562 | 563 | return startContainer(); 564 | }); 565 | } 566 | 567 | /** 568 | * Patch code location in case (1) serverless-plugin-typescript is 569 | * used, and (2) lambda.mountCode is enabled. 570 | */ 571 | patchTypeScriptPluginMountedCodeLocation() { 572 | if ( 573 | !this.shouldMountCode() || 574 | !this.detectTypescriptPluginType() || 575 | !this.isActive() 576 | ) { 577 | return; 578 | } 579 | const template = 580 | this.serverless.service.provider.compiledCloudFormationTemplate || {}; 581 | const resources = template.Resources || {}; 582 | Object.keys(resources).forEach((resName) => { 583 | const resEntry = resources[resName]; 584 | if (resEntry.Type === 'AWS::Lambda::Function') { 585 | resEntry.Properties.Handler = `${this.getTSBuildDir()}/${resEntry.Properties.Handler}`; 586 | } 587 | }); 588 | } 589 | 590 | /** 591 | * Patch S3 getBucketLocation invocation responses to return a 592 | * valid response ("us-east-1") instead of the default value "localhost". 593 | */ 594 | patchS3BucketLocationResponse() { 595 | const providerRequest = (service, method, params) => { 596 | const result = providerRequestOrig(service, method, params); 597 | if (service === 'S3' && method === 'getBucketLocation') { 598 | return result.then((res) => { 599 | if (res.LocationConstraint === 'localhost') { 600 | res.LocationConstraint = 'us-east-1'; 601 | } 602 | return Promise.resolve(res); 603 | }); 604 | } 605 | return result; 606 | }; 607 | const awsProvider = this.getAwsProvider(); 608 | const providerRequestOrig = awsProvider.request.bind(awsProvider); 609 | awsProvider.request = providerRequest; 610 | } 611 | 612 | /** 613 | * Patch S3 createBucket invocation to not add a LocationContraint if the region is `us-east-1` 614 | * The default SDK check was against endpoint and not the region directly. 615 | */ 616 | patchS3CreateBucketLocationConstraint() { 617 | AWS.util.update(AWS.S3.prototype, { 618 | createBucket: function createBucket(params, callback) { 619 | // When creating a bucket *outside* the classic region, the location 620 | // constraint must be set for the bucket and it must match the endpoint. 621 | // This chunk of code will set the location constraint param based 622 | // on the region (when possible), but it will not override a passed-in 623 | // location constraint. 624 | if (typeof params === 'function' || !params) { 625 | callback = callback || params; 626 | params = {}; 627 | } 628 | // copy params so that appending keys does not unintentionallly 629 | // mutate params object argument passed in by user 630 | const copiedParams = AWS.util.copy(params); 631 | if ( 632 | this.config.region !== 'us-east-1' && 633 | !params.CreateBucketConfiguration 634 | ) { 635 | copiedParams.CreateBucketConfiguration = { 636 | LocationConstraint: this.config.region, 637 | }; 638 | } 639 | return this.makeRequest('createBucket', copiedParams, callback); 640 | }, 641 | }); 642 | } 643 | 644 | /** 645 | * Patch the "serverless-secrets" plugin (if enabled) to use the local SSM service endpoint 646 | */ 647 | patchServerlessSecrets() { 648 | const slsSecretsAWS = this.findPlugin('ServerlessSecrets'); 649 | if (slsSecretsAWS) { 650 | slsSecretsAWS.config.options.providerOptions = 651 | slsSecretsAWS.config.options.providerOptions || {}; 652 | slsSecretsAWS.config.options.providerOptions.endpoint = 653 | this.getServiceURL(); 654 | slsSecretsAWS.config.options.providerOptions.accessKeyId = 'test'; 655 | slsSecretsAWS.config.options.providerOptions.secretAccessKey = 'test'; 656 | } 657 | } 658 | 659 | /** 660 | * Patch the AWS client library to use our local endpoint URLs. 661 | */ 662 | async reconfigureAWS() { 663 | if (this.isActive()) { 664 | if (this.reconfiguredEndpoints) { 665 | this.debug( 666 | 'Skipping reconfiguring of endpoints (already reconfigured)', 667 | ); 668 | return; 669 | } 670 | this.log('Using serverless-localstack'); 671 | const hostname = await this.getConnectHostname(); 672 | 673 | const configChanges = {}; 674 | 675 | // Configure dummy AWS credentials in the environment, to ensure the AWS client libs don't bail. 676 | const awsProvider = this.getAwsProvider(); 677 | const tmpCreds = awsProvider.getCredentials(); 678 | if (!tmpCreds.credentials) { 679 | const accessKeyId = process.env.AWS_ACCESS_KEY_ID || 'test'; 680 | const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY || 'test'; 681 | const fakeCredentials = new AWS.Credentials({ 682 | accessKeyId, 683 | secretAccessKey, 684 | }); 685 | configChanges.credentials = fakeCredentials; 686 | // set environment variables, ... 687 | process.env.AWS_ACCESS_KEY_ID = accessKeyId; 688 | process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey; 689 | // ..., then populate cache with new credentials 690 | awsProvider.cachedCredentials = null; 691 | awsProvider.getCredentials(); 692 | } 693 | 694 | // If a host has been configured, override each service 695 | const localEndpoint = this.getServiceURL(hostname); 696 | for (const service of this.awsServices) { 697 | const serviceLower = service.toLowerCase(); 698 | 699 | this.debug(`Reconfiguring service ${service} to use ${localEndpoint}`); 700 | configChanges[serviceLower] = { endpoint: localEndpoint }; 701 | 702 | if (serviceLower == 's3') { 703 | configChanges[serviceLower].s3ForcePathStyle = true; 704 | } 705 | } 706 | 707 | // Override specific endpoints if specified 708 | if (this.endpoints) { 709 | for (const service of Object.keys(this.endpoints)) { 710 | const url = this.endpoints[service]; 711 | const serviceLower = service.toLowerCase(); 712 | 713 | this.debug(`Reconfiguring service ${service} to use ${url}`); 714 | configChanges[serviceLower] = configChanges[serviceLower] || {}; 715 | configChanges[serviceLower].endpoint = url; 716 | } 717 | } 718 | 719 | // update SDK with overridden configs 720 | awsProvider.sdk.config.update(configChanges); 721 | if (awsProvider.cachedCredentials) { 722 | // required for compatibility with certain plugin, e.g., serverless-domain-manager 723 | awsProvider.cachedCredentials.endpoint = localEndpoint; 724 | } 725 | this.log('serverless-localstack: Reconfigured endpoints'); 726 | this.reconfiguredEndpoints = true; 727 | } else { 728 | this.endpoints = {}; 729 | this.log( 730 | 'Skipping serverless-localstack:\ncustom.localstack.stages: ' + 731 | JSON.stringify(this.config.stages) + 732 | '\nstage: ' + 733 | this.config.stage, 734 | ); 735 | } 736 | } 737 | 738 | /** 739 | * Load endpoint URLs from config file, if one exists. 740 | */ 741 | loadEndpointsFromDisk(endpointFile) { 742 | let endpointJson; 743 | 744 | this.debug('Loading endpointJson from ' + endpointFile); 745 | 746 | try { 747 | endpointJson = JSON.parse(fs.readFileSync(endpointFile)); 748 | } catch (err) { 749 | throw new ReferenceError( 750 | `Endpoint file "${this.endpointFile}" is invalid: ${err}`, 751 | ); 752 | } 753 | 754 | for (const key of Object.keys(endpointJson)) { 755 | this.debug('Intercepting service ' + key); 756 | this.endpoints[key] = endpointJson[key]; 757 | } 758 | } 759 | 760 | async interceptRequest(service, method, params) { 761 | // Enable the plugin here, if not yet enabled (the function call below is idempotent). 762 | // TODO: It seems that we can potentially remove the hooks / plugin loading logic 763 | // entirely and only rely on activating the -> we should evaluate this, as it would 764 | // substantially simplify the code in this file. 765 | this.beforeEventHook(); 766 | // Template validation is not supported in LocalStack 767 | if (method == 'validateTemplate') { 768 | this.log('Skipping template validation: Unsupported in Localstack'); 769 | return Promise.resolve(''); 770 | } 771 | 772 | const config = AWS.config[service.toLowerCase()] 773 | ? AWS.config 774 | : this.getAwsProvider().sdk.config; 775 | if (config[service.toLowerCase()]) { 776 | this.debug( 777 | `Using custom endpoint for ${service}: ${config[service.toLowerCase()].endpoint}`, 778 | ); 779 | 780 | if (config.s3 && params.TemplateURL) { 781 | this.debug(`Overriding S3 templateUrl to ${config.s3.endpoint}`); 782 | params.TemplateURL = params.TemplateURL.replace( 783 | /https:\/\/s3.amazonaws.com/, 784 | config.s3.endpoint, 785 | ); 786 | } 787 | } 788 | await this.reconfigureAWS(); 789 | 790 | return this.awsProviderRequest(service, method, params); 791 | } 792 | 793 | /* Utility functions below */ 794 | 795 | getEndpointPort() { 796 | const url = new URL(awsEndpointUrl); 797 | return url.port; 798 | } 799 | 800 | getEndpointHostname() { 801 | const url = new URL(awsEndpointUrl); 802 | return url.hostname; 803 | } 804 | 805 | getEndpointProtocol() { 806 | const url = new URL(awsEndpointUrl); 807 | return url.protocol.replace(':', ''); 808 | } 809 | 810 | getEdgePort() { 811 | return ( 812 | process.env.EDGE_PORT || this.config.edgePort || this.getEndpointPort() 813 | ); 814 | } 815 | 816 | /** 817 | * Determine the target hostname to connect to, as per the configuration. 818 | */ 819 | async getConnectHostname() { 820 | if (resolvedHostname) { 821 | // Use cached hostname to avoid repeated connection checks 822 | return resolvedHostname; 823 | } 824 | 825 | let hostname = 826 | process.env.LOCALSTACK_HOSTNAME || this.getEndpointHostname(); 827 | if (this.config.host) { 828 | hostname = this.config.host; 829 | if (hostname.indexOf('://') !== -1) { 830 | hostname = new URL(hostname).hostname; 831 | } 832 | } 833 | 834 | // Fall back to using local IPv4 address if connection to localhost fails. 835 | // This workaround transparently handles systems (e.g., macOS) where 836 | // localhost resolves to IPv6 when using Nodejs >=v17. See discussion: 837 | // https://github.com/localstack/aws-cdk-local/issues/76#issuecomment-1412590519 838 | // Issue: https://github.com/localstack/serverless-localstack/issues/125 839 | if (hostname === 'localhost') { 840 | try { 841 | const port = this.getEdgePort(); 842 | const options = { host: hostname, port: port }; 843 | await this.checkTCPConnection(options); 844 | } catch (e) { 845 | const fallbackHostname = '127.0.0.1'; 846 | this.debug( 847 | `Reconfiguring hostname to use ${fallbackHostname} (IPv4) because connection to ${hostname} failed`, 848 | ); 849 | hostname = fallbackHostname; 850 | } 851 | } 852 | 853 | // Cache resolved hostname 854 | resolvedHostname = hostname; 855 | return hostname; 856 | } 857 | 858 | /** 859 | * Checks whether a TCP connection to the given "options" can be established. 860 | * @param {object} options connection options of net.socket.connect() 861 | * https://nodejs.org/api/net.html#socketconnectoptions-connectlistener 862 | * Example: { host: "localhost", port: 4566 } 863 | * @returns {Promise} A fulfilled empty promise on successful connection and 864 | * a rejected promise on any connection error. 865 | */ 866 | checkTCPConnection(options) { 867 | return new Promise((resolve, reject) => { 868 | const socket = new net.Socket(); 869 | const client = socket.connect(options, () => { 870 | client.end(); 871 | resolve(); 872 | }); 873 | 874 | client.setTimeout(500); // milliseconds 875 | client.on('timeout', (err) => { 876 | client.destroy(); 877 | reject(err); 878 | }); 879 | 880 | client.on('error', (err) => { 881 | client.destroy(); 882 | reject(err); 883 | }); 884 | }); 885 | } 886 | 887 | getAwsProvider() { 888 | this.awsProvider = this.awsProvider || this.serverless.getProvider('aws'); 889 | return this.awsProvider; 890 | } 891 | 892 | getServiceURL(hostname) { 893 | if (process.env.AWS_ENDPOINT_URL) { 894 | return this.injectHostnameIntoLocalhostURL( 895 | process.env.AWS_ENDPOINT_URL, 896 | hostname, 897 | ); 898 | } 899 | hostname = hostname || 'localhost'; 900 | 901 | let proto = this.getEndpointProtocol(); 902 | if (process.env.USE_SSL) { 903 | proto = TRUE_VALUES.includes(process.env.USE_SSL) ? 'https' : 'http'; 904 | } else if (this.config.host) { 905 | proto = this.config.host.split('://')[0]; 906 | } 907 | const port = this.getEdgePort(); 908 | // little hack here - required to remove the default HTTPS port 443, as otherwise 909 | // routing for some platforms and ephemeral instances (e.g., on namespace.so) fails 910 | const isDefaultPort = 911 | (proto === 'http' && `${port}` === '80') || 912 | (proto === 'https' && `${port}` === '443'); 913 | if (isDefaultPort) { 914 | return `${proto}://${hostname}`; 915 | } 916 | return `${proto}://${hostname}:${port}`; 917 | } 918 | 919 | /** 920 | * If the given `endpointURL` points to `localhost`, then inject the given `hostname` into the URL 921 | * and return it. This helps fix IPv6 issues with node v18+ (see also getConnectHostname() above) 922 | */ 923 | injectHostnameIntoLocalhostURL(endpointURL, hostname) { 924 | const url = new URL(endpointURL); 925 | if (hostname && url.hostname === 'localhost') { 926 | url.hostname = hostname; 927 | } 928 | return url.origin; 929 | } 930 | 931 | log(msg) { 932 | if (this.serverless.cli) { 933 | this.serverless.cli.log.call(this.serverless.cli, msg); 934 | } 935 | } 936 | 937 | debug(msg) { 938 | if (this.config.debug) { 939 | this.log(msg); 940 | } 941 | } 942 | 943 | sleep(millis) { 944 | return new Promise((resolve) => setTimeout(resolve, millis)); 945 | } 946 | 947 | clone(obj) { 948 | return JSON.parse(JSON.stringify(obj)); 949 | } 950 | 951 | stepFunctionsReplaceDisplay() { 952 | const plugin = this.findPlugin('ServerlessStepFunctions'); 953 | if (plugin) { 954 | const endpoint = this.getServiceURL(); 955 | plugin.originalDisplay = plugin.display; 956 | plugin.localstackEndpoint = endpoint; 957 | 958 | const newDisplay = function () { 959 | const regex = /.*:\/\/([^.]+)\.execute-api[^/]+\/([^/]+)(\/.*)?/g; 960 | const newEndpoint = 961 | this.localstackEndpoint + '/restapis/$1/$2/_user_request_$3'; 962 | if (this.endpointInfo) { 963 | this.endpointInfo = this.endpointInfo.replace(regex, newEndpoint); 964 | } 965 | this.originalDisplay(); 966 | }; 967 | 968 | newDisplay.bind(plugin); 969 | plugin.display = newDisplay; 970 | } 971 | } 972 | patchCustomResourceLambdaS3ForcePathStyle() { 973 | const awsProvider = this.awsProvider; 974 | const patchMarker = path.join( 975 | awsProvider.serverless.serviceDir, 976 | '.serverless', 977 | '.internal-custom-resources-patched', 978 | ); 979 | const zipFilePath = path.join( 980 | awsProvider.serverless.serviceDir, 981 | '.serverless', 982 | awsProvider.naming.getCustomResourcesArtifactName(), 983 | ); 984 | 985 | function fileExists(filePath) { 986 | try { 987 | const stats = fs.statSync(filePath); 988 | return stats.isFile(); 989 | } catch (e) { 990 | return false; 991 | } 992 | } 993 | 994 | function createPatchMarker() { 995 | try { 996 | fs.open(patchMarker, 'a').close(); 997 | } catch (err) { 998 | return; 999 | } 1000 | } 1001 | 1002 | function patchPreV3() { 1003 | const utilFile = customResources.getEntry('utils.js'); 1004 | if (utilFile == null) return; 1005 | const data = utilFile.getData().toString(); 1006 | const legacyPatch = 'AWS.config.s3ForcePathStyle = true;'; 1007 | if (data.includes(legacyPatch)) { 1008 | createPatchMarker(); 1009 | return true; 1010 | } 1011 | const patchIndex = data.indexOf('AWS.config.logger = console;'); 1012 | if (patchIndex === -1) { 1013 | return false; 1014 | } 1015 | const newData = 1016 | data.slice(0, patchIndex) + legacyPatch + '\n' + data.slice(patchIndex); 1017 | utilFile.setData(newData); 1018 | return true; 1019 | } 1020 | 1021 | function patchV3() { 1022 | this.debug( 1023 | 'serverless-localstack: Patching V3', 1024 | ); 1025 | const customResourcesBucketFile = customResources.getEntry('s3/lib/bucket.js'); 1026 | if (customResourcesBucketFile == null) { 1027 | // TODO debugging, remove 1028 | this.log( 1029 | 'serverless-localstack: Could not find file s3/lib/bucket.js to patch.', 1030 | ); 1031 | return; 1032 | } 1033 | const data = customResourcesBucketFile.getData().toString(); 1034 | const oldClientCreation = 'S3Client({ maxAttempts: MAX_AWS_REQUEST_TRY });'; 1035 | const newClientCreation = 'S3Client({ maxAttempts: MAX_AWS_REQUEST_TRY, forcePathStyle: true });'; 1036 | if (data.includes(newClientCreation)) { 1037 | // patch already done 1038 | createPatchMarker(); 1039 | return; 1040 | } 1041 | const newData = data.replace(oldClientCreation, newClientCreation); 1042 | 1043 | customResourcesBucketFile.setData(newData); 1044 | } 1045 | 1046 | if (fileExists(patchMarker)) { 1047 | this.debug( 1048 | 'serverless-localstack: Serverless internal CustomResources already patched', 1049 | ); 1050 | return; 1051 | } 1052 | 1053 | const customResourceZipExists = fileExists(zipFilePath); 1054 | 1055 | if (!customResourceZipExists) { 1056 | return; 1057 | } 1058 | 1059 | const customResources = new AdmZip(zipFilePath); 1060 | 1061 | if (!patchPreV3.call(this)) { 1062 | patchV3.call(this); 1063 | } 1064 | customResources.writeZip(); 1065 | createPatchMarker(); 1066 | this.debug( 1067 | 'serverless-localstack: Serverless internal CustomResources patched', 1068 | ); 1069 | } 1070 | } 1071 | 1072 | module.exports = LocalstackPlugin; 1073 | --------------------------------------------------------------------------------