├── .nvmrc ├── .gitignore ├── .travis.yml ├── .eslintrc.js ├── LICENSE.md ├── package.json ├── README.md ├── index.js └── test └── plugin-test.js /.nvmrc: -------------------------------------------------------------------------------- 1 | serverless -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | /coverage 4 | /npm-debug.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4.3.2 4 | sudo: false 5 | install: 6 | - npm install 7 | script: 8 | - npm run lint 9 | - istanbul cover node_modules/.bin/_mocha test/plugin-test.js && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb/base', 3 | rules: { 4 | strict: 0, /* remove after Chrome supports strict mode in modules OR Babel is integrated */ 5 | 'no-unused-vars': [2, { args: 'after-used', argsIgnorePattern: '^_' }], 6 | 'no-param-reassign': [2, { props: false }], 7 | 'comma-dangle': ['error', 'only-multiline'], 8 | }, 9 | globals: {}, 10 | env: { 11 | node: true, 12 | mocha: true, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jason 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-resources-env", 3 | "version": "0.3.1", 4 | "description": "Serverlss framework plugin, which after a deploy, fetches cloudformation resource identifiers and sets them on AWS lambdas, and creates local .-env file", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "istanbul cover node_modules/.bin/_mocha test/plugin-test.js", 8 | "lint": "eslint .eslintrc.js **/*.js *.js --ignore-pattern '!.eslintrc.js'" 9 | }, 10 | "author": "Jason Chambers", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rurri/serverless-resources-env.git" 15 | }, 16 | "dependencies": { 17 | "bluebird": "^3.4.6", 18 | "dotenv": "^4.0.0", 19 | "lodash": "^4.17.1" 20 | }, 21 | "devDependencies": { 22 | "chai": "^3.5.0", 23 | "circular-json": "^0.3.1", 24 | "coveralls": "^2.11.15", 25 | "eslint": "^3.10.1", 26 | "eslint-config-airbnb": "^13.0.0", 27 | "eslint-plugin-import": "^2.2.0", 28 | "istanbul": "^0.4.5", 29 | "mocha": "^3.1.2", 30 | "properties-parser": "^0.3.1", 31 | "sinon": "^1.17.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/rurri/serverless-resources-env.svg?branch=master)](https://travis-ci.org/rurri/serverless-resources-env) 2 | [![Coverage Status](https://coveralls.io/repos/github/rurri/serverless-resources-env/badge.svg?branch=master)](https://coveralls.io/github/rurri/serverless-resources-env?branch=master) 3 | [![bitHound Overall Score](https://www.bithound.io/github/rurri/serverless-resources-env/badges/score.svg)](https://www.bithound.io/github/rurri/serverless-resources-env) 4 | 5 | A serverless framework plugin so that your functions know how to use resources created by cloudformation. 6 | 7 | In short, whether you are running your function as a lambda, or locally on your machine, 8 | the physical name or ARN of each resource that was part of your CloudFormation template will be available as an environment 9 | variable keyed to its logical name prefixed with `CF_`. 10 | 11 | For lambdas running on AWS, this plugin will set environment variables on your functions within lambda using the [Lambda environment variable support](https://aws.amazon.com/about-aws/whats-new/2016/11/aws-lambda-supports-environment-variables/). 12 | 13 | For running functions locally, it will also create a local env file for use in reading in environment variables for a specific region-stage-function while running functions locally. These are by default stored in a directory named: `.serverless-resources-env` in files named `.__`. Ex: `./.serverless-resources-env/.us-east-1_dev_hello`. 14 | These environment variables are set automatically by the plugin when running `serverless invoke local -f ...`. 15 | 16 | **Breaking Changes in 0.3.0:** *See below* 17 | 18 | ## Why? 19 | 20 | You have a CloudFormation template all set, and you are writing your functions. Now you are ready to use the 21 | resources created as part of your CF template. Well, you need to know about them! You could deploy and then try and manage 22 | configuration for these resources, or you can use this module which will automatically set environmet variables that map the 23 | logical resource name to the physical resource name for resources within the CloudFormation file. 24 | 25 | ## Example: 26 | 27 | You have defined resources in your serverless.yml called `mySQS` and `myTable`, and you want to actually use these in 28 | your function so you need their ARN or the actual table name that was created. 29 | 30 | ``` 31 | const sqs_arn = process.env.CF_mySQS; 32 | const my_dynamo_table_name = process.env.CF_myTable; 33 | ``` 34 | 35 | ## How it works 36 | This plugin attaches to the deploy post-deploy hook. After the stack is deployed to AWS, the plugin determines the name of the cloud formation stack, and queries AWS for all resources in this stack. 37 | 38 | After deployment, this plugin, will fetch all the CF resources for the current stack (stage i.e. 'dev'). It will then use the AWS 39 | SDK to set as environment variables the physical id's of each resource as an environment variable prefixed with `CF_`. 40 | 41 | It will also create a file with these values in a .properties file format named `./serverless-resources-env/.__`. 42 | These are then pulled in during a local invocation (`serverless invoke local -f...`) Each region, stage, and function will get its own file. 43 | When invoking locally the module will automatically select the correct .env information based on which region and stage is set. 44 | 45 | This means no code changes, or config changes no matter how many regions, and stages you deploy to. 46 | 47 | The lambdas always know exactly where to find their resources, whether that resource is a DynamoDB, SQS, SNS, or anything else. 48 | 49 | ## Install / Setup 50 | 51 | `npm install serverless-resources-env --save` 52 | 53 | Add the plugin to the serverless.yml. 54 | 55 | ``` 56 | plugins: 57 | - serverless-resources-env 58 | ``` 59 | 60 | Set your resources as normal: 61 | 62 | ``` 63 | resources: 64 | Resources: 65 | testTopic1: 66 | Type: AWS::SNS::Topic 67 | testTopic2: 68 | Type: AWS::SNS::Topic 69 | 70 | ``` 71 | 72 | Set which resources you want exported on each function. 73 | 74 | ``` 75 | functions: 76 | hello: 77 | handler: handler.hello 78 | custom: 79 | env-resources: 80 | - testTopic1 81 | - testTopic2 82 | ``` 83 | 84 | ## Breaking Changes since 0.2.0 85 | 86 | At version 0.2.0 and before, all resources were exported to both the local .env file and to each function automatically. 87 | 88 | This caused issues with AWS limits on the amount of information that could be exported as env variables onto lambdas deployed within AWS. This also exposed resources 89 | as env variables that were not needed by functions, as it was setting *all* resources, not just the ones the function needed. 90 | 91 | Starting at version 0.3.0 a list of which resources are to be exported to each function are required to be a part of the 92 | function definition in the .yml file, if the function needs any of these environment variables. (See current install instructions above) 93 | 94 | This also means that specific env files are needed per region / stage / function. This can potentially be a lot of files 95 | and therefore these files were also moved to a sub-folder. `.serverless-resources-env` by default. 96 | 97 | ## Common Errors 98 | 99 | `Unexpected key 'Environment' found in params`. Your aws-sdk is out of date. Setting environment variables on lambdas is new. See the Important note above. 100 | 101 | You may need to upgrade the version of the package `aws-sdk` being used by the serverless framework. 102 | 103 | In the 1.1.0 serverless framework, the `aws-sdk` is pegged at version 2.6.8 in the `npm-shrinkwrap.json` of serverless. 104 | 105 | If you have installed serverless locally as part of your project you can just upgrade the sdk. `npm upgrade aws-sdk`. 106 | 107 | If you have installed serverless globally, you will need to change to the serverless directory and run `npm upgrade aws-sdk` from there. 108 | 109 | The following commands should get it done: 110 | 111 | ``` 112 | cd `npm list serverless -g | head -n 1`/node_modules/serverless 113 | npm upgrade aws-sdk 114 | ``` 115 | 116 | 117 | ## Config 118 | 119 | By default, the mapping is written to a .env file located at `./.serverless-resources-env/.__env`. 120 | This can be overridden by setting an option in serverless.yml. 121 | 122 | ``` 123 | custom: 124 | resource-output-dir: .alt-resource-dir 125 | ``` 126 | 127 | ``` 128 | functions: 129 | hello: 130 | custom: 131 | resource-output-file: .alt-file-name 132 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | const _ = require('lodash'); 5 | const fs = require('fs'); 6 | const dotenv = require('dotenv'); 7 | 8 | class ServerlessResourcesEnv { 9 | 10 | /** 11 | * Constructor. Object is created by serverless framework. 12 | * 13 | * @param serverless context provided by framework 14 | * @param options command line options provided by framework 15 | */ 16 | constructor(serverless, options) { 17 | // Mark this plug-in as only usable with aws 18 | this.provider = 'aws'; 19 | 20 | // Define our hooks. We only care about modifying things when a full deploy is run as 21 | // only a function deploy will not modify any CF resources 22 | this.hooks = { 23 | 'after:deploy:deploy': this.afterDeploy.bind(this), 24 | 'before:invoke:local:invoke': this.beforeLocalInvoke.bind(this), 25 | }; 26 | 27 | // Stash the context away for later 28 | this.serverless = serverless; 29 | this.options = options; 30 | 31 | const awsProvider = this.serverless.getProvider('aws'); 32 | 33 | // The AWS Region is not set for us yet on the provider 34 | const region = this.getRegion(); 35 | 36 | // Set these on our object for easier injection by unit tests 37 | this.cloudFormation = new awsProvider.sdk.CloudFormation({ region }); 38 | this.lambda = new awsProvider.sdk.Lambda({ region }); 39 | this.fs = fs; 40 | this.dotenv = dotenv; 41 | } 42 | 43 | /** 44 | * Called by the serverless framework. Will return a promise of completion 45 | * @returns {Promise.} 46 | */ 47 | afterDeploy() { 48 | const stackName = this.getStackName(); 49 | 50 | // First fetch all of our Resources from AWS by doing a network call 51 | return this.fetchCFResources().then((resourceResult) => { 52 | // Map these to an object keyed by the Logical id pointing to the PhysicalId 53 | const resources = _.reduce(resourceResult.StackResources, (all, item) => { 54 | all[`CF_${item.LogicalResourceId}`] = item.PhysicalResourceId; 55 | return all; 56 | }, {}); 57 | 58 | // For each function, update the env files on that function. 59 | const updatePromises = _.map(_.keys(this.serverless.service.functions), (functionName) => { 60 | const awsFunctionName = `${stackName}-${functionName}`; 61 | const resourceList = _.map( 62 | this.serverless.service.functions[functionName].custom && 63 | this.serverless.service.functions[functionName].custom['env-resources'], 64 | resource => `CF_${resource}`); 65 | 66 | const thisFunctionsResources = _.pick(resources, resourceList); 67 | const notFoundList = _.difference(resourceList, _.keys(thisFunctionsResources)); 68 | const thisFunctionEnv = _.extend( 69 | {}, 70 | thisFunctionsResources, 71 | this.serverless.service.provider.environment || {}, 72 | this.serverless.service.functions[functionName].environment); 73 | 74 | if (notFoundList.length > 0) { 75 | this.serverless.cli.log( 76 | `[serverless-resources-env] WARNING: Could not find cloud formation resources for ${functionName}.` + 77 | `Could not find: ${_.join(notFoundList)}`); 78 | } 79 | if (_.keys(thisFunctionsResources).length === 0) { 80 | this.serverless.cli.log( 81 | `[serverless-resources-env] No env resources configured for ${functionName}. Clearing env vars`); 82 | } else { 83 | this.serverless.cli.log( 84 | `[serverless-resources-env] Setting env vars for ${functionName}. ${_.join(thisFunctionsResources)}`); 85 | } 86 | // Send a lambda update request to 87 | const awsUpdateResult = 88 | this.updateFunctionEnv(awsFunctionName, thisFunctionEnv).then((result) => { 89 | this.serverless.cli.log( 90 | `[serverless-resources-env] ENV Update for function ${result.FunctionName} successful`); 91 | return result; 92 | }); 93 | const createFileResult = this.createCFFile(functionName, thisFunctionsResources); 94 | return Promise.all([ 95 | awsUpdateResult, 96 | createFileResult, 97 | ]); 98 | }); 99 | // Return a promise that resolves once everything is done. 100 | return Promise.all(updatePromises); 101 | }); 102 | } 103 | 104 | beforeLocalInvoke() { 105 | const fileName = this.getEnvFileName(this.options.function); 106 | const path = this.getEnvDirectory(); 107 | const fullPath = `${path}/${fileName}`; 108 | this.serverless.cli.log(`[serverless-resources-env] Pulling in env variables from ${fullPath}`); 109 | dotenv.config({ path: fullPath }); 110 | } 111 | 112 | /** 113 | * Updates the environment variables for a single function. 114 | * @param functionName Name of function to update 115 | * @param envVars Environment vars to set on the function 116 | * @returns {Promise.} 117 | */ 118 | updateFunctionEnv(functionName, envVars) { 119 | const params = { 120 | FunctionName: functionName, /* required */ 121 | Environment: { 122 | Variables: envVars, 123 | }, 124 | }; 125 | return Promise.promisify(this.lambda.updateFunctionConfiguration.bind(this.lambda))(params); 126 | } 127 | 128 | /** 129 | * Looks up the CF Resources for this stack from AWS 130 | * @returns {Promise.} 131 | */ 132 | fetchCFResources() { 133 | const stackName = this.getStackName(); 134 | this.serverless.cli.log(`[serverless-resources-env] Looking up resources for CF Named: ${stackName}`); 135 | return this.fetchCFResourcesPages(stackName, null, []); 136 | } 137 | 138 | /** 139 | * Recursively look up the CF Resource pages for this stack from AWS 140 | * and concatenate the resource pages 141 | * @returns {Promise.} 142 | */ 143 | fetchCFResourcesPages(stackName, nextToken, resourceSummaries) { 144 | const self = this; 145 | return new Promise((resolve, reject) => { 146 | self.cloudFormation.listStackResources( 147 | { StackName: stackName, NextToken: nextToken }, 148 | (err, resourceResultPage) => { 149 | if (err == null) { 150 | if (resourceResultPage.NextToken == null) { 151 | const results = resourceSummaries.concat(resourceResultPage.StackResourceSummaries); 152 | self.serverless.cli.log(`[serverless-resources-env] Returned ${results.length} ResourceSummaries`); 153 | resolve({ StackResources: results }); 154 | } else { 155 | self.serverless.cli.log('[serverless-resources-env] Getting next Resources page'); 156 | const allSummaries = 157 | resourceSummaries.concat(resourceResultPage.StackResourceSummaries); 158 | resolve(self.fetchCFResourcesPages( 159 | stackName, 160 | resourceResultPage.NextToken, 161 | allSummaries)); 162 | } 163 | } else { 164 | reject(err); 165 | } 166 | } 167 | ); 168 | }); 169 | } 170 | 171 | getEnvDirectory() { 172 | const customDirectory = this.serverless.service.custom && this.serverless.service.custom['resource-output-dir']; 173 | const directory = customDirectory || '.serverless-resources-env'; 174 | return `${this.serverless.config.servicePath}/${directory}`; 175 | } 176 | 177 | getEnvFileName(functionName) { 178 | const stage = this.getStage(); 179 | const region = this.getRegion(); 180 | const customName = this.serverless.service.functions[functionName].custom && 181 | this.serverless.service.functions[functionName].custom['resource-output-file']; 182 | // Check if the filename is overridden, otherwise use ._- 183 | return customName || `.${region}_${stage}_${functionName}`; 184 | } 185 | 186 | /** 187 | * Creates a local file of all the CF resources for this stack in a .properties format 188 | * @param resources 189 | * @returns {Promise} 190 | */ 191 | createCFFile(functionName, resources) { 192 | // Check if the filename is overridden, otherwise use /-env 193 | const path = this.getEnvDirectory(); 194 | const fileName = this.getEnvFileName(functionName); 195 | 196 | if (!this.fs.existsSync(path)) { 197 | this.fs.mkdirSync(path, 0o700); 198 | } 199 | 200 | if (!this.fs.statSync(path).isDirectory()) { 201 | throw new Error(`Expected ${path} to be a directory`); 202 | } 203 | 204 | // Log so that the user knows where this file is 205 | this.serverless.cli.log(`[serverless-resources-env] Writing ${_.keys(resources).length}` + 206 | ` CF resources to ${fileName}`); 207 | 208 | const fullFileName = `${path}/${fileName}`; 209 | // Reduce this to a simple properties file format 210 | const data = _.reduce(resources, (properties, item, key) => 211 | `${properties}${key}=${item}\n`, ''); 212 | // Return a promise of this file being written 213 | return Promise.promisify(this.fs.writeFile)(fullFileName, data); 214 | } 215 | 216 | /** 217 | * Checks CLI options and settings to discover the current stage that is being worked on 218 | * @returns {string} 219 | */ 220 | getStage() { 221 | let returnValue = 'dev'; 222 | if (this.options && this.options.stage) { 223 | returnValue = this.options.stage; 224 | } else if (this.serverless.config.stage) { 225 | returnValue = this.serverless.config.stage; 226 | } else if (this.serverless.service.provider.stage) { 227 | returnValue = this.serverless.service.provider.stage; 228 | } 229 | return returnValue; 230 | } 231 | 232 | /** 233 | * Checks CLI options and settings to discover the current region that is being worked on 234 | * @returns {string} 235 | */ 236 | getRegion() { 237 | let returnValue = 'us-east-1'; 238 | if (this.options && this.options.region) { 239 | returnValue = this.options.region; 240 | } else if (this.serverless.config.region) { 241 | returnValue = this.serverless.config.region; 242 | } else if (this.serverless.service.provider.region) { 243 | returnValue = this.serverless.service.provider.region; 244 | } 245 | return returnValue; 246 | } 247 | 248 | /** 249 | * Returns the name of the current Stack. 250 | * @returns {string} 251 | */ 252 | getStackName() { 253 | return `${this.serverless.service.service}-${this.getStage()}`; 254 | } 255 | } 256 | 257 | module.exports = ServerlessResourcesEnv; 258 | -------------------------------------------------------------------------------- /test/plugin-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const sinon = require('sinon'); 5 | 6 | const ServerlessFetchStackResources = require('../index'); 7 | const _ = require('lodash'); 8 | const parse = require('properties-parser').parse; 9 | 10 | const serverlessStub = { 11 | config: { 12 | servicePath: '.', 13 | }, 14 | service: { 15 | service: 'unit-test-service', 16 | provider: { 17 | environment: { 18 | ooga: 'booga', 19 | }, 20 | }, 21 | functions: { 22 | function1: {}, 23 | function2: {}, 24 | }, 25 | }, 26 | cli: { 27 | log: sinon.stub(), 28 | }, 29 | getProvider: () => ({ 30 | sdk: { 31 | CloudFormation: Object, 32 | Lambda: Object, 33 | }, 34 | }), 35 | }; 36 | 37 | const providerEnvironment = serverlessStub.service.provider.environment; 38 | 39 | describe('serverless-fetch-stack-resource', () => { 40 | describe('constructor', () => { 41 | it('should setup to listen for hooks', () => { 42 | const instance = new ServerlessFetchStackResources(serverlessStub, {}); 43 | expect(instance.hooks).to.have.keys('after:deploy:deploy', 'before:invoke:local:invoke'); 44 | 45 | expect(instance.provider).to.equal('aws'); 46 | expect(instance.serverless).to.equal(serverlessStub); 47 | }); 48 | }); 49 | 50 | describe('getStage', () => { 51 | it('uses dev if no options set', () => { 52 | const instance = new ServerlessFetchStackResources( 53 | serverlessStub, {}); 54 | expect(instance.getStage()).to.equal('dev'); 55 | }); 56 | 57 | it('uses stage of option if set', () => { 58 | const instance = new ServerlessFetchStackResources( 59 | serverlessStub, { stage: 'from_option' }); 60 | expect(instance.getStage()).to.equal('from_option'); 61 | }); 62 | 63 | it('uses stage of config if set', () => { 64 | const instance = new ServerlessFetchStackResources( 65 | _.extend({}, serverlessStub, { 66 | config: { stage: 'from_config' }, 67 | service: { provider: {} }, 68 | }), {}); 69 | expect(instance.getStage()).to.equal('from_config'); 70 | }); 71 | 72 | it('uses stage of config if set', () => { 73 | const instance = new ServerlessFetchStackResources( 74 | _.extend({}, serverlessStub, { service: { provider: { stage: 'from_provider' } } }), {}); 75 | expect(instance.getStage()).to.equal('from_provider'); 76 | }); 77 | 78 | it('options will preempt other stages set', () => { 79 | const instance = new ServerlessFetchStackResources( 80 | _.extend({}, serverlessStub, { 81 | config: { stage: 'from_config' }, 82 | service: { provider: { stage: 'from_provider' } }, 83 | }), { stage: 'from_option' }); 84 | expect(instance.getStage()).to.equal('from_option'); 85 | }); 86 | }); 87 | 88 | describe('getRegion', () => { 89 | it('uses us-east-1 if no options set', () => { 90 | const instance = new ServerlessFetchStackResources( 91 | serverlessStub, {}); 92 | expect(instance.getRegion()).to.equal('us-east-1'); 93 | }); 94 | 95 | it('uses region of option if set', () => { 96 | const instance = new ServerlessFetchStackResources( 97 | serverlessStub, { region: 'from_option' }); 98 | expect(instance.getRegion()).to.equal('from_option'); 99 | }); 100 | 101 | it('uses region of config if set', () => { 102 | const instance = new ServerlessFetchStackResources( 103 | _.extend({}, serverlessStub, { 104 | config: { region: 'from_config' }, 105 | service: { provider: {} }, 106 | }), {}); 107 | expect(instance.getRegion()).to.equal('from_config'); 108 | }); 109 | 110 | it('uses region of config provider if set', () => { 111 | const instance = new ServerlessFetchStackResources( 112 | _.extend({}, serverlessStub, { service: { provider: { region: 'from_provider' } } }), {}); 113 | expect(instance.getRegion()).to.equal('from_provider'); 114 | }); 115 | }); 116 | 117 | describe('getStackName', () => { 118 | it('simple combination of service and stage', () => { 119 | const instance = new ServerlessFetchStackResources( 120 | _.extend({}, serverlessStub, { 121 | config: {}, 122 | service: { service: 'a_service', provider: {} }, 123 | }), { stage: 'from_option' }); 124 | expect(instance.getStackName()).to.equal('a_service-from_option'); 125 | }); 126 | }); 127 | 128 | describe('fetchCFResources', () => { 129 | it('Will use sdk to fetch resource informaiton from AWS', () => { 130 | const instance = new ServerlessFetchStackResources(_.extend({}, serverlessStub)); 131 | 132 | const resources = [ 133 | { LogicalResourceId: 'a', PhysicalResourceId: '1' }, 134 | { LogicalResourceId: 'b', PhysicalResourceId: '2' }, 135 | { LogicalResourceId: 'c', PhysicalResourceId: '3' }, 136 | ]; 137 | instance.cloudFormation.listStackResources = 138 | (params, callback) => { 139 | callback(null, { 140 | StackResourceSummaries: resources, 141 | }); 142 | }; 143 | 144 | return instance.fetchCFResources().then((result) => { 145 | expect(result.StackResources).to.deep.equal(resources); 146 | return true; 147 | }); 148 | }); 149 | }); 150 | 151 | describe('createCFFile', () => { 152 | it('Will create a file with the given set of resources with default filename', (done) => { 153 | const resources = { a: '1', b: '2', c: '3' }; 154 | const instance = new ServerlessFetchStackResources(_.extend({}, serverlessStub)); 155 | instance.fs = _.cloneDeep(instance.fs); 156 | instance.fs.writeFile = (fileName, data) => { 157 | expect(parse(data)).to.deep.equal(resources); 158 | expect(fileName).to.equal('./.serverless-resources-env/.us-east-1_dev_function1'); 159 | done(); 160 | }; 161 | instance.createCFFile('function1', resources); 162 | }); 163 | 164 | it('Will use config filename if it exists', (done) => { 165 | const resources = { a: '1', b: '2', c: '3' }; 166 | const instance = new ServerlessFetchStackResources(_.extend({}, serverlessStub)); 167 | instance.serverless.service.functions.function1.custom = { 'resource-output-file': 'customName' }; 168 | instance.fs = _.cloneDeep(instance.fs); 169 | instance.fs.writeFile = (fileName, data) => { 170 | expect(parse(data)).to.deep.equal(resources); 171 | expect(fileName).to.equal('./.serverless-resources-env/customName'); 172 | done(); 173 | }; 174 | instance.createCFFile('function1', resources); 175 | }); 176 | 177 | it('Will create the directory if it does not exist', (done) => { 178 | const resources = { a: '1', b: '2', c: '3' }; 179 | const instance = new ServerlessFetchStackResources( 180 | _.cloneDeep(serverlessStub)); 181 | instance.serverless.service.custom = { 'resource-output-dir': 'custom-dir' }; 182 | instance.serverless.service.functions.function1.custom = { 'resource-output-file': 'customName' }; 183 | 184 | instance.fs = _.cloneDeep(instance.fs); 185 | instance.fs.mkdirSync = sinon.stub(); 186 | instance.fs.statSync = () => ({ isDirectory: () => true }); 187 | instance.fs.writeFile = (fileName, data) => { 188 | expect(parse(data)).to.deep.equal(resources); 189 | expect(fileName).to.equal('./custom-dir/customName'); 190 | sinon.assert.calledWith(instance.fs.mkdirSync, './custom-dir', 0o700); 191 | done(); 192 | }; 193 | instance.createCFFile('function1', resources); 194 | }); 195 | 196 | it('Will error if the directory given exists but is not a directory', (done) => { 197 | const resources = { a: '1', b: '2', c: '3' }; 198 | const instance = new ServerlessFetchStackResources( 199 | _.cloneDeep(serverlessStub)); 200 | instance.serverless.service.custom = { 'resource-output-dir': 'custom-dir' }; 201 | instance.serverless.service.functions.function1.custom = { 'resource-output-file': 'customName' }; 202 | 203 | instance.fs = _.cloneDeep(instance.fs); 204 | instance.fs.mkdirSync = sinon.stub(); 205 | instance.fs.statSync = () => ({ isDirectory: () => false }); 206 | instance.fs.existsSync = () => true; 207 | try { 208 | instance.createCFFile('function1', resources); 209 | done('Expected an exception for a non-directory filename'); 210 | } catch (error) { 211 | done(); 212 | } 213 | }); 214 | }); 215 | 216 | describe('updateFunctionEnv', () => { 217 | it('Uses aws sdk to update a function\'s env settings', () => { 218 | const resources = _.extend({}, providerEnvironment, { CF_a: '1', CF_b: '2', CF_c: '3' }); 219 | const instance = new ServerlessFetchStackResources(_.extend({}, serverlessStub)); 220 | instance.lambda.updateFunctionConfiguration = (params, callback) => { 221 | expect(params).to.deep.equal({ 222 | FunctionName: 'UnitTestFunctionName', 223 | Environment: { 224 | Variables: resources, 225 | }, 226 | }); 227 | callback(null, true); 228 | }; 229 | return instance.updateFunctionEnv('UnitTestFunctionName', resources); 230 | }); 231 | }); 232 | 233 | describe('beforeLocalInvoke', () => { 234 | it('Should call dotenv based on stage', () => { 235 | const instance = new ServerlessFetchStackResources(_.extend({}, serverlessStub), { function: 'function1' }); 236 | 237 | sinon.stub(instance, 'getEnvFileName').returns('unit-test-filename'); 238 | sinon.stub(instance.dotenv, 'config').returns(true); 239 | instance.beforeLocalInvoke(); 240 | sinon.assert.calledWith(instance.dotenv.config, { path: './.serverless-resources-env/unit-test-filename' }); 241 | }); 242 | }); 243 | 244 | describe('afterDeploy', () => { 245 | it('Calls updateFunction for each function', () => { 246 | const instance = new ServerlessFetchStackResources(_.extend({}, _.cloneDeep(serverlessStub))); 247 | const resourceList = ['a', 'b', 'c']; 248 | instance.serverless.service.functions.function1.custom = { 'env-resources': resourceList }; 249 | instance.serverless.service.functions.function2.custom = { 'env-resources': resourceList }; 250 | instance.serverless.service.functions.function3 = {}; 251 | instance.serverless.service.functions.function4 = { custom: { 'env-resources': ['unknown'] } }; 252 | const resources = [ 253 | { LogicalResourceId: 'a', PhysicalResourceId: '1' }, 254 | { LogicalResourceId: 'b', PhysicalResourceId: '2' }, 255 | { LogicalResourceId: 'c', PhysicalResourceId: '3' }, 256 | ]; 257 | const mappedResources = { 258 | CF_a: '1', 259 | CF_b: '2', 260 | CF_c: '3', 261 | }; 262 | sinon.stub(instance, 'fetchCFResources').returns(Promise.resolve({ StackResources: resources })); 263 | sinon.stub(instance, 'updateFunctionEnv').returns(Promise.resolve(true)); 264 | sinon.stub(instance, 'createCFFile').returns(Promise.resolve(true)); 265 | return instance.afterDeploy().then(() => { 266 | sinon.assert.calledOnce(instance.fetchCFResources); 267 | sinon.assert.calledWith(instance.updateFunctionEnv, 'unit-test-service-dev-function1', _.extend({}, mappedResources, providerEnvironment)); 268 | sinon.assert.calledWith(instance.updateFunctionEnv, 'unit-test-service-dev-function2', _.extend({}, mappedResources, providerEnvironment)); 269 | sinon.assert.calledWith(instance.updateFunctionEnv, 'unit-test-service-dev-function3', _.extend({}, providerEnvironment)); 270 | sinon.assert.calledWith(instance.updateFunctionEnv, 'unit-test-service-dev-function4', _.extend({}, providerEnvironment)); 271 | return true; 272 | }); 273 | }); 274 | 275 | it('Includes env variables set for functions in the serverless.yml', () => { 276 | const instance = new ServerlessFetchStackResources(_.extend({}, _.cloneDeep(serverlessStub))); 277 | const resourceList = ['a', 'b', 'c']; 278 | instance.serverless.service.functions.function1.custom = { 'env-resources': resourceList }; 279 | instance.serverless.service.functions.function1.environment = { custom1: 'value1' }; 280 | 281 | const resources = [ 282 | { LogicalResourceId: 'a', PhysicalResourceId: '1' }, 283 | { LogicalResourceId: 'b', PhysicalResourceId: '2' }, 284 | { LogicalResourceId: 'c', PhysicalResourceId: '3' }, 285 | ]; 286 | const mappedResources = { 287 | CF_a: '1', 288 | CF_b: '2', 289 | CF_c: '3', 290 | }; 291 | sinon.stub(instance, 'fetchCFResources').returns(Promise.resolve({ StackResources: resources })); 292 | sinon.stub(instance, 'updateFunctionEnv').returns(Promise.resolve(true)); 293 | sinon.stub(instance, 'createCFFile').returns(Promise.resolve(true)); 294 | return instance.afterDeploy().then(() => { 295 | sinon.assert.calledOnce(instance.fetchCFResources); 296 | sinon.assert.calledWith( 297 | instance.updateFunctionEnv, 298 | 'unit-test-service-dev-function1', 299 | _.extend({}, mappedResources, providerEnvironment, { custom1: 'value1' })); 300 | return true; 301 | }); 302 | }); 303 | }); 304 | }); 305 | --------------------------------------------------------------------------------