├── .eslintrc.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json └── src ├── index.js └── lib ├── aws-helper.js ├── collectFunctionEnvVariables.js ├── resolveCloudFormationEnvVariables.js └── transformEnvVarsToString.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | env: 4 | es6: true 5 | node: true 6 | mocha: true 7 | plugins: 8 | - prettier 9 | extends: 10 | - eslint:recommended 11 | rules: 12 | prettier/prettier: error 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .history 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jimdo GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡️ Serverless Export Env Plugin 2 | 3 | [![serverless](https://img.shields.io/npm/dependency-version/serverless-export-env/peer/serverless.svg?style=flat-square)](https://www.serverless.com) 4 | [![nodejs](https://img.shields.io/node/v/serverless-export-env.svg?style=flat-square)](https://nodejs.org/) 5 | [![npm](https://img.shields.io/npm/v/serverless-export-env.svg)](https://www.npmjs.com/package/serverless-export-env) 6 | [![license](https://img.shields.io/github/license/arabold/serverless-export-env.svg)](https://github.com/arabold/serverless-export-env/blob/master/LICENSE) 7 | [![dependencies](https://img.shields.io/librariesio/github/arabold/serverless-export-env.svg)](https://www.npmjs.com/package/serverless-export-env) 8 | [![prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://prettier.io/) 9 | 10 | ## About 11 | 12 | The [Serverless Framework](https://www.serverless.com/) offers a very powerful feature: You can reference AWS resources anywhere from within your `serverless.yml` and it will automatically resolve them to their respective values during deployment. However, this only works properly once your code is deployed to AWS. The _Serverless Export Env Plugin_ extends the Serverless Framework's built-in variable solution capabilities by adding support many additional CloudFormation intrinsic functions (`Fn::GetAtt`, `Fn::Join`, `Fn::Sub`, etc.) as well as variable references (`AWS::Region`, `AWS::StackId`, etc.). 13 | 14 | The _Serverless Export Env Plugin_ helps solve two main use cases: 15 | 16 | 1. It will automatically hook into the `sls invoke local` and `sls offline start` (see [Serverless Offline Plugin](https://github.com/dherault/serverless-offline)) and help resolve your environment variables. This is fully transparent to your application and other plugins. 17 | 2. Invoke `sls export-env` from the command line to generate a `.env` file on your local filesystem. Then use a library such as [dotenv](https://www.npmjs.com/package/dotenv) to import it into your code, e.g. during local integration tests. 18 | 19 | ## Usage 20 | 21 | Add the npm package to your project: 22 | 23 | ```sh 24 | # Via yarn 25 | $ yarn add arabold/serverless-export-env --dev 26 | 27 | # Via npm 28 | $ npm install arabold/serverless-export-env --save-dev 29 | ``` 30 | 31 | Add the plugin to your `serverless.yml`. It should be listed first to ensure it can resolve your environment variables before other plugins see them: 32 | 33 | ```yaml 34 | plugins: 35 | - serverless-export-env 36 | ``` 37 | 38 | That's it! You can now call `sls export-env` in your terminal to generate the `.env` file. Or, you can run `sls invoke local -f FUNCTION` or `sls offline start` to run your code locally as usual. 39 | 40 | ### Examples 41 | 42 | ```sh 43 | sls export-env 44 | ``` 45 | 46 | This will export all project-wide environment variables into a `.env` file in your project root folder. 47 | 48 | ```sh 49 | sls export-env --function MyFunction --filename .env-MyFunction 50 | ``` 51 | 52 | This will export environment variables of the `MyFunction` Lambda function into a `.env-MyFunction` file in your project root folder. 53 | 54 | ## Referencing CloudFormation resources 55 | 56 | As mentioned before, the Serverless Framework allows you to reference AWS resources anywhere from within your `serverless.yml` and it will automatically resolve them to their respective values during deployment. However, Serverless' built-in variable resolution is limited and will not always work when run locally. The _Serverless Export Env Plugin_ extends this functionality and automatically resolves commonly used [intrinsic functions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html) and initializes your local environment properly. 57 | 58 | ### Supported instrinsic functions 59 | 60 | - Condition Functions 61 | - `Fn::And` 62 | - `Fn::Equals` 63 | - `Fn::If` 64 | - `Fn::Not` 65 | - `Fn::Or` 66 | - `Fn::FindInMap` 67 | - `Fn::GetAtt` 68 | - `Fn::GetAZs` 69 | - `Fn::Join` 70 | - `Fn::Select` 71 | - `Fn::Split` 72 | - `Fn::Sub` (at the moment only key-value map subtitution is supported) 73 | - `Fn::ImportValue` 74 | - `Ref` 75 | 76 | ### Examples 77 | 78 | ```yaml 79 | provider: 80 | environment: 81 | S3_BUCKET_URL: 82 | Fn::Join: 83 | - "" 84 | - - https://s3.amazonaws.com/ 85 | - Ref: MyBucket 86 | ``` 87 | 88 | Or the short version: 89 | 90 | ```yaml 91 | provider: 92 | environment: 93 | S3_BUCKET_URL: !Join ["", [https://s3.amazonaws.com/, Ref: MyBucket]] 94 | ``` 95 | 96 | You can then access the environment variable in your code the usual way (e.g. `process.env.S3_BUCKET_URL`). 97 | 98 | ## Configuration 99 | 100 | The plugin supports various configuration options under `custom.export-env` in your `serverless.yml` file: 101 | 102 | ```yaml 103 | custom: 104 | export-env: 105 | filename: .env 106 | overwrite: false 107 | enableOffline: true 108 | ``` 109 | 110 | ### Configuration Options 111 | 112 | | Option | Default | Description | 113 | | -------------- | ------- | ------------------------------------------------------------------------------------------------- | 114 | | filename | `.env` | Target file name where to write the environment variables to, relative to the project root. | 115 | | enableOffline | `true` | Evaluate the environment variables when running `sls invoke local` or `sls offline start`. | 116 | | overwrite | `false` | Overwrite the file even if it exists already. | 117 | | refMap | `{}` | Mapping of [resource resolutions](#Custom-Resource-Resolution) for the `Ref` function | 118 | | getAttMap | `{}` | Mapping of [resource resolutions](#Custom-Resource-Resolution) for the `Fn::GetAtt` function | 119 | | importValueMap | `{}` | Mapping of [resource resolutions](#Custom-Resource-Resolution) for the `Fn::ImportValue` function | 120 | 121 | ### Custom Resource Resolution 122 | 123 | The plugin will try its best to resolve resource references like `Ref`, `Fn::GetAtt`, and `Fn::ImportValue` for you. However, sometimes this might fail, or you might want to use mocked values instead. In those cases, you can override those values using the `refMap`, `getAttMap`, and `importValueMap` options. 124 | 125 | - `refMap` takes a mapping of _resource name_ to _value_ pairs. 126 | - `getAttMap` takes a mapping of _resource name_ to _attribute/value_ pairs. 127 | - `importValueMap` takes a mapping of _import name_ to _value_ pairs. 128 | 129 | ```yaml 130 | custom: 131 | export-env: 132 | refMap: 133 | # Resolve `!Ref MyDynamoDbTable` as `mock-myTable` 134 | MyDynamoDbTable: "mock-myTable" 135 | getAttMap: 136 | # Resolve `!GetAtt MyElasticSearchInstance.DomainEndpoint` as `localhost:9200` 137 | MyElasticSearchInstance: 138 | DomainEndpoint: "localhost:9200" 139 | importValueMap: 140 | # Resolve `!ImportValue MyLambdaFunction` as `arn:aws:lambda:us-east-2::function:my-lambda-function` 141 | MyLambdaFunction: "arn:aws:lambda:us-east-2::function:my-lambda-function" 142 | ``` 143 | 144 | > 👉 Generally, it is recommended to avoid the use of intrinsic functions in your environment variables. Often, the same can be achieved by simply predefining a resource name and then manually construct the desired variable values. To share resources between different Serverless services, check out the `${cf:stackName.outputKey}` [variable resolution](https://www.serverless.com/framework/docs/providers/aws/guide/variables/) mechanism. 145 | 146 | ## Command-Line Options 147 | 148 | Running `sls export-env` will, by default, only export _global_ environment variables into your `.env` file (those defined under `provider.environment` in your `serverless.yml`). If you want to generate the `.env` file for a specific function, pass the function name as a command-line argument as follows: 149 | 150 | ```sh 151 | sls export-env --function hello --filename .env-hello 152 | ``` 153 | 154 | | Option | Description | 155 | | --------- | ------------------------------------------------------------------------------------------ | 156 | | filename | Target filename where to write the environment variables to, relative to the project root. | 157 | | overwrite | Overwrite the file even if it exists already. | 158 | | function | Name of a function for which to generate the .env file. | 159 | | all | Merge environment variables of all functions into a single .env file. | 160 | 161 | ## Provided lifecycle events 162 | 163 | - `export-env:collect` - Collect environment variables from Serverless 164 | - `export-env:resolve` - Resolve CloudFormation references and import variables 165 | - `export-env:apply` - Set environment variables when testing Lambda functions locally 166 | - `export-env:write` - Write environment variables to file 167 | 168 | ## Migrating from 1.x to 2.x 169 | 170 | - Running `sls invoke local` or `sls offline start` will no longer create or update your `.env` file. If you want to create an `.env` file, simply run `sls export-env` instead. 171 | - By default, the plugin will no longer overwrite any existing `.env` file. To enable overwriting existing files, either specify `--overwrite` in the command-line or set the `custom.export-env.overwrite` configuration option. 172 | - Resource `Outputs` values (`resources.Resources.Outputs.*`) are no longer getting exported automatically. This has always been a workaround and causes more problems than it solved. The plugin will try its best to resolve `Fn::GetAtt` and other references for you now, so there should be little need for the old behavior anymore. Add the desired value as an environment variable to `provider.environment` instead. 173 | - Running `sls export-env` will no longer merge the environment variables of all functions into a single `.env` file. Instead, pass the name of the desired function as `--function` argument to the command line. If no function name is specified, only project-wide environment variables will get exported. To bring back the old behavior, pass `--all` in command line and it will generate a file including all environment variables of all functions. However, please be aware that the behavior is undefined if functions use conflicting values for the same environment variable name. 174 | - The configuration options `filename` and `pathFromRoot` have been merged to `filename` now. You can specify relative paths in `filename` such as `./dist/.env` now. Make sure the target folder exists! 175 | 176 | ## Releases 177 | 178 | ### 2.2.0 179 | 180 | - Fixed error with latest `cfn-resolver-lib`. Thanks to [estahn](https://github.com/estahn) for the fix. 181 | - Updated dependencies to latest versions. 182 | 183 | ### 2.1.0 184 | 185 | - Compatibility with Serverless v3.x 186 | - Updated dependencies' minor versions 187 | 188 | ### 2.0.0 189 | 190 | - Removed optimistic variable resolution for `Fn::GetAtt` as it was not working properly and caused hard to solve issues. If you rely on `Fn::GetAtt` in your environment variables, define a custom resolution using the `getAttMap` [configuration option](#Configuration-Options). 191 | 192 | ### alpha.1 193 | 194 | - Added `--all` command line parameter to merge the environment variables of all functions into a single `.env` file. Please note that the behavior is _undefined_ if functions use conflicting values for the same environment variable name. 195 | 196 | ### alpha.0 197 | 198 | - Complete rewrite of the variable resolver. We use the amazing [cfn-resolver-lib](https://github.com/robessog/cfn-resolver-lib) lib now. This allows us to support not only `Ref` and `Fn::ImportValue` as in previous releases, but we're able to resolve the most commonly used intrinsic functions automatically now. 199 | 200 |
201 | 1.x Releases 202 | 203 | ### 1.4.4 204 | 205 | - Reverted changes in 1.4.1. Unfortunately, we broke the semver contract by introducing a breaking feature in a patch update. This feature needs to be rethought and added back in a 1.5.x release as optional. Until then, I had to remove it again. 206 | 207 | #### 1.4.3 208 | 209 | - Internal version (not published) 210 | 211 | ### 1.4.2 212 | 213 | - Fixed some compatibility issues with the latest Serverless framework release. Thanks to [pgrzesik](https://github.com/pgrzesik) for the necessary updates. 214 | 215 | ### 1.4.1 216 | 217 | - Disabled calls to the real aws infrastructure when running with Serverless Offline. Thanks to marooned071 for the contribution. 218 | 219 | ### 1.4.0 220 | 221 | - Collect and set resource values from actual Cloud Formation stack output. Thanks to [andersquist](https://github.com/andersquist) for his contribution! 222 | - Fix error when serverless.yml doesn't contain a custom section. Thanks to [michael-wolfenden](https://github.com/michael-wolfenden)! 223 | 224 | ### 1.3.1 225 | 226 | - Explicitly set environment variables during local invocation of the Lambda (`sls invoke local`) 227 | 228 | ### 1.3.0 229 | 230 | - Support different output filename and path. Thanks to [philiiiiiipp](https://github.com/philiiiiiipp). 231 | - Export `Outputs` as environment variables. Thanks to [lielran](https://github.com/lielran). 232 | - Updated to latest dependencies 233 | 234 | ### 1.2.0 235 | 236 | - Use operating-system-specific end-of-line when creating `.env` file 237 | 238 | ### 1.1.3 239 | 240 | - Fixed an issue with `AWS::AccountId` being resolved as `[Object Promise]` instead of the actual value. 241 | 242 | ### 1.1.2 243 | 244 | - Fixed an issue with CloudFormation resources not being resolved properly if the stack has more than 100 resources or exports. 245 | 246 | ### 1.1.1 247 | 248 | - Fix issue with multiple environment variables for function (thanks to [@Nevon](https://github.com/Nevon)). 249 | 250 | ### 1.1.0 251 | 252 | - Support `Fn::Join` operation (contribution by [@jonasho](https://github.com/jonasho)) 253 | - Support pseudo parameters `AWS::Region`, `AWS::AccountId`, `AWS::StackId` and `AWS::StackName`. 254 | 255 | ### 1.0.2 256 | 257 | - The plugin now properly resolves and sets the environment variables if a Lambda function is invoked locally (`sls invoke local -f FUNCTION`). This allows seamless as if the function would be deployed on AWS. 258 | 259 | ### 1.0.1 260 | 261 | - Corrected plugin naming 262 | - Improved documentation 263 | 264 | ### 1.0.0 265 | 266 | - This is the initial release with all basic functionality 267 |
268 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-export-env", 3 | "version": "2.2.0", 4 | "description": "Serverless plugin to export environment variables into a .env file", 5 | "main": "src/index.js", 6 | "author": "Andre Rabold", 7 | "license": "MIT", 8 | "scripts": { 9 | "lint": "eslint 'src/**/*.js'" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/arabold/serverless-export-env.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/arabold/serverless-export-env/issues" 17 | }, 18 | "engines": { 19 | "node": ">=12.0.0" 20 | }, 21 | "homepage": "https://github.com/arabold/serverless-export-env", 22 | "dependencies": { 23 | "bluebird": "^3.7.2", 24 | "cfn-resolver-lib": "^1.1.7", 25 | "lodash": "^4.17.20" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^8.17.0", 29 | "eslint-plugin-prettier": "^4.0.0", 30 | "prettier": "^2.1.2" 31 | }, 32 | "peerDependencies": { 33 | "serverless": ">=2" 34 | }, 35 | "prettier": { 36 | "printWidth": 120, 37 | "tabWidth": 2, 38 | "useTabs": false, 39 | "semi": true, 40 | "singleQuote": false, 41 | "quoteProps": "as-needed", 42 | "trailingComma": "es5", 43 | "bracketSpacing": true, 44 | "jsxBracketSameLine": false, 45 | "arrowParens": "always" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"), 4 | BbPromise = require("bluebird"), 5 | fs = require("fs"), 6 | path = require("path"); 7 | 8 | const collectFunctionEnvVariables = require("./lib/collectFunctionEnvVariables"); 9 | const resolveCloudFormationEnvVariables = require("./lib/resolveCloudFormationEnvVariables"); 10 | const transformEnvVarsToString = require("./lib/transformEnvVarsToString"); 11 | const { listExports, listStackResources, describeStack } = require("./lib/aws-helper"); 12 | 13 | /** 14 | * Serverless Plugin to extract Serverless' Lambda environment variables into 15 | * a local .env file for integration testing. 16 | */ 17 | class ExportEnv { 18 | constructor(serverless, options) { 19 | this.serverless = serverless; 20 | this.options = options; 21 | 22 | this.commands = { 23 | "export-env": { 24 | usage: "Exports your Serverless environment variables to a .env file ", 25 | lifecycleEvents: ["collect", "resolve", "apply", "write"], 26 | options: { 27 | function: { 28 | usage: 'Specify the function for which you want to generate the .env file (e.g. "--function myFunction")', 29 | shortcut: "f", 30 | required: false, 31 | type: "string", 32 | }, 33 | all: { 34 | usage: 'Merge environment variables of all functions into a single .env file (e.g. "--all")', 35 | required: false, 36 | type: "boolean", 37 | }, 38 | filename: { 39 | usage: 'Name of output file (e.g. "--filename .env")', 40 | shortcut: "p", 41 | required: false, 42 | type: "string", 43 | }, 44 | overwrite: { 45 | usage: 'Overwrite existing file (e.g. "--overwrite")', 46 | required: false, 47 | type: "boolean", 48 | }, 49 | }, 50 | }, 51 | }; 52 | 53 | this.isOfflineHooked = false; 54 | this.hooks = { 55 | "before:offline:start:init": this.initOfflineHook.bind(this), 56 | "before:offline:start": this.initOfflineHook.bind(this), 57 | // We monkey-patch the AWS plugin instead; so the `invoke:local:invoke` is not needed anymore 58 | // "before:invoke:local:invoke": this.initOfflineHook.bind(this), 59 | "export-env:collect": this.collectEnvVars.bind(this), 60 | "export-env:resolve": this.resolveEnvVars.bind(this), 61 | "export-env:apply": this.setEnvVars.bind(this), 62 | "export-env:write": this.writeEnvVars.bind(this), 63 | }; 64 | 65 | this.globalEnvironmentVariables = {}; 66 | this.functionEnvironmentVariables = {}; 67 | this.filename = _.get(options, "filename", ".env"); 68 | this.enableOffline = true; 69 | this.overwrite = _.get(options, "overwrite", false); 70 | this.refMap = {}; 71 | this.getAttMap = {}; 72 | this.importValueMap = {}; 73 | this.isEnabled = true; // will be set in `_loadConfig()` below 74 | 75 | // Monkey-patch `AwsInvokeLocal` plugin to support our custom variable resolution 76 | const AwsInvokeLocal = serverless.pluginManager.plugins.find( 77 | (plugin) => plugin.constructor.name === "AwsInvokeLocal" 78 | ); 79 | if (AwsInvokeLocal) { 80 | const loadEnvVarsOriginal = AwsInvokeLocal.loadEnvVars.bind(AwsInvokeLocal); 81 | AwsInvokeLocal.loadEnvVars = () => { 82 | return this.initOfflineHook().then(() => loadEnvVarsOriginal()); 83 | }; 84 | } 85 | } 86 | 87 | _loadConfig() { 88 | const params = _.get(this.serverless, "service.custom.export-env"); 89 | this.filename = _.get(params, "filename", this.filename); 90 | this.enableOffline = _.get(params, "enableOffline", this.enableOffline); 91 | this.overwrite = _.get(params, "overwrite", this.overwrite); 92 | this.refMap = _.get(params, "refMap", this.refMap); 93 | this.getAttMap = _.get(params, "getAttMap", this.getAttMap); 94 | this.importValueMap = _.get(params, "importValueMap", this.importValueMap); 95 | 96 | // `true` if the plugin should run, `false` otherwise 97 | this.isEnabled = !this.isOfflineHooked || this.enableOffline; 98 | } 99 | 100 | initOfflineHook() { 101 | if (!this.isOfflineHooked) { 102 | this.isOfflineHooked = true; 103 | return this.serverless.pluginManager.run(["export-env"]); 104 | } 105 | return BbPromise.resolve(); 106 | } 107 | 108 | collectEnvVars() { 109 | return BbPromise.try(() => { 110 | this._loadConfig(); 111 | if (!this.isEnabled) { 112 | return BbPromise.resolve(); 113 | } 114 | 115 | // collect environment variables 116 | this.globalEnvironmentVariables = this.serverless.service.provider.environment || {}; 117 | this.functionEnvironmentVariables = collectFunctionEnvVariables(this.serverless); 118 | 119 | return BbPromise.resolve(); 120 | }); 121 | } 122 | 123 | resolveEnvVars() { 124 | return BbPromise.try(() => { 125 | this._loadConfig(); 126 | if (!this.isEnabled) { 127 | return BbPromise.resolve(); 128 | } 129 | 130 | const sls = this.serverless; 131 | const AWS = this.serverless.providers.aws; 132 | const globalEnv = this.globalEnvironmentVariables; 133 | const maps = { 134 | refMap: this.refMap, 135 | getAttMap: this.getAttMap, 136 | importValueMap: this.importValueMap, 137 | }; 138 | return BbPromise.all([describeStack(AWS), listStackResources(AWS), listExports(AWS)]).then( 139 | ([stack, resources, exports]) => { 140 | // Resolve global and function environment variables 141 | return BbPromise.all([ 142 | resolveCloudFormationEnvVariables(sls, globalEnv, stack, resources, exports, maps).then( 143 | (resolved) => (this.globalEnvironmentVariables = resolved) 144 | ), 145 | BbPromise.all( 146 | _.map(this.functionEnvironmentVariables, (funcEnv, funcName) => 147 | resolveCloudFormationEnvVariables(sls, funcEnv, stack, resources, exports, maps).then( 148 | (resolved) => (this.functionEnvironmentVariables[funcName] = resolved) 149 | ) 150 | ) 151 | ), 152 | ]).return(); 153 | } 154 | ); 155 | }); 156 | } 157 | 158 | setEnvVars() { 159 | return BbPromise.try(() => { 160 | this._loadConfig(); 161 | if (!this.isEnabled || !this.isOfflineHooked) { 162 | // This code does only run when offline but not when executed via `export-env` 163 | return BbPromise.resolve(); 164 | } 165 | 166 | // If this is a local lambda invoke, replace the service environment with the resolved one 167 | process.env.SLS_DEBUG && this.serverless.cli.log(`Updating serverless environment variable(s)`, "export-env"); 168 | this.serverless.service.provider.environment = this.globalEnvironmentVariables; 169 | if (_.has(this.serverless, "service.functions")) { 170 | _.forEach( 171 | this.serverless.service.functions, 172 | (func, key) => (func.environment = this.functionEnvironmentVariables[key]) 173 | ); 174 | } 175 | }); 176 | } 177 | 178 | writeEnvVars() { 179 | return BbPromise.try(() => { 180 | this._loadConfig(); 181 | if (!this.isEnabled || this.isOfflineHooked) { 182 | // This code does not run when offline but only when executed via `export-env` 183 | return BbPromise.resolve(); 184 | } 185 | 186 | const envFilePath = path.resolve(this.serverless.config.servicePath, this.filename); 187 | if (!fs.existsSync(envFilePath) || this.overwrite) { 188 | process.env.SLS_DEBUG && this.serverless.cli.log(`Writing ${this.filename} file`, "export-env"); 189 | 190 | const envVars = _.clone(this.globalEnvironmentVariables); 191 | if (this.options.all) { 192 | _.forEach(this.functionEnvironmentVariables, (vars) => _.assign(envVars, vars)); 193 | } else if (this.options.function) { 194 | _.assign(envVars, this.functionEnvironmentVariables[this.options.function]); 195 | } 196 | const envDocument = transformEnvVarsToString(envVars); 197 | 198 | fs.writeFileSync(envFilePath, envDocument); 199 | } else { 200 | process.env.SLS_DEBUG && 201 | this.serverless.cli.log(`${this.filename} already exists. Leaving it untouched.`, "export-env"); 202 | } 203 | }); 204 | } 205 | } 206 | 207 | module.exports = ExportEnv; 208 | -------------------------------------------------------------------------------- /src/lib/aws-helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"); 4 | 5 | function listExports(AWS, exports, nextToken) { 6 | exports = exports || []; 7 | return BbPromise.resolve(AWS.request("CloudFormation", "listExports", { NextToken: nextToken })) 8 | .then((response) => { 9 | exports.push.apply(exports, response.Exports); 10 | if (response.NextToken) { 11 | // Query next page 12 | return listExports(AWS, exports, response.NextToken); 13 | } 14 | }) 15 | .then(() => exports); 16 | } 17 | 18 | function listStackResources(AWS, resources, nextToken) { 19 | resources = resources || []; 20 | return BbPromise.resolve( 21 | AWS.request("CloudFormation", "listStackResources", { 22 | StackName: AWS.naming.getStackName(), 23 | NextToken: nextToken, 24 | }) 25 | ) 26 | .then((response) => { 27 | resources.push.apply(resources, response.StackResourceSummaries); 28 | if (response.NextToken) { 29 | // Query next page 30 | return listStackResources(AWS, resources, response.NextToken); 31 | } 32 | }) 33 | .then(() => resources); 34 | } 35 | 36 | function describeStack(AWS) { 37 | return BbPromise.resolve( 38 | AWS.request("CloudFormation", "describeStacks", { 39 | StackName: AWS.naming.getStackName(), 40 | }) 41 | ).then((response) => { 42 | return response.Stacks[0]; 43 | }); 44 | } 45 | 46 | module.exports = { 47 | listExports, 48 | listStackResources, 49 | describeStack, 50 | }; 51 | -------------------------------------------------------------------------------- /src/lib/collectFunctionEnvVariables.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | 5 | /** 6 | * Collects Serverless function specific environment variables 7 | * 8 | * @param {Serverless} serverless - Serverless Instance 9 | * @returns {String[]} Returns a list of environment variables 10 | */ 11 | function collectFunctionEnvVariables(serverless) { 12 | const functions = _.get(serverless, "service.functions", {}); 13 | const envVars = _.mapValues(functions, (func) => func.environment); 14 | return envVars; 15 | } 16 | 17 | module.exports = collectFunctionEnvVariables; 18 | -------------------------------------------------------------------------------- /src/lib/resolveCloudFormationEnvVariables.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BbPromise = require("bluebird"), 4 | _ = require("lodash"), 5 | NodeEvaluator = require("cfn-resolver-lib"); 6 | 7 | function resolveGetAtt(refs, resource) { 8 | // TODO: While this code was created in good intention (isn't it all?), it doesn't work in the current form. 9 | // There's no AWS API that can help resolve !GetAtt automatically and some attributes are impossible to 10 | // determine without retrieving additional details of the resource, e.g. using an additional API call. 11 | // So, for now, we completely disable this variable resolution mechanism and rely of hardcoding the `getAttMap` 12 | // in the config instead. 13 | // Please note that the code below doesn't work properly. 14 | 15 | // const Partition = refs["AWS::Partition"]; 16 | // const Region = refs["AWS::Region"]; 17 | // const AccountId = refs["AWS::AccountId"]; 18 | // switch (resource.ResourceType) { 19 | // case "AWS::Lambda::Function": 20 | // return { 21 | // Arn: `arn:${Partition}:lambda:${Region}:${AccountId}:function:${resource.PhysicalResourceId}`, 22 | // FunctionName: resource.PhysicalResourceId, 23 | // }; 24 | // case "AWS::SNS::Topic": 25 | // return { TopicName: _.last(_.split(resource.PhysicalResourceId, ":")) }; 26 | // case "AWS::SQS::Queue": 27 | // return { QueueName: _.last(_.split(resource.PhysicalResourceId, ":")) }; 28 | // case "AWS::CloudWatch::Alarm": 29 | // return { AlarmName: _.last(_.split(resource.PhysicalResourceId, ":")) }; 30 | // case "AWS::EC2::Subnet": 31 | // return { SubnetId: _.last(_.split(resource.PhysicalResourceId, ":")) }; 32 | // case "AWS::EC2::VPC": 33 | // return { VpcId: _.last(_.split(resource.PhysicalResourceId, ":")) }; 34 | // case "AWS::S3::Bucket": 35 | // return { BucketName: _.last(_.split(resource.PhysicalResourceId, ":")) }; 36 | // case "AWS::EC2::SecurityGroup": 37 | // return { SecurityGroupId: _.last(_.split(resource.PhysicalResourceId, ":")) }; 38 | // case "AWS::DynamoDB::Table": 39 | // return { TableName: _.last(_.split(resource.PhysicalResourceId, ":")) }; 40 | // case "AWS::IAM::Role": 41 | // return { Arn: `arn:${Partition}:iam::${AccountId}:role/${resource.PhysicalResourceId}` }; 42 | // case "AWS::ApiGateway::RestApi": 43 | // return { RootResourceId: resource.PhysicalResourceId }; 44 | // } 45 | 46 | return resource; 47 | } 48 | 49 | function resolveResources(AWS, stack, resources, exports, toBeResolved, maps) { 50 | return BbPromise.all([AWS.getRegion(), AWS.getAccountId()]).spread((region, accountId) => { 51 | const node = _.assign({}, { toBeResolved, Parameters: [] }); 52 | const refResolvers = _.merge( 53 | { 54 | "AWS::Partition": "aws", // FIXME How to determine correct value? 55 | "AWS::Region": region, 56 | "AWS::AccountId": accountId, 57 | "AWS::StackId": stack.StackId, 58 | "AWS::StackName": stack.StackName, 59 | }, 60 | _.reduce( 61 | resources, 62 | (values, resource) => { 63 | values[resource.LogicalResourceId] = resource.PhysicalResourceId; 64 | return values; 65 | }, 66 | {} 67 | ), 68 | maps.refMap 69 | ); 70 | const getAttResolvers = _.merge( 71 | _.reduce( 72 | resources, 73 | (values, resource) => { 74 | values[resource.LogicalResourceId] = resolveGetAtt(refResolvers, resource); 75 | return values; 76 | }, 77 | {} 78 | ), 79 | maps.getAttMap 80 | ); 81 | const importValueResolvers = _.merge( 82 | _.reduce( 83 | exports, 84 | (values, resource) => { 85 | values[resource.Name] = resource.Value; 86 | return values; 87 | }, 88 | {} 89 | ), 90 | maps.importValueMap 91 | ); 92 | // Pass all resources to allow Fn::GetAtt and Conditions resolution 93 | const evaluator = new NodeEvaluator(node, { 94 | RefResolvers: refResolvers, 95 | "Fn::GetAttResolvers": getAttResolvers, 96 | "Fn::ImportValueResolvers": importValueResolvers, 97 | }); 98 | const result = evaluator.evaluateNodes(); 99 | if (result && result.toBeResolved) { 100 | return result.toBeResolved; 101 | } 102 | 103 | return {}; 104 | }); 105 | } 106 | 107 | /** 108 | * Resolves CloudFormation references and import variables 109 | * 110 | * @param {Serverless} serverless - Serverless Instance 111 | * @param {Object[]} envVars - Environment Variables to resolve 112 | * @returns {Promise} Resolves with the list of resolves environment variables 113 | */ 114 | function resolveCloudFormationEnvVars(serverless, envVars, stack, resources, exports, maps) { 115 | const AWS = serverless.providers.aws; 116 | return resolveResources(AWS, stack, resources, exports, envVars, maps).then((resolved) => { 117 | process.env.SLS_DEBUG && 118 | _.map(resolved, (value, key) => 119 | serverless.cli.log(`Resolved environment variable ${key}: ${JSON.stringify(value)}`, "export-env") 120 | ); 121 | return BbPromise.resolve(resolved); 122 | }); 123 | } 124 | 125 | module.exports = resolveCloudFormationEnvVars; 126 | -------------------------------------------------------------------------------- /src/lib/transformEnvVarsToString.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const os = require("os"); 5 | 6 | /** 7 | * Copies environment variables into a string ready for wrtiting to a file 8 | * 9 | * @param {String[]} envVars - Environment Variables 10 | * @returns {String} 11 | */ 12 | function transformEnvVarsToString(envVars) { 13 | const output = _.map(envVars, (value, key) => { 14 | return `${key}=${value}`; 15 | }); 16 | 17 | return output.join(os.EOL); 18 | } 19 | 20 | module.exports = transformEnvVarsToString; 21 | --------------------------------------------------------------------------------